《AI 前端实战》第 2/8 篇
本篇目标:从 0 跑通可流式输出的 Chat 最小闭环
上篇:能力地图 · 下篇预告:Streaming 工程化(重连 / 合并 / 限流)
第 1 篇把能力分成了 L1→L4。本篇只做一件事:把你从 L1(等完整 JSON) 拉到 L2(流式对话)。
目标产物:
- Next.js App Router 项目
/api/chat服务端流式接口- 前端
useChat对话页 - Markdown 边生成边渲染(基础防闪烁)
30 分钟能跑通;跑不通也能按文末 checklist 自查。
你将学到
- 为什么要用 AI SDK,而不是手写
fetch+res.json() - App Router 下 Route Handler 怎么返回流
useChat如何管理消息列表与停止生成- 流式 Markdown 的最小可用写法
一、先说结论:别从手写 Stream 解析开始
你可以自己解析 ReadableStream,也能做成流式。但第一版更建议用 Vercel AI SDK,原因很务实:
- 统一多模型 Provider(OpenAI / Anthropic / 兼容网关)
- 前端有现成
useChat,消息状态不用从零设计 - 协议稳定,后续接 Tool Calling 成本更低
本篇默认:
- Next.js 15+(App Router)
- AI SDK(
ai+@ai-sdk/openai或兼容包) - TypeScript
没有 OpenAI Key 也没关系:很多国内网关提供 OpenAI 兼容接口,改
baseURL即可。
二、30 分钟路线图
| 分钟 | 动作 | 验收 |
|---|---|---|
| 0--5 | 建项目、装依赖、配 Key | pnpm dev 能起 |
| 5--15 | 写 /api/chat |
curl / 浏览器能看到流 |
| 15--25 | 写 Chat 页面 + useChat |
页面能边打字边出字 |
| 25--30 | Markdown 渲染 + 停止按钮 | 代码块不太闪,可停止 |
三、初始化项目
bash
npx create-next-app@latest ai-chat-demo --typescript --app --tailwind --eslint
cd ai-chat-demo
pnpm add ai @ai-sdk/openai react-markdown
环境变量 .env.local:
bash
OPENAI_API_KEY=sk-xxx
# 若用兼容网关,再加:
# OPENAI_BASE_URL=https://your-gateway/v1
四、服务端:/api/chat Route
创建 app/api/chat/route.ts:
ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o-mini"), // 可换成你的模型名
system: "你是简洁、靠谱的前端助手,优先给可执行建议。",
messages,
});
return result.toDataStreamResponse();
}
关键点:
- 用
streamText,不是一次性generateText - 返回
toDataStreamResponse(),前端useChat才能直接消费 maxDuration给长回答留时间(部署平台不同,限制不同)
本地快速验证(可选):
bash
curl -N http://localhost:3000/api/chat \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"用一句话解释 RSC"}]}'
能持续吐出数据,而不是等很久才一次性返回,就对了。
五、前端:useChat 对话页
创建 app/page.tsx:
tsx
"use client";
import { useChat } from "ai/react";
import ReactMarkdown from "react-markdown";
export default function Page() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
useChat();
return (
<main className="mx-auto max-w-2xl p-6 space-y-4">
<h1 className="text-2xl font-semibold">AI Chat(流式)</h1>
<div className="space-y-3 min-h-[320px] border rounded-xl p-4">
{messages.map((m) => (
<div key={m.id} className="text-sm">
<div className="opacity-60 mb-1">
{m.role === "user" ? "你" : "助手"}
</div>
{m.role === "assistant" ? (
<ReactMarkdown>{m.content}</ReactMarkdown>
) : (
<p>{m.content}</p>
)}
</div>
))}
{isLoading && <p className="text-xs opacity-60">生成中...</p>}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
className="flex-1 border rounded-lg px-3 py-2"
value={input}
onChange={handleInputChange}
placeholder="问点前端的问题..."
/>
{isLoading ? (
<button type="button" onClick={stop} className="px-4 py-2 border rounded-lg">
停止
</button>
) : (
<button type="submit" className="px-4 py-2 border rounded-lg">
发送
</button>
)}
</form>
</main>
);
}
你得到的能力:
- 消息列表自动维护
- 流式增量更新
assistant内容 - 一键
stop中断生成
这已经是可用的 L2 雏形。
六、Markdown 流式:先做到「能看」,再追求完美
流式场景下,Markdown 最容易出现:
- 代码围栏没闭合,整段样式乱跳
- 列表项半截导致闪烁
- 表格未完成时布局抖动
第 2 篇先用最小方案:
- 助手消息用
react-markdown渲染 - 生成中显示「生成中...」状态
- 不在流式过程做重型语法高亮(可留到稳定后再高亮)
进阶优化(下篇会展开):
- 未闭合 code fence 时降级为纯文本
- 对 thinking / tool / final 分通道渲染
- 用稳定
key和更细粒度组件减少重绘
七、本地跑通 Checklist
按顺序勾:
-
.env.local里 Key 已生效(改完要重启pnpm dev) -
/api/chat返回的是流,不是整包 JSON - 页面发送后,助手文字是「一个个出来」
- 点击「停止」后,不再继续增长内容
- 刷新后你理解:默认
useChat不持久化历史(这是正常的)
常见报错对照:
| 现象 | 排查 |
|---|---|
| 401 / invalid api key | Key 或 baseURL 错 |
| 一直转圈无字 | Route 没 toDataStreamResponse,或前端没 useChat |
| CORS | 同域 App Router 一般无此问题;别跨域硬调 |
| 内容一次性蹦出 | 你可能在服务端先 await 完再返回 |
八、这版故意没做的事(留给后面)
本篇为了 30 分钟闭环,刻意不做:
- 断线重连
- 会话持久化
- Tool Calling
- 多模型路由与成本控制
- 生产级监控
这些会在第 3~8 篇逐步补齐。先有一个「能流式聊」的底座,后面才有地方挂工程能力。
九、最小目录结构(对照)
text
ai-chat-demo/
app/
api/chat/route.ts
page.tsx
layout.tsx
.env.local
package.json
如果你用的是 AI SDK 新版本,hook 导入路径可能是 ai/react 或文档当前推荐路径,以你安装版本的官方文档为准;协议思想不变:服务端 streamText,前端 useChat。
小结
本篇把你推进到 L2:
- 服务端用
streamText产出流 - 前端用
useChat消费流并管理消息 - Markdown 增量渲染 + 停止生成
这就是 AI 前端最小可用闭环。接下来问题会变成:
网络抖了怎么办?消息乱了怎么办?工具调用中途挂了怎么办?
下篇预告 :《AI 前端实战》第 3/8 篇
Streaming UI 工程化:SSE、断线重连、消息合并与限流降级。
先把今天的 Demo 跑通。跑通后再看下篇,体感会完全不一样。