ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java从零实现AI Agent:ReAct循环与工具调用拆解

Java从零实现AI Agent:ReAct循环与工具调用拆解 当大模型从对话工具进化为能调用工具的智能体Agent框架的工程实现就成了关键。Python生态有LangChain、LlamaIndex但企业核心业务系统往往是Java的。把Python Agent嵌入Java系统意味着跨进程调用、序列化开销和运维复杂度翻倍。JavaManus的目标是用纯Java实现一个可扩展的ReAct Agent基于Spring AI 1.1.0 Spring Boot 3.4默认接入火山引擎Ark豆包整体不到30个Java文件。分层架构组合优于继承JavaManus的类层次是ManusControllerSSE接口→ ManusAgent装配工具→ ToolCallAgent工具编排→ ReActAgent → BaseAgent状态机/Memory/步数控制/卡死检测/事件分发。职责切分很清晰BaseAgent是纯状态机不关心LLM怎么调、工具怎么执行ReActAgent定义step() think() act()的骨架ToolCallAgent实现工具调用的think/act逻辑ManusAgent只负责装配具体工具。替换LLM或工具集不需要改Agent核心逻辑。ReAct循环状态机与模板方法BaseAgent.run()是循环入口把用户请求写入memory进入RUNNING状态在currentStep maxSteps且未FINISHED时反复调用step()每步后做卡死检测finally中重置状态、步数并清空memory防止记忆污染。step()由ReActAgent实现先think()若判定无需行动则直接返回最后一条消息文本否则执行act()。think()禁用Spring AI自动执行ToolCallAgent.think()把memory消息、system prompt和工具定义发给LLM解析文本与工具调用。关键配置是internalToolExecutionEnabled(false)。Spring AI默认会在ChatModel.call()内部自动执行工具并递归请求LLM但Agent框架需要手动控制执行时机——截断工具输出、触发事件、检测特殊工具——所以必须禁用自动执行自己编排循环。act()执行工具并截断输出act()遍历toolCalls解析参数、执行工具然后通过maxObserve截断超长结果。工具输出可能极大比如cat一个几万行文件全部喂回LLM会迅速耗尽context window。结果以ToolResponseMessage写回memory若命中特殊工具如terminate直接把状态置为FINISHED。Memory滑动窗口Memory本质是带上限的消息列表addMessage后检查是否超过maxMessages超过就丢弃最早的消息。这个滑动窗口策略很朴素但在长任务中能保证context不无限膨胀。更好的做法是摘要或向量检索做长期记忆但对大部分工具调用场景滑动窗口已经够用。工具系统统一抽象与路由BaseTool实现Spring AI的ToolCallback接口抽象execute(Map)返回纯文本call()负责JSON解析和异常兜底。ToolCollection用HashMap做工具路由未注册的工具抛ToolError。两个核心工具StrReplaceEditor支持view/create/strreplace/insert/undoedit五种命令是Agent修改代码的主要手段PythonExecute通过ProcessBuilder启动独立Python进程带超时保护让Agent能做数学计算和数据处理。Ark适配层ChatModel与消息映射Spring AI官方没有火山引擎starter需要自己实现ChatModel。ArkChatModel.call()把Spring AI的Prompt转成Ark的ChatCompletionRequest再把响应转回ChatResponse。最复杂的是tool call双向映射Spring AI的AssistantMessage.ToolCall与Ark的ChatToolCall之间转换其中arguments在两边都是JSON字符串可以直接透传。systemPrompt分两层ArkProperties.systemPrompt是全局默认提示词放在消息列表最前面Agent.systemPrompt是Agent专属角色设定通过SystemMessage传入。这样不同Agent可共享同一ChatModel但拥有不同角色。五个真实工程坑坑一PythonExecute超时完全失效。原代码先readAllBytes()再waitFor(timeout)但readAllBytes()会阻塞到进程关闭stdout即进程退出。死循环时它永远不返回waitFor根本没机会执行。修复是用CompletableFuture异步读取输出流先waitFor超时则destroyForcibly并取部分输出。这个坑单测很难发现因为测试代码都能快速结束。坑二文件编辑工具无沙箱。StrReplaceEditor只校验绝对路径不限制范围Agent可读写/etc/passwd等任意文件。修复是引入workspaceRoot所有路径normalize后必须startsWith(workspaceRoot)否则抛ToolError。坑三System.out.println泄露prompt。ArkChatModel.call()里直接打印完整prompt包含用户输入和工具参数。修复是改用Slf4j的log.debug日志级别可控。坑四LLM失败后死循环。think()中LLM抛异常时写入错误消息并返回false循环继续下一轮可能继续失败直到maxSteps才退出浪费API配额。修复是LLM失败时直接把state置为FINISHED并返回false。坑五卡死检测误判。原isStuck()遍历历史所有assistant消息统计相同文本数量导致两个问题工具调用后最后一条是ToolResponseMessage被跳过检测时机不对历史偶发重复被累计导致误判。修复是只统计连续重复的assistant消息从最后一条assistant向前比对遇到不同即break。SSE接口与虚拟线程Agent执行可能几十秒同步HTTP会超时。JavaManus用SSE推送thought/toolcall/toolresult/complete/error事件。几个要点Agent执行放在Thread.ofVirtual()中不阻塞Web容器线程ManusAgent有状态通过ObjectProvider.getObject()每次请求获取新实例prototype作用域SseEmitter超时由配置控制。与Python生态的工程取舍Python生态在LLM领域有压倒性优势但JavaManus展示了一条不同路径复用Spring AI的ToolCallback抽象、Spring Boot的依赖注入和配置体系让Agent直接嵌入现有Java服务。代价是生态工具较少、需要自己实现LLM适配层收益是无需跨进程调用、运维栈统一、类型安全。对于核心系统是Java的团队这个取舍通常是划算的。从JavaManus的实现看一个可用的ReAct Agent核心并不复杂状态机循环、工具注册表、滑动窗口记忆、事件分发。真正花时间的是边界处理——超时、沙箱、日志脱敏、失败终止、卡死检测。这些坑在单测中往往不暴露只有生产环境的死循环和异常输入才会触发。
返回列表