🦁 驯兽记:如何用 Zod 给 LLM 戴上“紧箍咒”

5 阅读5分钟

🦁 驯兽记:如何用 Zod 给 LLM 戴上“紧箍咒”

想象一下,你雇佣了一个博学多才但性格随性的私人助理(LLM)。你让他帮你整理一份“前端概念清单”,他可能给你写一首诗,也可能画一幅画,甚至可能跟你聊起他昨天的梦境,就是不给你那个规规矩矩的 JSON 表格。

在 AI 应用开发中,这就是我们每天面临的“驯兽”挑战。LLM 就像一个充满创造力的文科生,但程序需要的是一个严谨、守规矩的理科生。今天,我们就来聊聊如何用 ZodLangChain 的 OutputParser,给这位“狂野”的 AI 戴上一个牢不可破的“紧箍咒”。

🤔 为什么 LLM 的输出像个“开盲盒”?

大语言模型(LLM)本质上是概率模型。你给它一个提示词,它根据海量的训练数据预测下一个字是什么。这意味着,即使你明确要求“返回 JSON”,它也可能偶尔“抽风”,给你返回:

  • 带 Markdown 代码块的 JSON (json ... )
  • 前面加了一句“好的,这是你要的 JSON:”
  • 字段名拼写错误,或者类型不对(比如把数字返回成了字符串)

这对于需要稳定数据流的程序来说,简直是灾难。这就好比你的代码期待一个苹果,结果 AI 给了你一个香蕉,程序直接崩溃(Crash)。

🛠️ 我们的武器库:Zod + LangChain

为了解决这个问题,我们需要一套组合拳:

  1. Zod:TypeScript 的 Schema 验证库。你可以把它理解为一个“数据模具”或“契约”。我们用它来定义:“我需要什么样的数据,长什么样,什么类型”。
  2. LangChain OutputParser:LangChain 提供的一个工具,它能自动把 Zod 定义的“模具”翻译成 LLM 能看懂的指令,并在 LLM 输出后进行二次校验和清洗。

📜 从 JS 模块化到 AI 规范化:历史的押韵

在深入代码之前,让我们先来个“历史小课堂”。

回想一下早期的 JavaScript,没有 import / export,我们在 HTML 里用 <script src="a.js"></script> 像挂咸鱼一样把代码串起来。全局变量满天飞,代码耦合得像一团乱麻。后来,Node.js 带来了 CommonJS (require),再后来 ES6 带来了标准的模块化。这一切的演进,都是为了规范化,为了让代码更可控、更可维护。

今天,我们在 AI 领域面临着同样的问题。LLM 的输出就像早期的全局变量,混乱且不可预测。而 Zod + OutputParser,就是 AI 时代的“ES6 模块化规范”,它强制 AI 按照我们定义的接口(Schema)输出数据,让 AI 应用从“脚本小子”进化为“正规工程”。

💻 实战:给 AI 立规矩

让我们看看如何用代码实现“驯兽”。

第一步:定义“模具” (Zod Schema)

首先,我们需要告诉 Zod 我们想要什么。比如,我们要整理前端概念:

import { z } from 'zod'

// 定义一个严格的对象结构
const FrontendConceptSchema = z.object({
  name: z.string().describe("概念名称"), // 必须是字符串
  core: z.string().describe("核心要点"),
  useCase: z.array(z.string()).describe("常见使用场景"), // 必须是字符串数组
  difficulty: z.enum(['简单', '中等', '复杂']).describe("学习难度") // 只能是这三个选项之一
}) 

这里 describe 方法非常重要,它不仅是为了给人看,更是为了让 Zod 在生成提示词时,能把这些描述“翻译”给 LLM 听,告诉它每个字段具体该填什么。

第二步:制造“紧箍咒” (OutputParser)

接下来,我们把这个模具交给 LangChain 的解析器:

import { JsonOutputParser } from '@langchain/core/output_parsers'

// 创建解析器,它现在知道了数据的规矩
const jsonParser = new JsonOutputParser(FrontendConceptSchema);

第三步:编写“咒语” (Prompt)

这是最关键的一步。我们需要把解析器生成的“格式说明书”塞进提示词里。

import { PromptTemplate } from '@langchain/core/prompts'

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

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

  {format_instructions} // 这里是关键!解析器会自动填入详细的格式要求

  前端概念:{topic}
`);

注意那个 {format_instructions} 占位符。当你运行 jsonParser.getFormatInstructions() 时,它会生成一段类似这样的话:“输出应格式化为符合以下 JSON Schema 的 JSON 实例...”。这就相当于把 Zod 的规矩直接拍在了 LLM 的脸上。

第四步:召唤与驯服 (Chain & Invoke)

最后,我们将提示词、模型和解析器串联起来(Pipe):

import { ChatDeepSeek } from '@langchain/deepseek'

const model = new ChatDeepSeek({
  model: 'deepseek-reasoner',
  temperature: 0, // 温度设为0,让 AI 尽量确定性和严谨
});

// 链式调用:提示词 -> 模型 -> 解析器
const chain = prompt.pipe(model).pipe(jsonParser)   

const response = await chain.invoke({  
  topic: 'Promise.all',
  format_instructions: jsonParser.getFormatInstructions(), // 注入规则
});

console.log(response); 

🎉 结局:完美的 JSON

运行这段代码,奇迹发生了。哪怕 LLM 内部想写诗,经过 OutputParser 的拦截和 Zod 的校验,最终吐出来的 response 将是一个干干净净的 JavaScript 对象:

{
  "name": "Promise.all",
  "core": "并发处理多个 Promise,全部成功才成功...",
  "useCase": ["批量请求", "并行任务"],
  "difficulty": "中等"
}

📌 总结

通过 Zod 和 LangChain OutputParser,我们成功地将“生成式 AI”关进了“结构化数据”的笼子。

  • Zod 负责定义“契约”。
  • OutputParser 负责“翻译契约”给 AI 听,并在事后“检查作业”。
  • LLM 负责在规则内发挥它的智慧。