系列第 2 篇。第 1 篇我们用
JsonOutputParser拿到了对象,但它有个隐蔽的毛病:不校验。
设想:你让模型返回 name / birth_year / nationality 三个字段,它只回了 name 和 nationality,漏了 birth_year。JsonOutputParser 会怎样?
它照单全收,一声不吭。 result.birth_year 变成 undefined,而你毫无察觉,直到后面某行代码因为它炸了才发现。问题本质:JsonOutputParser 只做"解析",不做"验收"。
这一篇的主角 StructuredOutputParser 要补的正是验收这一环。它做的事情有两个,各占一半:
scss
┌─ 请求端(教)─────────────────────────────┐
│ getFormatInstructions() → 把 schema 讲给模型 │
└──────────────────────────────────────────┘
┌─ 输出端(验)─────────────────────────────┐
│ parse() → JSON.parse → 按 schema 逐项验收 │
└──────────────────────────────────────────┘
同一份 schema,两头用:教模型怎么输出,也验模型输得对不对。
请求端:这次格式说明是真货
还记得第 1 篇的坑吗?JsonOutputParser.getFormatInstructions() 返回空串。StructuredOutputParser 的不是 ------它会把你的 schema 序列化成一份正经的 JSON Schema 说明文字塞进 prompt(源码里是一整段"你必须按 JSON Schema 输出"的英文 + ```````json {...}````)。你把 parser.getFormatInstructions() 拼进 question,模型真的会看到。
两个解析器的分工差异,一句话:一个只收(不约束、不校验),一个先教后收再验。
两种构造方式
fromNamesAndDescriptions:最省事,但暗藏一个坑
js
import { StructuredOutputParser } from "@langchain/core/output_parsers";
const parser = StructuredOutputParser.fromNamesAndDescriptions({
name: "姓名",
birth_year: "出生年份",
nationality: "国籍",
});
看源码你就会发现:它内部把每个字段都转成了 z.string()------所有字段都是字符串类型 。所以如果你写 major_achievements: "主要成就, 用逗号分隔的字符串",解析出来拿到的就是一大串 "xx,yy",不是数组。不是你没说清楚,是这个构造方式根本表达不了数组。
fromZodSchema:完整的数据契约
要精确类型,就得自己写 zod schema 再交进去:
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("出生年份"), // 必须是数字!
nationality: z.string().describe("国籍"),
fields: z.array(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("理论简要描述"),
})
).describe("著名理论列表"),
death_year: z.number().optional().describe("死亡年份, 在世则不填"), // 可缺省
});
const parser = StructuredOutputParser.fromZodSchema(scientistSchema);
这一下表达力就上来了:
| schema 写法 | 含义 | 模型敢乱来就... |
|---|---|---|
z.number() |
必须是数字 | 输出 "1879" 字符串 → 校验报错 |
z.array(z.string()) |
数组 | 输出 "相对论,量子力学" → 校验报错 |
z.object({...}) 嵌套 |
每项还有子约束 | 漏一个子字段就挂 |
.optional() |
允许缺省 | 在世科学家不填也合法 |
输出端:parse 到底有多严
js
const result = await parser.parse(response.content);
console.log(result.name); // 校验通过才走到这里
它内部:剥 markdown 围栏 → JSON.parse → 丢给 zod 逐项校验 。字段缺失、类型不对,会抛 OutputParserException,被你的 catch 接住。这正是"解析"和"校验"的区别------JsonOutputParser 只做到前两步,StructuredOutputParser 多走了第三步。
describe() 的中文是写给模型看的
你可能好奇:describe("出生年份") 又不参与校验逻辑,写了干嘛?
它会透传到 JSON Schema 的 description 字段,随 getFormatInstructions() 一起进 prompt 给模型看 。这是"教模型字段含义"的通道------相当于把你 prompt 里手写的字段说明,挪进了 schema 内部统一管理。好处是:教模型的那份规则 = 验输出的那份规则,永远一致,不会两边各写各的对不上。
一个运行前置:记得装 zod
StructuredOutputParser 依赖 zod(@langchain/core 内部 import { z } from "zod/v3")。项目没装的话,连 import 都会崩,报 Cannot find module 'zod/v3'。装一下就好:
bash
pnpm add zod
小结
scss
JsonOutputParser: 剥围栏 + JSON.parse → 只解析,不验收
StructuredOutputParser:
请求端 getFormatInstructions() → 把 schema 讲给模型(真货)
输出端 parse() → JSON.parse + zod 逐项校验
从"哄"到"讲清楚再验收" ,是结构化输出可靠性的第一步。但它仍属于"文本派":它赌的是模型乖乖把 JSON 当作文本吐出来。要是模型压根不配合、给一堆废话,你再怎么校验也是拿到一堆解析错误。
下一篇换一条更硬的路------让模型把答案填进"工具参数"里,而不是写在作文里。