第 16 章 · Chatbot:useChat 与流协议
本章目标:
- 掌握
useChathook 的核心用法:消息流式渲染、状态管理与表单提交- 理解
status、error、stop、regenerate、setMessages等 UI 状态控制手段- 学会配置 transport、自定义请求头/请求体,以及
DirectChatTransport直连 Agent- 了解 Text Stream 与 Data Stream(UI Message Stream)两种流协议的区别与报文格式
16.1 第一个聊天界面
useChat hook 让你轻松为聊天应用创建对话式用户界面。它支持从 AI provider 流式接收聊天消息、管理聊天状态,并在新消息到达时自动更新 UI。
useChat 提供的核心能力:
- Message Streaming:AI provider 的所有消息实时流式传输到聊天 UI。
- Managed States:hook 帮你管理 input、messages、status、error 等状态。
- Seamless Integration:以最小代价把聊天 AI 集成到任何设计或布局中。
先看一个完整的 React(Next.js App Router)示例:
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Page() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
const [input, setInput] = useState('');
return (
<>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={e => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
}}
>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={status !== 'ready'}
placeholder="Say something..."
/>
<button type="submit" disabled={status !== 'ready'}>
Submit
</button>
</form>
</>
);
}服务端路由负责调用模型并把结果转换为 UI Message Stream 返回:
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
} from 'ai';
import { createGateway } from 'ai';
const gateway = createGateway({
apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: gateway('openai/gpt-5'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}💡 同样的服务端代码也适用于自定义 provider:把
gateway('openai/gpt-5')换成myProvider('your-model-id')即可(用createOpenAICompatible构造,见第 3 章)。
注意:UI message 有一个新的 parts 属性,包含消息的各个组成部分。我们推荐使用 parts 属性而非 content 属性来渲染消息——parts 支持文本、tool 调用、tool 结果等不同消息类型,能构建更灵活复杂的聊天 UI。
在 Page 组件中,当用户通过 sendMessage 发送消息时,useChat hook 会请求你的 AI endpoint,消息随后实时流回并显示在聊天界面中。
16.2 状态管理:status 与 error
Status
useChat 返回一个 status,可能的取值:
submitted:消息已发送到 API,正在等待响应流开始。streaming:响应正在从 API 流式传入。ready:完整响应已接收并处理完毕,可以提交新的用户消息。error:API 请求期间发生错误。
可以用 status 实现:处理中显示 loading 动画、显示「停止」按钮中断当前消息、禁用提交按钮等:
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Page() {
const { messages, sendMessage, status, stop } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
const [input, setInput] = useState('');
return (
<>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
)}
</div>
))}
{(status === 'submitted' || status === 'streaming') && (
<div>
{status === 'submitted' && <Spinner />}
<button type="button" onClick={() => stop()}>
Stop
</button>
</div>
)}
<form
onSubmit={e => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
}}
>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={status !== 'ready'}
placeholder="Say something..."
/>
<button type="submit" disabled={status !== 'ready'}>
Submit
</button>
</form>
</>
);
}Error State
类似地,error 状态反映 fetch 请求期间抛出的错误对象。可用于显示错误信息、禁用提交按钮或显示重试按钮:
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Chat() {
const { messages, sendMessage, error, regenerate } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
const [input, setInput] = useState('');
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.role}:{' '}
{m.parts.map((part, index) =>
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
)}
</div>
))}
{error && (
<>
<div>An error occurred.</div>
<button type="button" onClick={() => regenerate()}>
Retry
</button>
</>
)}
<form
onSubmit={e => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
}}
>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={error != null}
/>
</form>
</div>
);
}⚠️ 推荐向用户展示通用错误信息(如「Something went wrong」),避免泄露服务端细节。
16.3 操作消息:修改、取消与重新生成
有时你可能想直接修改已有消息——例如给每条消息加一个删除按钮。setMessages 函数可以完成这类任务:
const { messages, setMessages } = useChat()
const handleDelete = (id) => {
setMessages(messages.filter(message => message.id !== id))
}
return <>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) => (
part.type === 'text' ? (
<span key={index}>{part.text}</span>
) : null
))}
<button onClick={() => handleDelete(message.id)}>Delete</button>
</div>
))}
...可以把 messages 和 setMessages 理解成 React 中 state 和 setState 的配对。
取消生成:当响应还在流式返回时中止它——调用 useChat 返回的 stop 函数即可。点击「Stop」后 fetch 请求被 abort,避免浪费资源:
const { stop, status } = useChat()
return <>
<button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>Stop</button>
...重新生成:让 AI provider 重新处理最后一条消息:
const { regenerate, status } = useChat();
return (
<>
<button
onClick={regenerate}
disabled={!(status === 'ready' || status === 'error')}
>
Regenerate
</button>
...
</>
);点击「Regenerate」后,provider 会重新生成最后一条消息并替换当前内容。
节流更新:默认情况下,每收到一个 chunk 都会触发一次渲染。可以用 throttle 选项节流 UI 更新(目前仅 React 支持):
const { messages, ... } = useChat({
// Throttle the messages and data updates to 50ms:
throttle: 50
})16.4 事件回调与请求配置
Event Callbacks
useChat 提供可选的事件回调,覆盖聊天生命周期的不同阶段:
onFinish:助手响应完成时触发,包含响应消息、全部消息以及 abort/disconnect/error 标志。onError:fetch 请求出错时触发。onData:收到 data part 时触发。
import { UIMessage } from 'ai';
const {
/* ... */
} = useChat({
onFinish: ({ message, messages, isAbort, isDisconnect, isError }) => {
// use information to e.g. update other UI states
},
onError: error => {
console.error('An error occurred:', error);
},
onData: data => {
console.log('Received data part from server:', data);
},
});值得一提的是:在 onData 回调里抛出错误可以中止后续处理——这会触发 onError 并阻止该消息追加到聊天 UI,适合处理来自 provider 的意外响应。
自定义 headers、body 与 credentials
默认情况下 useChat 向 /api/chat 发送 POST 请求、以消息列表作为请求体。可以在 transport 层配置所有请求共用的选项:
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: '/api/custom-chat',
headers: {
Authorization: 'your_token',
},
body: {
user_id: '123',
},
credentials: 'same-origin',
}),
});也可以提供函数形式的动态配置,适合需要刷新的认证 token 或依赖运行时条件的配置:
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: '/api/custom-chat',
headers: () => ({
Authorization: `Bearer ${getAuthToken()}`,
'X-User-ID': getCurrentUserId(),
}),
body: () => ({
sessionId: getCurrentSessionId(),
preferences: getUserPreferences(),
}),
credentials: () => 'include',
}),
});对于随时间变化的组件状态,官方建议用 useRef 保存当前值并在配置函数里引用 ref.current,或者优先使用请求级配置。
请求级配置(推荐)
请求级选项比 hook 级更灵活:每次请求可单独定制,且优先生效。把选项作为第二个参数传给 sendMessage:
// Pass options as the second parameter to sendMessage
sendMessage(
{ text: input },
{
headers: {
Authorization: 'Bearer token123',
'X-Custom-Header': 'custom-value',
},
body: {
temperature: 0.7,
max_tokens: 100,
user_id: '123',
},
metadata: {
userId: 'user123',
sessionId: 'session456',
},
},
);请求级选项会与 hook 级选项合并,请求级优先。服务端可以从请求体中解构出这些附加字段:
export async function POST(req: Request) {
// Extract additional information ("customKey") from the body of the request:
const { messages, customKey }: { messages: UIMessage[]; customKey: string } =
await req.json();
//...
}16.5 Transport 配置与直连 Agent
通过 transport 选项可以自定义消息发送行为。例如只发送最新一条消息,由服务端加载历史:
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
export default function Chat() {
const { messages, sendMessage } = useChat({
id: 'my-chat',
transport: new DefaultChatTransport({
prepareSendMessagesRequest: ({ id, messages }) => {
return {
body: {
id,
message: messages[messages.length - 1],
},
};
},
}),
});
// ... rest of your component
}对应的服务端路由接收这种自定义格式:
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
} from 'ai';
import { createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
export async function POST(req: Request) {
const { id, message } = await req.json();
// Load existing messages and add the new one
const messages = await loadMessages(id);
messages.push(message);
const result = streamText({
model: gateway('openai/gpt-5'),
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}对于消息重新生成等复杂场景,还可以基于 trigger 做路由:客户端在 prepareSendMessagesRequest 中根据 trigger === 'submit-user-message' 或 'regenerate-assistant-message' 返回不同的 body,服务端据此决定是追加消息还是截断到 messageId 再重生成。
DirectChatTransport
如果想绕过 HTTP 直接与 Agent 通信,可以使用 DirectChatTransport,适用于服务端渲染场景、无网络的测试以及单进程应用:
import { useChat } from '@ai-sdk/react';
import { DirectChatTransport, ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions: 'You are a helpful assistant.',
});
export default function Chat() {
const { messages, sendMessage, status } = useChat({
transport: new DirectChatTransport({ agent }),
});
return (
<>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
)}
</div>
))}
<button
onClick={() => sendMessage({ text: 'Hello!' })}
disabled={status !== 'ready'}
>
Send
</button>
</>
);
}DirectChatTransport 直接调用 agent 的 stream() 方法,将 UI messages 转换为 model messages,并把响应作为 UI message chunks 流回。
16.6 控制响应流:错误透传与用量信息
使用 streamText 时,你可以控制错误信息和 usage 信息如何返回给客户端。
Error Messages
出于安全考虑,默认错误信息会被遮蔽为「An error occurred.」。可以通过提供 onError 函数转发原始错误或自定义错误消息:
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
} from 'ai';
import { createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: gateway('openai/gpt-5'),
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
onError: error => {
if (error == null) {
return 'unknown error';
}
if (typeof error === 'string') {
return error;
}
if (error instanceof Error) {
return error.message;
}
return JSON.stringify(error);
},
}),
});
}Usage Information
通过 message metadata 可以追踪 token 消耗与资源使用:定义带 usage 字段的自定义 metadata 类型(可选,用于类型安全)、在响应中使用 messageMetadata 附加 usage 数据、在 UI 组件中展示用量指标。
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
type LanguageModelUsage,
} from 'ai';
import { createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
// Create a new metadata type (optional for type-safety)
type MyMetadata = {
totalUsage: LanguageModelUsage;
};
// Create a new custom message type with your own metadata
export type MyUIMessage = UIMessage<MyMetadata>;
export async function POST(req: Request) {
const { messages }: { messages: MyUIMessage[] } = await req.json();
const result = streamText({
model: gateway('openai/gpt-5'),
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
originalMessages: messages,
messageMetadata: ({ part }) => {
// Send total usage when generation is finished
if (part.type === 'finish') {
return { totalUsage: part.totalUsage };
}
},
}),
});
}客户端通过 message.metadata 访问,也可以从 useChat 的 onFinish 回调读取:
'use client';
import { useChat } from '@ai-sdk/react';
import type { MyUIMessage } from './api/chat/route';
import { DefaultChatTransport } from 'ai';
export default function Chat() {
const { messages } = useChat<MyUIMessage>({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
onFinish: ({ message }) => {
// Access message metadata via onFinish callback
console.log(message.metadata?.totalUsage);
},
});
/* 渲染时:{m.metadata?.totalUsage && (
<div>Total usage: {m.metadata?.totalUsage.totalTokens} tokens</div>
)} */
return null;
}16.7 Reasoning、Sources 与附件
Reasoning
部分模型(如 DeepSeek deepseek-r1、Anthropic claude-sonnet-4-5-20250929)支持 reasoning tokens,通常出现在正文之前。可用 sendReasoning 选项把它们转发给客户端:
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
} from 'ai';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: 'deepseek/deepseek-r1',
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
sendReasoning: true,
}),
});
}客户端访问 reasoning parts(有 text 属性),部分模型还会产生 reasoning 文件(如图片),以 reasoning-file parts 形式提供:
messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) => {
// text parts:
if (part.type === 'text') {
return <div key={index}>{part.text}</div>;
}
// reasoning parts:
if (part.type === 'reasoning') {
return <pre key={index}>{part.text}</pre>;
}
})}
</div>
));Sources
Perplexity、Google 等 provider 会在响应中包含 sources(目前限于 grounding 响应的网页)。用 sendSources: true 转发后,客户端可渲染 source-url 与 source-document 两类 source parts。
图像生成
Google gemini-2.5-flash-image 等模型支持图像生成。生成的图像以文件形式暴露给客户端,渲染 file parts 即可:
messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <div key={index}>{part.text}</div>;
} else if (part.type === 'file' && part.mediaType.startsWith('image/')) {
return <img key={index} src={part.url} alt="Generated image" />;
}
})}
</div>
));Attachments
useChat 支持随消息一起发送文件附件。既可以用文件输入框的 FileList(自动转为 data URL 发送,目前仅 image/* 和 text/* 会自动转换),也可以直接传文件对象数组:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { FileUIPart } from 'ai';
export default function Page() {
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
const [files] = useState<FileUIPart[]>([
{
type: 'file',
filename: 'earth.png',
mediaType: 'image/png',
url: 'https://example.com/earth.png',
},
{
type: 'file',
filename: 'moon.png',
mediaType: 'image/png',
url: 'data:image/png;base64,iVBORw0KGgo...',
},
]);
return (
<form
onSubmit={event => {
event.preventDefault();
if (input.trim()) {
sendMessage({ text: input, files });
setInput('');
}
}}
>
<input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}16.8 工具类型推断
TypeScript 下 AI SDK UI 提供类型推断辅助函数,保证工具输入输出类型安全:
import { InferUITools, ToolSet, UIMessage, UIDataTypes } from 'ai';
import { z } from 'zod';
const tools = {
weather: {
description: 'Get the current weather',
inputSchema: z.object({
location: z.string().describe('The city and state'),
}),
execute: async ({ location }) => {
return `The weather in ${location} is sunny.`;
},
},
} satisfies ToolSet;
// Infer the types from the tool set
type MyUITools = InferUITools<typeof tools>;
type MyUIMessage = UIMessage<never, UIDataTypes, MyUITools>;
// Pass the custom type to useChat:
// const { messages } = useChat<MyUIMessage>();单个工具用 InferUITool<typeof weatherTool> 推断,整个工具集用 InferUITools<typeof tools>。
16.9 流协议:Text Stream 与 Data Stream
useChat 和 useCompletion 同时支持 text streams 与 data streams。stream protocol 定义了数据如何在 HTTP 之上流式传输到前端——利用这些信息可以为你的场景开发自定义前后端,比如用 Python/FastAPI 实现兼容的 API endpoint。
Text Stream Protocol
Text stream 以纯文本分块流式传输,每个 chunk 依次追加形成完整响应。使用 useChat 时需把 streamProtocol 设为 text(即使用 TextStreamChatTransport)。后端用 streamText 生成,把结果的 stream 交给 toTextStream 并用 createTextStreamResponse 返回:
'use client';
import { useChat } from '@ai-sdk/react';
import { TextStreamChatTransport } from 'ai';
import { useState } from 'react';
export default function Chat() {
const [input, setInput] = useState('');
const { messages, sendMessage } = useChat({
transport: new TextStreamChatTransport({ api: '/api/chat' }),
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map(message => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
))}
<form
onSubmit={e => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={e => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}import {
convertToModelMessages,
createTextStreamResponse,
streamText,
toTextStream,
UIMessage,
} from 'ai';
import { createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: gateway('openai/gpt-5'),
messages: await convertToModelMessages(messages),
});
return createTextStreamResponse({
stream: toTextStream({ stream: result.stream }),
});
}⚠️ text streams 只支持基础文本。若需流式传输 tool calls、usage 等其他类型数据,请使用 data streams。
Data Stream Protocol
Data stream 遵循 AI SDK 定义的特殊协议,采用 Server-Sent Events (SSE) 格式——标准化更好、支持 ping 保活、可重连、缓存处理更佳。自定义后端提供 data streams 时需要设置 header x-vercel-ai-ui-message-stream: v1。
主要 stream parts 一览(均为 data: {...} 形式的 SSE 事件):
| Part 类型 | 说明 |
|---|---|
start | 新消息开始,含 messageId |
text-start / text-delta / text-end | 文本块按 start/delta/end 模式流式传输,每个文本块有唯一 ID |
reasoning-start / reasoning-delta / reasoning-end | 推理内容的同款三段式 |
source-url / source-document | 外部内容来源引用 |
file | 文件引用(url + mediaType) |
data-* | 自定义数据部分(如 data-weather),前端可针对性处理 |
error | 错误部分,收到即追加到消息 |
tool-input-start / tool-input-delta / tool-input-available | 工具输入的流式生成三阶段 |
tool-approval-request / tool-approval-response | 工具审批请求与决策 |
tool-output-available / tool-output-denied | 工具执行结果 |
start-step / finish-step / reset-step | 步骤边界(一次 LLM API 调用为一个 step) |
finish | 消息完成 |
abort | 流被中止 |
例如文本增量事件的报文:
data: {"type":"text-delta","id":"msg_68679a454370819ca74c8eb3d04379630dd1afb72306ca5d","delta":"Hello"}流的结尾以特殊标记结束:
data: [DONE]data stream protocol 是 useChat 在前端的默认协议。后端只需把 streamText 结果流传给 toUIMessageStream 并用 createUIMessageStreamResponse 返回即可(见本章开头的服务端代码)。
本章小结
useChat通过DefaultChatTransport连接/api/chat,sendMessage触发请求,消息经parts属性流式渲染;status四态(submitted/streaming/ready/error)驱动 loading、Stop 按钮与输入禁用;stop()取消、regenerate()重生成、setMessages()直接改写消息列表;- 服务端用
convertToModelMessages+streamText+toUIMessageStream组合返回createUIMessageStreamResponse;onError可透传错误详情; - Transport 可定制请求格式(
prepareSendMessagesRequest),DirectChatTransport能不经 HTTP 直连 ToolLoopAgent; - 流协议分两种:纯文本的 text stream protocol 与基于 SSE 的 data stream protocol(默认),后者支持 tool calls、sources、data parts、审批等丰富 part 类型。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. useChat 返回的 status 取值不包括以下哪一项?
2. 关于 UI message 的 parts 属性,下列说法正确的是?
3. 自定义后端(如 Python/FastAPI)实现 Data Stream 协议时必须做什么?
4. 想让 useChat 直接与本地的 ToolLoopAgent 通信而不经过 HTTP,应该使用?
🛠️ 动手实践
- 用 Next.js App Router 搭建最小聊天应用:前端
useChat+DefaultChatTransport,后端streamText+createGateway('anthropic/claude-haiku-4.5')(再切换为createOpenAICompatible自定义 provider 验证一行切换)。 - 为聊天界面添加完整的状态交互:streaming 时显示 Stop 按钮、出错时显示 Retry(
regenerate)按钮,并用throttle: 50优化渲染频率。 - 把服务端的
toUIMessageStream加上sendReasoning: true,接入一个 reasoning 模型(如deepseek/deepseek-r1),在前端分别渲染text与reasoning两类 parts。