vercel AI SDK 学习

Vercel AI SDK 完整深入教程


一、它解决什么问题?

在没有 AI SDK 之前,你要:

  1. 自己学每家模型的 API 格式(OpenAI、Anthropic、Google 各不相同)
  2. 自己处理流式响应(SSE 解析、ReadableStream 管道)
  3. 自己管理前端聊天状态(消息列表、loading、error、重试)
  4. 自己做格式转换(前端消息格式 <-> 模型消息格式)
  5. 切换模型 = 重写大量代码

AI SDK 做了一件事:把所有这些差异封装起来,给你一套统一的 API。换模型 = 改一个参数。


二、三个包,各自干什么

项目中的三类包:

python 复制代码
ai                    → 核心包 (AI SDK Core)
@ai-sdk/openai        → 模型提供商适配 (Provider)
@ai-sdk/react         → 前端 UI 集成 (AI SDK UI)

2.1 ai --- Core 核心包

这是后端用的,提供调用模型的统一函数:

函数 / 对象 作用
generateText() 一次性生成文本
streamText() 流式生成文本(也支持结构化输出)
Output.object() 配合 streamText/generateText 做结构化 JSON 输出
Output.array() 配合 streamText/generateText 做结构化数组输出
tool() 定义工具(让AI调用你的函数)
stepCountIs() 定义多步执行的停止条件
convertToModelMessages() 把 UI 消息转成模型能理解的格式

注意 :v6 中 generateObject() / streamObject() 已弃用,改用 streamText + output: Output.object() 代替。

关键理解 :这个包与任何前端框架无关,它只做"调用模型"这件事。你可以在 Node.js CLI、Express、Next.js API Route 里用它。

2.2 @ai-sdk/openai --- Provider 适配包

它实现了一个翻译层:把 AI SDK 的统一调用格式,翻译成 OpenAI API 的请求格式;再把 OpenAI 的响应翻译回统一格式。

markdown 复制代码
你的代码 → AI SDK 统一格式 → Provider 翻译 → OpenAI API
                                               ↓
你的代码 ← AI SDK 统一格式 ← Provider 翻译 ← OpenAI 响应

因为 DeepSeek 兼容 OpenAI 格式,所以用 createOpenAI({ baseURL: '...' }) 就能接入。

换模型只需改这里

ts 复制代码
// 用 OpenAI
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-4o');

// 用 Google
import { google } from '@ai-sdk/google';
const model = google('gemini-pro');

// 用 Anthropic
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-3-sonnet');

// 下面的代码完全不用改
const result = await generateText({ model, prompt: '你好' });

2.3 @ai-sdk/react --- 前端 UI 包

提供 React Hooks(useChatuseObject 等),封装了:

  • 消息列表状态管理
  • 自动发 HTTP 请求到你的 API Route
  • 自动解析 SSE 流式响应
  • loading/error/streaming 状态
  • 中断生成、重新生成等操作

没有它你需要手写的代码量

ts 复制代码
// 没有 useChat,你需要自己做这些:
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

async function sendMessage(text) {
  setIsLoading(true);
  setMessages(prev => [...prev, { role: 'user', content: text }]);
  
  try {
    const res = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ messages: [...messages, { role: 'user', content: text }] })
    });
    
    // 手动解析 SSE 流
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let aiMessage = '';
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value);
      // 解析 SSE 格式...
      // 更新消息状态...
      aiMessage += parseSSE(chunk);
      setMessages(prev => {
        const newMessages = [...prev];
        // 更新最后一条AI消息...
        return newMessages;
      });
    }
  } catch (e) {
    setError(e);
  } finally {
    setIsLoading(false);
  }
}

有了 useChat,上面的全部简化为

ts 复制代码
const { messages, sendMessage, status } = useChat();

三、generateText 详解

最基础的用法

ts 复制代码
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

const { text, usage, finishReason } = await generateText({
  model: openai('gpt-4o'),
  prompt: '什么是量子计算?'
});

这个函数是同步等待的 (准确说是 await 一个 Promise)。模型生成完所有内容后,才返回结果。

返回值详解

ts 复制代码
const result = await generateText({ model, prompt });

result.text           // 生成的文本内容(string)
result.usage          // Token 使用量
  .promptTokens       //   输入消耗的 token
  .completionTokens   //   输出消耗的 token
  .totalTokens        //   总计
result.finishReason   // 为什么停止了?
  // 'stop' = 模型自然结束
  // 'length' = 到达 maxTokens 限制
  // 'tool-calls' = 模型想调用工具
result.toolCalls      // 如果模型调用了工具,这里有工具调用信息
result.toolResults    // 工具执行的结果
result.steps          // 多步执行时,每一步的详细记录

完整参数

ts 复制代码
await generateText({
  // 必填
  model: openai('gpt-4o'),        // 用哪个模型

  // 输入(三选一)
  prompt: '单条提示词',             // 方式1:简单提示
  messages: [...],                  // 方式2:多轮对话
  system: '你是一个助手',           // 系统指令(可与上面任一搭配)

  // 可选参数
  temperature: 0.7,                 // 0-2,越高越随机/有创意
  maxOutputTokens: 1000,            // 最大输出 token 数
  topP: 0.9,                        // 核采样概率
  frequencyPenalty: 0.5,            // 频率惩罚,减少重复
  presencePenalty: 0.5,             // 存在惩罚,鼓励新话题
  stopSequences: ['END'],           // 遇到这些文本就停止
  seed: 42,                         // 随机种子(尽量保证可复现)

  // 工具相关
  tools: { ... },                   // 可用工具定义
  stopWhen: stepCountIs(5),         // 多步执行时最大步数(需导入 stepCountIs)

  // 生命周期回调
  onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage }) => { },
});

messages 格式

ts 复制代码
await generateText({
  model,
  messages: [
    // system: 设定AI的行为规则
    { role: 'system', content: '你是一个专业的技术文档作者' },

    // user: 用户说的话
    { role: 'user', content: '解释一下 React 的 useState' },

    // assistant: AI 之前的回复(多轮对话需要传历史)
    { role: 'assistant', content: 'useState 是...' },

    // user: 用户继续追问
    { role: 'user', content: '能举个例子吗?' },
  ]
});

为什么要传历史消息? 因为大模型本身是无状态的。每次请求都是独立的。你需要把之前的对话全部传过去,模型才能"记住"上下文。useChat 自动帮你做了这件事。

实际返回值结构示例

以下是 generateText 返回的 result 对象的真实结构:

ts 复制代码
// 场景1:简单文本生成(无工具)
const result = await generateText({
  model: openai('gpt-4o'),
  prompt: '用一句话解释量子计算',
});

// result 的实际值:
{
  text: "量子计算是利用量子力学原理(如叠加和纠缠)来处理信息的计算方式,能在特定问题上远超传统计算机。",
  finishReason: "stop",
  usage: {
    promptTokens: 12,
    completionTokens: 45,
    totalTokens: 57,
  },
  toolCalls: [],      // 没有工具调用
  toolResults: [],    // 没有工具结果
  steps: [            // 只有一步
    {
      text: "量子计算是利用量子力学原理...",
      finishReason: "stop",
      usage: { promptTokens: 12, completionTokens: 45, totalTokens: 57 },
      toolCalls: [],
      toolResults: [],
    }
  ],
  response: {
    id: "chatcmpl-abc123",
    modelId: "gpt-4o-2024-08-06",
  },
}
ts 复制代码
// 场景2:带工具调用 + 多步执行
import { stepCountIs } from 'ai';

const result = await generateText({
  model: openai('gpt-4o'),
  tools: { getWeather: weatherTool },
  stopWhen: stepCountIs(3),
  prompt: '北京天气怎么样?',
});

// result 的实际值:
{
  text: "北京今天25°C,晴天,适合外出活动。",
  finishReason: "stop",
  usage: {                    // 所有步骤的总用量
    promptTokens: 85,
    completionTokens: 62,
    totalTokens: 147,
  },
  toolCalls: [],              // 最后一步的工具调用(最后一步是文本,所以为空)
  toolResults: [],
  steps: [
    // Step 1: 模型决定调用工具
    {
      text: "",               // 这一步没有生成文本
      finishReason: "tool-calls",
      usage: { promptTokens: 30, completionTokens: 18, totalTokens: 48 },
      toolCalls: [
        {
          toolCallId: "call_abc123",
          toolName: "getWeather",
          args: { city: "北京", unit: "celsius" },
        }
      ],
      toolResults: [
        {
          toolCallId: "call_abc123",
          toolName: "getWeather",
          args: { city: "北京", unit: "celsius" },
          result: { city: "北京", temperature: 25, condition: "晴" },
        }
      ],
    },
    // Step 2: 模型根据工具结果生成回答
    {
      text: "北京今天25°C,晴天,适合外出活动。",
      finishReason: "stop",
      usage: { promptTokens: 55, completionTokens: 44, totalTokens: 99 },
      toolCalls: [],
      toolResults: [],
    },
  ],
}

什么时候用 generateText?

  • 后台批处理(翻译100篇文档)
  • 需要完整结果后再处理(提取JSON、做判断)
  • CLI 工具
  • 不需要实时展示的场景

四、streamText 详解

和 generateText 的本质区别

makefile 复制代码
generateText:
  请求 ──────────── 等待3秒 ────────────→ 一次返回全部文本

streamText:
  请求 → "你" → "好" → "!" → "有" → "什" → "么" → "可以" → "帮你" → 结束
         ↑     ↑     ↑     ↑     ↑     ↑      ↑       ↑
       每个小块都立即到达,前端可以实时显示

基础用法

ts 复制代码
import { streamText } from 'ai';

const result = streamText({
  model: openai('gpt-4o'),
  prompt: '写一首诗',
});

// 注意:streamText 不需要 await(它立即返回一个 result 对象)
// 但读取流需要 for await
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);  // 实时输出每个文本块
}

在 Next.js API Route 里用

ts 复制代码
export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: deepseek.chat("deepseek-chat"),
    messages: await convertToModelMessages(messages),
  });

  // 转成前端 useChat 能解析的 SSE 流
  return result.toUIMessageStreamResponse();
}

toUIMessageStreamResponse() 做了什么:

  1. 创建一个 HTTP Response,Content-Type 是 text/event-stream(SSE)
  2. 把模型的每个输出块包装成 AI SDK 定义的协议格式
  3. 前端的 useChat 知道如何解析这种格式

流的回调

ts 复制代码
const result = streamText({
  model,
  messages,
  
  // 每收到一个数据块
  onChunk: ({ chunk }) => {
    // chunk.type 可能是 'text-delta', 'tool-call', 'tool-result' 等
    if (chunk.type === 'text-delta') {
      console.log('收到文本块:', chunk.textDelta);
    }
  },

  // 一步完成时(多步执行才会触发多次)
  onStepFinish: ({ text, toolCalls, toolResults, usage }) => {
    console.log('这一步生成了:', text);
    console.log('使用了', usage.totalTokens, 'tokens');
  },

  // 全部完成时
  onFinish: ({ text, usage, finishReason, steps }) => {
    // 在这里保存到数据库
    console.log('总共使用:', usage.totalTokens, 'tokens');
    console.log('完整文本:', text);
  },
});

streamText 的 result 对象

streamText 返回的不是最终数据,而是一个包含多个流的对象

ts 复制代码
const result = streamText({
  model: openai('gpt-4o'),
  prompt: '写一首诗',
});

// result 是一个对象,包含多个属性和流:

// 1. textStream --- 只有文本块的流
for await (const chunk of result.textStream) {
  console.log(chunk);
  // 第1次: "春"
  // 第2次: "风"
  // 第3次: "拂"
  // 第4次: "面"
  // ...每次一小段文字
}

// 2. fullStream --- 包含所有类型事件的流(文本、工具调用、完成信号等)
for await (const part of result.fullStream) {
  console.log(part);
  // { type: "text-delta", textDelta: "春" }
  // { type: "text-delta", textDelta: "风拂面" }
  // { type: "step-finish", usage: {...}, finishReason: "stop" }
  // { type: "finish", usage: {...} }
}

// 3. 等待最终结果的 Promise(流结束后可用)
const finalText = await result.text;      // 完整文本
const finalUsage = await result.usage;    // 总 token 用量
const finalReason = await result.finishReason; // 停止原因
const finalSteps = await result.steps;    // 所有步骤

// finalUsage 的值:
// { promptTokens: 8, completionTokens: 52, totalTokens: 60 }

// 4. 转成 HTTP 响应(在 API Route 里用)
return result.toUIMessageStreamResponse();
// 或
return result.toDataStreamResponse();

fullStream 各事件类型示例

ts 复制代码
for await (const part of result.fullStream) {
  switch (part.type) {
    case 'text-delta':
      // 文本增量
      // { type: "text-delta", textDelta: "你好" }
      break;

    case 'tool-call':
      // 模型发起工具调用
      // {
      //   type: "tool-call",
      //   toolCallId: "call_abc123",
      //   toolName: "getWeather",
      //   args: { city: "北京" }
      // }
      break;

    case 'tool-result':
      // 工具执行完毕
      // {
      //   type: "tool-result",
      //   toolCallId: "call_abc123",
      //   toolName: "getWeather",
      //   result: { temperature: 25, condition: "晴" }
      // }
      break;

    case 'step-finish':
      // 一步完成
      // {
      //   type: "step-finish",
      //   finishReason: "tool-calls",  // 或 "stop"
      //   usage: { promptTokens: 30, completionTokens: 18, totalTokens: 48 }
      // }
      break;

    case 'finish':
      // 全部完成
      // {
      //   type: "finish",
      //   finishReason: "stop",
      //   usage: { promptTokens: 85, completionTokens: 62, totalTokens: 147 }
      // }
      break;

    case 'error':
      // 出错
      // { type: "error", error: Error }
      break;
  }
}

什么时候用 streamText?

  • 聊天界面
  • 任何需要实时显示的场景
  • 用户需要看到"AI正在思考"的场景

五、结构化输出详解(v6:streamText + Output

注意 :v6 中 generateObject() / streamObject() 已弃用,统一使用 streamText / generateText + output 参数。

为什么需要结构化输出?

假设你让AI "提取这段文本中的人名和年龄":

没有结构化输出

arduino 复制代码
AI 回复: "这段文本中提到了张三,他今年28岁,还有李四,25岁。"
// 你需要用正则或其他方法从文本中提取数据

有结构化输出

json 复制代码
{
  "people": [
    { "name": "张三", "age": 28 },
    { "name": "李四", "age": 25 }
  ]
}
// 直接就是 JSON 对象,可以直接用

使用 Zod 定义 Schema + Output.object()

AI SDK v6 使用 Zod 定义数据结构,配合 Output.object() 实现结构化输出。

ts 复制代码
import { z } from 'zod';
import { generateText, Output } from 'ai';

const result = await generateText({
  model: openai('gpt-4o'),
  output: Output.object({
    schema: z.object({
      recipe: z.object({
        name: z.string().describe('菜品名称'),
        ingredients: z.array(
          z.object({
            name: z.string(),
            amount: z.string(),
          })
        ).describe('食材列表'),
        steps: z.array(z.string()).describe('步骤'),
        difficulty: z.enum(['简单', '中等', '困难']),
      }),
    }),
  }),
  prompt: '给我一个宫保鸡丁的菜谱',
});

// result.output 是完全类型安全的
console.log(result.output.recipe.name);           // string
console.log(result.output.recipe.ingredients[0]); // { name: string, amount: string }
console.log(result.output.recipe.difficulty);      // '简单' | '中等' | '困难'

.describe() 的作用:它会被转换成 JSON Schema 的 description 字段,帮助模型理解这个字段应该填什么。

流式结构化输出

ts 复制代码
import { streamText, Output } from 'ai';

const result = streamText({
  model: openai('gpt-4o'),
  output: Output.object({ schema: recipeSchema }),
  prompt: '宫保鸡丁菜谱',
});

// 方式1:流式文本(JSON 逐步生成)
return result.toTextStreamResponse();

// 方式2:等待完整结构化对象
const output = await result.output;  // 类型安全的完整对象

generateText + Output.object() 返回值结构示例

ts 复制代码
const result = await generateText({
  model: openai('gpt-4o'),
  output: Output.object({
    schema: z.object({
      recipe: z.object({
        name: z.string(),
        ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
        steps: z.array(z.string()),
        difficulty: z.enum(['简单', '中等', '困难']),
      }),
    }),
  }),
  prompt: '给我一个番茄炒蛋的菜谱',
});

// result.output 的实际值:
{
  recipe: {
    name: "番茄炒蛋",
    ingredients: [
      { name: "鸡蛋", amount: "3个" },
      { name: "番茄", amount: "2个" },
      { name: "葱", amount: "适量" },
      { name: "盐", amount: "少许" },
      { name: "糖", amount: "少许" },
      { name: "食用油", amount: "适量" },
    ],
    steps: [
      "鸡蛋打散,加少许盐搅匀",
      "番茄切块备用",
      "热锅倒油,倒入蛋液炒至凝固盛出",
      "锅中再加少许油,放入番茄翻炒出汁",
      "加入糖和盐调味",
      "倒回鸡蛋翻炒均匀",
      "撒上葱花即可出锅",
    ],
    difficulty: "简单",
  },
}

// 使用(完全类型安全):
result.output.recipe.name           // "番茄炒蛋" (string)
result.output.recipe.ingredients[0] // { name: "鸡蛋", amount: "3个" }
result.output.recipe.difficulty     // "简单" (枚举值)

streamText + Output.object() 的流式过程示例

ts 复制代码
const result = streamText({
  model: openai('gpt-4o'),
  output: Output.object({
    schema: z.object({
      recipe: z.object({
        name: z.string(),
        ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
        steps: z.array(z.string()),
      }),
    }),
  }),
  prompt: '番茄炒蛋菜谱',
});

// textStream 流式输出 JSON 文本:
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}
// 逐步输出:{"recipe":{"name":"番茄炒蛋","ingredients":[{"name":"鸡蛋"...

// 等待最终完整对象(类型安全):
const output = await result.output;
// output.recipe.name  → "番茄炒蛋"
// output.recipe.ingredients → [...]

const usage = await result.usage;
// { promptTokens: 45, completionTokens: 180, totalTokens: 225 }

Output 的几种模式

ts 复制代码
import { Output } from 'ai';

// 1. Output.object() --- 返回一个结构化对象
const result = await generateText({
  output: Output.object({
    schema: z.object({ ... }),
  }),
  prompt: '...',
});

// 2. Output.array() --- 返回数组
const result = await generateText({
  output: Output.array({
    element: z.object({ city: z.string(), country: z.string() }),
  }),
  prompt: '列出欧洲5个旅游城市',
});
// result.output = [{ city: "巴黎", country: "法国" }, ...]

// 3. Output.choice() --- 返回枚举值(分类任务)
const result = await generateText({
  output: Output.choice({
    options: ['action', 'comedy', 'drama', 'horror', 'sci-fi'],
  }),
  prompt: '分类这部电影:一群宇航员穿越虫洞...',
});
// result.output = "sci-fi"

六、Tool Calling(工具调用)详解

核心思想

模型本身不能 直接访问互联网、数据库或任何外部系统。但它可以请求调用你预先定义好的函数。

css 复制代码
用户: "北京今天天气怎么样?"
        ↓
模型思考: "我需要天气数据,我应该调用 getWeather 工具"
        ↓
模型输出: { toolCall: "getWeather", args: { city: "北京" } }
        ↓
AI SDK: 拿到工具调用请求 → 执行你定义的 execute 函数
        ↓
你的代码: 调用真实的天气 API → 返回 { temp: 25, condition: "晴" }
        ↓
AI SDK: 把工具结果传回模型
        ↓
模型: "北京今天25度,晴天,适合出门。"

定义工具

ts 复制代码
import { tool } from 'ai';
import { z } from 'zod';

const weatherTool = tool({
  // description 告诉模型这个工具是做什么的
  // 模型根据 description 决定什么时候调用它
  description: '获取指定城市的实时天气信息',

  // inputSchema 定义这个工具接收什么参数(v6 中 parameters 已改为 inputSchema)
  // 模型会根据用户的问题,自动填充这些参数
  inputSchema: z.object({
    city: z.string().describe('城市名称,如"北京"'),
    unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
  }),

  // execute 是你的实际业务逻辑
  // 当模型决定调用这个工具时,SDK 会自动执行这个函数
  execute: async ({ city, unit }) => {
    // 这里可以调用任何东西:API、数据库、文件系统...
    const response = await fetch(`https://api.weather.com/${city}`);
    const data = await response.json();
    return {
      city,
      temperature: data.temp,
      condition: data.condition,
      unit,
    };
  },
});

使用工具

ts 复制代码
const result = await generateText({
  model: openai('gpt-4o'),
  tools: {
    getWeather: weatherTool,
    searchWeb: anotherTool,
    queryDB: yetAnotherTool,
  },
  prompt: '北京今天天气如何?需要带伞吗?',
});

关键点

  • 你可以提供多个工具,模型自主决定调用哪个(或不调用)
  • 模型可能一次调用多个工具(并行工具调用)
  • 工具的 description 和参数的 .describe() 非常重要,它们是模型理解工具用途的唯一依据

七、Multi-Step / stopWhen(多步执行)详解

注意 :v6 中 maxSteps 已弃用,改用 stopWhen: stepCountIs(n)

单步 vs 多步

单步(默认,stepCountIs(1):模型只执行一次推理

复制代码
用户问题 → 模型回答(或调用一个工具)→ 结束

多步(设置 stopWhen:模型可以连续推理多次

css 复制代码
用户: "比较北京和上海的天气"
  → Step 1: 模型调用 getWeather({ city: "北京" }) → { temp: 25 }
  → Step 2: 模型调用 getWeather({ city: "上海" }) → { temp: 30 }
  → Step 3: 模型生成文本 "上海30度比北京25度高5度"
  → 结束(因为模型生成了文本而非工具调用)

代码

ts 复制代码
import { stepCountIs } from 'ai';

const result = await generateText({
  model: openai('gpt-4o'),
  tools: { getWeather, searchWeb, calculate },
  stopWhen: stepCountIs(5),  // 最多执行5步
  prompt: '比较北京和上海今天的温度差',
});

// result.steps 记录了每一步的信息
result.steps.forEach((step, i) => {
  console.log(`Step ${i + 1}:`);
  console.log('  finishReason:', step.finishReason);
  console.log('  工具调用:', step.toolCalls);
  console.log('  工具结果:', step.toolResults);
  console.log('  文本:', step.text);
  console.log('  Token用量:', step.usage);
});

停止条件

多步执行在以下情况停止:

  1. 达到 stepCountIs(n) 限制
  2. 模型生成了文本(而不是工具调用)
  3. 模型没有调用任何工具
ts 复制代码
import { stepCountIs, hasToolCall } from 'ai';

const result = await generateText({
  model,
  tools,
  stopWhen: [
    stepCountIs(5),              // 最多5步
    hasToolCall('finalAnswer'),  // 或者调用了 finalAnswer 工具
  ],
  prompt: '...',
});

这就是 Agent 的核心机制:给模型一组工具 + 允许多步执行 = 模型自主规划和执行任务。


八、useChat 详解

它到底做了什么

useChat 本质上是一个状态机

scss 复制代码
           sendMessage()
idle ──────────────────→ submitted
                            │
                            │ 收到第一个响应块
                            ▼
                         streaming ──── 收到更多块 ──→ 更新 messages
                            │
                            │ 流结束
                            ▼
                          idle(或 ready)

任何阶段出错 ──→ error

完整 API

ts 复制代码
const {
  // 状态
  messages,        // UIMessage[] --- 完整对话历史
  status,          // 'idle' | 'submitted' | 'streaming' | 'error' | 'ready'
  error,           // Error | null --- 错误信息

  // 操作
  sendMessage,     // (msg) => void --- 发送消息
  stop,            // () => void --- 中断当前生成
  reload,          // () => void --- 重新生成最后一条AI回复
  setMessages,     // (msgs) => void --- 手动设置消息列表

  // 输入管理(可选,你也可以自己用 useState)
  input,           // string --- 输入框内容
  setInput,        // (text) => void
  handleInputChange, // (e) => void --- 直接绑定 onChange
  handleSubmit,    // (e) => void --- 直接绑定 onSubmit
} = useChat({
  // 配置项
  api: '/api/chat',           // 后端 API 地址(默认 /api/chat)
  initialMessages: [],        // 初始消息
  id: 'unique-chat-id',       // 聊天会话 ID(多个聊天时区分)

  // 回调
  onFinish: (message) => {    // 一条AI消息完成时
    console.log('完成:', message);
  },
  onError: (error) => {       // 出错时
    console.error('错误:', error);
  },
});

UIMessage 的结构

ts 复制代码
interface UIMessage {
  id: string;           // 消息唯一 ID
  role: 'user' | 'assistant';
  parts: MessagePart[]; // 消息内容(可以有多种类型)
}

// parts 的类型
type MessagePart =
  | { type: 'text'; text: string }          // 文本
  | { type: 'tool-call'; toolName: string; args: any }  // 工具调用
  | { type: 'tool-result'; result: any }    // 工具结果
  | { type: 'reasoning'; text: string }     // 推理过程
  | { type: 'file'; url: string }           // 文件
实际 messages 数组示例

一段典型对话中,messages 数组的真实值:

ts 复制代码
// 场景:用户问天气,AI 调用了工具后回答
const messages = [
  // 第1条:用户消息
  {
    id: "msg-user-1",
    role: "user",
    parts: [
      { type: "text", text: "北京今天天气怎么样?" }
    ],
  },
  // 第2条:AI 回复(包含工具调用 + 最终文本)
  {
    id: "msg-assistant-1",
    role: "assistant",
    parts: [
      // Part 1: AI 先调用了工具
      {
        type: "tool-call",
        toolCallId: "call_abc123",
        toolName: "getWeather",
        args: { city: "北京" },
      },
      // Part 2: 工具返回了结果
      {
        type: "tool-result",
        toolCallId: "call_abc123",
        toolName: "getWeather",
        result: { city: "北京", temperature: 25, condition: "晴" },
      },
      // Part 3: AI 根据工具结果生成的文本回复
      {
        type: "text",
        text: "北京今天天气不错,25°C 晴天,很适合外出活动!",
      }
    ],
  },
  // 第3条:用户继续追问
  {
    id: "msg-user-2",
    role: "user",
    parts: [
      { type: "text", text: "需要带伞吗?" }
    ],
  },
  // 第4条:AI 直接回答(不需要工具)
  {
    id: "msg-assistant-2",
    role: "assistant",
    parts: [
      {
        type: "text",
        text: "不需要带伞,今天是晴天,降水概率很低。",
      }
    ],
  },
];
简单对话(纯文本,无工具)
ts 复制代码
// 最常见的场景:纯文字聊天
const messages = [
  {
    id: "msg-1",
    role: "user",
    parts: [{ type: "text", text: "你好" }],
  },
  {
    id: "msg-2",
    role: "assistant",
    parts: [{ type: "text", text: "你好!有什么可以帮你的吗?" }],
  },
  {
    id: "msg-3",
    role: "user",
    parts: [{ type: "text", text: "解释一下什么是 TypeScript" }],
  },
  {
    id: "msg-4",
    role: "assistant",
    parts: [{
      type: "text",
      text: "TypeScript 是 JavaScript 的超集,它添加了静态类型检查...",
    }],
  },
];

所以在前端渲染消息时需要遍历 parts

tsx 复制代码
{message.parts.map((part, i) => {
  switch (part.type) {
    case 'text':
      return <p key={i}>{part.text}</p>;
    case 'tool-call':
      return <div key={i}>正在调用 {part.toolName}...</div>;
    case 'tool-result':
      return <div key={i}>结果: {JSON.stringify(part.result)}</div>;
  }
})}

UIMessage vs ModelMessage

这是 AI SDK 一个重要的设计决策:

css 复制代码
UIMessage(前端用)                ModelMessage(发给模型)
┌──────────────────┐               ┌───────────────────┐
│ id: "msg-1"      │               │ role: "user"      │
│ role: "user"     │    转换        │ content: "你好"   │
│ parts: [{        │ ────────→     └───────────────────┘
│   type: "text",  │  convertToModelMessages()
│   text: "你好"   │
│ }]               │
│ metadata: {...}  │  ← 元数据不发给模型
└──────────────────┘

UIMessage 是给你的应用用的,可以包含元数据、时间戳等;ModelMessage 是精简版,只包含模型需要的信息。

这就是后端代码中 convertToModelMessages(messages) 的作用。


九、前后端通信的完整流程

scss 复制代码
┌─────────────────── 前端 ───────────────────┐
│                                             │
│   useChat()                                 │
│     │                                       │
│     │ 1. 用户点发送                          │
│     │ 2. 把用户消息加入 messages              │
│     │ 3. status → 'submitted'               │
│     │ 4. POST /api/chat                     │
│     │    body: { messages: UIMessage[] }     │
│     │                                       │
└─────│───────────────────────────────────────┘
      │
      ▼ HTTP POST
┌─────────────────── 后端 ───────────────────┐
│                                             │
│   route.ts                                  │
│     │                                       │
│     │ 5. 解析 messages                      │
│     │ 6. convertToModelMessages()           │
│     │ 7. streamText({ model, messages })    │
│     │    → SDK 调用 DeepSeek API            │
│     │                                       │
│     │ 8. toUIMessageStreamResponse()        │
│     │    → 把模型输出包装成 SSE 流           │
│     │                                       │
└─────│───────────────────────────────────────┘
      │
      ▼ SSE 流式响应
┌─────────────────── 前端 ───────────────────┐
│                                             │
│   useChat() 内部                            │
│     │                                       │
│     │ 9.  status → 'streaming'              │
│     │ 10. 解析每个 SSE 事件                  │
│     │ 11. 实时更新 messages 数组             │
│     │     (AI 消息内容逐字增长)             │
│     │ 12. React 重新渲染 → 用户看到打字效果  │
│     │                                       │
│     │ 流结束                                 │
│     │ 13. status → 'idle'/'ready'           │
│     │ 14. 触发 onFinish 回调                 │
│                                             │
└─────────────────────────────────────────────┘

十、实际的 SSE 协议长什么样

打开浏览器 DevTools → Network → 找到 /api/chat 请求 → EventStream 标签页,你会看到类似:

vbnet 复制代码
event: message-start
data: {"messageId":"msg-1"}

event: text-delta
data: {"textDelta":"你"}

event: text-delta
data: {"textDelta":"好"}

event: text-delta
data: {"textDelta":"!"}

event: step-finish
data: {"usage":{"promptTokens":20,"completionTokens":5}}

event: message-finish
data: {"finishReason":"stop"}

useChat 就是在解析这些事件,把 textDelta 拼接起来更新消息。


十一、Settings / 通用参数详解

这些参数 generateTextstreamText 都支持:

参数 类型 作用 默认值
temperature 0-2 控制随机性。0=确定性,1=正常,2=非常随机 模型默认值
maxOutputTokens number 最大输出长度(token数) 模型默认值
topP 0-1 核采样。0.1=只考虑概率最高的10%候选词 模型默认值
frequencyPenalty -2到2 惩罚重复出现的词,减少复读 0
presencePenalty -2到2 鼓励模型谈论新话题 0
seed number 尽量固定随机性(不保证完全一致) 随机
maxRetries number 网络错误时自动重试次数 2
abortSignal AbortSignal 允许外部取消请求 -
headers object 自定义 HTTP 头 -

相关推荐
mCell9 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell10 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭10 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清10 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木10 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_6070766010 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声10 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易10 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得011 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion11 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计