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

TypeScript 驱动的 ChatGPT 流式响应架构:从类型安全到函数调用

1. 请求与响应的类型约束:告别隐式 any

在前端接入 ChatGPT API 时,最常见的陷阱是将大模型的输入输出定义为 any。大模型的 Response 结构复杂,缺乏类型约束不仅导致补全失效,更会在处理 finish_reasontool_calls 时埋下隐患。

反例:缺乏约束的请求封装

const createChatCompletion = async (messages: any[]) => {
  const response = await fetch('/v1/chat/completions', {
    method: 'POST',
    body: JSON.stringify({ model: 'gpt-4', messages }),
  });
  const data: any = await response.json();
  return data.choices[0].message.content; // 运行时一旦结构变化,直接抛异常
};

正例:基于泛型与完整类型定义的约束

interface ChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  tool_calls?: ToolCall[];
  tool_call_id?: string;
}

interface ChatCompletionResponse {
  id: string;
  choices: {
    index: number;
    message: ChatMessage;
    finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter';
  }[];
}

const createChatCompletion = async (
  messages: ChatMessage[]
): Promise<ChatCompletionResponse> => {
  const response = await fetch('/v1/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: 'gpt-4', messages }),
  });
  return response.json() as Promise<ChatCompletionResponse>;
};

2. 流式响应(SSE)的类型推导:AsyncGenerator 的正确姿势

ChatGPT 的流式输出基于 SSE(Server-Sent Events),前端解析 stream 时极易产生类型断层。传统的 EventSource 或字符串拼接不仅难以维护,且无法精确推导每个 Chunk 的增量数据类型。

反例:粗暴的字符串拼接与类型断言

const streamChat = async (messages: ChatMessage[]) => {
  const res = await fetch('/v1/chat/completions', { /* stream: true */ });
  const reader = res.body!.getReader();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += new TextDecoder().decode(value);
    // 强行断言为 any,丢失 delta 结构的提示
    const lines = buffer.split('\n'); 
    for (const line of lines) {
      if (line.startsWith('data: ') && line !== 'data: [DONE]') {
        const data: any = JSON.parse(line.slice(6));
        process.stdout.write(data.choices[0].delta.content ?? '');
      }
    }
  }
};

正例:强类型 AsyncGenerator 封装

通过 AsyncGenerator 将流式数据的产出类型固定为 Delta,实现迭代时的类型安全。

interface StreamDelta {
  content?: string;
  tool_calls?: { index: number; id?: string; function?: { name?: string; arguments?: string } }[];
}

async function* parseSSEStream(
  stream: ReadableStream<Uint8Array>
): AsyncGenerator<StreamDelta, void, unknown> {
  const reader = stream.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop()!; // 保留未完成的行

    for (const line of lines) {
      if (line.startsWith('data: ') && line.trim() !== 'data: [DONE]') {
        const parsed = JSON.parse(line.slice(6));
        yield parsed.choices[0].delta as StreamDelta;
      }
    }
  }
}

// 消费端拥有完整的类型提示
const stream = await fetch('/v1/chat/completions', { /* ... */ });
for await (const delta of parseSSEStream(stream.body!)) {
  if (delta.content) console.log(delta.content);
  if (delta.tool_calls) console.log(delta.tool_calls[0].function!.name);
}

3. Function Calling 的类型安全闭环:Zod 与 TypeScript 的双剑合璧

ChatGPT 的 Function Calling 返回的 arguments 是 JSON 字符串,直接解析存在运行时崩溃风险。我们需要在编译期定义参数类型,在运行期使用 Zod 进行校验,形成双重保障。

反例:裸解析 JSON 字符串

const toolCall = message.tool_calls![0];
// 运行时若大模型返回格式错误,直接导致应用崩溃
const args = JSON.parse(toolCall.function.arguments) as { city: string; unit: string }; 
await fetchWeather(args.city, args.unit);

正例:Zod Schema 驱动的类型推导与校验

利用 z.infer<typeof Schema> 自动推导 TS 类型,实现声明式开发。

import { z } from 'zod';

// 1. 定义 Schema,同时获得 TS 类型与运行时校验
const WeatherArgsSchema = z.object({
  city: z.string().describe('城市名称'),
  unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});
type WeatherArgs = z.infer<typeof WeatherArgsSchema>;

// 2. 工具定义类型安全
const tools = [
  {
    type: 'function' as const,
    function: {
      name: 'get_current_weather',
      description: '获取指定城市的天气',
      parameters: WeatherArgsSchema,
    },
  },
];

// 3. 安全解析与执行
async function handleToolCall(toolCall: ToolCall) {
  if (toolCall.function.name === 'get_current_weather') {
    // Zod 安全解析,拒绝非法参数
    const parsed = WeatherArgsSchema.safeParse(
      JSON.parse(toolCall.function.arguments)
    );

    if (!parsed.success) {
      return { tool_call_id: toolCall.id, content: '参数解析失败' };
    }

    const args: WeatherArgs = parsed.data;
    const weatherData = await fetchWeather(args.city, args.unit);
    return { tool_call_id: toolCall.id, content: JSON.stringify(weatherData) };
  }
}

结语

在 ChatGPT 等 LLM 的前端集成中,大模型的不可控性要求我们在工程层面做更严格的防御。TypeScript 的静态类型检查结合 Zod 的运行时校验,不仅能有效约束大模型的输入输出边界,还能在流式解析和函数调用等复杂场景下提供坚实的代码提示与错误拦截,将大模型的“黑盒”行为纳入前端的类型安全体系之中。

0 评论

评论区

登录 后参与评论