你有没有遇到过这种情况?
你让大模型“返回一个包含 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,缺少引号!)
那如何去解决这个问题?
我们需要两样东西:
- 明确告诉模型:你必须按这个格式输出
- 自动验证并解析结果,失败就重试或报错
而这,正是 Zod 和 JsonOutputParser 的组合拳。
二、第一步:用 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 会帮你做两件事:
- 生成清晰的格式说明(format instructions)
- 自动解析并校验模型的输出
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);
这条链的含义非常清晰:
prompt:根据topic和format_instructions生成最终提示词model:调用 DeepSeek 模型,获取原始响应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 服务”
这不仅是技术技巧,更是一种工程思维的转变:
- 不再信任模型的“口头承诺”
- 用代码定义接口规范
- 用解析器保障数据质量