# sagent **Repository Path**: hdwang123/sagent ## Basic Information - **Project Name**: sagent - **Description**: 小代理(智能体) - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-18 - **Last Updated**: 2026-08-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Sagent Sagent 是一个基于 Spring AI 2.0 的智能 Agent 示例项目,实现了多类型消息路由、工具调用、技能系统、多 Agent 编排、审计与 Token 成本监控等核心功能。 用户发送消息后,系统先调用大模型进行消息分类,再根据分类结果路由到普通聊天、RAG 知识库检索、审批技能、技能执行或通用技能执行流程。聊天模型通过 OpenAI 兼容接口调用(默认 DeepSeek,可切换 OpenRouter),Embedding 模型在本地 JVM 中运行。所有 LLM 调用由 Token 成本 Advisor 逐轮计量入账,敏感操作由 AOP 审计切面记录日志,管理页面可查询审计与成本明细。 ## 功能特性 - **智能消息分类**:支持 `CHAT`、`ASKILL`、`RAG`、`SKILL`、`GSKILL`、`MCP` 六种消息类型 - **统一结构化返回**:所有 Handler 统一返回 `HandlerResult(answer, sources, code)` 结构,业务状态码可被编排层(`MultiAgentService`)使用;工具/LLM 中间结果使用 `AgentResult(code, content)` 结构化模型承载,反序列化时容错处理 LLM 输出中 `code` 缺失/null/非法文本的异常形态(由 `AgentResultDeserializer` 归一化为 200,避免 `Cannot map null into type int` 导致技能整体失败) - **普通聊天**:基于 OpenAI 兼容接口的多轮对话能力 - **RAG 知识库检索**:本地 ONNX Embedding + `SimpleVectorStore` 实现高效检索 - **SKILL 企业固定技能**:提示词引导单次调用单一工具(仍走工具调用循环,仅约束 LLM 一次只选一个工具) - `WebPageDownloadSkill`:网页下载处理(截图、下载内容、下载媒体、压缩打包) - `DocumentSkill`:生成 Markdown 文档、读取已生成文档内容、生成文本文件 - **GSKILL 通用技能**:由大模型决定调用工具计划,支持多轮工具调用循环 - `DataBaseSkill`:H2 内存数据库查询 - `AlarmSkill`:获取时间、设置闹钟 - **ASKILL 审批技能**:敏感操作需人工审批,审批通过后自动执行 - `ApprovalSqlSkill`:产品删除、修改价格、修改库存(需审批) - 审批机制:`@Approval(enable=true)` 标记的方法被 LLM 调用时自动创建 PENDING 记录 - 审批面板:前端 Element UI 表格展示待审批项,支持批准/拒绝 - 自动执行:批准后通过 `ToolRegistry` 重新唤起原始工具方法执行业务逻辑 - 用户查询:内置 `getMyApprovals()`、`checkApprovalById()` 查询审批状态 - 上下文防御:审批切面校验会话上下文,ThreadLocal 丢失(线程复用/异常未清理)时拒绝创建无归属的审批记录 - **MCP 外部服务**:通过 MCP 协议调用外部工具(计算器、天气、股票查询等),采用延迟初始化,不影响主应用启动;工具清单由 `McpHandler` 运行时从 MCP Server 实时拉取(`getToolDescriptors()`),消息分类提示词动态生成,MCP Server 新增工具无需人工维护描述 - **文件管理**:支持生成的文档、图片和压缩包下载,图片显示缩略图,点击可下载原图;中文文件名通过 RFC 5987 `filename*` 编码,避免 Tomcat 丢弃含非 ASCII 字符的 `Content-Disposition` 响应头 - **多轮会话记忆**:基于 `MessageChatMemoryAdvisor` 的会话管理 - **多 Agent 编排**:Planner 拆解任务(含图校验 + 多轮记忆注入)→ Executor 按依赖并行执行(复用现有 Handler,含异常/超时/死锁兜底 + 失败纠偏:重试/重新规划/止损,4xx 不重试/5xx 才重试)→ 汇总 Agent 生成最终回答(含 try-catch 兜底),支持"查询数据 → 生成文档"等复合任务(详见附录章节) - **审计日志**:`@AuditLog` 注解 + `AuditAspect` AOP 切面,拦截敏感操作异步记录审计日志(操作类型、资源类型、耗时、成功/失败),用户身份从 ThreadLocal 会话上下文解析,无上下文时兜底 `unknown` - **Token 成本监控**:`TokenUsageCostAdvisor`(order=+400,位于工具循环内层)逐轮记录每次 LLM 往返的 token 消耗,解决 Spring AI `ToolCallingAdvisor` 只统计最后一轮的问题(GitHub issue #6411);`ModelPricing` 按模型定价换算人民币费用,`CostMonitorService` 异步落库,不阻塞主流程 - **管理页面**:`admin.html` 提供审计日志与 Token 成本查询(按用户/时间范围筛选、汇总 token 与费用),聊天页顶部图标一键跳转 - **前端界面**:Vue 2 + Element UI 聊天测试页面 + 管理页面 - **详细响应**:返回路由类型、分类理由和 RAG 来源 ## 技术栈 | 技术 | 版本或用途 | | --- | --- | | JDK | 21 | | Spring Boot | 4.1.0 | | Spring AI | 2.0.0 | | DeepSeek / OpenRouter | OpenAI 兼容聊天接口(默认 DeepSeek,可切换 OpenRouter) | | Transformers | 本地运行 ONNX Embedding | | SimpleVectorStore | 内存向量库 | | H2 | 内存数据库(产品、审批、审计、成本记录) | | Vue 2 / Element UI | 聊天页面 + 管理页面 | ## 工作流程 ```mermaid flowchart TD U["用户 / chat.html"] --> C["POST /ai/chat"] C --> R["大模型消息分类"] R --> D{"RouteDecision"} D -->|"CHAT"| CH["普通聊天"] D -->|"ASKILL"| AS["审批技能(敏感操作需人工审批)"] D -->|"RAG"| RA["本地向量检索 + 大模型回答"] D -->|"SKILL"| SK["技能执行(提示词引导单次工具调用)"] D -->|"GSKILL"| GS["通用技能执行(多轮工具调用)"] D -->|"MCP"| MC["MCP 外部服务 Tool Calling"] CH --> M["MessageChatMemoryAdvisor"] AS --> M RA --> M SK --> M GS --> M MC --> M M --> A["AgentResponse"] A --> U ``` **设计要点**: - 分类器会读取历史消息来理解上下文,但不会使用会自动写入消息的记忆 Advisor,避免把 `RouteDecision` 写入正式聊天记录。分类优先级:`SKILL > GSKILL > ASKILL > RAG > MCP > CHAT` - **双窗口记忆架构**:CHAT/RAG 使用大窗口记忆(20条消息),保证多轮对话连贯;SKILL/GSKILL/ASKILL/MCP 使用小窗口记忆(4条消息),让旧查询结果快速淘汰,迫使 LLM 重新调用工具获取最新数据 - SKILL 提示词引导单次调用单一工具(框架仍走工具调用循环,仅约束 LLM 一次只选一个工具;GSKILL 不约束,模型一次响应可含多个工具调用,框架会全部执行后回填继续循环) - GSKILL/MCP 工具调用循环由 Spring AI 的 `ToolCallingAdvisor` 自动处理 - ASKILL 敏感操作需人工审批,审批通过后通过 `ToolRegistry` 重新调用原始方法执行业务逻辑 ## 项目结构 ```text src/main/java/com/example/sagent ├─ agent │ ├─ core Agent 核心调度层 │ │ ├─ AgentHandler 处理器接口 │ │ ├─ HandlerRegistry 处理器注册中心(EnumMap) │ │ └─ AgentService Agent 服务(消息路由) │ ├─ approval 审批系统 │ │ ├─ Approval.java @Approval 注解 │ │ ├─ ApprovalAspect.java @Aspect 切面拦截 │ │ ├─ ApprovalService.java 审批核心服务 │ │ ├─ ApprovalRepository.java 审批记录查询仓库 │ │ ├─ ApprovalBypass.java ThreadLocal 绕过标志 │ │ ├─ ToolRegistry.java 工具注册表(审批通过后重新调用原始工具) │ │ └─ UserIdResolver.java 用户 ID 解析器 │ ├─ audit 审计日志 │ │ ├─ AuditLog.java @AuditLog 注解 │ │ ├─ AuditAspect.java @Aspect 切面拦截(异步记录操作日志) │ │ ├─ AuditLogEntity.java 审计记录实体 │ │ ├─ AuditLogRepository.java 审计记录查询仓库 │ │ ├─ OperationType.java 操作类型枚举 │ │ └─ ResourceType.java 资源类型枚举 │ ├─ cost 成本监控 │ │ ├─ TokenUsageCostAdvisor.java 逐轮 token 计量 Advisor │ │ ├─ CostMonitorService.java 成本监控服务(异步落库) │ │ ├─ CostRecord.java 成本记录实体 │ │ ├─ CostRecordRepository.java 成本记录查询仓库 │ │ └─ ModelPricing.java 模型定价表(人民币计价) │ ├─ handlers Agent 处理器实现 │ │ ├─ ChatHandler 普通聊天处理器 │ │ ├─ ASkillHandler ASKILL 审批技能处理器 │ │ ├─ RagHandler RAG 检索处理器 │ │ ├─ SkillHandler SKILL 企业技能处理器 │ │ ├─ GSkillHandler GSKILL 通用技能处理器 │ │ └─ McpHandler MCP 外部服务处理器 │ ├─ skills 技能实现 │ │ ├─ Skill SKILL 接口 │ │ ├─ GSkill GSKILL 接口 │ │ ├─ ASkill ASKILL 接口 │ │ ├─ ToolDescriptor 工具描述接口(供消息分类动态生成工具列表) │ │ ├─ DataBaseSkill 数据库查询技能(GSKILL) │ │ ├─ WebPageDownloadSkill 网页下载技能(SKILL) │ │ ├─ DocumentSkill 文档生成/读取技能(SKILL) │ │ ├─ AlarmSkill 闹钟技能(GSKILL) │ │ ├─ ApprovalSqlSkill 审批 SQL 技能(ASKILL) │ │ └─ ApprovalContext 审批上下文(ThreadLocal) │ ├─ storage 文件存储 │ │ └─ DownloadStorage 下载文件输出目录与URL前缀统一管理 │ ├─ tools 工具类 │ │ └─ VectorKnowledgeRetriever 向量知识库检索器 │ ├─ memory 会话记忆 │ │ ├─ ChatMemoryConfiguration 聊天记忆配置 │ │ └─ ConversationHistory 会话历史管理 │ ├─ model 数据模型 │ │ ├─ AgentType Agent 类型枚举 │ │ ├─ AgentResult 结构化返回(record: code + content) │ │ ├─ AgentResultDeserializer AgentResult 自定义反序列化器(code 缺失/null 容错) │ │ ├─ AgentResultParser AgentResult 解析器(returnDirect 场景) │ │ ├─ AgentResponse 响应模型 │ │ ├─ HandlerResult 处理器结果 │ │ ├─ RouteDecision 路由决策 │ │ ├─ Task 多Agent子任务(record) │ │ ├─ TaskPlan 多Agent任务计划(record) │ │ ├─ ApprovalRecord 审批记录(record 类型) │ │ └─ Product 产品实体 │ ├─ multi 多Agent编排 │ │ ├─ MultiAgentService 编排门面(串联 Planner → Executor → Aggregator) │ │ ├─ Planner 任务规划(plan/replan + 图校验 + 多轮记忆注入) │ │ ├─ TaskExecutor 按依赖分波次执行(调度 + 纠偏 + 止损 + 超时兜底) │ │ └─ Aggregator 结果汇总(含 try-catch 兜底 + 下载链接提取) │ └─ routing 消息路由 │ └─ MessageClassifier 消息分类器 └─ controller HTTP 接口 ├─ ChatController 聊天接口 ├─ ApprovalController 审批接口 ├─ DataController 数据查询接口(产品、审批列表) ├─ AuditController 审计与成本查询接口 └─ FileController 文件管理接口 src/main/resources ├─ embedding 内嵌 ONNX Embedding 模型 ├─ knowledge 本地知识库文档 ├─ static chat.html、admin.html 及前端依赖 ├─ schema.sql H2 表结构 ├─ data.sql H2 演示数据 └─ application.yml 应用配置 ``` ## 测试 112 个单元/集成测试(14 个测试类)覆盖消息分类、6 个 Handler、审批系统、审计/成本监控(模型定价、Token Advisor 逐轮计量)、任务编排(Planner 图校验、TaskExecutor 调度/纠偏)、多 Agent 全链路集成、AgentResult 解析与容错反序列化等核心逻辑。 ```bash # 需使用 JDK 21 运行 mvn test -pl agentdemo ``` 多 Agent 全链路集成测试(`MultiAgentIntegrationTest`)不启动 Spring 上下文,手动装配真实 Planner → TaskExecutor(真实线程池)→ Aggregator → HandlerRegistry → 会话记忆,仅 mock LLM 输出,覆盖依赖分波次执行、失败传播、空计划降级、多轮记忆闭环与复合会话 ID 等编排行为。 ## 运行项目 ### 环境要求 - JDK 21 - Maven 3.9+ - DeepSeek API Key(或切换 OpenRouter 后使用 OpenRouter API Key) 不需要安装 Ollama、Python、Node.js、MySQL 或 Redis。 ### 配置模型服务(DeepSeek) 项目默认使用 DeepSeek 调用大模型,必须设置环境变量: ```text DEEPSEEK_API_KEY ``` 默认模型 `deepseek-v4-flash`。 **切换 OpenRouter**:将 `application.yml` 中 `spring.ai.openai` 配置的 DeepSeek 部分注释掉,取消 OpenRouter 部分注释,然后设置: ```text OPENROUTER_API_KEY ``` 可通过 `OPENROUTER_MODEL` 指定模型(默认 `openrouter/free`)。 **安全提示**:不要把真实 API Key 写入 `application.yml` 或提交到 Git。 ### 启动 MCP Server(可选) MCP 功能采用**延迟初始化**设计:agentdemo 启动时不会连接 MCP Server,只在首次收到 MCP 类型请求时才建立连接。因此无需 MCP 时可直接启动 agentdemo。 如需测试 MCP 功能,先启动 MCP Server: ```bash cd mcpserver mvn spring-boot:run ``` MCP Server 默认监听 `http://localhost:8081/mcp`,提供以下工具: - `calculator`: 计算器(支持加减乘除) - `get_weather`: 获取指定城市天气(北京/上海/广州/深圳/成都) - `get_stock_price`: 获取股票实时价格(AAPL/GOOGL/MSFT/TSLA/NVDA/BABA/JD) - `get_system_info`: 获取系统信息 - `echo`: 回显消息(测试用) MCP Server 地址通过 `mcp.server.url` 配置(默认 `http://localhost:8081/mcp`),可在 `application.yml` 中覆盖。 ### 启动 Agent Demo **Windows PowerShell**: ```powershell $env:OPENROUTER_API_KEY = "你的真实Key" cd agentdemo mvn spring-boot:run ``` **macOS / Linux**: ```bash export OPENROUTER_API_KEY="你的真实Key" cd agentdemo mvn spring-boot:run ``` **IDEA 配置**: 将 Project SDK 设置为 JDK 21,并在 `Run -> Edit Configurations -> Environment variables` 中添加环境变量。 ## 聊天页面 启动后访问: ```text http://localhost:8080/chat.html ``` 页面功能: - 多轮 Agent 对话 - 多Agent编排开关:开启后走 `/ai/multi-agent`,由 Planner 拆解任务并行执行并汇总 - 路由类型和分类原因展示 - RAG 来源展示 - 请求耗时展示 - 停止请求 - 清空页面和服务端会话记忆 - 下载链接渲染(SKILL 生成的文件,图片显示缩略图) - 审批面板(查看待审批项,支持批准/拒绝) - 产品查询面板(查看当前数据库产品,用于核对审批操作结果) - 审计日志 / Token 监控入口(顶部图标,跳转管理页面) 页面使用项目内的 Vue 和 Element UI 资源,不需要前端构建。 ### 管理页面 ```text http://localhost:8080/admin.html?tab=audit ``` 两个 Tab: - **审计日志**(`/api/admin/audit/list`):按用户/时间范围筛选操作日志,展示操作类型、资源类型、耗时与成功/失败状态 - **Token 监控**(`/api/admin/audit/cost`):按用户/时间范围查询 LLM 调用成本,汇总 input/output token 与人民币费用明细 ## 测试示例 ### 普通聊天 ```text 你好,请介绍一下你自己。 ``` ### RAG 知识库查询 ```text OPENROUTER_API_KEY 在哪里配置? Why was 1998 SH2 reclassified as a comet? What does WHO recommend to reduce dementia risk? ``` ### 数据库查询(GSKILL) ```text 数据库里有多少个产品? 查询价格不超过 70 元的产品。 ``` ### SKILL 网页下载与文档生成 ```text 下载这个网页 https://example.com 并生成文档。 截取百度首页的截图。 下载网页中的图片。 抓取网页内容并转换为 Markdown。 生成一份关于产品介绍的报告文档。 ``` ### GSKILL 通用技能 ```text 现在几点了? 帮我设置一个5分钟后的闹钟。 ``` ### ASKILL 审批技能 ```text 删除产品 3 修改产品 1 的价格为 199 查询我的审批状态 查看编号 xxx 的审批 ``` ### MCP 外部服务 ```text 帮我算一下 123 + 456 查询北京的天气 苹果股价现在多少? 获取系统信息 ``` ### 多轮记忆 ```text 第一轮:介绍一下 NASA 的那篇新闻。 第二轮:它为什么被重新分类? ``` ### 多 Agent 编排 需要先在聊天页面开启"多Agent"开关: ```text 查询所有产品的信息,然后生成一份markdown文档保存下来。 根据知识库介绍Sagent项目,并生成一份总结文档。 ``` ## 注意事项 - 会话记忆、向量库和 H2 数据都保存在内存中,应用重启后会清空(含审计日志与成本记录) - 审计/成本记录异步写入:`AuditAspect` 与 `CostMonitorService` 均不阻塞主流程,写入失败仅记日志不影响业务 - 双窗口记忆架构:CHAT/RAG 使用大窗口(20条消息),工具类处理器(SKILL/GSKILL/ASKILL/MCP)使用小窗口(4条消息),防止 LLM 复述历史查询结果而不重新调用工具 - RAG 知识文件位于 `src/main/resources/knowledge` - 数据库查询走 GSKILL/DataBaseSkill,删除和修改操作需通过 ASKILL/ApprovalSqlSkill 审批后方可执行 - SKILL 生成的文件保存在系统临时目录(`%TEMP%/sagent-downloads/`),应用重启后会清空 - 审批自动执行:`ToolRegistry` 使用 Spring AI 原生 `ToolCallback` 机制(`ToolCallbacks.from()` + `StaticToolCallbackResolver`)注册 ASkill Bean 的 `@Tool` 方法,审批通过后通过 `ToolCallback.call()` 重新唤起原始工具,参数类型转换(含泛型 JSON 反序列化)由框架自动完成 - MCP 客户端采用延迟初始化:不注册为 Spring Bean,由 `McpHandler` 在首次 MCP 请求时手动创建连接,避免启动时因 MCP Server 未就绪而导致应用启动失败。若连接失败会返回友好提示,不会阻塞其他功能 - ASKILL 审批机制:带 `@Approval(enable=true)` 注解的方法通过 AOP 切面拦截,自动创建 PENDING 记录并返回等待人工审批;查询类方法(`getMyApprovals`、`checkApprovalById`)无需审批直接放行 - 这是学习和功能验证项目,生产环境还需要鉴权、限流、持久化和安全审查 ## 附录 1:多 Agent 编排 多 Agent 编排是独立于单 Agent 路由的实验性功能,作为功能演示单独介绍。它不改变原有的消息分类机制——前端通过"多Agent"开关选择走 `/ai/multi-agent`,普通入口仍走 `/ai/chat` 单 Agent 路由。 核心思路:**Planner 拆解任务 → Executor 调度执行(复用现有 Handler)→ 汇总 Agent 生成最终回答**,编排层只负责"拆解、调度、汇总",不重复实现 Agent 能力。 ```mermaid flowchart TD U2["用户 / chat.html(多Agent开关)"] --> P["POST /ai/multi-agent"] P --> PL["Planner:LLM 拆解任务
结构化输出 TaskPlan(Task 列表,每个 Task 带 id)"] PL --> VALID["图校验:id 去重 / 悬空依赖过滤 / Kahn 环检测
非法则重拆"] VALID --> INIT["Executor 初始化
pending = 所有子任务
results = 空"] INIT --> WHILE{"pending 是否为空?"} WHILE -- "否" --> FILTER["筛选就绪任务 ready
① dependsOn 为空 → 就绪
② dependsOn 的 id 已在 results → 就绪
③ 其余留待下一波"] FILTER --> EMPTY{"ready 是否为空?"} EMPTY -- "是(循环依赖/依赖id不存在)" --> FAIL["剩余任务标记为 error 结果
break 退出循环"] EMPTY -- "否" --> REMOVE["pending 移除 ready"] REMOVE --> RUN["scheduleTask 线程池并行执行 ready
(独立会话 + 60s 超时 + 异常降级 + 5xx 重试/4xx 不重试)"] RUN --> INJECT["子Agent 有依赖时
按 id 查依赖结果拼入 goal"] INJECT --> JOIN["allOf().join()
等待本波次全部完成"] JOIN --> STORE["本波次结果写入 results
key = 子任务 id"] STORE --> CHECK{"本轮有失败任务?"} CHECK -- "否" --> WHILE CHECK -- "是" --> REPLAN{"还能重新规划?
replanCount < 2"} REPLAN -- "是(方案B)" --> DOREPLAN["调 Planner 重新规划剩余
pending 替换为新任务(r1/r2)
已完成结果保留"] DOREPLAN --> WHILE REPLAN -- "否(方案E)" --> STOPLOSS["递归标记依赖失败链的
后续任务止损跳过"] STOPLOSS --> WHILE FAIL --> AGG WHILE -- "是(全部完成)" --> AGG["汇总 Agent
整合所有子任务结果"] AGG --> RESP["AgentResponse"] RESP --> U2 ``` **波次示意**(示例:查询产品 → 生成文档,Planner 输出 2 个子任务): | 波次 | 就绪任务 | 说明 | | --- | --- | --- | | 波次 1 | T1(GSKILL,无依赖) | 直接就绪,线程池执行,结果写入 results | | 波次 2 | T2(SKILL,dependsOn=t1) | 依赖 id 已在 results → 就绪,按 id 查 T1 结果拼入 goal 后执行 | | 波次 3 | 无 | pending 为空,循环结束,进入汇总 | 依赖任务总是比其依赖晚一个波次,无依赖任务可并行;循环次数 = 任务依赖链的最大深度 + 1。 **与 Tool Calling 循环的区别**(两者是嵌套关系:多 Agent 的每个子 Agent 内部,跑的就是 Tool Calling 循环,如 SKILL/GSKILL 子任务复用各自 Handler 的工具调用能力): | 对比维度 | 多 Agent 编排 | Tool Calling 循环 | | --- | --- | --- | | 大脑数量 | 多个 LLM:Planner 拆解 → 各子 Agent 各自执行 → 汇总 Agent 整合 | 单个 LLM:自己规划、自己执行、自己总结 | | 控制流 | 分离控制流:任务切给不同 Agent 各跑各的,最后合并 | 单一循环:调 LLM → 执行工具 → 回填 → 再调 LLM(同一上下文演进) | | 上下文/记忆 | 隔离:每个子任务独立会话 ID,互不污染 | 共享单一上下文,中间结果都在对话历史里 | | 并行度 | 真正的并行:无依赖子任务同时在线程池里跑 | 本质串行迭代(一次响应内多个工具调用也只是"本回合内"执行,结果仍回填同一 LLM) | | 上下文成本 | 每个子 Agent 只承载自己的上下文,天然防爆 | 随步骤线性增长,步骤多了会撑爆窗口 | | 角色/视角 | 每个子 Agent 有独立目标与视角,通过 `dependsOn` 显式传递结果 | 只有一个视角,工具间数据紧密传递 | | 产物组织 | 汇总 Agent 二次整合,可强制保留下载链接 | LLM 直接总结工具调用结果 | **适用场景区别**: - **Tool Calling 循环**(如 GSKILL):单一领域内的多步任务、步骤间数据强耦合("查产品 → 筛选 → 统计"),上下文规模可控,需要保留完整推理轨迹 - **多 Agent 编排**:跨领域复合任务("查询数据 → 生成文档")、可并行的独立子任务、上下文隔离需求(子任务结果很大不想污染主上下文)、需要角色分离(Planner/Executor/Reviewer) **多 Agent 要点**: - 每个子任务带唯一 `id`(由 Planner 生成,如 t1/t2),`dependsOn` 引用依赖任务的 id(不再用 goal 原文),避免 LLM 输出文本不一致导致依赖匹配失败 - 每个子任务使用复合会话 ID(`原始conversationId#taskId`)执行:`#` 前缀保留原始会话 ID 供审批身份关联(`UserIdResolver` 解析时提取 `#` 前的部分),`#` 后缀的 taskId 保证并行子任务的 ChatMemory 互不污染 - 子任务声明 `dependsOn` 时,按 id 查依赖任务的执行结果拼入 goal,供子 Agent 参考(如"生成文档"依赖"查询产品数据") - 健壮性(三层兜底):单个子任务异常(try-catch)、超时(60s orTimeout)、死锁(循环依赖/依赖id不存在时剩余任务标记失败跳过)都不会中断整轮编排,统一降级为 error 结果 - 失败纠偏(递进式):① 方案A——子任务失败自动重试 1 次,重试时把失败原因拼入 goal 提示换方式;② 方案B——重试仍失败则调 Planner 基于已完成结果和失败原因重新规划剩余任务(最多 2 次,新任务 id 用 r1/r2);③ 方案E——重新规划次数用完后,递归标记依赖失败链的后续任务止损跳过,不白跑注定无意义的子任务 - 汇总阶段强制保留子任务结果中的 `/files/download/` 下载链接,并有正则兜底提取 - Planner 输出后做图校验:检测重复 id、过滤悬空依赖(dependsOn 引用不存在的 id)、Kahn 拓扑排序检测循环依赖,非法计划直接重拆而非送进 execute 白跑 - 失败重试区分错误类型:5xx(技术错误)触发重试,4xx(业务失败,如查不到数据)不重试直接走重新规划/止损,避免无意义重试 - 汇总阶段 try-catch 兜底:汇总 LLM 异常或返回空时,降级为拼接子任务结果原文,不会 NPE 或全盘皆输 - 重新规划后清理失败任务的旧 id,避免 LLM 复用 id 时 taskById 污染 - Planner 读取主会话历史,多 Agent 模式下连续对话(如"刚才那个产品再生成文档")上下文不断 - 线程池 `@PreDestroy` 优雅关闭,应用关停时不丢任务 ## 附录 2:LLM 返回解析机制 系统中有三种 LLM 返回解析模式,对应不同的 Handler 场景: | 解析模式 | 适用 Handler | 工具类型 | 解析方式 | 代码示例 | | --- | --- | --- | --- | --- | | **① returnDirect 透传** | SKILL(SkillHandler) | `@Tool(returnDirect=true)` | 工具方法直接返回 `AgentResult` 的 JSON 字符串,Handler 通过 `AgentResultParser` 手动反序列化提取 `code` 和 `content` | `DocumentSkill` 返回 `{"code":200,"content":"下载链接..."}` → `SkillHandler` 解析 | | **② entity 结构化输出** | GSKILL / ASKILL / MCP | `@Tool`(无 returnDirect) | LLM 调用工具汇总后,通过 `.entity(AgentResult.class)` 强制输出结构化 JSON,由 Spring AI `BeanOutputConverter` 自动反序列化 | `chatClient.prompt().call().entity(AgentResult.class)` → `GSkillHandler` 获取 `AgentResult` 对象 | | **③ 自然语言输出** | CHAT / RAG | 无工具调用 | LLM 直接生成自然语言文本,CHAT 默认 `code=200`;RAG 检索为空时 `code=404`,异常时 `code=500` | `chatClient.prompt().call().content()` → `ChatHandler` / `RagHandler` | ### AgentResult 状态码约定 | 状态码 | 含义 | 使用场景 | | --- | --- | --- | | 200 | 业务成功 | 正常完成 | | 400 | 业务校验失败 | 参数非法、业务规则不满足 | | 404 | 资源不存在 | 查询无结果、数据未找到 | | 500 | 技术错误 | 工具执行异常、外部服务不可达 | **反序列化容错**:模式②中 LLM 结构化输出可能出现 `code` 缺失、显式 `null` 或非法文本(如 `"abc"`),`AgentResult` 通过 `@JsonDeserialize(using = AgentResultDeserializer.class)` 统一归一化为 200(成功)。语义依据:技能实际执行结果以 `content` 为准,`code` 仅为状态标记——LLM 未输出有效 `code` 不代表操作失败,强行报"技能执行失败"会误导用户(如审批实际已提交成功)。 ## 附录 3:工具调用循环 Spring AI 2.0 将工具调用循环从 ChatModel 内部抽取为 `ToolCallingAdvisor` 递归顾问,作为顾问链的一部分统一管理。 ![工具调用循环](agentdemo/doc/toolCallingLoop.png) **核心机制**: 1. `ToolCallingAdvisor` 是递归顾问,通过 `callAdvisorChain.copy(this)` 创建子链进行循环调用 2. `ChatClient` 自动注册 `ToolCallingAdvisor`(默认优先级 `HIGHEST_PRECEDENCE + 300`) 3. 循环过程:注入工具定义 → 调用LLM → 执行工具 → 回填结果 → **再次调用LLM** → 循环 4. 停止条件:LLM 返回不含工具调用的最终响应 **关键流程**: - 循环的主体是**调用工具**,每次循环都会调用LLM来决定是否继续调用工具 - 每次工具调用后,结果会追加到对话历史,然后**再次调用LLM** - 最终LLM根据所有工具结果,生成自然语言回复给用户 **应用只需**: - 通过 `.tools()` 注册工具对象 - 使用 `@Tool` 注解定义可调用方法