尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring AI 2.0 构建企业级编码代理:从工具调用到 Agent Utils

Spring AI 2.0 构建企业级编码代理:从工具调用到 Agent Utils Spring AI 2.0 的 Agent 能力加上自建的 Agent Utils 工具集可以复刻 Claude Code 这类编码代理的核心工作方式给定一个需求Agent 自己规划步骤、调用工具、读取项目文件、生成代码并验证结果。Claude Code 之所以在开发者圈子中流行不完全是模型本身强而是它把 CLI、VSCode 插件、桌面客户端背后的工具调用链路打通了。对 Java 团队来说想在企业内部做一套类似的编码代理不一定非要依赖某一个商业客户端完全可以用 Spring AI 2.0 作为底座把工具注册、任务规划、上下文管理、审计日志这些能力沉淀成自己的 Agent Utils。这篇文章会从概念、环境、最小实现、验证、排错到企业级落地完整走一遍。1. 理解 Spring AI 2.0 的 Agent 实现基础模型、工具与循环1.1 编码 Agent 的最小工作闭环编码 Agent 和普通聊天机器人最大的区别是 Agent 必须能自己决定“下一步做什么”。普通聊天模型只会基于已有上下文生成回答而编码 Agent 会先判断自己缺少什么信息然后调用工具获取这些信息再把工具结果交给模型继续推理。一个最小闭环可以拆成五步用户输入目标例如“统计项目中所有 Controller 暴露的接口路径”。模型判断当前掌握的信息不足决定调用list_files或read_file。Agent 框架执行工具把文件列表或文件内容作为结构化结果返回。模型读取工具结果继续生成下一步决策或最终回答。当模型认为无需再调用工具时输出最终答案。这个循环和 Claude Code 内部运行的 agent loop 几乎一致。Claude Code 之所以能完成“读代码、改代码、跑测试、提交记录”这类复杂任务不是因为模型一次生成了正确答案而是因为它能反复使用终端工具把每一步的执行结果反馈给模型直到目标完成。如果只搭了一个 ChatClient 而没有工具调用本质上还是一个聊天接口。工具调用是编码 Agent 的分水岭。1.2 Spring AI 2.0 在 Agent 链路中承担什么职责Spring AI 2.0 并不是一个完整的 Agent 框架它提供的是构建 Agent 所需要的底层组件。把这些组件理解清楚后面写 Agent Utils 时才知道哪些要自己封装哪些可以直接用。组件作用在编码代理中的用途ChatClient统一模型调用入口支持 prompt、messages、tools、advisors处理用户请求自动或手动执行工具调用Tool / ToolCallback把 Java 方法暴露成模型可调用的函数将文件读取、命令执行、日志查询变成工具ChatMemory维护多轮对话上下文保存 Agent 与用户的历史会话Advisor对调用链做横切处理做日志、审核、上下文压缩、限流ChatModel底层模型抽象手动控制 agent loop做每个 step 的审计在企业级项目中组件之间不能散落。你需要在它们之上再封装一层 Agent Utils让工具注册、任务规划、记忆管理、审计日志有统一的入口。使用 Spring AI 2.0 时有一个容易被忽略的点如果只是用ChatClient.call()框架会在内部自动处理工具调用循环。但如果是复杂编码任务推荐至少在关键节点记录“模型请求了什么工具、工具返回了什么、决策依据是什么”。自动循环虽然省事但出了问题之后很难复盘。1.3 Claude Code、Codex 和 Spring AI 编码代理的形态差异从 Claude Code 的安装、VSCode 配置、桌面版、Skills 这些讨论中能看出开发者关心的不只是“模型多强”更关心“这个工具怎么接入我的项目”。Claude Code 以 CLI、VSCode 插件、桌面客户端三种形态出现而 Spring AI 的编码代理天然可以做成后端服务再暴露成 REST、CLI、IDE 插件或内部平台能力。维度Claude CodeCodexSpring AI Agent Utils运行形态CLI / VSCode / DesktopCLI / IDE后端服务 / CLI / REST / 嵌入业务系统工具来源官方内置 Skills官方内置Java 方法任意注册可加权限技能扩展配置 skill 文件配置 prompt / actionTool Advisor 提示词模板企业集成偏向个人开发者偏向个人和团队可与 Java 中间件、账号、审计无缝集成用 Spring AI 实现编码代理不是要替代 Claude Code而是要把类似的工具链路拿回自己手里。比如企业内部的代码仓库规范、CI 流程、缺陷单系统都可以通过自定义工具接入 Agent。这样 Agent 不只是一个“会聊代码的模型”而是一个“能操作企业工程体系的执行器”。2. 准备企业级编码代理的工程骨架依赖、配置与目录2.1 环境要求与版本确认开始写代码前先确认环境。Spring AI 2.0 的 API 还处于演进状态不同小版本的类名和构造方法可能有变化。落地前要先确认当前使用的 Spring AI 版本与 Spring Boot 版本的兼容关系不要盲目照着旧教程升级。环境项建议要求说明JDK17 及以上Spring Boot 3.x 的基础要求构建工具Maven 3.9 或 Gradle 8.x建议用 Maven教程示例基于 MavenSpring Boot与 Spring AI 2.0 兼容的 3.x 版本以 Spring AI 官方兼容矩阵为准Spring AI2.0 系列 BOM统一管理模型 starter 版本IDEIntelliJ IDEA 或 VSCode编码调试必用但不是运行前提实际项目中推荐先在本地创建一个最小工程把 Spring AI 依赖跑通后再往上加工具和 Agent 逻辑。这样出现问题容易定位不会出现“代码太多不知道是哪一层导致启动失败”的情况。2.2 引入 Maven 依赖下面是一个最小工程需要的pom.xml依赖片段。Spring AI 通过 BOM 统一管理版本这样后续引入多个模型 starter 时不会出现版本冲突。dependencyManagement dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version2.0.0-SNAPSHOT/version typepom/type scopeimport/scope /dependency /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency /dependencies这里用spring-ai-starter-model-openai是因为很多企业模型服务提供 OpenAI 兼容协议接口。如果你要直接调用 Claude 原生接口可以把依赖换成spring-ai-starter-model-anthropic。选择 OpenAPI 兼容模型的好处是切换模型服务商时只需要改配置不用大改代码。2.3 配置模型端点、API Key 与模型名称模型配置不要写死在application.yml里。API Key 和 Base URL 应该通过环境变量注入避免提交到 Git 仓库后泄密。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.2如果公司内部模型服务兼容 OpenAI 协议只需要设置export AI_BASE_URLhttp://your-model-server.example.com/v1 export AI_API_KEYyour-api-key export AI_MODELyour-model-name这里有一个常见坑模型名称必须和服务商实际提供的名称完全一致。有些服务商对外文档写一个名字实际接口返回的又是另一个名字一旦不一致Spring AI 会报模型不存在或 404。遇到这类问题时先拿一个最原始的 HTTP 客户端直接请求模型服务确认可用模型列表再回填到配置里。2.4 项目结构与一个能启动的空应用建议按编码代理的职责分包而不是把所有类堆在一个包下。src/main/java/com/example/agent/ ├── AgentApplication.java ├── config/ │ └── AgentConfig.java ├── controller/ │ └── AgentController.java ├── agent/ │ ├── AgentRunner.java │ ├── AgentRequest.java │ ├── AgentResult.java │ └── ToolRegistry.java ├── memory/ │ └── MemoryManager.java └── toolkit/ └── ProjectToolkit.java启动类就是一个普通 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); } }此时先不写 Agent 逻辑直接运行mvn spring-boot:run。如果应用能正常启动说明 Spring AI 2.0 相关 starter 已被正确加载。之后再逐步加入工具和 Agent 调度逻辑。3. 实现 Agent Utils 核心上下文、工具注册、规划和记忆3.1 为什么需要 Agent Utils直接在 Controller 里调用 ChatClient 也能跑通一个 Demo但进入复杂编码任务后会立刻遇到几个问题工具数量变多后模型不知道该用哪个工具需要给工具写清晰描述并统一管理。每个请求的上下文必须隔离不能让用户的 A 任务读到用户 B 的对话历史。工具执行必须有审计记录至少要知道模型调了哪些工具、传了什么参数、执行是否成功。复杂任务不能只靠模型自由发挥需要先规划步骤再按步骤执行。Agent Utils 在这个项目里不是某个第三方官方库而是把上面这些问题统一封装的一组工具类。后面代码中的agent包就是这套 Agent Utils 的实现。3.2 请求与结果模型先定义 Agent 的输入和输出。输入不能只是一个字符串还要带上会话 ID、模型参数、最大步数等信息。package com.example.agent; import java.util.Map; public class AgentRequest { private String sessionId; private String userMessage; private MapString, Object metadata; public String getSessionId() { return sessionId; } public String getUserMessage() { return userMessage; } public MapString, Object getMetadata() { return metadata; } // 构造函数和 Builder 省略实际项目中建议使用 Builder 模式 }输出结果需要包含最终回答、实际调用过的工具列表以及每一步的执行标记。package com.example.agent; import java.util.ArrayList; import java.util.List; public class AgentResult { private String finalAnswer; private ListString toolCalls new ArrayList(); public AgentResult(String finalAnswer) { this.finalAnswer finalAnswer; } public void addToolCall(String toolCall) { this.toolCalls.add(toolCall); } public String getFinalAnswer() { return finalAnswer; } public ListString getToolCalls() { return toolCalls; } }这里记录toolCalls不是为了展示而是为了审计。生产环境中应该把工具调用明细异步写入日志系统或审计表。3.3 工具注册表 ToolRegistrySpring AI 本身有 ToolCallback 机制但实际项目中还需要一个工具注册表用来统一登记可用工具、做权限校验、记录执行结果。package com.example.agent; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbacks; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ToolRegistry { private final MapString, ToolCallback callbacks new ConcurrentHashMap(); private final ListString deniedPrefixes List.of(/etc, /root, C:\\Windows); public void register(Object toolObject) { ToolCallback[] toolCallbacks ToolCallbacks.from(toolObject); for (ToolCallback callback : toolCallbacks) { callbacks.put(callback.getToolDefinition().name(), callback); } } public ToolCallback get(String name) { return callbacks.get(name); } public boolean isDenied(String name) { return deniedPrefixes.stream().anyMatch(name::startsWith); } public ListString toolNames() { return List.copyOf(callbacks.keySet()); } }ToolCallbacks.from(Object)可以扫描带有Tool注解的方法并生成对应的 ToolCallback。工具注册表还承担了一个职责防止敏感工具被调用。现实项目中可能有delete_file、release_apply这类高风险工具这些工具不应该注册给所有用户。3.4 任务规划 TaskPlanner复杂编码任务不能只靠模型逐步试错。更可控的方式是先让模型生成任务列表Agent 再按列表逐步执行。下面是一个TaskPlanner的示意实现。package com.example.agent; public class TaskPlanner { public ListString plan(ChatClient chatClient, String goal) { String prompt 你是一个编码任务规划器。 你的目标是把用户需求拆解成可执行的步骤。 只输出步骤列表每行一个步骤不要有多余解释。 步骤要尽量具体例如“查找配置文件并确认数据源地址”。 用户需求 %s .formatted(goal); String content chatClient.prompt() .user(prompt) .call() .content(); return content.lines() .map(String::trim) .filter(line - line.matches(^\\d\\..*)) .toList(); } }这里特意要求模型只输出编号步骤方便解析。你不要让模型自由发挥输出 JSON因为 JSON 解析失败是 Agent 开发里最常见的问题之一。后期可以换成更严格的 JSON Schema 校验但最小闭环先保持简单。3.5 会话记忆 MemoryManagerAgent 每轮请求都需要知道前文发生了什么。Spring AI 提供 ChatMemory但企业项目里通常还要结合业务上下文比如当前用户在哪个项目目录、角色是什么、能访问哪些仓库。package com.example.memory; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.messages.Message; import java.util.List; public class MemoryManager { private final ChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build(); public ListMessage history(String sessionId) { return chatMemory.get(sessionId, 20); } public void add(String sessionId, Message message) { chatMemory.add(sessionId, message); } public void clear(String sessionId) { chatMemory.clear(sessionId); } }MessageWindowChatMemory只保留最近的 N 条消息避免上下文无限膨胀。对编码代理来说20 条往往不够因为一次工具调用可能产生很长的输出。生产环境更推荐把历史消息和工具输出同时做截断或者在超过上下文窗口时丢弃中间步骤只保留结论。4. 手写一个类 Claude Code 的编码代理工具与提示词4.1 定义编码工具集为了让 Agent 能操作项目文件定义一个ProjectToolkit。其中每个方法都用Tool声明description写得越清楚模型就越知道什么时候调用。package com.example.toolkit; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; 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.util.stream.Stream; Component public class ProjectToolkit { private final Path baseDir Path.of(System.getProperty(user.dir)); Tool(description 读取项目内的文本文件path 是相对项目根目录的文件路径) public String read_file( ToolParam(description 文件相对路径) String path) throws IOException { Path target baseDir.resolve(path).normalize(); if (!target.startsWith(baseDir)) { return ERROR: 路径越界不允许读取项目根目录之外的文件; } if (!Files.exists(target)) { return ERROR: 文件不存在; } return Files.readString(target, StandardCharsets.UTF_8); } Tool(description 列出项目目录下的文件dir 是相对项目根目录的目录路径) public String list_files( ToolParam(description 目录相对路径) String dir, ToolParam(description 最大递归深度默认 2) int depth) throws IOException { Path target baseDir.resolve(dir).normalize(); if (!target.startsWith(baseDir) || !Files.isDirectory(target)) { return ERROR: 目录不存在或路径越界; } try (StreamPath paths Files.walk(target, Math.max(1, Math.min(depth, 3)))) { return paths .map(baseDir::relativize) .map(Path::toString) .reduce((a, b) - a \n b) .orElse(); } } }这份代码的关键点是路径安全。resolve和normalize可以处理../这类相对路径startsWith(baseDir)保证不能读取项目目录之外的内容。现实项目中还要考虑符号链接绕过、隐藏文件、Windows 路径分隔符等问题但最小 Demo 已经体现了思路。4.2 把工具绑定到 ChatClient在AgentConfig中创建ChatClientBean并把ProjectToolkit作为工具集绑定进去。package com.example.config; import com.example.toolkit.ProjectToolkit; 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 agentChatClient(ChatClient.Builder builder, ProjectToolkit toolkit) { return builder .defaultSystem( 你是一个企业内部编码助手。 你可以读取项目文件、列表目录、分析代码结构。 回答要简洁、可执行不要编造项目里不存在的文件。 如果信息不足先使用工具收集信息再回答。 ) .defaultTools(toolkit) .build(); } }defaultTools(toolkit)会扫描ProjectToolkit里所有带Tool注解的方法并自动注册到模型请求中。模型判断需要调用工具时Spring AI 会自动执行 Java 方法然后把结果回传给模型使用者不需要手动处理循环。如果你的版本中defaultTools行为不同也可以改用ToolCallbackProvider或MethodToolCallbackProvider核心思路是一样的。4.3 设计系统提示词系统提示词是 Agent Utils 里最容易低估的部分。Claude Code 的 Skills 机制本质上就是“可复用的提示词 工具集”。系统提示词内容决定了模型遇到什么情况会做决策。设计原则有三条明确工具边界告诉模型哪些用工具哪些不要用工具。明确输出风格代码代理的输出要贴近工程文档而不是散文。明确失败策略信息不足时先收集不要直接编造。下面是一个可以放进defaultSystem的提示词模板你是企业内部的项目分析 Agent可以读取项目文件、列出目录也能执行项目构建命令。 约束 1. 当用户询问代码逻辑、接口路径、配置内容时必须先读取文件再回答。 2. 不允许修改项目文件本文是只读模式。 3. 回答必须给出依据注明引用了哪个文件。 4. 如果工具返回错误如实说明错误不要假设文件存在。这个提示词虽然短但对模型行为的影响很明显。没有“先收集信息再回答”这条约束时模型很可能会根据训练记忆瞎猜项目里的类名。4.4 暴露 REST API编码代理需要有一个入口。最简单的方式是暴露一个 POST 接口。package com.example.controller; import com.example.agent.AgentRequest; import com.example.agent.AgentResult; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/agent) public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/run) public AgentResult run(RequestBody AgentRequest request) { String answer chatClient.prompt() .user(request.getUserMessage()) .call() .content(); AgentResult result new AgentResult(answer); return result; } }这个接口只完成了最小闭环。实际项目中还要加入 sessionId 隔离、历史消息回放、审计日志、工具调用记录等功能。你可以把AgentRunner封装在 Controller 和 ChatClient 之间而不是在 Controller 里直接处理所有逻辑。5. 运行验证从一次需求到一次工具调用闭环5.1 启动应用用开发模式启动mvn spring-boot:run启动日志里如果出现 Spring AI 相关的自动配置信息说明模型 starter 加载成功。5.2 发起一轮任务启动成功后调用 REST 接口curl -X POST http://localhost:8080/api/agent/run \ -H Content-Type: application/json \ -d { sessionId: demo-001, userMessage: 项目根目录下有哪些 Controller 文件请逐个读取并汇总它们暴露的接口路径。 }这是最直观的验证方式。如果模型配置正确它会调用list_files找到项目结构再调用read_file读取 Controller最终返回汇总结果。5.3 观察日志与工具调用链路Spring AI 默认会输出工具调用相关的日志。你能在启动日志或控制台看到类似这样的片段Tool call: list_files Tool call args: {dir: src/main/java, depth: 2} Tool response: src/main/java/com/example/controller/AgentController.java如果这些日志没有出现说明模型没有触发工具调用。可能的原因有三个工具描述不够清楚、模型参数把调用工具关闭了、请求中没有正确携带工具定义。5.4 预期输出示例一次成功调用后的 AgentResult 响应可能如下{ finalAnswer: 项目根目录下共有 1 个 Controller\n\n1. AgentController: POST /api/agent/run\n\n依据src/main/java/com/example/controller/AgentController.java, toolCalls: [] }注意这里toolCalls数组还是空的因为当前实现没有把工具调用记录写入结果。要看到工具调用链需要在AgentRunner里增加审计逻辑或者直接看 Spring AI 的日志。验证点可以用一个表格来对照验证点操作预期结果模型连接启动应用无 401、403、超时错误工具发现查看启动日志能找到list_files、read_file工具执行调用 REST 接口日志中出现 Tool call 记录回答准确性检查返回的接口路径路径与真实项目一致没有编造6. 常见问题排查工具失效、上下文超载、模型响应异常6.1 排查优先级遇到问题不要先怀疑模型不行。按下面顺序排查配置是否生效Base URL、API Key、模型名是否正确。工具是否注册启动日志中是否出现工具定义。提示词是否阻碍工具调用模型可能因为提示词太强而放弃调用工具。请求是否超过上下文窗口工具返回太长会触发截断。版本是否匹配Spring AI 2.0 API 是否与代码一致。6.2 工具调用未触发现象常见原因处理建议模型直接回答不调用工具工具描述不清晰模型不知道工具适用场景调整 Tool description增加触发条件模型想调用工具但报错找不到工具对象没有绑定到 ChatClient检查 defaultTools 或 ToolCallbackProvider工具执行了但结果没影响最终回答上下文太长模型忽略了工具结果缩短工具返回内容只保留关键信息模型每次都想调用同一个工具工具结果没有给模型足够的终止条件在提示词中要求“拿到信息后直接回答”一个很实用的排查方法把一次请求的工具定义
返回列表