agent之结构化输出
大模型输出结构化:Output Parser 与 withStructuredOutput,一文讲透
Leo
2026.08.27 · Updated 2026.09.13
大模型输出结构化:Output Parser 与 withStructuredOutput,一文讲透
示例代码全部在
output-parser/src/下,按文中顺序依次对应。
一、为什么需要结构化输出
开发 Agent / RAG / 信息抽取时,经常需要大模型返回固定结构的数据,而不是一段自然语言:
- 把人物信息提取成
{ name, birth_year, nationality, fields[] }存进数据库 - 把用户的指令解析成「工具名 + 参数」去调用函数
- 把结果渲染成表单、卡片、图表
但 LLM 天生是"文本生成器",它不知道什么是"字段、数组、类型",也不知道你要什么格式。于是有了结构化输出的诉求。
最朴素的方案(nomal.js):
const question = "请介绍一下爱因斯坦的信息。请以 JSON 格式返回,包含以下字段:name、birth_year、nationality、major_achievements(数组)、famous_theory。";
const response = await model.invoke(question);
const jsonResult = JSON.parse(response.content); // ❌ 可能直接崩
问题一箩筐:
- 模型可能在 JSON 前后加文字("好的,以下是……"),
JSON.parse直接抛错 - 字段名可能被改(
name写成Name、漏字段) - 模型可能不给你合法 JSON,给你一段散文
结论:光靠提示词 + 手动 JSON.parse,不靠谱。 需要框架来"管"这件事,这就是 Output Parser 和 withStructuredOutput 存在的意义。
二、方案一:Output Parser(提示词注入格式 + 事后解析)
2.1 原理:两步走
第一步:parser.getFormatInstructions() 把格式要求注入提示词
第二步:parser.parse() 解析模型返回的文本,得到结构化对象
Output Parser 做两件事:① 把格式"教"给模型(写进提示词),② 把模型返回的文本解析/校验成对象。
2.2 JsonOutputParser —— 最轻量的 JSON 解析
import { JsonOutputParser } from '@langchain/core/output_parsers';
const parser = new JsonOutputParser();
const question = `请介绍一下爱因斯坦的信息。请以 JSON 格式返回,包含以下字段:name、birth_year、nationality、major_achievements(数组)、famous_theory。
${parser.getFormatInstructions()}`; // ← 自动注入 JSON 格式要求
const response = await model.invoke(question);
const result = await parser.parse(response.content); // ← 自动解析
console.log(result.name); // "阿尔伯特·爱因斯坦"
getFormatInstructions()会向模型声明"只输出合法 JSON,不要额外文字"。parse()负责把文本转成对象;即使模型稍有偏差也能尽力兜住。- 适合:只要 JSON、字段简单、不需要类型校验的场景。
2.3 StructuredOutputParser —— 显式定义"字段 + 描述"
比 JsonOutputParser 更进一步:你声明字段和含义,框架把格式指令写得更明确。
方式一:fromNamesAndDescriptions(轻量字段表)
import { StructuredOutputParser } from '@langchain/core/output_parsers';
const parser = StructuredOutputParser.fromNamesAndDescriptions({
name: "姓名",
birth_year: "出生年份",
nationality: "国籍",
major_achievements: "主要成就,用逗号分隔的字符串",
famous_theory: "著名理论",
});
const question = `请介绍一下爱因斯坦的信息。\n\n${parser.getFormatInstructions()}`;
方式二:fromZodSchema(复杂嵌套结构)(structured-output-parser2.js)
当结构复杂起来——有数组、嵌套对象、可选字段——用 zod 定义,类型和校验一把梭:
import { z } from 'zod';
import { StructuredOutputParser } from '@langchain/core/output_parsers';
const scientistSchema = z.object({
name: z.string().describe("科学家的全名"),
birth_year: z.number().describe("出生年份"),
death_year: z.number().optional().describe("去世年份,如果还在世则不填"),
nationality: z.string().describe("国籍"),
fields: z.array(z.string()).describe("研究领域列表"),
awards: z.array(
z.object({
name: z.string().describe("奖项名称"),
year: z.number().describe("获奖年份"),
reason: z.string().optional().describe("获奖原因")
})
).describe("获得的重要奖项列表"),
famous_theories: z.array(
z.object({
name: z.string().describe("理论名称"),
year: z.number().optional().describe("提出年份"),
description: z.string().describe("理论简要描述")
})
).describe("著名理论列表"),
education: z.object({
university: z.string().describe("主要毕业院校"),
degree: z.string().describe("学位"),
graduation_year: z.number().optional().describe("毕业年份")
}).optional().describe("教育背景"),
biography: z.string().describe("简短传记,100字以内")
});
const parser = StructuredOutputParser.fromZodSchema(scientistSchema);
const question = `请介绍一下居里夫人的详细信息。\n\n${parser.getFormatInstructions()}`;
const response = await model.invoke(question);
const result = await parser.parse(response.content); // 输出还会被 zod 校验
.describe()是写给模型看的:每个字段的语义越清楚,模型填得越准。parse()后 zod 还会运行时校验:类型不对、缺必填字段会直接报ZodError,你可以在catch里拿到error.errors看具体哪里不合格。- 适合:字段多、有嵌套、需要类型校验的复杂场景。
2.4 XMLOutputParser —— 非 JSON 格式
有些系统需要 XML(老系统对接、特定协议),Output Parser 照样能处理:
import { XMLOutputParser } from '@langchain/core/output_parsers';
const parser = new XMLOutputParser();
const question = `请提取以下文本中的人物信息:阿尔伯特·爱因斯坦出生于 1879 年,是一位伟大的物理学家。
${parser.getFormatInstructions()}`;
const response = await model.invoke(question);
const result = await parser.parse(response.content);
关键点:withStructuredOutput 默认走 JSON 路线,非 JSON 格式只能靠 Output Parser。
三、方案二:withStructuredOutput(一行搞定,底层是 tool call)
3.1 先看底层原理:bindTools + tool_calls
Output Parser 靠"提示词约定",而更可靠的方式是让模型走 Tool Call——这是模型训练时就被保证的"原生结构输出能力":模型输出一段结构化的"调用指令",格式由模型自己保证,可靠性远超"靠提示词让它别乱写"。
手动写法(tool-call-args.js):
const modelWithTool = model.bindTools([
{
name: "extract_scientist_info",
description: "提取和结构化科学家的详细信息",
schema: scientistSchema // zod 转成的 JSON Schema
}
]);
const response = await modelWithTool.invoke("介绍一下爱因斯坦");
// 结构化结果就在 tool_calls 里,格式是模型训练时保证的
const result = response.tool_calls[0].args;
console.log(result.name); // "阿尔伯特·爱因斯坦"
3.2 withStructuredOutput:把这套封装成一行
const structuredModel = model.withStructuredOutput(scientistSchema);
const result = await structuredModel.invoke("介绍一下爱因斯坦");
console.log(result.name); // "阿尔伯特·爱因斯坦"
console.log(result.fields); // ["物理学", ...]
- 一行代码,不用自己 bindTools、不用自己从
tool_calls里取参数。 - 底层自动决策:框架会根据模型能力决定用 tool call 还是 output parser 实现。
- 可靠性更高:优先走 tool call,模型训练时就保证输出结构,不依赖"提示词约定"。
四、两者对比与选型
| 维度 | Output Parser | withStructuredOutput |
|---|---|---|
| 原理 | 提示词注入格式 + 事后解析 | 底层用 tool call(模型训练保证) |
| 可靠性 | 中(靠提示词约束) | 高(原生能力) |
| 代码量 | 手动建 parser、拼接提示词 | 一行 withStructuredOutput(schema) |
| 类型校验 | 部分支持(fromZodSchema) | 支持(zod schema) |
| 流式 | ✅ 支持 | ⚠️ 非真正流式(见下文) |
| 非 JSON 格式(XML 等) | ✅ 支持 | ❌ 默认 JSON |
| 推荐度 | 兜底 / 特定场景 | 首选 |
一句话选型:默认用 withStructuredOutput;只有需要流式或非 JSON 格式时才退回 Output Parser。
五、流式场景深挖:几个容易踩的坑
流式输出(model.stream())能让用户看到内容逐字生成,体验好,但结构化输出遇上流式就有讲究了。
5.1 普通流式(无结构化)—— baseline
const stream = await model.stream(prompt);
let fullContent = '';
for await (const chunk of stream) {
fullContent += chunk.content;
process.stdout.write(chunk.content); // 实时打印
}
只是逐块拼接文本,没有结构。流式是"过程",结构化是"结果",两者要结合着看。
5.2 坑一:withStructuredOutput + stream —— 不是真正的流式
const structuredModel = model.withStructuredOutput(schema);
const stream = await structuredModel.stream(prompt);
for await (const chunk of stream) {
// 你拿到的 chunk 不是"增量",而是"最终完整结果"
console.log(chunk);
}
文件末尾注释原话:该案例演示了如何使用 withStructuredOutput 流式接收结构化输出,但是不是真正的流式,会在最后才返回完整的结构化结果。
也就是说:用 withStructuredOutput 走 stream,体验上"卡到最后才出结果",失去了流式的意义。要看真正的"逐字流式",得回到 Output Parser。
5.3 Output Parser + stream —— 先流式打印,最后再 parse
const parser = StructuredOutputParser.fromZodSchema(schema);
const prompt = `详细介绍莫扎特的信息。\n\n${parser.getFormatInstructions()}`;
const stream = await model.stream(prompt);
let fullContent = '';
for await (const chunk of stream) {
fullContent += chunk.content;
process.stdout.write(chunk.content); // ✅ 实时逐字打印
}
const result = await parser.parse(fullContent); // 流结束后整体解析成结构
console.log(result.name); // "沃尔夫冈·阿马德乌斯·莫扎特"
这才是"既要流式、又要结构化"的正解:
- 过程中:文本实时打印,用户看得见;
- 结束后:把完整文本丢给 parser,得到结构化对象。
文件末尾注释原话:流式的情况下,用 output parser 还是更适合的。
5.4 进阶:JsonOutputToolsParser —— 流式中实时拿到 tool_calls 的 JSON
如果流式过程中就想拿到结构化的 tool 参数来实时调用工具(而不是等结束再 parse),用 JsonOutputToolsParser(stream-tool-calls-parser.js):
import { JsonOutputToolsParser } from '@langchain/core/output_parsers/openai_tools';
const modelWithTool = model.bindTools([
{ name: "extract_scientist_info", description: "提取科学家的详细信息", schema: scientistSchema }
]);
// 绑定工具 + 挂载解析器,组成链
const parser = new JsonOutputToolsParser();
const chain = modelWithTool.pipe(parser);
const stream = await chain.stream("详细介绍牛顿的生平和成就");
for await (const chunk of stream) {
if (chunk.length > 0) {
const toolCall = chunk[0];
console.log(toolCall.args); // 实时拿到结构化的 tool 参数 JSON
// 可以在这里直接调用对应工具
}
}
chain = modelWithTool.pipe(parser):工具调用链,流式输出经 parser 转成tool_calls对象。- 每个 chunk 里的
toolCall.args已经是 JSON 对象,可以边生成边调用工具,这就是"流式 Tool Calling Agent"的关键技术。
六、总结:一张图记住怎么选
三条硬结论:
- 普通结构化输出 → 用
withStructuredOutput,一行搞定、可靠性最高。 - 需要流式 → 用
Output Parser + model.stream(),先逐字打印、流结束后 parse 成结构。 - 需要流式期间就实时拿 JSON 调工具 → 用
JsonOutputToolsParser。
Output Parser 和 withStructuredOutput 的关系,不是"谁取代谁",而是"各管一段": withStructuredOutput 负责"默认可靠";Output Parser 负责"流式 + 非 JSON"这类 withStructuredOutput 覆盖不到的角落。真流式,是 output parser 的主场。
Leo
BloggerIndependent developer / Blogger and the maintainer of the original blog “大道至简”. Migrating years of posts and shiyu from WordPress to Next.js.
Reader comments
COMMENTS · 0Leave a comment