JavaManus 这个名字,拆开看是 Java 加 Manus,Manus 在拉丁语里是“手”的意思,连起来就是“Java 之手”。我给这个框架起名时想的不是蹭热点,而是它背后那个很本质的转变:让大模型长出一只手。模型本身只会输出文字,但接上工具调用、循环决策、任务拆解之后,它就能去查数据库、调内部系统、发消息、做分析,这才是 AI Agent 和普通智能问答的分水岭。
做这个框架的起因其实很朴素。我接手了一个内部数据项目,需要在现有 Java 服务里接入大模型能力。最初的想法是直接引一个成熟的 Python 版 Agent 框架,让 Python 服务做中转,结果越想越不对劲:部署环境要额外加一套 Python 运行时,监控链路要维护两套,最要命的是 Agent 的每次工具调用都要穿透到 Java 侧的核心业务系统,跨语言边界会一直横在所有调试、排障、扩展的路上。反复权衡之后,我决定在 Java 侧从零写一个轻量 Agent 框架,于是有了 JavaManus。这篇文章会把这套框架的架构设计、核心实现、以及我反复推翻过的决策和真实踩过的坑完整拆一遍。适合正在做 Java 技术栈大模型落地的工程师,也适合想彻底搞懂 Agent 原理、愿意动手复刻一遍的开发者。
1. 为什么是 Java 写 Agent:不是技术执念,是场景倒逼出来的选择
1.1 生态差距只是表面问题
很多人听到“用 Java 写 Agent 框架”的第一反应是:Agent 生态不都在 Python 那边吗?这句话对了一半。大模型训练、微调、学术研究的前沿生态确实集中在 Python,市面上主流的 Agent 编排库也大多是 Python 优先。但我们要区分“研究生态”和“工程生态”:如果你要做的是把模型接进一个已经在跑的生产系统,Java 的工程生态才是真正值钱的。
我做 JavaManus 之前做过一次简单对比。研究侧的常用组件,Prompt 模板、向量库客户端、模型 SDK,Python 确实丰富,但这些东西基本都有 Java 版本,只是文档少一点、更新慢一点;反过来,Java 侧的线程池、连接池、成熟 Web 框架、API 网关、分布式链路追踪、配置中心,Python 想达到同等工程等级,往往要花更多时间去拼装。Agent 落到生产环境之后,真正消耗精力的不是“调模型、写 Prompt”,而是限流、超时、并发、幂等、审计、灰度发布这些工程问题,而这些恰好是 Java 的强项。
1.2 企业内部落地的三个现实约束
第一是团队技能栈。我接触过的业务系统研发团队,绝大多数只招 Java 工程师。引入 Python 组件之后,代码评审、故障排查、安全走查都变成跨语言协作,每次出问题第一反应都是“这个 Agent 是 Python 写的,我不会看”,这种隐性成本会在长期运维里持续放大。与其赌团队愿意学第二语言,不如把 Agent 直接做成 Java 技术栈内的一层运行时。
第二是工具调用半径。Agent 的杀伤力来自它能调用企业内部系统的能力。Java 服务直接持有这些系统的客户端、连接池、缓存、消息队列,Agent 框架放在 Java 里,工具注册就是注入一个 Bean 的事;放在 Python 里,只能靠 HTTP 接口或者消息队列绕一层,每次工具调用都多一段链路,多一个故障点。我见过不少跨语言方案最后死在“调试连不上”“现场要开两个环境”这种琐碎问题上。
第三是部署与治理。公司的基础设施,从 CI/CD 到监控告警,基本都是围绕 Java 应用搭的,往里面插一个 Python 进程,等于重新走一遍发布流程、权限审批、资源配额。用 Java 实现 Agent 框架,整个服务就是原有部署体系里的一个普通应用,所有既有流程直接复用。这个优势不体现在代码里,但体现在每一个发布日的安稳上。
1.3 什么时候我反而会劝你用 Python
选 Java 不代表无脑上 Java。如果你的目标是快速验证想法、跑通一个研究原型,Python 依然是更顺畅的选择;如果你的 Agent 需要频繁对接研究前沿组件,比如天天追最新论文、随时换新的推理库,Python 生态会省很多事。
我给自己定的判断标准很简单:看问题本质是“研究验证”还是“生产落地”。前者用 Python,后者如果技术栈是 Java,就踏踏实实在 Java 侧实现。JavaManus 的定位非常明确,给企业级 Java 业务系统提供一个可嵌入、可治理的 Agent 运行时。下面这张表是我当时的选型对比,后来证明大部分判断都站得住:
| 对比维度 | Python 生态 | Java 侧自研 |
|---|---|---|
| 模型接入 SDK | 选择多,更新快 | 选择少,但够用 |
| 工程组件 | 需自行拼装 | 开箱即用 |
| 与企业系统集成 | 跨语言绕层 | 进程内直接调用 |
| 团队可维护性 | 依赖团队会 Python | 复用现有 Java 能力 |
| 适合阶段 | 研究验证、原型 | 生产落地、长期运营 |
2. Agent 核心循环:把“模型接口调用”改造成“目标驱动的执行引擎”
2.1 一次 API 调用和 Agent 循环的分水岭
普通的大模型 API 调用是“用户发一段话,模型回一段话”,一次交互就结束。Agent 不一样,它是“目标-执行-观察-再决策”的循环:模型每次输出可能是一段最终回答,也可能是一个工具调用指令,框架执行工具后把结果塞回对话历史,再次让模型决策,直到模型认为目标完成。这个循环,才是 Agent 区别于“智能问答”的核心机制。
JavaManus 的架构里有一个关键决定:把“对话服务”和“Agent 运行时”分开。对话服务只管收发消息,Agent 运行时负责循环。循环里的每一步都要可观测、可中断、可恢复,否则到了生产环境,出了问题连原因都查不到。整个运行时对外只暴露一个主入口,传入用户目标,返回最终结果,内部维护一个状态机。
2.2 最小循环骨架的实现
下面是 JavaManus 核心循环的简化版本,删掉了大量细节,但保留了最关键的结构:
public class AgentRuntime { private final ChatClient chatClient; private final ToolRegistry toolRegistry; private final ContextManager contextManager; private final AgentObserver observer; public AgentResult run(String userInput, AgentContext ctx) { ctx.appendUserMessage(userInput); int step = 0; while (step < maxSteps) { // 1. 按 token 预算裁剪/压缩历史,构造本次请求消息 List<Message> messages = contextManager.buildMessages(ctx); // 2. 调用模型,拿到本轮决策 ModelResponse response = chatClient.chat(messages); observer.onModelDecide(response, step, ctx); // 3. 决策分支:要么是最终答复,要么是一批工具调用 if (response.hasToolCalls()) { for (ToolCall call : response.getToolCalls()) { observer.onToolInvoke(call, step); ToolResult result = toolRegistry.execute(call); ctx.appendToolMessage(call.getId(), result.toText()); } } else { ctx.appendAssistantMessage(response.getContent()); return AgentResult.finished(response.getContent(), step); } step++; } // 4. 到达步数上限,强制返回当前状态 return AgentResult.overLimit(step, ctx.snapshot()); } }这段代码看着简单,但每个点都是被真实事故逼出来的。maxSteps 上限对应稳定性,没有它,模型陷入循环时会无限烧钱;observer 对应可观测性,每个决策点必须上报;contextManager 对应成本与上下文控制,它决定每一轮把多少历史消息发给模型。这三个点分别是稳定性、可观测性、成本控制,任何一个做不好,Agent 都只能停留在 Demo 阶段。
2.3 消息角色与工具结果的规范化
大模型的对话接口通常要求消息带角色。Agent 场景比普通聊天多一类特殊的消息——工具结果回填。我在 JavaManus 里把消息统一抽象成 Message 对象,角色枚举只有四种:system、user、assistant、tool。tool 角色专门放工具执行结果,并且必须携带工具调用 ID,以便和对应的 tool call 关联。
这里有个容易踩的坑:工具结果的格式。不同模型对工具结果的格式要求不一样,有的要求纯文本,有的要求必须带工具调用 ID,有的还要求带上“当前是第几次调用”之类的元信息。JavaManus 的折中方案是:所有工具结果统一转成单段文本,但文本第一行保留结构化前缀,比如执行状态、耗时、返回值类型。这样模型能读懂,调试时人也能读得懂。
注意:把工具结果写进历史,不只是为了给模型看。保持历史里每一步 tool call 与 tool result 的 ID 对应关系,是后续做“回溯修正”和“轨迹重放”的基础。如果图省事丢失了 ID 关联,运营期想排查一次错误决策会很痛苦。
2.4 步数上限、成本估算与循环终止策略
Agent 循环不能无限跑,这是成本控制的底线。我一开始把 maxSteps 设成 30,后来压到 15,发现整体成功率反而更高。原因是步数上限越小,模型越倾向于尽早给出阶段性结果,而不是在长链路里越走越偏。每一步循环的成本可以用一个简单公式估算:
单次任务成本约等于循环步数乘以每步平均 Token 数,再乘以单 Token 价格。比如每步平均消耗 3000 Token,跑 10 步就是 3 万 Token,商用模型一美元能处理几十万 Token,单次成本很低;但一旦模型陷入循环,跑 50 步,成本就翻五倍,更可怕的是用户还在等结果。所以循环终止策略我设计了三种:正常完成、步数超限强制终止、连续多轮无进展提前终止。第三种最实用,当相邻几轮模型都在重复调用同一个工具并得到相同结果时,基本可以判定它在原地打转,直接终止并返回当前进展比硬等更体面。
3. 工具系统:Agent 长出“手”的那一层
3.1 工具接口的统一抽象
工具是 Agent 与外部世界的桥梁。JavaManus 的工具接口定义得极简:
public interface AgentTool { String name(); // 全局唯一,模型通过它选择工具 String description(); // 给模型看的功能说明 boolean needsConfirm(); // 危险操作是否需要人工确认 String execute(String argumentsJson); }name 和 description 是模型做决策的依据,needsConfirm 是安全闸门,execute 是实际执行。execute 的参数设计成 String 而不是强类型对象,是因为模型返回的 arguments 是 JSON 文本,框架层不做强转,解析责任下放到具体工具实现里。刚开始我觉得这样不够优雅,后来发现好处很多:每个工具可以独立演化、独立测试,不会因为参数模型变化而被迫修改框架,也不会因为某个工具的参数设计得特别复杂而污染统一的调用链。
3.2 注解驱动的工具注册与 Schema 生成
模型要按规范调用工具,框架必须把每个工具的描述整理成模型要求的 JSON Schema。手工写 Schema 一定不可持续,所以我用注解加反射自动推导。每个工具就是一个带 @AgentTool 注解的类:
@AgentTool(name = "query_order", description = "根据订单号查询订单状态与物流信息") public class OrderQueryTool { public String execute( @ToolParam(name = "orderId", desc = "订单号,例如 ORD20240101") String orderId, @ToolParam(name = "withLogistics", desc = "是否返回物流轨迹", required = false) boolean withLogistics) { // 调用内部订单服务,返回 JSON 字符串 } }框架启动时扫描类路径,对每个带注解的方法生成一份工具描述,核心逻辑是从方法签名反推参数类型和必填项:
public static ToolSchema generateSchema(Method method) { ToolSchema schema = new ToolSchema(); for (Parameter p : method.getParameters()) { ToolParam ann = p.getAnnotation(ToolParam.class); schema.addProperty(p.getName(), new PropertySchema( mapJavaTypeToJsonType(p.getType()), ann == null ? "" : ann.desc(), ann == null ? true : ann.required())); } return schema; }有个细节必须强调:Java 里方法参数名默认在反射时是 arg0、arg1,所以必须依赖 @ToolParam 显式声明参数名,否则生成的 Schema 会变成一堆 argX。如果团队用的 Java 版本开启了保留参数名的编译参数,可以省掉注解里的 name,但我更推荐显式声明,因为 IDE 重构时参数名容易变,而 Schema 一旦发布给模型使用,改名也得同步提示词,显式注解反而能防止误改。
3.3 模型 tool call 的解析容错
模型输出的 tool call 参数是 JSON 文本,但实际生产里你会发现,模型经常输出不规范的 JSON:多余逗号、字符串没转义、字段大小写不一致、甚至把布尔值写成字符串。这些情况直接按 JSON 解析必然会失败。
JavaManus 的容错策略是分三级:先做标准解析;失败后用宽松模式,去掉未知字段、修正常见格式错误;再失败就把原始文本交给工具自己处理。宽松模式的核心是只提取已知字段,忽略未知内容。这个设计看起来不起眼,但在真实运行中能显著降低“模型输出格式错误导致整个任务失败”的概率。我自己统计过,加上宽松解析之后,工具调用阶段因为格式问题失败的次数下降了大约七成。
3.4 工具失败的反馈策略与人工确认闸门
工具执行必然有失败场景:超时、网络抖动、业务异常、参数校验失败。JavaManus 的执行器里做了统一异常包装:
public ToolResult execute(ToolCall call) { AgentTool tool = registry.get(call.getName()); if (tool == null) { return ToolResult.fail("未找到工具: " + call.getName()); } try { String text = tool.execute(call.getArguments()); return ToolResult.ok(text); } catch (Exception e) { return ToolResult.fail("执行失败: " + e.getClass().getSimpleName() + " - " + e.getMessage()); } }关键点在于:工具执行失败绝不直接向上抛异常让整个循环崩掉,而是把失败原因作为工具结果回填给模型,让模型自己决定怎么处理——换参数重试、换工具、还是承认搞不定并把问题交还给用户。Agent 能不能“聪明地犯错后自救”,很大程度上取决于失败信息的质量。fail 文本里我会尽量附带结构化信息:异常类型、可读的失败原因、重试建议,而不是一句冷冰冰的 Exception。
提示:对于删除、转账、发送外部消息这类有副作用的工具,建议在框架层强制开启 needsConfirm。JavaManus 的实现是执行前先返回一个“确认态”给用户,用户确认后再真正执行;确认信息本身也是一条特殊的工具结果,模型看到确认结果后才继续推进。
4. 规划与任务拆解:从 ReAct 到可中断恢复的任务图
4.1 ReAct 模式:思考、行动、观察的循环格式
早期版本里 JavaManus 只管“模型说调工具就调、说完就结束”,跑通很简单,但用户丢来一个复杂目标就废了。比如“统计上个月所有渠道的销售数据,按城市做对比分析,再生成摘要发给领导群”,这种目标需要拆成取数、清洗、分析、生成报告、发送消息五个环节,任何一环的中间结论都会影响后续步骤。模型靠单轮对话是 hold 不住的。
我采用的方案是经典的 ReAct 模式:Reasoning(思考)加 Acting(行动)加 Observation(观察)。每一轮模型先输出一段短思考,说明它现在打算干嘛、为什么,然后输出行动指令,框架执行后把观察结果返回,模型继续思考、继续行动。JavaManus 在系统提示词里显式约束模型的输出格式,要求按“Thought: ... Action: ..., Action Input: ...”的结构输出,同时用解析器把这段文本拆成结构化的决策对象。后来支持原生函数调用的大模型越来越多,模型可以直接输出结构化 tool call,但 ReAct 的思考链文本依然保留,因为它是我们看得到“Agent 为什么这么做”的线索。
4.2 Prompt 模板与决策解析
ReAct 模式的 Prompt 要写清楚,模型才不会跑偏。JavaManus 的系统提示词骨架大致长这样:
你是一个可以调用工具完成任务的智能助手。 每一轮请严格按照以下格式输出: Thought: 你当前对任务的分析 Action: 要调用的工具名 Action Input: 工具参数的 JSON 当任务完成后,请输出 Final Answer: 最终结果 注意:不要编造工具返回的数据,工具返回什么就基于什么继续。这段提示词看起来平平无奇,但里面的第五行“不要编造工具返回的数据”是血的教训。早期没有这一行,模型在拿不到完整数据时经常自己脑补,把不存在的订单状态写进报告,而且写得煞有介事。加上这一条之后,幻觉类错误明显减少。解析器这边,我用正则提取 Thought、Action、Action Input 三个字段,Action Input 再走一遍前面说的宽松 JSON 解析。整体不复杂,但稳定可靠。
4.3 子任务状态机与 DAG 依赖管理
多步执行时,我用一个简单的有向无环图管理子任务。TaskNode 表示一个子任务,持有依赖列表和状态;TaskGraph 负责拓扑排序和状态推进。状态定义非常简单:
public enum TaskState { PENDING, RUNNING, SUCCEEDED, FAILED, BLOCKED }执行顺序是:PENDING 的任务在所有依赖完成后进入 RUNNING,执行成功变 SUCCEEDED,失败变 FAILED;如果某个任务阻塞,比如等人工审批,它的下游全部标记为 BLOCKED。这个状态机看起来平淡无奇,但它解决了 Agent 落地时一个非常实际的问题——可中断恢复。任务一旦挂了,监护人可以根据状态快照知道它卡在哪个子任务、依赖什么,精确断点续跑,而不是让用户从头再来一遍。
4.4 一个多步任务的完整执行过程
拿“调研三个竞品的公开报价并汇总差异”举例,JavaManus 的规划器会生成类似下面的任务序列:
- 任务 A:调用查询工具,获取三个竞品的报价数据(依赖:无)
- 任务 B:对结果做清洗和结构化,提取价格与套餐项目(依赖:A)
- 任务 C:生成对比表格与差异摘要(依赖:B)
- 任务 D:可选,把摘要通过企业 IM 工具发送给指定频道(依赖:C,needsConfirm=true)
这个过程中最值得说的是规划器本身的实现。规划器可以是模型驱动,也可以是基于模板的。JavaManus 先走模板匹配,目标句式能匹配到已有模式就直接套用固定流程,匹配不到再交给模型现场规划,并把新规划缓存下来,下次类似目标就能复用。这种“先固定后动态”的设计,大幅降低了模型推理成本和结果不确定性,也让常见任务的执行链路越来越稳定。
5. 记忆与上下文管理:让 Agent 在有限窗口里记住该记的事
5.1 Token 预算的估算与裁剪
上下文窗口是有限的,而 Agent 每多执行一步,历史里就会多一组 tool call 和 tool result,很快会把窗口撑爆。不加以控制,模型要么遗忘前面的上下文,要么直接报超长错误,要么 Token 费用飞速上涨。我把它看作 Agent 工程化的隐形生命线。
JavaManus 的上下文管理器维护一个简单的 Token 预算模型:
public class ContextManager { private static final int MAX_CONTEXT_TOKENS = 8000; private static final int RESERVED_OUTPUT_TOKENS = 1500; public List<Message> buildMessages(AgentContext ctx) { List<Message> history = ctx.getHistory(); int budget = MAX_CONTEXT_TOKENS - RESERVED_OUTPUT_TOKENS - estimateTokens(ctx.getSystemPrompt()); List<Message> kept = new ArrayList<>(); int used = 0; for (int i = history.size() - 1; i >= 0 && used + tokensOf(history.get(i)) < budget; i--) { kept.add(0, history.get(i)); used += tokensOf(history.get(i)); } return kept; } }有个容易被忽略的细节:估算 Token 时,中文、英文、代码的折算系数完全不同。最可靠的方案是直接调用模型的 tokenizer,但每次请求都跑一遍太慢。JavaManus 用的是分段加权估算:纯中文按每字 1.5 Token、英文按每词 1.3 Token、代码按每行 4 Token。实践证明足够接近真实值,而且速度快到可以忽略。
5.2 历史消息的摘要压缩
预算裁剪会直接丢掉早期消息,但有些早期信息不能丢,尤其是用户最初的目标。所以裁剪之外必须有“摘要压缩”机制:当上下文快超限时,调用模型把早期消息压缩成一段摘要,放进 system 消息,替代被裁剪掉的原始消息。设计原则是“系统目标始终保留,工具结果可丢可摘要,对话过程按重要性保留”。
压缩触发不能太频繁,否则模型开销会反过来吃掉收益。我的阈值是:预算使用超过 70% 时触发压缩,压缩后预留至少 30% 的净空间;如果一轮压缩还不够,再进入强制裁剪。这样做的效果是,长任务跑到 20 步、30 步时,上下文质量依然稳定,模型不会因为丢掉了关键前提而开始胡言乱语。
5.3 长期记忆:轻量向量索引的实现
长期记忆这块,我一开始犹豫了很久要不要上专门的外部向量数据库。最后结论是:JavaManus 先不做重客户端,而是用 Embedding 接口加一个进程内向量索引,索引量在十万条以下完全够用。流程是:每完成一个重要子任务,就把结论写入记忆库;新任务进来时,先按用户目标做相似度检索,把最相关的几条历史结论拼进上下文。
检索部分用余弦相似度就够,核心代码不算复杂。这套轻量方案的好处是零独立部署成本,随应用启动,适合内部工具场景;如果未来记忆量真的大到需要独立存储,接口层的抽象可以直接替换成外部向量库,不影响上层逻辑。先跑起来,再谈扩展,这个顺序我一直很信奉。
6. 往生产环境推:超时、并发、可观测性的工程化
6.1 LLM 调用的三段式稳定性策略
写 Agent 最核心的稳定性话题,绕不开模型接口的不可靠。第一版我在这上面栽过大跟头:一个耗时 30 秒的推理任务,因为上游偶发超时,Agent 直接抛异常,用户不仅要重来,还看不懂发生了什么。后来我在模型客户端封装了完整的三段式策略:超时、重试、熔断。
超时分为连接超时和读超时,读超时设得比模型承诺的最长响应时间略长,比如 60 秒;重试只对安全请求开启,采用指数退避加随机抖动,避免重试风暴;熔断用一个简单的滑动窗口计数器,连续失败超过阈值就短时间内降级,直接返回“模型服务当前不可用”的友好提示,避免请求排队把整个应用拖垮。这三段策略单独拿出来都常见,但组合在一起,并且能在 Agent 循环内部生效,才算完整。
public ModelResponse chatWithRetry(List<Message> messages) { int attempt = 0; while (attempt < maxRetries) { try { return chatClient.chat(messages); } catch (RetryableException e) { attempt++; long backoff = baseDelayMs * (1L << attempt) + ThreadLocalRandom.current().nextInt(200); Thread.sleep(backoff); } } throw new AgentUnavailableException("模型服务暂时不可用"); }6.2 线程池隔离与并发限流
LLM 调用是典型的慢 IO 操作,动辄几秒甚至几十秒。如果直接放在业务线程池里跑,几个并发任务就能把线程池线程全部占满,拖垮整个应用。JavaManus 的做法是给模型调用单独建线程池,并配一个信号量限流。线程池大小根据模型接口的并发配额来定,限流值略低于线程数,确保请求不会在应用层无限堆积。
这个隔离带来的额外好处是:即使模型服务故障导致线程池占满,受影响的也只是 Agent 相关功能,业务主流程不受牵连。线上没有这个隔离的时候,一次模型抖动把整个应用线程池打满的场景我是见过的,那种“因为一个锦上添花的功能拖垮核心业务”的教训,一次就够了。
6.3 异步执行与流式输出回调
Agent 跑一个多步任务是要花时间的,用户的 HTTP 请求不可能同步等到天荒地老。JavaManus 的运行时支持异步启停,对外暴露 future 或回调。核心设计是回调钩子:整个循环里每个关键事件都有对应回调,从“开始规划”到“工具执行完成”到“最终回复完成”,注册方自行决定是写日志、推 SSE 还是通知前端。
流式输出对体验影响很大,用户看到“正在处理”比干等强得多。SSE 推送实现上用的是响应式流,按事件往输出流写 JSON 行。这里有一个经验:SSE 断连必须考虑重连一致性。JavaManus 给每条事件都加自增序号,前端重连时带上最后序号,后端从断点续推。这个设计在真实线上帮了大忙,否则前端刷新一次就丢整条执行轨迹,用户体验会很割裂。
6.4 用结构化 trace 给 Agent 写“日志”
最容易被低估的模块是可观测性。普通程序的问题可以看日志、堆栈、性能数据,Agent 的问题往往出在“模型为什么这么决策”。如果只有最终结果,排查一次错误决策几乎无从下手。JavaManus 内部给每一轮循环都生成一条结构化 trace,记录当时的输入消息、模型输出、工具调用参数、工具返回值、耗时、Token 消耗。
| 日志类型 | 记录内容 | 解决什么问题 |
|---|---|---|
| 决策日志 | 模型本轮输出的思考与行动 | 定位模型从哪一步开始跑偏 |
| 工具日志 | 工具名、入参、出参、耗时 | 定位工具参数传错、结果异常 |
| 预算日志 | 每轮 Token 估算、裁剪、压缩动作 | 定位上下文丢失导致的失忆 |
| 异常日志 | 超时、重试、熔断事件 | 定位上游故障与策略触发 |
有了 trace,还能做轨迹重放:用测试工具读入历史 trace 文件,离线模拟同一段对话,改 Prompt、换模型、调参数后对比决策差异。这一步相当于给 Agent 写“单元测试”,虽然测的是模糊的模型行为,但至少守住了“不回归”的底线。没有这套机制之前,我每次改 Prompt 都提心吊胆,有了之后心里踏实多了。
7. 实测复盘:效果、坑位与后续方向
7.1 实际场景的收益与局限
我把 JavaManus 接到一个内部数据汇总项目里跑了两个月,场景是让 Agent 根据自然语言指令查询多个数据源、做简单汇总并输出报表。对比之前用纯对话模型直接回答,有两个明显变化:一是能处理多数据源、多步骤的指令,这是普通问答做不到的;二是出错时能看到完整的推理与工具轨迹,人工介入修复的效率高了很多。
当然也有很多不完美的地方。复杂指令的规划偶尔会绕远路,明明有直达工具,模型却先调了三个别的工具;某些长任务里,模型会渐渐忽略系统提示里的格式要求;还有极少数情况,模型为了完成目标,会编造工具没有返回的数据。这些问题都是 Agent 的固有挑战,跟框架用什么语言关系不大,更多要靠 Prompt 优化、工具描述优化和更多真实数据来慢慢收敛。
7.2 压箱底的踩坑清单
如果只保留一张经验卡片,下面这几点是我最想分享的:
- 千万不要把 LLM 调用放在业务线程池里裸跑,一定要单独隔离线程池并做信号量限流,否则一次模型卡顿就可能拖垮整个应用。
- 工具描述要写“什么时候用、什么时候别用”。只写功能说明的模型经常乱用工具,我后来在 description 里加反例,误用率明显下降。
- 多步任务一定要做幂等设计。工具可能因为超时重试被执行两次,查询类问题不大,写操作必须能识别重复执行。
- 模型输出 JSON 参数时经常带多余字段或错误格式,不要直接强类型转换,先做宽松解析,扔掉未知字段。
- Agent 的步数上限不是越大越好。设小一点,让模型更早给出阶段性结果,整体成功率反而更高。
7.3 下一步想做的事
最后说说还没做完的事。一个是多 Agent 协作,目前是单 Agent 循环,未来可以拆成“规划 Agent、执行 Agent、审查 Agent”的组间协作模型;另一个是工具调用并行化,当多个子任务之间没有依赖时,并行执行能大幅缩短整个任务耗时;还有评估体系,现在只做了轨迹重放,理想状态下应该沉淀一批标准任务集,每次改版自动跑一遍,用成功率、步数、Token 消耗三个指标做回归。这些方向都想清楚了,但真正落地还得等业务场景需要。
把 JavaManus 从零写一遍,对我自己最大的收获,是彻底弄清楚了 Agent 不是魔法,而是一套由循环、工具、规划、记忆、治理组成的工程系统。如果你也想在 Java 技术栈里落地 Agent,我建议不要急着引一大堆依赖,先按这篇文章的骨架把最小循环跑通,再逐步把工具、记忆、可观测性填进去。你会发现最难的部分从来不是代码,而是想清楚每一步为什么要这么做——想清楚了,代码只是水到渠成的事。