让大模型乖乖输出 JSON:用 Zod + JsonOutputParser 构建可靠 AI 接口

46 阅读5分钟

你有没有遇到过这种情况?

你让大模型“返回一个包含 name、core、useCase 的 JSON”,结果它回你:

“好的!以下是关于 Promise 的信息:
name: 'Promise'
core: '用于处理异步操作...'
...(后面还有一大段解释)”

这根本不是 JSON!
你没法直接 JSON.parse,更别提在前端渲染了。

那怎么办呢?
难道每次都要写正则去“抠”出 JSON 部分?或者手动清洗响应?

其实,LangChain 早就为你准备好了答案:JsonOutputParser + Zod

今天,我们就用一段真实代码,看看如何让大模型像真正的 API 一样,只返回干净、结构化、可校验的 JSON


一、先看问题:为什么模型总“不听话”?

大模型天生是“自由表达者”。
即使你写:“请返回 JSON”,它也可能:

  • 在 JSON 前后加解释文字
  • 漏掉某个字段
  • 把数组写成字符串
  • 用中文键名而不是英文

比如你期望:

{ "name": "Promise", "core": "..." }

它可能返回:

这个概念叫 Promise,核心是……
{ name: "Promise", core: "..." }
(注意:这里甚至不是合法 JSON,缺少引号!)

那如何去解决这个问题?

我们需要两样东西:

  1. 明确告诉模型:你必须按这个格式输出
  2. 自动验证并解析结果,失败就重试或报错

而这,正是 ZodJsonOutputParser 的组合拳。


二、第一步:用 Zod 定义你的数据契约

Zod 是一个强大的 TypeScript 运行时校验库。
在 LangChain 中,它被用来声明你期望的输出结构

const FrontendConceptSchema = z.object({
  name: z.string().describe("概念名称"),
  core: z.string().describe("核心要点"),
  useCase: z.array(z.string()).describe("常见使用场景"),
  difficulty: z.enum(['简单', '中等', '复杂']).describe("学习难度")
});

这段代码做了什么?

  • 定义了一个对象结构
  • 每个字段都有类型约束(字符串、数组、枚举)
  • 通过 .describe() 添加语义说明(这些会被自动注入到提示词中!)

这就像你和模型签订了一份“数据合同”
“你必须返回符合这个结构的数据,否则我就拒收。”


三、第二步:用 JsonOutputParser 自动注入格式指令

有了 Schema,怎么让模型知道?

LangChain 的 JsonOutputParser 会帮你做两件事:

  1. 生成清晰的格式说明(format instructions)
  2. 自动解析并校验模型的输出
const jsonParser = new JsonOutputParser(FrontendConceptSchema);

然后,把它插入到提示词模板中:

const prompt = PromptTemplate.fromTemplate(`
  你是一个只会输出 JSON 的 API,不允许输出任何解释性文字。

  ⚠️ 你必须【只返回】符合以下 Schema 的 JSON:
  - 不允许增加字段
  - 不允许减少字段
  - 字段名必须完全一致
  - 返回结果必须可以被 JSON.parse 成功解析

  {format_instructions}

  前端概念:{topic}
`);

关键就在 {format_instructions} ——
当你调用 jsonParser.getFormatInstructions() 时,它会自动生成一段模型能理解的格式描述,比如:

The output should be a markdown code snippet formatted in the following schema:

{
  "name": string // 概念名称
  "core": string // 核心要点
  "useCase": Array<string> // 常见使用场景
  "difficulty": "简单" | "中等" | "复杂" // 学习难度
}

这比你自己手写“请返回 JSON”有效得多!


四、第三步:构建完整工作流 —— 提示词 → 模型 → 解析器

现在,把它们串起来:

const chain = prompt.pipe(model).pipe(jsonParser);

这条链的含义非常清晰:

  1. prompt:根据 topicformat_instructions 生成最终提示词
  2. model:调用 DeepSeek 模型,获取原始响应
  3. jsonParser:从响应中提取 JSON,并用 Zod 校验结构

最后调用:

const response = await chain.invoke({
  topic: 'Promise',
  format_instructions: jsonParser.getFormatInstructions(),
});

如果一切顺利,response 就是一个干净的 JavaScript 对象

{
  name: "Promise",
  core: "用于表示异步操作的最终完成或失败...",
  useCase: ["处理 fetch 请求", "封装异步逻辑", "避免回调地狱"],
  difficulty: "中等"
}

你可以直接传给前端,无需任何清洗!


五、但如果模型还是“不听话”呢?

即使加了格式指令,某些模型仍可能输出非法内容。

这时候,JsonOutputParser 会怎么做?

  • 它会尝试从响应中提取第一个合法的 JSON 对象
  • 如果提取失败或校验不通过,抛出错误

这意味着:你不会得到一个“看起来像 JSON 但实际有 bug”的结果,而是明确知道“这次调用失败了”。

你可以在上层捕获错误,进行重试、降级或记录日志:

try {
  const result = await chain.invoke({ topic: '闭包', format_instructions: ... });
  // 成功,直接使用
} catch (error) {
  console.error('模型未返回合法 JSON:', error.message);
  // 处理失败情况
}

这种“要么全对,要么报错”的机制,正是构建可靠 AI 系统的关键。


六、为什么这比手动解析强?

对比传统做法:

// 手动方式(脆弱!)
const raw = await model.invoke(...);
const match = raw.content.match(/```json([\s\S]*?)```/);
if (match) {
  const data = JSON.parse(match[1]);
  // 但字段对不对?类型对不对?没人知道!
}

而用 Zod + JsonOutputParser

  • 自动提取 JSON(支持多种包裹格式)
  • 自动校验字段是否存在、类型是否正确
  • 自动转换(如字符串转数字,如果 Schema 允许)
  • 错误信息清晰(“missing field 'name'”)

你不再需要猜测模型到底返回了什么——系统会替你验证。


七、小结:把大模型变成真正的 API

通过 Zod 定义结构 + JsonOutputParser 强制执行,我们实现了:

让大模型从“自由聊天者”变成“严格遵守契约的 API 服务”

这不仅是技术技巧,更是一种工程思维的转变:

  • 不再信任模型的“口头承诺”
  • 用代码定义接口规范
  • 用解析器保障数据质量