LangChain 输出解析器:从裸文本到结构化输出

一、先理解问题:为什么需要"输出解析器"?

大模型(LLM)的本质是"根据输入文字预测下一段文字"。所以你问它任何问题,它给你的永远是一段字符串。比如你让它"介绍一下爱因斯坦",它可能回:

复制代码
好的,爱因斯坦是著名的物理学家......(一大段文字)

如果这只是给人看的,没问题。但如果是程序要接着处理 呢?比如你想把"爱因斯坦的信息"存进数据库、渲染成卡片、或者传给下一个接口,程序就需要 JSON 对象,而不是一段人话。

这时候矛盾就来了:

  • 模型输出的是"文本"(往往是字符串)。
  • 程序需要的是"结构化数据"(对象、数组、字段明确的 JSON)。

怎么把模型吐出来的文本,安全、可靠地变成程序能用的结构?这就是 输出解析器 的职责。整个 src 目录,其实就是一条"越来越聪明"的解决方案演进路线。下面我们按演进顺序逐个拆解。


二、最原始的方式:让模型输出 JSON,再手动 parse(normal.mjs)

先看 normal.mjs。它的思路非常简单粗暴:在提示词里明确告诉模型"请以 JSON 格式返回",然后代码里用 JSON.parse 解析。

js 复制代码
const prompt = `
请介绍一下爱因斯坦的信息。 请以 JSON 格式返回,
包含以下字段: name(姓名)、birth_year(出生年份)、
nationality(国籍)、major_achievement(主要成就, 数组)、
famous_theory(著名理论)
`;
const response = await model.invoke(prompt);
const jsonResult = JSON.parse(response.content);

这段代码能跑,但有两个大坑

坑一:模型经常"不听话",在 JSON 外面包一层 markdown

模型很"爱美",常常会好心地把 JSON 包进代码块里:

json 复制代码
```json
{ "name": "爱因斯坦", ... }
ruby 复制代码
这时候你直接 `JSON.parse` 就会报错,因为字符串前后多了 ```json 和 ```。

### 坑二:模型偶尔会顺着话头说废话

即使你让它"只返回 JSON",它可能还是加上一句"好的,以下是有关爱因斯坦的信息:"。多出来的文字会让 `JSON.parse` 直接崩掉。

---

## 三、第一招手动化解:用正则剥掉 markdown 外壳

这个坑太常见了,所以上一轮我们就在 `normal.mjs` 里加了正则,先把代码块剥掉再解析:

```js
const jsonMatch = response.content.match(/```json\s*([\s\S]*?)\s*```/);
const jsonStr = jsonMatch ? jsonMatch[1] : response.content; // 剥掉外壳,取内部 JSON
const jsonResult = JSON.parse(jsonStr);

正则背后的逻辑:

  • /```json\s*([\s\S]*?)\s*```/:寻找 json 到 之间的内容。
  • \s* 用来吃掉开头和结尾的多余空白。
  • [\s\S]*?非贪婪 匹配,意思"尽量少地匹配任意字符(包括换行)",为的是精确框住第一对 json 之间的部分。
  • [1] 取出括号里捕获的那段------也就是真正的 JSON 字符串。

这个办法能解决 90% 的问题,但它太"手动"了 :正则本身写得复杂,而且如果模型输出根本不含代码块,还要做兜底(jsonMatch ? ... : response.content)。于是 LangChain 提供了专门的工具。这正是它存在的原因:把"从模型输出里可靠地拿到 JSON"这件事封装成标准组件。


四、LangChain 的 JsonOutputParser(normal.mjs 升级版)

normal.mjs 里其实已经用上了 LangChain 给的解析器 JsonOutputParser

js 复制代码
import { JsonOutputParser } from "@langchain/core/output_parsers";
const parser = new JsonOutputParser();
const prompt = `
请介绍一下爱因斯坦的信息。 请以 JSON 格式返回,
包含以下字段: ...
${parser.getFormatInstructions()}
`;
const jsonResult = await parser.parse(response.content);
console.log(jsonResult, jsonResult.name);

这里有三个关键点要记住:

  1. getFormatInstructions() :解析器会自动"教"模型该输出什么格式,并把这套说明追加到提示词末尾。这样模型更听话,输出的 JSON 更规范。
  2. parse(content):把模型返回的文本"解析"成真正的 JS 对象,这一步内部就处理了各种脏数据(比如前面说的 markdown 外壳)。
  3. jsonResult.name:解析成功后,就可以像用普通对象一样访问字段了。

JsonOutputParser 是"基础版":它只知道"你要 JSON 对象",但不知道 JSON 长什么样------字段叫什么、是数字还是字符串、哪些必填哪些可选,它一概不管。所以它适合"宽松一点"的场景。


五、进阶:StructuredOutputParser------用"字段清单"约束格式

structured-output-parser.mjs 用到了更严格的 StructuredOutputParser

js 复制代码
const parser = StructuredOutputParser.fromNamesAndDescriptions({
  name: "姓名",
  birth_year: "出生年份",
  nationality: "国籍",
  major_achievements: "主要成就, 用逗号分隔的字符串",
  famous_theory: "著名理论",
});
const question = `
请介绍一下爱因斯坦的信息。
${parser.getFormatInstructions()}
`;
const result = await parser.parse(response.content);
console.log(`姓名: ${result.name}`);

fromNamesAndDescriptions 的意思是:我告诉你"有哪些字段、每个字段是什么意思",你(解析器)帮我把它翻译成给模型的格式说明,并保证输出符合这份清单。

相比 JsonOutputParser,它更进一步------明确了字段名和含义 ,但还不够强:它只是"命名并描述",不能约束字段的类型 (比如 birth_year 是数字还是字符串?major_achievements 是数组还是单词串?)。


六、最强约束:zod 模式 + StructuredOutputParser.fromZodSchema

当需要精确约束类型、必填项、嵌套结构 时,就该用 zod 上场了。structured-output-parser2.mjs 展示了最完整的写法:

js 复制代码
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";

const scientistSchema = z.object({
  name: z.string().describe("科学家的姓名"),
  birth_year: z.number().describe("科学家的出生年份"),
  death_year: z.number().optional().describe("死亡年份, 在世则为空"),
  nationality: z.string().describe("科学家的国籍"),
  fields_of_study: z.array(z.string()).describe("研究领域"),
  awards: z.array(z.object({
    award_name: z.string().describe("奖项名称"),
    award_year: z.number().describe("奖项年份"),
    award_reason: z.string().describe("奖项原因"),
  })),
  major_achievements: z.array(z.string()).describe("主要成就列表"),
  famous_theory: z.array(z.object({
    name: z.string().describe("理论名称"),
    year: z.number().describe("理论年份"),
    description: z.string().describe("理论描述"),
  })),
  biography: z.string().describe("简短传记, 100字以内"),
});

const parser = StructuredOutputParser.fromZodSchema(scientistSchema);
const question = `
请介绍一下居里夫人的详细信息,
${parser.getFormatInstructions()}
`;
const result = await parser.parse(response.content);
console.log(`姓名: ${result.name}`);

为什么用 zod?

zod 是一个校验库,它能用可读的语法定义"数据结构长什么样"。这段 schema 声明了:

  • name 是字符串(z.string())。
  • birth_year 是数字(z.number())。
  • death_year 是可选的(.optional()),因为有些人还在世。
  • fields_of_study字符串数组z.array(z.string()))。
  • awards对象数组,每个对象里有奖项名、年份、原因。
  • .describe(...) 给每个字段加说明,解析器会把这些说明翻译成给模型的提示。

这种写法把"字段、类型、必填性、嵌套关系 "全都定义清楚了。解析时,LangChain 会严格按照 schema 校验并转换成 JS 对象,一旦字段不合规就能及时发现。这是生产环境最推荐的方式

面试重点:它能自动处理"字段类型对不上、缺字段、有多余字段"等问题 ,比手写 JSON.parse 可靠得多。


七、更省心的终极方案:withStructuredOutput(with-structured-output.mjs)

看到这里你可能想问:又要写 prompt、又要拿 getFormatInstructions()、又要手动 parse,能不能一步到位? 能。with-structured-output.mjs 就是答案:

js 复制代码
const scientistSchema = z.object({
  name: z.string().describe("科学家的姓名"),
  birth_year: z.number().describe("科学家的出生年份"),
  nationality: z.string().describe("科学家的国籍"),
  fields_of_study: z.array(z.string()).describe("科学家的研究领域"),
});
const structuredModel = model.withStructuredOutput(scientistSchema);
const result = await structuredModel.invoke("介绍一下爱因斯坦");
console.log(JSON.stringify(result, null, 2));

注意这里的巨大变化:

  • 不再需要手动拼 prompt 和调 parsewithStructuredOutput(schema) 把模型"包装"成一个"结构化输出模型",直接在背后自动处理格式指令和解析。
  • invoke 返回的就是一个干净的 JS 对象 ,可以直接 result.name 访问。

可以说 withStructuredOutput 是"输出解析器"的高级封装,它把上面那些繁琐步骤全都藏起来了,是如今开发 AI 应用最常用的写法之一。


八、再深一层:用 bindTools 让模型"产出"结构化参数(tool-call-args.mjs)

除此之外,还有一条"曲线救国"的路线------利用模型**调用工具(Tool Call)**的能力。tool-call-args.mjs

js 复制代码
const modelWithTool = model.bindTools([
  {
    name: "extract_scientist_info",
    description: "提取和结构化科学家的详细信息",
    schema: scientistSchema,
  },
]);
const response = await modelWithTool.invoke("请介绍一下爱因斯坦");
console.log(response.tool_calls[0].args);

模型本身支持"调用工具":当它发现你问了符合某个工具描述的问题,就会"准备调用"这个工具,并把工具的入参 (参数)以 JSON 形式填好。model.bindTools([...]) 就是把工具定义绑定到模型上,response.tool_calls[0].args 就能拿到这份结构化的参数。

这套逻辑的巧妙之处在于:它跳过了"让模型输出 JSON 再解析"这一步,而是利用模型与生俱来的"工具参数"能力来产出结构化数据。 不过目录里的注释也点出:这条"偏门"路线适合特定场景,大多数情况下还是 withStructuredOutput 更顺手。


九、大杀器:流式输出 + 结构化(stream 相关文件)

前面所有例子都是 invoke------一次性 把最终结果给你(同步等到底),体验上是"憋半天,然后一次性蹦出全部答案"。但像 ChatGPT 那样一个字一个字"蹦出来",靠的是 流式输出(Stream)

1. 普通流式输出(stream-normal.mjs)

js 复制代码
const stream = await model.stream(prompt);
for await (const chunk of stream) {
  const content = chunk.content;
  fullContent += content;
  process.stdout.write(content);  // 实时显示流式文本
}

关键点:

  • model.stream() 返回一个异步可迭代对象("水流")。
  • for await (const chunk of stream) 逐个读取"数据块(chunk)"。每个 chunk 只是答案的一小部分。
  • 把每个 chunk 累积(fullContent += content)就能得到完整内容。

好处:用户不用干等,答案一边生成一边显示,体验好。

2. 流式 + 结构化(stream-with-structured-output.mjs)

那能不能一边流式输出,一边又是结构化的 JSON?可以:

js 复制代码
const schema = z.object({ ... });
const structuredModel = model.withStructuredOutput(schema);
const stream = await structuredModel.stream(prompt);
for await (const chunk of stream) {
  console.log(JSON.stringify(chunk, null, 2));
}

这里把 withStructuredOutputstream 结合:流式地收到结构化片段(partial),前端可以边生成边渲染。


十、顺带了解:XML 解析器(xml-output-parser.mjs)

JSON 是当今数据交换的"事实标准",但在老一代,XML 才是主流。xml-output-parser.mjs 展示了 LangChain 也支持解析 XML 格式的输出:

js 复制代码
import { XMLOutputParser } from "@langchain/core/output_parsers";
const parser = new XMLOutputParser();
const question = `
请提取以下文本中的人物信息: 爱因斯坦生于1879年3月14日...
${parser.getFormatInstructions()}
`;
const result = await parser.parse(response.content);
console.log(result);

只要把 JsonOutputParser 换成 XMLOutputParser,模型就会按 XML 格式输出,解析器把 XML 转成 JS 对象。这告诉我们要点:输出解析器是可插拔、可替换的,只要模型输出什么格式,就换对应的解析器。


相关推荐
工业涂料百问1 小时前
【市场格局】系列(四)汽车、船舶、风电涂料赛道对比:1400亿工业涂料里,哪个细分最“肥“?
人工智能·汽车
桃西西呀1 小时前
红酒标签上的 87 分是怎么算出来的?我拿 1599 瓶真酒把线性回归和逻辑回归拆开讲
人工智能·机器学习·llm
帅哥的AI自修课1 小时前
你的AI开发还是单打独斗?MetaGPT焊死「多智能体协作」,从角色分工到代码生成一篇打通
人工智能
HZZD_HZZD1 小时前
商业综合体分户计量与能源计费:业态差异、公区分摊与转供电合规
大数据·人工智能·能源
DevOps老兵1 小时前
AI Infra实战11:模型部署Pipeline——CI/CD自动化
人工智能·ci/cd·ai·自动化·gitlab
一线数智1 小时前
制造业的下一场竞争,不在减人,而在重新定义人与机器的关系
人工智能
网络研究院1 小时前
告别野蛮生长!苹果 Apple Music 重拳监管 AI 音乐
人工智能·媒体·音乐·标注·歌曲·执行·监管
xlq223221 小时前
Ai大模型接入sdk day5
人工智能
Austin_YB1 小时前
如何用 AI全程托管开发(Agent + SAP MCP 工具链 + 自定义技能)把功能说明书变成系统功能
人工智能