我最早接触 Agent 这个概念还是通过 Python 生态里那一堆基于动态语言特性构建的开源项目。彼时就在想Java 这种强类型、重编译期的语言到底适不适合做 Agent 框架毕竟 Agent 的核心就是“动态”两个字——动态地感知环境、动态地调用工具、动态地决策下一步动作。直到我真正动手用 Java 从零开始写了一个名叫 JavaManus 的 AI Agent 框架之后才彻底想明白动态是一种能力不是一种语言特权。Java 不仅能做而且用好了反而比动态语言多出编译期保障和清晰的边界约束。这篇文章就想把 JavaManus 的设计和实现过程做一个深度拆解。它到底是什么能解决什么问题一句话说清楚它是一个完全基于 Java 生态构建的通用 AI Agent 运行时支持工具调用回环Tool Calling Loop、多轮对话管理、上下文裁剪、并行工具执行以及和主流大模型 API 的对接。适合谁看有 Java Web 开发经验、想进入 Agent 开发领域但从没碰过 Python 框架的同学以及已经用过某些 Java 版 Agent 框架但想搞清楚底层原理、自己也能写一个的开发者。这中间涉及的核心问题和取舍我都会摊开来讲。1. 核心架构为什么这么拆边界画在哪1.1 从“智能体”到底层的三层建模Agent 这个词被用烂了但落到工程实现上其实就三个东西模型Model、工具Tool、循环Loop。模型负责“思考”工具负责“行动”而循环把所有思考和行动串起来的胶水层。JavaManus 的核心思路就是把这三件事拆成三个互相独立的模块然后通过统一的消息协议串起来。很多人一开始做 Agent 都容易犯一个毛病——把“智能”写死在一个类里。比如写一个Agent类里边既处理 LLM 请求又判断该调用什么工具还要管理对话历史最后还把日志输出绑死控制台。一开始跑 Demo 确实飞快可一旦要加新的工具类型、换模型供应商、或者调整 Agent 的决策策略整个类就成了一坨改不动的大泥球。我在设计 JavaManus 时参考了主流 Agent 框架的成熟思路把整个系统画成三层基础层负责和外部世界通信。包括 LLM 客户端适配、工具定义与发现、消息模型。决策层即 Agent 的核心循环负责解析模型输出、维护上下文状态、分发工具调用、处理执行结果。交互层面向开发者的门面提供简单的编程接口就说“直接让你的 Java 应用跑起 Agent 能力”。这三层之间用消息对象解耦。LLM 返回的内容被解析成结构化的AgentMessage决策层看到的是消息、而不是字符串。这样做的好处是无论底层的模型是返回纯文本还是返回工具调用请求决策层都可以基于同一套消息协议运转。1.2 关键技术选型为什么这么别扭但又这么对先交代一下技术栈我用了 Java 17、Jackson、虚拟线程Project Loom 在 Spring Boot 3.x 里的封装、以及 JBoss 的TypeReference来做泛型反序列化。这些选型看起来平平无奇但每一处都有自己的理由。第一用 Jackson 来处理所有 JSON 序列化和反序列化不用别的。LLM 通信的本质就是 JSON 进、JSON 出Jackson 在这块的生态成熟度和性能都经过了长期验证。关键是它的ObjectMapper可以被精细配置能处理大模型中经常出现的各种“非标”JSON——比如模型返回了一段 Markdown 代码块包裹的 JSON或者 JSON 里混了注释甚至有的模型偶尔多输出一个逗号。我在工具层封装了一个专门的JsonParser它基于 Jackson 提供的容错配置做了二次加固实测能把市面上主流大模型的输出解析成功率从不到七成拉到九成以上。后来我意识到模型输出的健壮性是 Agent 框架最重要的基础能力之一。第二用 Java 虚拟线程来处理并行工具调用。Agent 执行过程中如果一个决策触发了多个彼此独立的工具调用串行执行会白白浪费几十秒时间。传统 Java 的线程模型处理这种场景很别扭——用线程池要管理生命周期、要设置超时、要处理线程等待。虚拟线程就不一样它可以极轻量地创建和销毁几十个并发调用完全无压力代码写法和同步调用一致没有异步编程的复杂度。这是我在实际对比了 CompletableFuture 方案之后才确定的路线。第三工具方法返回类型用泛型描述而不是用具体的封装类。调用工具的时候Agent 只知道“这个工具需要一个 X 类型的参数”至于 X 怎么实例化是在运行时通过TypeReference反射完成的。这个后边单独讲它是 Java 实现工具动态调用的一个关键设计。1.3 模块职责与扩展边界JavaManus 的源代码目录刻意设计成“你扫一眼包名就知道这模块是干嘛的”的结构agent-core循环器、消息路由、上下文管理是框架的大脑。agent-tool工具抽象、探测机制、参数解析是框架的手。agent-llmLLM 供应商适配层是框架的眼睛和耳朵。agent-runtime把上述模块拼装成可运行 Agent 的启动器附带调试接口。每个模块之间只通过接口通信。比如agent-core根本不知道某个工具是怎么实现的它只知道调用ToolRegistry的execute(name, args)方法拿到一个ToolResult。新增一个工具时只需要实现Tool接口并注册进去核心循环一行代码都不用改。这些“接口优先”的边界是 Java 强类型特性带来的忠实的回报——编译期不对运行就不可能对。2. Tool 抽象层把 Java 的静态类型“扭”成动态调用2.1 统一工具描述模型ToolSpecAgent 框架里最难处理的部分之一是工具的动态发现和调用。Python 生态里因为动态语言的特性函数直接传入框架就能被反射调用参数的校验也可以在运行时临时推断。到了 Java 这里如果你沿用 Python 的思路第一步就会卡死——Java 没有语言层面的动态函数结构。解决这个问题的核心是定义一个统一的工具描述模型ToolSpec。public record ToolSpec( String name, String description, ListParameterSpec parameters, boolean needsConfirmation ) { public record ParameterSpec( String name, String type, // integer, number, string, boolean, array, object String description, boolean required, String defaultValue ) {} }这里最关键的是type字段。它对应的是大模型 API 里 Function Calling 的 JSON Schema 定义。为什么要单独定义一套自己的描述模型而不直接复用 JSON Schema因为大模型 API 对 Function Calling 的参数格式有严格约定而定义工具时如果用纯 JSON Schema就会在解析时丢失 Java 的类型信息。比如 JSON Schema 里的integer对应 Java 的什么是int还是long还是Integer这套ToolSpec相当于在“Java 类型”和“大模型 API 描述格式”之间做了一层映射桥。2.2 工具注册与参数解析关于 Java 泛型的“高级魔法”当一个工具被注册进框架时JavaManus 会扫描其方法签名自动生成对应的ToolSpec。这里的核心机制是用 Java 的Method对象配合注解AgentTool(name get_stock_price, description 获取指定股票的实时价格) public String getStockPrice( ToolParam(description 股票代码如 AAPL) String symbol ) { // 实际的工具逻辑 }在注册阶段框架通过反射拿到Method的注解信息构建ToolSpec。但这里有个进阶问题——当工具方法的参数是一个复杂对象时如何把它从 LLM 返回的 JSON 字符串转换回 Java 对象最直接的方式是Object arg objectMapper.readValue(jsonString, method.getParameterTypes()[0]);但如果是泛型类型比如ListOrder、MapString, User这种getParameterTypes()返回的只是List.class类型信息丢失反序列化出来的就是一个ListLinkedHashMap。运行时对元素进行类型转换时会抛ClassCastException。解决方法是借助 JBoss 的TypeReference拿到完整的泛型信息Type type new TypeReferenceListOrder() {}.getType(); Object arg objectMapper.readValue(jsonString, type);工具作者在定义工具方法时把完整泛型信息声明出来框架在构建ParameterSpec时把这个“类型令牌”记录下来到运行时再用于反序列化。这里我踩过一个坑一开始用method.getGenericParameterTypes()[i]拿ParameterizedType但那个类型信息有时会和实际期望的 Bean 结构对不上。后来实践下来让工具方法直接显式声明完整的泛型返回类型反而最稳定。2.3 工具调用的幂等性与并发保障Agent 框架的另一个隐蔽问题是幂等。当一个 Agent 决策调用某个工具时如果执行超时框架应该重试但如果这个工具不是幂等的比如“扣款”操作一旦重试会出大事。所以ToolSpec里加了一个idempotent标记非幂等工具执行时框架会禁止自动重试并要求人工确认。考虑到工具并发调用的场景我在ToolRegistry里为每个工具标注了线程安全性。状态无关的工具比如查询天气可以无限制并行执行有状态修改的工具比如写库操作会在内部依赖锁串行处理避免并发写覆盖。3. Agent 核心循环工具调用回环Tool Calling Loop的完整实现3.1 循环骨架Think→Act→Observe没有一个智能体可以不“循环”就完成复杂任务因为大模型本质上是无状态的——每次调用都像失忆了重来。所以 JavaManus 最核心的部分就是一个循环器。我来画出这个循环的基本骨架系统初始化 ↓ 准备消息列表System Prompt 历史 新用户输入 ↓ 调用LLM接口 ↓ 解析响应 ├── 纯文本响应 → 返回给用户流程结束 └── 工具调用请求 → 解析出工具名参数JSON ↓ 在ToolRegistry中查找工具 ↓ 执行工具调用可能触发多个并行子任务 ↓ 将工具执行结果包装成消息追加到消息列表 ↓ 回到“调用LLM接口”步骤继续循环这个循环说起来很简单但实现时每一步都有隐藏的复杂度和各种坑。3.2 系统提示词System Prompt的生成策略循环的第一步是构建发给 LLM 的消息列表。很多人在这里把它简单地理解为“把历史对话原样丢给模型”但其实没那么简单。JavaManus 对 System Prompt 的构建有一套模块化的方式角色定义模块告诉模型“你是一个擅长使用工具的智能助手”。工具清单模块把所有已注册工具的ToolSpec转成 LLM 能理解的 JSON Schema 格式。行为约束模块定义模型什么时候应该调工具、什么时候应该直接回复、遇到错误时的处理原则。输出格式模块对不同的模型给出针对性的格式要求提示它“如果你需要调用工具请以特定格式输出”。这里有个细节值得提一下工具描述的质量对 Agent 的成功率影响非常大。同一个工具如果描述写成“获取天气”模型就会经常用错参数但写成“获取指定城市今天的实时天气数据支持输入城市名中文或英文输出包括温度、湿度、风速和天气状况”模型准确使用的概率就高得多。JavaManus 在工具注册时会做一次“描述健康检查”描述少于 15 个字符的工具会被标记为警告提醒开发者补充。不同模型的提示词敏感度不一样。有的模型在 System Prompt 里给一个 JSON 示例它就会照着严格的格式输出有的模型则需要把示例特别显眼地放在最后一句提示里才有效。所以提示词的组装逻辑必须做成策略模式每个模型供应商可以覆盖默认策略。3.3 解析 LLM 响应与“幻觉”和“格式不稳定”作斗争LLM 返回的响应并不总是严格遵循约束这是 Agent 开发中最折磨人的地方。我们发出去的是一个严格的结构化请求返回来的却可能是规范的 JSON 工具调用片段带 Markdown 代码块包裹的 JSON夹杂一段自然语言解释后再给出 JSON甚至干脆只有“我觉得应该调用 get_stock_price”这种自然语言描述而没有明确的 JSONJavaManus 在解析环节建立了一条容错流水线。第一步用正则和状态机剥离 Markdown 代码块。第二步用策略化的ObjectMapper去解析开启FAIL_ON_UNKNOWN_PROPERTIES禁用开启ALLOW_COMMENTS等宽松选项。第三步如果解析失败不会直接报错而是走一个“启发式恢复”通道尝试查找以{开头、以}结尾的可疑 JSON 片段进行二次解析。最后一步还是失败的话就把 LLM 的这段自然语言回复直接作为文本响应返回给用户同时记录一条解析失败日志供后续调试。这套容错流水线看似繁琐实际效果非常明显。在我对各种主流模型的实测里解析成功率从 65% 左右提升到了 95% 左右。剩下 5% 的失败发生在模型输出极端混乱的场景下这时让 Agent 暂停并请求用户重新描述任务比强行解析更安全。3.4 消息路由与状态管理循环的主体是消息列表的持续追加。这里踩过一个大坑如果每轮循环都把全部历史消息包括系统提示词、每轮工具调用、每轮工具结果发给 LLM上下文窗口很快就会溢出。JavaManus 设计了一个MessageHistory管理器它维护一个定长的滑动窗口默认保留最近 20 轮交互。窗口溢出时较早的消息会被压缩成一个摘要而不是直接丢弃。摘要的方式有两种一种是将旧消息交给 LLM 生成一段几十字的总结塞回历史里另一种是直接用模板提取关键信息。我实现的是混合模式——普通对话历史用模板提取涉及工具调用的关键决策节点保留原文。3.5 退出条件为什么纯靠“模型自己说结束”不可靠循环什么时候结束最简单的方法是让 LLM 自己判断“我已经回答完了输出纯文本结束”。但实测发现模型有时候会“自作主张”提前结束——明明用户让它做一连串操作它完成第一步就直接输出“好的已完成”。这是 LLM 的“省事”倾向。所以 JavaManus 的退出机制除了依赖模型的文本响应还加了一道硬性校验如果用户请求里包含了明确的多步任务关键词比如“先……然后……”“对比一下……和……”循环器会检查已发生的工具调用是否覆盖了所有必要的工具。如果发现有遗漏会主动提示模型“你还有 XX 步骤没有完成请继续”。虽然这不能完全杜绝模型偷懒但能明显减少提前结束的概率。4. 并行工具调用把虚拟线程和 CompletableFuture 结合的高级玩法4.1 并行调用的场景与困境Agent 经常需要一次调用多个工具。比如用户问“对比 A 和 B 这两款手机的综合参数和当前价格”如果串行执行get_phone_info(A)、get_phone_info(B)、get_price(A)、get_price(B) 四个工具调用依次等待每个调用平均 2 秒光工具执行就花掉 8 秒用户体验很差。但如果并行执行理论上只需要 2 秒左右。JavaManus 引入了并行调用执行器。核心逻辑是解析 LLM 返回时如果发现一条响应里包含多个工具调用请求就按依赖关系分组。没有依赖的提交到并行执行器有依赖的按拓扑顺序分批执行。4.2 虚拟线程实现的并行执行器我用 Java 虚拟线程做了个简单的执行器public ToolCallResultSet executeInParallel(ListToolInvocation invocations) { ListVirtualThread threads invocations.stream() .map(invocation - Thread.startVirtualThread(() - { ToolResult result executeSingle(invocation); results.put(invocation.id(), result); })) .toList(); for (Thread thread : threads) { thread.join(DEFAULT_TIMEOUT_MS); } return new ToolCallResultSet(results); }这里没有使用传统线程池因为虚拟线程的资源开销极低。一次跑二三十个工具调用传统线程池要么排队、要么需要精心调参虚拟线程则完全不用操心。当然虚拟线程不能无脑乱用它只适合这种 IO 密集型任务对于 CPU 密集的操作还是得上传统线程池。还有一个细节join的超时控制。如果某个工具卡死了必须保证整个执行器不会无限等待。这里我给每个工具设了默认超时和最大超时两种配置超时后的结果会标记为“TIMEOUT”并返回一条半结构化错误信息。4.3 数据竞争与状态一致性的处理并行调用最大的隐患是数据竞争。比如两个工具同时往同一个缓存里写数据或者一个测试工具内部维护了一个静态计数器。JavaManus 的处理策略很简单状态类工具默认标记为ConcurrencySafe(false)在注册时被放进串行执行通道。纯只读工具标记为ConcurrencySafe(true)允许并行执行。工具作者需要明确声明自己的工具是否并发安全。如果声明错误比如把内部有状态修改的工具标成了并发安全框架在运行时会通过日志记录潜在的数据竞争警告。5. 上下文管理让 Agent 不再“失忆”同时管住 Token 成本5.1 上下文窗口裁剪策略LLM 的上下文窗口虽然越来越大但 Agent 框架不能依赖它无限膨胀。理由有两个一是 Token 费用是真实成本二是过长的上下文会影响模型的注意力分布导致“中间遗忘”。JavaManus 采用三级上下文管理第一级精简系统提示词 工具描述这部分通过按需动态重构来缩减体积。比如只注入当前可能会用到的工具描述而不是全部工具。第二级窗口对话历史保留最近 20 轮。第三级压缩超过窗口的旧消息调用 LLM 生成摘要并替换原文。由于摘要调用本身也要消耗 Token所以框架专门做了一个“摘要预算”控制如果对话摘要预计超过原始长度的 40%宁可直接截断也不要生成摘要因为这种情况下摘要性地丢弃细节反而比重新生成更经济。5.2 摘要生成的关键经验用 LLM 做摘要时有一个很经典的问题摘要后 Agent 会忘记它曾经调用过哪些工具。用户后来问“你刚才是怎么查到那个数据的”Agent 答不上来因为摘要里没写。解决办法是在摘要模板里强制要求保留所有工具名称及核心参数不纠结于自然语言细节。这算是实测过最有效的方案之一——工具调用记录是推理链条的关键骨架必须完整保留哪怕自然语言描述被压缩得再干瘪。5.3 消息 ID 与依赖追踪在一个多轮循环里消息之间其实是有依赖关系的工具调用请求消息 → 工具执行结果消息 → 模型的下一轮决策。JavaManus 为每条消息分配了全局唯一的MessageId并在工具调用结果里携带对应“上游消息的 ID”。这样在审计、调试、回溯“Agent 为什么会做出这个决定”时可以顺着 ID 构建完整的决策链路图而不只是一堆时间戳和字符串。6. 流式输出与调试把“黑盒”变成“透明箱”6.1 SSE 流式输出的实现方案Agent 执行过程中如果是长时间的工具调用循环用户面对一个“转圈20秒”的空页面体验极糟。JavaManus 提供了 SSEServer-Sent Events流式输出模型决策阶段逐个推送文本增量。工具调用阶段推送“正在调用工具get_stock_price参数{...}”。工具结果返回推送工具执行结果摘要。具体实现时就是在一个异步线程池里跑 Agent 循环然后通过SseEmitter把事件推给前端。这里要特别提醒一个细节SSE 的线程和 Agent 循环线程必须独立管理否则一个长时间的工具调用会阻塞整条 SSE 流的推送线程。6.2 Agent Trace 日志开发调试的灵魂Agent 的调试是出了名的难普通日志根本定位不了问题。JavaManus 内置了一个 Trace 系统每轮 Agent 循环都会生成一条结构化 Trace 记录包含当前等待处理的用户请求LLM 的原始输入完整消息列表LLM 的原始输出解析结果是纯文本还是工具调用每个工具调用的耗时与结果摘要循环次数及当前累计 Token 消耗这套 Trace 信息既可以输出到日志文件也可以通过 Debug 接口实时查询。我强烈建议任何 Agent 框架开发者都做这种“可观测性”设计否则线上出问题排查成本比开发成本还高。6.3 运行时热更新与 Debug API考虑到 Agent 工具经常需要调整JavaManus 还实现了简单的热更新机制——通过 JMX 暴露工具注册信息运行中可以动态加载新的工具类并注册进正在运行的 Agent 实例。这个功能在开发调试阶段尤其好用无需重启整个服务改一行工具逻辑就能立刻生效。7. 完整示例从零构建一个“研究助手”Agent7.1 场景定义与需求梳理这里用一个完整的例子来演示整个框架的用法。我准备构建一个“研究助手 Agent”它的任务是给定一个研究主题自动搜索相关资讯、抓取页面内容、生成摘要最后综合输出一份简要报告。这个 Agent 需要四个工具web_search(query)调用搜索引擎 API 获取搜索结果列表。web_fetch(url)抓取指定 URL 的页面文本内容。text_summarize(text, max_words)调用 LLM 对文本做摘要。save_note(content, tags)把笔记存到本地文件系统。7.2 工具实现与注册每个工具就像写一个普通 Java 方法一样简单AgentTool(name web_search, description 根据关键词搜索互联网返回相关网页标题、URL 和摘要) public ListSearchResult webSearch( ToolParam(description 搜索关键词) String query, ToolParam(description 返回结果数量默认5, defaultValue 5) int topK ) { return searchClient.search(query, topK); } AgentTool(name web_fetch, description 抓取指定URL的网页正文内容返回纯文本) public String webFetch( ToolParam(description 要抓取的完整网页URL) String url ) { return pageFetcher.fetch(url); } AgentTool(name text_summarize, description 使用大模型对长文本进行摘要返回指定字数的简洁摘要) public String textSummarize( ToolParam(description 需要被摘要的原文) String content, ToolParam(description 摘要最大字数, defaultValue 200) int maxWords ) { return llmClient.summarize(content, maxWords); } AgentTool(name save_note, description 将研究过程中的重要发现以笔记形式持久化保存) public String saveNote( ToolParam(description 笔记正文内容) String content, ToolParam(description 标签列表) ListString tags ) { return noteRepository.save(content, tags); }注册也很简单Agent agent AgentBuilder.create() .withLlmProvider(openAiLlmProvider) .withTools(List.of(webSearchTool, webFetchTool, textSummarizeTool, saveNoteTool)) .build();7.3 执行效果与实测数据我用 JavaManus 跑了一个实际任务“对比 Spring Boot 和 Quarkus 的启动性能并生成一份简要调研报告”。实际执行过程如下第一轮决策调用web_search(query: Spring Boot startup benchmark Quarkus)。拿到 10 条搜索结果后第二轮决策调用web_fetch抓取前 3 个相关链接的内容。第三轮决策对抓取的内容分别调用text_summarize然后综合生成最终回答。全程耗时约 9 秒其中大部分时间花在搜索和抓取等待上。Token 消耗大致在 4500 左右包含中间摘要。如果并行优化一下耗时可以降到 5 秒内。这个流程在传统的人工操作里至少要花 15 分钟以上Agent 把它压缩到了个位数秒——这就是 Agent 框架的实际价值。8. 避坑指南Agent 框架开发中的经典问题与排查实录8.1 数据结构层面识别和处理 LLM 的输出陷阱问题一模型返回工具调用时随机给出多余的字段。比如明明要求只返回{name: get_weather, args: {...}}实际却返回了{name: get_weather, args: {...}, thought: ...}。如果你在反序列化时开启了FAIL_ON_UNKNOWN_PROPERTIES直接抛异常。解决方法是自定义ObjectMapper逐字段校验未知字段忽略。这个坑几乎每个 Agent 框架开发者都踩过提早处理能少走很多弯路。问题二参数名不匹配。模型有时把query写成了q把topK写成了top_k。JavaManus 在 ToolSchema 生成时做了一个启发式别名映射自动允许模型输出中常见的camelCase和snake_case互认。实现思路是给ParameterSpec生成一个aliases字段解析时按别名匹配最后再映射回 Java 字段名。问题三循环失控。Agent 陷入了死循环反复调用同一个工具却无法收敛。JavaManus 内置了循环次数上限默认 8 轮超过后强制终止并将已收集的结果按最后文本组装输出。这里要强调一下终止不能粗暴地抛异常而是应该返回“已完成部分任务但因循环限制提前结束”的半成品结果避免用户侧产生“服务报错”的错觉。8.2 线程层面虚拟线程和线程池工具共用时的那些坑虚拟线程虽好但和某个依赖线程局部变量的传统工具库共用时会出问题。比如有的老牌 HTTP 客户端库在多个线程间共享一个ThreadLocal连接池上下文当虚拟线程切换时ThreadLocal 内容可能混乱。所以我在框架里做了两层保险一是工具执行时统一包装成 Callable捕获所有上下文异常二是对工具作者建议不要在工具内部依赖ThreadLocal存过程数据一定要依赖时就得用InheritableThreadLocal或者显式传递上下文。这个建议是实践中得来的——排查一次偶发性的数据串线问题我花了整整一个下午最后定位到就是ThreadLocal在虚拟线程间“串门”导致的。8.3 资源层面Token 用量控制、限流和并发保护Agent 比普通 API 调用消耗的 Token 多得多。原因是每个工具的执行结果都会回传 LLM在一次复杂任务中可能有几十轮循环。如果业务量大Token 成本会让人眼前一黑。JavaManus 做了一套 Token 预算机制在每个 Agent 实例上设置maxContextTokens当累计 Token 消耗超过预算时自动开启“只保留必要信息”模式减少摘要、只保留最近 5 轮消息、工具全文参数改为摘要参数。这样虽然可能略微影响推理质量但把成本锁在了可接受范围内。另外对 LLM API 的限流也要注意。同一个模型供应商通常有 RPM每分钟请求数和 TPM每分钟 Token 数两个维度限制。JavaManus 在 LLM 适配器层内置了滑动窗口限流器超过限制时自动排队重试而不是直接把 429 错误抛给上层。这个设计在并发请求多的时候尤其重要——我见过团队上线 Agent 服务后因为没做限流保护直接被供应商封禁了一个小时。8.4 安全层面让 Agent 的“手”不要乱摸Agent 能调用工具也就意味着你的系统向 LLM 敞开了“手”的接口。如果某个工具是删除文件、发送消息这类敏感操作被模型误用会造成严重后果。JavaManus 给这类工具加上了needsConfirmationtrue标记执行前会暂停循环向用户发起确认请求只有用户点击“确认”按钮后才会真正执行。这个设计某种程度上是把“人在回路”Human-in-the-Loop原则落到了代码层面也是 Agent 落地到生产环境时的一个必要保险。9. 回望与心得Java 生态里做 Agent 框架真正难的是什么项目从零开始到第一版可运行我断断续续花了大约一个月的时间。把整个设计和实现过程回头看一遍最有价值的心得反而不是那些精巧的代码技巧而是对“Java 和 Agent 之间关系”的理解。很多人问我不去直接用现成的 Java Agent 框架为什么要从零造轮子我的回答是只有自己从零构建才能真正理解“工具定义即接口”这句话的深层含义。Agent 框架的成功与否百分之七十取决于工具抽象层的设计剩下的百分之三十才是 LLM 调用和循环管理。而工具抽象层在 Java 里恰恰是最考验对语言特性理解深度的地方——泛型、反射、注解、接口设计、并发模型每一个都能成为整个框架的瓶颈或者助推器。我也清醒地认识到JavaManus 和一些工业级框架的差距依然明显比如它没有内置完善的多模态工具支持没有分布式的多 Agent 协调机制工具生态也远不够丰富。但正因为一切都摊开了、读得懂、改得动所以它成了我理解 Agent 体系最好的“解剖样本”。最后分享一个非常实际的小建议不管你是从零造轮子还是用现成框架一定一定要在项目初期就把“可观测性”做进去——记录每一轮消息、每一个工具调用、每一步 Token 消耗。等你的 Agent 在线上真实服务用户的时候就会知道这些日志就是救命的稻草。