问答题30/1816Agent 如何实现流式输出?

难度:
2026-06-08 创建

参考答案:

Agent 的流式输出本质上是把一次“长任务”拆成一串可消费的事件,而不是等整个 Agent 执行完再返回完整结果。工程上通常会把链路拆成四层:模型流、Agent 编排流、传输层流、前端渲染流。

模型侧一般使用大模型 API 的 streaming 能力,持续拿到 token delta。Agent 编排层不能只把 token 原样透传,因为 Agent 执行过程中可能会经历多轮推理、工具调用、工具结果返回、继续生成最终答案。因此更合理的做法是把输出抽象成结构化事件,例如:

1type AgentStreamEvent = 2 | { type: 'message_delta'; content: string } 3 | { type: 'tool_call_start'; toolName: string; callId: string } 4 | { type: 'tool_call_result'; callId: string; result: unknown } 5 | { type: 'message_done'; messageId: string } 6 | { type: 'error'; message: string };

这样前端拿到的不是一段不可解释的字符串,而是一条可驱动 UI 状态机的事件流。比如 message_delta 用来追加文本,tool_call_start 用来展示“正在检索资料”或“正在调用工具”,tool_call_result 可以展示工具执行结果,error 用来终止当前会话并给出错误状态。

传输层常见选择是 SSE 或 WebSocket。
如果只是服务端持续向浏览器推送文本和状态,SSE 更简单,天然支持 HTTP、断线重连、文本事件,适合大多数 Agent 对话场景。后端可以通过 text/event-stream 持续写入事件:

1res.setHeader('Content-Type', 'text/event-stream'); 2res.setHeader('Cache-Control', 'no-cache'); 3res.setHeader('Connection', 'keep-alive'); 4 5for await (const event of agent.runStream(input)) { 6 res.write(`event: ${event.type}\n`); 7 res.write(`data: ${JSON.stringify(event)}\n\n`); 8} 9 10res.write(`event: done\ndata: {}\n\n`); 11res.end();

WebSocket 更适合双向高频交互,比如用户中途补充指令、实时语音、多人协作、复杂任务控制等场景。但如果只是问答流式返回,用 WebSocket 反而会增加连接管理、心跳、鉴权和重连成本。

前端消费时,一般用 EventSourcefetch + ReadableStreamEventSource 写法简单,但只支持 GET,请求体和自定义 Header 受限;如果需要 POST、复杂鉴权、AbortController 取消请求,通常会选择 fetch 读取流。

前端渲染时要注意不要每个 token 都触发重渲染,否则长回答会造成性能抖动。更稳妥的方式是把 delta 先放入缓冲区,用 requestAnimationFrame 或固定节流间隔批量更新。同时要维护消息状态,比如 streamingtoolRunningdoneerror,这样 UI 不会因为事件顺序变化而混乱。

Agent 流式输出还有一个重要边界:不要把模型内部的完整推理链路直接流给用户。生产环境更适合输出可解释的阶段性状态,比如“正在分析问题”“正在查询知识库”“正在整理答案”,而不是暴露原始 chain-of-thought。这样既保护系统提示和内部推理,也能给用户足够的进度感。

可靠性上,需要处理取消、超时、异常、断线和幂等。用户点击停止时,前端通过 AbortController 关闭请求,后端要停止模型流和工具调用;如果工具调用耗时过长,需要有超时控制;如果连接中断,最好通过 conversationIdmessageIdeventId 做续传或至少保证不会重复生成混乱内容。

最近更新时间:2026-06-09

赞赏支持

题库维护不易,您的支持就是我们最大的动力!