1. 为什么我要用 Java 从零写一个 AI Agent 框架
先说结论:市面上 Python 的 Agent 框架已经多到挑花眼,但真正能在 Java 技术栈里落地、能跟现有 Spring Boot 业务系统无缝对接的,少得可怜。我在一个以 Java 为主的后端团队里待了很多年,业务系统全是 Spring Boot + MyBatis-Plus 那一套,数据库是 MySQL,缓存是 Redis,权限用 Spring Security。当老板说“我们要接大模型,做个能自动干活的 Agent”时,我第一反应不是去装 Python 环境,而是想:能不能用 Java 把这件事干了?
JavaManus 就是在这个背景下诞生的。它不是要做一个比 LangChain 更牛的通用框架,而是解决一个非常具体的问题:让一个熟悉 Java 的工程师,能在自己熟悉的技术栈里,快速搭出一个具备“思考-行动-观察”闭环能力的 AI Agent。核心关键词就三个:Java、AI Agent、ReAct。ReAct 是 Reasoning + Acting 的缩写,是目前主流 Agent 架构里最实用的一种范式,它让模型不是一次性输出答案,而是像人一样“想一步、做一步、看一步结果、再想下一步”。
这篇文章适合谁看?如果你是一个 Java 后端工程师,想搞清楚 Agent 到底怎么运转的;如果你正在评估要不要引入 Agent 能力到现有系统;如果你已经看过一些 Python 的 Agent 教程,但不知道怎么在 Java 里复现——那这篇就是写给你的。我会把设计思路、核心代码结构、踩过的坑、参数怎么调,全部摊开讲。不堆概念,只讲能跑起来的东西。
2. 整体架构设计与技术选型思路
2.1 为什么是 ReAct 而不是 Plan-and-Execute
Agent 架构主流就那么几种:ReAct、Plan-and-Execute、Reflection、Multi-Agent 协作。我选 ReAct 作为 JavaManus 的核心范式,理由很实在。
Plan-and-Execute 是先让模型把整个任务拆成步骤列表,然后一步步执行。听起来很美好,但实际用下来问题很大:模型在第一步规划时,根本不知道后面会遇到什么。比如你让它“查一下上个月订单量下降的原因”,它规划的第一步可能是“查询订单表”,但查完发现数据量太大需要先聚合,这个信息在规划阶段是拿不到的。结果就是计划赶不上变化,要么频繁重新规划,要么硬着头皮执行一个已经过时的计划。
ReAct 的思路完全不同。它每一轮只做一件事:根据当前已有的观察结果,决定下一步该干什么。这个“当前观察结果”是动态累积的,模型每走一步都能看到前面所有步骤的真实反馈。这就好比你在陌生城市找路,Plan-and-Execute 是出发前查好完整路线,ReAct 是走一段问一次路。后者显然更适合信息不完全的场景。
Reflection 模式是在 ReAct 基础上加一层“自我批评”,让模型检查自己的输出。这个我留作了扩展点,但没放进核心循环,因为多一轮反思就多一轮 token 消耗和延迟,对于大多数业务场景,ReAct 的闭环已经够用了。
Multi-Agent 协作是另一个方向,多个 Agent 各司其职互相配合。这个架构很酷,但工程复杂度陡增,调试难度指数级上升。我的判断是:先把单 Agent 的 ReAct 做扎实,Multi-Agent 是后面的事。
2.2 技术栈选型:Spring AI 还是自己封装 HTTP
这是绕不开的一个决策点。Spring AI 是 Spring 官方推出的 AI 集成框架,提供了 ChatClient、EmbeddingClient 等抽象,看起来很美。但我最终选择了自己封装 HTTP 调用,原因有三。
第一,Spring AI 当时还处于快速迭代期,API 变动频繁,今天写的代码下周可能就编译不过。对于一个要长期维护的框架来说,这个风险太大。第二,Spring AI 的抽象层虽然统一了不同模型厂商的接口,但也屏蔽了很多模型特有的参数,比如某些模型支持的 tool_choice 强制调用、parallel tool calls 并行工具调用等,这些在 Agent 场景里很关键。第三,自己封装 HTTP 其实没多少代码,核心就是一个带重试和超时控制的 HTTP 客户端,用 Java 11 的 HttpClient 就够了,反而更可控。
当然,如果你的团队已经在用 Spring AI 且版本稳定,直接用它也没问题。JavaManus 的架构是分层的,模型调用层是可替换的,换成 Spring AI 的实现只需要改一个接口的实现类。
2.3 核心模块划分
JavaManus 的整体结构我分成了五层,从下往上依次是:
- 模型接入层:负责跟大模型 API 通信,处理请求构造、响应解析、流式输出、重试和限流。这一层对上暴露一个统一的
ChatModel接口。 - 工具层:定义 Agent 可以调用的工具。每个工具是一个 Java 类,通过注解声明名称、描述和参数 schema。这一层负责工具的注册、查找和调用。
- 记忆层:管理对话历史和中间观察结果。短期记忆就是当前任务的步骤列表,长期记忆可以接向量数据库做 RAG。
- Agent 核心层:ReAct 循环的实现。接收用户输入,驱动模型思考,解析工具调用,执行工具,把结果喂回模型,直到模型给出最终答案。
- 应用层:对外暴露的 API,可以是 REST 接口,也可以是命令行入口,或者嵌入到现有 Spring Boot 服务里作为一个 Service。
这个分层的好处是每一层都可以独立替换和测试。比如我想换一个模型厂商,只动模型接入层;想加一个新工具,只动工具层。
3. ReAct 循环的核心细节与实操要点
3.1 ReAct 的提示词到底怎么写
ReAct 的灵魂在提示词。提示词写不好,模型要么不调用工具,要么乱调用工具,要么陷入死循环。我前后改了十几版提示词,最终稳定下来的结构是这样的:
你是一个能够使用工具的智能助手。你可以通过调用工具来获取信息或执行操作。 你可以使用的工具如下: {tool_descriptions} 请严格按照以下格式进行思考和行动: 思考:你需要分析当前情况,决定下一步做什么。 行动:你要调用的工具名称,必须是上面列出的工具之一。 行动输入:调用工具所需的参数,必须是合法的 JSON 格式。 当你已经获得足够信息可以回答用户问题时,请使用以下格式: 思考:我已经获得足够信息来回答用户问题。 最终答案:你的回答内容。 重要规则: 1. 每次只能调用一个工具。 2. 行动输入必须是合法的 JSON,不要包含任何额外文字。 3. 如果工具调用失败,请分析失败原因并尝试其他方法。 4. 不要编造工具返回的结果。这个提示词有几个关键设计点。第一,明确规定了输出格式,让解析变得简单。第二,强调了“每次只能调用一个工具”,避免模型一次输出多个工具调用导致解析混乱。第三,加了“不要编造工具返回结果”这条,因为实测下来模型在工具返回空结果时,倾向于自己编一个合理的结果继续往下走,这个必须明确禁止。
提示:不同模型对格式的遵循能力差异很大。GPT-4 系列基本能严格遵守,一些开源模型可能需要更简化的格式,甚至用 few-shot 示例来引导。建议在提示词里放一到两个完整的示例对话。
3.2 输出解析:怎么从模型回复里提取行动
模型返回的是纯文本,我们需要从中解析出“思考”“行动”“行动输入”三个部分。最直接的方式是用正则表达式。我用的模式是这样的:
Pattern THOUGHT_PATTERN = Pattern.compile("思考[::](.*?)(?=行动[::]|最终答案[::]|$)", Pattern.DOTALL); Pattern ACTION_PATTERN = Pattern.compile("行动[::](.*?)(?=行动输入[::]|$)", Pattern.DOTALL); Pattern ACTION_INPUT_PATTERN = Pattern.compile("行动输入[::](.*?)(?=思考[::]|最终答案[::]|$)", Pattern.DOTALL); Pattern FINAL_ANSWER_PATTERN = Pattern.compile("最终答案[::](.*)", Pattern.DOTALL);这里有个坑:模型有时候会用英文冒号,有时候用中文冒号,所以正则里两种都要匹配。还有,模型可能在“行动输入”的 JSON 前后加一些解释性文字,比如“好的,我将调用查询工具,参数如下:{...}”。这种情况下,正则提取出来的内容不是纯 JSON,直接反序列化会失败。我的处理方式是:先尝试直接解析,失败后用括号匹配的方式从文本里提取第一个完整的 JSON 对象。
public static String extractJson(String text) { int start = text.indexOf('{'); if (start == -1) return null; int depth = 0; for (int i = start; i < text.length(); i++) { char c = text.charAt(i); if (c == '{') depth++; else if (c == '}') { depth--; if (depth == 0) { return text.substring(start, i + 1); } } } return null; }这个括号匹配法看起来简单,但实测比任何正则都可靠,因为它能正确处理嵌套的 JSON 对象。
3.3 工具调用的参数校验与容错
模型生成的参数 JSON 经常有问题。我遇到过的情况包括:参数名拼错、参数类型不对(该传数字传了字符串)、缺少必填参数、多传了不存在的参数。如果直接把这些参数传给工具方法,轻则报错,重则产生脏数据。
我的做法是在工具层加一道校验。每个工具方法用注解声明参数 schema,调用前先做校验:
@Tool(name = "queryOrder", description = "根据条件查询订单信息") public String queryOrder( @ToolParam(name = "startDate", description = "开始日期,格式 yyyy-MM-dd", required = true) String startDate, @ToolParam(name = "endDate", description = "结束日期,格式 yyyy-MM-dd", required = true) String endDate, @ToolParam(name = "status", description = "订单状态,可选值:PAID, SHIPPED, COMPLETED", required = false) String status ) { // 实际查询逻辑 }校验不通过时,不是直接抛异常终止,而是把错误信息作为观察结果返回给模型,让它自己修正。比如模型传了startDate: "2024/01/01",校验发现格式不对,返回“参数 startDate 格式错误,应为 yyyy-MM-dd,你传入的是 2024/01/01”。模型看到这个反馈,下一轮通常就能改对。这个“错误即反馈”的机制是 ReAct 容错能力的核心。
注意:参数校验的错误信息要写得具体,告诉模型错在哪里、应该是什么格式。笼统的“参数错误”模型很难修正。
3.4 循环终止条件与最大步数控制
ReAct 循环必须有终止条件,否则模型可能陷入无限循环。我设了三个终止条件:
第一,模型输出了“最终答案”,正常结束。第二,达到最大步数限制,强制结束并返回当前已收集的信息。第三,连续两轮调用了同一个工具且参数完全相同,判定为死循环,强制结束。
最大步数设多少合适?我的经验值是 10 到 15 步。太少了复杂任务做不完,太多了浪费 token 且增加出错概率。这个值可以做成配置项,根据任务复杂度动态调整。比如简单的信息查询设 5 步,复杂的数据分析任务设 20 步。
public class ReActAgent { private static final int DEFAULT_MAX_STEPS = 10; private static final int MAX_REPEAT_ACTION = 2; public AgentResult run(String userInput) { List<Step> steps = new ArrayList<>(); String currentInput = userInput; for (int i = 0; i < maxSteps; i++) { String response = chatModel.chat(buildPrompt(currentInput, steps)); ParsedOutput parsed = parseOutput(response); if (parsed.hasFinalAnswer()) { return AgentResult.success(parsed.getFinalAnswer(), steps); } if (isRepeatedAction(steps, parsed)) { return AgentResult.failure("检测到重复行动,已终止", steps); } String observation = toolExecutor.execute(parsed.getAction(), parsed.getActionInput()); steps.add(new Step(parsed.getThought(), parsed.getAction(), parsed.getActionInput(), observation)); } return AgentResult.failure("达到最大步数限制", steps); } }4. 工具系统的设计与实现细节
4.1 工具注册机制:注解加反射
工具系统的设计目标是:开发者写一个普通的 Java 方法,加上注解,就能被 Agent 调用。不需要继承任何基类,不需要实现任何接口。
@Component public class OrderTools { @Tool(name = "queryOrderCount", description = "查询指定时间范围内的订单数量") public int queryOrderCount( @ToolParam(name = "startDate", description = "开始日期 yyyy-MM-dd", required = true) String startDate, @ToolParam(name = "endDate", description = "结束日期 yyyy-MM-dd", required = true) String endDate ) { // 调用 MyBatis-Plus 查询 return orderMapper.selectCount(new LambdaQueryWrapper<Order>() .ge(Order::getCreateTime, startDate) .le(Order::getCreateTime, endDate)); } }框架启动时,扫描所有带@Tool注解的方法,把工具名称、描述、参数 schema 注册到一个 Map 里。调用时根据工具名称找到对应的方法,把 JSON 参数反序列化成方法参数,反射调用。
这里有个细节:工具描述非常重要。模型是根据描述来决定要不要调用这个工具的。描述写得太简单,模型不知道什么时候该用;写得太复杂,浪费 token。我的经验是:描述里要包含“这个工具做什么”和“什么时候用”两个信息。比如“查询指定时间范围内的订单数量”就比“订单查询”好得多。
4.2 工具返回结果的处理
工具返回的结果会作为观察结果拼接到提示词里。这里有几个处理原则。
第一,结果要精简。如果工具返回一个包含 1000 条记录的列表,直接塞进提示词会爆 token。我的做法是在工具层面就做聚合,返回统计值而不是原始数据。如果确实需要返回列表,限制返回条数,比如只返回前 10 条。
第二,结果要结构化。返回 JSON 格式比返回自然语言描述更好,因为模型对 JSON 的解析能力更强。比如返回{"count": 1523, "period": "2024-01"}比返回“2024年1月共有1523个订单”更好。
第三,敏感信息要脱敏。工具返回的数据里如果包含手机号、身份证号等敏感信息,在返回给模型之前要做脱敏处理。这个不是技术问题,是合规问题,必须做。
4.3 工具调用的超时与异常处理
工具执行可能超时或抛异常。我的处理策略是:每个工具调用设置独立的超时时间,默认 30 秒,可在注解里配置。超时后中断执行,把“工具执行超时”作为观察结果返回给模型。异常同理,捕获后把异常信息返回给模型。
public String execute(String toolName, String paramsJson) { ToolDefinition def = toolRegistry.get(toolName); if (def == null) { return "错误:未找到名为 " + toolName + " 的工具"; } try { Object[] args = def.parseParams(paramsJson); Future<Object> future = executor.submit(() -> def.invoke(args)); Object result = future.get(def.getTimeoutSeconds(), TimeUnit.SECONDS); return def.serializeResult(result); } catch (TimeoutException e) { future.cancel(true); return "错误:工具 " + toolName + " 执行超时"; } catch (Exception e) { return "错误:工具 " + toolName + " 执行失败," + e.getMessage(); } }提示:不要把异常的完整堆栈返回给模型,那会浪费大量 token 且模型也看不懂。只返回异常的核心信息即可。
5. 记忆管理与上下文控制
5.1 短期记忆:步骤列表的维护
ReAct 的短期记忆就是当前任务的步骤列表。每一步包含:思考、行动、行动输入、观察结果。这个列表会随着循环不断增长,最终拼接到提示词里。
但这里有个问题:步骤多了之后,提示词会变得很长,token 消耗大且可能超出模型上下文窗口。我的处理方式是:只保留最近 N 步的完整信息,更早的步骤只保留行动和观察结果的摘要。
private String buildHistoryPrompt(List<Step> steps) { StringBuilder sb = new StringBuilder(); int recentCount = Math.min(steps.size(), 5); int startIdx = steps.size() - recentCount; for (int i = 0; i < steps.size(); i++) { Step step = steps.get(i); if (i < startIdx) { // 早期步骤只保留摘要 sb.append("步骤").append(i + 1).append(":调用了").append(step.getAction()) .append(",结果:").append(truncate(step.getObservation(), 100)).append("\n"); } else { // 近期步骤保留完整信息 sb.append("思考:").append(step.getThought()).append("\n"); sb.append("行动:").append(step.getAction()).append("\n"); sb.append("行动输入:").append(step.getActionInput()).append("\n"); sb.append("观察:").append(step.getObservation()).append("\n\n"); } } return sb.toString(); }这个策略实测下来效果不错。模型主要依赖最近的几步来做决策,早期步骤的细节通常不重要,保留摘要足够让模型知道“我已经做过什么”。
5.2 长期记忆:什么时候需要 RAG
短期记忆解决的是单次任务内的上下文问题。长期记忆解决的是跨会话的知识积累问题。比如用户上周问过类似的问题,这周又问,Agent 能不能记住上次的结论?
这个场景就需要 RAG(检索增强生成)。把历史对话和结论向量化存入向量数据库,新问题来时先检索相关历史,作为上下文注入提示词。JavaManus 里我预留了MemoryStore接口,默认实现是内存版,生产环境可以换成基于 Redis 或向量数据库的实现。
但我要说句实话:大多数业务场景下,长期记忆的投入产出比并不高。除非你的 Agent 是面向同一个用户的持续服务,否则每次任务都是独立的,短期记忆就够了。不要为了技术而技术。
5.3 上下文窗口的 token 预算管理
Token 就是钱,也是延迟。一个 ReAct 循环跑 10 步,每步的提示词都包含历史步骤,token 消耗是累加的。如果不做控制,一个复杂任务跑下来可能消耗几万 token。
我的做法是给提示词设一个 token 预算上限,比如 4000 token。每次构建提示词时,先计算工具描述和系统提示词的固定开销,剩下的预算分配给历史步骤。如果历史步骤超出预算,就从最早的步骤开始压缩。
public class TokenBudget { private final int maxTokens; private final TokenCounter counter; public String fitToBudget(String systemPrompt, String toolDescriptions, List<Step> steps) { int fixedCost = counter.count(systemPrompt) + counter.count(toolDescriptions); int remaining = maxTokens - fixedCost - RESERVED_FOR_OUTPUT; // 从最近的步骤开始往前加,直到超出预算 List<Step> included = new ArrayList<>(); int used = 0; for (int i = steps.size() - 1; i >= 0; i--) { int stepCost = counter.count(steps.get(i).toPromptString()); if (used + stepCost > remaining) break; included.add(0, steps.get(i)); used += stepCost; } return buildPrompt(systemPrompt, toolDescriptions, included); } }Token 计数可以用简单的字符数除以 4 来估算,也可以用 tiktoken 的 Java 移植版精确计算。对于预算管理来说,估算就够了,没必要精确到个位数。
6. 常见问题与排查技巧实录
6.1 模型不调用工具怎么办
这是最常见的问题。模型直接给出了一个答案,没有走工具调用流程。原因通常有三个:提示词里没有明确要求必须使用工具、工具描述不够清晰、模型本身能力不足。
排查步骤:先看提示词里有没有“你必须使用工具来获取信息”这类强制要求。然后检查工具描述是否说清楚了工具的用途。如果都没问题,换一个能力更强的模型试试。实测下来,7B 级别的开源模型在工具调用上的表现很不稳定,13B 以上会好很多,GPT-4 级别的基本没问题。
6.2 工具调用参数格式错误
模型生成的 JSON 参数格式错误,比如用了单引号、缺少引号、多了逗号。这个问题在开源模型上尤其常见。
我的解决方案是在提示词里加一个 JSON 格式的示例,明确展示正确的参数格式。另外,在解析参数时做容错处理:先尝试标准 JSON 解析,失败后尝试用 Jackson 的宽松模式解析,再失败就尝试修复常见的格式问题(比如把单引号替换成双引号)。
private JsonNode parseParamsLenient(String json) { try { return objectMapper.readTree(json); } catch (Exception e1) { try { // 宽松模式 objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); return objectMapper.readTree(json); } catch (Exception e2) { // 尝试修复常见问题 String fixed = json.replace("'", "\"").replaceAll(",\\s*}", "}"); try { return objectMapper.readTree(fixed); } catch (Exception e3) { throw new ToolParamParseException("无法解析参数: " + json, e3); } } } }6.3 循环不终止
模型反复调用同一个工具,或者在不同工具之间来回切换但不给出最终答案。这个问题通常是因为提示词里没有明确的终止条件说明,或者模型陷入了“我再确认一下”的思维定式。
解决方案:在提示词里明确写“当你已经获得足够信息时,必须输出最终答案,不要继续调用工具”。同时设置最大步数硬限制作为兜底。另外,可以在提示词里加一句“如果你连续两次调用同一个工具且参数相同,说明你已经获得了所需信息,请直接给出最终答案”。
6.4 工具执行结果太长导致 token 爆炸
某个工具返回了大量数据,直接把提示词撑爆了。这个问题要在工具层面解决,不能指望模型自己处理。
我的做法是:每个工具在返回结果前做一次截断,超过一定长度(比如 2000 字符)就截断并加上“...(结果已截断)”的提示。如果模型需要完整数据,它应该调用更精确的查询工具,而不是一次性拉全量数据。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | 提示词未强制、工具描述不清 | 检查提示词和工具描述 | 加强制要求、优化描述、换更强模型 |
| 参数 JSON 格式错误 | 模型能力不足、缺少示例 | 查看模型原始输出 | 加 JSON 示例、宽松解析、格式修复 |
| 循环不终止 | 缺少终止条件说明 | 查看步骤列表 | 明确终止规则、设最大步数 |
| 结果太长 token 爆炸 | 工具返回数据量过大 | 查看工具返回内容 | 工具层截断、聚合、分页 |
| 工具调用超时 | 工具执行慢、外部依赖慢 | 查看工具执行日志 | 设超时、异步执行、优化查询 |
| 模型编造工具结果 | 提示词未禁止 | 对比工具实际返回 | 明确禁止编造、强调只使用真实结果 |
注意:以上问题的排查顺序建议从提示词开始,再到工具实现,最后才怀疑模型能力。大部分问题都出在前两者。
7. 性能优化与生产化考量
7.1 流式输出与用户体验
ReAct 循环的每一步都需要等模型完整输出才能解析,这意味着用户要等很久才能看到结果。如果任务跑了 10 步,每步模型响应 3 秒,用户要等 30 秒。这个体验是不可接受的。
我的优化方案是:把每一步的思考和行动实时推送给前端。用户能看到 Agent 正在“思考什么”“调用了什么工具”“得到了什么结果”,即使最终答案还没出来,用户也知道系统在工作。这个用 SSE(Server-Sent Events)实现,每完成一步就推送一个事件。
@GetMapping(value = "/agent/run", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter runAgent(@RequestParam String input) { SseEmitter emitter = new SseEmitter(300_000L); executor.submit(() -> { try { agent.run(input, step -> { emitter.send(SseEmitter.event() .name("step") .data(step.toJson())); }); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }7.2 并发控制与限流
Agent 调用大模型 API 是有成本的,而且很多 API 有 QPS 限制。如果不做限流,高并发场景下会大量请求失败。
我在模型接入层加了一个信号量做并发控制,默认最大并发 10。超过的请求排队等待,等待超时则返回“系统繁忙”。同时加了令牌桶限流,控制每秒的请求数。
public class RateLimitedChatModel implements ChatModel { private final Semaphore concurrencyLimiter = new Semaphore(10); private final RateLimiter rateLimiter = RateLimiter.create(5.0); // 每秒5个请求 @Override public String chat(String prompt) { rateLimiter.acquire(); try { concurrencyLimiter.acquire(); try { return delegate.chat(prompt); } finally { concurrencyLimiter.release(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AgentException("请求被中断", e); } } }7.3 可观测性:日志与指标
生产环境跑 Agent,没有可观测性就是盲人摸象。我记录了三类数据:每次模型调用的输入输出和耗时、每次工具调用的参数和结果、每个任务的完整步骤链路。
这些数据用结构化日志输出,方便后续分析。关键指标包括:任务成功率、平均步数、平均耗时、token 消耗量、工具调用失败率。这些指标能帮你判断系统是否健康,以及优化方向在哪里。
public class AgentMetrics { private final MeterRegistry registry; public void recordTask(AgentResult result) { registry.counter("agent.task.total", "status", result.isSuccess() ? "success" : "failure").increment(); registry.timer("agent.task.duration").record(result.getDuration()); registry.summary("agent.task.steps").record(result.getSteps().size()); registry.counter("agent.token.usage").increment(result.getTokenUsage()); } }7.4 成本控制:token 消耗的优化
Token 消耗是 Agent 运行成本的大头。优化方向有几个:压缩提示词、减少不必要的步骤、缓存重复的模型调用。
压缩提示词方面,工具描述要精简,历史步骤做摘要,系统提示词去掉冗余内容。减少步骤方面,提示词里引导模型尽快给出答案,不要做无谓的确认。缓存方面,相同输入和相同上下文的模型调用可以缓存结果,但要注意 Agent 场景下每次上下文都不同,缓存命中率通常不高。
我实测下来,一个中等复杂度的任务(5 步左右),用 GPT-4 级别的模型,token 消耗大约在 3000 到 5000 之间。如果用更便宜的模型,成本能降一个数量级,但成功率也会下降。这个平衡点需要根据业务场景来定。
8. 从 JavaManus 延伸出去的几个方向
JavaManus 的核心代码其实不多,ReAct 循环加上工具系统,核心逻辑大概一千行左右。但围绕它可以做的事情很多。
第一个方向是跟现有业务系统深度集成。比如把 MyBatis-Plus 的 Mapper 自动注册为工具,让 Agent 能直接查询数据库。这个用注解加反射很容易实现,但要注意权限控制,不能让 Agent 执行任意 SQL。
第二个方向是加 RAG 能力。把知识库向量化,Agent 在回答前先检索相关知识。这个在客服、文档问答场景下很有用。
第三个方向是多 Agent 协作。把复杂任务拆给多个专职 Agent,比如一个负责查数据,一个负责分析,一个负责写报告。这个架构复杂度高,但能处理更复杂的任务。
第四个方向是接入 MCP 协议。MCP 是模型上下文协议,可以让 Agent 调用外部工具服务。JavaManus 的工具层设计已经预留了扩展点,接入 MCP 只需要实现一个 MCP 工具适配器。
我在实际使用中最大的体会是:Agent 的能力上限取决于工具的质量,而不是模型的能力。给模型一堆垃圾工具,再强的模型也干不好活。反过来,工具设计得好,中等能力的模型也能跑出不错的效果。所以与其花时间调提示词,不如先把工具打磨好。