Vercel AI SDK 完整深入教程
- 官方文档:ai-sdk.dev/docs
- GitHub 仓库:github.com/vercel/ai
一、它解决什么问题?
在没有 AI SDK 之前,你要:
- 自己学每家模型的 API 格式(OpenAI、Anthropic、Google 各不相同)
- 自己处理流式响应(SSE 解析、ReadableStream 管道)
- 自己管理前端聊天状态(消息列表、loading、error、重试)
- 自己做格式转换(前端消息格式 <-> 模型消息格式)
- 切换模型 = 重写大量代码
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(useChat、useObject 等),封装了:
- 消息列表状态管理
- 自动发 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() 做了什么:
- 创建一个 HTTP Response,Content-Type 是
text/event-stream(SSE) - 把模型的每个输出块包装成 AI SDK 定义的协议格式
- 前端的
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);
});
停止条件
多步执行在以下情况停止:
- 达到
stepCountIs(n)限制 - 模型生成了文本(而不是工具调用)
- 模型没有调用任何工具
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 / 通用参数详解
这些参数 generateText 和 streamText 都支持:
| 参数 | 类型 | 作用 | 默认值 |
|---|---|---|---|
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 头 | - |