1. 从一次天气查询看 JoyAgent-JDGenie 的 SSE 调度链路JoyAgent-JDGenie 是京东开源的多智能体协作平台它最值得研究的地方不是界面而是从用户敲下回车到智能体真正开始干活之间那条 SSE 事件流。你输入“查看一下北京明天的天气”前端不会等后端全部算完再一次性返回而是通过 Server-Sent Events 把 plan_thought、plan、task、tool_call、final_answer 这些事件按发生顺序推回来。这条链路把输入解析、任务分发、结果回传三个环节串在一起任何一个环节的字段对不上前端就会卡在 loading 或者只显示一半内容。我试过把这套流程拆成可复制的配置和验证步骤发现大部分衔接问题都出在三个地方请求体里的 deepThink 参数决定走 REACT 还是 PLAN_SOLVE 智能体SSE 的 Content-Type 和超时设置决定连接能不能撑到最后一个事件前端 packageType 的 switch 分支决定消息能不能被正确渲染。这篇就按“用户输入 → SSE 建连 → 后端调度 → 事件回传 → 排错”的顺序把每个阶段的数据形态和触发时机讲清楚面向想复刻这套架构的开发者。核心检索词先摆出来JoyAgent-JDGenie 是一套基于 Java 17 Spring Boot 3.2.2 和 React 19 的多智能体调度系统SSE 是它实现流式响应的传输层智能体调度是它根据 deepThink 参数在 REACT 和 PLAN_SOLVE 之间切换的核心逻辑。适合谁看适合已经能跑通一个单体 LLM 接口、想进一步理解多智能体事件流怎么设计的后端和全栈开发者。下面所有配置片段都可以直接对照你的工程改。2. TaoToken 前置给智能体调度链路准备模型出口JoyAgent-JDGenie 本身负责调度和工具编排但 PlanningAgent、ExecutorAgent、SummaryAgent 在 think() 和 act() 阶段都要调用 LLM。如果你在本地复刻这套链路模型出口需要先固定下来否则调试 SSE 事件时你分不清是调度逻辑错了还是模型没返回。我一般用 TaoToken 作为统一的模型接入层它的 API 地址是 https://taotoken.net/api兼容常见的 OpenAI 风格调用这样 PlanningAgent 里的 llm.callLLM 不用改太多就能接上。先拿 Key。打开 https://taotoken.net/api-keys 创建一个新 Key复制出来。注意这个 Key 只显示一次丢了就重新建。拿到之后在项目里用环境变量存不要硬编码进 Java 或前端代码export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用的是 Claude Code 这类编码工具来辅助改 JoyAgent-JDGenie 的源码可以在工具里配置 Anthropic 兼容入口Base URL 填 https://taotoken.net/api Model ID 按你实际用的模型填。Cline 或 CC Switch 里也是三件套Base URL、API Key、Model ID缺一个都会在调用 PlanningAgent 时报 401。为什么要在调度链路之前先固定模型出口因为 JoyAgent-JDGenie 的 SSE 事件里plan_thought 和 final_answer 的内容都来自 LLM 返回。如果模型出口不稳定你会看到 SSE 连接建立了、plan 事件也推了但 plan_thought 一直空着最后超时断开。把模型出口先跑通后面排错才能聚焦在调度逻辑本身。你可以先用模型对话页面验证 Key 是否可用https://taotoken.net/models 发一句“返回 JSON{status:ok}”能正常返回就说明出口没问题。这一步的产出是一个可用的 Base URL 和 Key以及一个确认能返回内容的模型 ID。接下来所有配置片段里的模型调用都指向这个出口。3. 可复制配置SSE 请求体、智能体选择与工具集合这一节给三份可直接复制的配置片段分别对应前端请求体、后端智能体选择和工具集合构建。路径和字段名尽量和 JoyAgent-JDGenie 原工程保持一致你对照改的时候不容易错位。第一份是前端 SSE 请求体。ChatView 里 sendMessage 最终会拼出这个 JSON注意 deepThink 是 0 或 1outputStyle 决定最终答案的格式{ sessionId: session_1234567890, requestId: req_1234567890, query: 查看一下北京明天的天气, deepThink: 0, outputStyle: html }requestId 必须唯一前端用 getUniqId() 生成。如果你复刻时用了固定值多个请求会串事件前端 updatePlan 会把两次查询的计划混在一起。sessionId 用来关联多轮对话单次调试可以随便填但不要为空。第二份是后端智能体选择逻辑。MultiAgentServiceImpl 的 buildAgentRequest 根据 deepThink 决定 agentTypedeepThink0 走 REACT(5)deepThink1 走 PLAN_SOLVE(3)private AgentRequest buildAgentRequest(GptQueryReq req) { AgentRequest request new AgentRequest(); request.setRequestId(req.getTraceId()); request.setErp(req.getUser()); request.setQuery(req.getQuery()); request.setAgentType(req.getDeepThink() 0 ? 5 : 3); request.setSopPrompt(request.getAgentType() 3 ? genieConfig.getGenieSopPrompt() : ); request.setBasePrompt(request.getAgentType() 5 ? genieConfig.getGenieBasePrompt() : ); request.setIsStream(true); request.setOutputStyle(req.getOutputStyle()); return request; }这里有个容易踩的坑agentType 是 int但 AgentType 枚举里 COMPREHENSIVE1、WORKFLOW2、PLAN_SOLVE3、ROUTER4、REACT5。如果你在别处用枚举名判断记得两边对齐否则会出现“选了 REACT 却加载了 PLAN_SOLVE 的 prompt”这种静默错误。第三份是工具集合构建。GenieController 里 buildToolCollection 按配置加载 search、code、report 三类工具再叠加 MCP 工具private ToolCollection buildToolCollection(AgentContext agentContext, AgentRequest request) { ToolCollection toolCollection new ToolCollection(); toolCollection.setAgentContext(agentContext); FileTool fileTool new FileTool(); fileTool.setAgentContext(agentContext); toolCollection.addTool(fileTool); ListString agentToolList Arrays.asList( genieConfig.getMultiAgentToolListMap() .getOrDefault(default, search,code,report).split(,)); if (agentToolList.contains(search)) { DeepSearchTool deepSearchTool new DeepSearchTool(); deepSearchTool.setAgentContext(agentContext); toolCollection.addTool(deepSearchTool); } if (agentToolList.contains(code)) { CodeInterpreterTool codeTool new CodeInterpreterTool(); codeTool.setAgentContext(agentContext); toolCollection.addTool(codeTool); } if (agentToolList.contains(report)) { ReportTool htmlTool new ReportTool(); htmlTool.setAgentContext(agentContext); toolCollection.addTool(htmlTool); } addMcpTools(toolCollection, agentContext); return toolCollection; }天气查询会命中 DeepSearchTool。如果你复刻时把 search 从 default 列表里去掉智能体找不到天气工具plan 事件里会出现空 task前端一直等 final_answer 直到超时。这三份配置对齐之后链路的前半段就固定了。4. 验证请求用 curl 观察 SSE 事件流与成功结果配置写完不要直接开前端先用 curl 打后端接口观察 SSE 事件流。JoyAgent-JDGenie 的接口路径是 /web/api/v1/gpt/queryAgentStreamIncrproduces 是 text/event-streamcurl -N -X POST http://localhost:8080/web/api/v1/gpt/queryAgentStreamIncr \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d { sessionId: session_debug_001, requestId: req_debug_001, query: 查看一下北京明天的天气, deepThink: 0, outputStyle: html }-N 关闭缓冲你能实时看到事件逐条打印。正常情况你会依次看到这些 data 行每行是一个 JSONdata: {packageType:plan_thought,resultMap:{planThought:需要查询北京明天天气}} data: {packageType:plan,resultMap:{plan:[识别查询意图,调用搜索工具,整理天气结果]}} data: {packageType:task,resultMap:{task:调用 DeepSearchTool 搜索北京明天天气}} data: {packageType:tool_call,resultMap:{toolName:DeepSearchTool,status:running}} data: {packageType:tool_call,resultMap:{toolName:DeepSearchTool,status:success}} data: {packageType:final_answer,resultMap:{answer:北京明天晴...}}成功结果的判断标准有三个final_answer 事件出现且 answer 字段非空tool_call 的 status 从 running 变成 success连接在 final_answer 之后由后端 completecurl 正常退出而不是卡住。如果 deepThink 改成 1你会先看到 plan_thought 里出现更长的规划文本然后 plan 事件里步骤更多因为走的是 PLAN_SOLVE 的三阶段循环。前端侧对应的处理在 ChatView 的 handleMessage 里按 packageType 分发switch (packageType) { case plan_thought: updatePlanThought(resultMap); break; case plan: updatePlan(resultMap); break; case task: updateCurrentTask(resultMap); break; case tool_call: updateToolCall(resultMap); break; case final_answer: updateFinalAnswer(resultMap); setLoading(false); break; }验证时如果 curl 能看到 final_answer 但前端不显示问题一定在 packageType 的字符串匹配上。后端推的是 final_answer前端 case 写成 finalAnswer 就永远进不去loading 也不会停。这个对照表建议你直接抄进注释。5. 本篇常见错排查401、local proxy failed 与 reading choices排错按报错原文对照下面四个是我在复刻 JoyAgent-JDGenie 调度链路时实际遇到过的。第一个是 401 Unauthorized。出现在 PlanningAgent 调 LLM 的阶段SSE 连接已经建立但 plan_thought 一直空后端日志里能看到 401。原因通常是模型出口的 Key 没配或配错。检查三件套Base URL 是不是 https://taotoken.net/api Key 是不是从 https://taotoken.net/api-keys 新建的Model ID 是不是当前账号可用的。三个都对还报 401就看请求头里 Authorization 有没有被别的拦截器覆盖。第二个是 local proxy failed。这个报错一般出现在你本地起了转发或代理层但目标地址填错。JoyAgent-JDGenie 后端用 OkHttp 调模型如果你在 application.yml 里配了自定义 endpoint确认它指向的是 https://taotoken.net/api 而不是带路径的完整 URL。路径多一层或少一层都会让 OkHttp 报连接失败SSE 侧表现为连接建立后立刻 onerror。第三个是 reading choices 相关报错典型信息是 Error reading choices 或 choices is null。这说明模型返回体不是预期的 OpenAI 风格结构PlanningAgent 解析 response.getContent() 时拿到 null。排查顺序先用 curl 直接打模型接口确认返回体里有 choices 数组再检查 JoyAgent-JDGenie 里 LiteLLM 或 OkHttp 的响应解析类看它取的字段名和实际返回是否一致。如果模型出口返回的是流式 chunk而解析类按非流式取 choices[0].message.content就会读不到。第四个是 OAuth 或鉴权跳转类报错。如果你在 Claude Code 或 Cline 里配置时选了 OAuth 登录而不是 API Key工具会尝试走浏览器授权但 JoyAgent-JDGenie 的后端调用是服务端到服务端没有浏览器环境就会卡在 OAuth 回调。统一改成 API Key 方式Base URL 填 https://taotoken.net/api Model ID 显式指定不要留空让它自动选。排查时建议开两个终端一个跑 curl -N 看 SSE 原始事件一个看后端日志。SSE 侧停在哪个 packageType就去对应 Agent 的 think/act 里加日志。plan_thought 空查 PlanningAgent 的 LLM 调用tool_call 卡 running 查工具实现final_answer 不出现查 SummaryAgent 的 summarize 返回。6. 把调度链路接进你的工程从验证到长期编码链路验证通过之后下一步是把它接进你日常的编码流程。JoyAgent-JDGenie 的调度逻辑集中在 MultiAgentServiceImpl 和几个 Agent 类里改这些代码时如果每次都要手动起前后端、点界面、看 SSE效率很低。我的做法是先用 curl 脚本固定住接口行为改完 Agent 逻辑直接跑脚本对比事件流差异确认 plan 和 tool_call 的顺序没变再开前端看渲染。如果你要长期迭代这类多智能体调度项目建议把模型出口和编码工具分开管理。模型出口用 TaoToken 的 API Key编码辅助可以用 Coding Plan 这类长期方案避免每次调试都重新配环境。接入文档在 https://taotoken.net/doc 里面有 Base URL、鉴权和常见模型 ID 的说明复刻 JoyAgent-JDGenie 时对照着填就行。最后留一个实用技巧SSE 事件里的 messageOrder 字段是递增的前端可以用它做乱序保护。网络抖动时事件可能不按顺序到达如果 updatePlan 先于 updatePlanThought 执行界面会闪一下。在 handleMessage 里加一个 lastOrder 变量只处理 order 大于 lastOrder 的事件就能避免这种闪烁。这个改动很小但在弱网环境下体验差别很明显。