这篇笔记记录一个最小可用的 AI SDK 聊天接入方式:服务端负责把 UI 消息转成模型消息并发起流式生成,客户端负责维护输入框、消息列表和发送动作。
出处是 AI SDK 的 Next.js App Router quickstart,这里把模型 provider 换成 DeepSeek。
安装依赖
如果你用 Next.js App Router,可以先装这几个包:
npm install ai @ai-sdk/react @ai-sdk/deepseek
然后在 .env.local 里放 DeepSeek API key:
DEEPSEEK_API_KEY=your_deepseek_api_key
ai 是核心 SDK,提供 streamText、UIMessage 和 convertToModelMessages。@ai-sdk/react 提供客户端 hook,@ai-sdk/deepseek 负责把 DeepSeek 模型接进 AI SDK 的统一模型接口。
服务端 Route Handler
在 app/api/chat/route.ts 里写一个 POST handler:
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { createDeepSeek } from "@ai-sdk/deepseek";
const deepseek = createDeepSeek({
apiKey: process.env.DEEPSEEK_API_KEY ?? "",
});
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: deepseek("deepseek-chat"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
这里有几个关键点:
messages来自客户端useChat,类型是UIMessage[]。- 模型不直接吃
UIMessage[],所以要用convertToModelMessages(messages)转成模型侧需要的消息格式。 streamText返回的是流式生成结果。toUIMessageStreamResponse()会把服务端结果包装成客户端useChat能消费的 UI message stream。
如果你部署在有请求时长限制的平台,也可以按官方示例加上:
export const maxDuration = 30;
客户端页面
客户端组件里用 useChat 管消息状态和发送动作:
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
export default function Page() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat();
return (
<div className="stretch mx-auto flex w-full max-w-md flex-col py-24">
{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 bottom-0 mb-8 w-full max-w-md rounded border border-zinc-300 p-2 shadow-xl dark:border-zinc-800 dark:bg-zinc-900"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}
这个组件必须加 "use client",因为它用了 useState 和 useChat。默认情况下,useChat() 会把消息发到 /api/chat,也就是上面的 route handler。
数据流
这条链路可以按四步理解:
- 用户提交输入,客户端调用
sendMessage({ text: input })。 useChat把当前对话历史发送给/api/chat。- 服务端把
UIMessage[]转成模型消息,然后调用streamText。 - 服务端返回 UI message stream,客户端边收到边更新
messages。
message.parts 是 AI SDK UI 里很重要的结构。最简单的文本回复会出现在 part.type === "text" 的 part 里;以后如果你加工具调用、reasoning 或自定义 data part,也会继续从 parts 里渲染。
DeepSeek provider 的位置
这段代码用了 createDeepSeek:
const deepseek = createDeepSeek({
apiKey: process.env.DEEPSEEK_API_KEY ?? "",
});
如果只用默认环境变量,也可以直接用 provider 暴露的默认实例;但我更喜欢显式创建 provider,因为项目里以后可能会出现多个 key、base URL 或 fetch 配置。显式 provider 更容易迁移和测试。
模型这里用:
model: deepseek("deepseek-chat");
如果你要接推理模型,可以再看 DeepSeek provider 支持的 deepseek-reasoner,但普通聊天、总结、代码问答先用 deepseek-chat 就够了。
我会保留的最小边界
先不要急着加数据库、会话持久化和复杂 UI。最小版本只需要确认三件事:
- 服务端 route 能正常返回流式响应。
- 客户端能用
useChat收到增量文本。 - API key 只存在服务端环境变量里,不暴露给浏览器。
这条链路跑通以后,再加消息持久化、system prompt、工具调用、错误状态和 loading 状态,会更稳。