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 和调 parse 。withStructuredOutput(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));
}

这里把 withStructuredOutput 和 stream 结合:流式地收到结构化片段(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 对象。这告诉我们要点:输出解析器是可插拔、可替换的,只要模型输出什么格式,就换对应的解析器。


相关推荐
niuTaylor2 分钟前
飞腾 FT-2000/4 Ubuntu 内存卡 / NVMe SSD 系统制作完整指南
linux·服务器·人工智能·驱动开发·单片机
pusheng20259 分钟前
马年市场快报 | 阿尔及利亚政府推进2200万台家用CO报警器安装计划
大数据·人工智能
2601_9657422219 分钟前
布局GEO AI本地营销,我重点看这几个细节
大数据·人工智能·算法·ai·新媒体运营
EatFan20 分钟前
AI Agent 上生产前先加三道闸门:审批、限权、可回放的工程实践
人工智能·python·算法·多智能体·ai agent·mcp·harness
遥感知识服务28 分钟前
沙漠和洪水在SAR里都很黑,AI到底怎么分?
人工智能
小成很成29 分钟前
Skills:AI 时代必备技能,让大模型从聊天工具变成专业助手
人工智能
yu_zhidun31 分钟前
企业管理者看:员工上网行为管理软件,别再只靠网关“赌安全”
大数据·人工智能·安全·域智盾
-cywen-31 分钟前
MaskCLIP
人工智能
努力的小雨32 分钟前
被游戏虐烦后,我让 Seed-2.1-pro 帮我做了一款自己当王的 3D 游戏
人工智能
上海锝秉工控34 分钟前
微米级精准捕捉,激光测振解锁产线新效率
人工智能