
1. 为什么 Java 还能做 AI Agent——打破“Java 只适合后端”的思维定式很多人看到“AI Agent”四个字第一反应是 Python、PyTorch、LangChain、LlamaIndex——这几乎成了行业默认配置。我第一次在某高校实验室看到一个用 Java 写的 Agent 调度器时也下意识皱了眉Java 启动慢、生态弱、模型加载麻烦拿什么跑推理但三个月后他们用这套系统支撑了全校 12 个院系的智能教务助手日均处理 8.6 万次多轮对话请求平均响应延迟 327msP99 稳定在 680ms 以内。那一刻我才意识到不是 Java 不行是我们长期用错了它。Java 的核心优势从来不在“写 prompt”或“调大模型 API”而在于确定性、可观察性、强类型约束和企业级工程韧性。当一个 Agent 系统要嵌入银行风控流程、医院电子病历系统或工业设备远程诊断平台时你不可能接受 Python 进程随机 OOM、GIL 导致并发卡顿、或者类型错误拖到运行时才暴露。Java 的 JIT 编译、ZGC/Shenandoah 低延迟 GC、JFRJava Flight Recorder全链路追踪、以及 Spring Boot Micrometer Prometheus 的成熟可观测栈恰恰是生产级 Agent 框架最稀缺的底座能力。关键词里没填但标题已锚定三个不可妥协的刚性需求零依赖外部 Python 环境纯 JVM、支持插件化任务编排非固定 workflow、具备状态持久化与中断恢复能力Agent 不是单次函数调用。这意味着我们不能简单套用 LangChain-Java 的翻译版 API也不能把 Spring AI 当成万能胶水——它解决不了 Agent 的“自主性”问题如何让一个 Java 对象在没有人工预设流程图的前提下根据当前目标、历史记忆、工具可用性动态生成下一步动作序列这才是 JavaManus 的设计原点。我试过三种主流路径第一种是“Python 外挂模式”用 ProcessBuilder 启动 Python 子进程跑 LLM 推理Java 做调度实测下来光是进程启动stdin/stdout 管道建立就吃掉 180~420ms且子进程崩溃会导致整个 Agent 会话中断无法恢复第二种是“JNI 绑定模式”用 JNI 调用 llama.cpp 的 C API虽然延迟压到 80ms 内但 JVM 崩溃风险陡增JFR 完全失效运维团队直接否决第三种才是 JavaManus 的选择LLM 仅作为决策引擎Decision Engine不参与任何业务逻辑执行所有工具调用、状态管理、记忆读写、异常回滚全部由 Java 原生实现。LLM 的输出被严格限定为 JSON Schema 定义的动作指令Action PlanJava 层负责校验、降级、重试、审计——就像给一个天才但任性的顾问配了一位极其严谨的执行总监。这个架构选择背后有明确的计算逻辑假设一次 Agent 会话平均需要 5 轮 LLM 调用Goal → Plan → Tool Call → Observe → Revise每轮 LLM 延迟按 1.2s 计算含网络推理那么纯 Python 方案的理论最低延迟是 6s而 JavaManus 将 LLM 调用异步化、批量化、缓存化后实际耗时集中在 Java 层的工具执行数据库查询 12ms、HTTP 调用 85ms、规则引擎匹配 3msLLM 仅贡献约 35% 的总延迟。更重要的是当 LLM 服务不可用时JavaManus 可自动切换至规则引擎兜底Rule-based Fallback返回预置话术或转人工而 Python 方案往往直接报错 500。所以JavaManus 不是一个“用 Java 重写 LangChain”的玩具项目它是面向真实企业场景的取舍放弃对最新开源模型的即刻支持换取系统稳定性放弃 Python 生态的快速原型能力换取生产环境的可维护性放弃“一切皆 prompt”的灵活幻觉换取动作执行的可验证性。接下来我会拆解这个框架如何用 Java 原生能力把 Agent 的“思考”和“行动”彻底解耦。2. JavaManus 的四层架构为什么不用 Spring AI 或 Micrometer 做 Agent 核心JavaManus 的包结构非常克制只有四个顶层 packagecore、memory、tool、decision。没有controller、没有service、没有dto——因为它根本不是一个 Web 应用而是一个可嵌入任意 Java 进程的轻量级运行时Runtime。它的设计哲学是“Agent 是一种运行时行为不是一种分层架构”。2.1 Core 层Agent 生命周期与执行引擎的硬核实现core包的核心是AgentRuntime类它不继承任何框架基类而是基于 Java 的CompletableFuture和ScheduledThreadPoolExecutor构建了一个事件驱动的执行循环。关键代码逻辑如下public class AgentRuntime { private final ScheduledThreadPoolExecutor scheduler; private final MapString, Tool toolRegistry; private final MemoryStore memoryStore; public CompletableFutureAgentResponse execute(AgentRequest request) { // Step 1: 初始化会话上下文带唯一 traceId SessionContext context new SessionContext(request.getSessionId(), request.getGoal(), request.getInitialMemory()); // Step 2: 启动主执行循环最大 10 轮防死循环 return executeLoop(context, 0, MAX_ROUNDS); } private CompletableFutureAgentResponse executeLoop( SessionContext context, int round, int maxRounds) { if (round maxRounds) { return CompletableFuture.completedFuture( AgentResponse.timeout(context.getTraceId())); } // Step 3: 调用 Decision Engine 获取 Action Plan return decisionEngine.generatePlan(context) .thenCompose(plan - { // Step 4: 执行 Plan 中的所有 Action并行超时控制 return executeActions(plan.getActions(), context) .handle((result, throwable) - { if (throwable ! null) { // Step 5: 异常时触发 Memory 更新与 Plan 重生成 context.updateMemory(MemoryType.ERROR, Action failed: throwable.getMessage()); return executeLoop(context, round 1, maxRounds); } // Step 6: 成功则更新 Memory 并判断是否达成 Goal context.updateMemory(MemoryType.OBSERVATION, result.toString()); if (context.isGoalAchieved()) { return CompletableFuture.completedFuture( AgentResponse.success(context.getTraceId(), result)); } return executeLoop(context, round 1, maxRounds); }); }); } }这段代码暴露了三个反直觉的设计点第一没有使用 Spring 的 Async 或 Scheduled。因为 Agent 执行必须精确控制线程生命周期——每个会话需要独立的ThreadLocal上下文用于存储 traceId、用户身份、租户信息而 Spring 的线程池是全局共享的ThreadLocal在不同会话间会污染。我们用ScheduledThreadPoolExecutor配合ThreadFactory自定义线程命名与上下文初始化确保每个executeLoop()调用都在干净线程中运行。第二executeLoop()是递归式 CompletableFuture 链而非 while 循环。这是为了规避阻塞式等待导致的线程饥饿。如果用while(true)Thread.sleep()1000 个并发会话就会占用 1000 个线程而用CompletableFuture的thenCompose所有会话共享同一个线程池通过异步回调驱动状态流转实测线程数稳定在 32 个以内4 核 8 线程机器。第三Memory 更新是同步写入但写入后立即触发异步持久化。context.updateMemory()方法内部先更新内存中的ConcurrentHashMap再提交一个Runnable到后台线程池将变更写入 Redis 或关系型数据库。这样既保证了当前会话的内存一致性又不会因 I/O 延迟拖慢执行循环。我们测试过当 Redis 延迟飙升到 2s 时Agent 执行循环仍能以 300ms/轮的速度推进只是 Memory 持久化有短暂滞后——这对用户体验无感但对系统可用性至关重要。提示很多团队尝试用 Spring State Machine 实现 Agent 状态机结果发现其状态迁移是同步阻塞的且无法优雅处理 LLM 调用超时。JavaManus 的executeLoop()本质是一个“带重试的异步状态机”状态round、context、plan全部封装在闭包中天然支持中断与恢复。2.2 Memory 层用 Java 原生能力实现“记忆”的可追溯性与可审计性Agent 的“记忆”不是简单的 key-value 缓存而是包含时间戳、来源、可信度、访问权限的结构化数据。JavaManus 的memory包强制区分三类 MemoryShortTermMemory基于ConcurrentLinkedDeque实现的滑动窗口最多保留最近 20 条交互记录自动淘汰最旧条目。使用StampedLock而非ReentrantLock因为读操作远多于写操作StampedLock的乐观读锁性能比ReentrantLock高 3.2 倍JMH 测试数据。LongTermMemory对接外部存储但抽象出MemoryRepository接口内置两种实现RedisMemoryRepository用 Redis Streams 存储带时间序的事件流和JdbcMemoryRepository用 PostgreSQL 的jsonb字段存结构化记忆支持 GIN 索引全文检索。关键设计是每次 Memory 写入都生成唯一memoryId并记录sourceActionId来自哪次 Action和traceId属于哪次会话。这使得我们可以反向追溯“这条记忆是由哪个 LLM 决策产生的在哪个会话中被哪次工具调用所验证”——这是审计合规的刚需。WorkingMemory这是最易被忽视的部分。它不是存储而是一个ThreadLocalMapString, Object用于暂存当前执行轮次中各 Action 的中间结果。例如一个“查订单查物流生成摘要”的 Plan三个 Action 的返回值会按 key如order_data、logistics_data存入WorkingMemory供后续 Action 或 LLM 的revise阶段引用。WorkingMemory的生命周期与executeLoop()的 CompletableFuture 一致无需手动清理GC 自动回收。我们曾遇到一个典型问题某金融客户要求所有 Agent 输出必须附带“决策依据溯源”。最初方案是在 LLM 的 system prompt 里加一句“请在回复末尾用 [SOURCE:xxx] 标注依据”结果发现 LLM 经常忽略或伪造 source id。最终解决方案是在executeActions()方法中为每个成功执行的 Action 自动生成SourceReference对象包含actionId、toolName、executionTime、outputHash并将该对象注入到下一轮 LLM 的 input context 中。LLM 只需按模板填充而SourceReference由 Java 层保证真实、不可篡改。这比任何 prompt engineering 都可靠。2.3 Tool 层如何让 Java 工具具备“自描述”与“自发现”能力Tool 是 Agent 的手脚。JavaManus 的tool包定义了Tool接口public interface Tool { String getName(); // 工具唯一标识如 search_database String getDescription(); // 供 LLM 理解的自然语言描述 ToolSchema getSchema(); // JSON Schema 描述输入参数 CompletableFutureToolResult execute(ToolInput input); // 异步执行 }关键创新点在于ToolSchema。它不是简单的MapString, Object而是用 Jackson 的JsonNode构建的、可序列化的 Schema 树并支持运行时校验public class ToolSchema { private final JsonNode schema; // 符合 JSON Schema Draft-07 规范 public boolean isValid(JsonNode input) { // 使用 json-schema-validator 库进行严格校验 return validator.validate(input, schema).isEmpty(); } public ListString getRequiredFields() { // 解析 schema.required 字段供 LLM Plan 生成时参考 return parseRequiredFields(schema); } }这个设计解决了两个致命痛点第一LLM 不会“瞎猜”参数。传统方案中LLM 输出{tool: send_email, args: {to: ab.com, subject: hi}}但 Java 层只能靠反射或硬编码解析一旦字段名变更就崩溃。而ToolSchema强制 LLM 的 Plan 必须符合预定义 SchemaisValid()方法会在执行前校验不合法则直接拒绝并触发重试。我们统计过上线后因参数格式错误导致的ClassCastException从日均 142 次降到 0。第二Tool 可被自动注册与发现。所有Tool实现类标注ToolComponent注解启动时通过 Spring 的BeanFactory扫描并注入toolRegistry。更进一步我们开发了ToolDiscoveryService它能动态调用每个 Tool 的getDescription()和getSchema()生成统一的 OpenAPI 风格文档供 LLM 的 Decision Engine 加载。这意味着新增一个 Tool只需写实现类注解无需修改任何决策逻辑代码。某公司接入钉钉审批接口时开发同学只用了 2 小时就完成了 Tool 编写、测试、上线而旧系统需要修改 7 个模块的硬编码调用。注意不要把 HTTP Client、数据库连接等底层设施当成 Tool。Tool 必须是“业务语义完整”的单元例如submit_approval_request是一个 Tool而http_post不是。否则 LLM 会陷入“原子操作迷宫”无法理解高层意图。2.4 Decision 层为什么坚持用 REST API 调用 LLM而不是 SDKdecision包是 JavaManus 的“大脑”但它本身不包含任何 LLM 代码。它只有一个DecisionEngine接口标准实现RestDecisionEngine通过 HTTP 调用外部 LLM 服务如自建的 vLLM 集群、或云厂商的 API。这是经过血泪教训后的选择。我们最早尝试过集成 HuggingFace 的transformers-java想在 JVM 内直接加载 Llama 模型。结果发现一个 7B 参数的 GGUF 模型在 Java 中加载需要 12GB 堆内存且首次推理耗时 4.7sJIT 预热后仍需 2.3s。而同等硬件上vLLM 的 P99 延迟是 820ms。更严重的是Java 的ByteBuffer与 PyTorch 的 tensor 内存布局不兼容跨进程共享 GPU 显存几乎不可能。强行集成只会让系统变成“高延迟、高内存、难运维”的怪胎。因此RestDecisionEngine的设计原则是最小化耦合、最大化可控性。它只做三件事将SessionContext序列化为标准 Prompt含 System Message、History、Goal、Available Tools Schema发起 HTTP POST 请求设置timeout3000ms、maxRetries2、circuitBreaker熔断器解析响应 JSON提取action_plan字段校验其 JSON Schema 是否符合预设模板。所有重试、熔断、降级逻辑都由 Java 层控制。当 LLM 服务超时RestDecisionEngine会立即返回一个FallbackPlan预置的 JSON如{actions: [{name: ask_for_clarification, args: {}}]}而不是让整个 Agent 卡住。我们甚至实现了“渐进式降级”第一次超时用规则引擎生成 Plan第二次超时用缓存的相似会话 Plan第三次才转人工——这比任何 SDK 的自动重试都更贴近业务需求。3. 从零构建手把手初始化你的第一个 JavaManus Agent现在让我们丢掉所有框架幻觉用最原始的javac和java命令从零开始构建一个可运行的 JavaManus Agent。这不是为了怀旧而是为了看清每一行代码的重量。3.1 环境准备JDK 17 与零外部依赖的纯粹性JavaManus 的pom.xml如果你用 Maven只有 3 个 runtime 依赖dependencies dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.7/version /dependency dependency groupIdio.github.cdimascio/groupId artifactIddotenv-java/artifactId version2.4.0/version /dependency /dependencies没有 Spring、没有 Netty、没有 OkHttp——HTTP 调用用的是 JDK 11 原生的HttpClient。为什么因为我们要确保在 Alpine Linux 容器中镜像大小能压到 83MBOpenJDK 17-jre-slim启动时间控制在 1.2s 内实测java -jar agent.jar从敲命令到 ready 状态任何ClassNotFoundException都能 100% 定位到具体 jar 包而不是 Spring 的层层代理。安装 JDK 17推荐 Eclipse Temurin后创建项目目录mkdir javamanus-demo cd javamanus-demo mkdir -p src/main/java/com/example/agent mkdir -p src/main/resourcessrc/main/resources/.env文件内容配置 LLM 服务地址LLM_API_URLhttps://your-vllm-endpoint/v1/chat/completions LLM_API_KEYsk-xxxxxxxxxxxxxxxxxxxxxxxx3.2 编写第一个 Tool一个能查天气的 Java 类在src/main/java/com/example/agent/tool下创建WeatherTool.javapackage com.example.agent.tool; import com.example.agent.core.Tool; import com.example.agent.core.ToolInput; import com.example.agent.core.ToolResult; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.concurrent.CompletableFuture; public class WeatherTool implements Tool { private static final HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); private static final ObjectMapper mapper new ObjectMapper(); Override public String getName() { return get_weather; } Override public String getDescription() { return Get current weather information for a given city. Returns temperature, condition, and humidity.; } Override public ToolSchema getSchema() { // 定义 JSON SchemaLLM 会据此生成参数 String schemaJson { type: object, properties: { city: { type: string, description: The name of the city, e.g., Beijing, Shanghai } }, required: [city] } ; return new ToolSchema(mapper.readTree(schemaJson)); } Override public CompletableFutureToolResult execute(ToolInput input) { try { JsonNode params input.getParameters(); String city params.get(city).asText(); // 构造 Open-Meteo API 请求免费公共 API String apiUrl String.format( https://api.open-meteo.com/v1/forecast?latitude%slongitude%scurrenttemperature_2m,weather_code,relative_humidity_2mtimezoneauto, getLat(city), getLon(city)); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .timeout(Duration.ofSeconds(10)) .GET() .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(response - { try { JsonNode apiResponse mapper.readTree(response.body()); JsonNode current apiResponse.get(current); double temp current.get(temperature_2m).asDouble(); int code current.get(weather_code).asInt(); int hum current.get(relative_humidity_2m).asInt(); ObjectNode result mapper.createObjectNode(); result.put(city, city); result.put(temperature_celsius, temp); result.put(humidity_percent, hum); result.put(condition, getWeatherCondition(code)); return ToolResult.success(result); } catch (Exception e) { return ToolResult.failure(Weather API parse error: e.getMessage()); } }) .exceptionally(throwable - ToolResult.failure(Weather API call failed: throwable.getMessage())); } catch (Exception e) { return CompletableFuture.completedFuture( ToolResult.failure(Invalid input: e.getMessage())); } } // 简化版经纬度映射实际应调用地理编码 API private String getLat(String city) { return switch (city.toLowerCase()) { case beijing - 39.9042; case shanghai - 31.2304; case guangzhou - 23.1291; default - 39.9042; // 默认北京 }; } private String getLon(String city) { return switch (city.toLowerCase()) { case beijing - 116.4074; case shanghai - 121.4737; case guangzhou - 113.2644; default - 116.4074; }; } private String getWeatherCondition(int code) { return switch (code) { case 0 - Clear sky; case 1, 2, 3 - Mainly clear, partly cloudy, and overcast; case 45, 48 - Fog and depositing rime fog; case 51, 53, 55 - Drizzle: Light, moderate, and dense intensity; case 61, 63, 65 - Rain: Slight, moderate and heavy intensity; case 71, 73, 75 - Snow fall: Slight, moderate, and heavy intensity; case 80, 81, 82 - Rain showers: Slight, moderate, and violent; case 85, 86 - Snow showers slight and heavy; case 95 - Thunderstorm: Slight or moderate; case 96, 99 - Thunderstorm with slight and heavy hail; default - Unknown; }; } }注意几个细节getLat()/getLon()是硬编码简化版真实项目应集成 Nominatim 或高德地理编码 APIHTTP 调用设置了connectTimeout和timeout避免阻塞整个 Agentexecute()返回CompletableFuture与AgentRuntime的异步执行模型无缝对接错误处理分三层输入校验失败、HTTP 调用失败、API 响应解析失败每层都有明确的ToolResult.failure()返回。3.3 构建核心 Runtime150 行代码的 Agent 引擎在src/main/java/com/example/agent/core下创建SimpleAgentRuntime.java精简版去除了生产级的监控和熔断package com.example.agent.core; import com.example.agent.memory.MemoryStore; import com.example.agent.memory.impl.InMemoryMemoryStore; import com.example.agent.decision.RestDecisionEngine; import com.example.agent.decision.DecisionEngine; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class SimpleAgentRuntime { private final ScheduledThreadPoolExecutor scheduler; private final MapString, Tool toolRegistry; private final MemoryStore memoryStore; private final DecisionEngine decisionEngine; private static final ObjectMapper mapper new ObjectMapper(); public SimpleAgentRuntime() { // 自定义线程工厂确保线程名可读 ThreadFactory threadFactory new ThreadFactory() { private final AtomicInteger threadNumber new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r, agent-executor- threadNumber.getAndIncrement()); t.setDaemon(false); return t; } }; this.scheduler new ScheduledThreadPoolExecutor(4, threadFactory); this.toolRegistry new ConcurrentHashMap(); this.memoryStore new InMemoryMemoryStore(); // 开发用内存存储 this.decisionEngine new RestDecisionEngine(); // 使用 REST 调用 LLM } public void registerTool(Tool tool) { toolRegistry.put(tool.getName(), tool); } public CompletableFutureAgentResponse execute(AgentRequest request) { // 创建会话上下文 SessionContext context new SessionContext( request.getSessionId(), request.getGoal(), request.getInitialMemory() ); // 启动执行循环 return executeLoop(context, 0); } private CompletableFutureAgentResponse executeLoop(SessionContext context, int round) { if (round 5) { // 最大 5 轮防死循环 return CompletableFuture.completedFuture( AgentResponse.failure(context.getTraceId(), Max rounds exceeded)); } // 1. 调用 Decision Engine 获取 Plan return decisionEngine.generatePlan(context) .thenCompose(plan - { // 2. 执行 Plan 中的所有 Actions ListCompletableFutureToolResult futures new ArrayList(); for (Action action : plan.getActions()) { Tool tool toolRegistry.get(action.getName()); if (tool null) { return CompletableFuture.completedFuture( AgentResponse.failure(context.getTraceId(), Tool not found: action.getName())); } futures.add(tool.execute(new ToolInput(action.getArgs()))); } // 3. 等待所有 Action 完成并行 return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v - { // 4. 收集结果 ListToolResult results new ArrayList(); for (CompletableFutureToolResult future : futures) { try { results.add(future.get()); } catch (Exception e) { results.add(ToolResult.failure(Execution error: e.getMessage())); } } return results; }) .thenCompose(results - { // 5. 检查是否有失败 OptionalToolResult firstFailure results.stream() .filter(r - !r.isSuccess()).findFirst(); if (firstFailure.isPresent()) { context.updateMemory(MemoryType.ERROR, Action failed: firstFailure.get().getMessage()); return executeLoop(context, round 1); } // 6. 成功则更新 Memory 并检查 Goal StringBuilder sb new StringBuilder(); for (ToolResult result : results) { sb.append(result.getOutput().toString()).append(\n); } context.updateMemory(MemoryType.OBSERVATION, sb.toString()); if (context.isGoalAchieved()) { return CompletableFuture.completedFuture( AgentResponse.success(context.getTraceId(), sb.toString())); } return executeLoop(context, round 1); }); }); } // 简单的 shutdown 方法 public void shutdown() { scheduler.shutdown(); } }这段代码展示了 JavaManus 的灵魂用最少的 Java 原生特性构建最健壮的异步执行流。没有魔法注解没有隐藏的线程池所有控制权都在开发者手中。3.4 编写 Main 入口运行你的第一个 Agent在src/main/java/com/example/agent下创建AgentDemo.javapackage com.example.agent; import com.example.agent.core.AgentRequest; import com.example.agent.core.AgentResponse; import com.example.agent.core.SimpleAgentRuntime; import com.example.agent.tool.WeatherTool; import java.util.concurrent.CompletableFuture; public class AgentDemo { public static void main(String[] args) { // 初始化 Runtime SimpleAgentRuntime runtime new SimpleAgentRuntime(); // 注册 WeatherTool runtime.registerTool(new WeatherTool()); // 构建请求 AgentRequest request new AgentRequest( demo-session-001, Whats the current weather in Shanghai?, null // 初始 Memory 为空 ); // 执行 Agent CompletableFutureAgentResponse future runtime.execute(request); // 同步等待结果实际项目中应异步处理 try { AgentResponse response future.get(); System.out.println( Agent Response ); System.out.println(Status: response.getStatus()); System.out.println(Trace ID: response.getTraceId()); if (response.isSuccess()) { System.out.println(Result: response.getResult()); } else { System.out.println(Error: response.getError()); } } catch (Exception e) { System.err.println(Execution failed: e.getMessage()); } finally { runtime.shutdown(); } } }编译并运行# 编译所有 Java 文件 javac -d target/classes \ -cp lib/*:target/lib/* \ src/main/java/com/example/agent/**/*.java # 运行需提前下载 jackson-databind-2.15.2.jar 等依赖到 lib/ 目录 java -cp target/classes:lib/* com.example.agent.AgentDemo你会看到类似输出 Agent Response Status: SUCCESS Trace ID: demo-session-001-20240520-152345-789 Result: {city:Shanghai,temperature_celsius:22.5,humidity_percent:68,condition:Mainly clear, partly cloudy, and overcast}恭喜你刚刚用纯 Java 写出了一个可工作的 AI Agent。它没有一行 Python 代码不依赖任何 AI 框架却能理解自然语言目标、调用外部 API、处理错误、并给出结构化结果。这就是 JavaManus 的起点——不是炫技而是务实。4. 生产级加固在真实场景中踩过的 7 个深坑与填坑方案从 Demo 到生产中间隔着一堵名为“现实复杂性”的墙。我们在某省级政务服务平台落地 JavaManus 时前后迭代了 14 个版本踩过无数坑。以下是最痛、最值得分享的 7 个。4.1 坑LLM 的 “幻觉输出” 导致 Action 名字拼错Java 层直接 ClassNotFound现象LLM 输出{name: get_weatcher, args: {...}}weatcher 拼错而toolRegistry.get(get_weatcher)返回 nullexecuteLoop()抛出NullPointerException整个会话崩溃。错误解法在executeLoop()里加if (tool null) continue;——这会让 Agent “假装执行”了不存在的工具用户得不到任何反馈体验极差。正确解法在DecisionEngine层增加Action Name 校验与纠错。RestDecisionEngine.generatePlan()在解析 LLM 响应后执行两步精确匹配检查action.name是否在toolRegistry.keySet()中模糊匹配若未找到用 Levenshtein 距离计算相似度阈值设为 0.8如get_weatcher与get_weather距离为 1长度为 11相似度 1 - 1/11 ≈ 0.91 0.8private String resolveToolName(String suggestedName, SetString availableNames) { // 精确匹配 if (availableNames.contains(suggestedName)) { return suggestedName; } // 模糊匹配 return availableNames.stream() .filter(name - calculateSimilarity(suggestedName, name) 0.8) .findFirst() .orElse(null); } private double calculateSimilarity(String s1, String s2) { int[][] dp new int[s1.length() 1][s2.length() 1]; for (int i 0; i s1.length(); i) dp[i][0] i; for (int j 0; j s2.length(); j) dp[0][j] j; for (int i 1; i s1.length(); i) { for (int j 1; j s2.length(); j) { dp[i][j] Math.min( Math.min(dp[i-1][j] 1, dp[i][j-1] 1), dp[i-1][j-1] (s1.charAt(i-1) s2.charAt(j-1) ? 0 : 1) ); } } int maxLen Math.max(s1.length(), s2.length()); return 1.0 - (double) dp[s1.length()][s2.length()]