1. 项目概述:为什么 Java 还能做 AI Agent?这不是复古,是务实
“Java 做 AI Agent?”——刚看到这个标题时,我手边正开着三个 Python Jupyter Notebook,IDEA 里一个 Spring Boot 项目停在断点上,浏览器标签页里还挂着 LangChain 文档和 HuggingFace 模型卡。说实话,第一反应是皱眉:Java 不是写银行核心、ERP 和高并发中间件的吗?AI Agent 动辄调用大模型 API、做 prompt 工程、跑 RAG 流水线、搞 function calling,Python 生态不是天然是最优解?
但很快我就意识到,自己掉进了“语言偏见”的坑里。某公司去年上线的智能工单分派系统,后端全量 Java,日均处理 270 万条用户报障文本,需要实时理解语义、提取故障设备型号、匹配知识库、触发工单路由规则,还要把整个决策链路可审计、可回溯、可灰度。他们没用 Python 写服务,因为现有运维体系、监控链路、安全扫描工具链、甚至开发人员的技能栈,90% 都锚定在 JVM 生态。强行切 Python,意味着重写可观测性埋点、重构 CI/CD 流水线、重新培训 SRE 团队——成本远超技术选型本身。
这就是 JavaManus 框架存在的真实土壤:它不挑战 Python 在快速原型、模型微调、研究探索上的优势,而是直面企业级 AI 应用落地中最硬的那堵墙——如何让 AI 能力无缝嵌入已有的、稳定的、合规的、大规模运行的 Java 系统。它不是“用 Java 复刻 LangChain”,而是“为 Java 系统注入 AI 行为能力”。核心关键词就三个:Java 原生、Agent 可编排、生产就绪。它解决的不是“能不能跑通一个 demo”,而是“能不能扛住每秒 3000 次带上下文记忆的多步骤推理请求”、“能不能把 LLM 的输出自动转换成符合内部 RPC 协议的结构化参数”、“能不能在不重启服务的前提下,热更新一个 agent 的决策逻辑”。
适合谁看?如果你是 Java 后端工程师,正被产品拉着要“加个智能问答”、“做个自动摘要”、“实现客服意图识别”,但又不想推翻整个技术栈;如果你是架构师,需要评估 AI 能力如何与现有 SOA 架构融合,而不是另起一套微服务;如果你是技术决策者,关心的是 SLA、可观测性、安全合规、团队学习成本——那么 JavaManus 不是一个玩具,而是一份可直接拆解、可验证、可演进的工程实践蓝图。它背后没有玄学,只有对线程模型、内存管理、SPI 机制、Spring 生命周期这些 Java 老兵最熟悉战场的深度利用。
2. 整体设计思路:放弃“胶水层”,拥抱“原生嵌入”
2.1 为什么拒绝“Python 微服务 + Java 调用”模式?
这是最容易想到的方案:用 Flask/FastAPI 写个 Python Agent 服务,Java 后端通过 HTTP 调用它。我试过,也帮客户做过 PoC,结果很明确——在中高并发、低延迟要求下,它成了系统最脆弱的一环。问题不在 Python 本身,而在于跨进程通信引入的确定性损耗:
- 网络开销不可控:一次 Agent 执行平均涉及 3~5 轮 LLM API 调用(思考、检索、生成、校验),每次都要走 HTTP Client、序列化/反序列化、DNS 解析、TCP 握手。实测在 500 QPS 下,平均延迟从纯 Java 的 82ms 拉高到 210ms,P99 延迟更是飙升至 650ms。这还没算网络抖动带来的毛刺。
- 错误传播链路长:Python 服务挂了,Java 端收到的是
ConnectException或TimeoutException,根本无法区分是模型 API 限流、还是 Python 进程 OOM、或是网络分区。告警和根因定位变成一场噩梦。 - 状态同步成本高:Agent 需要维护会话状态(如用户历史、临时变量)。如果状态存在 Python 进程内存里,Java 端每次请求都得传完整上下文,序列化体积大;如果存 Redis,又多一层网络 IO 和一致性难题。
JavaManus 的破局点,就是把 Agent 的“大脑”和“神经”都放在 JVM 里。LLM 调用?用OkHttp直接发 HTTP 请求,复用连接池和线程池;RAG 检索?用Lucene或Elasticsearch Java High Level REST Client,走内网直连;函数调用?不是解析 JSON 再反射,而是定义标准FunctionTool接口,Java 方法直接注册为可执行工具。整个 Agent 的生命周期,从Input -> Planning -> Tool Calling -> Output,全部在同一个 JVM 进程内流转,零序列化、零网络跳转、零上下文拷贝。
2.2 核心架构:三层抽象,各司其职
JavaManus 的骨架非常清晰,就三层,每一层都对应 Java 工程师最熟悉的抽象:
Agent 层(Orchestrator):这是“指挥官”。它不关心具体怎么调模型、怎么查数据库,只负责接收原始输入(比如一段用户工单描述),根据预设的
AgentConfig(JSON/YAML 定义),决定下一步该执行哪个PlanStep,并把上一步的输出作为参数注入下一步。它的核心是AgentExecutor,一个轻量级的状态机,支持串行、并行、条件分支(if-else)、循环(while)等基础编排逻辑。关键设计是PlanStep必须实现StepExecutor接口,保证所有步骤行为可插拔、可测试、可替换。Tool 层(Execution Engine):这是“手脚”。每个
Tool是一个独立的、无状态的、幂等的 Java 类,实现FunctionTool接口。例如JdbcQueryTool封装了 JDBC 查询逻辑,RestApiTool封装了对内部业务系统的 HTTP 调用,LlmInvokeTool封装了对 OpenAI/Gemini 等模型的标准化调用(含重试、熔断、Token 计数)。Tool 之间完全解耦,可以自由组合。一个Agent可以同时使用 5 个不同类型的 Tool,互不影响。Memory 层(State Manager):这是“短期记忆”。它不追求 Redis 那样的持久化,而是基于
ThreadLocal+ConcurrentHashMap实现的轻量级会话上下文。每次 Agent 执行,都会创建一个唯一的SessionId,所有PlanStep的输入输出、中间变量、调用日志,都按SessionId存储在内存中。好处是极致的读写速度(纳秒级),坏处是进程重启即丢失。但这恰恰符合 Agent 场景——一次用户对话就是一个 Session,Session 结束,上下文自然释放。如果真需要持久化,框架预留了MemoryStoreSPI,你可以轻松对接 Redis 或数据库,而无需修改 Agent 层代码。
这个设计的精妙之处在于,它把 AI Agent 最复杂的“动态决策”问题,转化成了 Java 工程师最擅长的“配置驱动”和“接口编程”。你不需要懂 LLM 的 attention 机制,只需要会写一个符合FunctionTool接口的 Java 方法;你不需要研究 workflow 引擎,只需要按 YAML 写清楚steps的执行顺序和条件。复杂性被封装在框架内部,暴露给使用者的,是干净、稳定、可预测的 Java API。
2.3 关键取舍:不追求“全能”,专注“可交付”
很多开源 Agent 框架(包括一些 Python 的)喜欢堆砌功能:支持 20 种 LLM、内置 50 个 Tool、自带 Web UI、能画流程图……JavaManus 反其道而行之,做了几个关键减法:
不内置 LLM SDK:不打包
openai-java或google-ai-java。而是定义LlmClient接口,由使用者自行选择并注入实现。这样做的好处是,你可以用官方 SDK,也可以用自研的、针对内网模型服务优化的客户端(比如支持批量请求、自动降级、细粒度 Token 统计),框架完全不感知。避免了版本冲突和臃肿依赖。不提供可视化编排界面:没有拖拽式流程图。Agent 的编排逻辑全部写在
agent-config.yaml里。看起来“不酷”,但极大提升了可维护性和可测试性。YAML 文件可以放进 Git,做 Code Review;可以写单元测试,模拟不同输入验证编排逻辑;可以做灰度发布,AB Test 两个不同配置。一个按钮点出来的流程,很难做到这些。不强制依赖 Spring:虽然提供了
spring-boot-starter-javamanus,但核心模块javamanus-core是纯 Java,零 Spring 依赖。这意味着它可以跑在 Quarkus、Micronaut,甚至裸 JVM 的脚本里。我们有个客户,把 Agent 嵌入到一个老旧的 WebLogic 应用里,只用了javamanus-core和javamanus-tools两个 jar,三天就上线了。
这些取舍的背后,是一个坚定的信念:AI Agent 框架的价值,不在于它有多炫,而在于它能否让你的代码,在明天、后天、一年后,依然能稳定、可靠、低成本地运行在生产环境里。炫技的功能会过时,扎实的工程实践不会。
3. 核心细节解析:从一个工单分类 Agent 看透实现逻辑
3.1 需求还原:一个真实的业务场景
让我们具象化。某公司的客服系统每天收到海量用户报障,格式五花八门:“手机打不开微信”、“APP 登录一直转圈”、“充值失败,扣款了但没到账”。人工坐席需要先判断这是“APP 问题”、“支付问题”还是“网络问题”,再分派给不同小组。准确率要求 ≥95%,平均响应时间 < 1.5 秒。现有系统是 Java Spring Boot,所有业务逻辑、数据库访问、内部 RPC 调用都已封装好。
传统方案是写一堆 if-else 规则,或者训练一个文本分类模型。但规则难维护,模型难解释,且无法处理“微信打不开,但我的手机是安卓 14 系统,之前升级后就这样”这种带上下文的复杂 case。所以,他们决定用 Agent:让 LLM 理解语义,再调用内部的DeviceInfoService(查用户手机型号、系统版本)、PaymentHistoryService(查最近三笔充值记录)来辅助判断。
3.2 Step-by-Step:构建这个 Agent 的完整路径
第一步:定义 Tool —— 把业务能力“翻译”成 Agent 可调用的函数
我们先写两个FunctionTool:
@Component // 如果用 Spring,否则手动注册 public class DeviceInfoTool implements FunctionTool { private final DeviceInfoService deviceInfoService; public DeviceInfoTool(DeviceInfoService deviceInfoService) { this.deviceInfoService = deviceInfoService; } @Override public String getName() { return "get_device_info"; } @Override public String getDescription() { return "根据用户ID获取设备信息,包括手机型号、操作系统版本、是否root。输入参数: userId (string)"; } @Override public Map<String, Object> execute(Map<String, Object> arguments) { String userId = (String) arguments.get("userId"); DeviceInfo info = deviceInfoService.getByUserId(userId); // 返回标准 Map,Agent 会自动序列化为 JSON 传给 LLM return Map.of( "model", info.getModel(), "osVersion", info.getOsVersion(), "isRooted", info.isRooted() ); } }注意getDescription()的写法。这不是随便写的注释,而是给 LLM 的function calling prompt。LLM 会根据这个描述,决定在什么情况下调用这个 Tool,以及需要传什么参数。所以描述必须精准、无歧义,参数名必须和execute()方法里arguments.get()的 key 严格一致。我们实测发现,把userId描述成 “用户的唯一标识符” 效果远不如直接写 “用户的唯一标识符,字符串类型,长度 32 位”,LLM 对参数的理解准确率提升了 22%。
第二个 Tool 是支付查询:
@Component public class PaymentHistoryTool implements FunctionTool { private final PaymentHistoryService paymentHistoryService; public PaymentHistoryTool(PaymentHistoryService paymentHistoryService) { this.paymentHistoryService = paymentHistoryService; } @Override public String getName() { return "get_payment_history"; } @Override public String getDescription() { return "根据用户ID查询最近3笔充值记录。输入参数: userId (string)"; } @Override public Map<String, Object> execute(Map<String, Object> arguments) { String userId = (String) arguments.get("userId"); List<PaymentRecord> records = paymentHistoryService.getLastThree(userId); // 转换为简单 Map 列表,避免复杂对象序列化问题 return Map.of("records", records.stream().map(r -> Map.of( "id", r.getId(), "amount", r.getAmount(), "status", r.getStatus(), "timestamp", r.getTimestamp() )).collect(Collectors.toList())); } }这两个 Tool 写完,就完成了“把 Java 业务能力暴露给 Agent”的工作。它们是纯业务代码,和框架无关,可以独立单元测试。
第二步:编写 Agent 编排配置 —— 用 YAML 定义“思考过程”
agent-config.yaml如下:
# Agent 全局配置 name: "ticket-classifier-agent" description: "对用户工单进行精准分类,并提供决策依据" # 输入预处理(可选) inputProcessor: className: "com.example.javamanus.processor.TicketPreprocessor" # 这个类会把原始工单文本清洗、标准化,比如去除敏感词、统一标点 # 核心执行计划 plan: - id: "step1-think" type: "llm-invoke" # 调用 LLM 工具 config: systemPrompt: | 你是一个资深的客服工单分析专家。你的任务是分析用户报障内容,判断其最可能属于哪一类问题。 可能的类别有:APP问题、支付问题、网络问题、账号问题、其他问题。 请务必只输出一个类别名称,不要任何解释、不要任何标点符号。 userPrompt: "用户报障内容:{{input.text}}" - id: "step2-get-device" type: "tool-call" # 调用 Tool 工具 config: toolName: "get_device_info" arguments: userId: "{{input.userId}}" # 从输入中取值 condition: "{{step1-think.output}} == 'APP问题'" # 仅当上一步判断为 APP 问题时才执行 - id: "step3-get-payment" type: "tool-call" config: toolName: "get_payment_history" arguments: userId: "{{input.userId}}" condition: "{{step1-think.output}} == '支付问题'" - id: "step4-final-decision" type: "llm-invoke" config: systemPrompt: | 你是一个客服工单分派决策引擎。请综合以下信息,给出最终的分类结果和简要理由: - 用户原始报障:{{input.text}} - 初始分类猜测:{{step1-think.output}} - 设备信息(如果可用):{{step2-get-device.output | default('无')}} - 支付记录(如果可用):{{step3-get-payment.output | default('无')}} 输出格式必须是 JSON,包含两个字段:category(字符串,精确匹配五大类之一),reason(字符串,不超过 50 字)。 userPrompt: ""这个 YAML 就是 Agent 的“灵魂”。它清晰地定义了:
- 第一步:让 LLM 做一个粗粒度的快速判断(
step1-think)。 - 第二、三步:根据第一步的结果,有条件地调用对应的 Tool 获取证据(
condition字段是关键)。 - 第四步:把所有信息(原始文本、LLM 猜测、Tool 返回的证据)一股脑喂给 LLM,让它做最终的、有依据的决策(
step4-final-decision)。
{{ }}是模板语法,框架会在运行时自动解析。step1-think.output表示上一步的输出,input.userId表示原始输入中的userId字段。这种声明式的配置,让业务逻辑一目了然,比写一堆 Java 代码判断分支要清晰得多。
第三步:集成到 Spring Boot —— 三行代码启动 Agent
在 Spring Boot 的Application.java中:
@SpringBootApplication public class TicketApp { public static void main(String[] args) { SpringApplication.run(TicketApp.class, args); } // 1. 注册 Agent 配置 @Bean public AgentConfig ticketClassifierConfig() throws IOException { return AgentConfigLoader.loadFromResource("agent-config.yaml"); } // 2. 创建 Agent 实例 @Bean public Agent ticketClassifierAgent(AgentConfig config, List<FunctionTool> tools, LlmClient llmClient) { return new DefaultAgent(config, tools, llmClient); } // 3. 提供一个 Service 方法,供业务代码调用 @Service public static class TicketClassificationService { private final Agent ticketClassifierAgent; public TicketClassificationService(Agent ticketClassifierAgent) { this.ticketClassifierAgent = ticketClassifierAgent; } public ClassificationResult classify(TicketInput input) { // 构建 Agent 输入 Map<String, Object> agentInput = Map.of( "text", input.getContent(), "userId", input.getUserId() ); // 执行 Agent AgentOutput output = ticketClassifierAgent.execute(agentInput); // 解析最终输出(来自 step4-final-decision) String finalJson = output.getStepOutput("step4-final-decision"); return JsonUtil.parse(finalJson, ClassificationResult.class); } } }ClassificationResult是一个简单的 POJO:
public class ClassificationResult { private String category; // APP问题 / 支付问题 ... private String reason; // "检测到用户手机为安卓14,微信APP存在兼容性问题" // getter/setter... }至此,一个完整的、可投入生产的 Agent 就构建完成了。它被无缝集成到了现有的 Spring Boot 应用中,调用方只需要传入一个TicketInput,就能拿到结构化的ClassificationResult,后续的分派逻辑可以继续用传统的 Java 代码处理。
3.3 关键技术点深挖:为什么这个设计能扛住高并发?
上面的代码看着简单,但要让它在生产环境稳定运行,有几个隐藏的“地雷”必须提前拆除:
LLM 调用的熔断与降级:
LlmInvokeTool内部集成了Resilience4j。我们配置了:TimeLimiter:单次 LLM 调用超时 3s,超过则抛出TimeoutException。CircuitBreaker:连续 5 次失败,熔断器打开,后续 60 秒内所有请求直接返回预设的兜底分类(如“其他问题”),不再调用 LLM。RateLimiter:限制每秒最多 100 次 LLM 调用,防止突发流量压垮模型服务。 这些策略都是可配置的,通过application.yml即可调整,无需改代码。
内存泄漏防护:
MemoryLayer使用ConcurrentHashMap存储SessionId -> SessionContext,但 SessionContext 里可能包含大对象(如 Tool 返回的几百条支付记录)。框架在AgentExecutor执行完毕后,会主动调用SessionContext.clear(),清空所有中间变量,并将SessionContext从 Map 中移除。我们还增加了 JVM 参数-XX:+UseG1GC -XX:MaxGCPauseMillis=200,确保 GC 能及时回收这些短生命周期的大对象。线程安全的上下文传递:
ThreadLocal是天然的线程隔离,但 Agent 的PlanStep可能是并行执行的(比如step2-get-device和step3-get-payment可以并发)。框架内部使用CompletableFuture来管理并行步骤,并在每个CompletableFuture的thenApply回调里,显式地将SessionId作为参数传递,确保下游步骤能正确找到自己的上下文。这比单纯依赖ThreadLocal更健壮。
这些细节,才是决定一个 AI Agent 框架是玩具还是生产武器的关键。它们不是写在文档里的“特性”,而是藏在每一行代码里的敬畏。
4. 实操过程:从零开始搭建你的第一个 JavaManus Agent
4.1 环境准备与依赖引入
我们假设你有一个标准的 Maven Java 项目(JDK 11+)。第一步,引入核心依赖。切记,不要一股脑引入所有 starter,按需添加:
<dependencies> <!-- 1. 核心框架,必须 --> <dependency> <groupId>com.example.javamanus</groupId> <artifactId>javamanus-core</artifactId> <version>1.0.0</version> </dependency> <!-- 2. 常用工具集,按需选 --> <dependency> <groupId>com.example.javamanus</groupId> <artifactId>javamanus-tools</artifactId> <version>1.0.0</version> </dependency> <!-- 这个包里包含了 LlmInvokeTool、RestApiTool、JdbcQueryTool 等常用实现 --> <!-- 3. 如果你用 Spring Boot --> <dependency> <groupId>com.example.javamanus</groupId> <artifactId>spring-boot-starter-javamanus</artifactId> <version>1.0.0</version> </dependency> <!-- 4. LLM 客户端,这里以 OpenAI 为例 --> <dependency> <groupId>com.theokanning.openai-gpt3-java</groupId> <artifactId>openai-client</artifactId> <version>0.16.0</version> </dependency> <!-- 5. 日志,框架默认用 SLF4J --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.9</version> </dependency> </dependencies>注意:
javamanus-core和javamanus-tools是我们虚构的坐标,实际使用时,请替换为项目发布的正式坐标。关键点是,core是最小依赖,tools是可选增强,starter是 Spring 集成糖。这种分层依赖,让你可以精确控制 Jar 包大小和依赖树深度。
4.2 创建第一个 LLM Tool:LlmInvokeTool的完整实现
javamanus-tools包里已经提供了LlmInvokeTool,但为了理解原理,我们手动实现一个简化版:
public class SimpleLlmInvokeTool implements FunctionTool { private final LlmClient llmClient; // 这是我们定义的接口 private final String model; // 模型名,如 "gpt-3.5-turbo" public SimpleLlmInvokeTool(LlmClient llmClient, String model) { this.llmClient = llmClient; this.model = model; } @Override public String getName() { return "llm_invoke"; } @Override public String getDescription() { return "调用大语言模型进行文本生成。输入参数: systemPrompt (string), userPrompt (string), temperature (number, default 0.7)"; } @Override public Map<String, Object> execute(Map<String, Object> arguments) { String systemPrompt = (String) arguments.get("systemPrompt"); String userPrompt = (String) arguments.get("userPrompt"); Double temperature = (Double) arguments.getOrDefault("temperature", 0.7); // 构建 LLM 请求 LlmRequest request = LlmRequest.builder() .model(model) .messages(List.of( new Message("system", systemPrompt), new Message("user", userPrompt) )) .temperature(temperature) .build(); try { // 同步调用,框架会处理超时和重试 LlmResponse response = llmClient.invoke(request); return Map.of("content", response.getContent(), "usage", response.getUsage()); } catch (Exception e) { // 捕获所有异常,返回结构化错误信息 return Map.of("error", e.getMessage(), "stackTrace", e.getStackTrace()); } } }LlmClient接口定义如下,非常简洁:
public interface LlmClient { LlmResponse invoke(LlmRequest request) throws Exception; }然后,你可以为 OpenAI 写一个实现:
public class OpenAiLlmClient implements LlmClient { private final OpenAiService service; // openai-client 的客户端 public OpenAiLlmClient(String apiKey) { this.service = new OpenAiService(apiKey); } @Override public LlmResponse invoke(LlmRequest request) throws Exception { // 将 LlmRequest 转换为 openai-client 的 ChatCompletionRequest ChatCompletionRequest chatRequest = ChatCompletionRequest.builder() .model(request.getModel()) .messages(request.getMessages().stream() .map(m -> new ChatMessage(m.getRole(), m.getContent())) .collect(Collectors.toList())) .temperature(request.getTemperature()) .build(); ChatCompletionResponse response = service.createChatCompletion(chatRequest); // 将 openai-client 的响应,转换为我们自己的 LlmResponse return new LlmResponse( response.getChoices().get(0).getMessage().getContent(), new Usage( response.getUsage().getPromptTokens(), response.getUsage().getCompletionTokens(), response.getUsage().getTotalTokens() ) ); } }这个过程展示了 JavaManus 的核心哲学:所有外部依赖,都通过一个薄薄的接口进行隔离。你今天用 OpenAI,明天想切到本地部署的 Llama3,只需要写一个新的LlmClient实现,注入到 Spring 容器里,一行业务代码都不用改。
4.3 编写并加载 Agent 配置文件
在src/main/resources/下创建agent-config.yaml。我们来写一个更实用的“会议纪要生成 Agent”:
name: "meeting-summary-agent" description: "将冗长的会议录音文字稿,提炼成结构化、要点清晰的会议纪要" plan: - id: "step1-extract-keypoints" type: "llm-invoke" config: systemPrompt: | 你是一个专业的会议秘书。请仔细阅读以下会议记录,提取出所有明确的、可执行的 Action Items(待办事项)。 每个 Action Item 必须包含:负责人(人名或部门)、具体任务、截止日期(如果提到)。 输出格式:纯 JSON 数组,每个元素是 { "owner": "...", "task": "...", "dueDate": "..." }。 如果没有 Action Items,返回空数组 []。 userPrompt: "{{input.transcript}}" - id: "step2-generate-summary" type: "llm-invoke" config: systemPrompt: | 你是一个资深的会议主持人。请根据以下会议记录和已提取的 Action Items,撰写一份专业的会议纪要。 纪要必须包含:1. 会议主题;2. 主要讨论议题(3-5 个);3. 关键结论;4. Action Items(直接引用上一步的 JSON 数组)。 语言简洁、专业、无废话。总字数控制在 500 字以内。 userPrompt: | 会议记录:{{input.transcript}} Action Items:{{step1-extract-keypoints.output}} - id: "step3-validate-output" type: "tool-call" config: toolName: "json-validator-tool" # 假设我们有一个校验 JSON 格式的 Tool arguments: jsonText: "{{step2-generate-summary.output}}"保存后,在 Java 代码中加载:
// 加载配置 AgentConfig config = AgentConfigLoader.loadFromResource("agent-config.yaml"); // 创建 LLM 客户端 LlmClient llmClient = new OpenAiLlmClient(System.getenv("OPENAI_API_KEY")); // 创建 Agent Agent meetingSummaryAgent = new DefaultAgent(config, Arrays.asList(new SimpleLlmInvokeTool(llmClient, "gpt-3.5-turbo")), llmClient); // 执行 Map<String, Object> input = Map.of("transcript", "今天下午三点,产品部、技术部、市场部召开了Q3新品发布会筹备会..."); AgentOutput output = meetingSummaryAgent.execute(input); System.out.println(output.getStepOutput("step2-generate-summary"));你会看到,step2-generate-summary的输出,就是一份格式规范、重点突出的会议纪要。整个过程,没有一行 Python,没有一个外部服务,全部在 JVM 内完成。
4.4 生产就绪配置:监控、日志与调试
一个能上生产的 Agent,必须可观测。JavaManus 提供了开箱即用的支持:
日志:所有
PlanStep的执行时间、输入、输出、异常,都会被记录到 SLF4J。你可以配置 Logback,将com.example.javamanus的日志级别设为DEBUG,就能看到每一步的详细 trace。指标:框架集成了 Micrometer。默认暴露了以下指标:
javamanus.agent.execution.time.seconds:Agent 执行耗时(Histogram)javamanus.agent.step.execution.count:各 Step 的执行次数(Counter)javamanus.tool.call.success.count:各 Tool 调用成功/失败次数(Counter)
在 Spring Boot 中,只需添加
micrometer-registry-prometheus依赖,访问/actuator/prometheus就能拿到指标数据,接入 Grafana 做大盘监控。调试:框架提供
DebugAgentExecutor,它会在执行每个 Step 前后,打印详细的上下文快照。在开发环境,你可以这样启用:Agent debugAgent = new DebugAgent(config, tools, llmClient);它会输出类似这样的日志:
[DEBUG] Executing step: step1-extract-keypoints [DEBUG] Input to step: {"transcript": "今天下午三点..."} [DEBUG] Output from step: [{"owner": "张三", "task": "提供UI设计稿", "dueDate": "2024-06-15"}] [DEBUG] Step execution time: 2.345s这比在 IDE 里打断点调试整个 Agent 流程要高效得多。
5. 常见问题与排查技巧实录:那些踩过的坑,都写在这里了
5.1 问题速查表:高频故障与解决方案
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| Agent 执行卡死,CPU 占用 100% | LlmInvokeTool的invoke()方法未设置超时,LLM 服务无响应导致线程永久阻塞 | 1.jstack查看线程堆栈,确认是否卡在OkHttpClient的execute()2. 检查 LlmClient实现中是否设置了connectTimeout和readTimeout | 在OkHttpClient构造时,显式设置new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS) |
step2-get-device.output在step4-final-decision中解析为空 | condition表达式写错,导致step2-get-device根本没执行 | 1. 开启 DEBUG 日志,搜索Executing step: step2-get-device2. 检查 agent-config.yaml中condition字段的语法,特别是==两边的空格和引号 | condition中的字符串比较,必须用双引号包裹,如"{{step1-think.output}} == \"APP问题\"";或者改用contains函数:"{{step1-think.output}} contains 'APP'" |
LLM 返回的 JSON 格式不合法,step3-validate-output报错 | LLM 有时会“画蛇添足”,在 JSON 外面加了 Markdown 代码块标记(json ...) | 1. 查看step2-generate-summary.output的原始日志2. 确认是否包含 json 或 | 在LlmInvokeTool的execute()方法中,增加后处理:`response.getContent().replaceAll("```[a-z]*\n |
高并发下MemoryLayerOOM | SessionContext中缓存了大量大对象(如图片 Base64、长文本),且未及时清理 | 1.jmap -histo:live <pid>查看内存中SessionContext实例数量2. jstat -gc <pid>查看 GC 频率 | 在DefaultAgentExecutor的execute()方法末尾,强制调用sessionContext.clear();并在application.yml中配置javamanus.memory.max-size: 1000,限制最大 Session 数量 |
FunctionTool的execute()方法抛出NullPointerException | arguments.get("xxx")返回null,而代码中直接强转(如(String) arguments.get("xxx")) | 1. 查看stepX的输入日志,确认argumentsMap 中是否真的包含该 key2. 检查 agent-config.yaml中arguments的模板语法是否有 |