Spring AI 2.0 + Agent Utils:构建企业级AI编程助手的完整实践
2026/8/30 13:34:38 网站建设 项目流程

关于标题里那个“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 工作流。

一次典型的编程任务大概是这样的:

  1. 用户提出需求:找到项目中所有没被引用的公共方法。
  2. 模型先把需求拆成步骤:扫描目录、读取文件、分析引用关系、给出结论。
  3. 模型调用工具:读文件、搜索关键词、执行 grep 或代码分析命令。
  4. 工具返回结果,模型继续分析。
  5. 反复多轮,直到形成最终答案。

这个循环不是一次问答,而是“计划、执行、观察、再计划”的迭代过程。用 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。

排查顺序:

  1. 确认环境变量API_KEY是否真的注入成功。
  2. 确认基础配置里的 base-url 是否拼写正确。
  3. 确认 model 名称和平台支持的名称完全一致,包括版本号和后缀。
  4. 看应用启动日志里有没有加载到配置,不要只改配置文件忘了重启。

如果模型名是从环境变量读的,可以在测试环境先打印一下spring.ai.model相关配置,确认没有空格或转义问题。

7.2 工具调用失败或返回空

工具调用是最容易出问题的一环。常见原因:

  • 工具描述不准确,模型不知道什么时候该用。
  • 参数类型不匹配,模型传了字符串,工具方法需要 Integer。
  • 工具返回内容太大,被截断。
  • 工具内部抛异常,但 Agent 框架没把异常转成明确消息。

这个阶段我会打开调试日志,打印模型完整请求和工具调用参数。如果模型压根没调用工具,就改进描述;如果调了但返回空,就检查工具方法逻辑。

注意:工具方法里不要返回堆栈信息给模型,模型会把错误信息当成正常内容,导致后续判断混乱。尽量把异常转成用户能看懂的一句话。

7.3 上下文超长

Agent 带工具调用后,上下文增长很快。工具返回的目录列表、搜索结果、文件内容都可能几百行。

如果报错提示超出模型上下文限制,顺着这个顺序处理:

  1. 调小工具返回内容,搜索结果只返回前 50 条匹配。
  2. 对文件内容做截断,比如每行截断 200 字符,总行数限制。
  3. 清理对话历史,只保留最近几轮消息。
  4. 在系统提示词里限制模型“不要一次性读取大文件”。

上下文太长不是靠换更大的模型就能解决的,更合理的方式是让 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 的项目,真正难的不是“让模型回答问题”,而是“让模型在规则边界内安全地操作代码”。把工具、会话、审计做好,这个项目就成功了一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询