✦ 大道至简 · 时光是画在卷上的河流 · 行到水穷处,坐看云起时

agent2026.08.27 · 23 分钟阅读

agent之结构化输出

大模型输出结构化:Output Parser 与 withStructuredOutput,一文讲透

L

Leo

2026.08.27 · 更新于 2026.09.13

3 次浏览
agent之结构化输出

大模型输出结构化: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 ParserwithStructuredOutput
原理提示词注入格式 + 事后解析底层用 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"的关键技术。

六、总结:一张图记住怎么选

三条硬结论:

  1. 普通结构化输出 → 用 withStructuredOutput,一行搞定、可靠性最高。
  2. 需要流式 → 用 Output Parser + model.stream(),先逐字打印、流结束后 parse 成结构。
  3. 需要流式期间就实时拿 JSON 调工具 → 用 JsonOutputToolsParser。

Output Parser 和 withStructuredOutput 的关系,不是"谁取代谁",而是"各管一段": withStructuredOutput 负责"默认可靠";Output Parser 负责"流式 + 非 JSON"这类 withStructuredOutput 覆盖不到的角落。真流式,是 output parser 的主场。


标签 / TAGSagent
L

Leo

博主

独立开发者 / Blogger,原博客「大道至简」维护者。正在把 WordPress 上攒了几年的文章与拾语迁移到 Next.js。

读者留言

COMMENTS · 0

发表留言

评论经审核后展示 · 请友善发言0/100