当前位置: 首页 > news >正文

狼人杀 AI 对局:后端如何用 SSE 流式推送到前端?

一、为什么要流式,而不是等一局跑完再返回 JSON?

九人局里,一个阶段可能连续触发多次 LLM 调用:狼人讨论、白天发言、逐人投票……单次推理往往要几秒到十几秒。

如果后端等 LangGraph 整段跑完再return {"state": ...},前端只能转圈等待,用户看不到:

Bot 正在想什么,发言是否正在生成,投票是否一人一人公布。

所以我们采用 SSE(Server-Sent Events):在一次 HTTP 连接里,后端持续推送多条 JSON 事件,前端边收边更新 UI;连接结束时再推一条done: true的最终快照。

二、总体架构

核心思想是一个队列,多路生产者

  1. 每个对局用thread_id区分,对应一个asyncio.Queue
  2. LangGraph 在后台 task里跑,不阻塞 SSE 写出
  3. 图节点里产生的流式事件,都投进同一个队列
  4. SSE 生成器只负责:queue.get()yield "data: ...\n\n"

三、后端入口:所有推进游戏的接口都走同一条流

/start/advance/action/speak/action/vote等,最终都调用run_and_stream,返回:

return StreamingResponse(event_stream(), media_type="text/event-stream")

event_stream的逻辑:

async def run_and_stream(input_data, thread_id): thought_queue = asyncio.Queue() register_thought_stream(thread_id, thought_queue, loop) async def graph_producer(): async for event in graph.astream_events(input_data, cfg(thread_id), version="v1"): if event["event"] == "on_chat_model_stream": chunk = event["data"]["chunk"] if chunk.content: await thought_queue.put({"type": "token", "content": chunk.content}) await thought_queue.put(SENTINEL) # 图跑完 graph_task = asyncio.create_task(graph_producer()) while True: item = await thought_queue.get() if item is SENTINEL: break yield f"data: {json.dumps(item)}\n\n" await graph_task s = graph.get_state(cfg(thread_id)) final_data = {"state": pick_values(s.values), "next": s.next, "done": True} yield f"data: {json.dumps(final_data)}\n\n"

为什么用astream_events


LangGraph 对 LangChain 模型调用会发出on_chat_model_stream事件,可以拿到 LangChain 路径下的 token,推给前端做消息打字机。

为什么最后还要get_state


流式过程中前端只做预览/增量;节点结束时 state 可能还有 patch(如__waiting_for__等人机交互)。最终以 checkpoint 快照为准,避免前后端状态漂移。

四、同步 DSPy 节点怎么往异步 SSE 里推 token?

LangGraph 里很多节点调 DSPy(同步),而 SSE 消费在 asyncio 事件循环里。直接阻塞会卡死整个服务。

stream_context.py:线程本地 callback + 跨线程入队

# 全局:thread_id → (Queue, EventLoop) _registries: dict[str, tuple] = {} def setup_node_streaming(thread_id): queue, loop = _registries[thread_id] def callback(token: str): loop.call_soon_threadsafe(queue.put_nowait, { "type": "thought_token", "content": token, }) _local.thought_callback = callback # threading.local

DSPy 自定义 LM(ai_dspy/__init__.py)在__call__里检测 callback,有则stream=True调 OpenAI API,每个 token 回调:

callback = get_thought_callback() if callback is not None: for chunk in stream: token = chunk.choices[0].delta.content callback(token) # → 安全投进 asyncio.Queue

game_logic.py:异步节点里用asyncio.to_thread包 DSPy

async def _run_dspy_streamed(thread_id, label, func, *args, **kwargs): emit_thought_event(thread_id, {"type": "thought_start", "label": label}) def _run(): setup_node_streaming(thread_id) try: return func(*args, **kwargs) finally: teardown_node_streaming() return await asyncio.to_thread(_run) # 同步 DSPy 不阻塞事件循环

白天发言等路径则用_run_llm_streamed:同样to_thread+setup_node_streaming,直接流式调 OpenAI-compatible API。

五、为什么不用 WebSocket?

SSE 单向推送足够(服务端 → 客户端);操作仍用 POST。实现简单,和 FastAPIStreamingResponse天然契合,Demo 阶段性价比最高。

http://www.jsqmd.com/news/1077623/

相关文章:

  • Linux 内核调优进阶:从 TCP 缓冲区到文件描述符的系统级性能优化
  • CodeWarrior Device Initialization:图形化工具加速MCU外设配置与代码生成
  • 【2026最新版】生产力工具Notion实测:这些隐藏功能让你效率翻倍!
  • LLaMA泄露事件:基础大模型治理的临界点与实践启示
  • 如何3步解锁Windows远程桌面多用户连接:免费解决方案揭秘
  • 从MoE到Multi-Agent:化工AI如何破解大模型的专业瓶颈
  • 从“辅助驾驶”到“驾驶智能体”:截止2026年6月中国无人驾驶汽车发展现状综述及未来方向
  • OpCore-Simplify:如何将OpenCore配置时间从8小时压缩到15分钟的终极指南
  • 【源码解析】musl libc 中 shmget/shmctl 的三层兼容设计
  • GUCCI红配绿,丑到哭?
  • 微服务拆分的极简法则:从领域边界识别到服务自治的架构实践
  • 为自助售货机量身定制的5寸-10寸屏幕模组驱动方案
  • 终极免费指南:如何用LinkSwift让网盘下载速度提升10倍
  • DailyTech-20260624
  • KMS_VL_ALL_AIO:智能激活脚本的完整技术解析与实战指南
  • Web测试入门:从手工到自动化,构建你的测试知识体系与实战项目
  • 2026年国内优质招投标平台推荐:适配选型需求的标讯对接机构评估指南
  • 公墓设计同质化严重?这家服务商在36座城市找到了“不撞脸”的答案
  • 【Agent Harness】Gliding Horse 给 Agent OS 装上双曲空间引擎与默克尔树边云同步
  • AMD Ryzen终极调试工具SMUDebugTool:硬件性能深度掌控实战指南
  • 终极指南:如何在Linux系统快速安装Balena Etcher镜像烧录工具
  • ReACT智能体:让大模型真正做事的推理-行动闭环框架
  • 工业级梯度下降实战:优化器选型、学习率调度与收敛诊断
  • Python使用Darts预测数据:让时间序列预测像调sklearn一样简单
  • 3种颠覆式部署方案:如何高效搭建Elasticsearch监控平台?
  • 性价比高的奥托尼克斯代理商排名
  • 计算机毕业设计之“花遇” 线上鲜花销售系统设计与实现
  • 万亿参数模型如何实现高效推理:稀疏激活工程实践
  • 真懂行老板如何看百达翡丽正装表搭配哲学
  • OpenHarmony学习笔记【总篇:从入门到放弃】