Schema 校验与 defineTool 模式
专栏:TypeScript 与 Node 地基 · 第 7 / 13 篇:::info 学习目标
完成本篇后你能够:用 zod 定义 schema 并自动推导 TS 类型;理解运行时校验与编译期类型为什么缺一不可;亲手实现一个迷你版 defineTool。
前置:第 5~6 篇完成。预计时长:60 分钟。
:::
边界上的数据都是不可信的:用户输入、API 响应、模型的工具调用参数。第 1 篇说过类型在运行时会被擦除——所以编译期类型再正确,也挡不住运行时来了个畸形参数。解法:用一份 schema 同时生成”编译期类型”和”运行时校验器”。
zod:一份 schema,两份收益
import { z } from 'zod'
const ReadFileArgs = z.object({
path: z.string().describe('要读取的文件绝对路径'),
limit: z.number().int().positive().optional(),
})
type ReadFileArgs = z.infer<typeof ReadFileArgs>
// 自动得到:{ path: string; limit?: number | undefined }
const parsed = ReadFileArgs.safeParse({ path: '/etc/hosts' })
if (!parsed.success) {
console.log(parsed.error.issues) // 哪个字段、什么原因、期望什么
} else {
parsed.data.path // 类型完整,值已通过校验
}
z.infer 是泛型魔法(上一篇的技巧):schema 常量 → 推导出 TS 类型。schema 是唯一事实源:改 schema,类型与校验同时变,永不脱节。另外 .describe() 的描述还能序列化成 JSON Schema——正是模型可见的工具参数说明(第 5 篇的工具描述)。
亲手实现迷你版 defineTool
把上一篇的泛型用起来,30 行实现 dsh defineTool 的核心机制:
import { z, ZodType } from 'zod'
interface ToolDef<Args> {
name: string
description: string
schema: ZodType<Args> // schema 决定 Args 类型
execute: (args: Args) => Promise<string>
}
function defineTool<Args>(def: ToolDef<Args>) {
return {
...def,
// 模型调用入口:先校验,再执行——校验失败返回可读错误而不是崩溃
async handle(rawArgs: unknown): Promise<string> {
const parsed = def.schema.safeParse(rawArgs)
if (!parsed.success) {
return `Error: 参数不合法 - ${parsed.error.issues
.map(i => `${i.path.join('.')}: ${i.message}`).join('; ')}`
}
return def.execute(parsed.data)
},
}
}
const readFileTool = defineTool({
name: 'read_file',
description: '读取文本文件',
schema: z.object({ path: z.string() }),
async execute(args) {
return args.path // args 有完整类型提示
},
})
await readFileTool.handle({ path: '/etc/hostname' }) // ✓ 正常执行
await readFileTool.handle({ limit: 3 }) // → "Error: 参数不合法 - path: Required"
对照 dsh 的真实 defineTool:它多做了三件事——参数校验失败的结构化错误码(INVALID_ARGS,回流给模型而非炸轮次)、输出值也要过 schema 校验、从 schema 自动生成模型可见的参数描述。机制完全相同,工程细节更厚。
校验失败的错误信息:给模型看的
上例的错误信息直接拼接字段与原因——它是回流给模型的,不是给人类调试的。好的校验错误能让模型下一轮自我纠正(第 4 篇”错误回填”原则),差的校验错误(如裸抛 TypeError)会让模型反复撞墙。
校验在调用链上的位置
常见踩坑
常见踩坑
- schema 与实际校验脱节——手写校验 + 单独维护类型,改一处忘另一处;用 z.infer 从 schema 推导类型,永不脱节;
- describe 缺失——schema 里没有
.describe(),转成 JSON Schema 后模型只能靠参数名猜含义; - 在校验前展开不可信输入——
execute({...raw})先展开再校验等于没校验,先safeParse再使用; - 对返回值不校验——dsh 连输出都有 schema(
output.schema),返回什么、渲染什么都有契约。
随堂练习(带验收标准)
- 给
read_file补limit(可选正整数)与offset(可选非负整数)字段。验收:limit: -1返回可读校验错误; - 给工具加
output.schema(返回值类型校验)。验收:execute 返回值不符时得到结构化错误; - 打开 dsh 的
packages/core/tools/src/schema.ts,找出defineTool做了哪三件你本篇没做的事。