构建类型安全的 ChatGPT 工具调用:TypeScript 泛型与 JSON Schema 的深度实践
引言
在 ChatGPT 的 Function Calling(工具调用)工作流中,最令人头疼的问题并非大模型的能力边界,而是类型漂移。大模型返回的 JSON 结构随时可能缺失字段或改变类型,如果缺乏编译期的约束,运行时的 undefined 报错将防不胜防。
本文将探讨如何利用 TypeScript 泛型、Zod 与 JSON Schema 的组合,打造从函数定义、Schema 生成到响应解析的端到端类型安全工具链。
一、函数定义的类型约束
OpenAI 的 functions 参数接受一个 JSON Schema 数组,而前端业务逻辑依赖具体的 TypeScript Interface。重复定义是万恶之源。
反例:类型与 Schema 割裂
// 业务类型
interface WeatherArgs {
city: string;
unit?: 'celsius' | 'fahrenheit';
}
// 手写 JSON Schema,与 WeatherArgs 无强绑定
const weatherSchema = {
type: 'object',
properties: {
city: { type: 'string' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
},
required: ['city']
};
一旦 WeatherArgs 增加了 country 字段,weatherSchema 忘记同步,大模型将永远无法返回该字段。
正例:Zod 统一数据源
使用 Zod 定义 Schema,同时推导出 TS 类型,确保单一数据源(Single Source of Truth)。
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const WeatherArgsSchema = z.object({
city: z.string().describe('城市名称'),
unit: z.enum(['celsius', 'fahrenheit']).optional().describe('温度单位')
});
// 自动推导类型,与 Schema 强绑定
type WeatherArgs = z.infer<typeof WeatherArgsSchema>;
// 转换为 OpenAI 兼容的 JSON Schema
const weatherJsonSchema = zodToJsonSchema(WeatherArgsSchema);
二、工具注册表的泛型封装
我们需要一个注册表来管理所有的工具,并确保工具的执行函数与其参数类型严格对应。
反例:Any 贯穿始终
const tools = {
get_weather: {
schema: weatherJsonSchema,
// args 为 any,毫无类型提示与保护
execute: async (args) => {
return `${args.city} 25°C`;
}
}
};
正例:泛型与映射类型构建注册表
type ToolDefinition<T extends z.ZodType> = {
schema: Record<string, any>;
execute: (args: z.infer<T>) => Promise<string>;
};
type ToolRegistry = Record<string, ToolDefinition<any>>;
// 类型安全的工具创建工厂
function createTool<T extends z.ZodType>(
schema: T,
execute: (args: z.infer<T>) => Promise<string>
): ToolDefinition<T> {
return {
schema: zodToJsonSchema(schema),
execute
};
}
const registry: ToolRegistry = {
get_weather: createTool(WeatherArgsSchema, async (args) => {
// args 具备完整类型提示: { city: string, unit?: 'celsius' | 'fahrenheit' }
return `${args.city} 当前温度 25${args.unit === 'fahrenheit' ? 'F' : 'C'}`;
})
};
三、响应解析与安全校验
大模型返回的 function_call.arguments 是一个 JSON 字符串,直接 JSON.parse 是极其危险的。
反例:信任大模型的输出
const response = await openai.chat.completions.create({...});
const fnCall = response.choices[0].message.function_call;
// 致命错误:大模型可能返回不符合预期的 JSON,或者缺少必要字段
const args = JSON.parse(fnCall.arguments) as WeatherArgs;
console.log(args.city.toUpperCase()); // 运行时可能抛出 TypeError
as 只是类型断言,不会在运行时做任何校验。
正例:Zod 运行时校验与类型收窄
async function handleToolCall(name: string, argsStr: string) {
const tool = registry[name];
if (!tool) throw new Error(`Unknown tool: ${name}`);
// 1. 安全解析 JSON
let rawArgs: unknown;
try {
rawArgs = JSON.parse(argsStr);
} catch (e) {
return { error: 'Invalid JSON format' };
}
// 2. 运行时校验 + 编译期类型收窄
const parsed = WeatherArgsSchema.safeParse(rawArgs);
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
// 3. 此时 parsed.data 是严格符合类型的,安全执行
return tool.execute(parsed.data);
}
四、端到端类型安全的调用流
将上述模块组合,构建一个对 OpenAI API 的类型安全包装器,实现请求构建与响应处理的闭环。
import OpenAI from 'openai';
const client = new OpenAI();
async function typeSafeChatCompletion(messages: OpenAI.ChatCompletionMessageParam[]) {
// 1. 从注册表提取 OpenAI 需要的 functions 参数
const functions = Object.entries(registry).map(([name, tool]) => ({
name,
...tool.schema
}));
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
functions
});
const message = response.choices[0].message;
// 2. 检查是否触发工具调用
if (message.function_call) {
const { name, arguments: argsStr } = message.function_call;
const result = await handleToolCall(name, argsStr);
// 将工具结果回传大模型
messages.push(message);
messages.push({
role: 'function',
name,
content: JSON.stringify(result)
});
return typeSafeChatCompletion(messages); // 递归调用
}
return message.content;
}
总结
在 ChatGPT 工具调用架构中,TypeScript 绝不仅限于在 IDE 中提供提示,它必须与运行时校验库(如 Zod)深度结合,形成防御性编程的闭环。
核心原则如下:
- 消除重复定义:使用 Zod 作为单一数据源,同时生成 JSON Schema 与 TS 类型。
- 拒绝类型断言:永远不要用
as处理大模型返回的 JSON,必须经过safeParse。 - 泛型约束执行:通过泛型工厂模式,确保注册的工具执行函数与参数类型在编译期强绑定。
通过这种架构,大模型的幻觉或格式错误将被拦截在 safeParse 阶段,前端业务逻辑可以完全信任校验后的数据类型,从而大幅降低 AI 应用在生产环境的崩溃率。
评论区
登录 后参与评论