摘要 :在人工智能应用开发中,最让人头疼的不是 AI 不够聪明,而是它"太爱说话"。本文将深入浅出地介绍如何利用 LangChain 的 JsonOutputParser 和 Zod,给 AI 戴上"紧箍咒",将它从一个随意的聊天对象,变成一个精准的结构化数据生成器。
引言:当 AI 遇上程序代码
在 ChatGPT 等大模型(LLM)爆火的今天,我们习惯了用自然语言和 AI 对话。你问它:"什么是 Promise?"它会洋洋洒洒给你写一篇几百字的文章,甚至贴心地加上代码示例和总结。
这对人类来说非常完美,但对于程序开发来说,这简直是场"灾难"。
为什么?因为程序代码听不懂"散文"。如果你正在开发一个自动生成技术文档的软件,你的代码需要的是精确的、结构化的数据,比如:
JSON
{
"name": "Promise",
"difficulty": "中等",
"description": "异步编程解决方案..."
}
如果你直接问 AI,它可能会在 JSON 数据外面包上一层 Markdown 格式,或者在开头加一句"好的,这是你要的数据",甚至把字段名 difficulty 随手写成 level。只要有一个符号不对,你的程序就会报错崩溃。
今天,我们就来揭秘一种"驯服" AI 的技术手段------利用 LangChain 框架配合 Zod 验证库,让 AI 乖乖听话,只输出我们想要的标准格式,最后会附上我的一段完整实例代码。
核心武器:LangChain 与 Zod
要解决这个问题,我们需要一套严密的"流水线"。在这个流水线中,我们引入了两个关键角色:
- Zod(规则制定者) :它负责定义"什么是正确的数据格式"。你可以把它想象成一个模具,所有出厂的产品必须长得和它一样。
- JsonOutputParser(监工) :它是 LangChain 提供的解析器,负责把 Zod 定义的规则翻译成 AI 能听懂的指令,并负责检查 AI 的输出是否合格。
下面,我们将通过实际的代码逻辑(基于 DeepSeek 模型),看看这套机制是如何运转的。
第一步:定义"契约" (Schema)
一切始于规则。我们需要明确告诉计算机,我们想要什么样的数据。
在代码中,我们使用 zod 库来创建一个"概念模型"。假设我们要让 AI 解释前端技术概念,我们规定输出必须包含四个特定的信息点:
JavaScript
// 定义数据的"模具"
const FrontendConceptSchema = z.object({
name: z.string().describe('概念名称'),
core: z.string().describe('核心要点'),
useCase: z.array(z.string()).describe('常见使用场景'),
difficulty: z.enum(['简单', '中等', '困难']).describe('学习难度')
});
这段代码看似简单,实则暗藏玄机:
- 精确的类型 :规定
name必须是文字,useCase必须是数组。 - 严格的选项 :
difficulty(难度)只能在"简单、中等、困难"这三个词里选,AI 如果自创一个"地狱级",就会被判定为违规。 - 语义描述 :注意
.describe(...)这一部分。这不仅仅是给程序员看的注释,LangChain 会把这些文字提取出来传给 AI,告诉它每个字段具体代表什么含义。
第二步:聘请"监工" (Parser)
有了规则,我们需要一个执行者。我们将刚才定义的 Schema 交给 JsonOutputParser:
JavaScript
const jsonParser = new JsonOutputParser(FrontendConceptSchema);
这个解析器有一个极其强大的功能:getFormatInstructions()。它会自动根据我们定义的规则,生成一段专门写给 AI 看的"提示词指令"。这段指令大概长这样(翻译后):
"你必须输出一个 JSON 对象,字段必须包含 name、core、useCase... 不要输出任何 Markdown 标记,确保可以被代码解析..."
第三步:构建指令与模型 (Prompt & Model)
接下来,我们配置 AI 模型。这里使用的是推理能力强大的 DeepSeek (deepseek-reasoner)。为了让输出更稳定,我们将模型的 temperature 设置为 0,意味着让它由"创意模式"切换为"严谨模式"。
然后,我们将"监工"生成的指令嵌入到给 AI 的最终提示词中:
JavaScript
const prompt = PromptTemplate.fromTemplate(`
你是一个只会输出 JSON 的 API,不允许输出任何解释性文字。
⚠️ 你必须【只返回】符合以下 Schema 的 JSON:
- 不允许增加字段
- 不允许减少字段
- 字段名必须完全一致
{format_instructions}
前端概念:{topic}
`);
注意 {format_instructions} 这个占位符,解析器生成的复杂指令会自动填充到这里。这意味着我们不需要手动去写那些繁琐的格式要求,全部自动化完成。
第四步:启动流水线 (Chain)
LangChain 的魅力在于可以将各个步骤像管道一样连接起来:
JavaScript
// 流程:提示词 -> 模型思考 -> JSON解析校验
const chain = prompt.pipe(model).pipe(jsonParser);
这个链条的工作流程非常清晰:
- 输入:用户输入主题(例如"Promise")。
- Prompt:系统自动组合出包含严格格式要求的完整提示词。
- Model:DeepSeek 模型接收指令,进行推理,生成符合要求的文本。
- JsonOutputParser:解析器拦截模型的输出,将其清洗、验证,并转化为真正的 JavaScript 对象。
最终效果
当我们运行这段代码,向系统询问 "Promise" 这个概念时,我们将不再得到一段模糊的聊天记录,而是得到一个完美的数据对象:
JSON
{
"name": "Promise",
"core": "Promise 是 JavaScript 中用于处理异步操作的对象...",
"useCase": [ "网络请求", "文件读取", "数据库查询" ],
"difficulty": "中等"
}
程序可以直接读取 response.difficulty 来做逻辑判断,或者直接遍历 response.useCase 渲染页面列表。
为什么这很重要?
对于普通用户来说,这似乎只是程序员的"强迫症"。但对于 AI 应用的发展来说,这是一次质的飞跃。
- 自动化集成的基石:只有当 AI 的输出是结构化、可预测的,它才能被嵌入到复杂的软件系统中(比如自动报表生成、智能客服工单系统)。
- 告别"幻觉" :通过 Zod 的严格校验,我们能很大程度上过滤掉 AI 瞎编乱造的格式,保证系统稳定性。
- 开发效率倍增:开发者不再需要编写复杂的正则表达式去清洗 AI 的回答,一切由框架自动完成。
通过 JsonOutputParser,我们成功地将"生成式 AI"关进了"结构化数据"的笼子里,让它从一个只会聊天的文科生,变成了一个严谨工作的理科生。这就是现代 AI 工程化的力量。
源码
js
import { ChatDeepSeek } from '@langchain/deepseek';
import { PromptTemplate } from '@langchain/core/prompts';
import { JsonOutputParser } from '@langchain/core/output_parsers';
// Zod 是一个用于 TypeScript 的高性能、声明式 schema 验证库,支持运行时类型检查与静态类型推导。
import { z } from 'zod';
import 'dotenv/config'
const model = new ChatDeepSeek({
model: 'deepseek-reasoner',
temperature: 0,
});
const FrontendConceptSchema = z.object({
name: z.string().describe('概念名称'),
core: z.string().describe('核心要点'),
useCase: z.array(z.string()).describe('常见使用场景'),
difficulty: z.enum(['简单', '中等', '困难']).describe('学习难度')
})
const jsonParser = new JsonOutputParser(FrontendConceptSchema);
const prompt = PromptTemplate.fromTemplate(`
你是一个只会输出 JSON 的 API,不允许输出任何解释性文字。
⚠️ 你必须【只返回】符合以下 Schema 的 JSON:
- 不允许增加字段
- 不允许减少字段
- 字段名必须完全一致,使用name、core、useCase、difficulty
- 返回结果必须可以被 JSON.parse 成功解析
{format_instructions}
前端概念:{topic}
`);
const chain = prompt.pipe(model).pipe(jsonParser);
const response = await chain.invoke({
topic: 'Promise',
format_instructions: jsonParser.getFormatInstructions(),
});
console.log(response);