最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
LLM 结构化输出实战:用 LangChain 从 JSON 解析走向 withStructuredOutput
时间:2026-09-13 16:24:01 编辑:袖梨 来源:一聚教程网
在 LLM 应用中,让模型回答问题并不难,难的是让返回结果稳定进入后续程序。即使提示词要求输出 JSON,响应里仍可能出现 Markdown 包裹、额外说明或字段类型偏差。要解决这些问题,需要理解 LangChain 如何在调用前约束格式、在调用后解析数据,以及 withStructuredOutput 为什么更适合生产场景。
导语
做过 LLM 应用开发的同学一定遇到过这样的场景:你让大模型返回 JSON,它偏偏给你套一层 ```json ``` 的 Markdown 代码块;你想拿到一个数组,它却返回了一段自然语言描述。如何让大模型的输出"听话",是所有 AI 应用从 Demo 走向生产的第一道坎。
LangChain 作为当前最主流的 LLM 应用框架,提供了一整套结构化输出解决方案。但很多同学只会用 JSON.parse() 硬解析,踩坑无数;还有人连 getFormatInstructions() 到底干了什么都说不清楚。
今天这篇文章,我带大家从源码设计和实战代码出发,彻底吃透 LangChain 的结构化输出体系。
一、核心概念:结构化输出到底在解决什么问题?
1.1 一个生活化的比喻
把大模型想象成一位能力很强但很随性的翻译官。你告诉他"请用表格形式输出",他可能会:
- 用 Markdown 表格
- 用纯文本对齐
- 用 JSON
- 甚至用一段散文描述
LangChain 的结构化输出机制就是给这位翻译官配的"格式校对员" ——它负责两件事:
- 事前约定:通过
getFormatInstructions()把格式要求"贴"到 Prompt 里,告诉模型该怎么输出 - 事后清洗:通过
parse()把模型返回的内容(可能带 Markdown 包裹)解析成程序可用的对象
1.2 四种姿势,四个段位
| 层级 | 方案 | 约束强度 | 底层原理 | 适用场景 |
|---|---|---|---|---|
| 青铜 | 手动正则 + JSON.parse | 无 | 纯字符串处理 | 不推荐 |
| 白银 | JsonOutputParser | 弱 | Prompt 约定 + 解析 | 简单 JSON |
StructuredOutputParser.fromZodSchema | 中 | Zod 转 Prompt 指令 | 复杂字段 | |
| 王者 | withStructuredOutput | 最强 | 原生 Function Calling | 生产首选 |
二、核心疑点破解:getFormatInstructions() 到底干了什么?
这是 90% 的初学者都会困惑的一个点,也是理解结构化输出的关键。 我们把它单独拿出来讲透。
2.1 一句话说清楚它的职责
getFormatInstructions()的作用是:将 Zod schema 转换为 LLM 能理解的输出格式指令,告诉大模型必须按照指定的 JSON 结构来输出。
具体来说,当你写下:
javascriptJavaScript
const parser = StructuredOutputParser.fromZodSchema(scientistSchema);
const question = `请介绍一下居里夫人的详细信息,${parser.getFormatInstructions()}`;
console.log(question); // ? 打印出来你会发现多了一大段"格式化指令"
getFormatInstructions() 会根据你定义的 scientistSchema 自动生成一段提示词指令,类似这样:
text文本
You must format your output as a JSON value that adheres to a given "JSON Schema" instance.
"JSON Schema" is a declarative language that allows you to annotate and validate JSON documents.
For example, the example "JSON Schema" instance
{"properties": {"foo": {"description": "a list of test words", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
would match an object with one required property, "foo".
The "type" property specifies the type...
The object MUST have the following properties: "name", "birth_year", "nationality", "fields", "awards", "major_achievement", "famous_theory", "biography".
... (包含所有字段的 JSON Schema 定义) ...
Please output the extracted information in JSON format according to this schema.
2.2 为什么必须拼在提示词里?
因为 LLM 默认会自由发挥输出格式。 这是最本质的原因。

- 不加这段指令:模型可能返回一段纯文本的居里夫人介绍,字段全凭它心情
- 加上这段指令:模型会严格按你定义的 schema(
name、birth_year、nationality、fields、awards等)返回结构化 JSON
这样后面的 parser.parse(response.content) 才能成功解析为 scientistSchema 对应的对象。两段代码是配合使用的:
javascript
// 事前:注入格式指令
const question = `请介绍一下居里夫人的详细信息,${parser.getFormatInstructions()}`;
// 事中:调用 LLM(此时模型已经被格式指令"洗脑")
const response = await model.invoke(question);
// 事后:解析(因为前面注入了指令,这里才大概率能解析成功)
const result = await parser.parse(response.content);
2.3 关键澄清:为什么 JsonOutputParser 打印为空?
注意:getFormatInstructions() 的具体行为取决于解析器的实现,这也是最容易混淆的地方。
| 解析器 | 是否传 schema | getFormatInstructions() 返回 |
|---|---|---|
JsonOutputParser | ❌ 无 schema | 空字符串 "" |
JsonOutputParser | ✅ 带 schema | 详细的 JSON Schema 指令 |
StructuredOutputParser.fromNamesAndDescriptions | — | 字段级描述指令 |
StructuredOutputParser.fromZodSchema | — | 完整 JSON Schema 指令(较长) |
所以:
javascript
// 场景 A:打印为空
const parser = new JsonOutputParser();
console.log(parser.getFormatInstructions()); // "" ? 空!
// 场景 B:打印一大堆指令
const parser2 = new JsonOutputParser({ schema: scientistSchema });
console.log(parser2.getFormatInstructions()); // "Please output a JSON object..."
// 场景 C:也是详细指令
const parser3 = StructuredOutputParser.fromZodSchema(scientistSchema);
console.log(parser3.getFormatInstructions()); // "You must format your output as..."
为什么 JsonOutputParser 无 schema 时返回空? 设计者认为 JSON 是 LLM 训练语料的"常识格式" ,简单场景直接说"返回 JSON"模型就能理解,无需啰嗦。这属于 "约定优于配置" 的经典设计——零成本覆盖 80% 的简单需求。
2.4 用生活比喻理解这个机制
想象你去餐厅点餐:
- 不写格式指令(
getFormatInstructions为空)→ 告诉服务员"随便给我来点吃的",厨师(LLM)自由发挥,可能给你端个炒饭,也可能端个拉面 - 写了格式指令(
getFormatInstructions返回详细 Schema)→ 告诉服务员"我要一份{主料:牛肉,配菜:土豆+胡萝卜,口味:微辣}",厨师必须按你的单子做
格式指令就是把"随便来点"变成"精确订单"的关键一步。
三、痛点与场景:为什么我们需要它?
3.1 一个真实的"翻车"现场
javascript
// ❌ 错误示范:直接 JSON.parse 模型输出
const response = await model.invoke("请返回爱因斯坦信息的JSON")
const jsonResult = JSON.parse(response.content) // ? 大概率报错
为什么会失败? 因为 LLM 的输出往往长这样:
text
好的,这是爱因斯坦的信息:
```json
{
"name": "阿尔伯特·爱因斯坦",
"birth_year": 1879
}
```
希望对你有帮助!
直接 JSON.parse 必然抛出 Unexpected token 错误。这就是核心痛点:LLM 输出常被 Markdown 格式包裹,这是它展示信息的天性。
3.2 手动处理的"原始方案"
第一版解决方案非常典型:
javascript
// 使用正则提取 markdown ```json ... ``` 中的 JSON 内容
const match = response.content.match(/```jsons*([sS]*?)s*```/)
const jsonStr = match ? match[1] : response.content
const jsonResult = JSON.parse(jsonStr)
这段代码能跑,但有三个问题:
- 正则脆弱:模型可能用
```JSON(大写)、```(无语言标记)等变体 - 重复造轮子:每个 AI 调用点都要写一遍
- 无语义约束:无法校验字段类型、必填项
LangChain 的封装就是把这段"业务脏活"标准化了。
四、重难点剖析(核心)
重难点一:StructuredOutputParser 的字段级约束升级
设计者为什么这么写?
StructuredOutputParser.fromNamesAndDescriptions 通过字段描述生成格式指令:
javascript
const parser = StructuredOutputParser.fromNamesAndDescriptions({
name: '姓名',
birth_year: '出生年份',
nationality: '国籍',
major_achievement: '主要成就,数组',
famous_theory: '著名的理论',
})
但它有致命弱点——只描述字段名和语义,不约束类型。升级到 Zod:
javascript
const scientistSchema = z.object({
name: z.string().describe('科学家的姓名'),
birth_year: z.number().int().describe('出生年份,纯数字整数'),
death_year: z.number().optional().describe('死亡年份,在世则缺省'),
fields: z.array(z.string()).describe('科学家的领域,字符串数组'),
awards: z.array(
z.object({
name: z.string(),
year: z.number(),
reason: z.string(),
})
).describe('科学家获得的奖励,对象数组'),
});
const parser = StructuredOutputParser.fromZodSchema(scientistSchema);
Zod Schema 的威力:
- 类型强制(
numbervsstring) - 可选字段(
.optional()) - 嵌套对象和数组
- 正则、枚举、范围校验
fromZodSchema 会把整个 Zod 结构转成一段 JSON Schema 描述,作为 getFormatInstructions() 的返回值,注入到 Prompt 里。
重难点二(重点):withStructuredOutput 才是终极答案
从 bindTools 到 withStructuredOutput 的演进
先看一个"偏门但可靠"的写法——手动 bindTools:
javascript
const modelWithToolCall = model.bindTools([
{
name: 'extract_scientist_info',
description: '提取和结构化科学家的详细信息',
schema: scientistSchema,
}
])
const response = await modelWithToolCall.invoke('介绍一下爱因斯坦')
console.log(response.tool_calls[0].args) // 需要手动取 tool_calls[0].args
核心洞察:这走的是模型原生 Function Calling 通道,比 Prompt 注入格式指令再手动解析可靠得多。
但每次都手动 bindTools + 手动取 tool_calls[0].args 太啰嗦了。LangChain 为此提供了一步到位的封装:
javascript
// ✨ 终极推荐写法
const structuredModel = model.withStructuredOutput(scientistSchema, {
name: 'extract_scientist_info',
});
const result = await structuredModel.invoke('介绍一下爱因斯坦');
console.log(result.name); // "阿尔伯特·爱因斯坦"
console.log(result.birth_year); // 1879
withStructuredOutput 内部做的事:

优势总结:
| 维度 | 手动 bindTools | withStructuredOutput |
|---|---|---|
| 代 | 5~10 行 | 1~2 行 |
| 返回内容 | tool_calls[0].args | 直接是对象 |
| 类型安全 | 手动断言 | 自动推导 |
| 兼容性处理 | 自己写 | 内部自动降级到 JSON 模式 |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
三种方案的关键差异对比

核心差异:
StructuredOutputParser是 "软约束" ——通过 Prompt 里注入格式指令,让模型"尽量"听话withStructuredOutput是 "硬约束" ——通过模型原生 Function Calling 通道,让模型"必须"按规范输出
关键问题:OutputParser 还有存在必要吗?
答案是:有,但场景在收窄。
- ✅ 需要 OutputParser:模型不支持 Function Calling(如某些开源小模型)、需要流式增量解析、简单 JSON 提取、教学演示
- ❌ 不需要 OutputParser:生产环境 + 模型支持 tools → 无脑上
withStructuredOutput
标准决策流程:

五、避坑指南/最佳实践
坑 1:搞不清 getFormatInstructions() 何时返回空
javascript
// ❌ 误区:以为所有 Parser 的 getFormatInstructions 都返回指令
const parser = new JsonOutputParser();
console.log(parser.getFormatInstructions()); // "" ? 空的!
// ✅ 想要非空输出,给它传 schema
const parser2 = new JsonOutputParser({ schema: scientistSchema });
// 或者直接用 StructuredOutputParser.fromZodSchema
排查口诀:看 Parser 类型,看是否传 schema。空字符串是设计行为,不是 bug。
坑 2:忘记把 getFormatInstructions() 拼到 Prompt 里
javascript
// ❌ 白调了指令,没拼进去
const question = `请介绍一下居里夫人`;
const response = await model.invoke(question);
const result = await parser.parse(response.content); // ? 大概率失败
// ✅ 必须拼进去
const question = `请介绍一下居里夫人,${parser.getFormatInstructions()}`;
注意:getFormatInstructions() 是不自动生效的,它只是返回一段字符串,你得自己拼到 Prompt 里。这是新手最常见的失误。
坑 3:正则匹配不严谨
javascript
// ❌ 只匹配小写 json
response.content.match(/```jsons*([sS]*?)s*```/)
// ✅ 使用解析器,内部已处理各种变体
const result = await parser.parse(response.content)
坑 4:字段描述写得太敷衍
javascript
// ❌ 描述含糊,模型容易搞错
z.object({ birth_year: z.number().describe('年份') })
// ✅ 明确语义 + 附加约束 + 示例
z.object({
birth_year: z.number().int()
.describe('出生年份,纯数字整数,例如 1879'),
famous_theory: z.array(z.string())
.describe('著名理论名称数组,例如 ["相对论", "光电效应"]'),
})
坑 5:无脑堆 OutputParser,忽视 withStructuredOutput
javascript
// ❌ 落后写法(模型支持 tools 的情况下)
const parser = StructuredOutputParser.fromZodSchema(schema)
const response = await model.invoke(`${question}n${parser.getFormatInstructions()}`)
const result = await parser.parse(response.content)
// ✅ 现代写法
const result = await model
.withStructuredOutput(schema, { name: 'extract' })
.invoke(question)
坑 6:不处理异常
javascript
async function safeInvoke(model, schema, prompt, maxRetry = 3) {
for (let i = 0; i < maxRetry; i++) {
try {
const structuredModel = model.withStructuredOutput(schema)
return await structuredModel.invoke(prompt)
} catch (error) {
console.warn(`第 ${i + 1} 次解析失败:`, error.message)
if (i === maxRetry - 1) return null // 降级
}
}
}
六、面试高频考点
考点 1:getFormatInstructions() 的作用是什么?为什么有的 Parser 返回空字符串?
回答要点:
- 作用:把 Zod schema(或字段描述)转换成 LLM 能理解的 JSON Schema 指令,注入 Prompt 中,让模型输出从"自由文本"变成"结构化 JSON"
- 返回空的场景:
JsonOutputParser无 schema 时返回""——设计者认为 JSON 是 LLM 的"常识格式",简单场景无需啰嗦,属于"约定优于配置" - 必须手动拼到 Prompt 里:
getFormatInstructions()只返回字符串,不会自动生效,新手最容易在这里翻车 - 配合关系:
getFormatInstructions()负责"事前约定",parser.parse()负责"事后清洗",两者缺一不可
考点 2:withStructuredOutput 和 OutputParser 有什么本质区别?为什么不无脑用它?
回答要点:
-
底层机制不同:
OutputParser= Prompt 注入格式 + 手动正则清洗 +JSON.parse(软约束)withStructuredOutput= 模型原生 Function Calling(tools,硬约束)
-
可靠性差异巨大:原生 function calling 走的是模型训练过的通道,不存在"忘记加 JSON 标记"的问题
-
不无脑用的原因:
- 模型兼容性:部分开源小模型不支持 tools
- Token 开销:tools 定义会消耗额外 token
- 流式场景:
JsonOutputParser支持增量解析,withStructuredOutput通常返回完整对象
-
最佳实践:生产 + 支持 tools 的模型 → 首选
withStructuredOutput;其余场景按需选 Parser
考点 3:fromZodSchema 相比 fromNamesAndDescriptions 的优势是什么?
回答要点:
- 类型约束:Zod 支持
.int()、.min()、.max()、.regex()等校验,能生成更严格的格式指令 - 嵌套支持:支持对象数组、可选字段等复杂结构,
fromNamesAndDescriptions只能描述扁平字段 - 解析后校验:即使模型输出格式对了,还能用 Zod 二次校验类型,防止"格式对但语义错"
- 可复用:Zod Schema 可以同时用于
fromZodSchema、withStructuredOutput、bindTools,一份定义多场景使用
七、总结
一句话记忆:
getFormatInstructions()是 "把 Zod 翻译成 Prompt 指令" ——事前约定parser.parse()是 "把 Markdown 洗成对象" ——事后清洗JsonOutputParser是"能不说话就不说话"的极简派StructuredOutputParser是"字字珠玑"的描述派withStructuredOutput是"直接走模型原生通道"的终极派
能用 withStructuredOutput 就别用 Parser。