基于Spring AI 2.0的Java代码生成助手:Agent实战解析
发布时间:2026/8/29 9:51:07 作者:尧图编辑部 阅读量:1,286

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.0Agent 相关能力被放到了更核心的位置。这里说的 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 17Maven 3.8 或 Gradle 8.x按习惯选一个可以调用的大模型 API优先选 OpenAI 兼容接口本机内存至少 8G16G 会更舒服。为什么内存不能太低因为 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构建工具选 MavenSpring 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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在 dependencies 里加dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependencySpring Initializr 如果在生成时选了 Spring AI它会自动帮你配好仓库。手动加依赖时记得检查是否包含 Spring 的 Release 或 Milestone 仓库例如repositories repository idspring-milestones/id urlhttps://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、MapperAgent 设计改动方案调用写文件工具生成或修改代码助手向用户返回执行结果。这个流程里的每一步模型都说了算。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 (StreamPath 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 在不同版本里会有变化但核心循环是稳定的用户消息 - 模型 - 如果有工具调用 - 执行工具 - 返回结果给模型 - 模型继续 - 直到没有工具调用输出最终文本如果框架没有自动处理你需要手动维护一个消息列表。伪代码如下ListMessage 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 与平时一问一答有一个显著差异它需要记住自己做过什么。否则一个多步骤任务模型读完了文件下一步就忘了。最简单的做法是用一个ListMessage保存当前会话消息每次请求都带上。在 Spring Boot 项目里可以用ConcurrentHashMapString, ListMessage按会话 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。我建议temperature0.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和