我会从架构角度回答:先结合当前代码里暴露的问题,再对照主流 Agent 产品/框架对长任务、流式输出、切换会话和恢复订阅的常见做法。主流做法不是让“HTTP/SSE 连接本身等于一次 Agent 执行”。更先进的 Agent 对话系统通常把它拆成两件事:
- Agent Run 在后端独立执行
- 前端只是订阅这个 Run 的事件流
也就是说,用户切走页面、断开 SSE、切换 history,都不应该影响后端那个
thread_id/run_id 继续生成。典型架构是:
主流 Agent 的核心模式
最常见的是 Thread + Run + Event Log:
thread_id:一段聊天历史。
run_id:这次用户消息触发的一次 Agent 执行。
message store:最终可恢复的对话消息。
event log:流式 token、tool_start、tool_end、interrupt、done 等事件。
subscriber:前端连接,只负责接收事件,不拥有执行生命周期。
所以用户从 A 会话切到 B 会话时:
- A 的 run 继续在后端跑。
- B 的 history 正常加载,不被 A 的 run 堵住。
- 过一会切回 A,可以先
GET /history拿已落库内容,再重新订阅 A 的未完成 run。
- 如果 SSE 断了,可以用
Last-Event-ID或cursor从事件日志补拉。
为什么这样做
因为 Agent 对话已经不是普通 request-response 了。它可能包含:
- 长时间 LLM 推理
- 工具调用
- 人工确认 interrupt
- 多步骤 workflow
- 后台任务
- 浏览器刷新/切换 tab
- 移动端断网重连
如果把这些都绑在一个
/chat/completions HTTP 请求里,体验就会变脆:连接断了任务也断,任务慢了其它接口也可能受影响,前端状态也很难恢复。对你当前项目的直接启发
你现在更像是:
更推荐改成:
先进实现一般还会有这些细节
- 会话级 streaming 状态:不是全局
isStreaming,而是streamingByThreadId。
- 可重连事件流:SSE/WebSocket 只是一种订阅,断开后可以继续订阅。
- 事件持久化:token 或至少阶段性 assistant content 会写入后端,history 能看到部分回复。
- 后台任务隔离:Agent run 放到
asyncio task、worker、队列或 durable execution 系统里。
- 非阻塞工具调用:所有 LLM/tool/http/db 操作都必须 async,或丢到线程池/worker,不能堵 FastAPI event loop。
- 明确取消语义:切走页面不取消;用户点“停止生成”才取消 run。
一句话总结:
现代 Agent 对话系统把“生成任务”做成后端可持久、可恢复、可订阅的 run,而不是依赖浏览器当前这条 SSE 请求活着。
作者:NA YANG
链接:
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。