Java AI Agent框架JavaManus:生产级可控性设计
1. 项目概述为什么一个 Java 工程师需要亲手造轮子JavaManus 这个名字听起来像某个开源社区里突然冒出来的硬核项目但其实它背后藏着一群在企业级 AI 应用一线摸爬滚打多年的老兵的真实焦虑——不是“能不能用”而是“能不能控”。当 Spring AI 的 auto-configuration 让你三行代码接入 LLM当 React 框架把 Observation、Action、Thought 封装成几个注解你确实能跑通 demo但一旦上线后 token 超限导致任务卡死、工具调用链路里某一步返回 null 却没被拦截、或者业务规则变更时发现 Agent 的决策逻辑散落在 7 个不同配置文件里……这时候框架的“便利性”就变成了“黑盒性”。我带过三个金融风控类 AI Agent 项目全部基于 Java 技术栈。客户明确要求所有推理链路可审计、所有工具调用可熔断、所有状态流转可回溯、所有 prompt 版本可灰度。Spring AI 做不到——它本质是 LLM 客户端抽象层不是 Agent 编排引擎React 模式本身是范式不是实现而市面上所谓“Java AI Agent 框架”90% 是把 Python 的 LangChain 概念强行翻译成 Java 注解连最基础的异步工具调用超时控制都靠 try-catch Thread.sleep 硬扛。JavaManus 就是在这种背景下诞生的它不追求“最快上手”而是追求“最可控落地”。它用纯 JavaJDK 17、零外部脚本、无反射魔法、全同步/异步双模设计把 ReAct 范式拆解成可插拔的 5 个核心组件State状态机、Planner规划器、Executor执行器、ToolRegistry工具注册中心、ObservationHandler观测处理器。每个组件都暴露完整生命周期钩子比如 Executor 不仅执行工具还强制要求你实现 onBeforeExecute() 和 onAfterExecute()这样你才能在调用前注入 traceId、在调用后做 schema 校验。关键词里反复出现的 “java” 和 “ai agent” 并非简单并列而是指向一个被严重低估的现实Java 生态里真正能进生产环境的 AI Agent 框架至今仍是空白。MyBatis-Plus 能根据实体生成建表 SQL是因为它吃透了 JDBC 规范和数据库元数据Spring Boot 能自动装配是因为它把 Bean 生命周期玩到了极致。JavaManus 的设计哲学一脉相承——不是让 Java 去适配 AI 范式而是用 Java 最擅长的工程化能力把 AI 范式重新锚定在 JVM 的确定性之上。所以它不支持 YAML 配置驱动 Agent 流程所有编排必须写 Java 代码它拒绝把 Tool 当作黑盒函数每个 Tool 必须实现 ToolDescriptor 接口显式声明输入 schema、输出 schema、失败重试策略、超时阈值。这不是反生产力而是把“AI 不可靠”这个前提提前固化到框架契约里。如果你正在评估是否要引入 AI Agent 到现有 Java 系统或者已经踩过 Spring AI 自研调度层的坑又或者正被“AI 工程化落地难”这个问题反复折磨——那么 JavaManus 不是一个玩具项目而是一份可直接抄作业的生产级 Agent 架构说明书。它不教你大模型原理但会告诉你怎么让一个 Java 方法在被 LLM 调用时既能拿到完整的上下文快照又能保证事务一致性它不讲 React 的理论优势但会实操演示如何用 StateMachineBuilder 构建一个支持分支跳转、循环中断、人工干预的决策状态机。接下来的内容我会带你从零开始一行行代码还原 JavaManus 的骨架与血肉。2. 整体架构设计为什么放弃“开箱即用”选择“可拆卸组装”2.1 核心矛盾LLM 的不确定性 vs Java 的确定性所有 AI Agent 框架设计的第一道分水岭就是如何看待 LLM 的输出。Python 社区普遍接受“LLM 输出即真理”LangChain 的 RunnableSequence 默认把上一步输出原样喂给下一步而 Java 生态里我们每天打交道的是银行转账、订单扣减、库存锁任何一步的不可靠都会引发雪崩。JavaManus 的破局点很朴素把 LLM 的输出降级为一种“建议”而非“指令”。这直接决定了整个架构的走向。我们来看一个典型风控场景用户申请贷款Agent 需要依次调用「查询征信」→「计算负债率」→「校验收入流水」→「生成审批结论」。如果按传统 React 模式LLM 生成的 Action 可能是 {tool: checkCredit, input: {id: U123}}然后框架直接反射调用。但现实中“checkCredit” 工具可能因第三方接口超时返回空结果也可能因用户 ID 格式错误抛出 IllegalArgumentException——这些异常在 Python 里常被忽略或吞掉但在 Java 里我们必须明确知道这个异常该重试该降级还是该终止整个流程并通知人工JavaManus 的解法是引入Observation-Driven State Transition机制。它不信任 LLM 直接给出的 tool name而是要求 LLM 输出结构化 Observation观测例如{ thought: 需要先验证用户征信情况, action: checkCredit, action_input: {userId: U123}, observation: 等待工具执行结果 }注意最后这句observation: 等待工具执行结果—— 它不是 LLM 的“思考”而是框架强加的中间态。框架收到这个 Observation 后会进入Executor 阶段先校验 action 是否在白名单再校验 action_input 是否符合 ToolDescriptor 定义的 JSON Schema然后才真正调用工具。工具执行完无论成功失败都必须返回标准 Result 对象包含 status、data、error、traceId。这个 Result 会被封装成新的 Observation再喂给 Planner由 Planner 决定下一步是继续 Action、还是回退、或是触发人工审核。整个过程LLM 只参与“提议”不参与“决策”真正的决策权牢牢掌握在 Java 的 if-else 和状态机手里。2.2 五大核心组件每个都是可替换、可监控、可测试的独立单元JavaManus 的架构图没有花哨的箭头和云朵只有 5 个清晰的 Java 接口每个接口都遵循单一职责原则并预留了 SPI 扩展点State继承自StateMachineState, Event但做了关键增强——支持 snapshot快照和 rollback回滚。比如在「校验收入流水」失败时State 可以一键回滚到「查询征信」完成后的状态而不是从头再来。我们用 Caffeine Cache 实现内存快照序列化用 Jackson避免 Gson 的泛型擦除问题快照 key 是sessionId stepIndex这样运维人员可以直接通过 sessionId 查看任意时刻的完整上下文。Planner不是简单的 prompt 拼接器。它接收当前 State 和历史 Observation 列表输出下一个 Action。JavaManus 提供两种默认实现LLMPlanner调用大模型 API和RuleBasedPlanner基于 Drools 规则引擎。重点在于Planner 的输入输出都被严格定义输入是PlannerInput含 stateSnapshot、recentObservations、systemPrompt输出是PlannerOutput含 nextAction、reasoningTrace、confidenceScore。这意味着你可以轻松替换 Planner——比如用本地小模型做初筛高置信度请求走 LLM低置信度走规则引擎完全不影响 Executor 和 ToolRegistry。Executor这是最体现 Java 工程能力的部分。它不直接调用工具方法而是通过ToolInvocation封装所有调用细节public class ToolInvocation { private String toolName; private MapString, Object inputParams; // 已经过 schema 校验 private long timeoutMs 5000; private int maxRetries 2; private String circuitBreakerKey; // 熔断器标识 }Executor 内部集成 Resilience4j每个 ToolInvocation 都绑定独立的 CircuitBreaker 和 TimeLimiter。更关键的是Executor 强制要求所有工具实现Tool接口该接口定义了validateInput()输入校验、execute()核心逻辑、postProcess()后处理如日志脱敏三个方法。这样哪怕你换掉底层工具实现比如把 HTTP 调用换成 gRPC只要接口不变Executor 就无需修改。ToolRegistry不是 HashMapString, Tool。它采用分层注册全局工具如日志记录、会话级工具如当前用户专属风控模型、临时工具如本次任务动态生成的 SQL 查询器。注册时必须指定ToolScope枚举并支持按 scope 动态启停。运维后台可以实时看到每个 scope 下激活的工具列表、调用成功率、平均耗时——这才是企业级可观测性的起点。ObservationHandler负责把原始 LLM 输出、工具执行结果、系统事件如超时、熔断统一转换成标准 Observation。它内置 JSON Schema 校验器任何不符合预设 schema 的 Observation 都会被拦截并标记为INVALID_OBSERVATION触发告警而非静默失败。我们甚至为 Observation 设计了版本号v1/v2当 LLM 输出格式升级时旧版 Handler 仍能兼容解析新版 Handler 则启用新字段。这个架构放弃了一切“魔法”没有自动依赖注入、没有运行时字节码增强、没有 YAML 驱动流程。所有组件通过构造函数注入所有配置通过 Builder 模式设置。好处是什么——单元测试覆盖率轻松达到 92%线上问题 10 分钟内可定位到具体组件新同事三天内就能看懂整个调用链路。这正是 Java 生态最珍贵的资产可预测性。2.3 为什么不用 Spring Boot 自动装配——一次真实的生产事故复盘去年 Q3某券商的智能投顾 Agent 上线后连续三天凌晨 2 点出现偶发性任务卡死。排查发现问题出在 Spring AI 的AiResponse解析器上当 LLM 返回极长的 reasoning text超过 64KB时Jackson 反序列化会触发 GC 频繁而 Spring 的Async线程池恰好配置了无界队列导致线程堆积最终拖垮整个服务。这件事直接催生了 JavaManus 的“零 Spring 依赖”原则。我们不是反对 Spring而是反对把框架复杂度藏在自动装配背后。JavaManus 的启动入口就是一个标准的main()方法public class JavaManusApplication { public static void main(String[] args) { // 1. 构建 State内存快照 Redis 持久化 State state State.builder() .snapshotStore(new CaffeineSnapshotStore()) .persistenceStore(new RedisPersistenceStore(redis://...)) .build(); // 2. 注册 Tools显式、类型安全 ToolRegistry registry new DefaultToolRegistry(); registry.register(queryStockPrice, new QueryStockPriceTool()); registry.register(calculateRiskScore, new CalculateRiskScoreTool()); // 3. 组装 Agent所有依赖显式传入 Agent agent Agent.builder() .state(state) .planner(new LLMPlanner(https://api.xxx.com/v1/chat/completions)) .executor(new DefaultExecutor(registry)) .observationHandler(new DefaultObservationHandler()) .build(); // 4. 启动无隐藏线程、无自动扫描 agent.start(); } }这个启动流程看似“啰嗦”但它带来了三个不可替代的价值启动即可见main()方法里每一行代码都对应一个真实对象的创建和初始化。没有ComponentScan的隐式加载没有PostConstruct的神秘回调所有资源线程池、连接池、缓存都在这里显式配置、显式关闭。测试即生产单元测试可以直接 new 出DefaultExecutormockToolRegistry传入伪造的ToolInvocation断言它是否按预期调用了熔断器、是否正确处理了超时异常。而 Spring Boot 的SpringBootTest启动整个上下文单测耗时 3 秒起步根本无法支撑高频迭代。部署即确定打包成 fat jar 后java -jar app.jar的行为 100% 可预测。不会因为 classpath 里多了一个spring-boot-starter-web就自动开启 Tomcat也不会因为少了一个application.yml就找不到配置源。我们在 Dockerfile 里只保留FROM openjdk:17-jre-slim没有任何 Spring 相关 layer。这或许违背了“约定优于配置”的现代开发理念但对于需要 7x24 小时稳定运行的金融级 AI Agent确定性比便利性重要一百倍。JavaManus 的设计者们都是在凌晨三点被 PagerDuty 唤醒、对着 GC 日志抓狂过的人——他们写的不是代码是 SLA 的担保书。3. 核心模块实现从 State 机器到 Tool 注册手把手拆解关键代码3.1 State 模块如何让 AI 决策具备“后悔”能力AI Agent 最大的痛点不是“做错”而是“做错后无法回到上一步”。人类在做决策时会自然回溯比如填表时发现身份证号输错会按 CtrlZ客服对话中用户说“等等我换个问题”系统会暂停当前流程。但绝大多数 Agent 框架把流程设计成单向链表一旦某步失败只能整体重启既浪费算力又丢失上下文。JavaManus 的 State 模块核心目标只有一个让每一步决策都可快照、可回滚、可审计。它不是简单的 MapString, Object而是一个嵌套的状态机内部结构如下State (Root) ├── sessionMetadata: SessionMeta (会话元信息) ├── currentStep: Step (当前步骤索引) ├── history: ListStepSnapshot (历史快照列表) ├── context: MapString, Object (当前上下文键值对) └── variables: MapString, Object (变量空间支持嵌套)最关键的创新在于StepSnapshot。它不是深拷贝整个 State而是采用增量快照Delta Snapshot策略public class StepSnapshot { private final int stepIndex; private final String stepName; // e.g., QUERY_CREDIT private final long timestamp; private final MapString, Object contextDelta; // 仅保存本次步骤修改的 key-value private final MapString, Object variablesDelta; // 同上 private final String previousSnapshotId; // 指向上一个快照形成链表 }为什么不用深拷贝因为一次风控决策可能涉及 200 字段的上下文深拷贝一次耗时 15ms而增量快照只需 0.8ms。我们实测过在 1000 QPS 的压测下深拷贝快照导致 GC 暂停时间从 12ms 涨到 47ms而增量快照几乎无影响。具体实现上State 的saveSnapshot()方法会自动计算 deltapublic void saveSnapshot(String stepName) { StepSnapshot current new StepSnapshot(); current.setStepName(stepName); current.setStepIndex(this.currentStep); // 计算 context delta只取当前 context 与上一个快照 context 的 diff MapString, Object contextDelta computeDelta( this.context, getLastSnapshot().map(s - s.getContextDelta()).orElse(Collections.emptyMap()) ); current.setContextDelta(contextDelta); // 同理计算 variables delta MapString, Object variablesDelta computeDelta( this.variables, getLastSnapshot().map(s - s.getVariablesDelta()).orElse(Collections.emptyMap()) ); current.setVariablesDelta(variablesDelta); this.history.add(current); this.snapshotStore.save(current); // 异步写入 Caffeine Redis }回滚操作rollbackToStep(int targetStep)就变得极其高效它不需要重建整个 State而是从targetStep的快照开始按previousSnapshotId链表逆序遍历逐个应用 deltapublic void rollbackToStep(int targetStep) { StepSnapshot target findSnapshot(targetStep); // 清空当前 context 和 variables this.context.clear(); this.variables.clear(); // 从 target 快照开始正向应用所有 delta注意是正向因为 delta 本身就是“从上一步到这一步”的变化 StepSnapshot cursor target; while (cursor ! null) { this.context.putAll(cursor.getContextDelta()); this.variables.putAll(cursor.getVariablesDelta()); cursor findSnapshotById(cursor.getPreviousSnapshotId()); } }这个设计带来的实际收益非常直观某次线上事故中用户在「校验收入流水」步骤因网络抖动失败系统自动回滚到「查询征信」完成后的状态300ms 内恢复用户无感知而同类产品采用全量快照回滚耗时 2.3s用户已关闭页面。提示增量快照的 diff 算法我们用的是 Guava 的Maps.difference()但做了关键优化——对大文本字段如 LLM 的 reasoning text不做内容 diff而是用 MD5 哈希值比较。因为比较 10KB 文本的 diff 耗时 8ms而计算 MD5 只需 0.3ms。3.2 ToolRegistry 与 Tool 接口让每个工具都成为“可管理的微服务”在 JavaManus 里“工具”Tool不是一段可调用的代码而是一个有生命周期、有健康指标、有权限边界的“微服务”。这源于一个残酷事实生产环境中90% 的 Agent 失败不是因为 LLM 不行而是因为工具不可靠——接口超时、参数校验失败、返回格式突变、下游服务降级。ToolRegistry 的设计目标很明确让工具的注册、发现、调用、监控全部标准化、可视化、可治理。它不接受FunctionMapString,Object, Object这种匿名函数而是强制要求所有工具实现Tool接口public interface Tool { String getName(); // 工具唯一标识如 queryStockPrice ToolDescriptor getDescriptor(); // 描述工具能力含 schema、超时、重试等 // 输入校验在调用 execute 前执行失败则直接拒绝 ValidationResult validateInput(MapString, Object input); // 核心执行逻辑必须是纯函数式无副作用 ToolResult execute(MapString, Object input) throws ToolException; // 后处理用于日志脱敏、结果格式化、指标上报 void postProcess(ToolResult result); }ToolDescriptor是治理的关键它定义了工具的“契约”public class ToolDescriptor { private final String name; private final String description; // 供 LLM 理解的自然语言描述 private final JsonNode inputSchema; // JSON Schema用于 validateInput private final JsonNode outputSchema; // 同上 private final long timeoutMs; private final int maxRetries; private final String circuitBreakerKey; // 熔断器标识通常为 tool: name private final boolean requiresAuth; // 是否需要鉴权 }注册一个工具必须提供完整 descriptorToolRegistry registry new DefaultToolRegistry(); registry.register(queryStockPrice, new QueryStockPriceTool(), ToolDescriptor.builder() .name(queryStockPrice) .description(查询指定股票的最新价格和涨跌幅输入{symbol: string}) .inputSchema(JsonLoader.fromResource(/schema/query-stock-input.json)) .outputSchema(JsonLoader.fromResource(/schema/query-stock-output.json)) .timeoutMs(3000) .maxRetries(1) .circuitBreakerKey(tool:queryStockPrice) .requiresAuth(true) .build() );这个设计带来三个直接收益前置校验validateInput()在 Executor 调用execute()前执行确保非法参数如 symbol 为空、数字格式错误被拦截在网关层不消耗下游资源。可观测性ToolRegistry 内置 Micrometer 指标收集器自动上报每个工具的tool.calls.total、tool.calls.failed、tool.calls.duration。运维可以在 Grafana 看到queryStockPrice工具过去 1 小时失败率 12%而calculateRiskScore是 0.3%立刻定位问题域。动态治理通过 Admin API可以实时禁用某个工具registry.disableTool(queryStockPrice)或调整其超时阈值registry.updateTimeout(queryStockPrice, 5000)无需重启服务。某次上游行情接口故障我们 30 秒内禁用了所有股票查询类工具切换到缓存模式用户无感知。注意ToolResult不是简单 POJO它包含statusSUCCESS/FAILED/TIMEOUT、data序列化后的结果、error结构化错误码非 Exception message、traceId全链路追踪 ID。这确保了 ObservationHandler 能生成标准化的 Observation也方便 ELK 做聚合分析。3.3 Planner 模块如何让 LLM 的“思考”变成可审计的决策日志Planner 是 Agent 的“大脑”但 JavaManus 拒绝把它做成黑盒。我们的目标是每一次 LLM 的“思考”都必须留下可追溯、可验证、可复盘的决策痕迹。这通过PlannerOutput的结构化设计和Planner的双通道机制实现。PlannerOutput定义如下public class PlannerOutput { private final String nextAction; // LLM 建议的下一步工具名 private final MapString, Object actionInput; // 结构化输入参数 private final String reasoningTrace; // LLM 的原始 reasoning 文本未经加工 private final double confidenceScore; // 置信度0.0~1.0由 LLM 或后处理模型给出 private final String plannerVersion; // 当前 Planner 版本用于灰度 }关键点在于reasoningTrace和confidenceScore。前者是原始输出后者是量化指标。我们不信任 LLM 自己说的“我很确定”而是用一个轻量级 BERT 模型部署在本地对reasoningTrace做二分类是否包含充分依据evidence-based。比如高置信用户信用分 720 650且负债率 35% 50%符合放款条件→ score0.92低置信我觉得这个用户可以放款→ score0.21Planner 的双通道机制如下主通道LLMPlanner调用大模型 API输入是PlannerInput含当前 State 快照、最近 3 条 Observation、system prompt输出PlannerOutput。备通道RuleBasedPlanner当confidenceScore 0.6时自动触发它不依赖 LLM而是读取 Drools 规则库XML 格式匹配creditScore 700 debtRatio 0.4等条件生成确定性 Action。两个通道的输出都会被PlannerAuditLogger记录到 Kafka{ sessionId: S123456, stepIndex: 3, plannerType: LLM, reasoningTrace: 用户信用分 720 650..., confidenceScore: 0.92, nextAction: approveLoan, actionInput: {loanAmount: 100000}, auditTime: 2024-06-15T14:22:33.123Z }这个日志流是风控合规的基石。监管检查时我们可以直接导出某次审批的完整决策链路LLM 的 reasoning、置信度、备选规则触发记录、最终 Action、工具执行结果——所有环节环环相扣无可辩驳。实操心得LLM 的 prompt engineering 我们不追求“完美指令”而是聚焦“最小必要信息”。system prompt 只有 3 行你是一个风控决策助手请严格按以下格式输出 JSON {next_action: ..., action_input: {...}, reasoning_trace: ..., confidence_score: 0.0~1.0} 不要添加任何额外字段或解释。这样做的好处是LLM 输出格式高度稳定JSON 解析失败率从 15% 降到 0.2%。而“完美 prompt”往往导致 LLM 生成冗长文本增加解析负担和幻觉风险。4. 实操全流程从初始化到上线一个真实风控 Agent 的构建实录4.1 环境准备与依赖管理为什么只用 JDK 17 和 7 个核心库JavaManus 的 pom.xml 极其克制核心依赖仅 7 个全部经过生产验证dependencies !-- 1. JDK 17 原生特性 -- dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId version6.1.0/version scopeprovided/scope !-- 仅用其 StringUtils不引入容器 -- /dependency !-- 2. JSON 处理Jackson避免 Gson 的泛型问题 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency !-- 3. HTTP 客户端OkHttp比 RestTemplate 更可控 -- dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency !-- 4. 熔断与限流Resilience4j轻量且无 Spring 依赖 -- dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-circuitbreaker/artifactId version2.0.2/version /dependency !-- 5. 缓存Caffeine性能碾压 Guava Cache -- dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId version3.1.8/version /dependency !-- 6. JSON Schema 校验networknt/json-schema-validator -- dependency groupIdcom.networknt/groupId artifactIdjson-schema-validator/artifactId version1.4.1/version /dependency !-- 7. 日志SLF4J Logback不绑定具体实现 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.9/version /dependency /dependencies为什么不用 Spring Boot前面已详述。为什么不用 WebClient因为 OkHttp 提供了更细粒度的连接池控制ConnectionPool、更精准的超时设置callTimeout、connectTimeout、readTimeout分开配置这对工具调用至关重要。为什么不用 Jackson 2.14因为 2.15.2 修复了JsonNode在高并发下的线程安全 bug我们在压测中发现 2.14 版本在 5000 QPS 下会出现JsonProcessingException。JDK 版本锁定为 17是因为我们需要sealed classes用于 State 的枚举状态、record用于 immutable 的 Observation、以及Virtual Threads在 Executor 中处理海量工具调用。我们实测过用 Virtual Threads 替代传统线程池相同硬件下工具并发数提升 3.2 倍而内存占用下降 40%。注意所有依赖的scope都严格控制。spring-core设为provided因为我们只用它的StringUtils和Assert绝不引入ApplicationContextlogback-classic不在 pom 中而是由部署方按需提供确保日志框架与现有系统一致。4.2 构建第一个 Agent风控审批工作流的完整代码现在让我们动手构建一个真实的风控审批 Agent。需求很简单对用户贷款申请依次执行「查询征信」→「计算负债率」→「校验收入流水」→「生成审批结论」任何一步失败则终止并返回原因。第一步定义 Tools// 查询征信工具 public class CheckCreditTool implements Tool { private final CreditService creditService; // 依赖注入 Override public ToolDescriptor getDescriptor() { return ToolDescriptor.builder() .name(checkCredit) .description(查询用户征信报告输入{userId: string}) .inputSchema(JsonLoader.fromResource(/schema/check-credit-input.json)) .outputSchema(JsonLoader.fromResource(/schema/check-credit-output.json)) .timeoutMs(5000) .maxRetries(1) .circuitBreakerKey(tool:checkCredit) .build(); } Override public ValidationResult validateInput(MapString, Object input) { String userId (String) input.get(userId); if (userId null || userId.trim().isEmpty()) { return ValidationResult.invalid(userId 不能为空); } return ValidationResult.valid(); } Override public ToolResult execute(MapString, Object input) throws ToolException { String userId (String) input.get(userId); try { CreditReport report creditService.query(userId); return ToolResult.success(report); } catch (TimeoutException e) { throw new ToolException(征信查询超时, ToolError.TIMEOUT); } catch (Exception e) { throw new ToolException(征信查询失败, ToolError.INTERNAL_ERROR); } } } // 计算负债率工具省略类似实现 public class CalculateDebtRatioTool implements Tool { ... } // 校验收入流水工具省略类似实现 public class VerifyIncomeTool implements Tool { ... } // 生成审批结论工具 public class GenerateApprovalDecisionTool implements Tool { Override public ToolDescriptor getDescriptor() { return ToolDescriptor.builder() .name(generateApprovalDecision) .description(根据征信、负债率、收入流水生成审批结论输入{creditScore: number, debtRatio: number, income: number}) .inputSchema(JsonLoader.fromResource(/schema/generate-approval-input.json)) .outputSchema(JsonLoader.fromResource(/schema/generate-approval-output.json)) .timeoutMs(1000) .maxRetries(0) // 此工具无外部依赖不重试 .circuitBreakerKey(tool:generateApprovalDecision) .build(); } Override public ToolResult execute(MapString, Object input) throws ToolException { double creditScore (Double) input.get(creditScore); double debtRatio (Double) input.get(debtRatio); double income (Double) input.get(income); String decision REJECT; String reason ; if (creditScore 700 debtRatio 0.4 income 10000) { decision APPROVE; } else if (creditScore 650 debtRatio 0.5) { decision PENDING; reason 需人工复核; } return ToolResult.success(Map.of(decision, decision, reason, reason)); } }第二步构建 State 和 ToolRegistry// 初始化 State支持内存快照 Redis 持久化 State state State.builder() .snapshotStore(new CaffeineSnapshotStore()) .persistenceStore(new RedisPersistenceStore(redis://127.0.0.1:6379)) .build(); // 注册所有 Tools ToolRegistry registry new DefaultToolRegistry(); registry.register(checkCredit, new CheckCreditTool(creditService)); registry.register(calculateDebtRatio, new CalculateDebtRatioTool(debtService)); registry.register(verifyIncome, new VerifyIncomeTool(incomeService)); registry.register(generateApprovalDecision, new GenerateApprovalDecisionTool()); // 构建 Planner主通道 LLM备通道规则引擎 Planner planner Planner.builder() .llmPlanner(new LLMPlanner(https://api.openai.com/v1/chat/completions, apiKey)) .rulePlanner(new RuleBasedPlanner(rules/drools-rules.drl)) .build();第三步定义 Agent 工作流// 创建 Agent 实例 Agent agent Agent.builder() .state(state) .planner(planner) .executor(new DefaultExecutor(registry)) .observationHandler(new DefaultObservationHandler()) .build(); // 启动 Agent注意这是同步阻塞调用适合单次任务 public class LoanApprovalService { public ApprovalResult processLoanApplication(String userId) { // 1. 初始化 State state.initialize(Map.of(userId, userId, applicationId, APP- UUID.randomUUID())); // 2. 执行 Agent 流程 try { // 循环执行直到达到终态或超时 int maxSteps 10; for (int i 0; i max