StructuredOutputParser——别再靠"哄",把 schema 讲清楚再验收

系列第 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 当作文本吐出来。要是模型压根不配合、给一堆废话,你再怎么校验也是拿到一堆解析错误。

下一篇换一条更硬的路------让模型把答案填进"工具参数"里,而不是写在作文里。

相关推荐
1点东西5 天前
做了近两年的Agent开发,其实真正要学的就是这五件事
llm·agent·ai编程
程序猿编码5 天前
告别改源码适配模型:纯 C++ 可配置 LLM 推理引擎,全格式全结构兼容
c++·大模型·llm·推理引擎
小林ixn5 天前
从 LangChain 到 LangGraph:用「网状工作流」解锁多 Agent 协作的正确姿势
langchain·llm·agent
武子康5 天前
自己做一个 Mini Reviewer:让 AI 审到本次准备提交的代码
人工智能·llm·agent
武子康5 天前
GPU Pod 已经 Running,为什么扩容还没变成推理容量?
人工智能·llm·agent
BlackStar_L5 天前
第二章 上下文工程
大模型·llm·agent
Together_CZ6 天前
DeepSeek-V4.1-Flash:Pushing the Limits of KV Cache Compression——推动 KV 缓存压缩的极限
缓存·llm·compression·kv cache·deepseek·v4.1-flash·推动 kv 缓存压缩的极限
桃西西呀6 天前
你拍的一堆硬币,手机怎么一眼数出有几枚?聊聊边缘、轮廓和模板匹配
人工智能·llm·图像识别
slacker-kian6 天前
[实践]-让 SAP 工程 Skill 脱离 opencode跑在自定义Agent 上
ai·llm·sap·agent·abap·adt·opencode
武子康6 天前
CLAUDE.md 越写越长,哪些规则该放到子目录?
人工智能·llm·agent