Java Agent实战:用Spring AI 2.0打造仿ClaudeCode项目助手
2026/8/30 23:38:11 网站建设 项目流程

最近在业务里做 Java 侧的大模型 Agent 应用时,一个很明显的感受是:Java 生态里能直接落地的 Agent 实战资料,比 Python 少了很多。很多教程停留在“用 Spring Boot 调一次大模型 API,返回一段文本”的阶段,但真正想实现类似 ClaudeCode 这种能浏览项目文件、根据用户指令自动分析代码、再总结回答的 Agent 时,你会发现要补的细节非常多:工具调用怎么注册、多轮工具结果怎么回传、怎么防止 Agent 越权读取目录,等等。

本文会围绕 Spring AI 2.0 生态,结合 Agent Utils 和 Spring AI Alibaba 写一套从零到可运行的 Java Agent 实战流程。核心目标是做一个“仿 ClaudeCode”的 Java 项目助手:用户用自然语言提问,Agent 自己决定要不要查看项目文件、要不要读取某个文件内容,最后给出分析结论。适合正在学习 Spring AI、想从“调 API”走向“写 Agent”的 Java 开发者,也适合准备把 AI 能力集成进内部工具链的后端团队。

1. 为什么用 Java 开发大模型 Agent

1.1 从“调用大模型”到“构建 Agent”

早期 Java 后端接大模型,最常见的写法是封装一个 HttpClient,把用户问题拼进 Prompt,请求大模型接口,拿到回复后回显给前端。这是“单轮问答”,本质上是一个远程函数调用,不存在智能决策。

Agent 的差别在于:模型不再只生成文本,它可以根据用户目标,决定“是否需要调用工具”“调用哪个工具”“拿到工具结果之后下一步怎么走”。这个过程通常叫 Agent 循环(Agent Loop),或者工具调用循环(Tool Calling Loop)。

用大白话解释:普通问答是“用户问一句,模型答一句”;Agent 是“用户提一个目标,模型自己拆步骤、调工具、看结果、再继续做,直到任务完成”。

例如用户说“看看这个项目有哪些文件,顺便读一下 README 总结项目用途”。如果只是普通问答,模型只能瞎编。但在 Agent 场景下,模型会先请求调用listFiles工具,拿到文件列表,再调用readFile("README.md")拿到内容,最后结合工具结果生成总结。这就是仿 ClaudeCode 的核心体验。

1.2 Spring AI 2.0 生态:Agent Utils 与 Spring AI Alibaba

Spring AI 是 Spring 官方推出的 AI 应用开发框架,目标是让 Java 开发者用一套统一的 API 接入不同大模型。它把 ChatModel、EmbeddingModel、Tool Calling、结构化输出等能力抽象成 Spring 风格接口,开发者不需要关心底层 HTTP 协议和各家 API 差异。

Spring AI 2.0 相比早期版本,在模型接入、工具调用链、可观测性上有明显演进。围绕 Agent 场景,Spring AI 生态里有几个重要模块:

  • ChatModel / ChatClient:统一对话模型入口,ChatClient 是推荐的高层 API,支持链式调用、工具注册和流式返回。
  • Agent Utils:面向 Agent 开发的基础模块,提供 Tool Callback 定义、方法工具适配、模型上下文管理等能力。平时写的工具类通过@Tool标注后,可以方便地被 Agent 使用。
  • Spring AI Alibaba:阿里巴巴开源的 Spring AI 适配组件,提供阿里云百炼(DashScope)接入、通义千问系列模型的 starter,以及在 Java 侧使用大模型时常见的企业级扩展。

这三个组件组合起来,就能搭建一个类似 ClaudeCode 的 Java Agent 底座。需要说明的是,Agent Utils 的具体模块名在不同版本可能调整,实际开发时以当前 Spring AI 官方 Release 的文档为准。

1.3 仿 ClaudeCode 项目的核心思路

ClaudeCode 给人的体感不是简单的聊天窗口,而是“一个能动手操作代码库的助手”。它能看到项目结构、读取文件、甚至执行命令,然后基于真实信息回答。

在 Java 里仿这种形态,不需要照搬它的前端交互,关键是实现“模型 + 工具 + 循环”这套内核。我们用 Spring AI 2.0 做一次最小实现,包含三个部分:

  1. 把“查看文件列表”“读取文件内容”封装成工具方法。
  2. 把工具注册给 ChatClient,让大模型知道自己能调用哪些能力。
  3. 调用 ChatClient 时,框架自动处理多轮工具调用循环。

后面的实战章节会逐步展开这三个点。先看环境和项目怎么搭。

2. 环境准备与项目初始化

2.1 版本与依赖说明

本文示例以 Spring Boot 3.x + JDK 17 为基准。当前 Spring AI 2.0 对 JDK 版本有要求,建议使用 JDK 17 或更高版本,如果条件允许可以用 JDK 21。Maven 建议 3.8 以上。

需要特别提醒:Spring AI 版本迭代很快,不要直接照抄网上任意一个版本的依赖坐标。优先去 Spring AI 官方 Release 页面和 Spring AI Alibaba 官方仓库查看当前稳定版本。以下代码是为了展示依赖结构,版本号需要按你自己的环境调整。

组件说明
JDK17 或 21
Maven3.8+
Spring Boot3.4 及以上(具体看 Spring AI 2.0 兼容矩阵)
Spring AI BOM以官方 Release 版本为准
Spring AI Alibaba Starter以官方 Release 版本为准

2.2 创建 Spring Boot 项目

推荐直接使用 Spring Initializr 生成基础项目,选择 Spring Web 依赖。项目结构如下:

java-agent-demo/ ├── pom.xml └── src/main/ ├── java/com/example/agent/ │ ├── AgentApplication.java │ ├── controller/ │ │ └── AgentChatController.java │ └── tool/ │ └── ProjectTools.java └── resources/ └── application.yml

pom.xml中,先引入 Spring AI BOM,再添加相关依赖。关键片段如下:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <!-- 请使用官方发布的 2.0 版本号 --> <version>2.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI Alibaba:用于接入阿里云百炼/DashScope --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <!-- 版本请查看 Spring AI Alibaba 官方 Release --> </dependency> <!-- Agent Utils:Agent 工具调用相关基础能力 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-agent-utils</artifactId> </dependency> </dependencies>

如果实际项目中依赖解析不到spring-ai-agent-utils,需要去官方文档确认当前 2.0 版本的模块命名,不同版本存在改名或拆分的情况。

2.3 配置大模型 API

我这边以阿里云百炼为例,因为 Spring AI Alibaba 对这个场景支持最完整。在阿里云百炼控制台开通模型服务后,获取 API Key,配置到环境变量中。

编辑src/main/resources/application.yml

spring: application: name: java-agent-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 agent: project-base-path: ./

如果你使用的是 DeepSeek 或其他兼容 OpenAI 协议的模型,也可以通过 Spring AI 的 OpenAI 协议接入。配置思路如下:

spring: ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: model: deepseek-chat

具体模型名和接入方式以模型服务商文档为准。配置完成后,先不要急着写 Agent 逻辑,我们来理解一下工具调用循环的原理,这是整个实战的核心。

3. 核心原理:工具调用循环

3.1 ChatModel 与 ChatClient

在 Spring AI 中,ChatModel 是底层接口,负责与模型服务端通信。开发者一般不会直接操作 ChatModel,而是使用 ChatClient 这个高层封装。

ChatClient 支持链式调用,典型结构是:

String answer = chatClient.prompt() .user("用户问题") .tools(toolObject) .call() .content();

你可以先设置系统提示词(defaultSystem),再在每次请求中追加用户消息和工具列表。.tools()是核心:把 Java 方法暴露给模型,模型会根据用户意图自行决定是否调用。

3.2 @Tool 注解与 ToolCallback

工具本身就是一个普通 Java 方法。为了让模型知道这个方法的存在、作用和参数,Spring AI 提供了@Tool注解。在方法上写清楚描述,模型就能在生成结果时看到这段描述,从而决定是否调用。

示例:

import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; @Component public class SimpleTools { @Tool(description = "获取当前时间") public String currentTime() { return java.time.LocalDateTime.now().toString(); } }

Spring AI 扫描到@Tool方法后,会把它封装成ToolCallback。这个对象包含了工具名称、描述、参数结构,以及真正执行时对 Java 方法的调用逻辑。工具描述写得好不好,直接影响模型调用工具的准确率。

3.3 Agent 循环如何工作

很多初学者会误以为“模型会一边回答一边执行 Java 方法”,实际上模型不会主动执行你的代码。模型只负责输出一个特殊结构:tool_calls,其中包含工具名和参数。

流程如下:

  1. 用户输入消息,框架把消息和工具定义一起发给模型。
  2. 模型判断需要调用工具,返回一个ToolCall,例如listFiles
  3. Spring AI 框架收到ToolCall后,在本地执行对应的 Java 方法。
  4. 框架把工具执行结果作为一条新消息回传给模型。
  5. 模型继续生成回复,可能再次请求调用其他工具。
  6. 循环重复,直到模型不再请求工具,返回最终文本。

这套自动循环就是 Agent 的“自主性”来源。你不需要手写 while 循环,Spring AI 会自动处理多轮工具调用。理解这一点后,下面可以开始写一个真实可运行的项目。

4. 实战:实现一个能读懂项目的 Java Agent

4.1 设计工具集

仿 ClaudeCode 的第一步,是让 Agent 具备“查看项目文件”和“读取文件内容”的能力。这两个工具足以完成很多代码分析任务。

工具设计如下:

  • listFiles:查看项目根目录下的文件清单。
  • readFile:读取指定文件的文本内容。

安全上要做两个约束:

  1. 无论用户传入什么路径,都只能访问项目根目录内的文件。
  2. 文件内容过长时要截断,避免超出模型上下文窗口。

4.2 实现项目文件浏览工具

创建src/main/java/com/example/agent/tool/ProjectTools.java

package com.example.agent.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; @Component public class ProjectTools { private final Path basePath; public ProjectTools(@Value("${agent.project-base-path:./}") String basePath) { this.basePath = Paths.get(basePath).toAbsolutePath().normalize(); } @Tool(description = "列出项目根目录下的文件清单,最多返回50条。") public String listFiles() throws IOException { StringBuilder sb = new StringBuilder(); try (Stream<Path> paths = Files.walk(basePath)) { paths.filter(Files::isRegularFile) .limit(50) .forEach(p -> sb.append(relativize(p)).append(System.lineSeparator())); } return sb.length() == 0 ? "项目目录为空" : sb.toString(); } @Tool(description = "读取指定文件的内容。path 必须是相对于项目根目录的路径。") public String readFile(String path) throws IOException { Path target = basePath.resolve(path).normalize(); if (!target.startsWith(basePath)) { return "拒绝访问:路径越界,只允许读取项目根目录内的文件。"; } if (!Files.exists(target) || !Files.isRegularFile(target)) { return "文件不存在或不是普通文件:" + path; } String content = Files.readString(target); if (content.length() > 3000) { content = content.substring(0, 3000) + "\n...(内容过长已截断)"; } return content; } private String relativize(Path path) { return basePath.relativize(path).toString(); } }

关键点说明:

  • basePath在构造时通过配置注入,默认是当前目录。
  • readFile里做了startsWith(basePath)校验,防止../路径穿越。
  • 文件内容超过 3000 字符后截断,避免无用 token 消耗。

4.3 编写 Controller 调用 Agent

创建src/main/java/com/example/agent/controller/AgentChatController.java

package com.example.agent.controller; import com.example.agent.tool.ProjectTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/agent") public class AgentChatController { private final ChatClient chatClient; private final ProjectTools projectTools; public AgentChatController(ChatClient.Builder builder, ProjectTools projectTools) { this.projectTools = projectTools; this.chatClient = builder .defaultSystem("你是一个 Java 项目助手。你可以查看项目文件、读取文件内容,帮助用户分析代码和项目结构。回答要简洁、直接。") .build(); } @PostMapping("/chat") public Map<String, String> chat(@RequestBody Map<String, String> body) { String message = body.get("message"); String answer = chatClient.prompt() .user(message) .tools(projectTools) .call() .content(); return Map.of("answer", answer == null ? "" : answer); } }

启动类保持 Spring Boot 默认即可:

package com.example.agent; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class AgentApplication { public static void main(String[] args) { SpringApplication.run(AgentApplication.class, args); } }

这里有一个工程上的细节:ChatClient.Builder是 Spring AI 自动配置好的 Bean。通过构造器注入,就不需要手动创建ChatClient,后续所有 Agent 调用都基于同一个客户端。

4.4 运行与验证

启动应用后,用 curl 模拟用户请求:

curl -X POST http://localhost:8080/agent/chat \ -H "Content-Type: application/json" \ -d '{"message":"请查看这个项目有哪些文件?"}'

预期结果类似:

{ "answer": "项目根目录下包含以下文件:\npom.xml\nsrc/main/java/com/example/agent/AgentApplication.java\n..." }

再测试一个需要读取文件并总结的场景:

curl -X POST http://localhost:8080/agent/chat \ -H "Content-Type: application/json" \ -d '{"message":"读取 README.md 并总结项目用途"}'

如果 README 存在,模型会先调用readFile工具,拿到内容后再总结。日志里能看到模型请求工具、工具执行、结果回传的过程。

4.5 结果说明

到这里,一个最简版的“仿 ClaudeCode”Agent 已经跑通了。用户提问、模型决策、工具执行、最终回答这一整条链路完全由 Spring AI 驱动。

不过你可能会发现,当前这个版本还比较简陋:

  • 每次请求都要重复传入ProjectTools对象。
  • 没有流式输出,大模型回答慢的时候体验不好。
  • 文件工具只有只读能力,不能写文件。

这些正是下一章要扩展的内容。

5. 扩展:让 Agent 更接近 ClaudeCode 体验

5.1 增加流式输出

ClaudeCode 的交互感很大程度来自流式输出。Spring AI 的 ChatClient 支持.stream(),返回响应式流。示例片段:

import reactor.core.publisher.Flux; Flux<String> answerStream = chatClient.prompt() .user(message) .tools(projectTools) .stream() .content();

如果你使用 Spring WebFlux,可以直接把Flux<String>作为接口返回值,前端通过 SSE 接收。默认情况下,工具调用阶段不会输出文字,只有模型最终生成文本时才产生流式内容。

5.2 限制 Agent 的工作目录

在实际项目中,不能允许 Agent 扫描整个服务器目录。建议启动时明确指定工作目录,只让 Agent 操作某个项目仓库的副本:

java -jar java-agent-demo.jar --agent.project-base-path=/var/repos/my-project

这样工具类的basePath被限定在指定目录内,路径穿越校验继续生效。如果团队有多个项目仓库,可以按仓库维度启动一个 Agent 实例。

5.3 与代码仓库结合:后续演进方向

仿 ClaudeCode 还可以继续叠加这些能力:

  • git diff工具:让 Agent 查看当前分支改动,辅助代码审查。
  • searchCode工具:基于关键词搜索代码片段,替代全量读取文件。
  • writeFile工具:让 Agent 修改代码,但必须加权限控制和操作审计。
  • runTests工具:执行测试命令并返回结果,适合在 CI 环境使用。

需要强调的是,越接近真实的代码操作助手,安全设计越重要。工具的能力越强,越不能把 Agent 直接暴露给不可信用户。

6. 常见问题与排查思路

6.1 Spring AI 连接 DeepSeek 不输出 content

有开发者反馈,Spring AI 连接 DeepSeek 时请求成功,但最终返回的content为空。这个问题的常见原因有几种:

原因现象解决方向
模型返回了tool_calls,而不是content第一次响应 content 为空,但模型申请调用工具确认是不是工具调用场景,检查finish_reason
模型名配置错误返回内容为空或直接报错检查模型名,例如deepseek-chatdeepseek-reasoner
API Key 或额度问题偶发空内容查看服务商控制台的调用日志
框架版本兼容问题工具调用回传后第二次请求异常升级 Spring AI / Spring AI Alibaba 版本

排查建议是先开启 Spring AI 的请求日志,看原始响应内容到底是什么,再决定是模型侧问题还是框架侧问题。不要一上来就改代码。

6.2 依赖解析失败 / 模块找不到

如果在 Maven 中找不到spring-ai-agent-utils或 Spring AI Alibaba Starter,大概率是版本匹配问题。Spring AI 2.0 的模块命名和 1.0 有差异,BOM 中并不一定包含所有模块。

解决思路:

  1. 先确认 Spring AI BOM 版本正确。
  2. 去官方文档查看当前版本推荐的 artifactId。
  3. 如果使用 Spring AI Alibaba,需要单独引入它的 BOM 或版本号。
  4. 不要混用 1.x 和 2.x 的依赖。

6.3 编译与运行内存问题

大模型相关项目常出现以下内存异常:

java: OutOfMemoryError: insufficient memory

可能原因:

  • IDE 构建过程内存不足,而不是 JVM 运行内存。
  • Spring Boot 应用启动了多个实例,导致宿主机内存被耗尽。
  • 工具读取超大文件,把文件内容全部加载到内存。

解决方向:

  • IDE 中调整构建进程堆内存。
  • 启动参数增加-Xmx,例如-Xmx512m
  • 工具方法读取文件时限制大小,避免一次性读取超大文件。

6.4 Windows 下 Docker / WSL 服务缺失

有的开发者在 Windows 上运行依赖 Docker 的 ClaudeCode 或本地模型环境时,会看到类似missing hcs services: hns, vmcompute, vfpext的报错。这个报错说明 Windows 的 Hyper-V 相关服务没有完全启动,Docker Desktop 无法创建虚拟网络。

排查步骤:

  1. 检查 Docker Desktop 是否正常运行,尝试重启。
  2. 打开 Windows 功能,确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”已开启。
  3. 打开 PowerShell 执行wsl --status查看 WSL 状态。
  4. 重启电脑后再次启动 Docker Desktop。

这个问题主要影响本地中间件环境,不影响 Spring AI 本身的编码。

6.5 Lombok 与 JDK 版本冲突

报错示例:

java: You aren't using a compiler supported by lombok, so lombok will not work.

常见原因是 Lombok 版本过旧,不支持当前使用的 JDK。解决方法是把 Lombok 升级到较新版本,并确保 IDE 编译器的 JDK 配置与项目一致。如果你在 Spring AI 示例代码中大量使用@Slf4j这类注解,这步配置直接影响项目是否能编译通过。

7. 最佳实践与工程建议

7.1 API Key 与配置管理

API Key 是最高优先级的安全资源,绝对不能硬编码到代码或配置文件里。推荐方式:

  • 本地开发用环境变量,例如DASHSCOPE_API_KEY
  • 生产环境使用配置中心或密钥管理服务。
  • .gitignore中排除包含密钥的本地配置文件。
  • 定期轮换 API Key,降低泄露风险。

7.2 提示词与工具设计

Agent 的表现往往不取决于模型,而取决于工具描述和系统提示词的设计。

给工具方法写description时,要写清楚“这个工具是干什么的”“参数应该传什么”“什么场景下使用”。比如:

@Tool(description = "读取指定文件的内容。path 必须是相对于项目根目录的路径,例如 pom.xml 或 src/main/java/xxx.java。")

清晰的描述能显著提高大模型调用工具的准确率。系统提示词中,也要说明 Agent 的角色边界,例如“你是项目助手,不要编造文件内容,如果需要信息请先读取文件”。

7.3 安全边界

这是 Agent 工程里最容易被忽略的部分。

  • 所有文件路径必须做归一化和前缀校验,防止路径穿越。
  • 不要直接给 Agent 暴露任意命令执行工具,尤其是bashrmsudo这类高风险命令。
  • 如果必须执行命令,一定要做白名单控制和操作审计。
  • 工具返回值不要包含数据库密码、API Key 等敏感信息。
  • 生产环境建议把 Agent 能力封装成内部服务,通过鉴权控制访问范围。

7.4 可观测性与成本控制

Agent 的多轮工具调用会放大 token 消耗。一次看似简单的问题,可能背后发生了 3 次模型请求。建议在工程化时记录:

  • 每次请求的模型名称和 Token 数量。
  • 工具调用顺序和时间。
  • 超时、重试、失败的次数。
  • 流式输出的首字延迟和总耗时。

Spring AI 提供了一些可观测性扩展点,可以对接 Micrometer、Prometheus 等监控体系。成本控制方面,优先使用更便宜的模型处理简单的工具调用,只在关键任务上使用更强模型。

7.5 测试策略

Agent 应用的测试和传统单元测试不一样,它依赖外部大模型,结果有一定随机性。建议这样测:

  • 用接口测试验证工具方法本身的正确性,重点测路径穿越、文件不存在、内容截断等边界。
  • 用录制好的模型响应做回归测试,保证工具调用链路稳定。
  • 在真实模型上做少量人工验证,观察工具描述是否清晰、调用是否正确。
  • 不要把大量真实模型请求写进 CI,避免费用失控和结果不稳定。

8. 总结与下一步学习路线

通过本文的实战,你已经完成了一个基于 Spring AI 2.0 的 Java Agent 最小闭环:理解工具调用循环、用@Tool暴露文件工具、用 ChatClient 完成多轮调用、最终实现类似 ClaudeCode 的项目浏览与代码分析能力。

接下来可以按这个路线继续深入:

  • RAG 增强:把项目文档、API 文档向量化,让 Agent 在回答时能检索知识库。
  • 多 Agent 协作:拆分成“代码阅读 Agent”“测试执行 Agent”“评审 Agent”,让多个角色协同完成任务。
  • Spring AI Alibaba 的 Graph 能力:在复杂任务中用图编排控制 Agent 的执行流程。
  • 模型微调:如果工具描述和系统提示词已经无法提升效果,再考虑用领域数据微调小模型。

更实际的做法是先把本文代码跑通,然后尝试把ProjectTools扩展成你真实项目的工具类,让 Agent 能读取你的业务代码和配置文件。在动手过程中,你会比看十篇原理文章更清楚 Agent 的边界在哪里。

如果本文对你有帮助,可以收藏备用。后面我会继续更新 Spring AI 2.0 的 RAG 实战和多 Agent 编排案例,欢迎持续关注。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询