前端开发··2 阅读·预计 11 分钟

构建类型安全的 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)深度结合,形成防御性编程的闭环。

核心原则如下:

  1. 消除重复定义:使用 Zod 作为单一数据源,同时生成 JSON Schema 与 TS 类型。
  2. 拒绝类型断言:永远不要用 as 处理大模型返回的 JSON,必须经过 safeParse
  3. 泛型约束执行:通过泛型工厂模式,确保注册的工具执行函数与参数类型在编译期强绑定。

通过这种架构,大模型的幻觉或格式错误将被拦截在 safeParse 阶段,前端业务逻辑可以完全信任校验后的数据类型,从而大幅降低 AI 应用在生产环境的崩溃率。

0 评论

评论区

登录 后参与评论