关于标题里那个“Spring AI 2.0 + Agent Utils 开发企业级 Claude Code 项目”,我先给一个明确结论:如果你以为这是讲 Claude Code 的安装命令、快捷键或 VSCode 插件配置,可以直接关掉;但如果你是想通过 Java / Spring AI 技术栈,在企业内部做一个类似 Claude Code 的 AI 编程助手,那下面这套流程值得照着做一遍。
Spring AI 2.0 的价值,是让 Java 项目接入大模型时不用自己维护一堆 HTTP 调用、JSON 解析和流式响应逻辑。Agent Utils 的价值,是把“模型思考、调用工具、观察结果、再次思考”这个 Agent 循环从你手里收走。两个东西组合起来,就能用相对少的代码,完成一个带工具调用、会话管理、审计日志的企业级编程助手核心骨架。把“Claude Code”拆开看,它不是一个黑盒,而是一套可复现的工作流:模型负责理解任务,工具负责操作代码,循环负责持续推进。本文就按这个思路,从零开始一步步落地。
1. 先想清楚:企业级 Claude Code 项目,核心到底是什么
1.1 我们做的不是安装器,而是 Agent 工作流
Claude Code 这类工具,用户看到的只是终端里的一个交互窗口,但真正值钱的不是界面,而是界面后面的 Agent 工作流。
一次典型的编程任务大概是这样的:
- 用户提出需求:找到项目中所有没被引用的公共方法。
- 模型先把需求拆成步骤:扫描目录、读取文件、分析引用关系、给出结论。
- 模型调用工具:读文件、搜索关键词、执行 grep 或代码分析命令。
- 工具返回结果,模型继续分析。
- 反复多轮,直到形成最终答案。
这个循环不是一次问答,而是“计划、执行、观察、再计划”的迭代过程。用 Spring AI 2.0 做企业级项目,核心就是把这条循环接住,并让它稳定、可追踪、可控制。
1.2 企业级和玩具 Demo 的差距在哪里
很多团队都能在半天内跑通一个 ChatGPT 式的问答 Demo,但做企业级项目时,差距会立刻暴露:
- 对话上下文怎么保存,重启后还在不在。
- 多个用户同时用,会话之间会不会串。
- 工具能不能执行,执行权限怎么控制。
- 失败重试、超时、限流怎么做。
- 每个操作能不能追溯到人和时间。
这些都不是模型本身的问题,而是工程问题。Spring AI 2.0 解决了一部分,Agent Utils 又解决一部分,剩下的要靠项目结构和管理能力补上。
下面用一个表格拆开看:
| 维度 | 玩具 Demo | 企业级项目 |
|---|---|---|
| 对话上下文 | 进程内存里存一下 | 数据库持久化,支持恢复 |
| 用户隔离 | 单用户测试 | 多租户/多项目隔离 |
| 工具调用 | 写死一个方法 | 统一注册、权限校验、审计 |
| 失败处理 | 报错就结束 | 重试、降级、日志记录 |
| 性能 | 一次一请求 | 并发控制、队列、限流 |
| 可观测性 | 没有 | 耗时、Token、调用次数、成功率 |
所以,后续每一步都不能只看“能不能跑”,还要看“能不能稳定地跑,能不能出问题后快速定位”。
1.3 技术选型:为什么是 Spring AI 2.0 + Agent Utils
Java 团队做 AI 应用,比较自然的选择是 Spring AI 而不是自己写 SDK。原因很简单:
- 模型切换成本低,Spring AI 把 OpenAI、Anthropic、兼容网关等都封装成了统一接口。
- 和 Spring Boot 集成好,配置、依赖、注入、拦截器都能复用现有体系。
- ChatClient 支持流式、工具调用、Advisor 链,方便加日志和权限。
- Agent Utils 可以把 Agent 循环抽出来,不在业务代码里塞一堆 while 循环。
这里要说明一句:Spring AI 2.0 的版本更新比较快,Agent Utils 在不同版本里的模块名和类名也可能调整。下面所有代码都是演示结构,落地时以你实际引入的版本和官方迁移说明为准。
2. 全景拆解一个类 Claude Code 的 Agent 应该长什么样
2.1 四个核心模块:对话入口、模型调用、工具执行、会话与审计
一个可企业化的编程助手,我习惯拆成四层来看。
第一层是入口。可以是 REST API、命令行、WebSocket,也可以是内部系统的一个页面。入口层只负责收请求和返回结果,不写业务逻辑。
第二层是模型调用。用 Spring AI 的 ChatClient 统一封装,把用户输入加上系统提示词,发给大模型。这一层要处理好流式输出和超时。
第三层是工具执行。模型不能直接操作数据库和文件系统,它需要“请求”工具。工具层负责真正执行读文件、搜索代码、分析依赖等操作,并把结果返回给模型。
第四层是会话与审计。每一次请求属于哪个会话,每一条工具调用是谁触发的,结果是什么,都要留下记录。这一层最容易在 Demo 阶段被忽略,但企业落地时最重要。
四层之间的关系是单向依赖的:入口依赖模型层,模型层依赖工具层,会话审计横跨所有层。
2.2 工具不是越多越好,关键是“权限边界”
很多第一次做 Agent 的团队,会把能想到的工具全部注册进去:读文件、写文件、执行 shell、调 API、发邮件。结果发现问题很多。
工具多,模型就容易被干扰。模型可能调错工具、传错参数,也可能因为工具太多导致上下文过长。更关键的是安全。
我的建议是分几类控制:
- 只读工具:读文件、查目录、搜索代码、查看 Git 状态。可以放开。
- 写操作:修改文件、新增文件。需要明确范围,最好限定在指定目录。
- 危险操作:执行 shell 命令、删除文件、提交代码。必须二次确认,或者干脆不开放。
- 外部系统调用:必须经过 API 网关、鉴权、限流。
在企业级 Claude Code 项目里,Agent 的“能力边界”比“能力范围”更重要。先用最小权限设计工具集,再按业务需要逐步放权,是最稳妥的方式。
2.3 会话和任务的纵向拆分
如果只用“用户说一句,Agent 回一句”的思路,后面代码会越来越乱。我建议一开始就把两个概念分开:
- 会话:用户和 Agent 之间的一段持续交流,包含多轮消息。
- 任务:一次请求中,Agent 从接入开始到最终输出结束的完整处理过程。
会话可以有上下文,任务必须能独立追踪。每个任务应该对应一个 taskId,工具调用、错误日志、Token 消耗都挂在 taskId 下。这样排查问题的时候,就不用在一个巨大的消息表里翻来翻去。
3. 从零搭工程:环境、依赖和最小可运行骨架
3.1 环境准备和检查
开始写代码之前,先确认基础环境。我在本地实测时一般按这个顺序检查:
- JDK:建议 JDK 17 以上,Spring AI 2.0 对高版本 JDK 支持更稳。
- 构建工具:Maven 3.9+ 或 Gradle 8+。
- Spring Boot:版本不要乱选,最好和你引入的 Spring AI 版本官方匹配。版本差距太大会出现类找不到。
- 模型 API Key:一个可用的模型平台账号,或者企业内部模型网关地址。
- 网络:确认应用能访问到模型服务的 base-url。如果走企业网关,直接配置网关心跳。
这里要注意,不要一上来就追最新的快照版本。快照版本可能有新功能,但依赖冲突和类变更概率也高。我的习惯是先选一个已经发布的稳定版本,跑通后再升级。
3.2 创建 Spring Boot 工程并引入依赖
假设项目叫enterprise-code-agent,使用 Maven 管理。
pom.xml 里需要引入 Spring AI BOM,再引入对应的模型 starter。下面的坐标是演示结构,具体版本号以你实际拉取到的为准。
<dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.0-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependency> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-anthropic</artifactId> </dependency> <!-- 如果 Agent Utils 已经拆分成独立模块,以官方坐标为准 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-agent-utils</artifactId> </dependency> </dependencies>如果你的公司使用的是兼容 OpenAI 协议的网关,也可以把 Anthropic starter 换成 OpenAI starter。重要的是必须先确定模型服务协议,再选依赖。
3.3 配置模型客户端
配置放在application.yml里。不要硬编码 API Key,通过环境变量注入。
spring: ai: model: anthropic: api-key: ${ANTHROPIC_API_KEY} model: ${AI_MODEL:}model不要随便填,需要填模型平台真正支持的模型名称。你可以用环境变量AI_MODEL来指定,这样换模型时不需要重新打包。
如果使用兼容 OpenAI 协议的网关,配置类似:
spring: ai: openai: base-url: ${AI_BASE_URL:https://api.example.com} api-key: ${AI_API_KEY} chat: options: model: ${AI_MODEL}这里容易踩的坑是:只改了 base-url,忘了改模型名。很多网关要求传完整模型名,和大模型官方平台的名字不一样,配置好后要先用一个最小请求验证。
3.4 先跑通不带工具的 Agent 对话
不要一开始就接入 Agent Utils 和工具。先写一个最小服务,确认 ChatClient 能正常收到模型返回值。
@Service public class CodeAgentService { private final ChatClient chatClient; public CodeAgentService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }再暴露一个简单接口:
@RestController public class AgentController { private final CodeAgentService codeAgentService; public AgentController(CodeAgentService codeAgentService) { this.codeAgentService = codeAgentService; } @PostMapping("/chat") public String chat(@RequestBody String message) { return codeAgentService.chat(message); } }启动后用 curl 验证:
curl -X POST http://localhost:8080/chat \ -H "Content-Type: text/plain" \ -d "请用一句话介绍 Spring AI"如果这一步返回乱码/报错/超时,先不要动 Agent 配置,优先检查 API Key、模型名、网络连通性。先跑通单轮对话,再往上加复杂度。
4. 接入 Agent Utils:让 Agent 真正开始干活
4.1 Agent Utils 的工作单元:一次思考、一次工具调用、一次观察
Agent Utils 的核心不是帮你创建一个“机器人”,而是把 Agent 循环标准化。每一次循环通常包括:
- 模型根据当前对话历史判断下一步动作。
- 如果模型决定调用工具,Agent 框架会把工具名和参数解析出来。
- 工具实际执行,返回结果。
- 结果重新作为消息内容,交给模型继续判断。
- 直到模型认为任务完成,或达到最大迭代次数。
这个循环的好处是,业务代码不需要自己维护“模型到底该调用哪个方法”的逻辑。你需要做的,是定义好工具,并告诉 Agent 这些工具怎么用。
4.2 定义第一组工具:读文件、查目录、搜索关键字
定义工具的时候,我建议先做三个最常用的只读工具。
第一个是读文件:
@Component public class FileTools { @Tool(description = "读取项目文件内容,path 是相对项目根目录的路径") public String readFile(String path) throws IOException { Path filePath = Path.of(path).toAbsolutePath().normalize(); if (!Files.exists(filePath)) { return "文件不存在: " + path; } return Files.readString(filePath, StandardCharsets.UTF_8); } }第二个是列出目录:
@Tool(description = "列出指定目录下的文件和子目录") public String listDirectory(String path) throws IOException { Path dirPath = Path.of(path).toAbsolutePath().normalize(); if (!Files.isDirectory(dirPath)) { return "目录不存在: " + path; } try (Stream<Path> paths = Files.list(dirPath)) { return paths.map(p -> Files.isDirectory(p) ? p + "/" : p.toString()) .collect(Collectors.joining("\n")); } }第三个是关键字搜索。示例可以先做一个简单实现:
@Tool(description = "在项目文件中搜索关键字,返回包含关键字的文件和行号") public String searchKeyword(String keyword, String path) throws IOException { Path root = Path.of(path).toAbsolutePath().normalize(); StringBuilder result = new StringBuilder(); try (Stream<Path> paths = Files.walk(root)) { paths.filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".java")) .limit(100) .forEach(p -> { try (Stream<String> lines = Files.lines(p, StandardCharsets.UTF_8)) { int[] lineNo = {0}; lines.forEach(line -> { lineNo[0]++; if (line.contains(keyword)) { result.append(p).append(":").append(lineNo[0]) .append(":").append(line).append("\n"); } }); } catch (IOException e) { // ignore } }); } return result.toString().isBlank() ? "未找到关键字" : result.toString(); }代码里最关键的是两点:路径要做标准化,避免模型传../跑到项目外;返回内容不要太长,否则会撑爆上下文。
4.3 将工具注册进 Agent 并验证调用链
把工具注册到 Agent 的方式在每个版本里略有不同。下面是一个演示结构,重点看思路:
AgentExecutor executor = AgentExecutor.builder(chatClient) .tools(fileTools, codeSearchTool) .maxIterations(8) .build(); String result = executor.execute( "分析当前项目,找出所有包含 TODO 的 Java 文件" );这里的AgentExecutor是演示类名,实际落地时以 Agent Utils 提供的核心类为准。只要循环能跑起来,模型就会自动判断“需要读目录”“需要搜索关键字”,然后调用对应的工具方法。
验证调用链是否正常,最好的方式不是直接看最终答案,而是看中间过程。你可以在工具方法里加日志,打印模型传入的参数和返回结果。如果模型没有调用任何工具,大概率是工具描述不够清楚,或者系统提示词里没有说明“你可以使用工具”。
4.4 为什么 maxIterations 是第一个要调的参数
Agent 循环最怕两种情况:循环太少,任务没完成;循环太多,Token 消耗失控。
maxIterations 就是控制循环上限的。我建议第一次先设 5 到 10,让 Agent 做简单任务。跑几次后看日志里实际调用了多少次工具,再调整。
如果任务是“统计项目代码量”,5 次可能够;如果是“重构一个模块”,20 次都可能不够。但企业环境里,我一般不会设置成无限循环。宁可一次任务没做完返回部分结果,也不能让一个请求吃掉大量成本、卡住整个服务。
注意:如果模型连续调用同一个工具两次以上还没进展,不要急着调大 maxIterations,先看工具返回结果和 prompt 有没有歧义。
5. 给 Agent 增加企业级约束:会话持久化和审计
5.1 会话应该存数据库,而不是只放内存
Demo 阶段把对话放在内存里没问题,但企业应用一旦重启,用户上下文全部丢失,没法接受。Spring AI 提供了内存型 ChatMemory,也支持扩展持久化实现。我的建议是直接落库。
简单起见,可以设计三张表:
| 表 | 作用 |
|---|---|
| conversation | 会话主表,存用户、项目、会话状态 |
| message | 每次请求/响应的消息内容,存角色和文本 |
| tool_call_log | 工具调用记录,存工具名、参数、结果摘要、耗时 |
会话表要保存用户 ID 和项目 ID,确保不同项目之间不会串上下文。message 表要按会话 ID 索引,方便恢复上下文。
如果消息量太大,还需要考虑裁剪。Spring AI 的 MessageWindow 可以按窗口大小保留最近 N 条消息,但窗口太大会超 token 限制,太小又会丢失历史。具体参数要以你的模型上下文窗口和单条消息长度为准。
5.2 审计日志记录什么
企业级项目里,模型调用不可避免会接触到代码内容、路径、甚至密钥信息。审计日志至少应该包含:
- 用户 ID / 调用来源。
- 会话 ID 和任务 ID。
- 模型名称和输入 Token / 输出 Token。
- 工具名称、入参、出参摘要。
- 耗时、成功/失败状态。
- 错误信息和重试次数。
特别注意,工具返回结果可能很大,也可能包含敏感信息。不要直接把完整结果存库,存截断后的摘要即可。完整结果可以放到临时文件或对象存储里,设置过期时间。
5.3 敏感操作怎么做二次确认
如果 Agent 只能读文件、查搜索,限制会比较清晰。但如果要写文件、执行命令,风险就上来了。
我的方案是给操作加“审批位”。Agent 遇到写操作时,不直接执行,而是先返回一条“需要人工确认”的消息。系统管理员在管理端看到操作请求,确认后,对应工具才真正执行。
实现上可以抽象一个接口:
public interface CommandExecutor { boolean requireApproval(String toolName); String executeAfterApproval(String taskId, String toolName, String params); }这个设计会让 Agent 流程多一个环节,但企业落地时非常值得。很多安全事故不是模型不够聪明,而是工具权限放得太宽。
6. 模型接入的通用套路和参数细节
6.1 ChatClient 和 Model 接口的区别
Spring AI 中有底层 Model 接口和上层 ChatClient。很多刚接触的人会混淆。
- Model 接口偏向底层,面向某个具体的模型服务,比如 AnthropicApi、OpenAiApi。
- ChatClient 是面向业务的高层 API,封装了 prompt、消息历史、Advisor 和工具调用。
企业开发里,我建议业务代码统一依赖 ChatClient。这样后续换模型,只需要改配置和依赖,业务代码基本不用动。
Agent Utils 也是基于 ChatClient 来工作的。你可以在 ChatClient 外面包 Advisor,比如日志 Advisor、Token 统计 Advisor、权限校验 Advisor,而不影响 Agent 循环本身。
6.2 temperature、maxTokens、topP 到底影响什么
模型参数不是越多越好。刚开始只需要关注三个:
- temperature:控制随机性。代码生成、结构化输出适合低值,比如 0 到 0.2;开放聊天可以稍微高一点。
- maxTokens:控制单次输出最大长度。代码任务建议设得大一些,否则工具调用还没结束,响应就被截断。
- topP:控制采样范围,一般不用同时调 temperature 和 topP。我通常保持 topP 默认,只调 temperature。
这几个参数在不同模型里的取值范围并不完全一致。有的模型 temperature 是 0 到 1,有的是 0 到 2。配置前先看模型官方文档,不要盲目套用 OpenAI 的经验。
6.3 换模型时最容易踩的坑
换模型看起来只是改一个配置,实际上容易踩几个坑:
- 模型名不识别:看日志里请求体传的 model 字段,很多平台报错都在这里。
- 工具调用格式不兼容:不同模型对 function/tool 参数的描述格式不同,Spring AI 封装后会有差异,但版本不对时会暴露。
- 上下文窗口不同:把 Claude 的项目直接换成别的模型,可能会因为上下文窗口变小而报错。
- 参数范围不同:temperature 范围、maxTokens 范围都要重新确认。
我的排查顺序是:先看报错请求体,再看响应体,最后才怀疑代码。90% 的“模型接入失败”问题,都出在配置而不是代码。
7. 常见报错和排查顺序
7.1 401 / 模型名不识别
现象最直接:调用模型接口返回 401,或者提示 model not found。
排查顺序:
- 确认环境变量
API_KEY是否真的注入成功。 - 确认基础配置里的 base-url 是否拼写正确。
- 确认 model 名称和平台支持的名称完全一致,包括版本号和后缀。
- 看应用启动日志里有没有加载到配置,不要只改配置文件忘了重启。
如果模型名是从环境变量读的,可以在测试环境先打印一下spring.ai.model相关配置,确认没有空格或转义问题。
7.2 工具调用失败或返回空
工具调用是最容易出问题的一环。常见原因:
- 工具描述不准确,模型不知道什么时候该用。
- 参数类型不匹配,模型传了字符串,工具方法需要 Integer。
- 工具返回内容太大,被截断。
- 工具内部抛异常,但 Agent 框架没把异常转成明确消息。
这个阶段我会打开调试日志,打印模型完整请求和工具调用参数。如果模型压根没调用工具,就改进描述;如果调了但返回空,就检查工具方法逻辑。
注意:工具方法里不要返回堆栈信息给模型,模型会把错误信息当成正常内容,导致后续判断混乱。尽量把异常转成用户能看懂的一句话。
7.3 上下文超长
Agent 带工具调用后,上下文增长很快。工具返回的目录列表、搜索结果、文件内容都可能几百行。
如果报错提示超出模型上下文限制,顺着这个顺序处理:
- 调小工具返回内容,搜索结果只返回前 50 条匹配。
- 对文件内容做截断,比如每行截断 200 字符,总行数限制。
- 清理对话历史,只保留最近几轮消息。
- 在系统提示词里限制模型“不要一次性读取大文件”。
上下文太长不是靠换更大的模型就能解决的,更合理的方式是让 Agent 先把问题定位到具体文件和行号,再读文件片段。
7.4 Agent 一直循环或超时
如果 Agent 反复调用同一个工具,或者卡在某个步骤不结束,先看 maxIterations 是否太小或太大。太小会提前结束但经常没完成;太大会让无意义循环拖很久。
还要看工具返回结果。如果工具返回“文件不存在”这类错误,模型应该调整路径,而不是继续调同一个参数。如果模型反复用同一个错误参数,说明提示词里没有给出“遇到错误后换一种方式”的指令。
最后还要加超时和熔断。Agent 循环整体耗时可能会到几十秒甚至几分钟,不能让 HTTP 请求一直挂着。建议给每次工具调用设置超时,给整个任务设置最大时长。
8. 从 Demo 到生产:还要补哪些东西
8.1 批量任务和并发控制
本地跑通之后,很多人第一反应就是开接口给团队用。这时要控制并发。
Agent 请求不是普通 HTTP 请求,它会连续调用模型接口多次。一次并发 10 的 Agent 请求,可能相当于模型接口 50 次调用。如果限流没有提前设计,后端的模型网关和成本账单都会很难看。
我的建议是:
- 用线程池限制并发数,而不是让请求无限进入。
- 加一个任务队列,超过并发上限的任务排队处理。
- 同一个用户同时只允许一个 Agent 任务。
- 为每个任务设置预算上限,Token 消耗超过预算就停止。
8.2 RAG:让 Agent 理解企业私有代码
你可能已经发现,简单工具调用只能帮 Agent 看到局部文件。企业级项目里,代码量很大,Agent 不可能把所有代码读完,这时候 RAG 就派上用场。
思路是:先把企业代码按类、方法、注释拆成小块,用向量模型转成向量,存入向量数据库。Agent 回答问题前,先检索相关代码片段,再把检索结果拼进 prompt,然后再进入工具循环。
Spring AI 提供了向量存储抽象和 EmbeddingModel 接口。落地时可以先用本地文件做测试,再切换到 PostgreSQL + pgvector 或专业的向量数据库。注意检索结果不要太多,一般取 top 5 到 top 10 就足够。
8.3 可观测性和验收清单
生产环境最怕黑盒。Agent 环节多,任何一个环节失败都不容易定位。所以日志结构一定要统一。
建议给每个任务打一个taskId,所有日志都带上这个 ID。日志内容至少包括:
- 每次模型输入的 token 数和输出 token 数。
- 模型调用了哪些工具,参数是什么。
- 工具返回的摘要和耗时。
- 最终答案是否正常返回。
最后给一份验收清单,供自己或团队检查:
- 单任务是否多次运行结果稳定。
- 工具权限是否限定在指定目录。
- 会话是否可以恢复,不同用户是否隔离。
- 高并发下模型网关是否被打爆。
- 工具超时和失败重试是否正常。
- 审计日志是否完整,是否可以查到具体任务链路。
- 模型名称和参数是否通过配置管理,不写死在代码里。
我自己的习惯是先把第一部分单聊跑稳,再把第二个工具接上,最后才扩到批量任务和多用户。很多问题不是模型能力不够,而是前置环境和输入材料没有处理干净。类 Claude Code 的项目,真正难的不是“让模型回答问题”,而是“让模型在规则边界内安全地操作代码”。把工具、会话、审计做好,这个项目就成功了一大半。