用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 之外的方案的吗,评论区蹲一个对比。

相关推荐
学习日记5256 分钟前
第 5 章:自定义 Skill——把重复操作封装成一键命令
人工智能·ai·prompt
云上工程笔记7 分钟前
AstraFlow + React/Vite:如何用自然语言开发并部署一个 AI 咖啡网站原型
前端·人工智能·react.js
醍醐实验室10 分钟前
长文本外推中的 RoPE Base 调优:从 10,000 到 500,000 的数学底数奥秘
人工智能
搞科研的小刘选手23 分钟前
【香港中文大学(深圳)、IEEE主办 | 深圳举办】第六届IEEE能源工程与电力系统国际学术会议(EEPS 2026)
人工智能·电力系统·能源工程·香港中文大学(深圳)·会议推荐
weixin_4462608528 分钟前
面向编码智能体的「先学测试,再用测试提升修复能力」框架
人工智能
2401_8653825031 分钟前
政务信息化项目审价的现实意义
大数据·人工智能·政务
lvts_cs34 分钟前
汽车零部件产业发展趋势,对地方招商工作带来的影响
大数据·人工智能·汽车
AI云海34 分钟前
计算机视觉之YOLO11整体架构、多任务能力
人工智能·计算机视觉·架构
Axis tech36 分钟前
基于Ego-Pi框架与Manus手套的仿人机器人灵巧操作研究
人工智能·机器学习·机器人
我有满天星辰43 分钟前
《从 0 打造我的本地 AI 知识库:Obsidian + Ollama + Milvus + RAG + MCP + Agent》第 01 篇
人工智能·milvus