Next.js App Router AI流式输出完整指南
最近把AI对话页整体迁移到了Next.js App Router上。流式输出在Pages Router时代闭眼就能写,但切换到App Router加上RSC和Server Action这套新架构后,硬是卡了大半天。把踩过的坑和绕过去的路线整理出来,希望能帮到正在折腾同样事情的同行。
第一个纠结:Route Handler 还是 Server Action
App Router下实现流式输出,主要有两条路可选。
先说Route Handler。在app/api/chat/route.ts里定义接口,这是最稳妥的方案。本质上还是返回一个标准Response,body里塞一个ReadableStream:
export async function POST(req: Request) {const stream = new ReadableStream({async start(controller) {const res = await callModel(/* ... */)for await (const chunk of res) {controller.enqueue(new TextEncoder().encode(chunk.delta))}controller.close()},})return new Response(stream, {headers: { 'Content-Type': 'text/event-stream' },})}
客户端这边,依然用fetch加getReader()来读,跟前端时代的做法差不多。
再说Server Action。这条路径看起来更新潮,能在组件里直接await调用。但说句实在话,纯Server Action要做逐字流式,其实挺别扭的——这东西本来就是为表单提交和数据变更设计的,不是专门给长连接流式用的。非得硬写的话,得配合useActionState,或者返回一个可迭代对象,绕来绕去。试了一版之后,体感上确实不如Route Handler直接爽快。
我的选择
结论简单直接:流式走Route Handler,Server Action只用来做非流式的副作用——比如把本轮对话存库、更新会话标题。各司其职,别让Server Action硬扛流式。
踩到的坑
坑一:动态渲染。默认是有缓存的,对话接口必须显式声明export const dynamic = 'force-dynamic'。不然你会惊喜地发现,流式接口被缓存住,第二次问返回的是上次的内容,屏幕前的你整个人都愣住了。
坑二:客户端组件边界。渲染对话气泡、维护输入状态的那部分,必须加上'use client'。一开始还犯了这个低级错误,忘了加,useState直接报错——App Router默认是Server Component这点,得时刻记在心里。
坑三:Edge Runtime在流式方面更顺滑,但部分Node API用不了,得按需取舍。
没做完美的地方
Server Action存库失败时的回滚,目前做得比较糙,就打了条日志,还没有真正的补偿机制。先这么用着。
模型那端接的是讯飞MaaS,现成的推理服务直接调,省去了自建这部分麻烦。各位在App Router下搞流式,是走Handler还是Action?评论区交个底。
