跳到主要内容

AI SDK 6 与前端 AI 工程化

问题

如何使用 AI SDK 6 构建类型安全的 AI 应用?如何正确处理 UIMessage、SSE 流、结构化输出、工具调用、多 Provider、持久化与错误恢复?

面试速答版

AI SDK 的核心价值不是“少写一个 fetch”,而是提供三层统一抽象:

  1. AI SDK CoregenerateTextstreamTextOutputtool,统一不同模型供应商。
  2. AI SDK UIuseChatUIMessage.parts、UI Message Stream,统一聊天状态和流协议。
  3. Provider 层:把统一的消息、工具和输出描述转换成各厂商协议。

AI SDK 6 的关键变化是:结构化输出并入 generateText / streamTextoutput;前端消息使用 UIMessage.parts;聊天发送使用 sendMessage;服务端返回 toUIMessageStreamResponse();多步工具循环使用 stopWhen

版本边界

本文以 AI SDK 6 为准。旧教程中的 generateObjectstreamObjectLanguageModelV1append()Message.contenttoDataStreamResponse() 和数字前缀 DataStream 都不应继续作为新项目模板。

一、整体架构

负责什么不应该负责什么
UI输入、渲染、取消、重试、可访问性API Key、权限判断、工具执行
BFF/API Route鉴权、限流、上下文组装、模型调用信任模型输出、跳过业务校验
Provider协议转换、能力适配业务路由和用户权限
Tool确定性业务操作自己决定是否有权操作

二、最小可用聊天应用

1. 安装

npm install ai @ai-sdk/react @ai-sdk/openai zod
模型 ID

模型 ID 和能力变化很快,应放在服务端配置中心或环境变量中。下面使用 process.env.OPENAI_MODEL,避免把某个时点的“最佳模型”固化进业务代码。

2. 服务端路由

app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import {
convertToModelMessages,
streamText,
type UIMessage,
} from 'ai';

export const maxDuration = 60;

export async function POST(request: Request): Promise<Response> {
// 生产环境应先完成鉴权、限流、输入长度与预算检查。
const body = (await request.json()) as { messages: UIMessage[] };

const result = streamText({
model: openai(process.env.OPENAI_MODEL!),
system: '你是一个严谨的中文技术助手。',
// UIMessage 面向渲染;ModelMessage 面向模型调用,二者不要混用。
messages: await convertToModelMessages(body.messages),
abortSignal: request.signal,
});

return result.toUIMessageStreamResponse({
originalMessages: body.messages,
onError: () => '生成失败,请稍后重试',
});
}

3. React 客户端

app/chat/page.tsx
'use client';

import { useChat } from '@ai-sdk/react';
import { FormEvent, useState } from 'react';

export default function ChatPage() {
const [input, setInput] = useState('');
const {
messages,
sendMessage,
status,
error,
stop,
regenerate,
} = useChat();

const isRunning = status === 'submitted' || status === 'streaming';

async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const text = input.trim();
if (!text || isRunning) return;

setInput('');
await sendMessage({ text });
}

return (
<main aria-busy={isRunning}>
<section aria-live="polite">
{messages.map(message => (
<article key={message.id} data-role={message.role}>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <p key={index}>{part.text}</p>;
}

if (part.type === 'reasoning') {
return (
<details key={index}>
<summary>推理摘要</summary>
<p>{part.text}</p>
</details>
);
}

return null;
})}
</article>
))}
</section>

{error && (
<p role="alert">
请求失败。<button onClick={() => regenerate()}>重试</button>
</p>
)}

<form onSubmit={onSubmit}>
<label htmlFor="prompt">消息</label>
<textarea
id="prompt"
value={input}
onChange={event => setInput(event.target.value)}
/>
{isRunning ? (
<button type="button" onClick={stop}>停止</button>
) : (
<button type="submit">发送</button>
)}
</form>
</main>
);
}

关键点:

  • 不再依赖单个 message.content,而是按 message.parts 渲染文本、工具、来源、文件和自定义数据。
  • submitted 表示请求已发出但尚未开始流式返回,streaming 表示正在接收。
  • “停止”必须把取消信号传到服务端和 Provider,而不只是停止 DOM 更新。
  • UI 中的 reasoning 应理解为供应商允许返回的推理摘要或可见块,不等于模型的原始隐藏思维链。

三、UI Message Stream 协议

AI SDK 6 的 UI 流采用 SSE,每个事件的 data 是带类型的 JSON。自定义后端必须返回:

Content-Type: text/event-stream
x-vercel-ai-ui-message-stream: v1

典型事件如下:

data: {"type":"start","messageId":"msg_1"}
data: {"type":"text-start","id":"text_1"}
data: {"type":"text-delta","id":"text_1","delta":"你好"}
data: {"type":"text-end","id":"text_1"}
data: {"type":"finish"}
不要自行按网络 chunk 解析消息

一次 reader.read() 可能包含半个 SSE 事件,也可能包含多个事件。协议解析必须基于缓冲区和事件边界,不能把一个 TCP chunk 当成一个 token 或一个 JSON 对象。

流式自定义数据

状态、引用、进度和业务组件应该作为类型化 Data Part 发送,而不是塞进 Markdown:

lib/ai-types.ts
import type { UIMessage } from 'ai';

export type AppUIMessage = UIMessage<
{ model?: string; totalTokens?: number },
{
progress: { stage: string; percent: number };
citation: { title: string; url: string };
}
>;

使用相同 id 更新 Data Part,可以让“检索中 → 重排中 → 生成中”在同一组件中平滑更新;临时通知则不应写入持久消息历史。

四、结构化输出

AI SDK 6 将结构化输出统一到 generateTextstreamText

非流式对象

lib/extract-ticket.ts
import { openai } from '@ai-sdk/openai';
import { generateText, Output } from 'ai';
import { z } from 'zod';

const ticketSchema = z.object({
category: z.enum(['bug', 'feature', 'question']),
priority: z.enum(['low', 'medium', 'high']),
summary: z.string().max(120),
});

export async function extractTicket(input: string) {
const { output } = await generateText({
model: openai(process.env.OPENAI_MODEL!),
output: Output.object({ schema: ticketSchema }),
prompt: `提取工单字段:${input}`,
});

// output 已经过 Schema 解析,但仍需做业务规则和权限校验。
return output;
}

流式对象

lib/stream-dashboard.ts
import { openai } from '@ai-sdk/openai';
import { Output, streamText } from 'ai';
import { z } from 'zod';

const dashboardSchema = z.object({
title: z.string(),
cards: z.array(z.object({
label: z.string(),
value: z.number(),
})),
});

const result = streamText({
model: openai(process.env.OPENAI_MODEL!),
output: Output.object({ schema: dashboardSchema }),
prompt: '根据销售数据生成仪表盘摘要',
});

for await (const partial of result.partialOutputStream) {
// partial 可能缺少字段,渲染层必须允许“不完整状态”。
console.log(partial);
}

const completeDashboard = await result.output;
Schema 合规不等于业务正确

结构化输出解决的是“形状”,不是“事实”和“权限”。仍需处理拒答、截断、供应商错误、Schema 支持差异,并对 URL、金额、资源 ID、枚举间约束等做确定性校验。

五、工具调用与多步执行

app/api/agent/route.ts
import { openai } from '@ai-sdk/openai';
import { stepCountIs, streamText, tool } from 'ai';
import { z } from 'zod';

const getWeather = tool({
description: '查询指定城市的实时天气',
inputSchema: z.object({ city: z.string().min(1) }),
execute: async ({ city }) => {
// 工具内部仍要鉴权、限时、校验响应,不能信任模型参数。
return { city, temperature: 26, condition: '晴' };
},
});

export async function POST(request: Request) {
const { prompt } = (await request.json()) as { prompt: string };

const result = streamText({
model: openai(process.env.OPENAI_MODEL!),
prompt,
tools: { getWeather },
// 代替旧版 maxSteps;避免 Agent 无限循环和成本失控。
stopWhen: stepCountIs(5),
abortSignal: request.signal,
});

return result.toUIMessageStreamResponse();
}

生产环境还需要:

  1. 工具允许列表和按用户授权。
  2. 输入 Schema 之外的业务校验。
  3. 网络、文件和数据库操作的超时与资源配额。
  4. 写操作的幂等键和审计记录。
  5. 转账、删除、发布等高风险操作的人工确认。

六、Provider 与模型路由

不要让业务代码散落模型 ID。模型选择应该面向能力和服务等级:

lib/model-router.ts
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { openai } from '@ai-sdk/openai';

type ModelRole = 'fast' | 'reasoning' | 'longContext';

const models = {
fast: openai(process.env.MODEL_FAST!),
reasoning: anthropic(process.env.MODEL_REASONING!),
longContext: google(process.env.MODEL_LONG_CONTEXT!),
} satisfies Record<ModelRole, unknown>;

export function selectModel(role: ModelRole) {
return models[role];
}

路由时至少考虑:工具支持、结构化输出、多模态、上下文长度、区域、延迟、限额、数据政策和实测任务质量。Provider Fallback 只对限流、超时、服务端错误等可重试故障生效,不能把认证失败或业务校验失败切到另一个供应商重放。

七、持久化与恢复

推荐分别存储:

types/persistence.ts
interface ConversationRecord {
id: string;
userId: string;
title: string;
createdAt: string;
}

interface MessageRecord {
id: string;
conversationId: string;
role: 'user' | 'assistant';
parts: unknown; // 数据库中保存经版本化校验的 UIMessage parts
status: 'streaming' | 'completed' | 'failed' | 'aborted';
model?: string;
usage?: { inputTokens: number; outputTokens: number };
}
  • 服务端分配消息 ID,避免刷新和重试导致重复消息。
  • 保存输入后再开始生成;生成完成后原子更新状态和用量。
  • 中断时保留已生成内容并标为 aborted,允许“继续生成”,不要假装已完成。
  • 保存协议版本或自行定义稳定的领域消息格式,避免 SDK 升级后历史消息无法读取。
  • 原始消息、附件和日志应设置最小保留期限,并对 PII、密钥和授权头脱敏。

八、错误、重试与可观测性

错误至少分为四类:

类型示例处理方式
用户可修复上下文过长、内容不合规明确提示如何修改,不盲目重试
临时故障429、超时、供应商 5xx有上限的指数退避与抖动
配置故障无效 Key、错误模型 ID立即失败并告警
流中断网络断开、用户取消保留部分结果,区分取消与失败

每次调用至少记录:requestId、用户/租户、功能、模型角色、供应商、首块延迟、总耗时、输入/输出 token、工具步骤数、结束原因和错误类型。日志不要保存原始密钥,也不要默认记录完整敏感 Prompt。

九、AI SDK RSC 怎么看

ai/rscstreamUI 能演示服务端组件流,但官方仍将 AI SDK RSC 视为实验性方案。稳定生产项目优先采用:

  1. 模型输出受限的工具调用或结构化数据。
  2. 服务端校验并转换为类型化 UIMessage.parts
  3. 前端从白名单组件注册表中选择渲染器。

这比让模型生成任意 JSX 更容易测试、持久化、回放、跨框架和做权限控制。

常见面试问题

Q1: AI SDK Core 和 AI SDK UI 有什么区别?

答案

Core 面向模型调用,提供文本生成、结构化输出、工具与 Provider 抽象;UI 面向交互状态,提供 useChatUIMessage 和流协议。服务端用 Core,浏览器通常用 UI,两层通过 UI Message Stream 连接。

Q2: 为什么 UIMessageModelMessage 要分开?

答案

UIMessage 保存渲染所需的 parts、metadata 和交互状态;ModelMessage 是发送给模型的规范化上下文。分开后可以在 UI 中保留进度、来源和组件状态,同时只把必要信息发给模型,降低 token、隐私和协议耦合。

Q3: toUIMessageStreamResponse() 解决了什么?

答案

它把模型输出转换成 AI SDK UI 能识别的 SSE 事件,并处理文本块、工具、来源、错误、结束和取消等状态。旧版 toDataStreamResponse() 及数字前缀协议不应继续用于 AI SDK 6 新项目。

Q4: 为什么不能把一次网络 chunk 当成一次 token?

答案

HTTP/TCP 分块与模型 token 没有一一对应关系。一个 chunk 可能包含半个 UTF-8 字符或多个 SSE 事件,因此必须用 TextDecoder 流式解码并按协议边界解析。

Q5: 结构化输出是否保证内容正确?

答案

不保证。它主要提高 Schema 形状的合规性,字段值仍可能失实、越权或不满足跨字段业务约束。服务端必须继续做业务验证、权限检查和失败降级。

Q6: AI SDK 6 如何实现多步工具调用?

答案

定义带 inputSchematool,通过 stopWhen: stepCountIs(n) 限制步骤。每个工具内部做确定性授权和校验,高风险写操作需要人工确认。

Q7: 为什么模型 ID 不应散落在代码里?

答案

模型版本、价格、区域和限额变化很快。业务应依赖 fastreasoningvision 等能力角色,由配置中心映射到实际模型,升级时只修改一处并经过评测门禁。

Q8: 流式请求如何处理取消?

答案

客户端调用 stop(),传输层中止请求,服务端把 request.signal 传给模型调用和工具。数据库将消息标记为 aborted,保留部分输出但不当作完整成功结果。

Q9: 为什么 Generative UI 推荐白名单组件?

答案

白名单组件把模型权限限制为“选择组件和提供参数”,不允许执行任意代码;它更容易做 Schema 校验、可访问性、持久化、埋点、回放和安全审查。

Q10: Provider Fallback 最大的坑是什么?

答案

不同供应商在工具、Schema、多模态和安全策略上并不完全等价。Fallback 前要做能力检查和错误分类,还要考虑请求重放是否会造成重复扣款或重复写入。

Q11: 消息持久化为什么要保存版本?

答案

SDK 的消息 parts 和协议会升级。保存领域格式或附带 schemaVersion,才能在升级时迁移旧会话,避免历史记录因类型变化而无法渲染。

Q12: 如何评估 AI SDK 升级风险?

答案

先看迁移指南,再针对聊天、工具、结构化输出、取消、错误和持久化建立集成测试;录制协议样本做回放;灰度观察失败率、首块延迟、token 和业务成功率,而不是只看 TypeScript 能否通过。

相关链接