
1. 项目概述为什么 Java 还能做 AI Agent这不是复古是务实“Java 做 AI Agent”——刚看到这个标题时我手边正开着三个 Python Jupyter NotebookIDEA 里一个 Spring Boot 项目停在断点上浏览器标签页里还挂着 LangChain 文档和 HuggingFace 模型卡。说实话第一反应是皱眉Java 不是写银行核心、ERP 和高并发中间件的吗AI Agent 动辄调用大模型 API、做 prompt 工程、跑 RAG 流水线、搞 function callingPython 生态不是天然是最优解但很快我就意识到自己掉进了“语言偏见”的坑里。某公司去年上线的智能工单分派系统后端全量 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 拉高到 210msP99 延迟更是飙升至 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这是“指挥官”。它不关心具体怎么调模型、怎么查数据库只负责接收原始输入比如一段用户工单描述根据预设的AgentConfigJSON/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 那样的持久化而是基于ThreadLocalConcurrentHashMap实现的轻量级会话上下文。每次 Agent 执行都会创建一个唯一的SessionId所有PlanStep的输入输出、中间变量、调用日志都按SessionId存储在内存中。好处是极致的读写速度纳秒级坏处是进程重启即丢失。但这恰恰符合 Agent 场景——一次用户对话就是一个 SessionSession 结束上下文自然释放。如果真需要持久化框架预留了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 可调用的函数我们先写两个FunctionToolComponent // 如果用 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 MapString, Object execute(MapString, Object arguments) { String userId (String) arguments.get(userId); DeviceInfo info deviceInfoService.getByUserId(userId); // 返回标准 MapAgent 会自动序列化为 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 MapString, Object execute(MapString, Object arguments) { String userId (String) arguments.get(userId); ListPaymentRecord 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, ListFunctionTool 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 输入 MapString, 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是一个简单的 POJOpublic 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:MaxGCPauseMillis200确保 GC 能及时回收这些短生命周期的大对象。线程安全的上下文传递ThreadLocal是天然的线程隔离但 Agent 的PlanStep可能是并行执行的比如step2-get-device和step3-get-payment可以并发。框架内部使用CompletableFuture来管理并行步骤并在每个CompletableFuture的thenApply回调里显式地将SessionId作为参数传递确保下游步骤能正确找到自己的上下文。这比单纯依赖ThreadLocal更健壮。这些细节才是决定一个 AI Agent 框架是玩具还是生产武器的关键。它们不是写在文档里的“特性”而是藏在每一行代码里的敬畏。4. 实操过程从零开始搭建你的第一个 JavaManus Agent4.1 环境准备与依赖引入我们假设你有一个标准的 Maven Java 项目JDK 11。第一步引入核心依赖。切记不要一股脑引入所有 starter按需添加dependencies !-- 1. 核心框架必须 -- dependency groupIdcom.example.javamanus/groupId artifactIdjavamanus-core/artifactId version1.0.0/version /dependency !-- 2. 常用工具集按需选 -- dependency groupIdcom.example.javamanus/groupId artifactIdjavamanus-tools/artifactId version1.0.0/version /dependency !-- 这个包里包含了 LlmInvokeTool、RestApiTool、JdbcQueryTool 等常用实现 -- !-- 3. 如果你用 Spring Boot -- dependency groupIdcom.example.javamanus/groupId artifactIdspring-boot-starter-javamanus/artifactId version1.0.0/version /dependency !-- 4. LLM 客户端这里以 OpenAI 为例 -- dependency groupIdcom.theokanning.openai-gpt3-java/groupId artifactIdopenai-client/artifactId version0.16.0/version /dependency !-- 5. 日志框架默认用 SLF4J -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version /dependency /dependencies注意javamanus-core和javamanus-tools是我们虚构的坐标实际使用时请替换为项目发布的正式坐标。关键点是core是最小依赖tools是可选增强starter是 Spring 集成糖。这种分层依赖让你可以精确控制 Jar 包大小和依赖树深度。4.2 创建第一个 LLM ToolLlmInvokeTool的完整实现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 MapString, Object execute(MapString, 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); // 执行 MapString, 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.secondsAgent 执行耗时Histogramjavamanus.agent.step.execution.count各 Step 的执行次数Counterjavamanus.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 APPLLM 返回的 JSON 格式不合法step3-validate-output报错LLM 有时会“画蛇添足”在 JSON 外面加了 Markdown 代码块标记json ...1. 查看step2-generate-summary.output的原始日志2. 确认是否包含json 或在LlmInvokeTool的execute()方法中增加后处理response.getContent().replaceAll([a-z]*\n高并发下MemoryLayerOOMSessionContext中缓存了大量大对象如图片 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()方法抛出NullPointerExceptionarguments.get(xxx)返回null而代码中直接强转如(String) arguments.get(xxx)1. 查看stepX的输入日志确认argumentsMap 中是否真的包含该 key2. 检查agent-config.yaml中arguments的模板语法是否有