仿Claude Code:Spring AI 2.0 Java Agent开发实战

仿Claude Code:Spring AI 2.0 Java Agent开发实战 最近在业务系统中做大模型能力落地时我明显感觉到一个转变单纯调用大模型返回一段文本已经不能满足需求团队更希望模型能像人一样“动手干活”比如读取文件、修改配置、执行命令、查数据库最后把结果反馈给用户。这种形态正是当前大模型应用最热的方向——Agent。而在 Java 生态里能把大模型、工具调用、Agent 编排串起来形成完整闭环的方案Spring AI 2.0 是我认为目前最值得投入的一条路线。网上关于 AI Agent 的教程大部分集中在 Python 生态Java 侧的完整实战案例偏少。本文会围绕 Spring AI 2.0、Agent Utils、Spring AI Alibaba 三个核心组件从零搭建一个仿 Claude Code 的 Java AI 编程助手。这个项目可以读取项目文件、修改文件、执行命令也能在终端里和用户进行多轮交互帮助你理解 Java 技术栈下 Agent 的完整开发流程。文章会提供可复制的依赖配置、完整代码、模型接入方式和常见问题排查表适合有 Java Spring Boot 基础、想进军 AI Agent 开发的读者也适合想在企业内部系统里接入大模型能力的后端开发者。1. 背景与核心概念1.1 为什么 Java 开发者需要关注 AI Agent先理解一个很朴素的逻辑大模型本身是一个“大脑”但只有大脑是不能干活的。它需要眼睛观察环境需要手去操作文件需要脚去执行命令。当模型能够调用外部工具并根据工具返回结果继续推理时它就从“聊天机器人”变成了“智能体”也就是 Agent。在企业级系统中这一点尤其重要。比如后端系统接入了大模型接口如果只是做一个智能问答那模型只需要输出文本即可但如果你希望它自动修复一个测试用例、生成一个接口文档、分析一段日志并给出优化后的代码就必须让模型具备操作你项目代码的能力。Java 是后端系统的绝对主力语言很多企业的核心服务、中间件、数据层都是 Java 构建的。如果 Agent 能力只能用 Python 实现那意味着你需要维护两套技术栈。而 Spring AI 的出现让 Java 开发者可以用熟悉的 Spring Boot 风格开发 AI 应用这也是本文选用 Spring AI 作为核心框架的原因。1.2 Claude Code 是什么仿 ClaudeCode 项目要做什么Claude Code 是 Anthropic 推出的一款终端 AI 编程助手。它在终端里启动后可以阅读整个项目的文件结构理解用户需求然后自主修改代码、运行命令、检查运行结果整个流程就像有一个经验丰富的程序员坐在你旁边。本文要做的“仿 ClaudeCode 项目”并不是要完整复刻 Claude Code 的全部功能而是实现一个最小可用的 Agent 闭环用户在终端输入自然语言需求。Agent 分析需求判断需要调用哪些工具。Agent 调用文件读取、文件写入、命令执行等工具。工具返回结果后模型继续推理。最终输出解决结果或修改后的内容。这个闭环看起来简单但它是所有 AI 编程助手的核心骨架。把这个骨架跑通之后后续接入 MCP 协议、接入更多企业服务只是横向扩展的问题。1.3 Spring AI 2.0 Agent Utils Spring AI Alibaba 技术栈拆解先分别看一下这三个组件的作用。Spring AI 是 Spring 官方推出的大模型应用开发框架。它把各家大模型 API 的差异做了抽象底层无论是 OpenAI、DeepSeek、Ollama还是通义千问上层都可以使用统一的 ChatModel、ChatClient 接口。这样做的好处是模型可以灵活替换业务代码不用大改。Agent Utils 是 Spring AI 面向 Agent 场景提供的辅助模块。它的目标是减少手写 Agent 编排逻辑的复杂度比如工具回调管理、消息历史组织、多轮调用限制等。需要说明的是Agent 领域演进速度很快不同小版本之间的 API 调整也比较频繁本文会以它作为 Agent 编排的辅助能力来介绍具体模块坐标和类名建议以你实际拉取到的 BOM 版本为准。Spring AI Alibaba 是阿里云主导的 Spring AI 适配组件专门用于接入阿里云 DashScope 平台上的通义千问等中文大模型。对于国内开发者来说这个组件的价值在于中文模型、中文场景适配更好同时它也会提供一些国内开发者常用的扩展能力。这三者不是互相替代的关系而是组合关系。Spring AI 提供基础抽象能力Spring AI Alibaba 解决中文模型接入问题Agent Utils 则让多步骤 Agent 编排更顺畅。1.4 核心原理Function Calling 与 Agent 自动工具调用要理解本文代码必须先理解 Function Calling。传统的 Chat 调用流程是用户发送消息模型返回文本结束。这个过程模型无法访问外部世界。Function Calling 在中间加了一个环节模型在生成回复时如果判断自己需要某些外部数据或操作能力它会输出一个结构化的“工具调用请求”。比如模型可能说我需要读取/src/main/java/Application.java这个文件。框架收到这个请求后会去执行对应的工具函数然后把执行结果作为上下文再次发送给模型。模型基于工具返回结果继续推理并生成最终回答。用文字描述这个循环是用户输入需求。框架将用户输入和可用的工具列表发送给模型。模型决定是否调用工具。如果不需要直接返回答案。如果需要模型输出工具调用请求。框架执行对应工具获取结果。框架将工具结果返回给模型。模型根据工具结果继续推理。重复步骤直到模型给出最终答案或达到最大迭代次数。在 Spring AI 中这个循环已经由框架内置。我们只需要定义好工具注册到 ChatClient 中框架就会自动完成上面的调度。这也是 Spring AI 开发 Agent 比手写循环高效很多的原因。2. 环境准备与版本说明2.1 基础环境要求在开始写代码之前先确认基础环境。本文示例使用 JDK 17这是 Spring Boot 3.x 和 Spring AI 2.0 比较常见的 Java 版本。如果你使用 JDK 21也没有问题。Maven 建议使用 3.8 以上版本。IDE 推荐 IntelliJ IDEA方便识别注解和自动导入依赖。重要提醒Spring AI 2.0 仍然是一个迭代速度很快的框架不同小版本的依赖坐标、配置前缀、API 名称可能有差异。本文示例以常见用法为准重点演示整个 Agent 开发思路。当你实际创建项目时如果发现某个类或配置项不存在请以官方文档和本地拉取的依赖源码为准。2.2 大模型 API 准备本文需要一个大模型 API。根据你的实际情况有三种选择。第一种是 DeepSeek 的 OpenAI 兼容接口。只需要在 https://platform.deepseek.com 注册并创建 API Key然后在配置中把 base-url 指向 DeepSeek 的接口地址即可。第二种是阿里云 DashScope 平台。在阿里云控制台开通 DashScope 服务获取通义千问的 API Key然后使用 Spring AI Alibaba 接入。第三种是本地模型。如果你希望完全本地运行可以安装 Ollama拉取 qwen2.5 或其他模型Spring AI 也提供了对应的 Ollama 模块。无论使用哪种方案API Key 都不要硬编码在代码仓库里。生产环境中请用环境变量或配置中心管理。2.3 项目结构规划为了让读者对整个项目有一个全局认识先看一下目录结构。ai-coder-agent/ ├── pom.xml ├── src/main/java/com/example/aicoder/ │ ├── AICoderApplication.java │ ├── agent/ │ │ └── CodingAgent.java │ ├── tools/ │ │ └── CodingTools.java │ └── console/ │ └── ConsoleRunner.java └── src/main/resources/ └── application.yml每个文件的职责如下AICoderApplication 是 Spring Boot 启动类。CodingTools 是工具类里面定义 Agent 可以调用的文件读写、命令执行等方法。CodingAgent 是 Agent 核心服务负责创建 ChatClient、定义 System Prompt、调度工具。ConsoleRunner 是终端交互入口使用 CommandLineRunner 在应用启动后进入命令行对话框。3. 搭建 Spring Boot 项目3.1 创建 Maven 项目与依赖打开 IDEA创建一个新的 Spring Boot Maven 项目。你也可以直接创建一个空 Maven 项目然后手动添加依赖。下面是完整 pom.xml 示例?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.5/version relativePath/ /parent groupIdcom.example/groupId artifactIdai-coder-agent/artifactId version1.0.0-SNAPSHOT/version nameai-coder-agent/name description仿 Claude Code 的 Java AI 编程助手/description properties java.version17/java.version spring-ai.version2.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- OpenAI 兼容接口可用于对接 DeepSeek 或其他 OpenAI 兼容服务 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- Agent 工具辅助模块注意版本需与 Spring AI BOM 对齐 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-agent-utils/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project需要注意版本号需要根据你创建项目时的实际依赖调整。Spring AI 2.0 的默认 starter 是spring-ai-starter-model-openai它既支持官方 OpenAI也支持任何 OpenAI 兼容协议的服务比如 DeepSeek。这是本文的主要接入方式。3.2 配置 application.yml在src/main/resources目录下新建application.yml。这里以 DeepSeek 为例给出一个完整配置spring: application: name: ai-coder-agent ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7 client: connect-timeout: 30s read-timeout: 300s配置项解释base-url 是指 OpenAI 兼容接口的地址。DeepSeek 的接口就是 https://api.deepseek.com。api-key 使用环境变量注入避免明文凭证进入代码仓库。model 使用 deepseek-chat这是 DeepSeek 的通用对话模型。temperature 控制回答随机性编程任务建议设置在 0.6 到 0.8 之间。client.read-timeout 需要设置大一些因为 Agent 多轮工具调用耗时较长默认超时时间很容易不够用。如果你使用的是其他 OpenAI 兼容服务只需要改 base-url、api-key 和 model 三个配置即可。3.3 启动类与基础验证创建启动类package com.example.aicoder; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class AICoderApplication { public static void main(String[] args) { SpringApplication.run(AICoderApplication.class, args); } }先不要急着写 Agent 代码直接运行启动类。如果项目能正常启动说明依赖引入和 Spring AI 自动配置已经生效。Spring AI AutoConfiguration 会在检测到 ChatModel 时自动注册一个ChatClient.BuilderBean。这个 Bean 是后续所有 Agent 代码的核心入口。4. 编写 Agent 核心代码4.1 定义工具类Agent 的能力强弱很大程度上取决于工具设计。本文先实现四个基础工具列出目录文件、读取文件、写入文件、执行命令。新建CodingTools.javapackage com.example.aicoder.tools; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; Component public class CodingTools { Tool(description 列出指定目录下的文件和子目录) public String listFiles(ToolParam(description 目录路径默认为当前目录) String path) { String targetPath (path null || path.isBlank()) ? . : path; try (StreamPath stream Files.list(Paths.get(targetPath))) { return stream .map(p - { String name p.getFileName().toString(); return Files.isDirectory(p) ? name / : name; }) .limit(50) .collect(Collectors.joining(\n)); } catch (IOException e) { return 读取目录失败: e.getMessage(); } } Tool(description 读取指定文件的文本内容) public String readFile(ToolParam(description 文件路径) String filePath) { try { return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8); } catch (IOException e) { return 读取文件失败: e.getMessage(); } } Tool(description 将内容写入指定文件如果文件不存在会自动创建如果存在会覆盖) public String writeFile(ToolParam(description 文件路径) String filePath, ToolParam(description 需要写入的文本内容) String content) { try { Path path Paths.get(filePath); if (path.getParent() ! null) { Files.createDirectories(path.getParent()); } Files.writeString(path, content, StandardCharsets.UTF_8); return 写入成功: filePath; } catch (IOException e) { return 写入文件失败: e.getMessage(); } } Tool(description 在指定目录执行 shell 命令返回命令输出和退出码) public String runCommand(ToolParam(description 要执行的命令) String command, ToolParam(description 命令工作目录默认为当前目录) String workDir) { try { String os System.getProperty(os.name).toLowerCase(); ProcessBuilder pb new ProcessBuilder(); if (os.contains(win)) { pb.command(cmd, /c, command); } else { pb.command(sh, -c, command); } if (workDir ! null !workDir.isBlank()) { pb.directory(new File(workDir)); } pb.redirectErrorStream(true); Process process pb.start(); String output new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); boolean finished process.waitFor(30, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); return 命令执行超时(30秒)已强制终止。\n输出\n output; } return 退出码 process.exitValue() \n输出\n output; } catch (Exception e) { return 命令执行失败: e.getMessage(); } } }这段代码需要注意几个点。Tool 注解是 Spring AI 提供的工具标记它在编译时会生成工具描述模型会根据 description 决定何时调用这个方法。ToolParam 用于给每个参数添加说明模型需要理解参数含义所以描述要尽量清晰。listFiles 使用 Files.list 读取目录并将子目录加上斜杠区分。readFile 和 writeFile 分别使用 Java 11 引入的 Files.readString 和 Files.writeString代码非常简洁。runCommand 处理了 Windows 和 Linux 的差异Windows 使用 cmd /cLinux 和 macOS 使用 sh -c。同时限制了最长执行时间为 30 秒防止子进程卡死。这里要特别强调一个真实风险允许 Agent 执行任意命令意味着模型可以通过工具在你本地系统上执行任何命令。这只是一个实验项目如果要在真实环境中使用必须对命令做白名单限制并考虑使用容器或沙箱隔离。4.2 创建 ChatClient 与 Agent 对话逻辑工具定义完成后就可以创建 Agent 核心服务了。新建CodingAgent.javapackage com.example.aicoder.agent; import com.example.aicoder.tools.CodingTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; Service public class CodingAgent { private final ChatClient chatClient; public CodingAgent(ChatClient.Builder builder) { this.chatClient builder .defaultSystem( 你是一名资深 Java 开发工程师用户会提出编程需求。 你可以使用以下工具完成任务 1. listFiles查看目录结构 2. readFile读取文件内容 3. writeFile创建或修改文件 4. runCommand执行命令 要求 - 动手操作前先简述你的执行计划 - 如果需要读取多个文件请逐个读取不要臆测文件内容 - 修改文件后如果可能请通过命令验证结果 ) .defaultTools(new CodingTools()) .build(); } public String execute(String userInput, String workspaceDir) { return chatClient.prompt() .user(u - u.text( 工作目录{workspaceDir} 用户需求{userInput} 请结合工作目录实际情况完成任务。 ) .param(workspaceDir, workspaceDir) .param(userInput, userInput)) .call() .content(); } }这段代码是核心。ChatClient.Builder 是 Spring AI 自动注册的构造函数直接注入即可。defaultSystem 设置了系统提示词它会告诉模型它的身份、可用的工具和操作约束。defaultTools 将 CodingTools 中的 Tool 方法注册给模型。在 execute 方法中用户输入和工作目录通过参数模板传入。这里使用参数化 Prompt避免字符串拼接导致的安全问题。Spring AI 在收到用户请求后如果模型决定调用工具会自动执行工具并继续下一次模型推理。所以这里不需要我们自己写循环代码。这个“自动工具调用循环”就是 ChatClient 最强大的地方。4.3 实现控制台交互入口有了 Agent 服务还需要一个可以与用户在终端对话的入口。新建ConsoleRunner.javapackage com.example.aicoder.console; import com.example.aicoder.agent.CodingAgent; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.util.Scanner; Component public class ConsoleRunner implements CommandLineRunner { private final CodingAgent codingAgent; public ConsoleRunner(CodingAgent codingAgent) { this.codingAgent codingAgent; } Override public void run(String... args) { String workspaceDir args.length 0 ? args[0] : .; System.out.println(AI Coder Agent 已启动); System.out.println(工作目录: new java.io.File(workspaceDir).getAbsolutePath()); System.out.println(输入需求后按回车执行输入 exit 退出); System.out.println(-------------------------------------); Scanner scanner new Scanner(System.in); while (true) { System.out.print( ); String input scanner.nextLine(); if (input null) { break; } String trimmed input.trim(); if (trimmed.isEmpty()) { continue; } if (exit.equalsIgnoreCase(trimmed) || quit.equalsIgnoreCase(trimmed)) { break; } try { String result codingAgent.execute(trimmed, workspaceDir); System.out.println(); System.out.println(result); System.out.println(); } catch (Exception e) { System.err.println(执行出错: e.getMessage()); } } System.out.println(Bye!); } }这个类实现了 CommandLineRunnerSpring Boot 启动后会自动执行它的 run 方法。用户输入 exit 或 quit 后退出循环。4.4 Agent 工具调用循环验证到这里一个最小可用的仿 Claude Code 项目已经完成了。现在运行 AICoderApplication 启动项目在控制台输入类似这样的指令 查看当前目录下的所有文件模型如果决定调用 listFiles 工具日志中会出现工具调用相关信息最终控制台会输出文件列表并附上模型的分析。再试一个更复杂的指令 创建一个名为 hello.txt 的文件内容是 Hello Agent然后读取它并展示内容这个指令会触发两次工具调用第一次调用 writeFile第二次调用 readFile。模型在两次调用之间会等待工具返回结果然后继续下一步。这个过程就是 Agent 的基本工作方式。当你看到控制台自动输出“写入成功”和文件内容时说明整个 Function Calling 链路已经跑通了。5. 接入不同大模型5.1 使用 DeepSeek 模型本文第 3 章的配置已经使用了 DeepSeek。这里单独列出来强调一下完整配置spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7DeepSeek 的接口与 OpenAI 兼容所以不需要额外依赖新的模块直接使用 spring-ai-starter-model-openai 即可。如果你在接入 DeepSeek 时出现“模型无输出”的情况请先检查 base-url 是否正确。DeepSeek 的兼容地址是 https://api.deepseek.com不要把版本号或者不相关的路径拼进去。同时确认 api-key 环境变量是否已正确导出。5.2 使用通义千问Spring AI Alibaba如果要用 Spring AI Alibaba 接入通义千问需要做两件事。首先在 pom.xml 中引入 Alibaba starter并加入对应的 BOM 管理dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId /dependency然后在 dependencyManagement 中加入 Alibaba BOM版本号以官方发布的版本为准。引入之后application.yml 可以这样配置spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus这里需要特别提醒如果你的 pom.xml 中同时有 openai starter 和 alibaba starterSpring 容器中会存在两个 ChatModel Bean导致注入 ChatClient.Builder 时出现歧义。常见做法是只保留一个模型 starter。如果你需要在不同环境切换模型建议使用 Maven Profile 或 Spring Boot Profile 来管理不同依赖。5.3 使用本地模型如果网络条件不允许访问外部 API或者出于数据隐私考虑可以使用本地模型。Ollama 是最简单的本地模型运行方案。先安装 Ollama然后拉取模型ollama pull qwen2.5:7b在 pom.xml 中引入dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependency配置如下spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b本地模型的优点是免费、数据不出内网但小模型的工具调用能力和复杂指令理解能力会明显弱于云端大模型这也是本地部署需要接受的取舍。5.4 环境隔离与配置管理实际项目中开发、测试、生产环境使用的模型可能不同。推荐用 Spring Boot Profile 管理配置。在 application.yml 中保留通用配置然后在application-dev.yml中配置本地模型在application-prod.yml中配置云端模型。启动时通过--spring.profiles.activedev切换。同样API Key 不要写在配置文件里。可以使用环境变量、配置中心或者云厂商的密钥管理服务。6. 运行效果与分析6.1 启动步骤整个项目的启动步骤如下第一步创建项目并复制本文代码。第二步安装并配置模型 API确保环境变量已设置。第三步运行 AICoderApplication 的 main 方法。第四步在控制台输入测试指令观察 Agent 输出。6.2 一个完整的运行示例假设我们想验证 Agent 是否能自动修改代码文件可以在控制台输入 读取 src/main/java/com/example/aicoder/tools/CodingTools.java告诉我这个文件里定义了哪些工具模型首先会调用 readFile 工具读取文件内容然后根据文件内容总结工具列表。最终输出大致如下该文件定义了 4 个工具 1. listFiles列出指定目录下的文件和子目录 2. readFile读取指定文件的文本内容 3. writeFile将内容写入指定文件 4. runCommand在指定目录执行 shell 命令再试一个更复杂的指令让 Agent 新建并编译一个 Java 文件 创建一个 Hello.java 文件输出 Hello Agent然后尝试用命令运行它这个指令会依次触发 writeFile 和 runCommand。模型会先写出 Java 文件然后执行 javac 和 java 命令验证结果。整个过程中模型会等待每个工具的执行结果再决定下一步行动。6.3 关于工具调用日志的解读Spring AI 默认会输出一些日志你可以从中看到工具调用的过程。核心日志信息包括模型返回了工具调用请求、框架执行了哪个工具、工具返回值是什么。如果你的日志框架配置了 DEBUG 级别还能看到完整的大模型请求和响应内容这对排查 Agent 行为很有帮助。7. 常见问题与排查思路7.1 常见问题排查表以下表格总结了本文项目开发中可能遇到的高频问题。问题现象常见原因解决思路springai 连接 DeepSeek 不输出 contentbase-url 或 api-key 配置错误模型调用超时检查配置文件先用 curl 测试 DeepSeek API 连通性工具调用不生效模型直接返回文本Tool 注解未被扫描到或者工具未注册到 ChatClient确认 CodingTools 上加了 Component且 defaultTools 已注册启动报多个 ChatModel 冲突同时引入 openai starter 和 alibaba starter保留一个模型 starter或用 Profile 隔离依赖命令执行工具在 Windows 上报错使用 sh -c 但不兼容 Windows检查 runCommand 中是否根据系统切换 cmd /c内存溢出JVM 报 OutOfMemoryError并发任务过多或者单次读取超大文件控制并发数限制 readFile 读取文件大小增大 JVM 内存参数Lombok 编译警告与当前 JDK 编译器版本不匹配升级 Lombok 版本或去掉 lombok 改用普通代码请求超时Agent 多轮工具调用耗时