在 LLM 应用中,让模型回答问题并不难,难的是让返回结果稳定进入后续程序。即使提示词要求输出 JSON,响应里仍可能出现 Markdown 包裹、额外说明或字段类型偏差。要解决这些问题,需要理解 LangChain 如何在调用前约束格式、在调用后解析数据,以及 withStructuredOutput 为什么更适合生产场景。
做过 LLM 应用开发的同学一定遇到过这样的场景:你让大模型返回 JSON,它偏偏给你套一层 ```json ``` 的 Markdown 代码块;你想拿到一个数组,它却返回了一段自然语言描述。如何让大模型的输出"听话",是所有 AI 应用从 Demo 走向生产的第一道坎。
LangChain 作为当前最主流的 LLM 应用框架,提供了一整套结构化输出解决方案。但很多同学只会用 JSON.parse() 硬解析,踩坑无数;还有人连 getFormatInstructions() 到底干了什么都说不清楚。
今天这篇文章,我带大家从源码设计和实战代码出发,彻底吃透 LangChain 的结构化输出体系。
把大模型想象成一位能力很强但很随性的翻译官。你告诉他"请用表格形式输出",他可能会:
LangChain 的结构化输出机制就是给这位翻译官配的"格式校对员" ——它负责两件事:
getFormatInstructions() 把格式要求"贴"到 Prompt 里,告诉模型该怎么输出parse() 把模型返回的内容(可能带 Markdown 包裹)解析成程序可用的对象| 层级 | 方案 | 约束强度 | 底层原理 | 适用场景 |
|---|---|---|---|---|
| 青铜 | 手动正则 + JSON.parse | 无 | 纯字符串处理 | 不推荐 |
| 白银 | JsonOutputParser | 弱 | Prompt 约定 + 解析 | 简单 JSON |
StructuredOutputParser.fromZodSchema | 中 | Zod 转 Prompt 指令 | 复杂字段 | |
| 王者 | withStructuredOutput | 最强 | 原生 Function Calling | 生产首选 |
getFormatInstructions() 到底干了什么?这是 90% 的初学者都会困惑的一个点,也是理解结构化输出的关键。 我们把它单独拿出来讲透。
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.
因为 LLM 默认会自由发挥输出格式。 这是最本质的原因。

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);
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% 的简单需求。
想象你去餐厅点餐:
getFormatInstructions 为空)→ 告诉服务员"随便给我来点吃的",厨师(LLM)自由发挥,可能给你端个炒饭,也可能端个拉面getFormatInstructions 返回详细 Schema)→ 告诉服务员"我要一份{主料:牛肉,配菜:土豆+胡萝卜,口味:微辣}",厨师必须按你的单子做格式指令就是把"随便来点"变成"精确订单"的关键一步。
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 格式包裹,这是它展示信息的天性。
第一版解决方案非常典型:
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(大写)、```(无语言标记)等变体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 的威力:
number vs string).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 通道,让模型"必须"按规范输出答案是:有,但场景在收窄。
withStructuredOutput标准决策流程:

getFormatInstructions() 何时返回空javascript
// ❌ 误区:以为所有 Parser 的 getFormatInstructions 都返回指令
const parser = new JsonOutputParser();
console.log(parser.getFormatInstructions()); // "" ? 空的!
// ✅ 想要非空输出,给它传 schema
const parser2 = new JsonOutputParser({ schema: scientistSchema });
// 或者直接用 StructuredOutputParser.fromZodSchema
排查口诀:看 Parser 类型,看是否传 schema。空字符串是设计行为,不是 bug。
getFormatInstructions() 拼到 Prompt 里javascript
// ❌ 白调了指令,没拼进去
const question = `请介绍一下居里夫人`;
const response = await model.invoke(question);
const result = await parser.parse(response.content); // ? 大概率失败
// ✅ 必须拼进去
const question = `请介绍一下居里夫人,${parser.getFormatInstructions()}`;
注意:getFormatInstructions() 是不自动生效的,它只是返回一段字符串,你得自己拼到 Prompt 里。这是新手最常见的失误。
javascript
// ❌ 只匹配小写 json
response.content.match(/```jsons*([sS]*?)s*```/)
// ✅ 使用解析器,内部已处理各种变体
const result = await parser.parse(response.content)
javascript
// ❌ 描述含糊,模型容易搞错
z.object({ birth_year: z.number().describe('年份') })
// ✅ 明确语义 + 附加约束 + 示例
z.object({
birth_year: z.number().int()
.describe('出生年份,纯数字整数,例如 1879'),
famous_theory: z.array(z.string())
.describe('著名理论名称数组,例如 ["相对论", "光电效应"]'),
})
withStructuredOutputjavascript
// ❌ 落后写法(模型支持 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)
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 // 降级
}
}
}
getFormatInstructions() 的作用是什么?为什么有的 Parser 返回空字符串?回答要点:
JsonOutputParser 无 schema 时返回 ""——设计者认为 JSON 是 LLM 的"常识格式",简单场景无需啰嗦,属于"约定优于配置"getFormatInstructions() 只返回字符串,不会自动生效,新手最容易在这里翻车getFormatInstructions() 负责"事前约定",parser.parse() 负责"事后清洗",两者缺一不可withStructuredOutput 和 OutputParser 有什么本质区别?为什么不无脑用它?回答要点:
底层机制不同:
OutputParser = Prompt 注入格式 + 手动正则清洗 + JSON.parse(软约束)withStructuredOutput = 模型原生 Function Calling(tools,硬约束)可靠性差异巨大:原生 function calling 走的是模型训练过的通道,不存在"忘记加 JSON 标记"的问题
不无脑用的原因:
JsonOutputParser 支持增量解析,withStructuredOutput 通常返回完整对象最佳实践:生产 + 支持 tools 的模型 → 首选 withStructuredOutput;其余场景按需选 Parser
fromZodSchema 相比 fromNamesAndDescriptions 的优势是什么?回答要点:
.int()、.min()、.max()、.regex() 等校验,能生成更严格的格式指令fromNamesAndDescriptions 只能描述扁平字段fromZodSchema、withStructuredOutput、bindTools,一份定义多场景使用一句话记忆:
getFormatInstructions() 是 "把 Zod 翻译成 Prompt 指令" ——事前约定parser.parse() 是 "把 Markdown 洗成对象" ——事后清洗JsonOutputParser 是"能不说话就不说话"的极简派StructuredOutputParser 是"字字珠玑"的描述派withStructuredOutput 是"直接走模型原生通道"的终极派能用 withStructuredOutput 就别用 Parser。
ubuntu怎么选择最快的更新源? ubuntu更改最快的更新源的图文教程
Claude 订阅停止覆盖第三方 Agent 后还能通过 OAuth 使用吗?
Ubuntu系统的笔记本触摸板怎么调节鼠标光标速度?
Claude Code 订阅套餐和 API 按量调用的成本如何管理?
新一代 AI 工具进阶指南:从基本使用到高效工作流
Pi Agent 应该如何通过 Agent SDK 正确连接 Claude 或 Codex?