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

系列第 2 篇。第 1 篇我们用 JsonOutputParser 拿到了对象,但它有个隐蔽的毛病:不校验

设想:你让模型返回 name / birth_year / nationality 三个字段,它只回了 namenationality,漏了 birth_yearJsonOutputParser 会怎样?

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

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

相关推荐
武子康2 小时前
从声学信号到工具阻断:实时语音安全决策门的系统设计
人工智能·llm·agent
tachibana23 小时前
Embedding 有哪几种算法?
人工智能·算法·ai·大模型·llm·embedding·agent
吴佳浩12 小时前
FDE:从系统落地工程师,演变为企业 AI 能力的知识架构师
人工智能·llm·ai编程
吴佳浩12 小时前
从 OpenClaw、Codex 到 Hermes,看懂 AI Agent 架构为什么正在收敛
人工智能·llm·agent
stereohomology13 小时前
Wolfram会以什么姿态拥抱LLM
llm
掰头战士19 小时前
从LLM到Agent、Agent的6大核心。这些基础知识你还记得吗
node.js·llm·agent
甜辣uu19 小时前
智能体Agent性能优化从原理到实战
人工智能·性能优化·大模型·llm·agent·rag·智能体
XLYcmy21 小时前
Self-Adapting Language Models论文分享
自然语言处理·llm·微调·sft·论文笔记·强化学习·自进化
桃西西呀1 天前
用 AI 写得更快,上线却容易炸?拆解 AI 编码生产力悖论的 5 个机制,附 9 个坑的自检清单
人工智能·llm·ai编程