Skip to content

第 16 章 · Chatbot:useChat 与流协议

本章目标:

  • 掌握 useChat hook 的核心用法:消息流式渲染、状态管理与表单提交
  • 理解 statuserrorstopregeneratesetMessages 等 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)示例:

tsx
'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 返回:

ts
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 动画、显示「停止」按钮中断当前消息、禁用提交按钮等:

tsx
'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 请求期间抛出的错误对象。可用于显示错误信息、禁用提交按钮或显示重试按钮:

tsx
'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 函数可以完成这类任务:

tsx
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>
  ))}
  ...

可以把 messagessetMessages 理解成 React 中 statesetState 的配对。

取消生成:当响应还在流式返回时中止它——调用 useChat 返回的 stop 函数即可。点击「Stop」后 fetch 请求被 abort,避免浪费资源:

tsx
const { stop, status } = useChat()

return <>
  <button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>Stop</button>
  ...

重新生成:让 AI provider 重新处理最后一条消息:

tsx
const { regenerate, status } = useChat();

return (
  <>
    <button
      onClick={regenerate}
      disabled={!(status === 'ready' || status === 'error')}
    >
      Regenerate
    </button>
    ...
  </>
);

点击「Regenerate」后,provider 会重新生成最后一条消息并替换当前内容。

节流更新:默认情况下,每收到一个 chunk 都会触发一次渲染。可以用 throttle 选项节流 UI 更新(目前仅 React 支持):

tsx
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 时触发。
tsx
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 层配置所有请求共用的选项:

tsx
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 或依赖运行时条件的配置:

tsx
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

tsx
// 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 级选项合并,请求级优先。服务端可以从请求体中解构出这些附加字段:

ts
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 选项可以自定义消息发送行为。例如只发送最新一条消息,由服务端加载历史:

tsx
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
}

对应的服务端路由接收这种自定义格式:

ts
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,适用于服务端渲染场景、无网络的测试以及单进程应用:

tsx
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 函数转发原始错误或自定义错误消息:

ts
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 组件中展示用量指标。

ts
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 访问,也可以从 useChatonFinish 回调读取:

tsx
'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 选项把它们转发给客户端:

ts
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 形式提供:

tsx
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-urlsource-document 两类 source parts。

图像生成

Google gemini-2.5-flash-image 等模型支持图像生成。生成的图像以文件形式暴露给客户端,渲染 file parts 即可:

tsx
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/* 会自动转换),也可以直接传文件对象数组:

tsx
'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 提供类型推断辅助函数,保证工具输入输出类型安全:

tsx
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

useChatuseCompletion 同时支持 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 返回:

tsx
'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>
  );
}
ts
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/chatsendMessage 触发请求,消息经 parts 属性流式渲染;
  • status 四态(submitted/streaming/ready/error)驱动 loading、Stop 按钮与输入禁用;stop() 取消、regenerate() 重生成、setMessages() 直接改写消息列表;
  • 服务端用 convertToModelMessages + streamText + toUIMessageStream 组合返回 createUIMessageStreamResponseonError 可透传错误详情;
  • 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,应该使用?

🛠️ 动手实践

  1. 用 Next.js App Router 搭建最小聊天应用:前端 useChat + DefaultChatTransport,后端 streamText + createGateway('anthropic/claude-haiku-4.5')(再切换为 createOpenAICompatible 自定义 provider 验证一行切换)。
  2. 为聊天界面添加完整的状态交互:streaming 时显示 Stop 按钮、出错时显示 Retry(regenerate)按钮,并用 throttle: 50 优化渲染频率。
  3. 把服务端的 toUIMessageStream 加上 sendReasoning: true,接入一个 reasoning 模型(如 deepseek/deepseek-r1),在前端分别渲染 textreasoning 两类 parts。