Claude Code 这类终端式 Agent 最近确实火,但很多 Java 后端团队的疑问很现实:我们不在 Python/Node 生态里,能不能用纯 Java 做一个自己的代码生成助手?答案是可以,而且核心链路不用自己造轮子,Spring AI 2.0 已经把模型接入、工具调用、多轮记忆这些事做完了。
这篇文章要做一个可落地的项目:用 Spring AI 2.0 + DeepSeek,手写一个类似 Claude Code 的代码生成助手。它不是只调 API 出文本,而是让模型能主动读取工作区文件、创建代码文件、执行只读命令,像真正的 Agent 一样完成从需求到代码落盘的过程。
同时,我会把接口封装、批量任务、超时排查一起讲清楚。如果你正在评估 Spring AI 版本升级、想在 Java 后端内置一个 Agent 能力,或者想给团队做内部编码助手,这篇文章可以直接收藏。先给结论:硬件上没有门槛,不需要独立显卡,核心前置是 Java 17+ 和一个大模型 API Key;建议使用 DeepSeek 这类 OpenAI 兼容接口,国内可以直接访问。
1. 核心能力速览
先说结论,方便快速判断这个方案适不适合你。
| 能力项 | 说明 |
|---|---|
| 项目类型 | Java 后端 Agent 应用,基于 Spring AI 2.0 手写代码生成助手 |
| 核心依赖 | Spring Boot 3.x + Spring AI 2.0 + OpenAI 兼容接口 |
| 主要功能 | 代码生成、代码修改、文件读写、只读命令执行、多轮对话、REST 接口封装、批量任务 |
| 硬件门槛 | 无 GPU 要求,普通开发机即可 |
| 模型要求 | 需要支持 Function Calling 的大模型,示例使用 DeepSeek |
| 支持平台 | Windows / macOS / Linux,依赖 JDK 17+ |
| 启动方式 | Maven 或 Gradle 启动 Spring Boot 应用 |
| 是否支持 API | 支持,本文会封装 REST 接口 |
| 是否支持批量任务 | 支持,线程池 + 任务队列 |
| 适合场景 | 企业内部编码助手、智能运维、代码审查、RAG 应用扩展 |
需要说明的是,Spring AI 2.0 不是一套全新的编程模型,而是把 1.x 时代分散的 API 收拢到了ChatClient这个统一入口上。对 Java 后端来说,最大的价值是:不用学 Python 的 LangChain,也不需要在项目里引一堆 AI 框架,Spring 原生的依赖注入、配置中心、监控体系都能直接复用。
2. 适用场景与使用边界
2.1 适合谁
这个方案最适合三类人:
- Java 后端工程师:想在自己的业务系统里接入大模型能力,既要快点出效果,又要能交给 Spring 管理生命周期。
- 内部工具链负责人:需要给团队做一个统一入口的代码助手,可以读项目、改代码、执行命令,但又不想把代码库整个交给公网 SaaS 工具。
- Spring AI 学习者:已经学过 ChatClient 基础调用,想进一步理解 Agent、Tool Calling、多轮记忆和接口封装。
2.2 不适合什么场景
- 如果你的目标只是生成一次性代码片段,不涉及文件系统,那直接用 OpenAI 兼容接口写一个 HTTP 客户端就够了,不需要引入 Spring AI。
- 如果团队已经有成熟的 Claude Code 工作流,并且已经在终端环境里跑得很顺,不一定要换成 Java 实现。Spring AI 的好处是能嵌进 Web 服务,但终端交互体验需要自己补齐。
- 如果业务要求大模型必须离线部署、断网运行,这个方案并不适合,Spring AI 2.0 默认还是面向云端模型接口设计的。
2.3 使用边界与合规提醒
代码生成助手具备文件读写和命令执行能力,这是它效率高的原因,也是最大的风险点。
- 任何一个允许模型执行命令的 Agent,都必须做命令白名单,不能让模型随意执行
rm -rf、删除数据库、修改生产配置这类操作。 - 涉及公司代码、客户资料、内部接口文档时,要确认你的模型服务是否会把请求内容用于训练。对敏感项目,建议用私有化部署模型或者明确签署数据协议的服务。
- 生成代码的质量由模型决定,接入生产环境前必须有人工 Code Review,不要自动合并生成结果。
- 如果后续做声音、图像、人脸相关功能,一定要确认素材授权,但本文的代码生成助手不涉及这些能力,重点约束就在命令执行和文件写入边界上。
3. 环境准备与项目初始化
3.1 运行环境检查清单
先确认本机环境,缺哪个补哪个。
| 环境项 | 建议配置 | 检查方式 |
|---|---|---|
| JDK | 17 或更高版本 | java -version |
| Maven | 3.8+ 或 Gradle 8+ | mvn -v |
| Spring Boot | 3.3+ | 通过父 POM 管理 |
| Spring AI | 2.0 当前版本 | 在 Maven 中央仓库确认 |
| API Key | DeepSeek 或其他兼容接口 | 在对应平台申请 |
3.2 创建 Spring Boot 工程
建议直接通过 Spring Initializr 创建工程,也可以手动创建 Maven 项目。下面是完整的 Maven 依赖配置:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.1</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>2.0.0-M1</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI 官方 OpenAI 兼容模块 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- Tool Calling 支持 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tool-calling</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- 可选:内存记忆,用于多轮对话上下文管理 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-memory</artifactId> <version>${spring-ai.version}</version> </dependency> </dependencies>注意一点:Spring AI 的 Maven 依赖坐标在不同小版本之间可能会有微调。网上搜到的很多帖子写的是 1.0.0 的旧坐标,如果你用的是 2.0,一定以 Maven 中央仓库里实际发布的 artifactId 为准。版本号也不要写死,先跑通再说。
3.3 准备模型服务
本文示例使用 DeepSeek,因为它是 OpenAI 兼容接口,可以直接通过配置 base-url 接入,也适合国内网络环境。你需要在 DeepSeek 开放平台申请 API Key,并确保账户有足够余额。申请完成之后,把 Key 保存到环境变量里,不要直接写死在代码和配置文件里。
如果你是公司内部已经部署了其他 OpenAI 兼容模型服务,也可以替换,只需要把 base-url 和模型名改掉。
4. 接入 DeepSeek 的两种配置方式
Spring AI 接入 DeepSeek 有两种常见方式,二选一即可。
4.1 方式一:官方 DeepSeek Starter
如果当前 Spring AI 2.0 版本已经提供 DeepSeek 模块,直接使用官方配置最省事:
spring: ai: deepseek: api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.2这种方式的好处是代码里不需要手动指定 base-url,模型客户端由框架自动装配。
4.2 方式二:OpenAI 兼容模式
如果你更熟悉 OpenAI 的接入方式,或者需要兼容多个模型服务商,推荐使用 OpenAI 兼容模式。DeepSeek 的 API 兼容 OpenAI 协议,配置如下:
spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.2两种方式的核心参数是model: deepseek-chat。如果你在开发者平台看到的是其他模型名,以平台文档为准。这里要提醒一个高频坑:很多人把模型名写成了deepseek-coder或者其他历史名称,导致请求返回 400 Invalid Model。deepseek-chat是当前最常见的对话模型名称。
4.3 验证模型连通性
配置写完之后,先写一个最简单的 Controller 或 CommandLineRunner 验证连通性,不做任何 Agent 逻辑。
package com.example.aiagent; import org.springframework.ai.chat.client.ChatClient; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class PingRunner implements CommandLineRunner { private final ChatClient chatClient; public PingRunner(ChatClient.Builder builder) { this.chatClient = builder.build(); } @Override public void run(String... args) { String reply = chatClient.prompt("用一句话介绍你自己") .call() .content(); System.out.println("模型回复: " + reply); } }如果这一步能正常打印出模型回复,说明 API Key、模型名、网络链路全部没问题。如果这里就报错,不要继续往下做 Agent,先把基础链路排查清楚,方法和下文第 10 节一致。
5. 手写 Agent:从 ChatClient 到 Tool Calling
5.1 Agent 的核心链路
一个类 Claude Code 的 Agent 通常包含四层能力:
- 模型层:负责理解用户意图、生成代码、决定下一步动作。
- 工具层:提供文件读取、文件写入、命令执行等能力,模型通过 Function Calling 决定何时调用。
- 记忆层:保存多轮对话上下文,避免每次都丢失前面的任务状态。
- 执行层:把模型生成的代码、修改后的文件落盘,并返回执行结果继续给模型判断。
Spring AI 2.0 的ChatClient是这一切的主入口。你需要做的不是自己写 Agent 编排框架,而是把工具类注册给模型,让模型在回答过程中按需调用。
5.2 定义代码工具集
先创建一个工具类,里面定义模型可以调用的方法。Spring AI 2.0 使用@Tool注解标记工具方法,方法的参数和描述会被框架转换成模型可识别的 Function Calling 结构。
package com.example.aiagent.tools; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @Component public class CodeWorkspaceTools { // 工作区根目录,所有文件操作都被限制在这个目录内 private static final Path ROOT = Paths.get(System.getProperty("user.home"), "agent-workspace"); @Tool(description = "读取工作区中指定路径的文本文件") public String readFile(String path) { try { Path target = resolvePath(path); return Files.readString(target, StandardCharsets.UTF_8); } catch (IOException e) { return "读取失败: " + e.getMessage(); } } @Tool(description = "将文本内容写入工作区中指定路径,如果父目录不存在则自动创建") public String writeFile(String path, String content) { try { Path target = resolvePath(path); Files.createDirectories(target.getParent()); Files.writeString(target, content, StandardCharsets.UTF_8); return "写入成功: " + target; } catch (IOException e) { return "写入失败: " + e.getMessage(); } } @Tool(description = "列出工作区指定目录下的文件列表") public String listFiles(String path) { try { Path target = resolvePath(path); StringBuilder sb = new StringBuilder(); try (var stream = Files.list(target)) { stream.forEach(p -> sb.append(p.getFileName()).append("\n")); } return sb.toString(); } catch (IOException e) { return "列目录失败: " + e.getMessage(); } } private Path resolvePath(String path) { Path target = ROOT.resolve(path).normalize(); if (!target.startsWith(ROOT)) { throw new IllegalArgumentException("路径越界,不允许访问工作区之外的文件"); } return target; } }这里最核心的是resolvePath方法。它把所有路径先 normalize 再判断是否以工作区根目录开头,防止模型通过../../路径跳出工作区读写系统文件。Agent 工具越权是排第一位的安全问题,代码必须提前兜住。
5.3 配置 ChatClient 和系统提示词
系统提示词决定了 Agent 的行为模式。这里我们把它定义成一个“运行在终端环境中的代码生成助手”,并明确告知模型哪些工具可用、什么情况下使用工具。
package com.example.aiagent.config; import com.example.aiagent.tools.CodeWorkspaceTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AgentConfig { @Bean ChatClient codingAgentChatClient( ChatClient.Builder builder, CodeWorkspaceTools tools) { return builder .defaultSystem(""" 你是一个运行在终端环境中的代码生成助手,类似 Claude Code。 你的职责是帮助用户完成 Java 项目中的代码生成和代码修改任务。 可用工具: 1. readFile(path) - 查看工作区文件内容 2. writeFile(path, content) - 创建或覆盖文件 3. listFiles(path) - 查看工作区目录结构 工作规则: - 在生成代码之前,先查看目标目录的现有文件,避免重复创建。 - 修改已有代码时,先读取文件内容,再决定修改方案。 - 不要编造文件内容,如果文件读取失败,主动告诉用户原因。 - 生成代码时给出必要的注释,但不要过度装饰。 - 输出结果时用中文,代码本身保持 Java 语法。 """) .defaultTools(tools) .build(); } }defaultTools这一步是关键。模型能不能真正调用工具,取决于这里是否把工具类传给了 ChatClient。很多新手做完工具类发现模型不调用,大概率就是忘了注册工具。
5.4 创建 Agent 服务层
把 ChatClient 封装到一个 Service 里,方便后续给 Controller 和批量任务复用。
package com.example.aiagent.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class CodingAgentService { private final ChatClient chatClient; public CodingAgentService(ChatClient chatClient) { this.chatClient = chatClient; } public String run(String userPrompt) { return chatClient.prompt() .user(userPrompt) .call() .content(); } }到这里,你已经拥有了一个最基础的代码生成 Agent。接下来做功能测试。
6. 功能测试与效果验证
6.1 测试一:基础代码生成
先测试基础能力,让 Agent 直接生成一个 Java 工具类。
操作步骤:
- 启动 Spring Boot 应用。
- 调用
CodingAgentService.run,输入提示词:
请在工作区生成一个 StringUtils.java,包含一个判断字符串是否为空的方法。- 观察模型是否先调用
listFiles查看目录,再调用writeFile写入文件。
判断成功的标准:
- 工作区
agent-workspace目录下出现StringUtils.java文件。 - 文件内容包含完整的方法签名和实现。
常见失败:
- 模型只输出代码文本但没有写入文件,说明模型没有触发工具调用。
- 写入路径不对,因为提示词里没有指定包名,模型可能直接写了根目录文件。
建议在测试时把需求描述得更精确,例如“在src/main/java/com/example/demo/utils目录下生成”。
6.2 测试二:读取并修改已有代码
这个测试更接近真实 Agent 场景。先手动在工作区放一个Calculator.java,内容故意只有加法,然后让 Agent 增加一个减法方法。
工作区里有一个 Calculator.java,请先读取它,然后增加一个减法方法并保存。判断成功的标准:
- 模型先调用
readFile拿到原始代码。 - 模型再调用
writeFile写入包含减法方法的完整文件。 Calculator.java中同时存在加法和减法方法。
如果模型没有先读文件就直接写,说明系统提示词约束不够强,可以把“修改代码前必须先读取文件”进一步强调,或者改成多轮对话强制确认。
6.3 测试三:多轮对话上下文
一个合格的 Agent 需要记住用户在前几轮提到的信息。加入 Spring AI 的内存机制后,模型可以在同一会话内记住之前的任务。
在使用ChatClient时启用ChatMemory:
package com.example.aiagent.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatMemoryConfig { @Bean ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean ChatClient memoryChatClient( ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } }测试方法:先问“我们项目的包名是 com.example.demo”,再问“请在这个包名下生成一个 User.java”。如果第二轮的生成结果自动带上com.example.demo包名,说明上下记忆生效。
6.4 判断 Agent 是否在正常工作
在开发阶段,强烈建议开启 Spring AI 日志,观察模型和工具之间的调用过程。
logging: level: org.springframework.ai: DEBUG日志里会看到模型返回的 tool calls、工具执行结果和最终回复。Debug 日志看着会有点多,但排查 Agent 问题非常有效。
7. 接口 API 与批量任务
7.1 封装 REST 接口
Agent 跑通之后,下一步就是对外提供接口能力。这个方案是 Web 服务,天然适合封装成 REST API,供前端、命令行工具或其他服务调用。
package com.example.aiagent.controller; import com.example.aiagent.service.CodingAgentService; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/agent") public class AgentController { private final CodingAgentService codingAgentService; public AgentController(CodingAgentService codingAgentService) { this.codingAgentService = codingAgentService; } @PostMapping("/run") public Map<String, String> run(@RequestBody Map<String, String> request) { String prompt = request.get("prompt"); if (prompt == null || prompt.isBlank()) { return Map.of("error", "prompt 不能为空"); } String result = codingAgentService.run(prompt); return Map.of("result", result); } }启动服务后,用 curl 验证:
curl -X POST http://127.0.0.1:8080/api/agent/run \ -H "Content-Type: application/json" \ -d '{"prompt": "请生成一个 HelloController,默认返回 hello"}'预期返回 JSON:
{ "result": "已生成 HelloController.java,内容如下..." }7.2 批量任务设计
代码生成助手常用于批量生成工具类、批量补测试、批量修复警告。批量任务不能直接在 HTTP 请求里同步执行,否则一个任务跑 1 分钟,接口就很容易超时。正确做法是把任务提交到线程池,异步执行,用一个任务 ID 去查结果。
这里给一个通用模板:
package com.example.aiagent.service; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; @Service public class BatchAgentService { private final CodingAgentService codingAgentService; private final ExecutorService executor = Executors.newFixedThreadPool(4); private final ConcurrentHashMap<String, CompletableFuture<String>> tasks = new ConcurrentHashMap<>(); private final AtomicLong idGenerator = new AtomicLong(0); public BatchAgentService(CodingAgentService codingAgentService) { this.codingAgentService = codingAgentService; } public String submit(String prompt) { String taskId = "task-" + idGenerator.incrementAndGet(); CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> codingAgentService.run(prompt), executor) .exceptionally(ex -> "任务执行失败: " + ex.getMessage()); tasks.put(taskId, future); return taskId; } public Map<String, Object> query(String taskId) { CompletableFuture<String> future = tasks.get(taskId); if (future == null) { return Map.of("error", "任务不存在"); } if (future.isDone()) { return Map.of("status", "done", "result", future.join()); } return Map.of("status", "running"); } public void submitBatch(List<String> prompts) { prompts.forEach(this::submit); } }使用方式:
# 提交任务 curl -X POST http://127.0.0.1:8080/api/agent/batch/submit \ -H "Content-Type: application/json" \ -d '{"prompt": "批量生成 10 个工具方法备注"}' # 查询任务 curl http://127.0.0.1:8080/api/agent/batch/query/task-1批量任务要注意几个点:
- 线程池大小要根据模型接口的 QPS 限制来定,