Crispy Admin AI 助手:前端 SSE 流式对话实现解析
Crispy 3 后台 AI 助手(/admin/ai-agent)通过 POST /api/ai/agent 与浏览器通信。响应类型为 text/event-stream(SSE),而非 WebSocket 或一次性 JSON。本文从协议、客户端解析、服务端推流到上游 LLM 串联,说明「为什么能边生成边显示」。
1. 请求概览
Admin 对话使用一条 POST 请求,携带 Cookie 鉴权与会话上下文:
1POST /api/ai/agent2Content-Type: application/json3Cookie: payload-token=...45{6 "sessionId": 12, // 可选;新会话省略7 "messages": [8 { "role": "user", "content": "列出最近 5 篇文章" },9 { "role": "assistant", "content": "..." },10 { "role": "user", "content": "把第一篇标题改成..." }11 ]12}
服务端校验 Admin 登录与 canUseAiAgent 权限后,创建或追加 ai-chat-sessions,再返回 SSE 流。
2. 为何不用 EventSource
浏览器原生 EventSource 只支持 GET,无法 POST 携带 messages 历史。因此 Crispy 使用 fetch + response.body.getReader() 手动解析 SSE 帧。解析逻辑集中在 consumeAgentStream.ts,前台助手与后台 Agent 共用。
3. 客户端:useAiAgentChat
Hook 位于 src/components/AdminAiAgent/useAiAgentChat.ts,核心流程:
乐观更新 UI:先插入 user 消息与 loading 中的 assistant 占位
fetch POST /api/ai/agent,credentials: include 携带 Admin Cookie
调用 consumeAgentStream 逐块读取响应
onSession 保存 sessionId;onText 拼接全文;onToolStart/onToolResult 更新工具状态
流结束后 refreshSessions 刷新侧边栏会话列表
1const res = await fetch('/api/ai/agent', {2 method: 'POST',3 credentials: 'include',4 headers: { 'Content-Type': 'application/json' },5 body: JSON.stringify({ sessionId, messages: history }),6})78await consumeAgentStream(res, {9 onSession: (id) => setSessionId(id),10 onText: (_chunk, fullText) => updateAssistantContent(fullText),11 onToolStart: (id, name, args) => appendRunningTool(id, name, args),12 onToolResult: (id, name, result) => markToolDone(id, result),13})
4. SSE 解析:consumeAgentStream
getReader() 循环 read(),按空行切分 SSE 事件,解析 data: 行中的 JSON:
1while (true) {2 const { done, value } = await reader.read()3 if (done) break4 buffer += decoder.decode(value, { stream: true })5 // 按 \n\n 切分,解析 data: {...}6 if (event.type === 'text') {7 fullText += event.text8 callbacks?.onText?.(event.text, fullText)9 }10}
5. SSE 事件类型(AgentStreamEvent)
session — 会话 ID(仅后台 Agent,流开始时推送)
tool_start — Function Calling 开始(如 find_documents)
tool_result — 工具执行结果或 error
text — 模型输出文本增量,客户端拼接为完整回复
done — 本轮结束
error — 错误信息
示例响应体:
1data: {"type":"session","sessionId":42}23data: {"type":"tool_start","id":"call_1","name":"find_documents","args":{"collection":"posts"}}45data: {"type":"tool_result","id":"call_1","name":"find_documents","result":{"totalDocs":5}}67data: {"type":"text","text":"找到"}8data: {"type":"text","text":" 5 篇"}910data: {"type":"done"}
6. API 路由:ReadableStream 推流
src/app/(payload)/api/ai/agent/route.ts 创建 ReadableStream,每 yield 一个事件即 enqueue:
1const stream = new ReadableStream({2 async start(controller) {3 const send = (data) => {4 controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))5 }6 send({ type: 'session', sessionId })7 for await (const event of runAiAgentStream(req, messages)) {8 send(event)9 if (event.type === 'done' || event.type === 'error') break10 }11 controller.close()12 },13})1415return new Response(stream, {16 headers: {17 'Content-Type': 'text/event-stream; charset=utf-8',18 'Cache-Control': 'no-cache, no-transform',19 Connection: 'keep-alive',20 },21})
连接在 done/error 之前保持打开,因此浏览器可持续收到增量,无需等待整段回复生成完毕。
7. 推理层:runAiAgentStream
src/ai/agent/runAgentStream.ts 负责工具循环(最多 8 轮):
调用 openAiChatCompletionWithToolsStream,body 设 stream: true
LLM 每吐出一个 token → yield { type: text, text }
若模型返回 tool_calls → yield tool_start,执行 executeAgentTool,再 yield tool_result
将 tool 结果追加到 conversation,进入下一轮直到模型给出最终文本
8. 上游 LLM 也是 SSE
openaiCompatible.ts 向 OpenAI 兼容端点 POST /v1/chat/completions,stream: true。提供商按 token 推送 SSE,Crispy 服务端解析后再次转发给浏览器——形成 LLM → Crispy API → 浏览器 的双层流式链路。
9. 会话持久化
与前台助手(仅浏览器内存)不同,后台 Agent 在流开始前写入 user 消息,流结束后将 assistant 回复与 tools 活动写入 ai-chat-sessions Collection。session 事件让客户端在首帧即获得 sessionId,后续请求可带上以续聊。
10. 与前台助手对比
前台:GET/POST /api/ai/assistant,无鉴权,无 session 事件,工具只读检索
后台:POST /api/ai/agent,Admin Cookie,含 session,工具可 CRUD
共用:consumeAgentStream、openAiChatCompletionWithToolsStream、AgentStreamEvent 协议
11. 小结
Admin AI 助手的前端流式体验依赖 SSE:服务端 ReadableStream 持续推送 JSON 事件,客户端 fetch + getReader 边读边渲染。相比一次性 JSON,用户能实时看到工具执行状态与打字机式回复,显著降低长任务等待感。相关源码:useAiAgentChat.ts、consumeAgentStream.ts、api/ai/agent/route.ts、runAgentStream.ts。
暂无评论,来抢沙发吧。