用zod在运行时兜住AI返回的JSON

让 AI 返回结构化 JSON 这事,写 demo 的时候很爽,上生产那天就开始挨打。

我们有个功能让模型把用户的一段自然语言解析成表单字段,约定它返回固定结构的 JSON。前端拿到直接 JSON.parse 然后 data.amount.toFixed(2)。前两周岁月静好,某天线上报错:Cannot read properties of undefined。模型那次返回里 amount 字段没了,它自作主张换成了 total

模型的输出是概率性的。你给它再严格的 prompt,它也有小概率不照做。TypeScript 那套类型断言在编译期信誓旦旦,运行时一点忙帮不上------as ParsedForm 只是你对它撒的谎。

编译期类型 + 运行时校验,一份 schema 搞定

我用 zod 把这事统一了。schema 既是运行时校验器,又能反推出 TS 类型,不用两头维护:

css 复制代码
import { z } from 'zod'

const ParsedFormSchema = z.object({
  intent: z.enum(['create', 'update', 'query']),
  amount: z.number().nonnegative(),
  currency: z.string().length(3).default('CNY'),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  tags: z.array(z.string()).max(10).default([]),
})

type ParsedForm = z.infer<typeof ParsedFormSchema> // 类型从 schema 来,不手写

z.infer 这步是关键。类型和校验逻辑同源,改 schema 类型自动跟着变,杜绝了「类型写了 number、实际没校验」的对不上。

解析失败要给得出兜底

safeParse 不抛异常,返回成功失败的判别联合,逼你必须处理失败分支:

typescript 复制代码
function parseAIResult(raw: string): ParsedForm | null {
  let json: unknown
  try {
    json = JSON.parse(raw)
  } catch {
    return null // 模型有时会在 JSON 外面包 ```json 代码块,先清洗
  }
  const result = ParsedFormSchema.safeParse(json)
  if (!result.success) {
    console.warn('AI 输出不符合 schema', result.error.flatten())
    return null
  }
  return result.data
}

result.error.flatten() 排错时特别有用,直接告诉你哪个字段挂了。上面那次 amounttotal 的事故,它一眼就能定位到 amount: Required

模型爱在 JSON 外面套壳

实战里最高频的不是字段错,是模型把 JSON 包在 markdown 代码块里返回,前面还带句「好的,这是解析结果:」。直接 parse 必炸。我加了层清洗:

sql 复制代码
function extractJSON(raw: string): string {
  const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/)
  if (fenced) return fenced[1].trim()
  const start = raw.indexOf('{')
  const end = raw.lastIndexOf('}')
  return start >= 0 && end > start ? raw.slice(start, end + 1) : raw
}

这个清洗函数我们组现在每个对接 AI 的项目都复制一份。不优雅,但 work。

用 coerce 容忍小毛病

模型偶尔把数字返回成字符串 "100"。早期我对这种直接判失败,后来发现太严了------明明语义对的,何必。用 z.coerce 做温和转换:

scss 复制代码
const Amount = z.coerce.number().nonnegative()
// "100" -> 100,但 "abc" 仍然失败

这里有个取舍:coerce 用多了,schema 的「严格保证」就松了。我的原则是只对数字、布尔、日期这种有明确转换规则的字段开 coerce,枚举和关键业务字段绝不放松。这条线我们团队讨论了挺久,最后定的就是「能无歧义转的才转」。

一个仍在忍受的缺点

zod 的校验在超大对象上有性能开销,我们一个嵌套很深的 schema 单次 parse 大概 3-5ms。量大的批处理场景里这个不能忽略。我试过 valibot 更快但生态没 zod 全,纠结之后还是留在 zod,体积和速度的账先记着,等真成瓶颈再说。

类型安全这事,在 AI 接口面前不能只靠 TS。运行时这道闸,迟早得补。


说回来,能容忍模型偶尔抽风、又不至于翻车,前提是后端模型本身足够稳定可靠。我们用的是讯飞 这类 MaaS 提供的现成模型 API,结构化输出的成功率比自己瞎调高不少,前端兜底的活儿也轻一些。

你们怎么校验 AI 的结构化输出?有用 zod 之外的方案的吗,评论区蹲一个对比。

相关推荐
Linging_242 分钟前
基于Langchain得AI Agent相关知识学习
人工智能·学习·langchain
haluhalu.7 分钟前
prompts.chat:07-few-shot-prompting
人工智能·算法
weixin_4398575419 分钟前
在Ubuntu 26.04版本上借助本地AI助手在AMD显卡基础上安装并运行Qwen3.6-35B-A3B,作为本地编程AI
linux·人工智能·ubuntu
YOLO数据集集合23 分钟前
多模态AI道路病害检测系统:YOLO+DeepSeek融合实战
人工智能·yolo·目标检测·计算机视觉·目标跟踪
程序员脚趾8 小时前
GPT-5.6 上线:当 GPT 与 Codex 走向融合,AI 正从“回答问题”走向“完成工作”
人工智能
thubier(段新建)8 小时前
owtb 3pl 面向城市配送物流企业需求V0.2
大数据·人工智能
Token炼金师9 小时前
模型的防线:Prompt 注入防御、越狱攻击与对齐、红队测试、价值观对齐、对抗样本鲁棒性、安全评测与边界 —— 模型安全六防
人工智能·红队测试·prompt 注入防御·越狱攻击与对齐·价值观对齐·对抗样本鲁棒性·安全评测与边界
嘘神秘用9 小时前
布:AI 驱动的 Redis 客户端,更快、更直观
数据库·人工智能·redis
黒亱中旳9 小时前
Java AI 框架三国杀:Solon AI vs Spring AI vs LangChain4j 深度对比
java·人工智能·spring
小和尚同志9 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc