这次我们来看一个经常被问到的组合:Spring AI 2.0 + Agent Utils + Spring AI Alibaba,目标是用 Java 生态,像 ClaudeCode 那样搭出一个能对话、能调用工具、能拆解任务并逐步执行的 Agent 项目。如果你正在做 Java 大模型 Agent 开发,或者想从“调用大模型 API”升级到“让模型自主完成任务”,这篇可以直接参考。文章会从架构设计、环境准备、代码实现到接口 API 与批量任务完整走一遍,最后给出可落地的排查清单和工程建议。
先说结论:用 Java 做 Agent 完全可行,而且并不比 Python 差太多。模型推理本身发生在远端 API 或本地推理服务,Java 这边主要负责三件事:组织提示词、管理对话上下文、编排工具调用。这三件事刚好是 Spring AI 最擅长的领域。本文所有内容基于 Spring AI 2.0 时代的编程模型展开,结合 Spring AI Alibaba 的国内模型接入能力,最后会落到一个仿 ClaudeCode 的 Java Agent 实战项目上。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目定位 | Java 大模型 Agent 实战项目,仿 ClaudeCode 的对话式任务执行模式 |
| 核心框架 | Spring AI 2.0(统一模型接入与工具调用) |
| 模型接入 | Spring AI Alibaba 接入通义千问,也可通过 OpenAI 兼容模式接入 DeepSeek 等 |
| Agent 工具层 | Agent Utils,统一管理工具注册、上下文窗口、任务拆解与结果回填 |
| 运行方式 | Spring Boot 应用,提供 Web 对话接口与命令行交互两种模式 |
| 是否需要 GPU | 不需要。默认调用云端大模型 API;如需本地部署模型,再单独接入推理服务 |
| 支持批量任务 | 支持,通过任务队列 + 线程池实现批量对话与工具执行 |
| 接口 API | 提供 REST API,便于接入 Web 前端、运维平台或自动化脚本 |
| 适合读者 | Java 后端工程师、对 Agent 开发有兴趣但不熟 Python 的开发者 |
这里要先提醒一点:Spring AI 2.0 的具体 API 在版本迭代中会有调整。文章里展示的ChatClient、@Tool、消息内存等写法,都是当前版本的主干用法,实际使用时要按你引入的版本对应官方文档做微调,但整体设计思路不变。
2. 为什么用 Java 做 Agent 项目
Python 生态在大模型领域确实先发优势明显,LangChain、LlamaIndex 这类框架都是从 Python 起步的。但如果你所在团队的技术栈以 Java 为主,强行引入 Python 服务会带来额外的部署和维护成本。Java 侧做 Agent 的真正优势有三个:
第一,Spring AI 已经把模型接入抽象成统一的ChatModel和ChatClient,切换模型厂商通常只需要改配置,不用改业务代码。这比自己在代码里拼接 HTTP 请求维护多套 API 要高效得多。
第二,Java 的类型系统和 Spring 的依赖注入非常适合做工具管理。Agent 最核心的能力是调用外部工具,而 Spring 的 Bean 容器天然可以管理一组工具 Bean,通过注解或注册表把工具暴露给模型,比脚本语言里用字典维护函数列表更清晰。
第三,企业级集成成本低。Agent 最终要落到业务系统里,需要对接权限、数据库、消息队列、监控告警。这些能力在 Java 生态非常成熟,而 Spring AI 可以无缝嵌进现有 Spring Boot 工程,不需要额外搭一座“桥”。
所以这个实战项目的定位不是复刻一个完整的 ClaudeCode 命令行工具,而是把 ClaudeCode 的核心工作方式抽象出来:用户给目标 → Agent 拆解任务 → 调用工具 → 根据结果继续执行 → 输出最终答复。这套模式用 Java 实现一遍,你就掌握了 Agent 开发最通用的骨架。
3. 环境准备与前置条件
在做代码之前,先把运行环境准备好。按下面清单逐项检查即可:
| 检查项 | 要求 |
|---|---|
| JDK | JDK 17 及以上,推荐 JDK 21 |
| Maven | Maven 3.9+,国内网络环境可配置阿里云镜像 |
| Spring Boot | 3.3 及以上版本 |
| 模型 API Key | 通义千问 DashScope API Key,或 DeepSeek API Key |
| 网络 | 能正常访问模型服务商 API 即可 |
| IDE | IntelliJ IDEA 或 Eclipse,推荐 IDEA |
这里有一个很多人会搞混的点:如果你的 Agent 只调用云端 API,本地完全不需要 GPU 和显存。显存只有在本地部署大模型推理服务时才需要。我们的项目默认走云端 API,所以一台普通开发机就能跑通。
如果你的需求是本地部署大模型,那么建议先单独部署一个兼容 OpenAI 接口的推理服务,比如 vLLM 或 Ollama,再把 Spring AI 的 base-url 指向本地服务。后面章节会给出对应的配置方法。
4. 项目初始化与依赖接入
4.1 创建 Spring Boot 项目
可以直接在 Spring Initializr 生成一个基础工程,也可以手动创建 Maven 工程。关键依赖是spring-ai-bom和spring-ai-alibaba-starter。下面是 pom.xml 的核心片段:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <properties> <java.version>21</java.version> <spring-ai.version>2.0.0</spring-ai.version> <spring-ai-alibaba.version>1.0.0</spring-ai-alibaba.version> </properties> <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</artifactId> <version>${spring-ai.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <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>注意版本号需要以 Maven 中央仓库实际可用的版本为准。spring-ai-alibaba-starter的版本演进比较快,尽量选稳定版。
4.2 配置文件
如果你使用通义千问,通过 Spring AI Alibaba 接入,application.yml可以这样写:
spring: application: name: java-agent-claude ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus如果你使用 DeepSeek,Spring AI 的 OpenAI 兼容模式可以这样配置:
spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat两套配置的核心区别只在于base-url和api-key。项目里建议通过环境变量或配置中心管理密钥,不要把真实 Key 提交到 Git。
5. 仿 ClaudeCode Agent 的架构设计
ClaudeCode 的交互模式本质是一个“目标驱动的循环”:用户输入目标 → 模型规划 → 执行工具 → 观察结果 → 继续决策 → 输出最终结果。我们的 Java 项目按这个思路拆成五个模块,对应下图,这里不使用流程图,仅用表格说明模块职责:
| 模块 | 职责 | 对应实现 |
|---|---|---|
| chat-api | 对外交互入口 | Controller / 命令行 REPL |
| agent-core | 对话循环与任务编排 | AgentLoop、TaskPlanner |
| agent-tools | 工具注册与调用 | @Tool 注解 + ToolRegistry |
| agent-memory | 上下文状态管理 | MessageWindowChatMemory |
| agent-utils | 公共工具与批量任务 | PromptBuilder、BatchTaskExecutor |
Agent Utils这个词在标题里可以理解为“Agent 公共工具层”,它不一定是某个第三方开源库,而是我们工程里负责任务拆解、上下文裁剪、工具结果解析、批量任务调度的模块统称。这样组织的好处是:Agent 的核心循环代码保持稳定,新增工具、新增模型、新增交互方式都只动对应模块。
整个执行的伪代码如下:
接收用户目标 将目标写入消息会话 循环: 将消息列表发送给模型 如果模型返回工具调用请求: 执行对应工具 将工具结果作为消息回填给模型 否则: 返回最终答复,结束循环这套循环是 ClaudeCode 这类 Agent 最核心的骨架。Spring AI 的ChatClient已经帮你处理了大部分底层细节,我们要做的是把工具注册和上下文管理接进来。
6. 核心功能实现
6.1 对话交互层
先实现一个 Web 接口,接收用户的对话请求。这是整个 Agent 的入口。
@RestController @RequestMapping("/api/agent") public class AgentController { private final AgentService agentService; public AgentController(AgentService agentService) { this.agentService = agentService; } @PostMapping("/chat") public AgentResponse chat(@RequestBody AgentRequest request) { return agentService.chat(request.sessionId(), request.message()); } }请求与响应用 Java Record 定义,简洁且适合 Spring 的 JSON 序列化:
public record AgentRequest(String sessionId, String message) {} public record AgentResponse(String sessionId, String answer, boolean taskFinished) {}sessionId用来标识一次独立会话。同一个 session 内的多条消息会共享上下文,不同 session 之间互不干扰。
6.2 Agent 服务层
AgentService是整个项目的中心。它负责创建ChatClient、维护会话上下文、调用工具并返回结果。
@Service public class AgentService { private final ChatModel chatModel; private final ToolRegistry toolRegistry; private final Map<String, ChatMemory> sessionMemory = new ConcurrentHashMap<>(); public AgentService(ChatModel chatModel, ToolRegistry toolRegistry) { this.chatModel = chatModel; this.toolRegistry = toolRegistry; } public AgentResponse chat(String sessionId, String userMessage) { ChatMemory memory = sessionMemory.computeIfAbsent(sessionId, k -> MessageWindowChatMemory.builder() .maxMessages(20) .build()); ChatClient chatClient = ChatClient.builder(chatModel) .defaultTools(toolRegistry.getAllToolNames().toArray(String[]::new)) .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()) .build(); String answer = chatClient.prompt() .user(userMessage) .call() .content(); return new AgentResponse(sessionId, answer, true); } }MessageWindowChatMemory负责控制上下文窗口,避免无限对话导致请求内容过长。maxMessages可以根据你的模型上下文上限调整,通义千问和 DeepSeek 的长文本能力都不错,但单轮请求还是建议控制在合理范围。
6.3 工具注册与调用
Agent 与普通聊天机器人的最大区别是能调用工具。Spring AI 提供了@Tool注解,可以直接把 Java 方法暴露给模型。我们做一个计算器工具和一个时间工具:
@Component public class CommonTools { @Tool(description = "获取当前系统时间") public String getCurrentTime() { return LocalDateTime.now().toString(); } @Tool(description = "计算两个数字的加减乘除,运算符支持 add、subtract、multiply、divide") public String calculate(double a, double b, String operator) { double result = switch (operator) { case "add" -> a + b; case "subtract" -> a - b; case "multiply" -> a * b; case "divide" -> { if (b == 0) { throw new IllegalArgumentException("除数不能为0"); } yield a / b; } default -> throw new IllegalArgumentException("不支持的运算符: " + operator); }; return String.valueOf(result); } }工具类需要注册到 Spring 容器。可以在配置类中把所有@ToolBean 的工具方法收集到ToolRegistry:
@Configuration public class AgentConfig { @Bean public ToolRegistry toolRegistry(List<ToolCallbackProvider> providers) { ToolRegistry registry = new ToolRegistry(); providers.forEach(provider -> provider.getToolCallbacks() .forEach(registry::registerToolCallback)); return registry; } }模型在需要时会在响应中返回工具调用指令。Spring AI 的ChatClient会自动执行已注册的工具,并把结果回填给模型,无需我们自己写调用逻辑。这比手动解析 OpenAI 函数调用格式要省事很多。
6.4 任务拆解与多步执行
ClaudeCode 风格的 Agent 有很强的任务拆解能力。简单场景下,模型会自己根据用户指令决定调用哪些工具;复杂场景下,我们需要主动引导模型先生成执行计划,再逐步执行。
这里可以用 Spring AI 的结构化输出能力,把模型输出约束成一个任务列表:
@Service public class TaskPlanner { private final ChatClient chatClient; public TaskPlanner(ChatModel chatModel) { this.chatClient = ChatClient.builder(chatModel).build(); } public List<TaskStep> plan(String goal) { String prompt = """ 你是任务规划器。请把用户目标拆解为最多5个可执行步骤。 每个步骤要包含:步骤名、执行动作、预期结果。 用户目标:%s """.formatted(goal); TaskPlan plan = chatClient.prompt() .user(prompt) .call() .entity(TaskPlan.class); return plan.steps(); } }public record TaskStep(String stepName, String action, String expectedResult) {} public record TaskPlan(List<TaskStep> steps) {}结构化输出是 Spring AI 的强项,模型返回的 JSON 会自动绑定到 Java Record 上。拿到步骤列表后,Agent 就可以按顺序执行每一步,每一步的结果继续回填到上下文中。
多步执行的核心循环可以这样组织:
public String executePlan(String sessionId, String goal) { List<TaskStep> steps = taskPlanner.plan(goal); StringBuilder resultBuilder = new StringBuilder(); for (int i = 0; i < steps.size(); i++) { TaskStep step = steps.get(i); resultBuilder.append("步骤") .append(i + 1) .append(":") .append(step.action()) .append("\n"); String stepResult = chat(sessionId, "请执行步骤:" + step.action()).answer(); resultBuilder.append(stepResult).append("\n"); } return resultBuilder.toString(); }这里的实现刻意保持简单,方便理解核心逻辑。生产环境需要考虑步骤失败时的降级和重试机制。
6.5 命令行交互模式
仿 ClaudeCode 的项目很适合提供一个命令行交互入口。Spring Boot 的CommandLineRunner可以实现程序启动后进入 REPL 模式:
@Component public class CommandLineAgentRunner implements CommandLineRunner { private final AgentService agentService; private final Scanner scanner; public CommandLineAgentRunner(AgentService agentService) { this.agentService = agentService; this.scanner = new Scanner(System.in); } @Override public void run(String... args) { System.out.println("Java Agent 已启动,输入 /quit 退出"); while (true) { System.out.print("> "); String input = scanner.nextLine(); if ("/quit".equalsIgnoreCase(input.trim())) { break; } AgentResponse response = agentService.chat("cli-session", input); System.out.println("Agent: " + response.answer()); } } }命令行模式的好处是便于快速验证 Agent 的工具调用是否符合预期,也方便在没有前端页面的服务器上做演示。
7. 接口 API 与批量任务设计
7.1 对外 API 接口
Web 接口已经在上文给出了/api/agent/chat,它的能力本质是“单轮对话 + 工具调用”。为了让外部系统更好地接入,可以补充两个接口:
@RestController @RequestMapping("/api/agent") public class AgentController { @PostMapping("/chat") public AgentResponse chat(@RequestBody AgentRequest request) { return agentService.chat(request.sessionId(), request.message()); } @PostMapping("/tasks") public TaskExecuteResponse executeTasks(@RequestBody TaskExecuteRequest request) { return agentService.executeBatch(request.sessionId(), request.messages()); } @GetMapping("/sessions/{sessionId}/history") public List<ChatMessage> history(@PathVariable String sessionId) { return agentService.getHistory(sessionId); } }executeBatch会接收一批消息并逐个处理,这样外部系统可以批量提交任务。
7.2 批量任务实现
批量任务的核心是并发控制。大模型 API 通常有速率限制,不能无限并发。我们可以用线程池限制并发数,并且记录每个任务的状态,方便失败重试:
@Service public class BatchTaskService { private final AgentService agentService; private final ExecutorService executor; public BatchTaskService(AgentService agentService) { this.agentService = agentService; this.executor = Executors.newFixedThreadPool(5); } public void processMessages(String sessionId, List<String> messages) { List<Future<?>> futures = messages.stream() .map(msg -> executor.submit(() -> { agentService.chat(sessionId, msg); })) .toList(); // 等待所有任务完成 futures.forEach(future -> { try { future.get(); } catch (Exception e) { // 记录失败任务并加入重试队列 System.err.println("任务执行失败: " + e.getMessage()); } }); } }注意ExecutorService使用完毕后要调用shutdown(),或者在 Spring 生命周期中统一管理线程池。批量任务建议在配置文件中支持线程池大小和队列长度配置,避免突发流量把 API 限额打满。
7.3 curl 调用示例
接口启动后,可以用 curl 快速验证:
curl -X POST http://localhost:8080/api/agent/chat \ -H "Content-Type: application/json" \ -d '{ "sessionId": "test-001", "message": "请帮我查一下当前系统时间" }'如果工具调用成功,返回结果中会包含模型基于工具输出生成的最终答复。这样一个请求走通后,后面接前端或自动化脚本就很顺畅了。
8. 资源占用与性能观察
因为默认走云端 API,本地 Java 进程的资源占用主要取决于 JVM 本身和并发任务数。你可以重点观察四个指标:
| 指标 | 观察方式 | 正常范围 |
|---|---|---|
| JVM 堆内存 | IDEA 自带监控,或 jstat 命令 | 根据场景,通常几百 MB 以内 |
| CPU 使用率 | 系统监控工具 | 空闲时低,批量任务时会升高 |
| API 响应延迟 | 业务日志记录耗时 | 取决于模型厂商,通常 1~10 秒 |
| 线程池排队情况 | 自定义日志或 ThreadPoolExecutor 指标 | 不应该有大量任务堆积 |
如果你把模型部署到本地,那么资源观察重点会转移到 GPU 显存和推理延迟。常见做法是单独部署 Ollama 或 vLLM,用nvidia-smi查看显存占用,再调整并发数和上下文长度。通过spring.ai.openai.base-url指向本地推理服务后,Java 侧不需要任何代码改动。
在性能调优时要注意:上下文长度越长,请求处理时间越长,费用也越高。MessageWindowChatMemory是一个窗口,超出部分会被丢弃。如果你的业务需要更长的历史记忆,可以把对话历史持久化到 Redis 或数据库,按需加载,而不是全部塞进每次请求。
9. 常见问题与排查方法
下面整理这个项目最常见的几类问题,按现象、原因、排查方式、解决方案列出:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动报错 chatModel Bean 找不到 | 未正确引入模型 Starter 依赖 | 检查 pom.xml 是否包含 spring-ai-alibaba-starter | 补全依赖并刷新 Maven |
| 401 Unauthorized | API Key 配置错误或为空 | 检查环境变量 DASHSCOPE_API_KEY | 重新配置 Key 并重启应用 |
| 模型返回内容与预期不符 | 提示词设计不合理,或工具描述不够清晰 | 查看请求日志中的完整消息列表 | 优化系统提示词和 @Tool 的 description |
| 工具一直不被调用 | 工具方法没有注册到 ChatClient | 检查 ToolRegistry 是否包含该方法 | 确保工具类被 Spring 扫描,且方法上有 @Tool 注解 |
| 任务执行中卡住 | 模型返回异常格式,或网络超时 | 查看日志中模型返回的原始响应 | 给调用设置超时时间,并加入失败重试逻辑 |
| 显存不足 | 本地部署了模型推理服务 | 使用 nvidia-smi 查看显存占用 | 降低并发数,选择更小的模型,或切换到云端 API |
| 批量任务频繁失败 | 触发了 API 限流 | 查看模型服务商返回的状态码 | 降低线程池并发数,增加重试间隔 |
| 会话上下文串乱了 | sessionId 使用不当 | 检查日志中每个请求的 sessionId | 确认统一会话使用相同 sessionId |
如果你的项目是接入本地 Ollama,遇到“连接失败”时,先确认 Ollama 服务是否启动、端口是否正确、是否能通过浏览器访问。localhost:11434是一个常见默认端口,但具体以你本机配置为准。
10. 最佳实践与合规边界
项目跑通后,建议从以下几个方面做工程化加固。
第一,先小参数验证再上批量。第一次运行先只测一条消息,确认工具调用正常、上下文累积效果符合预期,再放开批量任务。批量任务必须加日志、加失败重试、加并发限制。
第二,模型输入输出要留痕。Agent 的输入是用户指令,输出是模型生成的文本或动作。生产环境中,建议把每次完整请求、工具调用、模型响应都记录到日志或数据库中。这样一旦出现异常,可以回溯是提示词问题、模型问题还是工具实现问题。
第三,权限与数据边界。Agent 能调用工具,本质上是把模型的能力和外部系统能力打通了。工具方法必须做参数校验和权限控制,不能因为模型说了一个参数就直接执行危险操作。比如删除、转账、发布类工具,一定要有二次确认或审批流。
第四,隐私和版权合规。用户输入和业务数据如果会发送到云端大模型 API,必须先确认是否符合公司的数据安全规范。涉及个人信息、商业机密的内容,要脱敏后再发送,或者选择私有化部署模型。模型生成的内容,尤其是代码、合同、新闻类文本,商用前必须人工复核,不能完全信任模型输出。
第五,工具描述要写清楚。模型是依据@Tool注解里的 description 来决定是否调用工具的。描述越具体,模型越容易正确选择。比如“计算两个数字的加减乘除”就比“计算方法”强很多。
11. 总结与下一步
这个 Java 大模型 Agent 项目最值得尝试的点,是把 Spring AI 2.0 的ChatClient、工具调用、消息记忆和结构化输出完整串起来,形成一套仿 ClaudeCode 的可运行骨架。你已经可以照着上面的步骤,从零初始化项目、接入模型、注册工具、启动 Web 接口、跑通批量任务,然后逐步扩展成自己的 Agent 平台。
最优先验证的功能是工具调用:让 Agent 调用一次时间工具或计算器工具,观察它是否能在多轮对话里保持上下文、执行工具并回填结果。最容易踩的坑也集中在这里——工具没有注册、@Tool 描述不清晰、会话 sessionId 不一致,都会导致结果异常。
后续扩展方向有四个:一是把工具从简单方法扩展成外部系统 API 调用,对接数据库查询、HTTP 请求、定时任务;二是引入多 Agent 协作,规划 Agent、执行 Agent、审查 Agent 各司其职;三是把对话历史持久化到 Redis,支持跨服务共享会话;四是在项目里加入指标监控,统计每次 Agent 运行的耗时、Token 消耗和工具调用成功率,为后续优化提供数据依据。
建议现在就动手做一件事:用这五个模块的最小骨架,先跑通一个“查询时间 + 做一次数学计算”的 Agent 会话,把工具调用链路验证完成,再继续扩展任务拆解和批量执行。代码不复杂,核心逻辑都在ChatClient和工具注册上,剩下的就是业务场景的填充。