我用Java后端的视角读了一遍JoyAgent-JDGenie

我用 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 queryAgentStreamIncrAutoAgent:确实存在两层 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 错误经常被包装成“正常结果”)
    • [五、如果让我重新拆 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 GitHub

我刚看到 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 querydeepThinkoutputStyle、会话和请求 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 决定交付物格式,也会影响后端装配哪些工具。前端的重点不是业务计算,而是把 taskIdmessageId 和事件类型对应的增量内容合并起来。

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() 是模板方法。它维护 currentStepmaxStepsAgentState,循环调用 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 工具 McpToolgenie-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) 写入用户消息,并用 maxStepsAgentState 控制循环
step() 留给子类实现单步策略,是模板方法的变化点
updateMemory() 把 USER、SYSTEM、ASSISTANT、TOOL 统一转换成 Message
executeTool() 解析模型生成的 JSON 参数,再交给 ToolCollection 路由
executeTools() 用线程池和 CountDownLatch 并行执行本轮多个 ToolCall

BaseAgent 本身并不知道 ReAct、Planner 或 Executor 的业务区别。ReactImplAgentPlanningAgentExecutorAgent 通过继承把不同 Prompt、Tool 集合和 think/act 行为塞进同一个循环。这种实现对 Java 开发很直观:入口就是一个 while,状态就是字段,调用关系可以顺着方法读到底。

不过它也把不少运行期责任压进了一个有状态对象。run() 开始只把状态设为 IDLE,没有统一把 currentStep 清零;达到步数上限后反而把状态改回 IDLEexecuteTools()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 字段、AgentContextMemory 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 支持 createupdatemark_stepfinish。这点我觉得设计得不错:计划不是只留在 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_weatherToolCollection 便能找到正确的下游地址。

当前实现每个 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 核心 DataAgentServiceNl2SqlService 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 queryAgentStreamIncrAutoAgent:确实存在两层 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;
  • ResponseResponseBodyBufferedReader 没有统一用 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 同时有 sessionIdrequestIdtraceIdGptProcessServiceImpl 又生成 traceId;buildAgentRequest() 把 traceId 写入 AgentRequest.requestIdAutoAgent 创建上下文时,再把 AgentContext.sessionIdrequestId 都设成这个 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。其中有可变的 currentTaskproductFilestaskProductFilesdigitalEmployeesPrinter

多个任务并发时可能出现:

  • 任务 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 开发来说,真正值得练习的,是怎样把这些已经跑通的能力改造成有权限、有预算、能取消、可恢复的任务运行时。

相关推荐
常山云栈19 分钟前
如何理解python中的鸭子模(类)型?
开发语言·python
SQL-First布道者23 分钟前
⚡ Spring JDBC 完整体系 · 第 5 讲 · Spring Boot Starter JDBC
java·spring boot·spring·mybatis·spring jdbc
y = xⁿ23 分钟前
DeepSeek harness工具系统什么设计的?
开发语言·python
宋哥转AI26 分钟前
深入理解 AI Agent · 多 Agent 编排 #03:Supervisor 与 Orchestrator——Dream-SaaS 双层编排架构拆解
java·人工智能·ai
luj_176827 分钟前
尾椎藏今生记忆?骨盆对应不确定性
开发语言·网络·c++·经验分享·算法
木井巳30 分钟前
【JavaEE】Spring Web MVC 入门
java·spring boot·spring·servlet·java-ee
ttwuai32 分钟前
Go 后台接入 CAS 单点登录后,原来的权限怎么继续生效?
开发语言·后端·golang
断点之下38 分钟前
C++类和对象:六个默认成员函数详解
开发语言·c++