1. Spring AI 与 JManus 组合到底解决什么问题
Spring AI 是 Spring 官方推出的 AI 应用开发框架,它把不同模型厂商的接口抽象成统一的ChatClient、EmbeddingClient等组件,让 Java 后端不用为每个模型写一套适配代码。JManus 则是一个轻量级智能体引擎,负责模型路由、Prompt 模板管理、上下文记忆和缓存优化。两者组合后,你在 Spring Boot 项目里调用大模型,体验接近调用一个普通的 Service Bean。
适合谁?有 Spring Boot 基础、想把大模型能力接进现有 Java 服务的后端开发者。你不需要先学 Python,也不用理解 Transformer 结构,只要会写@Service、会配application.yml,就能跑通第一条智能体调用链。
我试过在一个订单查询服务里接入这套组合,核心诉求是:统一 Key 管理、统一 base-url、统一模型 ID,避免每个模块各自维护一套配置。TaoToken 在这里扮演的角色是统一入口——一个 Key 覆盖多种模型,base-url 指向https://taotoken.net/api,Spring AI 的 OpenAI Starter 直接兼容这个地址格式。
这一篇会按可跟做的顺序展开:先讲依赖坐标和版本对齐,再给application.yml的可复制配置,然后写启动类和 Controller,最后用一次真实对话请求验证返回结果,并列出 401、连接失败、reading choices这类常见报错的排查路径。全程围绕 Spring Boot 3.x + Spring AI 1.0.0-M4 这个组合,JManus 以引擎层的方式嵌入。
需要提前说明一点:Spring AI 在 1.0.0-M4 阶段 API 还在演进,ChatClient的调用方式和后续版本可能有差异。本文所有代码都在 M4 上实测通过,如果你用的是其他里程碑版本,注意对照官方迁移说明调整方法名。
2. TaoToken 前置准备与 Spring AI 依赖坐标对齐
在写代码之前,先把两件事定下来:模型接入的 base-url 和 Key 从哪来,以及 Maven 依赖怎么配。这两步没对齐,后面启动必然报错。
2.1 获取统一 Key 与 base-url
TaoToken 的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 Spring AI 的base-url使用。Key 的获取入口在控制台的 API Keys 页面,登录后创建一个新 Key,复制出来形如sk-开头的一串字符。
这里有个容易踩的坑:Spring AI 的 OpenAI Starter 默认会把base-url和/v1/chat/completions拼接。所以你在application.yml里填的base-url应该是https://taotoken.net/api,而不是带/v1的完整路径。填错了会得到 404,而不是 401,排查时容易误判。
模型 ID 方面,TaoToken 支持多种模型,你在配置里填的model值需要和平台上的模型标识一致。比如gpt-4o-mini、claude-3-5-sonnet这类常见标识,具体以控制台模型列表为准。JManus 的智能路由能力,本质上就是根据 Prompt 特征在多个模型 ID 之间做选择,所以模型 ID 的准确性直接决定路由是否生效。
2.2 Maven 依赖坐标
Spring AI 的依赖需要从 Spring Milestones 仓库拉取,因为 1.0.0-M4 还没进 Maven Central。pom.xml里要同时配dependencyManagement和repositories,缺一个都会导致依赖解析失败。
<properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M4</spring-ai.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-openai-spring-boot-starter</artifactId> </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> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories>Java 版本要求 17 及以上,Spring Boot 用 3.2.x。如果你用的是 Spring Boot 3.3,Spring AI M4 也能兼容,但建议先按 3.2.5 起步,减少变量。
JManus 本身没有强制的 Maven 坐标,它更像是一层引擎逻辑,你可以把它实现为项目里的@Service,也可以引入其官方 starter(如果有的话)。本文采用自实现引擎层的方式,这样依赖最少,也方便你理解每一层在做什么。
2.3 目录结构约定
为了让后面的配置路径和代码位置对得上,先约定包结构:
com.example.springai ├── config │ └── JManusEngineConfig.java ├── engine │ └── JManusEngine.java ├── service │ └── AiChatService.java ├── controller │ └── AiChatController.java └── SpringAiJmanusApplication.javaapplication.yml放在src/main/resources下。这个结构不复杂,但能清晰体现「配置层—引擎层—服务层—接口层」的分层,后面排查问题时能快速定位是哪一层出的错。
3. application.yml 可复制配置与 JManus 引擎装配
这一节给出完整的application.yml片段和引擎装配代码。配置里的base-url、api-key、model三个值,就是 TaoToken 接入的三件套,缺一不可。
3.1 application.yml 完整配置
server: port: 8080 spring: application: name: spring-ai-jmanus-demo ai: openai: api-key: ${TAOTOKEN_API_KEY:sk-your-key-here} base-url: https://taotoken.net/api chat: options: model: gpt-4o-mini temperature: 0.7 max-tokens: 2000 client: connect-timeout: 10s read-timeout: 60s max-connections: 50 jmanus: engine: default-model: gpt-4o-mini fallback-model: gpt-4o-mini max-history-size: 20 cache-enabled: true这里api-key用了环境变量占位符${TAOTOKEN_API_KEY:sk-your-key-here},好处是本地开发时可以直接在 IDE 里配环境变量,不用把 Key 写死在文件里。如果你图省事,直接把sk-your-key-here替换成真实 Key 也能跑,但提交代码前记得改回来。
base-url填https://taotoken.net/api,不要加/v1。model填你在 TaoToken 控制台看到的模型标识。temperature和max-tokens按需调整,对话类场景 0.7 比较自然,代码生成类可以降到 0.2。
3.2 JManus 引擎装配
JManus 引擎的核心职责是根据请求特征选择模型、管理 Prompt 模板、维护上下文。下面这个JManusEngine实现了最简版本的路由逻辑。
package com.example.springai.engine; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Slf4j @Component public class JManusEngine { private final ChatClient chatClient; @Value("${jmanus.engine.default-model:gpt-4o-mini}") private String defaultModel; public JManusEngine(ChatClient chatClient) { this.chatClient = chatClient; } public String generate(Prompt prompt) { log.info("JManus 路由到模型: {}", defaultModel); return chatClient.prompt(prompt) .call() .content(); } public String generateWithModel(Prompt prompt, String model) { log.info("JManus 指定模型: {}", model); return chatClient.prompt(prompt) .call() .content(); } }注意ChatClient是通过构造器注入的,Spring AI 的自动配置会帮你创建这个 Bean,前提是spring-ai-openai-spring-boot-starter在 classpath 上且api-key和base-url配置正确。
3.3 配置类补充
如果你需要更细粒度的控制,比如自定义ChatClient的默认系统提示,可以加一个配置类:
package com.example.springai.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JManusEngineConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem("你是一个 Java 技术助手,回答简洁准确。") .build(); } }这个defaultSystem会作为所有请求的默认系统提示,JManus 引擎在构建 Prompt 时可以覆盖它。配置类不是必须的,但加上之后,你的引擎层代码会更干净。
3.4 启动类
package com.example.springai; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringAiJmanusApplication { public static void main(String[] args) { SpringApplication.run(SpringAiJmanusApplication.class, args); } }到这里,配置和装配部分就完成了。启动前再核对一遍:base-url是https://taotoken.net/api,api-key是真实 Key,model是有效模型 ID。三个都对,启动就不会在初始化阶段报错。
4. 对话请求验证与返回结果核对
配置写完了,得用一次真实请求验证整条链路是否打通。这一节给出 Service、Controller 的完整代码,以及用 curl 验证的步骤和预期返回。
4.1 AiChatService 实现
package com.example.springai.service; import com.example.springai.engine.JManusEngine; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class AiChatService { private final JManusEngine jmanusEngine; private final Map<String, List<Message>> historyStore = new ConcurrentHashMap<>(); public AiChatService(JManusEngine jmanusEngine) { this.jmanusEngine = jmanusEngine; } public String chat(String message) { Prompt prompt = new Prompt(new UserMessage(message)); return jmanusEngine.generate(prompt); } public String chatWithContext(String sessionId, String message) { List<Message> history = historyStore.computeIfAbsent(sessionId, k -> new ArrayList<>()); List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个 Java 技术助手。")); messages.addAll(history); messages.add(new UserMessage(message)); String response = jmanusEngine.generate(new Prompt(messages)); history.add(new UserMessage(message)); history.add(new AssistantMessage(response)); if (history.size() > 20) { historyStore.put(sessionId, new ArrayList<>(history.subList(history.size() - 20, history.size()))); } return response; } }chatWithContext里维护了一个按sessionId分组的对话历史,每次请求把历史消息拼进 Prompt。JManus 引擎负责实际调用,Service 层只管上下文。
4.2 Controller 实现
package com.example.springai.controller; import com.example.springai.service.AiChatService; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.UUID; @RestController @RequestMapping("/ai") public class AiChatController { private final AiChatService aiChatService; public AiChatController(AiChatService aiChatService) { this.aiChatService = aiChatService; } @GetMapping("/chat") public Map<String, Object> chat(@RequestParam String message, @RequestParam(required = false) String sessionId) { if (sessionId == null || sessionId.isEmpty()) { sessionId = UUID.randomUUID().toString(); } String reply = aiChatService.chatWithContext(sessionId, message); return Map.of( "sessionId", sessionId, "reply", reply, "model", "gpt-4o-mini" ); } @GetMapping("/health") public Map<String, Object> health() { return Map.of("status", "UP", "service", "Spring AI + JManus"); } }4.3 启动与验证
启动命令:
mvn spring-boot:run看到Started SpringAiJmanusApplication后,先访问健康检查:
curl http://localhost:8080/ai/health预期返回:
{"status":"UP","service":"Spring AI + JManus"}然后发一条对话请求:
curl "http://localhost:8080/ai/chat?message=用一句话解释什么是Spring%20AI"预期返回类似:
{ "sessionId": "a1b2c3d4-...", "reply": "Spring AI 是 Spring 生态中用于简化大模型集成的框架,提供统一的 ChatClient 抽象。", "model": "gpt-4o-mini" }拿到reply字段有内容,说明整条链路通了:Controller 收到请求 → Service 组装 Prompt → JManus 引擎调用 → Spring AI 通过 TaoToken 的 base-url 发出 HTTP 请求 → 模型返回 → 逐层回传。
4.4 多轮对话验证
用同一个sessionId发两次请求,验证上下文是否生效:
curl "http://localhost:8080/ai/chat?message=我叫小明&sessionId=test-001" curl "http://localhost:8080/ai/chat?message=我叫什么&sessionId=test-001"第二次的reply应该能说出「小明」。如果第二次回答不知道你的名字,说明历史消息没有正确拼进 Prompt,检查chatWithContext里的messages.addAll(history)是否执行。
4.5 返回结果核对要点
核对返回时重点看三个字段:reply是否有实际内容、sessionId是否稳定、model是否和你配置的一致。如果reply为空字符串,通常是模型返回了空内容,检查max-tokens是否设得太小。如果sessionId每次都在变,说明前端没传sessionId,多轮对话会失效。
5. 常见报错排查:401、连接失败与 reading choices
这一节列出实际接入时最常遇到的几类报错,给出报错原文特征和排查路径。这些错误我在不同项目里都遇到过,按顺序排查能省不少时间。
5.1 401 Unauthorized
报错特征:
401 Unauthorized: {"error":{"message":"Invalid API key","type":"invalid_request_error"}}排查顺序:
第一,确认application.yml里的api-key是真实 Key,不是占位符sk-your-key-here。如果你用了环境变量${TAOTOKEN_API_KEY},确认 IDE 或启动命令里确实设置了这个变量。在 IDEA 里可以通过 Run Configuration 的 Environment variables 设置。
第二,确认 Key 没有多余空格。从控制台复制时容易带上首尾空格,YAML 里看不出来,但请求时会失败。可以在 Key 前后加引号,或者用trim处理。
第三,确认 Key 没有过期或被删除。去 TaoToken 控制台的 API Keys 页面核对 Key 的状态。
5.2 连接失败与超时
报错特征:
java.net.ConnectException: Connection refused或者:
java.net.SocketTimeoutException: Read timed out排查顺序:
第一,确认base-url是https://taotoken.net/api,没有拼错域名,也没有多加/v1。多加/v1会导致路径变成/api/v1/chat/completions,如果服务端不认这个路径,可能返回 404 或连接异常。
第二,确认网络能访问该地址。可以在终端执行:
curl -I https://taotoken.net/api如果 curl 也连不上,说明是网络层问题,不是代码问题。
第三,如果是Read timed out,说明连接建立了但响应太慢。把read-timeout从 60s 调大,或者检查max-tokens是否设得过大导致模型生成时间过长。
5.3 reading choices 报错
报错特征:
Cannot deserialize value of type ... from Array value (token `JsonToken.START_ARRAY`)或者日志里出现reading choices相关字样。这类错误通常是响应体结构和 Spring AI 预期的结构不匹配。
排查顺序:
第一,确认base-url没有多加/v1。Spring AI 的 OpenAI Starter 会自己拼接/v1/chat/completions,如果你在base-url里已经带了/v1,最终路径会变成/api/v1/v1/chat/completions,服务端返回的错误结构就不是标准的choices数组,反序列化自然失败。
第二,确认模型 ID 有效。如果模型 ID 写错,服务端可能返回一个错误对象而不是标准的 chat completion 响应,Spring AI 尝试按choices解析就会报错。
第三,打开 debug 日志看原始响应:
logging: level: org.springframework.ai: DEBUG在日志里找到实际返回的 JSON,对照标准结构看缺了哪个字段。
5.4 OAuth 与认证方式不匹配
报错特征:
OAuth2 authentication failed或者:
Bearer token is malformed这类错误通常出现在 Key 格式不对,或者请求头里的认证方式和服务端预期不一致。Spring AI 的 OpenAI Starter 默认用Authorization: Bearer <api-key>的方式发送 Key。如果你用的 Key 不是这个格式,就会报认证失败。
排查:确认 Key 是sk-开头的标准格式,没有手动改过请求头。如果你在项目里自定义了RestClient或WebClient拦截器,检查有没有覆盖默认的认证头。
5.5 模型返回空内容
报错特征:请求成功(HTTP 200),但reply是空字符串。
排查顺序:
第一,检查max-tokens是否设得太小。如果设成 1 或 2,模型可能还没生成有效内容就截断了。
第二,检查 Prompt 是否为空。如果message参数是空字符串,模型可能返回空。
第三,检查temperature是否设得过高导致输出不稳定。对话场景 0.7 比较合适,超过 1.0 可能输出乱码或空内容。
5.6 依赖冲突导致启动失败
报错特征:
NoSuchMethodError: org.springframework.ai.chat.client.ChatClient.prompt这类错误通常是 Spring AI 版本和 Spring Boot 版本不匹配,或者 classpath 上有多个版本的 Spring AI 依赖。
排查:执行mvn dependency:tree | grep spring-ai,确认只有一个版本的spring-ai-core和spring-ai-openai。如果有多个版本,用<exclusions>排除掉旧版本。
6. 把调用链接进你的现有项目
到这里,一个可运行的 Spring AI + JManus 示例已经跑通了。接下来要考虑的是怎么把它接进你现有的 Spring Boot 项目,而不是停留在 demo 阶段。
第一件事是配置外置。把api-key、base-url、model这三个值放到配置中心或环境变量里,不要写死在application.yml。TaoToken 的统一 Key 设计在这里有优势:一个 Key 可以覆盖多个模型,你不需要为每个模型单独维护一套认证信息。切换模型时只改model字段,base-url和api-key保持不变。
第二件事是引擎层的扩展。本文的JManusEngine只做了最简单的路由,实际项目里你可以根据 Prompt 长度、任务类型、成本预算来做更细的路由决策。比如短查询走轻量模型,长文本分析走能力更强的模型。路由逻辑集中在引擎层,Service 层不需要感知模型差异。
第三件事是上下文存储。本文用的是内存ConcurrentHashMap,重启就丢。生产环境建议换成 Redis,按sessionId存储对话历史,设置合理的过期时间。Spring AI 本身提供了ChatMemory抽象,你可以基于它做持久化实现。
第四件事是可观测性。在JManusEngine.generate方法里加日志和指标埋点,记录每次调用的模型、耗时、token 消耗。这些数据对成本控制和性能优化很关键。Spring Boot Actuator 配合 Micrometer 可以快速接入。
如果你需要更细的接入文档和 API Key 管理入口,可以从这几个地址进入:API Keys 页面用于创建和管理 Key,接入文档页面有各语言的调用示例,模型对话页面可以直接在浏览器里测试模型连通性。长期做编码和 Agent 场景的话,Coding Plan 页面有更完整的方案说明。
最后提醒一点:Spring AI 在里程碑阶段 API 变动较频繁,升级版本时先看官方迁移指南,重点核对ChatClient的调用方法和Prompt的构造方式。本文代码在 1.0.0-M4 上验证通过,后续版本如果有 breaking change,按官方说明调整即可。整条链路的核心不变:配置三件套对齐、引擎层做路由、Service 层管上下文、Controller 层暴露接口。