我用 Java 后端的视角读了一遍 JoyAgent-JDGenie
-
- 一、这是个什么项目?------定位与核心痛点分析
-
- [1.1 它解决了什么"不爽"的问题?](#1.1 它解决了什么“不爽”的问题?)
- [1.2 项目的核心定位与边界](#1.2 项目的核心定位与边界)
- [1.3 我的第一印象与适用场景](#1.3 我的第一印象与适用场景)
- 二、模块分析
-
- [2.1 总览](#2.1 总览)
- [2.2 UI 模块:后端开发只要记住两个入口](#2.2 UI 模块:后端开发只要记住两个入口)
- [2.3 Backend 模块:一个进程里放了两套业务系统](#2.3 Backend 模块:一个进程里放了两套业务系统)
- [2.4 Tool 模块:不是工具本身,而是 Agent 的能力目录和路由器](#2.4 Tool 模块:不是工具本身,而是 Agent 的能力目录和路由器)
- [2.5 Client:代码不复杂,但位置比较敏感](#2.5 Client:代码不复杂,但位置比较敏感)
- 三、核心链路及关键点分析
-
- [3.1 先把整条请求串起来:HTTP、LLM、Tool、MCP 和 SSE 怎样交互](#3.1 先把整条请求串起来:HTTP、LLM、Tool、MCP 和 SSE 怎样交互)
- [3.2 BaseAgent:自研 Runtime 的最小执行内核](#3.2 BaseAgent:自研 Runtime 的最小执行内核)
- [3.3 自研 Runtime 与 LangChain、LangGraph:机制差异在哪里](#3.3 自研 Runtime 与 LangChain、LangGraph:机制差异在哪里)
- [3.4 ReAct:模型决定下一步,Java 控制别跑飞](#3.4 ReAct:模型决定下一步,Java 控制别跑飞)
- [3.5 Plan-and-Execute:外层编排,内层仍然是 ReAct](#3.5 Plan-and-Execute:外层编排,内层仍然是 ReAct)
- [3.6 内置 Tool 调用:从 JSON Schema 到 1601](#3.6 内置 Tool 调用:从 JSON Schema 到 1601)
- [3.7 MCP 工具发现](#3.7 MCP 工具发现)
- [3.8 MCP 工具调用](#3.8 MCP 工具调用)
- [3.9 SSE:同一个过程,项目里有三套事件模型](#3.9 SSE:同一个过程,项目里有三套事件模型)
- [3.10 轻量问数:Schema 召回、NL2SQL、JDBC 和图表](#3.10 轻量问数:Schema 召回、NL2SQL、JDBC 和图表)
- [3.11 深度问数:复用通用 Agent,而不是轻量链路加长版](#3.11 深度问数:复用通用 Agent,而不是轻量链路加长版)
- 四、当前系统存在争议点
-
- [4.1 `queryAgentStreamIncr` 和 `AutoAgent`:确实存在两层 SSE](#4.1
queryAgentStreamIncr和AutoAgent:确实存在两层 SSE) - [4.2 Java 写法的混乱,主要是"名字、类型和真实语义对不上"](#4.2 Java 写法的混乱,主要是“名字、类型和真实语义对不上”)
- [4.3 `GenieController` 做得太多](#4.3
GenieController做得太多) - [4.4 线程池静默拒绝,Latch 还可能永远等](#4.4 线程池静默拒绝,Latch 还可能永远等)
- [4.5 Plan 并发共享 `AgentContext`](#4.5 Plan 并发共享
AgentContext) - [4.6 Agent 状态不够像严格状态机](#4.6 Agent 状态不够像严格状态机)
- [4.7 错误经常被包装成"正常结果"](#4.7 错误经常被包装成“正常结果”)
- [4.1 `queryAgentStreamIncr` 和 `AutoAgent`:确实存在两层 SSE](#4.1
- [五、如果让我重新拆 Java Backend](#五、如果让我重新拆 Java Backend)
-
- [5.1 请求上下文和运行状态分开](#5.1 请求上下文和运行状态分开)
- [5.2 Tool 从"大接口"拆成描述和执行](#5.2 Tool 从“大接口”拆成描述和执行)
- [5.3 用一种事件连接 Memory、SSE 和观测](#5.3 用一种事件连接 Memory、SSE 和观测)
- [5.4 重构顺序不能反过来](#5.4 重构顺序不能反过来)
我刚看到 JoyAgent-JDGenie 时,最容易产生误解的地方,是把它和 Spring AI Alibaba、LangGraph、Dify、MCP、Mem0 之类的项目放在同一张功能表里比较。它们都属于 Agent 生态,却不一定交付同一种东西。
如果按"开源仓库最终交给开发者什么"来分,大致可以看成五类。Spring AI Alibaba、Coze SDK 这类项目主要提供模型接入和应用开发接口;Eko、OpenManus、OWL 等更偏 Agent 编排;Dify、n8n 一类项目把编排做成了带界面的应用平台或工作流平台;MCP、A2A、AG-UI 解决的是工具、Agent 或前后端之间怎样通信;Mem0、LlamaIndex 则集中处理记忆、数据接入和检索等能力。JDGenie 想强调的不同点,是把 UI、Java Backend、Agent Runtime、Tool、MCP、智能问数和报告交付放进同一套可运行源码里,交付形态更接近一个端到端 Agent 产品。
这套分类比单纯比较"是否开源"更有意义,但也不能机械照抄。很多项目会跨层:Dify 和 n8n 不只是一个代码框架,LlamaIndex 也不只是孤立的技术模块;"代码可见""采用开源许可证""可以独立部署""开放了完整产品链路"同样是四个不同问题。依赖某个云厂商生态也不是简单的是或否,还要看模型、存储、工具和部署环境能否替换。
所以我理解 JDGenie 的卖点,不是它比 SDK、框架或协议更先进,而是它把这些零件装成了一版可以直接阅读、运行和二次开发的产品。这个差别有点像 RuoYi 和 Spring Boot:RuoYi 的价值不是替代 Spring Boot,而是把一个后台系统的常见能力预先组装好。带着这个视角,我把项目里的 UI、Java Backend、Tool 和 MCP Client 几部分源码顺着读了一遍。
这就像拿一个已经做好的管理系统去和 Spring Boot 比。前者打开就有页面和业务功能,后者只是帮你开发应用。它们当然可以讨论,但不是同一层的东西。带着这个疑问,我把项目里的 UI、Java Backend、Tool 和 MCP Client 几部分源码顺着读了一遍。
一、这是个什么项目?------定位与核心痛点分析
1.1 它解决了什么"不爽"的问题?
自己从零做一个 Agent 应用,麻烦的往往不是调通一次模型接口。
一个能交给用户使用的产品,至少还要有对话页面、流式输出、任务规划、工具调用、文件管理、报告展示、异常处理。做数据问答的话,还要接元数据、Schema 召回、NL2SQL、数据库查询和图表。
用 LangGraph、Spring AI 或模型厂商 SDK 都能做,但这些能力不会自动长出来。框架提供零件,项目团队仍然要决定状态放在哪里、工具怎么注册、事件怎么推给前端、任务何时结束。
JDGenie 做的事,就是先把一版通用 Agent 产品组装出来。用户输入一句话,系统可以搜索资料、执行代码、生成文件,也可以走 Planner 和 Executor,把复杂任务拆成几步再执行。最终结果可以是网页、文档、PPT 或表格。
我觉得它更像"Agent 领域的 RuoYi"。这个类比不完全严谨,但比直接和 LangGraph 比完整度更容易理解:
| Java 应用开发 | Agent 应用开发 |
|---|---|
| Spring Boot、MyBatis | Spring AI、LangGraph、模型 SDK |
| RuoYi 这类通用应用底座 | JoyAgent-JDGenie |
| 企业自己的管理系统 | 基于 JDGenie 改出来的业务 Agent |
RuoYi 的价值不是取代 Spring Boot,而是把登录、权限、菜单、页面和常见后台能力先装好。JDGenie 也差不多。它把 UI、Agent Runtime、Tool、SSE 和文件交付先接了起来。
1.2 项目的核心定位与边界
为了不把不同层级混在一起,我现在习惯把 Agent 技术分成五层:
text
L4 具体 Agent 产品
JoyAgent-JDGenie、OpenHands
L3 Agent 应用平台
Dify、Coze
L2 Agent 编排框架
LangGraph、CrewAI、AutoGen
L1 AI 开发 SDK
Spring AI、LangChain、模型厂商 SDK
L0 基础模型与推理服务
GPT、Claude、DeepSeek、Qwen
#mermaid-svg-qyF6OhHKtHRuA66p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qyF6OhHKtHRuA66p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qyF6OhHKtHRuA66p .error-icon{fill:#552222;}#mermaid-svg-qyF6OhHKtHRuA66p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qyF6OhHKtHRuA66p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qyF6OhHKtHRuA66p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qyF6OhHKtHRuA66p .marker.cross{stroke:#333333;}#mermaid-svg-qyF6OhHKtHRuA66p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qyF6OhHKtHRuA66p p{margin:0;}#mermaid-svg-qyF6OhHKtHRuA66p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qyF6OhHKtHRuA66p .cluster-label text{fill:#333;}#mermaid-svg-qyF6OhHKtHRuA66p .cluster-label span{color:#333;}#mermaid-svg-qyF6OhHKtHRuA66p .cluster-label span p{background-color:transparent;}#mermaid-svg-qyF6OhHKtHRuA66p .label text,#mermaid-svg-qyF6OhHKtHRuA66p span{fill:#333;color:#333;}#mermaid-svg-qyF6OhHKtHRuA66p .node rect,#mermaid-svg-qyF6OhHKtHRuA66p .node circle,#mermaid-svg-qyF6OhHKtHRuA66p .node ellipse,#mermaid-svg-qyF6OhHKtHRuA66p .node polygon,#mermaid-svg-qyF6OhHKtHRuA66p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qyF6OhHKtHRuA66p .rough-node .label text,#mermaid-svg-qyF6OhHKtHRuA66p .node .label text,#mermaid-svg-qyF6OhHKtHRuA66p .image-shape .label,#mermaid-svg-qyF6OhHKtHRuA66p .icon-shape .label{text-anchor:middle;}#mermaid-svg-qyF6OhHKtHRuA66p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qyF6OhHKtHRuA66p .rough-node .label,#mermaid-svg-qyF6OhHKtHRuA66p .node .label,#mermaid-svg-qyF6OhHKtHRuA66p .image-shape .label,#mermaid-svg-qyF6OhHKtHRuA66p .icon-shape .label{text-align:center;}#mermaid-svg-qyF6OhHKtHRuA66p .node.clickable{cursor:pointer;}#mermaid-svg-qyF6OhHKtHRuA66p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qyF6OhHKtHRuA66p .arrowheadPath{fill:#333333;}#mermaid-svg-qyF6OhHKtHRuA66p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qyF6OhHKtHRuA66p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qyF6OhHKtHRuA66p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qyF6OhHKtHRuA66p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qyF6OhHKtHRuA66p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qyF6OhHKtHRuA66p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qyF6OhHKtHRuA66p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qyF6OhHKtHRuA66p .cluster text{fill:#333;}#mermaid-svg-qyF6OhHKtHRuA66p .cluster span{color:#333;}#mermaid-svg-qyF6OhHKtHRuA66p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qyF6OhHKtHRuA66p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qyF6OhHKtHRuA66p rect.text{fill:none;stroke-width:0;}#mermaid-svg-qyF6OhHKtHRuA66p .icon-shape,#mermaid-svg-qyF6OhHKtHRuA66p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qyF6OhHKtHRuA66p .icon-shape p,#mermaid-svg-qyF6OhHKtHRuA66p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qyF6OhHKtHRuA66p .icon-shape .label rect,#mermaid-svg-qyF6OhHKtHRuA66p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qyF6OhHKtHRuA66p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qyF6OhHKtHRuA66p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qyF6OhHKtHRuA66p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主要位于
内部自带轻量 Runtime
基础模型与推理服务
AI 开发 SDK
Agent 编排框架
Agent 应用平台
具体 Agent 产品
JoyAgent-JDGenie
JDGenie 的主要形态在 L4。它是一个能直接面向用户的应用,不是单纯的 Java 或 Python SDK。
不过它内部确实自己写了一部分 L2 能力,比如 BaseAgent 状态循环、ReAct、Plan-and-Execute、ToolCollection 和 Memory。所以更准确的说法是:
它是一个完整 Agent 应用,内部带着一套自研的轻量 Agent Runtime。
这个项目也有明确边界。它不是通用工作流引擎,不提供成熟的租户、权限、审批、任务恢复和版本治理。它也不是一个可靠的生产级代码沙箱。源码里的 H2、默认本地端口和若干调试接口,能看出它更偏向开源演示和二次开发起点。
1.3 我的第一印象与适用场景
只看源码,我的第一印象是"产品链路很完整,Java 工程细节比较粗"。
它适合拿来学习这些问题:
- 一个 Agent 产品怎样把 UI、LLM、Tool 和文件串起来;
- ReAct 和 Plan-and-Execute 在代码里是什么样;
- Tool Schema 怎样交给模型;
- MCP 工具怎样动态发现;
- Agent 执行过程怎样通过 SSE 实时展示。
它也适合做内部研究助手、报告生成、数据分析 Demo,或者作为二次开发的参考代码。
但如果要直接接生产数据库、开放代码执行、允许外部 MCP 工具,现有安全和并发设计还不够。对高合规、高并发、要求断点恢复的系统,我不会直接把原项目部署上去。
二、模块分析
2.1 总览
项目由四个进程组成:
| 模块 | 技术与端口 | 主要职责 |
|---|---|---|
ui |
React、Vite、TypeScript,:3000 |
对话输入、计划和工具过程展示、文件与报告预览 |
genie-backend |
Java 17、Spring Boot 3.2.2,:8080 |
Agent 编排、LLM 调用、Tool 路由、DataAgent、SSE |
genie-tool |
Python、FastAPI/Uvicorn,:1601 |
代码、搜索、报告、文件、数据分析和 NL2SQL |
genie-client |
Python、FastAPI,:8188 |
把 Java HTTP 请求转换为外部 MCP 调用 |
模块交互大致是:
#mermaid-svg-JAHeWll5guK4otn5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JAHeWll5guK4otn5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JAHeWll5guK4otn5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JAHeWll5guK4otn5 .error-icon{fill:#552222;}#mermaid-svg-JAHeWll5guK4otn5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JAHeWll5guK4otn5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JAHeWll5guK4otn5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JAHeWll5guK4otn5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JAHeWll5guK4otn5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JAHeWll5guK4otn5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JAHeWll5guK4otn5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JAHeWll5guK4otn5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JAHeWll5guK4otn5 .marker.cross{stroke:#333333;}#mermaid-svg-JAHeWll5guK4otn5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JAHeWll5guK4otn5 p{margin:0;}#mermaid-svg-JAHeWll5guK4otn5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JAHeWll5guK4otn5 .cluster-label text{fill:#333;}#mermaid-svg-JAHeWll5guK4otn5 .cluster-label span{color:#333;}#mermaid-svg-JAHeWll5guK4otn5 .cluster-label span p{background-color:transparent;}#mermaid-svg-JAHeWll5guK4otn5 .label text,#mermaid-svg-JAHeWll5guK4otn5 span{fill:#333;color:#333;}#mermaid-svg-JAHeWll5guK4otn5 .node rect,#mermaid-svg-JAHeWll5guK4otn5 .node circle,#mermaid-svg-JAHeWll5guK4otn5 .node ellipse,#mermaid-svg-JAHeWll5guK4otn5 .node polygon,#mermaid-svg-JAHeWll5guK4otn5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JAHeWll5guK4otn5 .rough-node .label text,#mermaid-svg-JAHeWll5guK4otn5 .node .label text,#mermaid-svg-JAHeWll5guK4otn5 .image-shape .label,#mermaid-svg-JAHeWll5guK4otn5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-JAHeWll5guK4otn5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JAHeWll5guK4otn5 .rough-node .label,#mermaid-svg-JAHeWll5guK4otn5 .node .label,#mermaid-svg-JAHeWll5guK4otn5 .image-shape .label,#mermaid-svg-JAHeWll5guK4otn5 .icon-shape .label{text-align:center;}#mermaid-svg-JAHeWll5guK4otn5 .node.clickable{cursor:pointer;}#mermaid-svg-JAHeWll5guK4otn5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JAHeWll5guK4otn5 .arrowheadPath{fill:#333333;}#mermaid-svg-JAHeWll5guK4otn5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JAHeWll5guK4otn5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JAHeWll5guK4otn5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JAHeWll5guK4otn5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JAHeWll5guK4otn5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JAHeWll5guK4otn5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JAHeWll5guK4otn5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JAHeWll5guK4otn5 .cluster text{fill:#333;}#mermaid-svg-JAHeWll5guK4otn5 .cluster span{color:#333;}#mermaid-svg-JAHeWll5guK4otn5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JAHeWll5guK4otn5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JAHeWll5guK4otn5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-JAHeWll5guK4otn5 .icon-shape,#mermaid-svg-JAHeWll5guK4otn5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JAHeWll5guK4otn5 .icon-shape p,#mermaid-svg-JAHeWll5guK4otn5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JAHeWll5guK4otn5 .icon-shape .label rect,#mermaid-svg-JAHeWll5guK4otn5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JAHeWll5guK4otn5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JAHeWll5guK4otn5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JAHeWll5guK4otn5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP / SSE
Chat Completions
HTTP / SSE
HTTP JSON
MCP over SSE
UI :3000
genie-backend :8080
外部 LLM
genie-tool :1601
genie-client :8188
外部 MCP Server
H2 元数据库
目标业务数据库
Qdrant / Elasticsearch
上图适合先建立进程级印象。如果继续展开 genie-backend,把两条业务链和外部依赖放到一张图里,完整调用架构是下面这样:
#mermaid-svg-Zi25Mi5ZZ7wIg71n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .error-icon{fill:#552222;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .marker.cross{stroke:#333333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n p{margin:0;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster-label text{fill:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster-label span{color:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster-label span p{background-color:transparent;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .label text,#mermaid-svg-Zi25Mi5ZZ7wIg71n span{fill:#333;color:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .node rect,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node circle,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node ellipse,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node polygon,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .rough-node .label text,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node .label text,#mermaid-svg-Zi25Mi5ZZ7wIg71n .image-shape .label,#mermaid-svg-Zi25Mi5ZZ7wIg71n .icon-shape .label{text-anchor:middle;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .rough-node .label,#mermaid-svg-Zi25Mi5ZZ7wIg71n .node .label,#mermaid-svg-Zi25Mi5ZZ7wIg71n .image-shape .label,#mermaid-svg-Zi25Mi5ZZ7wIg71n .icon-shape .label{text-align:center;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .node.clickable{cursor:pointer;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .arrowheadPath{fill:#333333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Zi25Mi5ZZ7wIg71n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zi25Mi5ZZ7wIg71n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster text{fill:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .cluster span{color:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Zi25Mi5ZZ7wIg71n rect.text{fill:none;stroke-width:0;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .icon-shape,#mermaid-svg-Zi25Mi5ZZ7wIg71n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .icon-shape p,#mermaid-svg-Zi25Mi5ZZ7wIg71n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .icon-shape .label rect,#mermaid-svg-Zi25Mi5ZZ7wIg71n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zi25Mi5ZZ7wIg71n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Zi25Mi5ZZ7wIg71n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Zi25Mi5ZZ7wIg71n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} genie-backend :8080
HTTP 回环 /AutoAgent
UI / 外部调用方
GenieController
DataAgentController
GptProcessService
MultiAgentService
AgentHandlerFactory
ReactImplAgent
PlanningAgent
ExecutorAgent
SummaryAgent
DataAgentService
Nl2SqlService
Schema / Table RAG
JdbcDataProvider
LLM Client
ToolCollection
SSEPrinter / ResponseHandler
ChatModelInfoService
genie-tool :1601
genie-client :8188
外部 LLM
外部 MCP Server
Qdrant / Elasticsearch
元数据库 H2
目标业务数据库
这张图最重要的不是类的数量,而是两条主链的边界。通用 Agent 从 GenieController 进入,经 Handler 选择 ReAct 或 Plan-and-Execute,再访问 LLM 和 Tool;轻量问数从 DataAgentController 进入,经 Schema 召回、NL2SQL 和 JDBC 返回数据。两条链共用同一个 Java 进程,却没有共用同一套运行内核。
这里最容易混淆的是 1601 和 8188。
genie-tool:1601 是项目自带的工具执行服务。genie-client:8188 自己没有工具数据,也不执行天气、地图之类的业务。它只是 MCP 协议桥接器。真正的 MCP 工具在 server_url 指向的外部 MCP Server 里。
2.2 UI 模块:后端开发只要记住两个入口
我是后端开发,所以 UI 不准备展开讲组件树。顺着 ChatView/index.tsx 和请求代码看下来,记住三件事就够了:五种展示模式最终收敛到两条执行链;UI 固定调用四个 Java 接口;utils/chat.ts 负责把 SSE 增量事件整理成页面状态。
| 用途 | Method / Path | 关键参数 | 响应 |
|---|---|---|---|
| 通用 Agent | POST /web/api/v1/gpt/queryAgentStreamIncr |
query、deepThink、outputStyle、会话和请求 ID |
SSE |
| 轻量问数 | POST /data/chatQuery |
content |
SSE |
| 模型目录 | GET /data/allModels |
无 | JSON |
| 数据预览 | GET /data/previewData?modelCode=... |
modelCode |
JSON |
#mermaid-svg-ArJ7JmdUvsPW5ouO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ArJ7JmdUvsPW5ouO .error-icon{fill:#552222;}#mermaid-svg-ArJ7JmdUvsPW5ouO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ArJ7JmdUvsPW5ouO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .marker.cross{stroke:#333333;}#mermaid-svg-ArJ7JmdUvsPW5ouO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ArJ7JmdUvsPW5ouO p{margin:0;}#mermaid-svg-ArJ7JmdUvsPW5ouO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster-label text{fill:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster-label span{color:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster-label span p{background-color:transparent;}#mermaid-svg-ArJ7JmdUvsPW5ouO .label text,#mermaid-svg-ArJ7JmdUvsPW5ouO span{fill:#333;color:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .node rect,#mermaid-svg-ArJ7JmdUvsPW5ouO .node circle,#mermaid-svg-ArJ7JmdUvsPW5ouO .node ellipse,#mermaid-svg-ArJ7JmdUvsPW5ouO .node polygon,#mermaid-svg-ArJ7JmdUvsPW5ouO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .rough-node .label text,#mermaid-svg-ArJ7JmdUvsPW5ouO .node .label text,#mermaid-svg-ArJ7JmdUvsPW5ouO .image-shape .label,#mermaid-svg-ArJ7JmdUvsPW5ouO .icon-shape .label{text-anchor:middle;}#mermaid-svg-ArJ7JmdUvsPW5ouO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .rough-node .label,#mermaid-svg-ArJ7JmdUvsPW5ouO .node .label,#mermaid-svg-ArJ7JmdUvsPW5ouO .image-shape .label,#mermaid-svg-ArJ7JmdUvsPW5ouO .icon-shape .label{text-align:center;}#mermaid-svg-ArJ7JmdUvsPW5ouO .node.clickable{cursor:pointer;}#mermaid-svg-ArJ7JmdUvsPW5ouO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .arrowheadPath{fill:#333333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ArJ7JmdUvsPW5ouO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ArJ7JmdUvsPW5ouO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ArJ7JmdUvsPW5ouO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster text{fill:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO .cluster span{color:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ArJ7JmdUvsPW5ouO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ArJ7JmdUvsPW5ouO rect.text{fill:none;stroke-width:0;}#mermaid-svg-ArJ7JmdUvsPW5ouO .icon-shape,#mermaid-svg-ArJ7JmdUvsPW5ouO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ArJ7JmdUvsPW5ouO .icon-shape p,#mermaid-svg-ArJ7JmdUvsPW5ouO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ArJ7JmdUvsPW5ouO .icon-shape .label rect,#mermaid-svg-ArJ7JmdUvsPW5ouO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ArJ7JmdUvsPW5ouO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ArJ7JmdUvsPW5ouO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ArJ7JmdUvsPW5ouO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} html / docs / ppt / table
DataAgent 非深度
DataAgent 深度
用户选择模式
通用 Agent SSE
轻量问数 SSE
utils/chat.ts 聚合事件
Plan / 工具过程 / 文件 / 图表
allModels / previewData
deepThink=0 走 ReAct,非零值走 Plan-and-Execute。outputStyle 决定交付物格式,也会影响后端装配哪些工具。前端的重点不是业务计算,而是把 taskId、messageId 和事件类型对应的增量内容合并起来。
2.3 Backend 模块:一个进程里放了两套业务系统
genie-backend 是 Spring MVC 单体,但业务上有两块:
text
通用多 Agent
├─ ReAct
├─ Plan-and-Execute
├─ Tool Calling
└─ Summary
DataAgent
├─ 数据模型元数据
├─ Schema / 列值召回
├─ NL2SQL
├─ 动态 JDBC
└─ 图表数据
#mermaid-svg-wHuCZ0lKXStjUQ85{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wHuCZ0lKXStjUQ85 .error-icon{fill:#552222;}#mermaid-svg-wHuCZ0lKXStjUQ85 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wHuCZ0lKXStjUQ85 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .marker.cross{stroke:#333333;}#mermaid-svg-wHuCZ0lKXStjUQ85 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wHuCZ0lKXStjUQ85 p{margin:0;}#mermaid-svg-wHuCZ0lKXStjUQ85 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster-label text{fill:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster-label span{color:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster-label span p{background-color:transparent;}#mermaid-svg-wHuCZ0lKXStjUQ85 .label text,#mermaid-svg-wHuCZ0lKXStjUQ85 span{fill:#333;color:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .node rect,#mermaid-svg-wHuCZ0lKXStjUQ85 .node circle,#mermaid-svg-wHuCZ0lKXStjUQ85 .node ellipse,#mermaid-svg-wHuCZ0lKXStjUQ85 .node polygon,#mermaid-svg-wHuCZ0lKXStjUQ85 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .rough-node .label text,#mermaid-svg-wHuCZ0lKXStjUQ85 .node .label text,#mermaid-svg-wHuCZ0lKXStjUQ85 .image-shape .label,#mermaid-svg-wHuCZ0lKXStjUQ85 .icon-shape .label{text-anchor:middle;}#mermaid-svg-wHuCZ0lKXStjUQ85 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .rough-node .label,#mermaid-svg-wHuCZ0lKXStjUQ85 .node .label,#mermaid-svg-wHuCZ0lKXStjUQ85 .image-shape .label,#mermaid-svg-wHuCZ0lKXStjUQ85 .icon-shape .label{text-align:center;}#mermaid-svg-wHuCZ0lKXStjUQ85 .node.clickable{cursor:pointer;}#mermaid-svg-wHuCZ0lKXStjUQ85 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .arrowheadPath{fill:#333333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wHuCZ0lKXStjUQ85 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wHuCZ0lKXStjUQ85 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wHuCZ0lKXStjUQ85 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster text{fill:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 .cluster span{color:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wHuCZ0lKXStjUQ85 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wHuCZ0lKXStjUQ85 rect.text{fill:none;stroke-width:0;}#mermaid-svg-wHuCZ0lKXStjUQ85 .icon-shape,#mermaid-svg-wHuCZ0lKXStjUQ85 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wHuCZ0lKXStjUQ85 .icon-shape p,#mermaid-svg-wHuCZ0lKXStjUQ85 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wHuCZ0lKXStjUQ85 .icon-shape .label rect,#mermaid-svg-wHuCZ0lKXStjUQ85 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wHuCZ0lKXStjUQ85 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wHuCZ0lKXStjUQ85 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wHuCZ0lKXStjUQ85 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通用任务
智能问数
HTTP / SSE 接入
业务分流
AgentHandlerFactory
ReactImplAgent
PlanningAgent + ExecutorAgent
LLM
ToolCollection
DataAgentService
Schema / 列值召回
NL2SQL
JDBC 查询
1601 / 8188 / 本地 Plan
它不是常见的 Controller → Service → DAO 三层。Agent 部分更像运行时:
text
HTTP 接入
→ Agent Handler
→ Agent 状态循环
→ LLM / Tool
→ Memory 和 SSE
Agent 类关系不复杂:
text
BaseAgent
├─ ReActAgent
│ ├─ ReactImplAgent
│ ├─ PlanningAgent
│ └─ ExecutorAgent
└─ SummaryAgent
BaseAgent.run() 是模板方法。它维护 currentStep、maxSteps 和 AgentState,循环调用 step()。ReActAgent.step() 固定为 think() 再 act()。
从 Java 设计模式看:
| 类 | 我对它的理解 |
|---|---|
BaseAgent |
模板方法和循环控制器 |
ReactImplAgent |
直接解决用户问题的 ReAct 执行器 |
PlanningAgent |
只维护计划的特殊 ReAct Agent |
ExecutorAgent |
执行具体子任务的 ReAct Agent |
SummaryAgent |
最终结果聚合器 |
AgentHandlerFactory |
执行模式策略工厂 |
AgentContext |
请求级共享上下文 |
Memory |
每个 Agent 的消息历史 |
比较意外的是,UI 主接口没有直接调用 Agent Runtime。它先走 MultiAgentServiceImpl,再通过 OkHttp 请求同一个 JVM 的 /AutoAgent:
text
/queryAgentStreamIncr
→ 外层 SseEmitter
→ HTTP http://127.0.0.1:8080/AutoAgent
→ 内层 AgentResponse SSE
→ Java 再解析
→ 转成 GptProcessResult SSE
→ UI
协议适配可以理解,但同 JVM 走本地 HTTP,我觉得有点绕。这个问题后面单独说。
2.4 Tool 模块:不是工具本身,而是 Agent 的能力目录和路由器
一开始我以为 ToolCollection 会直接调用 1601。继续跟到 ToolCollection.execute() 才发现,它只负责按名称找对象。
text
ToolCollection = 工具注册表 / Dispatcher
BaseTool = 工具 SPI
具体 Java Tool = 下游 Adapter
BaseTool 同时定义控制信息和执行入口:
java
public interface BaseTool {
String getName();
String getDescription();
Map<String, Object> toParams();
Object execute(Object input);
}
getName()、getDescription()、toParams() 用来生成给 LLM 的 Function Schema。execute() 才是真正的运行入口。
ToolCollection 有两张表:
text
toolMap
└─ Java 内置工具实例
mcpToolMap
└─ MCP 工具名、描述、Schema、serverUrl
执行时优先查 toolMap,再查 mcpToolMap。真正的下游有三种:
| 分支 | 路径 |
|---|---|
| 内置远程工具 | Java Tool → genie-tool:1601 |
| MCP 工具 | McpTool → genie-client:8188 → 外部 MCP Server |
| 本地状态工具 | PlanningTool → JVM 内存 Plan |
#mermaid-svg-3Buj0q8doslVMXbR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3Buj0q8doslVMXbR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3Buj0q8doslVMXbR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3Buj0q8doslVMXbR .error-icon{fill:#552222;}#mermaid-svg-3Buj0q8doslVMXbR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3Buj0q8doslVMXbR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3Buj0q8doslVMXbR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3Buj0q8doslVMXbR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3Buj0q8doslVMXbR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3Buj0q8doslVMXbR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3Buj0q8doslVMXbR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3Buj0q8doslVMXbR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3Buj0q8doslVMXbR .marker.cross{stroke:#333333;}#mermaid-svg-3Buj0q8doslVMXbR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3Buj0q8doslVMXbR p{margin:0;}#mermaid-svg-3Buj0q8doslVMXbR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3Buj0q8doslVMXbR .cluster-label text{fill:#333;}#mermaid-svg-3Buj0q8doslVMXbR .cluster-label span{color:#333;}#mermaid-svg-3Buj0q8doslVMXbR .cluster-label span p{background-color:transparent;}#mermaid-svg-3Buj0q8doslVMXbR .label text,#mermaid-svg-3Buj0q8doslVMXbR span{fill:#333;color:#333;}#mermaid-svg-3Buj0q8doslVMXbR .node rect,#mermaid-svg-3Buj0q8doslVMXbR .node circle,#mermaid-svg-3Buj0q8doslVMXbR .node ellipse,#mermaid-svg-3Buj0q8doslVMXbR .node polygon,#mermaid-svg-3Buj0q8doslVMXbR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3Buj0q8doslVMXbR .rough-node .label text,#mermaid-svg-3Buj0q8doslVMXbR .node .label text,#mermaid-svg-3Buj0q8doslVMXbR .image-shape .label,#mermaid-svg-3Buj0q8doslVMXbR .icon-shape .label{text-anchor:middle;}#mermaid-svg-3Buj0q8doslVMXbR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3Buj0q8doslVMXbR .rough-node .label,#mermaid-svg-3Buj0q8doslVMXbR .node .label,#mermaid-svg-3Buj0q8doslVMXbR .image-shape .label,#mermaid-svg-3Buj0q8doslVMXbR .icon-shape .label{text-align:center;}#mermaid-svg-3Buj0q8doslVMXbR .node.clickable{cursor:pointer;}#mermaid-svg-3Buj0q8doslVMXbR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3Buj0q8doslVMXbR .arrowheadPath{fill:#333333;}#mermaid-svg-3Buj0q8doslVMXbR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3Buj0q8doslVMXbR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3Buj0q8doslVMXbR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Buj0q8doslVMXbR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3Buj0q8doslVMXbR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Buj0q8doslVMXbR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3Buj0q8doslVMXbR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3Buj0q8doslVMXbR .cluster text{fill:#333;}#mermaid-svg-3Buj0q8doslVMXbR .cluster span{color:#333;}#mermaid-svg-3Buj0q8doslVMXbR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3Buj0q8doslVMXbR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3Buj0q8doslVMXbR rect.text{fill:none;stroke-width:0;}#mermaid-svg-3Buj0q8doslVMXbR .icon-shape,#mermaid-svg-3Buj0q8doslVMXbR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Buj0q8doslVMXbR .icon-shape p,#mermaid-svg-3Buj0q8doslVMXbR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3Buj0q8doslVMXbR .icon-shape .label rect,#mermaid-svg-3Buj0q8doslVMXbR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Buj0q8doslVMXbR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3Buj0q8doslVMXbR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3Buj0q8doslVMXbR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ToolCall
toolMap
mcpToolMap
planning
工具 name / description / schema
LLM
BaseAgent.executeTool
ToolCollection.execute
具体 BaseTool
genie-tool :1601
McpTool
genie-client :8188
外部 MCP Server
本地 Java Plan
Memory / SSE / Files
常见内置工具包括:
| Java Tool | 工具名 | 下游 |
|---|---|---|
CodeInterpreterTool |
code_interpreter |
/v1/tool/code_interpreter |
DeepSearchTool |
deep_search |
/v1/tool/deepsearch |
ReportTool |
report_tool |
/v1/tool/report |
FileTool |
file_tool |
/v1/file_tool/* |
DataAnalysisTool |
data_analysis |
/v1/tool/auto_analysis |
Tool 的返回也不是普通 Service 那种单一 DTO。一次执行可能同时产生:
text
返回文本 → 写入 Memory,给下一轮 LLM
过程事件 → Printer/SSE,给 UI
文件信息 → productFiles/taskProductFiles
这也是 Tool 代码副作用比较多的原因。
2.5 Client:代码不复杂,但位置比较敏感
genie-client 暴露的核心接口只有两个:
http
POST /v1/tool/list
POST /v1/tool/call
Java 传入 server_url。Python 用 MCP SDK 建立 SSE 会话,执行 list_tools() 或 call_tool(),随后把结果包装成 JSON 返回。
它没有数据库,也没有自己的业务数据。三类信息来自不同位置:
| 信息 | 来源 |
|---|---|
| MCP Server 地址 | Java application.yml |
| 工具名、描述、Schema | 外部 MCP Server 的 list_tools() |
| 工具结果 | 外部 MCP Server 背后的 API 或数据库 |
所以它就是一个协议 Adapter:
外部 MCP Server genie-client :8188 genie-backend 外部 MCP Server genie-client :8188 genie-backend #mermaid-svg-UYvo1rbhnMEhIYgF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UYvo1rbhnMEhIYgF .error-icon{fill:#552222;}#mermaid-svg-UYvo1rbhnMEhIYgF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UYvo1rbhnMEhIYgF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UYvo1rbhnMEhIYgF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UYvo1rbhnMEhIYgF .marker.cross{stroke:#333333;}#mermaid-svg-UYvo1rbhnMEhIYgF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UYvo1rbhnMEhIYgF p{margin:0;}#mermaid-svg-UYvo1rbhnMEhIYgF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UYvo1rbhnMEhIYgF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UYvo1rbhnMEhIYgF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-UYvo1rbhnMEhIYgF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-UYvo1rbhnMEhIYgF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-UYvo1rbhnMEhIYgF .sequenceNumber{fill:white;}#mermaid-svg-UYvo1rbhnMEhIYgF #sequencenumber{fill:#333;}#mermaid-svg-UYvo1rbhnMEhIYgF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-UYvo1rbhnMEhIYgF .messageText{fill:#333;stroke:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UYvo1rbhnMEhIYgF .labelText,#mermaid-svg-UYvo1rbhnMEhIYgF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .loopText,#mermaid-svg-UYvo1rbhnMEhIYgF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UYvo1rbhnMEhIYgF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-UYvo1rbhnMEhIYgF .noteText,#mermaid-svg-UYvo1rbhnMEhIYgF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-UYvo1rbhnMEhIYgF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UYvo1rbhnMEhIYgF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UYvo1rbhnMEhIYgF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UYvo1rbhnMEhIYgF .actorPopupMenu{position:absolute;}#mermaid-svg-UYvo1rbhnMEhIYgF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-UYvo1rbhnMEhIYgF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UYvo1rbhnMEhIYgF .actor-man circle,#mermaid-svg-UYvo1rbhnMEhIYgF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-UYvo1rbhnMEhIYgF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不保存业务数据,不执行具体工具 HTTP JSON(server_url, name, arguments) 建立 MCP SSE Session initialize + list_tools / call_tool MCP Result HTTP JSON(code, message, data)
逻辑不复杂,但它可以访问动态 server_url、透传 Cookie 和指定 Header。生产环境里,这种薄网关反而要认真做白名单、鉴权和日志脱敏。
三、核心链路及关键点分析
3.1 先把整条请求串起来:HTTP、LLM、Tool、MCP 和 SSE 怎样交互
前面按模块分别看,很容易知道每个类做什么,却还是说不清一次请求到底怎样跑完。我把通用 Agent 主链压成下面这张图:
#mermaid-svg-8MTbD2sYcGmShqxV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8MTbD2sYcGmShqxV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8MTbD2sYcGmShqxV .error-icon{fill:#552222;}#mermaid-svg-8MTbD2sYcGmShqxV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8MTbD2sYcGmShqxV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8MTbD2sYcGmShqxV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8MTbD2sYcGmShqxV .marker.cross{stroke:#333333;}#mermaid-svg-8MTbD2sYcGmShqxV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8MTbD2sYcGmShqxV p{margin:0;}#mermaid-svg-8MTbD2sYcGmShqxV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8MTbD2sYcGmShqxV .cluster-label text{fill:#333;}#mermaid-svg-8MTbD2sYcGmShqxV .cluster-label span{color:#333;}#mermaid-svg-8MTbD2sYcGmShqxV .cluster-label span p{background-color:transparent;}#mermaid-svg-8MTbD2sYcGmShqxV .label text,#mermaid-svg-8MTbD2sYcGmShqxV span{fill:#333;color:#333;}#mermaid-svg-8MTbD2sYcGmShqxV .node rect,#mermaid-svg-8MTbD2sYcGmShqxV .node circle,#mermaid-svg-8MTbD2sYcGmShqxV .node ellipse,#mermaid-svg-8MTbD2sYcGmShqxV .node polygon,#mermaid-svg-8MTbD2sYcGmShqxV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8MTbD2sYcGmShqxV .rough-node .label text,#mermaid-svg-8MTbD2sYcGmShqxV .node .label text,#mermaid-svg-8MTbD2sYcGmShqxV .image-shape .label,#mermaid-svg-8MTbD2sYcGmShqxV .icon-shape .label{text-anchor:middle;}#mermaid-svg-8MTbD2sYcGmShqxV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8MTbD2sYcGmShqxV .rough-node .label,#mermaid-svg-8MTbD2sYcGmShqxV .node .label,#mermaid-svg-8MTbD2sYcGmShqxV .image-shape .label,#mermaid-svg-8MTbD2sYcGmShqxV .icon-shape .label{text-align:center;}#mermaid-svg-8MTbD2sYcGmShqxV .node.clickable{cursor:pointer;}#mermaid-svg-8MTbD2sYcGmShqxV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8MTbD2sYcGmShqxV .arrowheadPath{fill:#333333;}#mermaid-svg-8MTbD2sYcGmShqxV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8MTbD2sYcGmShqxV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8MTbD2sYcGmShqxV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8MTbD2sYcGmShqxV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8MTbD2sYcGmShqxV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8MTbD2sYcGmShqxV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8MTbD2sYcGmShqxV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8MTbD2sYcGmShqxV .cluster text{fill:#333;}#mermaid-svg-8MTbD2sYcGmShqxV .cluster span{color:#333;}#mermaid-svg-8MTbD2sYcGmShqxV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8MTbD2sYcGmShqxV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8MTbD2sYcGmShqxV rect.text{fill:none;stroke-width:0;}#mermaid-svg-8MTbD2sYcGmShqxV .icon-shape,#mermaid-svg-8MTbD2sYcGmShqxV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8MTbD2sYcGmShqxV .icon-shape p,#mermaid-svg-8MTbD2sYcGmShqxV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8MTbD2sYcGmShqxV .icon-shape .label rect,#mermaid-svg-8MTbD2sYcGmShqxV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8MTbD2sYcGmShqxV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8MTbD2sYcGmShqxV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8MTbD2sYcGmShqxV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本机 HTTP POST
deepThink = 0
deepThink != 0
内置 Tool
MCP Tool
不再调用工具
UI 发起 queryAgentStreamIncr
创建外层 SseEmitter
MultiAgentServiceImpl
GptQueryReq 转 AgentRequest
AutoAgent 内层 SSE
创建 AgentContext
构建 ToolCollection
注册内置 Tool
向 8188 发现 MCP Tool
AgentHandlerFactory
ReactImplAgent
PlanningAgent + ExecutorAgent
LLM.askTool
Memory + Tool Schema
LLM 是否返回 ToolCall
Java Tool → genie-tool :1601
McpTool → genie-client :8188
→ 外部 MCP Server
Observation 写回 Memory
Summary / 最终结果
内层 AgentResponse SSE
AgentResponseHandler 转换协议
外层 GptProcessResult SSE
这张图里有两个容易误解的地方。
其一,LLM 不会直接访问 1601 或 MCP Server。Java 先把工具的名称、描述和 JSON Schema 放进模型请求。模型只返回"调用哪个工具、参数是什么",真正的网络请求仍由 Java 执行。
其二,内置 Tool 和 MCP 不是串行必经步骤,而是 ToolCollection 的两条候选路由。模型本轮可能选择搜索,也可能选择一个 MCP 天气工具,还可能不调用工具直接结束。工具结果进入 Memory 后,下一轮 LLM 才能看到观察结果并继续判断。
还有一个时间点值得注意:MCP 工具发现发生在 Agent 正式执行之前。/AutoAgent 每次构建 ToolCollection 时,都会向配置的 MCP Server 执行 list_tools()。所以 MCP Server 抖动不仅影响工具执行,也可能增加整次 Agent 的启动耗时。
3.2 BaseAgent:自研 Runtime 的最小执行内核
前面的总链路里,BaseAgent 很容易被一个方框带过。实际上它是通用 Agent 运行时的核心:Memory、步数、状态、单步多态和 Tool 并发,都在这个基类附近汇合。
它采用的是典型的模板方法写法。run() 定义不变的外层循环,具体子类通过 step() 决定每一步做什么;ReActAgent 又把 step() 固定成 think() 和 act()。
#mermaid-svg-Z0fL4A5K9RiFytAT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z0fL4A5K9RiFytAT .error-icon{fill:#552222;}#mermaid-svg-Z0fL4A5K9RiFytAT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z0fL4A5K9RiFytAT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z0fL4A5K9RiFytAT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z0fL4A5K9RiFytAT .marker.cross{stroke:#333333;}#mermaid-svg-Z0fL4A5K9RiFytAT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z0fL4A5K9RiFytAT p{margin:0;}#mermaid-svg-Z0fL4A5K9RiFytAT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster-label text{fill:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster-label span{color:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster-label span p{background-color:transparent;}#mermaid-svg-Z0fL4A5K9RiFytAT .label text,#mermaid-svg-Z0fL4A5K9RiFytAT span{fill:#333;color:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT .node rect,#mermaid-svg-Z0fL4A5K9RiFytAT .node circle,#mermaid-svg-Z0fL4A5K9RiFytAT .node ellipse,#mermaid-svg-Z0fL4A5K9RiFytAT .node polygon,#mermaid-svg-Z0fL4A5K9RiFytAT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z0fL4A5K9RiFytAT .rough-node .label text,#mermaid-svg-Z0fL4A5K9RiFytAT .node .label text,#mermaid-svg-Z0fL4A5K9RiFytAT .image-shape .label,#mermaid-svg-Z0fL4A5K9RiFytAT .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z0fL4A5K9RiFytAT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z0fL4A5K9RiFytAT .rough-node .label,#mermaid-svg-Z0fL4A5K9RiFytAT .node .label,#mermaid-svg-Z0fL4A5K9RiFytAT .image-shape .label,#mermaid-svg-Z0fL4A5K9RiFytAT .icon-shape .label{text-align:center;}#mermaid-svg-Z0fL4A5K9RiFytAT .node.clickable{cursor:pointer;}#mermaid-svg-Z0fL4A5K9RiFytAT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z0fL4A5K9RiFytAT .arrowheadPath{fill:#333333;}#mermaid-svg-Z0fL4A5K9RiFytAT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z0fL4A5K9RiFytAT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z0fL4A5K9RiFytAT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z0fL4A5K9RiFytAT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z0fL4A5K9RiFytAT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z0fL4A5K9RiFytAT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster text{fill:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT .cluster span{color:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z0fL4A5K9RiFytAT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z0fL4A5K9RiFytAT rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z0fL4A5K9RiFytAT .icon-shape,#mermaid-svg-Z0fL4A5K9RiFytAT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z0fL4A5K9RiFytAT .icon-shape p,#mermaid-svg-Z0fL4A5K9RiFytAT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z0fL4A5K9RiFytAT .icon-shape .label rect,#mermaid-svg-Z0fL4A5K9RiFytAT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z0fL4A5K9RiFytAT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z0fL4A5K9RiFytAT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z0fL4A5K9RiFytAT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
是
state = FINISHED
达到 maxSteps
抛异常
handler 调用 agent.run query
state 设为 IDLE
query 是否为空
Memory 追加 USER 消息
currentStep < maxSteps
且 state != FINISHED
currentStep + 1
多态调用 step
ReActAgent.step
think: Memory + Prompt + Tool Schema → LLM
是否需要 act
act: 执行 ToolCall
本轮多个 ToolCall
ThreadUtil 执行 Tool A
ThreadUtil 执行 Tool B
CountDownLatch 等待汇合
结果写回 Memory
返回末次 step 结果
currentStep 清零
state 回到 IDLE
state = ERROR 后继续抛出
从职责上拆,BaseAgent 做了五件事:
| 位置 | 作用 |
|---|---|
run(query) |
写入用户消息,并用 maxSteps 和 AgentState 控制循环 |
step() |
留给子类实现单步策略,是模板方法的变化点 |
updateMemory() |
把 USER、SYSTEM、ASSISTANT、TOOL 统一转换成 Message |
executeTool() |
解析模型生成的 JSON 参数,再交给 ToolCollection 路由 |
executeTools() |
用线程池和 CountDownLatch 并行执行本轮多个 ToolCall |
BaseAgent 本身并不知道 ReAct、Planner 或 Executor 的业务区别。ReactImplAgent、PlanningAgent 和 ExecutorAgent 通过继承把不同 Prompt、Tool 集合和 think/act 行为塞进同一个循环。这种实现对 Java 开发很直观:入口就是一个 while,状态就是字段,调用关系可以顺着方法读到底。
不过它也把不少运行期责任压进了一个有状态对象。run() 开始只把状态设为 IDLE,没有统一把 currentStep 清零;达到步数上限后反而把状态改回 IDLE;executeTools() 的 countDown() 不在 finally 中,await() 也没有超时。这里先理解机制,第四章再评价这些写法带来的问题。
3.3 自研 Runtime 与 LangChain、LangGraph:机制差异在哪里
需要先把比较对象说准:这里比较的是 JDGenie 内部的轻量 Agent Runtime ,不是拿整个 JDGenie 产品和 LangChain、LangGraph 比完整度。源码中 genie-backend 没有引入这些框架,它直接用 Java 类、继承、循环、Memory 和 ToolCollection 实现编排。
如果做一个概念映射,大致可以这样看:
#mermaid-svg-eP6PJeOvVXff2vs6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eP6PJeOvVXff2vs6 .error-icon{fill:#552222;}#mermaid-svg-eP6PJeOvVXff2vs6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eP6PJeOvVXff2vs6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eP6PJeOvVXff2vs6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eP6PJeOvVXff2vs6 .marker.cross{stroke:#333333;}#mermaid-svg-eP6PJeOvVXff2vs6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eP6PJeOvVXff2vs6 p{margin:0;}#mermaid-svg-eP6PJeOvVXff2vs6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster-label text{fill:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster-label span{color:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster-label span p{background-color:transparent;}#mermaid-svg-eP6PJeOvVXff2vs6 .label text,#mermaid-svg-eP6PJeOvVXff2vs6 span{fill:#333;color:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 .node rect,#mermaid-svg-eP6PJeOvVXff2vs6 .node circle,#mermaid-svg-eP6PJeOvVXff2vs6 .node ellipse,#mermaid-svg-eP6PJeOvVXff2vs6 .node polygon,#mermaid-svg-eP6PJeOvVXff2vs6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eP6PJeOvVXff2vs6 .rough-node .label text,#mermaid-svg-eP6PJeOvVXff2vs6 .node .label text,#mermaid-svg-eP6PJeOvVXff2vs6 .image-shape .label,#mermaid-svg-eP6PJeOvVXff2vs6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-eP6PJeOvVXff2vs6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eP6PJeOvVXff2vs6 .rough-node .label,#mermaid-svg-eP6PJeOvVXff2vs6 .node .label,#mermaid-svg-eP6PJeOvVXff2vs6 .image-shape .label,#mermaid-svg-eP6PJeOvVXff2vs6 .icon-shape .label{text-align:center;}#mermaid-svg-eP6PJeOvVXff2vs6 .node.clickable{cursor:pointer;}#mermaid-svg-eP6PJeOvVXff2vs6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eP6PJeOvVXff2vs6 .arrowheadPath{fill:#333333;}#mermaid-svg-eP6PJeOvVXff2vs6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eP6PJeOvVXff2vs6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eP6PJeOvVXff2vs6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eP6PJeOvVXff2vs6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eP6PJeOvVXff2vs6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eP6PJeOvVXff2vs6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster text{fill:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 .cluster span{color:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eP6PJeOvVXff2vs6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eP6PJeOvVXff2vs6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-eP6PJeOvVXff2vs6 .icon-shape,#mermaid-svg-eP6PJeOvVXff2vs6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eP6PJeOvVXff2vs6 .icon-shape p,#mermaid-svg-eP6PJeOvVXff2vs6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eP6PJeOvVXff2vs6 .icon-shape .label rect,#mermaid-svg-eP6PJeOvVXff2vs6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eP6PJeOvVXff2vs6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eP6PJeOvVXff2vs6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eP6PJeOvVXff2vs6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LangChain / LangGraph 常见抽象
JDGenie 自研 Runtime
执行循环
一步推理或动作
运行状态
路由与外层编排
工具注册和调用
BaseAgent.run 循环
ReActAgent.step
AgentContext + Memory
Handler / PlanSolveHandler
ToolCollection
Agent Executor / 预置 Agent
Node / Tool Node
Messages / Graph State
Graph + Edge + Router
Tool Registry
三者解决的是相近问题,但表达方式不同:
| 维度 | JDGenie 自研 Runtime | LangChain 常见 Agent 抽象 | LangGraph 常见图编排 |
|---|---|---|---|
| 控制流 | Java while、继承和方法调用 |
Agent/Tool 组件与执行器封装 | Node、Edge、条件路由组成显式状态图 |
| 单步表达 | step → think → act 固定在类层次中 |
模型调用、Tool 调用由 Agent 执行器组织 | 每个 Node 接收并返回部分 State |
| 状态 | Agent 字段、AgentContext、Memory |
Messages 与链式调用上下文 | 显式 Graph State,通常配合 reducer 合并 |
| 分支与循环 | 写在 Handler、if/while 和状态值里 |
多由预置 Agent 循环或自定义链控制 | Edge 和 conditional edge 明确表达 |
| 多 Agent | PlanSolveHandlerImpl 手工创建 Planner/Executor |
需要自行组合 Agent、Chain 或 Router | 可把 Agent 作为 Node/Subgraph 组合 |
| 持久化与恢复 | 当前实现主要在进程内,没有通用 checkpoint | 取决于具体组合和外部存储 | 图运行模型更适合接 checkpoint、恢复和人工中断 |
| 调试方式 | 顺着 Java 调用栈、日志和字段排查 | 观察 Chain/Agent/Tool 调用记录 | 可按节点、状态变更和边迁移观察 |
| 扩展成本 | 改源码最直接,但容易牵动共享上下文 | 组件生态丰富,需适应框架抽象 | 复杂流程更清楚,但要先设计 State 和图拓扑 |
我觉得 JDGenie 这种写法的优点是"没有黑盒感"。Java 开发能直接看到停止条件、ToolCall 怎么并发、结果怎样写回 Memory,也不必为了两三个固定节点引入一整套图运行时。做源码学习或流程较固定的 Agent,它很容易读懂和定制。
代价是流程一复杂,控制关系会散在继承树、Handler、共享字段和字符串协议里。节点不是一等对象,边也不是显式模型。要加 checkpoint、人工审批、失败后从指定步骤恢复,往往得自己补运行记录和恢复协议。LangGraph 的优势正是在这类长流程上:状态和迁移显式,节点容易隔离测试,也更适合持久化执行。LangChain 更偏向提供模型、Prompt、Tool、Retriever 和 Agent 等通用组件,减少适配代码;但复杂状态编排通常仍要借助图或工作流抽象。
这并不代表用了 LangGraph 就自动具备生产可靠性。权限、幂等、超时、预算、数据库只读、Tool 副作用和数据泄露仍然要由业务工程处理。选型可以简单一点:流程短、团队希望完全控制、现有实现已稳定时,自研循环未必有问题;分支多、运行时间长、需要恢复和人工介入时,显式图通常更省后期维护成本。
3.4 ReAct:模型决定下一步,Java 控制别跑飞
ReAct 链路可以压缩成:
text
Memory + Tool Schema
→ LLM.askTool()
→ ToolCall[]
→ ToolCollection.execute()
→ Tool 结果写回 Memory
→ 下一轮 LLM
完整一点是:
Tool ToolCollection LLM Memory ReactImplAgent User Tool ToolCollection LLM Memory ReactImplAgent User #mermaid-svg-GMZHoIoM5uBqExLX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GMZHoIoM5uBqExLX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GMZHoIoM5uBqExLX .error-icon{fill:#552222;}#mermaid-svg-GMZHoIoM5uBqExLX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GMZHoIoM5uBqExLX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GMZHoIoM5uBqExLX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GMZHoIoM5uBqExLX .marker.cross{stroke:#333333;}#mermaid-svg-GMZHoIoM5uBqExLX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GMZHoIoM5uBqExLX p{margin:0;}#mermaid-svg-GMZHoIoM5uBqExLX .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GMZHoIoM5uBqExLX text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GMZHoIoM5uBqExLX .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GMZHoIoM5uBqExLX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GMZHoIoM5uBqExLX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GMZHoIoM5uBqExLX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GMZHoIoM5uBqExLX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GMZHoIoM5uBqExLX .sequenceNumber{fill:white;}#mermaid-svg-GMZHoIoM5uBqExLX #sequencenumber{fill:#333;}#mermaid-svg-GMZHoIoM5uBqExLX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GMZHoIoM5uBqExLX .messageText{fill:#333;stroke:none;}#mermaid-svg-GMZHoIoM5uBqExLX .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GMZHoIoM5uBqExLX .labelText,#mermaid-svg-GMZHoIoM5uBqExLX .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GMZHoIoM5uBqExLX .loopText,#mermaid-svg-GMZHoIoM5uBqExLX .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GMZHoIoM5uBqExLX .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GMZHoIoM5uBqExLX .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GMZHoIoM5uBqExLX .noteText,#mermaid-svg-GMZHoIoM5uBqExLX .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GMZHoIoM5uBqExLX .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GMZHoIoM5uBqExLX .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GMZHoIoM5uBqExLX .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GMZHoIoM5uBqExLX .actorPopupMenu{position:absolute;}#mermaid-svg-GMZHoIoM5uBqExLX .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GMZHoIoM5uBqExLX .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GMZHoIoM5uBqExLX .actor-man circle,#mermaid-svg-GMZHoIoM5uBqExLX line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GMZHoIoM5uBqExLX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 有 ToolCall 没有 ToolCall loop currentStep \< maxSteps query 写入 USER 消息 Memory + System Prompt + Tool Schema content + ToolCall\[\] 写入 ASSISTANT 消息 execute(name, arguments) execute(arguments) observation 写入 TOOL 结果 state = FINISHED
这里模型负责判断,Java 负责控制。
模型可能一直调用工具,也可能参数写错。Java 需要限制最大步数、工具权限、超时和参数格式。BaseAgent 的存在,就是给不确定的模型行为套一个确定的循环边界。
有个细节容易漏掉:工具结果不一定都以 role=TOOL 写入 Memory。原生 Function Calling 会生成 TOOL Message;struct_parse 模式则把结果拼到上一条 Assistant 消息里。两种模式做的是同一件事,协议表达不同。
3.5 Plan-and-Execute:外层编排,内层仍然是 ReAct
我刚看 PlanSolveHandlerImpl 时,以为它实现了另一套 Agent 引擎。顺着继承关系看完才明白,Planner 和 Executor 自己也继承 ReActAgent。
它实际是两层循环:
text
外层 PlanSolveHandlerImpl
├─ PlanningAgent.run()
├─ ExecutorAgent.run()
├─ 把执行结果交回 PlanningAgent
└─ 继续下一轮或完成
内层每个 Agent
└─ think → act → observe
调用链大致是:
#mermaid-svg-YwmjBdCjYkmRHcZa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwmjBdCjYkmRHcZa .error-icon{fill:#552222;}#mermaid-svg-YwmjBdCjYkmRHcZa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwmjBdCjYkmRHcZa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwmjBdCjYkmRHcZa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwmjBdCjYkmRHcZa .marker.cross{stroke:#333333;}#mermaid-svg-YwmjBdCjYkmRHcZa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwmjBdCjYkmRHcZa p{margin:0;}#mermaid-svg-YwmjBdCjYkmRHcZa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster-label text{fill:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster-label span{color:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster-label span p{background-color:transparent;}#mermaid-svg-YwmjBdCjYkmRHcZa .label text,#mermaid-svg-YwmjBdCjYkmRHcZa span{fill:#333;color:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa .node rect,#mermaid-svg-YwmjBdCjYkmRHcZa .node circle,#mermaid-svg-YwmjBdCjYkmRHcZa .node ellipse,#mermaid-svg-YwmjBdCjYkmRHcZa .node polygon,#mermaid-svg-YwmjBdCjYkmRHcZa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YwmjBdCjYkmRHcZa .rough-node .label text,#mermaid-svg-YwmjBdCjYkmRHcZa .node .label text,#mermaid-svg-YwmjBdCjYkmRHcZa .image-shape .label,#mermaid-svg-YwmjBdCjYkmRHcZa .icon-shape .label{text-anchor:middle;}#mermaid-svg-YwmjBdCjYkmRHcZa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YwmjBdCjYkmRHcZa .rough-node .label,#mermaid-svg-YwmjBdCjYkmRHcZa .node .label,#mermaid-svg-YwmjBdCjYkmRHcZa .image-shape .label,#mermaid-svg-YwmjBdCjYkmRHcZa .icon-shape .label{text-align:center;}#mermaid-svg-YwmjBdCjYkmRHcZa .node.clickable{cursor:pointer;}#mermaid-svg-YwmjBdCjYkmRHcZa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YwmjBdCjYkmRHcZa .arrowheadPath{fill:#333333;}#mermaid-svg-YwmjBdCjYkmRHcZa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YwmjBdCjYkmRHcZa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YwmjBdCjYkmRHcZa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwmjBdCjYkmRHcZa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YwmjBdCjYkmRHcZa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwmjBdCjYkmRHcZa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster text{fill:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa .cluster span{color:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YwmjBdCjYkmRHcZa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YwmjBdCjYkmRHcZa rect.text{fill:none;stroke-width:0;}#mermaid-svg-YwmjBdCjYkmRHcZa .icon-shape,#mermaid-svg-YwmjBdCjYkmRHcZa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwmjBdCjYkmRHcZa .icon-shape p,#mermaid-svg-YwmjBdCjYkmRHcZa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YwmjBdCjYkmRHcZa .icon-shape .label rect,#mermaid-svg-YwmjBdCjYkmRHcZa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwmjBdCjYkmRHcZa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YwmjBdCjYkmRHcZa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YwmjBdCjYkmRHcZa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 还有任务
finish
用户问题
PlanningAgent
PlanningTool 创建 Java Plan
当前可执行任务
ExecutorAgent A
ExecutorAgent B
结构并不严格的文本结果
SummaryAgent
PlanningTool 支持 create、update、mark_step、finish。这点我觉得设计得不错:计划不是只留在 Prompt 文本里,而是存成 Java Plan 对象。
不过状态语义有点混。PlanningAgent 产出下一批任务后也会设置 FINISHED。这里的完成表示"当前 Planner 调用结束",不是"整个用户任务结束"。一个枚举值承担两层含义,读代码时容易误会。
Plan 模式还有两级并发:多个 Executor 可以并发,一个 Executor 内多个 ToolCall 也可以并发。这样能缩短部分任务耗时,也会放大线程数、LLM 并发、Token 消耗和共享状态竞争。
3.6 内置 Tool 调用:从 JSON Schema 到 1601
以 code_interpreter 为例,调用过程是:
Memory / UI / Files genie-tool :1601 CodeInterpreterTool LLM BaseAgent ToolCollection GenieController Memory / UI / Files genie-tool :1601 CodeInterpreterTool LLM BaseAgent ToolCollection GenieController #mermaid-svg-vd0vDgjUxTiz01HY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vd0vDgjUxTiz01HY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vd0vDgjUxTiz01HY .error-icon{fill:#552222;}#mermaid-svg-vd0vDgjUxTiz01HY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vd0vDgjUxTiz01HY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vd0vDgjUxTiz01HY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vd0vDgjUxTiz01HY .marker.cross{stroke:#333333;}#mermaid-svg-vd0vDgjUxTiz01HY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vd0vDgjUxTiz01HY p{margin:0;}#mermaid-svg-vd0vDgjUxTiz01HY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vd0vDgjUxTiz01HY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vd0vDgjUxTiz01HY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vd0vDgjUxTiz01HY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vd0vDgjUxTiz01HY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vd0vDgjUxTiz01HY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vd0vDgjUxTiz01HY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vd0vDgjUxTiz01HY .sequenceNumber{fill:white;}#mermaid-svg-vd0vDgjUxTiz01HY #sequencenumber{fill:#333;}#mermaid-svg-vd0vDgjUxTiz01HY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vd0vDgjUxTiz01HY .messageText{fill:#333;stroke:none;}#mermaid-svg-vd0vDgjUxTiz01HY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vd0vDgjUxTiz01HY .labelText,#mermaid-svg-vd0vDgjUxTiz01HY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vd0vDgjUxTiz01HY .loopText,#mermaid-svg-vd0vDgjUxTiz01HY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vd0vDgjUxTiz01HY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vd0vDgjUxTiz01HY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vd0vDgjUxTiz01HY .noteText,#mermaid-svg-vd0vDgjUxTiz01HY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vd0vDgjUxTiz01HY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vd0vDgjUxTiz01HY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vd0vDgjUxTiz01HY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vd0vDgjUxTiz01HY .actorPopupMenu{position:absolute;}#mermaid-svg-vd0vDgjUxTiz01HY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vd0vDgjUxTiz01HY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vd0vDgjUxTiz01HY .actor-man circle,#mermaid-svg-vd0vDgjUxTiz01HY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vd0vDgjUxTiz01HY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注册 CodeInterpreterTool messages + code_interpreter schema ToolCall(name, arguments) execute(name, arguments) execute(arguments) POST /v1/tool/code_interpreter SSE 中间事件 + 最终结果 更新文件并发送 UI 事件 observation 写入工具结果
模型看到的工具契约类似:
json
{
"type": "function",
"function": {
"name": "code_interpreter",
"description": "执行代码和数据处理",
"parameters": {
"type": "object",
"properties": {
"task": {"type": "string"}
},
"required": ["task"]
}
}
}
Tool Schema 只是告诉模型"能调什么、参数是什么"。它不会自己触发 HTTP。只有模型返回 ToolCall 后,具体 Tool 的 execute() 才会请求 1601。
Tool 结果回来后,Java 还会维护两种文件:
text
productFiles
= 后续工具可以使用的全部文件,包括内部材料
taskProductFiles
= 最终交付给用户的文件
例如 DeepSearchTool 搜完后还会创建 FileTool,把搜索材料和最终 Markdown 上传。看上去是一个搜索工具,实际上还负责文件化。这种实现方便,但职责边界不算干净。
3.7 MCP 工具发现
MCP 分成发现和执行两个阶段。发现发生在每次 /AutoAgent 构建 ToolCollection 时:
LLM ToolCollection 外部 MCP Server genie-client :8188 Java McpTool GenieController application.yml LLM ToolCollection 外部 MCP Server genie-client :8188 Java McpTool GenieController application.yml #mermaid-svg-YgLKnbRjuRPOjFmy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YgLKnbRjuRPOjFmy .error-icon{fill:#552222;}#mermaid-svg-YgLKnbRjuRPOjFmy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YgLKnbRjuRPOjFmy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YgLKnbRjuRPOjFmy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YgLKnbRjuRPOjFmy .marker.cross{stroke:#333333;}#mermaid-svg-YgLKnbRjuRPOjFmy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YgLKnbRjuRPOjFmy p{margin:0;}#mermaid-svg-YgLKnbRjuRPOjFmy .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YgLKnbRjuRPOjFmy text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YgLKnbRjuRPOjFmy .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-YgLKnbRjuRPOjFmy .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-YgLKnbRjuRPOjFmy #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-YgLKnbRjuRPOjFmy .sequenceNumber{fill:white;}#mermaid-svg-YgLKnbRjuRPOjFmy #sequencenumber{fill:#333;}#mermaid-svg-YgLKnbRjuRPOjFmy #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-YgLKnbRjuRPOjFmy .messageText{fill:#333;stroke:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YgLKnbRjuRPOjFmy .labelText,#mermaid-svg-YgLKnbRjuRPOjFmy .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .loopText,#mermaid-svg-YgLKnbRjuRPOjFmy .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YgLKnbRjuRPOjFmy .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-YgLKnbRjuRPOjFmy .noteText,#mermaid-svg-YgLKnbRjuRPOjFmy .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-YgLKnbRjuRPOjFmy .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YgLKnbRjuRPOjFmy .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YgLKnbRjuRPOjFmy .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YgLKnbRjuRPOjFmy .actorPopupMenu{position:absolute;}#mermaid-svg-YgLKnbRjuRPOjFmy .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-YgLKnbRjuRPOjFmy .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YgLKnbRjuRPOjFmy .actor-man circle,#mermaid-svg-YgLKnbRjuRPOjFmy line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-YgLKnbRjuRPOjFmy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每个 MCP Server mcp_server_url\[\] listTool(serverUrl) POST /v1/tool/list initialize + list_tools() name / description / inputSchema code / message / data 工具列表 JSON addMcpTool(..., serverUrl) 动态 tools\[\]
这条链的作用,是把一个运行时才知道的外部工具,转换成模型能理解的 Function Schema。
比如外部高德 MCP 返回:
json
{
"name": "maps_weather",
"description": "根据城市查询天气",
"inputSchema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
Java 会同时记录这个工具来自哪个 server_url。后面模型只返回 maps_weather,ToolCollection 便能找到正确的下游地址。
当前实现每个 Agent 请求都会重新发现一次,没有全局缓存。MCP 工具很多时,这会增加初始化延迟,也会把外部 MCP Server 的短暂抖动直接带到每次 Agent 请求里。
3.8 MCP 工具调用
假设用户问"上海天气怎么样",模型选择 maps_weather:
外部业务 API/数据 External MCP Server genie-client :8188 genie-backend LLM 外部业务 API/数据 External MCP Server genie-client :8188 genie-backend LLM #mermaid-svg-qSKyqjsj5U6e4lOS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qSKyqjsj5U6e4lOS .error-icon{fill:#552222;}#mermaid-svg-qSKyqjsj5U6e4lOS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qSKyqjsj5U6e4lOS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qSKyqjsj5U6e4lOS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qSKyqjsj5U6e4lOS .marker.cross{stroke:#333333;}#mermaid-svg-qSKyqjsj5U6e4lOS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qSKyqjsj5U6e4lOS p{margin:0;}#mermaid-svg-qSKyqjsj5U6e4lOS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qSKyqjsj5U6e4lOS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qSKyqjsj5U6e4lOS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qSKyqjsj5U6e4lOS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qSKyqjsj5U6e4lOS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qSKyqjsj5U6e4lOS .sequenceNumber{fill:white;}#mermaid-svg-qSKyqjsj5U6e4lOS #sequencenumber{fill:#333;}#mermaid-svg-qSKyqjsj5U6e4lOS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qSKyqjsj5U6e4lOS .messageText{fill:#333;stroke:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qSKyqjsj5U6e4lOS .labelText,#mermaid-svg-qSKyqjsj5U6e4lOS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .loopText,#mermaid-svg-qSKyqjsj5U6e4lOS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qSKyqjsj5U6e4lOS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qSKyqjsj5U6e4lOS .noteText,#mermaid-svg-qSKyqjsj5U6e4lOS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qSKyqjsj5U6e4lOS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qSKyqjsj5U6e4lOS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qSKyqjsj5U6e4lOS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qSKyqjsj5U6e4lOS .actorPopupMenu{position:absolute;}#mermaid-svg-qSKyqjsj5U6e4lOS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qSKyqjsj5U6e4lOS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qSKyqjsj5U6e4lOS .actor-man circle,#mermaid-svg-qSKyqjsj5U6e4lOS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qSKyqjsj5U6e4lOS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ToolCall(maps_weather, city=上海) ToolCollection 查询 mcpToolMap POST /v1/tool/call server_url + name + arguments MCP initialize + call_tool 查询天气数据 天气结果 MCP result code/message/data JSON 写入 Memory 的工具观察结果 下一轮 Memory + Tool result
从 genie-client 的视角:
text
上游:genie-backend
中间:genie-client
下游:server_url 指向的 MCP Server
真实数据:MCP Server 背后的系统
Java 到 8188 是普通同步 HTTP/JSON。8188 到外部 MCP Server 才是 MCP over SSE。这一点如果不分清,很容易以为 Java 直接在说 MCP。
3.9 SSE:同一个过程,项目里有三套事件模型
UI /queryAgentStreamIncr ResponseHandler /AutoAgent SSEPrinter Agent / Tool UI /queryAgentStreamIncr ResponseHandler /AutoAgent SSEPrinter Agent / Tool #mermaid-svg-zPvLgv69Uo61Q958{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zPvLgv69Uo61Q958 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zPvLgv69Uo61Q958 .error-icon{fill:#552222;}#mermaid-svg-zPvLgv69Uo61Q958 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zPvLgv69Uo61Q958 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zPvLgv69Uo61Q958 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zPvLgv69Uo61Q958 .marker.cross{stroke:#333333;}#mermaid-svg-zPvLgv69Uo61Q958 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zPvLgv69Uo61Q958 p{margin:0;}#mermaid-svg-zPvLgv69Uo61Q958 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zPvLgv69Uo61Q958 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zPvLgv69Uo61Q958 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zPvLgv69Uo61Q958 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zPvLgv69Uo61Q958 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zPvLgv69Uo61Q958 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zPvLgv69Uo61Q958 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zPvLgv69Uo61Q958 .sequenceNumber{fill:white;}#mermaid-svg-zPvLgv69Uo61Q958 #sequencenumber{fill:#333;}#mermaid-svg-zPvLgv69Uo61Q958 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zPvLgv69Uo61Q958 .messageText{fill:#333;stroke:none;}#mermaid-svg-zPvLgv69Uo61Q958 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zPvLgv69Uo61Q958 .labelText,#mermaid-svg-zPvLgv69Uo61Q958 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zPvLgv69Uo61Q958 .loopText,#mermaid-svg-zPvLgv69Uo61Q958 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zPvLgv69Uo61Q958 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zPvLgv69Uo61Q958 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zPvLgv69Uo61Q958 .noteText,#mermaid-svg-zPvLgv69Uo61Q958 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zPvLgv69Uo61Q958 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zPvLgv69Uo61Q958 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zPvLgv69Uo61Q958 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zPvLgv69Uo61Q958 .actorPopupMenu{position:absolute;}#mermaid-svg-zPvLgv69Uo61Q958 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zPvLgv69Uo61Q958 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zPvLgv69Uo61Q958 .actor-man circle,#mermaid-svg-zPvLgv69Uo61Q958 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zPvLgv69Uo61Q958 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} /data/chatQuery 另走 ChatDataMessage SSE send(messageType, data) AgentResponse SSE 本地 HTTP 回环消费事件 转换为 GptProcessResult 外层 SSE taskId + messageId + eventData
后端实际上有三套 SSE:
| 接口 | 载荷模型 | 使用方 |
|---|---|---|
/AutoAgent |
AgentResponse |
后端内部回环 |
/web/api/v1/gpt/queryAgentStreamIncr |
GptProcessResult |
UI 通用 Agent |
/data/chatQuery |
ChatDataMessage |
UI 智能问数 |
内置 Tool 还会直接调用 Printer.send() 推送代码、搜索、文件和报告事件。也就是说,工具执行有两路输出:一份给模型,一份给 UI。
这种设计能把完整过程展示出来,但事件协议散在 Agent、Tool、Printer 和 Handler 中。客户端断开时,底层 LLM、Tool、NL2SQL 也没有统一取消。我读到这里时最大的感觉是:它解决了"过程怎么展示",还没解决"整个任务生命周期由谁负责"。
3.10 轻量问数:Schema 召回、NL2SQL、JDBC 和图表
轻量问数不经过 BaseAgent、Planner 或 ToolCollection。它是一条相对固定的数据处理流水线:自然语言决定查什么,Java 决定允许使用哪些模型和字段,Python NL2SQL 服务负责生成 SQL,Java 再解析并执行。
业务数据库 JdbcDataProvider genie-tool :1601 Nl2SqlService TableRagService DataAgentService DataAgentController UI 业务数据库 JdbcDataProvider genie-tool :1601 Nl2SqlService TableRagService DataAgentService DataAgentController UI #mermaid-svg-VTUJGZtitiqS0s20{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VTUJGZtitiqS0s20 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VTUJGZtitiqS0s20 .error-icon{fill:#552222;}#mermaid-svg-VTUJGZtitiqS0s20 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VTUJGZtitiqS0s20 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VTUJGZtitiqS0s20 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VTUJGZtitiqS0s20 .marker.cross{stroke:#333333;}#mermaid-svg-VTUJGZtitiqS0s20 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VTUJGZtitiqS0s20 p{margin:0;}#mermaid-svg-VTUJGZtitiqS0s20 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VTUJGZtitiqS0s20 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VTUJGZtitiqS0s20 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VTUJGZtitiqS0s20 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VTUJGZtitiqS0s20 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VTUJGZtitiqS0s20 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VTUJGZtitiqS0s20 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VTUJGZtitiqS0s20 .sequenceNumber{fill:white;}#mermaid-svg-VTUJGZtitiqS0s20 #sequencenumber{fill:#333;}#mermaid-svg-VTUJGZtitiqS0s20 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VTUJGZtitiqS0s20 .messageText{fill:#333;stroke:none;}#mermaid-svg-VTUJGZtitiqS0s20 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VTUJGZtitiqS0s20 .labelText,#mermaid-svg-VTUJGZtitiqS0s20 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VTUJGZtitiqS0s20 .loopText,#mermaid-svg-VTUJGZtitiqS0s20 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VTUJGZtitiqS0s20 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VTUJGZtitiqS0s20 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VTUJGZtitiqS0s20 .noteText,#mermaid-svg-VTUJGZtitiqS0s20 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VTUJGZtitiqS0s20 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VTUJGZtitiqS0s20 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VTUJGZtitiqS0s20 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VTUJGZtitiqS0s20 .actorPopupMenu{position:absolute;}#mermaid-svg-VTUJGZtitiqS0s20 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VTUJGZtitiqS0s20 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VTUJGZtitiqS0s20 .actor-man circle,#mermaid-svg-VTUJGZtitiqS0s20 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VTUJGZtitiqS0s20 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 召回为空 召回成功 POST /data/chatQuery {content} webChatQueryData(req) 加载模型信息和基础 Schema 按问题召回相关字段 回退到数据库中的全部 Schema 合并默认召回字段 runNL2SQLSse(NL2SQLReq) POST /v1/tool/nl2sql THINK / finished_stream / DATA THINK 过程事件 Calcite 解析 SELECT 和模型编码 模型编码替换为物理表或子查询 执行真实 SQL Statement.executeQuery ResultSet QueryResult 推导维度、指标、过滤条件和图表配置 ChatQueryData\[\] CHART_DATA / READY
getBaseNl2SqlReq() 先把全部数据模型的编码、名称、业务 Prompt、类型和内容装进请求。enrichNl2Sql() 再通过 TableRagService 缩小字段范围;召回为空时,会回退到元数据库里的 Schema,而不是让请求直接失败。
NL2SQL 返回的内容也没有直接交给 JDBC。Nl2SqlService 会先检查返回码和数据,再用 Calcite 解析 SELECT,识别查询列、过滤条件和来源模型。模型编码随后被替换为配置中的物理表名或 SQL 子查询,才进入 JdbcDataProvider。
说实话,这条链更像"带 LLM 的查询编译器",而不是自由行动的 Agent。它的优点是步骤固定、容易约束;风险则集中在 Schema 权限、SQL AST 校验、查询超时、行数上限和数据库只读账号。这里用普通工作流反而比套一层 ReAct 更合适。
3.11 深度问数:复用通用 Agent,而不是轻量链路加长版
UI 选择 DataAgent 并开启深度模式时,并不会调用 /data/chatQuery。请求仍然进入通用 Agent 接口,只是带上 outputStyle=dataAgent 和非零 deepThink。
#mermaid-svg-FNwgzBehDgq3J0zk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FNwgzBehDgq3J0zk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FNwgzBehDgq3J0zk .error-icon{fill:#552222;}#mermaid-svg-FNwgzBehDgq3J0zk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FNwgzBehDgq3J0zk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FNwgzBehDgq3J0zk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FNwgzBehDgq3J0zk .marker.cross{stroke:#333333;}#mermaid-svg-FNwgzBehDgq3J0zk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FNwgzBehDgq3J0zk p{margin:0;}#mermaid-svg-FNwgzBehDgq3J0zk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FNwgzBehDgq3J0zk .cluster-label text{fill:#333;}#mermaid-svg-FNwgzBehDgq3J0zk .cluster-label span{color:#333;}#mermaid-svg-FNwgzBehDgq3J0zk .cluster-label span p{background-color:transparent;}#mermaid-svg-FNwgzBehDgq3J0zk .label text,#mermaid-svg-FNwgzBehDgq3J0zk span{fill:#333;color:#333;}#mermaid-svg-FNwgzBehDgq3J0zk .node rect,#mermaid-svg-FNwgzBehDgq3J0zk .node circle,#mermaid-svg-FNwgzBehDgq3J0zk .node ellipse,#mermaid-svg-FNwgzBehDgq3J0zk .node polygon,#mermaid-svg-FNwgzBehDgq3J0zk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FNwgzBehDgq3J0zk .rough-node .label text,#mermaid-svg-FNwgzBehDgq3J0zk .node .label text,#mermaid-svg-FNwgzBehDgq3J0zk .image-shape .label,#mermaid-svg-FNwgzBehDgq3J0zk .icon-shape .label{text-anchor:middle;}#mermaid-svg-FNwgzBehDgq3J0zk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FNwgzBehDgq3J0zk .rough-node .label,#mermaid-svg-FNwgzBehDgq3J0zk .node .label,#mermaid-svg-FNwgzBehDgq3J0zk .image-shape .label,#mermaid-svg-FNwgzBehDgq3J0zk .icon-shape .label{text-align:center;}#mermaid-svg-FNwgzBehDgq3J0zk .node.clickable{cursor:pointer;}#mermaid-svg-FNwgzBehDgq3J0zk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FNwgzBehDgq3J0zk .arrowheadPath{fill:#333333;}#mermaid-svg-FNwgzBehDgq3J0zk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FNwgzBehDgq3J0zk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FNwgzBehDgq3J0zk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FNwgzBehDgq3J0zk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FNwgzBehDgq3J0zk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FNwgzBehDgq3J0zk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FNwgzBehDgq3J0zk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FNwgzBehDgq3J0zk .cluster text{fill:#333;}#mermaid-svg-FNwgzBehDgq3J0zk .cluster span{color:#333;}#mermaid-svg-FNwgzBehDgq3J0zk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FNwgzBehDgq3J0zk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FNwgzBehDgq3J0zk rect.text{fill:none;stroke-width:0;}#mermaid-svg-FNwgzBehDgq3J0zk .icon-shape,#mermaid-svg-FNwgzBehDgq3J0zk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FNwgzBehDgq3J0zk .icon-shape p,#mermaid-svg-FNwgzBehDgq3J0zk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FNwgzBehDgq3J0zk .icon-shape .label rect,#mermaid-svg-FNwgzBehDgq3J0zk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FNwgzBehDgq3J0zk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FNwgzBehDgq3J0zk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FNwgzBehDgq3J0zk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DataAgent + 深度模式
queryAgentStreamIncr
AutoAgent
outputStyle = dataAgent
只装配 ReportTool
DataAnalysisTool
以及 MCP Tool
Plan-and-Execute
ExecutorAgent
DataAnalysisTool
POST :1601/v1/tool/auto_analysis
分析结果 / 数据文件
ReportTool
POST :1601/v1/tool/report
HTML / Markdown / PPT 等产物
SummaryAgent
通用 Agent SSE 返回 UI
这条链和轻量问数不是"同一套代码开了更多步骤",而是两套实现:
| 对比项 | 轻量问数 | 深度问数 |
|---|---|---|
| HTTP 入口 | /data/chatQuery |
/queryAgentStreamIncr |
| Java 核心 | DataAgentService、Nl2SqlService |
PlanSolveHandlerImpl、ExecutorAgent |
| 控制方式 | 固定流水线 | Planner + ReAct Tool Calling |
| 主要下游 | /v1/tool/nl2sql + JDBC |
/v1/tool/auto_analysis + /v1/tool/report |
| 主要输出 | 查询数据、维度指标、图表 | 分析过程、文件和报告 |
因此,轻量问数适合边界明确的即时查询;深度问数适合需要拆任务、反复分析并生成报告的场景。两者不能只按"快"和"慢"理解,权限与数据治理方式也不同:轻量链路的关键边界在 SQL 和数据库,深度链路还要额外控制 Tool 权限、文件流转、Token 和执行预算。
四、当前系统存在争议点
这些问题不是同一层级,但大多可以追到三类根因:任务生命周期缺少统一负责人、并发状态所有权不清楚、外部能力缺少严格边界。
#mermaid-svg-XqiPxoQCzJexIA7I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XqiPxoQCzJexIA7I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XqiPxoQCzJexIA7I .error-icon{fill:#552222;}#mermaid-svg-XqiPxoQCzJexIA7I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XqiPxoQCzJexIA7I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XqiPxoQCzJexIA7I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XqiPxoQCzJexIA7I .marker.cross{stroke:#333333;}#mermaid-svg-XqiPxoQCzJexIA7I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XqiPxoQCzJexIA7I p{margin:0;}#mermaid-svg-XqiPxoQCzJexIA7I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XqiPxoQCzJexIA7I .cluster-label text{fill:#333;}#mermaid-svg-XqiPxoQCzJexIA7I .cluster-label span{color:#333;}#mermaid-svg-XqiPxoQCzJexIA7I .cluster-label span p{background-color:transparent;}#mermaid-svg-XqiPxoQCzJexIA7I .label text,#mermaid-svg-XqiPxoQCzJexIA7I span{fill:#333;color:#333;}#mermaid-svg-XqiPxoQCzJexIA7I .node rect,#mermaid-svg-XqiPxoQCzJexIA7I .node circle,#mermaid-svg-XqiPxoQCzJexIA7I .node ellipse,#mermaid-svg-XqiPxoQCzJexIA7I .node polygon,#mermaid-svg-XqiPxoQCzJexIA7I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XqiPxoQCzJexIA7I .rough-node .label text,#mermaid-svg-XqiPxoQCzJexIA7I .node .label text,#mermaid-svg-XqiPxoQCzJexIA7I .image-shape .label,#mermaid-svg-XqiPxoQCzJexIA7I .icon-shape .label{text-anchor:middle;}#mermaid-svg-XqiPxoQCzJexIA7I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XqiPxoQCzJexIA7I .rough-node .label,#mermaid-svg-XqiPxoQCzJexIA7I .node .label,#mermaid-svg-XqiPxoQCzJexIA7I .image-shape .label,#mermaid-svg-XqiPxoQCzJexIA7I .icon-shape .label{text-align:center;}#mermaid-svg-XqiPxoQCzJexIA7I .node.clickable{cursor:pointer;}#mermaid-svg-XqiPxoQCzJexIA7I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XqiPxoQCzJexIA7I .arrowheadPath{fill:#333333;}#mermaid-svg-XqiPxoQCzJexIA7I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XqiPxoQCzJexIA7I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XqiPxoQCzJexIA7I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XqiPxoQCzJexIA7I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XqiPxoQCzJexIA7I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XqiPxoQCzJexIA7I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XqiPxoQCzJexIA7I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XqiPxoQCzJexIA7I .cluster text{fill:#333;}#mermaid-svg-XqiPxoQCzJexIA7I .cluster span{color:#333;}#mermaid-svg-XqiPxoQCzJexIA7I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XqiPxoQCzJexIA7I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XqiPxoQCzJexIA7I rect.text{fill:none;stroke-width:0;}#mermaid-svg-XqiPxoQCzJexIA7I .icon-shape,#mermaid-svg-XqiPxoQCzJexIA7I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XqiPxoQCzJexIA7I .icon-shape p,#mermaid-svg-XqiPxoQCzJexIA7I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XqiPxoQCzJexIA7I .icon-shape .label rect,#mermaid-svg-XqiPxoQCzJexIA7I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XqiPxoQCzJexIA7I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XqiPxoQCzJexIA7I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XqiPxoQCzJexIA7I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生命周期缺少统一负责人
同 JVM HTTP / 双层 SSE
Latch / Future 无统一 Deadline
失败被包装成正常结果
并发状态所有权不清
共享 AgentContext
状态和完成语义混用
外部能力边界偏薄
原始 SQL 调试接口
MCP / URL / Tool 权限
敏感内容进入日志
Java 语义和契约不清
命名与真实行为不一致
ID / Object / 字符串协议混用
测试保护不足
4.1 queryAgentStreamIncr 和 AutoAgent:确实存在两层 SSE
这条链可以明确写成:
Agent Runtime AutoAgent MultiAgentServiceImpl queryAgentStreamIncr Browser Agent Runtime AutoAgent MultiAgentServiceImpl queryAgentStreamIncr Browser #mermaid-svg-qzjhtF8teKEmN767{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qzjhtF8teKEmN767 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qzjhtF8teKEmN767 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qzjhtF8teKEmN767 .error-icon{fill:#552222;}#mermaid-svg-qzjhtF8teKEmN767 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qzjhtF8teKEmN767 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qzjhtF8teKEmN767 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qzjhtF8teKEmN767 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qzjhtF8teKEmN767 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qzjhtF8teKEmN767 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qzjhtF8teKEmN767 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qzjhtF8teKEmN767 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qzjhtF8teKEmN767 .marker.cross{stroke:#333333;}#mermaid-svg-qzjhtF8teKEmN767 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qzjhtF8teKEmN767 p{margin:0;}#mermaid-svg-qzjhtF8teKEmN767 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzjhtF8teKEmN767 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qzjhtF8teKEmN767 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qzjhtF8teKEmN767 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qzjhtF8teKEmN767 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qzjhtF8teKEmN767 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qzjhtF8teKEmN767 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qzjhtF8teKEmN767 .sequenceNumber{fill:white;}#mermaid-svg-qzjhtF8teKEmN767 #sequencenumber{fill:#333;}#mermaid-svg-qzjhtF8teKEmN767 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qzjhtF8teKEmN767 .messageText{fill:#333;stroke:none;}#mermaid-svg-qzjhtF8teKEmN767 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzjhtF8teKEmN767 .labelText,#mermaid-svg-qzjhtF8teKEmN767 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qzjhtF8teKEmN767 .loopText,#mermaid-svg-qzjhtF8teKEmN767 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qzjhtF8teKEmN767 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qzjhtF8teKEmN767 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qzjhtF8teKEmN767 .noteText,#mermaid-svg-qzjhtF8teKEmN767 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qzjhtF8teKEmN767 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzjhtF8teKEmN767 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzjhtF8teKEmN767 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzjhtF8teKEmN767 .actorPopupMenu{position:absolute;}#mermaid-svg-qzjhtF8teKEmN767 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qzjhtF8teKEmN767 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzjhtF8teKEmN767 .actor-man circle,#mermaid-svg-qzjhtF8teKEmN767 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qzjhtF8teKEmN767 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 建立外层 SSE searchForAgentRequest OkHttp POST 127.0.0.1:8080/AutoAgent 建立内层 SSE 异步执行 Agent AgentResponse 事件 data: AgentResponse 解析并转换为 GptProcessResult outerEmitter.send(result) data: GptProcessResult
需要说准一点:浏览器没有直接嵌套两条连接。浏览器只持有外层 SSE;Java 作为客户端消费 /AutoAgent 的内层 SSE,再把事件转换后发给浏览器。所以它是服务端内部 SSE 中继,不是前端协议层面的 SSE 套 SSE。
问题依然存在,而且不只是一点性能损耗:
- 同一个 JVM 为一个任务维护两条 HTTP/SSE 生命周期和两套序列化;
- 地址写死为
http://127.0.0.1:8080/AutoAgent,修改server.port、context path 或部署拓扑后会失效; - 外层 emitter 完成、超时或浏览器断开时,没有保存并取消内层 OkHttp
Call; onFailure、非 2xx、空 body 和解析异常多处只写日志,不会completeWithError()外层 emitter;/AutoAgent的执行线程 catch 到异常后也只写日志,没有结束内层 emitter;Response、ResponseBody和BufferedReader没有统一用 try-with-resources 管理;- 收到 finished 后虽然调用了外层
complete(),读取循环没有立即break,后续事件仍可能尝试发送。
这些路径组合起来,可能出现浏览器一直 loading、Agent 已经失败但连接等到超时、客户端退出后内部任务仍继续跑等现象。这里的"可能"是因为我只做了静态阅读,没有做断网和取消实验;但取消句柄没有保存、失败分支没有完成 emitter,是源码可以直接确认的。
MultiAgentServiceImpl#buildHttpRequest() 也很有歧义:
java
private Request buildHttpRequest(AgentRequest autoReq) {
String reqId = autoReq.getRequestId();
autoReq.setRequestId(autoReq.getRequestId());
String url = "http://127.0.0.1:8080/AutoAgent";
// 序列化并构造 POST Request
autoReq.setRequestId(reqId);
return new Request.Builder().url(url).post(body).build();
}
setRequestId(getRequestId()) 没有改变任何值,后面的"恢复"也恢复成原值。这像是旧逻辑删掉一半后留下的代码,会让读者误以为请求 ID 在内外协议间做过转换。方法名又过于宽泛:它并不是通用 HTTP Request Builder,而是把业务 DTO 序列化后,构造一个固定指向本机 /AutoAgent 的回环请求。
如果暂时保留回环,我至少会改成 buildAutoAgentLoopbackRequest(),删除无效赋值,并把地址、Header 和超时放进明确的 Gateway。更合适的做法仍然是:
text
两个 HTTP 入口
→ 同一个 AgentApplicationService
→ AgentOrchestrator 产生 AgentEvent
→ 两个协议适配器按需输出
SSE 留在系统边界,进程内不再拿 HTTP 当方法调用。
4.2 Java 写法的混乱,主要是"名字、类型和真实语义对不上"
源码里有一些问题单看都不大,放在 Agent 这种长链路里却会增加理解和维护成本。
请求 ID 的语义在链路中不断变化。 GptQueryReq 同时有 sessionId、requestId 和 traceId。GptProcessServiceImpl 又生成 traceId;buildAgentRequest() 把 traceId 写入 AgentRequest.requestId;AutoAgent 创建上下文时,再把 AgentContext.sessionId 和 requestId 都设成这个 requestId。原始 sessionId 没有作为独立字段进入 Agent Runtime。这样工具代码虽然读取的是 sessionId,拿到的实际仍是本轮 Agent 请求标识,多轮会话和单次追踪的含义被揉在一起。
枚举存在,入口却继续写魔法数字。 AgentType 已经定义了类型,但 buildAgentRequest() 仍写成 deepThink == 0 ? 5 : 3,并且任何非零值都进入 PlanSolve。代码能运行,读者却要在整数、枚举和 Handler 实现之间来回跳。
方法名与实现相反。 DataAgentService#getNl2SqlReqWithOutRecall() 和 getNl2SqlReq() 都调用了 enrichNl2Sql(),目前行为几乎相同。"WithoutRecall" 这个名字并没有兑现。DataAgentController 的 /queryModelInfo Java 方法也叫 vectorRecall(),而类里另有一个真正处理 /vectorRecall 的同名重载。
Java 命名和 Web 路由混在一起。 GenieController#AutoAgent() 用大写开头,更像类型名。URL 叫 /AutoAgent 不一定要改,但 Java 方法完全可以叫 autoAgent()。大小写本身不会制造故障,问题是它和回环地址、产品概念、运行时入口共用同一个模糊名字。
类型契约偏弱。 BaseTool.execute() 和 ToolCollection.execute() 返回 Object,实际调用链又经常把它当文本观察结果处理;失败则可能返回 null 或错误字符串。计划任务用 <sep>,消息类型用 "plan"、"task"、"finish" 等字符串。缺少类型后,编译器无法检查协议是否一致。
依赖管理方式不统一。 Controller 使用字段注入,工具由 Controller 手工 new,工具内部再通过 SpringContextHolder 取配置。结果是同一条链同时出现 Spring Bean、手工对象和 Service Locator,单元测试很难只替换一个下游 Gateway。
还有一些明显的代码残留。 runNL2SQLSync() 创建了从未被写入的 AtomicReference<Throwable>;buildDefaultAutobotsResult() 是未被调用的私有方法;ThreadUtil.await() 和 sleep() 吞掉异常及中断信号;HTTP 工具类有复用单例 Client、每次新建 Client 和手工读取 SSE 三套风格。它们不一定立即形成线上故障,但说明实现缺少一次系统性的收口。
我觉得这类代码最有迷惑性的地方,不是"不优雅",而是名字会让人建立错误的运行模型。Agent 系统本来就有请求、会话、任务、步骤、ToolCall 多级标识,如果 Java DTO 再把这些概念折叠,排查串话、重复执行和取消失败会很痛苦。
4.3 GenieController 做得太多
GenieController 不只是接收请求。它还创建 AgentContext、装配 Tool、发现 MCP、选择 Handler、创建线程和管理 SSE。
这会让协议层、业务编排和基础设施混在一起。新增一种工具要改 Controller,测试 MCP 发现也要绕过 Controller 的其他职责。
可以拆成:
text
AgentApplicationService
ToolRegistry
McpDiscoveryService
SseSessionManager
AgentHandlerFactory
需要说明的是,问题主要集中在 GenieController,不能说整个 Controller 包都是上帝类。DataAgentController 相对薄一些。
4.4 线程池静默拒绝,Latch 还可能永远等
ThreadUtil 的拒绝策略是空实现,而且 await() 与 sleep() 还会吞掉中断:
java
RejectedExecutionHandler handler = (r, executor) -> {};
任务过载时会被直接丢掉。调用方却使用没有超时的 CountDownLatch.await()。如果被拒绝的任务本该执行 countDown(),当前请求就可能一直挂着。
还有些 Worker 的 countDown() 没放在 finally。工具抛出异常也可能出现同样结果。
这个问题不是"代码风格不好",而是明确的可用性风险。吞掉 InterruptedException 后,线程即使收到取消信号也不会恢复中断标志。这里需要有界线程池、可观测的拒绝策略、finally、统一 Deadline 和可传播的取消。
4.5 Plan 并发共享 AgentContext
多个 Executor 共用一个请求级 AgentContext。其中有可变的 currentTask、productFiles、taskProductFiles、digitalEmployees 和 Printer。
多个任务并发时可能出现:
- 任务 A 覆盖任务 B 的当前任务;
- 文件归属和顺序错乱;
ArrayList并发写;- SSE 事件交叉;
- 汇总结果与计划顺序不一致。
简单换成线程安全集合不够。更合理的是每个并发任务有独立 TaskContext,执行结束后由父协调器按计划序号合并结果和文件副作用。
4.6 Agent 状态不够像严格状态机
AgentState 只有 IDLE/RUNNING/FINISHED/ERROR,但使用并不一致:
run()开始会设IDLE;- Planner 用
FINISHED表示暂时交回控制权; - ReAct 用
FINISHED表示整个执行完成; - 部分异常也会设置
FINISHED; - 达到最大步数后又回到
IDLE。
状态更像循环开关,不像业务状态。
我觉得应该拆开两个概念:
text
AgentExecutionState
CREATED / RUNNING / SUCCEEDED / FAILED / CANCELLED / TIMED_OUT
PlannerDecision
NEXT_TASKS / REPLAN / PLAN_COMPLETED
另一个坑是 BaseAgent.run() 没统一重置 currentStep。同一个 Executor 被多次复用时,步数预算可能跨任务累计。ExecutorAgent 还使用了 Planner 的 maxSteps 配置。这些都会让终止行为变得难以预测。
4.7 错误经常被包装成"正常结果"
项目里不少失败会退化为:
text
数据库异常 → 空集合
向量召回失败 → 空集合
Tool 失败 → "ToolX Error."
MCP 失败 → code=500 的 JSON 文本
SSE 发送失败 → 写日志后继续
模型并不知道这些字符串代表系统故障,可能继续推理,甚至由 SummaryAgent 包装成一段看似正常的结论。
Tool 和下游调用应该返回结构化结果,至少包含状态、错误码、是否可重试、阶段和耗时。业务无数据也要和系统失败区分开。
五、如果让我重新拆 Java Backend
前面已经列了不少问题,这里不再重复"哪些值得学、哪些不能抄"。我更想换个角度:如果保留现有产品能力,只重整 Java Backend,我会把它拆成什么样。
我不会按 Controller、Service、DAO 生硬切三层。Agent 不是普通 CRUD,真正需要分开的,是协议接入、任务编排、运行状态和外部能力。
#mermaid-svg-hRdRS0Njf6YwXcTh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hRdRS0Njf6YwXcTh .error-icon{fill:#552222;}#mermaid-svg-hRdRS0Njf6YwXcTh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hRdRS0Njf6YwXcTh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hRdRS0Njf6YwXcTh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hRdRS0Njf6YwXcTh .marker.cross{stroke:#333333;}#mermaid-svg-hRdRS0Njf6YwXcTh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hRdRS0Njf6YwXcTh p{margin:0;}#mermaid-svg-hRdRS0Njf6YwXcTh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster-label text{fill:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster-label span{color:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster-label span p{background-color:transparent;}#mermaid-svg-hRdRS0Njf6YwXcTh .label text,#mermaid-svg-hRdRS0Njf6YwXcTh span{fill:#333;color:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh .node rect,#mermaid-svg-hRdRS0Njf6YwXcTh .node circle,#mermaid-svg-hRdRS0Njf6YwXcTh .node ellipse,#mermaid-svg-hRdRS0Njf6YwXcTh .node polygon,#mermaid-svg-hRdRS0Njf6YwXcTh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hRdRS0Njf6YwXcTh .rough-node .label text,#mermaid-svg-hRdRS0Njf6YwXcTh .node .label text,#mermaid-svg-hRdRS0Njf6YwXcTh .image-shape .label,#mermaid-svg-hRdRS0Njf6YwXcTh .icon-shape .label{text-anchor:middle;}#mermaid-svg-hRdRS0Njf6YwXcTh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hRdRS0Njf6YwXcTh .rough-node .label,#mermaid-svg-hRdRS0Njf6YwXcTh .node .label,#mermaid-svg-hRdRS0Njf6YwXcTh .image-shape .label,#mermaid-svg-hRdRS0Njf6YwXcTh .icon-shape .label{text-align:center;}#mermaid-svg-hRdRS0Njf6YwXcTh .node.clickable{cursor:pointer;}#mermaid-svg-hRdRS0Njf6YwXcTh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hRdRS0Njf6YwXcTh .arrowheadPath{fill:#333333;}#mermaid-svg-hRdRS0Njf6YwXcTh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hRdRS0Njf6YwXcTh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hRdRS0Njf6YwXcTh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hRdRS0Njf6YwXcTh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hRdRS0Njf6YwXcTh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hRdRS0Njf6YwXcTh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster text{fill:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh .cluster span{color:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hRdRS0Njf6YwXcTh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hRdRS0Njf6YwXcTh rect.text{fill:none;stroke-width:0;}#mermaid-svg-hRdRS0Njf6YwXcTh .icon-shape,#mermaid-svg-hRdRS0Njf6YwXcTh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hRdRS0Njf6YwXcTh .icon-shape p,#mermaid-svg-hRdRS0Njf6YwXcTh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hRdRS0Njf6YwXcTh .icon-shape .label rect,#mermaid-svg-hRdRS0Njf6YwXcTh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hRdRS0Njf6YwXcTh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hRdRS0Njf6YwXcTh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hRdRS0Njf6YwXcTh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} UI / API Caller
AgentController / DataAgentController
AgentApplicationService
AgentOrchestrator
AgentRuntime
ReAct Engine
Plan Executor
Memory Manager
ToolRegistry
ToolExecutor
LlmGateway
GenieToolGateway :1601
McpGateway :8188
JdbcQueryGateway
AgentEventBus
SseEventAdapter
Metrics / Trace / Audit
FileRegistry
5.1 请求上下文和运行状态分开
现在的 AgentContext 既放请求信息,又放当前任务、文件、工具和 Printer。并发一上来,谁能改哪些字段就说不清了。
我会拆成三类对象:
text
AgentRequestContext(不可变)
├─ requestId / user / tenant
├─ query / deadline
└─ permission / budget
AgentRunState(由 Orchestrator 单点维护)
├─ plan / status / step
├─ memory / files
└─ usage / errors
TaskContext(每个并发任务独立)
├─ taskId / planIndex
├─ localMemory
└─ pendingEvents / pendingFiles
子任务只返回结构化结果。父级按 planIndex 合并,避免多个 Executor 直接修改共享列表。
5.2 Tool 从"大接口"拆成描述和执行
现在 BaseTool 同时负责 Schema 和执行,而且工具内部还能发 SSE、改文件列表。为了兼容现有模型调用,我会保留工具名和 Schema,但把副作用交给运行时处理:
java
interface ToolDescriptor {
String name();
String description();
JsonSchema inputSchema();
}
interface ToolExecutor {
ToolExecutionResult execute(ToolInvocation invocation);
}
ToolExecutionResult 明确区分成功、失败、超时、取消和是否可重试。文件和 UI 事件作为结构化产物返回,不再由 Tool 随手修改 AgentContext。
5.3 用一种事件连接 Memory、SSE 和观测
Tool 结果给模型,过程事件给 UI,指标给监控。这三件事现在散在不同类里。我会先在进程内定义类型化事件,不急着上 MQ:
text
ToolStarted
ToolCompleted
FileProduced
PlanUpdated
AgentFailed
AgentCompleted
MemoryProjector 决定哪些事件写进模型上下文,SseEventAdapter 决定前端看到什么,审计和指标监听器只处理自己关心的数据。这样不用再通过本地 HTTP 把一套 SSE 翻译成另一套 SSE。
5.4 重构顺序不能反过来
我不会先拆包和改类名。那样看着整齐,但最危险的问题还在。实际顺序应该是:
#mermaid-svg-pXJqyharZTfBmjJp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pXJqyharZTfBmjJp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pXJqyharZTfBmjJp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pXJqyharZTfBmjJp .error-icon{fill:#552222;}#mermaid-svg-pXJqyharZTfBmjJp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pXJqyharZTfBmjJp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pXJqyharZTfBmjJp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pXJqyharZTfBmjJp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pXJqyharZTfBmjJp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pXJqyharZTfBmjJp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pXJqyharZTfBmjJp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pXJqyharZTfBmjJp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pXJqyharZTfBmjJp .marker.cross{stroke:#333333;}#mermaid-svg-pXJqyharZTfBmjJp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pXJqyharZTfBmjJp p{margin:0;}#mermaid-svg-pXJqyharZTfBmjJp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pXJqyharZTfBmjJp .cluster-label text{fill:#333;}#mermaid-svg-pXJqyharZTfBmjJp .cluster-label span{color:#333;}#mermaid-svg-pXJqyharZTfBmjJp .cluster-label span p{background-color:transparent;}#mermaid-svg-pXJqyharZTfBmjJp .label text,#mermaid-svg-pXJqyharZTfBmjJp span{fill:#333;color:#333;}#mermaid-svg-pXJqyharZTfBmjJp .node rect,#mermaid-svg-pXJqyharZTfBmjJp .node circle,#mermaid-svg-pXJqyharZTfBmjJp .node ellipse,#mermaid-svg-pXJqyharZTfBmjJp .node polygon,#mermaid-svg-pXJqyharZTfBmjJp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pXJqyharZTfBmjJp .rough-node .label text,#mermaid-svg-pXJqyharZTfBmjJp .node .label text,#mermaid-svg-pXJqyharZTfBmjJp .image-shape .label,#mermaid-svg-pXJqyharZTfBmjJp .icon-shape .label{text-anchor:middle;}#mermaid-svg-pXJqyharZTfBmjJp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pXJqyharZTfBmjJp .rough-node .label,#mermaid-svg-pXJqyharZTfBmjJp .node .label,#mermaid-svg-pXJqyharZTfBmjJp .image-shape .label,#mermaid-svg-pXJqyharZTfBmjJp .icon-shape .label{text-align:center;}#mermaid-svg-pXJqyharZTfBmjJp .node.clickable{cursor:pointer;}#mermaid-svg-pXJqyharZTfBmjJp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pXJqyharZTfBmjJp .arrowheadPath{fill:#333333;}#mermaid-svg-pXJqyharZTfBmjJp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pXJqyharZTfBmjJp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pXJqyharZTfBmjJp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pXJqyharZTfBmjJp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pXJqyharZTfBmjJp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pXJqyharZTfBmjJp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pXJqyharZTfBmjJp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pXJqyharZTfBmjJp .cluster text{fill:#333;}#mermaid-svg-pXJqyharZTfBmjJp .cluster span{color:#333;}#mermaid-svg-pXJqyharZTfBmjJp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pXJqyharZTfBmjJp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pXJqyharZTfBmjJp rect.text{fill:none;stroke-width:0;}#mermaid-svg-pXJqyharZTfBmjJp .icon-shape,#mermaid-svg-pXJqyharZTfBmjJp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pXJqyharZTfBmjJp .icon-shape p,#mermaid-svg-pXJqyharZTfBmjJp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pXJqyharZTfBmjJp .icon-shape .label rect,#mermaid-svg-pXJqyharZTfBmjJp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pXJqyharZTfBmjJp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pXJqyharZTfBmjJp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pXJqyharZTfBmjJp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 封堵 SQL / 鉴权 / 日志泄密
Deadline / 取消 / 有界并发
隔离 TaskContext 与状态机
统一 ToolResult 与 AgentEvent
移除 JVM HTTP 回环
再拆 Controller 和 Gateway
补契约、并发和故障测试
这套目标结构不是为了让类图更漂亮。它要解决的是三个具体问题:一个任务由谁负责到结束、一个状态只能由谁修改、一次外部调用失败后系统该怎样收场。
源码读到这里,我对 JDGenie 的看法也更具体了。它不是拿来替代 LangGraph 的"更完整框架",也不只是套壳聊天页面。它把多 Agent、Tool、MCP、文件交付和数据问答接成了一个参考产品。对 Java 开发来说,真正值得练习的,是怎样把这些已经跑通的能力改造成有权限、有预算、能取消、可恢复的任务运行时。