Spring AI 2.0 的 Agent 能力,让我愿意重新把 Java 后端和大模型放到一起考虑。平时我们用 ChatGPT 写代码,只能把代码复制来复制去;这次要聊的是在 Spring Boot 工程里自己写一个类似 Claude Code 的代码生成助手,让模型自己读文件、改文件、生成代码,Java 后端也能直接跑通 Agent 流程。先给结论:这个方案真正值得关注的地方,不是“AI 生成了一堆代码”,而是模型通过工具调用真正操作了项目文件,整个过程可控、可审计、可以被 Java 代码接管。适合正在做 AI 应用、想把大模型接入业务系统的 Java 工程师,也适合面试前补 Agent 知识的同学。
下面按我实际的测试顺序,从概念、环境、代码、参数排查一直拆到生产化。
1. Spring AI 2.0 的 Agent 到底是什么,先别急着写代码
1.1 Spring AI 从“调模型”到“让模型干活”
Spring AI 是 Java 生态里的大模型应用框架,它把模型接入、Prompt 模板、结构化输出、向量存储、工具调用这些能力统一到了 Spring Boot 的编程模型里。早期大家用 Spring AI,最常用的是 ChatClient,也就是把用户的对话请求发给模型,然后把文本结果拿回来展示。这个阶段的模型只是“会聊天”,它不能碰真实业务数据,也不能操作文件系统,更不会主动决定下一步要做什么。
到了 2.0,Agent 相关能力被放到了更核心的位置。这里说的 Agent 不是玄学,也没有那么神秘。它的核心循环就是:模型在生成回复的时候,不仅输出文字,还可以输出“我要调用哪个工具、传什么参数”的指令。你的 Java 代码收到这些指令后,去执行真正的文件读写、接口请求或数据库操作,再把执行结果放回给模型,让模型继续推理。这个“模型决策 -> 工具执行 -> 结果回传 -> 再次推理”的循环,是 Agent 的最小单元。
所以 Spring AI 2.0 对 Java 后端最大的意义,是 Agent 开发从“框架帮你封装”变成了“你也能理解、能控制、能扩展”的工程能力。代码生成助手只是其中一个典型场景。
1.2 为什么需要 Agent 来写代码
普通 Chat 模式处理代码任务,会立刻遇到一个现实问题:项目文件太多,上下文塞不下。你不可能把一个完整的 Spring Boot 项目粘贴给模型,token 限制不允许,噪声也太大。
Agent 模式解决问题的思路完全不同。模型不需要一次看到全部文件,它可以先看项目结构,再按需读取指定文件,生成新文件后写入磁盘。整个过程由模型决定下一步做什么,Java 代码只负责安全执行。这个体验和 Claude Code 很像:你在终端里说“帮我加一个登录接口”,它会自己去查看 Controller、Service、Mapper,然后修改相关文件。
但这种能力不是白来的。要落地成 Java 版代码生成助手,你需要自己做三件事:定义模型能调用的工具、维护多轮对话状态、把模型返回的工具调用指令安全地执行出来。这三件事都不难,但每一件都有坑。
1.3 Agent 的边界:不是所有任务都需要 Agent
这里要给新手提个醒。不要一上来就把所有功能都做成 Agent。如果任务只是“翻译一句话”,或者“总结一段文本”,用 ChatClient 就够了,引入工具调用反而增加延迟和失败率。
Agent 适合的任务有三个特征:多步骤、依赖当前环境、需要真实执行操作。代码生成助手完全符合,因为它必须感知项目结构,必须调用文件工具。如果你只是做一个客服问答机器人,不查数据库、不调用文件、不改配置,那还不需要上 Agent。
我的建议是:先跑通一个最小闭环,再谈扩展。下面这部分,我会按“环境准备 -> 工具定义 -> 对话循环 -> 参数调优”的顺序来拆。
2. 开发环境与工程初始化:先把最小工程跑起来
2.1 环境准备:JDK、构建工具和模型 API
代码生成助手是标准 Java 后端项目。我建议环境如下:
- JDK 17 或更高,Spring Boot 3.x 官方支持 JDK 17;
- Maven 3.8+ 或 Gradle 8.x,按习惯选;
- 一个可以调用的大模型 API,优先选 OpenAI 兼容接口;
- 本机内存至少 8G,16G 会更舒服。
为什么内存不能太低?因为 Spring Boot 本身要占一部分,Agent 请求高并发时,模型响应和文件内容都会在内存里做缓冲。如果只有 4G 内存,跑单条任务可能没事,但连续跑几条就会碰到java: outofmemoryerror: insufficient memory这类问题。这个报错后面会单独说。
模型 API 这块,可以用 OpenAI 官方地址,也可以用国内服务商提供的 OpenAI 兼容接口,比如 DeepSeek、通义等。只要 base-url 和 api-key 能对上,Spring AI 的 OpenAI Starter 通常都能兼容。如果不想依赖公网模型服务,本地 Ollama 也可以作为备选,但代码生成类任务对模型能力要求不低,建议至少用一个中大规模模型。
2.2 创建 Spring Boot 工程
我一般不会从零手写配置,直接用 Spring Initializr 生成工程。语言选 Java,构建工具选 Maven,Spring Boot 版本选 3.x,依赖先加 Web 和 Lombok。Spring AI 相关依赖会在下一步手动加。
初始工程结构不需要复杂,一个入口类、一个配置类、一个 Agent 服务类就够了。记住一个原则:先不要让程序启动即加载一堆 Agent 逻辑,目标是把工程跑起来,然后能调用模型,最后才加工具。
生成后,在application.yml里预留配置项。模型 key 不要写死在文件里,用环境变量注入,这样即使代码不小心提交到仓库,也不会泄露密钥。
2.3 引入 Spring AI 2.0 依赖
Spring AI 目前还处于快速迭代阶段,不同小版本的 API 可能有变化。所以不要记死某个坐标,而是用一个 BOM 统一管理版本。
Maven 里可以这样配:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后在 dependencies 里加:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>Spring Initializr 如果在生成时选了 Spring AI,它会自动帮你配好仓库。手动加依赖时,记得检查是否包含 Spring 的 Release 或 Milestone 仓库,例如:
<repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> </repositories>这里最容易踩的坑是:依赖加完启动报找不到类,大多数时候是 BOM 版本和 Spring Boot 版本不匹配。解决方法是先跑一个样例接口,确认 ChatClient 能注入,再写 Agent 逻辑。
2.4 配置模型客户端
配置文件比想象中简单。最简配置如下:
spring: ai: openai: base-url: ${AI_BASE_URL:https://api.openai.com} api-key: ${AI_API_KEY} chat: options: model: ${AI_MODEL:gpt-4o-mini} temperature: 0.3如果你用的是 DeepSeek,可以把base-url换成https://api.deepseek.com,模型名换成deepseek-chat。如果你的服务商要求额外请求头,再按官方文档补充。Spring AI 的 OpenAI Starter 整体是兼容这套协议的。
这里有一个经验:第一次启动时,不要急着写复杂提示词。先注入ChatClient,调用一次prompt().user("你好").call().content(),能返回内容就说明配置没问题。这样你能把模型问题、依赖问题、项目代码问题区分开。
3. 从零实现一个类似 Claude Code 的代码生成助手
3.1 先拆解 Claude Code 的核心工作流
要手写一个类似 Claude Code 的助手,先得定义清楚它要做什么。
我理解的核心流程是:
- 用户输入一个需求,比如“给项目加一个获取用户列表的接口”;
- Agent 读取项目目录结构,了解项目语言和结构;
- Agent 读取相关文件,比如 Controller、Service、Mapper;
- Agent 设计改动方案,调用写文件工具生成或修改代码;
- 助手向用户返回执行结果。
这个流程里的每一步,模型都说了算。Java 代码只负责提供工具和循环。Spring AI 的工具调用能力,正好能用上。
在设计上,我会先定义三个最小工具:listProjectStructure、readFile、writeFile。后续有需要再加runCommand,但不建议默认开启,因为让模型执行任意命令的风险太高。
3.2 用 @Tool 定义文件操作能力
Spring AI 里最常见的工具定义方式是用@Tool注解标记一个 Bean 方法。为了演示,我定义一个ProjectTools组件,并把项目根目录限定在一个固定目录下。
示例代码如下:
@Component public class ProjectTools { private final Path projectRoot; public ProjectTools(@Value("${agent.project-root:./demo-project}") String root) { this.projectRoot = Path.of(root).toAbsolutePath().normalize(); } @Tool("读取项目目录结构,返回目录树") public String listProjectStructure() throws IOException { try (Stream<Path> paths = Files.walk(projectRoot)) { return paths .map(p -> projectRoot.relativize(p).toString()) .limit(500) .collect(Collectors.joining("\n")); } } @Tool("读取指定文件内容,path 是相对项目根目录的路径") public String readFile(String path) throws IOException { Path p = resolveSafe(path); return Files.readString(p); } @Tool("将 content 写入指定文件,path 是相对项目根目录的路径,会覆盖已存在文件") public String writeFile(String path, String content) throws IOException { Path p = resolveSafe(path); Files.createDirectories(p.getParent()); Files.writeString(p, content, StandardCharsets.UTF_8); return "success"; } private Path resolveSafe(String path) throws IOException { Path p = projectRoot.resolve(path).normalize(); if (!p.startsWith(projectRoot)) { throw new IOException("非法路径:" + path); } return p; } }这段代码有一个关键细节:resolveSafe方法强制把路径规范化后再判断是否在项目根目录内。没有这层保护,模型可能会读出系统文件,也可能往任意目录写文件。生产环境还建议先判断 path 是不是绝对路径,再走后续校验。
3.3 维护对话循环,把工具结果喂回模型
有工具还不够,关键是把模型的工具调用和 Java 工具执行串起来。Spring AI 的具体 API 在不同版本里会有变化,但核心循环是稳定的:
用户消息 -> 模型 -> 如果有工具调用 -> 执行工具 -> 返回结果给模型 -> 模型继续 -> 直到没有工具调用,输出最终文本如果框架没有自动处理,你需要手动维护一个消息列表。伪代码如下:
List<Message> messages = new ArrayList<>(); messages.add(new UserMessage(userPrompt)); for (int i = 0; i < maxIterations; i++) { ChatResponse response = chatModel.call(new Prompt(messages)); AssistantMessage assistantMessage = response.getResult().getOutput(); messages.add(assistantMessage); if (assistantMessage.hasToolCalls()) { for (ToolCall toolCall : assistantMessage.getToolCalls()) { Object result = toolExecutor.execute(toolCall.name(), toolCall.arguments()); messages.add(new ToolResponseMessage(toolCall.id(), result)); } } else { return assistantMessage.getText(); } } throw new RuntimeException("Agent 超出最大迭代次数");这段代码不是某个版本的精确 API,但理解了它,你就能看懂官方封装的自动执行逻辑。maxIterations建议设为 5 到 10,避免模型陷入死循环。如果模型反复调用同一个工具,日志里会非常明显。
3.4 把模型输出转换为文件变更
模型最终输出有两种情况:一是通过工具完成了文件写入,二是只输出建议代码,没有实际落盘。对于代码生成助手,我们更希望它真正落盘。
为了让写文件更安全,我建议在writeFile前增加一个确认回调;如果是团队内部使用,可以先让模型直接写,再在业务层记录日志。每个写操作至少包含:文件路径、操作人、会话 ID、时间、变更前后摘要。这样即使模型生成错误代码,也能追踪到是哪一轮对话触发的。
另一个经验是:写文件时一定要用 UTF-8,并且不要用默认本地编码。Windows 机器默认可能是 GBK,编码不对会导致生成的 Java 类注释或中文字符串变成乱码。
4. 让助手真正可用:上下文、项目感知和路径安全
4.1 会话历史与上下文窗口
Agent 与平时一问一答有一个显著差异:它需要记住自己做过什么。否则一个多步骤任务,模型读完了文件,下一步就忘了。
最简单的做法是用一个List<Message>保存当前会话消息,每次请求都带上。在 Spring Boot 项目里,可以用ConcurrentHashMap<String, List<Message>>按会话 ID 存储。注意线程安全,不要再为每个请求创建一个只读的局部变量。
但会话历史不能无限增长。模型上下文窗口有限,文件内容、目录结构都会占用 token。我在实测时发现,连续读取三四个大文件后,模型就开始截断或忽略之前的指令。解决办法有几个:
- 超大文件只读取关键片段,比如方法签名和实体定义;
- 让模型先读目录,再决定读哪个文件;
- 定期对旧消息做摘要,把摘要放到上下文里。
没有统一标准,但原则是:上下文里放“够用的信息”,而不是“全部信息”。
4.2 项目结构感知:先给一张地图
Claude Code 给人感觉很聪明,一个很重要的原因是它知道整个项目长什么样。对应到我们的实现,就是给模型一个listProjectStructure工具。
第一次进入会话时,可以自动调用这个工具,把目录树返回给模型。目录树不要无限展开,忽略target、node_modules、.git这类目录,否则模型会被噪声干扰。
示例实现里,我用Files.walk并limit(500),这是一种粗暴但有效的限制。生产环境可以改成读取.gitignore,把忽略规则也应用到 Agent 的目录遍历上。这样的话,模型看到的项目结构和程序员日常看到的保持一致。
4.3 路径安全与写文件防护
这是整个项目里最重要的边界。模型不是人,它可能因为理解错误,生成一个../../etc的路径,也可能写文件时把整个文件清空。
我推荐的防护方案有三层。
第一层,路径归一化校验。所有相对路径都要经过resolve().normalize(),然后判断是否以项目根目录开头。不是就拒绝。
第二层,写文件前进行内容检查。如果模型返回的content为空,或者文件原本很大但新内容很小,要触发警告。可以在模型写入前做一个 diff,超过某个变化比例就暂停。
第三层,文件备份。正式环境可以在写入前把原文件复制到.backup目录。这个成本不高,但能救回很多“AI 误操作”。
4.4 失败边界与降级策略
Agent 不是百分百可靠。常见失败有几种:模型返回格式错误、工具调用参数缺少字段、文件写入失败、模型超时。每类失败都要有对应的处理方式。
工具调用参数错误时,不要直接把异常抛给用户。更好的做法是把异常信息封装成一条工具结果,回传给模型,让模型修正参数后重试。比如readFile抛非法路径,就返回“路径必须在项目根目录内”。
模型超时时,如果是一次性任务,可以设置业务级别超时并返回“请稍后再试”。如果是代码生成任务,保留已经执行的文件操作,不要回滚全部,保证原子性的代价太高。这里的原则是:Agent 应该有能力告诉用户“我执行到了哪一步、哪些成功了、哪些失败了”,而不是只扔一个堆栈异常。
5. 参数调优和问题排查:从能跑到好用
5.1 参数取舍:temperature、maxTokens、timeout
代码生成任务与闲聊任务不同,对确定性要求更高。temperature太高,模型会生成风格飘忽不定的代码,有时甚至会凭空发明不存在的 API。我建议:
temperature:0.2 到 0.4 之间;maxTokens:按生成代码的规模调整,2000 到 8000 都算正常;timeout:默认值可能不够,Agent 要连续调用多个工具,单次请求 30 秒以上很常见,把超时配置放大到 60 秒或 120 秒;topP:保持默认或在 0.9 左右,不要和 temperature 同时拉满。
这些参数没有绝对标准。实际调参时,用同一个 Prompt 和同一个小任务,修改一个参数跑三轮,观察输出变化,才能找到适合你模型的组合。
5.2 验证助手是否“真会干活”
我建议按下面三个级别验证。
第一级,模型能正常聊天。在同一个工程里,先调用 ChatClient,确认模型连接没问题。
第二级,模型能调用单个工具。比如让模型调用listProjectStructure,然后看返回的目录结构是否被模型理解。
第三级,完整任务。给它一个真实需求,比如“读取UserController.java,新增一个GET /users/{id}接口”,然后检查文件是否真的发生变更。
不要一上来就让它改整个项目。先用一个小模块跑通,再逐步加大范围。这样出了问题,你知道该排查模型、工具,还是工作流。
5.3 常见报错与排查顺序
我在实际测试中遇到过几类高频问题,这里给你一个排查顺序。
- 模型不调用工具。先看系统提示词里是否说清楚“你有这些工具可以调用”,再看工具描述是否足够具体。描述里必须有“做什么、参数是什么、何时用”。
- 工具调用后模型突然不继续了。检查工具结果是不是太长,可能把上下文塞满了。简化返回内容。
- 报错
the agent execution provider did not respond in time. this may indicate the...。先看模型服务端是否过载,再看超时配置。注意 Agent 执行可能包含多轮工具调用,总耗时比单次请求更长。 - 报错
java: outofmemoryerror: insufficient memory。先加大 JVM 堆内存,比如-Xmx2g,再降低并发量。读大文件时,避免一次性把整个文件内容装进消息列表。 - 模型返回 529。这是模型服务端限流,设置指数退避重试,同时把并发压下来。
- 写出的文件乱码。检查文件编码是否 UTF-8,尤其是 Windows 环境。
这些报错不全是框架问题。我的经验是:先看日志里的工具调用记录,再去看模型返回,最后才改代码。
5.4 资源占用监控
Agent 应用和后端接口不一样,它可能一次性占用较长连接和内存。启动时加两个 JVM 参数会有帮助:
java -Xms512m -Xmx2g -jar code-agent.jar同时观察三件事:并发数、平均任务耗时、内存增长曲线。如果内存持续上升,优先怀疑会话历史没有清理,文件工具返回了大量内容并长期留在消息列表里。比较稳妥的做法是给每个会话设置最大消息数,超过之后自动丢弃最旧的消息。
6. 从 Demo 到生产:接口化、并发和日志
6.1 把助手包装成 REST API
代码生成助手不能只在 IDE 里跑本地方法,给它一个 HTTP 入口,才能被其他系统复用。我一般会暴露一个简单接口:
@RestController @RequestMapping("/api/agent") public class AgentController { private final CodeAgent codeAgent; public AgentController(CodeAgent codeAgent) { this.codeAgent = codeAgent; } @PostMapping("/run") public AgentResult run(@RequestBody AgentRequest request) { return codeAgent.run(request.sessionId(), request.message()); } }AgentRequest至少包含两个字段:sessionId和 `