十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring AI实战:构建仿ClaudeCode的Java终端Agent

Spring AI实战:构建仿ClaudeCode的Java终端Agent 在实际的 Java 大模型应用开发中真正的分水岭不是能不能调用某个模型的 API而是能不能让模型在一个任务上下文里反复思考、调用工具、拿到结果后继续推进。ClaudeCode 这类终端编码代理之所以让开发者印象深刻是因为它把读代码、改文件、执行验证串成了一个多步骤 Agent 流程。来到 Java 技术栈Spring AI 2.0、Agent Utils 和 Spring AI Alibaba 的组合正在把这个能力带到 Spring 开发者熟悉的工程体系里。目标是从一个可运行的最小项目出发搭建一个仿 ClaudeCode 的 Java Agent先讲 Agent 的底层链路再配置模型供应商然后实现工具调用、多轮记忆和终端交互最后补充常见问题排查和工程化建议。读完可以跑通 demo也能理解每个环节为什么这样设计。1. 先拆解 Agent 项目ClaudeCode 在终端里做了什么1.1 ClaudeCode 的工作方式ClaudeCode 的交互形式可以简单概括为用户在终端输入自然语言任务代理负责理解意图、规划步骤、读取项目文件、编辑文件、执行命令、根据结果修正方案直到任务完成。它本质不是一个单次问答而是一个循环过程用户输入 - 模型决策输出纯文本或请求调用工具 - 如果请求调用工具执行工具 - 把工具结果回填给模型 - 再来一轮 - 如果输出最终文本返回给用户这个循环就是 Agent 与普通 Chat 应用的核心区别。普通 Chat 只有一轮“输入模型返回”Agent 则多了一个“观察工具结果后继续决策”的环节。模型负责判断下一步做什么工具负责产出真实世界的数据循环负责把两者拼接起来。1.2 Java Agent 的技术骨架模型、工具、记忆、循环实现一个 Agent最少需要四类组件配合。它们各自解决一个问题缺一个就会出现“模型瞎编”“答非所问”“上下文断裂”之类的现象。组件作用在 Spring AI 中的对应物模型LLM负责理解任务、生成文本、决定是否调用工具ChatModel、ChatClient工具Tool让模型具备读写文件、查数据库、调接口等外部能力Tool、ToolCallingManager记忆Memory保留多轮对话历史和任务执行现场ChatMemory、Advisor循环Loop反复“思考-调用-观察”直到任务完成AgenticChatClient 或手写循环为什么要拆成这四块模型负责“想”工具负责“做”记忆负责“记住”循环负责“推进”。很多入门项目失败不是因为模型选得不好而是没有把工具调用结果正确回填给模型或者没有控制循环次数导致模型反复调用同一个工具。1.3 Spring AI 2.0 与 Spring AI Alibaba 的分工Spring AI 2.0 是 Spring AI 主线的下一个大版本通常跟随 Spring Boot 4 的发布节奏推进统一模型接入、提示词管理、工具调用等 API。Spring AI Alibaba 是面向阿里巴巴 DashScope 通义模型体系的适配层也兼容 OpenAI 格式的第三方服务。两者不是竞争关系而是“主框架 供应商适配”的关系。用一个容易记的说法Spring AI 是整套积木Agent Utils 是把积木拼成 Agent 的说明书和胶水Spring AI Alibaba 是开箱即用的模型接线盒。由于这些模块还在快速迭代下面所有代码中的版本号和 API 形态都按当前主流写法给出落地前一定要对着官方当前稳定版本核对。2. 环境准备先把 JDK、Spring Boot 和 Spring AI 版本对齐2.1 本机环境要求Agent 项目本质上还是一个 Spring Boot 工程环境要求并不特殊。下面是推荐配置。环境项推荐配置说明JDK17 或 21Spring Boot 4 主线建议直接使用 21Maven3.9也可以使用 Gradle本文以 Maven 为例IDEIntelliJ IDEA / Eclipse需要支持 Lombok 和 Spring 注解处理模型 API KeyDashScope 或 DeepSeek 等至少准备一个可用的模型供应商Docker可选本地部署 Ollama 等模型时需要Windows 用户要注意两点路径分隔符是反斜杠Java 代码里建议统一用PathAPI 处理终端中文输入输出乱码时把运行参数加上-Dfile.encodingUTF-8。2.2 创建 Maven 项目并引入依赖先创建一个空的 Maven 工程。pom.xml的骨架如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId !-- Spring AI 2.0 通常对应 Spring Boot 4.x版本以官方里程碑为准 -- version4.0.0-M4/version relativePath/ /parent properties java.version21/java.version spring-ai.version2.0.0-M2/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement核心依赖需要三类Spring AI 主模块、Agent 工具模块、模型供应商适配模块。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-agent-utils/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter-dashscope/artifactId /dependency /dependencies这里要特别提醒spring-ai-agent-utils和spring-ai-alibaba-starter-dashscope的 artifactId 在不同版本里可能调整过命名建议不要直接照抄版本打开spring-ai-bom内容或者去官方示例仓库确认一遍。依赖写错是最常见的起步问题报错往往还不是编译失败而是“类找不到”或“Bean 找不到”。2.3 配置模型供应商在src/main/resources/application.yml中配置模型供应商。如果使用 DashScope 通义千问配置是这样的spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7如果使用 DeepSeek可以走 OpenAI 兼容接口把 base-url 指过去spring: ai: openai: base-url: https://api.deepseek.com/v1 api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7api-key建议通过环境变量注入不要直接写在 yml 里提交到 Git。temperature控制随机性工具调用类任务建议保持在 0.2 到 0.7 之间太高会导致模型选择工具不稳定。2.4 启动类与最小烟雾测试创建启动类SpringBootApplication public class AgentApplication { public static void main(String[] args) { SpringApplication.run(AgentApplication.class, args); } }接着写一个SpringBootTest验证链路是否通SpringBootTest class AgentSmokeTest { Autowired private ChatClient chatClient; Test void smoke() { String content chatClient.prompt(用一句话解释什么是 Agent) .call() .content(); System.out.println(content); } }如果测试能输出一段合理的文字说明依赖、Key、模型接口全部正常。这个检查点很重要它把“环境问题”和“后续代码问题”隔离开。不要跳过这一步直接写复杂功能否则后面报错时很难定位是哪一层的问题。3. 实现第一个最小 Agent让模型学会调用工具3.1 最小 Agent 循环是什么最小 Agent 循环可以不用任何框架封装核心逻辑就是发请求给模型检查返回结果里有没有工具调用请求如果有就执行工具、把结果拼回消息、再次请求模型直到模型返回最终文本。Spring AI 的ChatClient已经把大部分流程封装好了开发者只需要定义工具框架自动完成“模型请求工具结果回填”的往返。3.2 配置 ChatClient为了让模型在每次回复时都有稳定的行为边界一般会配置一个默认系统提示词Configuration public class ChatClientConfig { Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个 Java 工程师助手。回答要简洁、准确尽量给出可执行的方案。) .build(); } }Spring AI 启动时会自动装配ChatClient.Builder只需要注入即可。系统提示词相当于给 Agent 定了一个“岗位说明书”作用是约束语气、回答长度和任务边界。3.3 定义第一个工具先实现一个最常用的能力读取文件和列出目录。这里用 Spring AI 的Tool注解声明工具。import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Collectors; import java.util.stream.Stream; Component public class FileTools { Tool(description 读取指定路径文件的文本内容) public String readFile(ToolParam(description 文件绝对路径) String path) { try { return Files.readString(Path.of(path)); } catch (IOException e) { return 读取失败 e.getMessage(); } } Tool(description 列出指定目录下的文件和子目录名称) public String listDirectory(ToolParam(description 目录绝对路径) String path) { try (StreamPath paths Files.list(Path.of(path))) { return paths.map(p - p.getFileName().toString()) .sorted() .collect(Collectors.joining(\n)); } catch (IOException e) { return 列目录失败 e.getMessage(); } } }Tool注解告诉 Spring AI“这个方法是可被模型调用的工具”description会拼进提示词交给模型判断什么场景该调用哪个工具。ToolParam的 description 同样会被模型看到写得越清楚模型选参就越准。这里有一个关键约束工具方法的参数尽量使用 String、Integer、Boolean 这类简单类型。如果参数是复杂对象模型生成的 JSON 参数很容易解析失败导致工具调用链路中断。3.4 验证工具调用写一个测试让模型自然触发工具SpringBootTest class ToolCallingTest { Autowired private ChatClient chatClient; Test void listFiles() { String answer chatClient.prompt(列出 /demo-project 目录下的文件) .call() .content(); System.out.println(answer); } }验证标准不是“能不能输出”而是“输出的文件名是不是真实存在的”。如果模型没有走工具而是凭训练数据编造了一个目录结构那就是典型的幻觉说明工具调用没有触发。此时需要看日志中模型返回的finish_reason是否为tool_calls同时检查Tool类是否被 Spring 扫描为 Bean。4. 用 Spring AI Agent Utils 把单轮对话升级成可复用 Agent4.1 Agent Utils 提供了哪些组件Spring AI Agent Utils 的价值是封装 Agent 开发中反复出现的通用能力记忆管理、工具调用编排、多步骤循环。常用组件包括ChatMemory对话记忆存储接口内存实现是InMemoryChatMemory。MessageChatMemoryAdvisor每次请求前自动从ChatMemory取出历史消息拼入上下文。ToolCallingManager统一管理工具注册、参数解析、结果回填。AgenticChatClient把“模型-工具-记忆-循环”组合成开箱即用形态。这些组件不是必须全部使用但对于多轮 Agent记忆组件几乎是必须的。没有记忆模型每轮都像第一次见面无法完成“先读文件再改文件”这种需要上下文的连续任务。4.2 给 Agent 加上多轮记忆先定义ChatMemoryBean再把它挂到ChatClient上Configuration public class AgentConfig { Bean ChatMemory chatMemory() { return new InMemoryChatMemory(); } Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultSystem(你是一个 Java 工程师助手可以读取项目文件并辅助修改。) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } }MessageChatMemoryAdvisor的构造方式在不同版本有差异有的版本需要传入retrievalMode编写时要先看当前版本的 API。它的工作机制是每次请求前把对话 ID 对应的历史消息从ChatMemory中取出拼到最新消息之前再一起发给模型。要注意代价记忆越多每次请求消耗的 token 越多。学习阶段用InMemoryChatMemory足够生产环境要考虑把记忆持久化到 Redis或者对长历史做摘要压缩。4.3 用 AgenticChatClient 收敛循环逻辑如果工具较多、流程较长可以交给AgenticChatClient管理循环。下面是一个接近官方示例的写法Bean AgenticChatClient agenticChatClient(ChatClient.Builder builder, ChatMemory chatMemory, ToolCallingManager toolCallingManager) { return AgenticChatClient.builder(builder) .chatMemory(chatMemory) .toolCallingManager(toolCallingManager) .maxIterations(5) .build(); }maxIterations是循环上限用来防止模型反复调用工具导致死循环同时也是重要的成本控制手段。设置太小会导致复杂任务完不成设置太大会让单次任务时间和 token 消耗不可控。一般先从 3 到 5 开始观察任务复杂度再调整。4.4 什么时候该手写循环不是所有场景都适合AgenticChatClient。当需要精确控制每次工具调用后的错误处理、需要插入人工确认、或者需要记录完整工具调用链时手写循环更合适。简化的选择标准如下。场景推荐方式理由单工具、单轮返回ChatClient简单直接无额外抽象多工具、多步骤、需要记忆AgenticChatClient自动管理循环和记忆需要人工审批、精细日志、异常恢复手写循环每一步都在自己控制范围内5. 接入 Spring AI Alibaba用 DashScope 跑通完整链路5.1 Spring AI Alibaba 的价值Spring AI Alibaba 把 DashScope 通义模型接入 Spring AI API 体系。使用它的收益是不需要自己封装 HTTP 调用不需要手写 OpenAI 兼容协议配置好 Key 就能用ChatClient操作通义模型。它在国内网络环境下访问稳定模型列表也能通过官方文档确认不涉及猜测版本的问题。5.2 模型与工具调用兼容性在配置 DashScope 时注意模型选择会影响工具调用的稳定性。qwen-plus是通用场景的稳妥选择qwen-max适合复杂任务但成本和延迟更高。学习阶段优先使用qwen-plus把工具调用链路跑通后再根据效果调整。spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus5.3 多模型配置切换实际开发时经常需要对比不同模型的效果建议用 Spring Profile 隔离配置。创建application-dashscope.yml和application-deepseek.yml两个文件分别写对应的模型配置。启动时用参数切换mvn spring-boot:run -Dspring-boot.run.profilesdashscope或mvn spring-boot:run -Dspring-boot.run.profilesdeepseek这样在同一套 Agent 代码下可以快速验证不同模型对工具调用准确率的影响。工具注册、循环逻辑、记忆逻辑完全不需要改换模型只是换配置这正是 Spring AI 统一抽象的核心价值。5.4 本地部署模型的取舍有热词提到“本地部署大模型”很多开发者也想把 Agent 接到本地模型上。Spring AI 对 Ollama 有专门支持配置方式类似spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b但要有一个明确预期本地小模型在工具调用能力上通常弱于云端大模型尤其是“从文本中准确抽取参数并选择工具”这一步小模型容易出现参数残缺或直接放弃调用。建议先用云端模型把业务逻辑验证清楚再评估本地模型是否够用。本地模型的主要价值是数据不出内网和降低单次调用成本代价是效果和运维成本。6. 仿 ClaudeCode 实战做一个能看代码、能改文件的终端助手6.1 需求拆解与安全边界现在把前面所有能力组合成一个命令行小助手。这个助手需要满足用户用自然语言下达任务。助手能列出项目目录、读取项目文件。助手能预览将要写入的文件内容。多轮对话中能记住用户前面说过的话。强制安全边界不允许访问项目目录之外的路径不真正执行任意命令写文件前只做预览。为什么不实现“真正的写文件和命令执行”因为生产环境的 Agent 一旦具备任意写权限一个错误路径就能覆盖重要文件。学习项目里先用“预览”机制演示工具调用流程生产环境再按需要加入人工确认的写操作这是更安全的做法。6.2 带路径校验的工具集路径校验是工具安全的核心。使用resolve加normalize加startsWith三层判断防止../这类路径穿越。Component public class ProjectFileTools { private static final Path ROOT Path.of(System.getProperty(user.dir)).toAbsolutePath().normalize(); Tool(description 列出项目根目录下的一级文件和目录名称) public String listFiles() { try (StreamPath stream Files.list(ROOT)) { return stream.map(p - p.getFileName().toString()) .sorted() .collect(Collectors.joining(\n)); } catch (IOException e) { return 列出目录失败 e.getMessage(); } } Tool(description 读取项目根目录下相对路径的文件内容) public String readFile(ToolParam(description 相对路径例如 src/main/java/com/example/App.java) String relativePath) { Path target resolveSafe(relativePath); if (target null) { return 拒绝访问路径超出项目目录; } try { return Files.readString(target); } catch (IOException e) { return 读取失败 e.getMessage(); } } Tool(description 预览即将写入文件的内容不真正写入) public String previewWrite(ToolParam(description 相对路径) String relativePath, ToolParam(description 文件内容) String content) { Path target resolveSafe(relativePath); if (target null) { return 拒绝访问路径超出项目目录; } return 将写入文件 target \n content; } private Path resolveSafe(String relativePath) { Path target ROOT.resolve(relativePath).normalize(); if (!target.startsWith(ROOT)) { return null; } return target; } }startsWith(ROOT)是路径穿越防护的关键。任何通过../逃出项目目录的路径都会在normalize()之后暴露出真实位置从而被startsWith拦截。6.3 终端主循环使用CommandLineRunner实现命令行交互Component public class CliRunner implements CommandLineRunner { private final ChatClient chatClient; private final Scanner scanner new Scanner(System.in); public CliRunner(ChatClient chatClient) { this.chatClient chatClient; } Override public void run(String... args) { System.out.println(Java Agent CLI 已启动输入 exit 退出); while (true) { System.out.print( ); if (!scanner.hasNextLine()) { break; } String input scanner.nextLine(); if (exit.equalsIgnoreCase(input.trim())) { break; } String answer chatClient.prompt(input).call().content(); System.out.println(answer); } } }这段代码的重点在chatClient.prompt(input).call()。Spring AI 会在内部自动完成“模型决定调用工具执行工具回填结果再生成最终文本”的完整流程。对上层业务来说只看到一句调用即可。6.4 运行效果与验证启动应用后终端交互大致如下Java Agent CLI 已启动输入 exit 退出 列出项目下的文件 项目根目录下有一级条目src、pom.xml、README.md 读取 src/main/java/com/example/AgentApplication.java 正在读取该文件...验证时要关注三个点模型是否真的调用了工具而不是编造文件列表。路径越界时工具是否返回“拒绝访问”。多轮对话中第二个问题是否能记住第一个问题的信息。如果模型没有调用工具优先检查工具类是否注册成 Bean再看工具描述是否足够清晰。工具描述太模糊是模型不调用工具的常见原因。7. 常见问题排查从“不出结果”到“内存溢出”7.1 请求正常但 content 为空这个现象在 DeepSeek 兼容 OpenAI 接口时比较常见。模型可能返回了reasoning_content而content为null或者直接返回了tool_calls没有生成普通文本。检查方式打开日志看模型原始返回中的finish_reason。如果是tool_calls这说明模型在请求调用工具此时要继续执行工具并回填结果而不是直接取content。如果是流式响应还要检查流式事件里是否也包含content片段。处理建议先关闭流式输出跑一遍确认非流式下链路正常再排查工具调用结果是否正确回填。不要盲目觉得是模型 Key 或网络问题。7.2 工具调用不触发或不生效可能的典型原因有三个工具类没有注册成 Spring BeanComponent缺失。工具描述不清晰模型不知道什么场景该调用它。模型本身不支持 function calling或模型参数中关闭了工具调用。排查顺序先确认工具类被容器扫描再看日志中模型请求的 messages 里是否包含 tools 定义最后看模型响应中是否出现tool_calls。如果请求里根本没有 tools说明工具没有传给模型需要检查ChatClient的配置和版本 API。7.3 编译期 OOM 和 Lombok 不支持 JDK常见报错是java: OutOfMemoryError: insufficient memory或java: You arent using a compiler supported by lombok前者通常是 Maven 编译器 fork 进程内存不足或 IDE 编译器堆太小。处理方式是调大 Maven 内存export MAVEN_OPTS-Xmx2048m或者在pom.xml中给maven-compiler-plugin配置fork和compilerArgs。后者说明本机 JDK 版本比当前 Lombok 支持的版本新升级 Lombok 版本或者把项目 JDK 降到 Lombok 支持的版本范围内。7.4 中文乱码与 Windows 路径问题终端启动后中文输入输出乱码多半是控制台编码和 Java 文件编码不一致。启动参数加-Dfile.encodingUTF-8IDE 中把项目编码统一为 UTF-8。Windows 下工具返回路径时建议统一用PathAPI 处理不要手工拼接反斜杠。下面把常见问题和排查建议整理成速查表。问题现象常见原因检查方式处理建议请求成功但 content 为空模型返回了 tool_calls 或 reasoning_content查看原始响应和 finish_reason回填工具结果后再请求或处理流式事件工具调用不触发工具类没注册、描述不清、模型不支持检查请求中是否包含 tools确认 Component、优化工具描述编译期内存不足Maven 或 IDE 编译器堆太小查看编译日志调大 MAVEN_OPTS 或编译器堆Lombok 不工作JDK 版本超出 Lombok 支持范围查看编译期警告升级 Lombok 或对齐 JDK中文乱码文件编码与控制台编码不一致查看终端编码统一 UTF-88. 学习环境与生产环境的差异以及工程化建议8.1 学习环境怎么快速跑通学习阶段不要一上来就追求复杂架构。推荐顺序是只接入一个模型供应商跑通最基础的 Chat 对话。加一个最简单的工具比如读取文件验证工具调用链路。加多轮记忆验证上下文连续性。再加第二个工具验证多工具切换和参数解析。最后做成命令行交互验证完整闭环。每一步都要写一个SpringBootTest用日志确认按钮。工具调用类问题在测试环境下定位比在终端交互里定位快得多。8.2 生产环境还要补什么从学习 demo 到生产 Agent差距主要在工程保障不在模型效果。以下每一项都需要单独设计密钥管理模型 Key 通过环境变量或配置中心注入禁止提交到仓库。日志追踪记录每次请求的模型、token 消耗、工具调用输入输出方便排查幻觉和异常。工具权限所有写操作都要有白名单和人工确认机制。参考 ClaudeCode 的“确认”交互但生产环境不要盲目取消确认应该按操作风险分级处理。成本控制设置maxIterations、单次任务 token 上限、超时时间。写文件安全保障正式写文件前先备份原文件写失败要能回滚。记忆持久化InMemoryChatMemory只在单机单进程内有效生产环境改用 Redis并给对话设置过期时间。流式输出与取消长任务建议使用流式响应同时支持用户主动取消。关于“如何不用一直点确认”这个需求合理的做法不是完全
返回列表