前言
在做 LLM 应用开发时,让大模型稳定返回 JSON 结构化数据 几乎是必做需求。 最早我是用假 ToolCall(工具调用 hack) ,手动写res.tool_calls[0].args去抠结果; 后面学会了JsonOutputParser输出解析器,靠 Prompt 哄模型输出 JSON; 现在 LangChain 推荐使用withStructuredOutput,一行代码绑定 Schema,直接返回 JS 对象。
很多新手会混淆三者:什么时候用 ToolCall?OutputParser 和 withStructuredOutput 到底差在哪?底层原理、适用场景、坑点,本文一次性讲清楚。
本文代码基于 LangChain JS / TS。
一、第一代:传统 ToolCall 「讨巧方案」
原理
本质是欺骗模型调用一个不存在的工具 ,把我们想要的结构化 JSON 塞到tool_call.args参数里面。 这是早期没有原生结构化输出能力时,社区的 hack 做法。
go
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o" });
// 定义一个假工具,只是用来承载返回的结构化数据
const tools = [
{
type: "function" as const,
function: {
name: "get_scientist_info",
description: "获取科学家基础信息",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "科学家名字" },
field: { type: "string", description: "研究领域" },
birthYear: { type: "number", description: "出生年份" }
},
required: ["name", "field", "birthYear"]
}
}
}
];
// 绑定工具调用
const res = await model.bindTools(tools).invoke("介绍爱因斯坦");
// 重点!必须手动取下标0,再JSON.parse解析字符串
const toolCall = res.tool_calls[0];
const scientistInfo = JSON.parse(toolCall.args);
console.log(scientistInfo);
// { name:"爱因斯坦", field:"理论物理", birthYear:1879 }
代码解析
bindTools(tools):给模型绑定工具定义,开启 function calling- 返回结果
res是AIMessage,里面带有tool_calls数组 - 数组第一个元素下标是
[0],取出 toolCall toolCall.args是JSON 字符串 ,必须手动JSON.parse转 JS 对象
缺点
- 必须手动处理数组下标
[0],写大量解析胶水代码 - 模型偶尔输出异常 JSON,要写 try/catch 容错
- 语义很别扭:不是真的调用工具,只是借工具调用的壳返回数据
- 适合场景:真的要调用外部函数(Agent 执行工具动作,查询数据库、接口)
一句话总结:ToolCall 是用来干活调用外部函数的,单纯拿结构化 JSON 属于 "借壳使用" 。
二、第二代:OutputParser 输出解析器(JsonOutputParser)
原理
不使用工具调用,完全靠 Prompt 文本约束模型输出 JSON,模型输出纯文本字符串,再在后端代码做解析。 Parser 是一个可插拔的 Runnable 组件,放在链的末尾,负责清洗、解析大模型返回的文本。
javascript
import { ChatOpenAI } from "@langchain/openai";
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
const model = new ChatOpenAI({ model: "gpt-4o" });
const parser = new JsonOutputParser();
// 提示词注入格式要求
const prompt = PromptTemplate.fromTemplate(`
你需要提取科学家信息,严格输出JSON。
{format_instructions}
问题:{question}
`).partial({ format_instructions: parser.getFormatInstructions() });
// 链式:prompt -> model -> parser
const chain = prompt.pipe(model).pipe(parser);
const scientistInfo = await chain.invoke({ question: "介绍爱因斯坦" });
console.log(scientistInfo);
代码解析
parser.getFormatInstructions()自动生成一大段文本,塞进 Prompt,告诉模型输出 JSON 格式- LLM 返回纯文本字符串
- 管道末尾
pipe(parser)自动执行 JSON.parse,返回对象
缺点
- 约束弱:模型不听话,会附带解释文字、markdown 代码块、残缺 JSON,直接抛出解析异常
- 依赖 Prompt 工程,不同模型效果差异巨大
适用场景
- 模型不支持 ToolCall / 原生结构化输出(很多本地开源大模型)
- 需要自定义文本清洗逻辑,灵活处理脏输出
- 老项目兼容
一句话总结:OutputParser:大模型随便输出文字,后端代码努力把文字抠成 JSON。
三、第三代:withStructuredOutput 原生结构化输出(推荐)
原理
Schema 直接传递给模型底层 API,不是写在 Prompt 里面,由大模型原生能力强制保证输出符合 Schema 。 底层内部可以自动选择function_calling或者json_schema两种策略,LangChain 把取下标、JSON.parse 全部封装隐藏。
go
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o" });
// 定义Schema,描述期望返回的数据结构
const scientistSchema = {
type: "object",
properties: {
name: { type: "string", description: "科学家名字" },
field: { type: "string", description: "研究领域" },
birthYear: { type: "number", description: "出生年份" }
},
required: ["name", "field", "birthYear"]
};
// 绑定Schema,生成包装后的模型
const structuredModel = model.withStructuredOutput(scientistSchema);
// 直接调用,返回已经解析完成的JS对象!
const scientistInfo = await structuredModel.invoke("介绍爱因斯坦");
console.log(scientistInfo);
代码解析
model.withStructuredOutput(schema):包装模型实例,绑定数据契约- 调用 invoke,直接拿到结构化对象,不需要 tool_calls 0,不需要 JSON.parse
- LangChain 内部自动选择底层策略(ToolStrategy / Provider 原生 JSON Schema)
优点
✅ 稳定性极强,模型 API 层面强制约束,极少出现 JSON 格式崩坏 ✅ 不需要修改 Prompt,不用注入一大段格式说明文字 ✅ 代码极简,上层看不到 tool_calls 解析逻辑
适用场景
只想要 AI 返回固定结构 JSON,不需要调用外部工具。 信息提取、数据结构化、实体抽取,优先选这个。
一句话总结:withStructuredOutput:提前告诉模型接口输出规范,模型原生保证输出合规 JSON。
四、三者横向对比表
表格
| 方案 | 底层原理 | 是否依赖 tool_call | 解析位置 | 稳定性 | 最佳场景 |
|---|---|---|---|---|---|
| 传统 ToolCall | 假工具调用 hack | ✅是 | 业务代码手动解析tool_calls[0] |
中等 | Agent,真的调用外部函数 |
| JsonOutputParser | Prompt 文本约束 + 后端文本解析 | ❌否 | 输出解析器,模型输出文本后解析 | 低,容易脏输出 | 开源模型、需要自定义清洗逻辑 |
| withStructuredOutput | 模型原生结构化输出,内部封装 toolcall | 底层可选 | LangChain 内部自动解析 | ⭐⭐⭐⭐⭐高 | 提取数据、结构化返回,无外部函数调用 |
五、怎么选型?3 条判断规则
- ✅ 需要调用外部工具(查询接口、数据库) → 用 ToolCall /bindTools
- ✅ 只是提取信息,输出固定 JSON,模型支持原生结构化 → 优先
withStructuredOutput - ✅ 本地开源模型,不支持原生结构化输出 → 选用 OutputParser
六、常见误区澄清
误区 1:withStructuredOutput 完全脱离 tool call?
不是。底层策略function_calling模式下,内部依然走 tool call。但是 LangChain 把tool_calls[0]、JSON.parse 全部封装,上层业务代码看不见。 底层在用 tool call,但是上层不用写 tool call 解析代码。
误区 2:OutputParser 被淘汰了?
没有。当模型不支持结构化输出时,OutputParser 依然是唯一方案,灵活性最高,可以自定义正则清洗、修复脏文本。
结尾
做 AI Agent 开发,分清这三者的定位,能避开大量 "模型输出 JSON 格式错乱" 的坑。 简单一句话记忆:
- 要干活调用外部工具:ToolCall
- 哄模型按文字要求输出 JSON:OutputParser
- 单纯拿结构化数据,追求稳定:withStructuredOutput