☰
SpringAI 的那些事儿:从 401 报错到 Base URL 改到 TaoToken 的排查实录
2026/10/1 15:00:17 网站建设 项目流程

1. 从一次 401 报错说起:SpringAI 接入大模型时的鉴权链路到底卡在哪

如果你正在用 SpringAI 写第一个对话接口,大概率会遇到这样一幕:代码编译通过,Spring Boot 启动日志干干净净,结果一调/ai/chat就给你甩回一个401 Unauthorized,或者更让人摸不着头脑的local proxy failed、Connection refused。这两个报错看起来八竿子打不着,实际上都指向同一件事——请求根本没到达你期望的那个模型服务端点。

SpringAI 的定位是 Spring 生态里的大模型应用框架,它把 ChatClient、EmbeddingModel、VectorStore 这些能力封装成你熟悉的 Bean 和 Starter。适合谁用?适合已经熟悉 Spring Boot、不想为了调个模型再学一套 Python 工具链的 Java 开发者。但它的配置项分散在spring.ai.*命名空间下,不同模型供应商的 starter 对base-url、api-key的读取方式还不完全一样,这就导致排查 401 时经常找错方向。

我见过最常见的误区是:一看到 401 就以为是 Key 填错了,反复去复制粘贴 API Key,结果真正的问题出在base-url还指向默认的官方地址,而你的 Key 是另一个服务商签发的。SpringAI 在启动时不会校验这个组合是否匹配,只有真正发起请求那一刻才会暴露。所以这篇排查实录的核心思路就一条:先确认请求打到了哪里,再确认鉴权头带没带上,最后才怀疑 Key 本身。

下面我会按“定位报错 → 配置 Base URL → 可复制配置 → curl 验证 → 常见错排查”的顺序走一遍,每一步都给出你能直接粘贴的命令和配置片段。整个过程不需要你改 SpringAI 源码,也不需要额外装什么中间件。

2. 前置准备:在 TaoToken 拿到 Base URL 和 API Key,理清 SpringAI 的鉴权读取顺序

在动手改配置之前,先把“弹药”备齐。你需要两样东西:一个可用的 Base URL,和一个对应的 API Key。这里我用 TaoToken 作为接入端点来演示,因为它的接口路径和 OpenAI 兼容规范一致,SpringAI 的 OpenAI starter 可以直接对接,省去自定义适配的麻烦。

先访问官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 注册并登录,然后进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。在控制台里找到 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,创建一个新的 Key。创建时建议给它起个能认出来的名字,比如springai-local-dev,方便以后区分环境。Key 只在创建时完整显示一次,复制下来存到安全的地方。

接下来是理解 SpringAI 的鉴权读取顺序,这一步决定了你 401 到底该改哪个配置。以spring-ai-starter-model-openai为例,它读取 API Key 的优先级大致是:

  1. 构造OpenAiApiBean 时显式传入的apiKey参数;
  2. application.yml里spring.ai.openai.api-key的值;
  3. 环境变量OPENAI_API_KEY;
  4. 系统属性openai.api.key。

Base URL 的读取顺序类似,对应spring.ai.openai.base-url、环境变量OPENAI_BASE_URL等。问题就出在这里:如果你在 yml 里只配了api-key没配base-url,SpringAI 会默认用https://api.openai.com,而你的 Key 是 TaoToken 签发的,两边对不上,服务端自然返回 401。反过来,如果你只配了base-url没配api-key,请求会以匿名身份发出,同样 401。

所以正确的做法是成对配置,并且确保 Base URL 的路径前缀正确。TaoToken 的 API 端点是https://taotoken.net/api,注意这里不要加 UTM 参数,配置里写干净的地址就行。有些同学会把官网首页地址误填进base-url,那请求会打到网页服务器而不是 API 网关,表现就是 404 或者返回一段 HTML,而不是 JSON。

还有一个容易忽略的点:SpringAI 的 OpenAI starter 在拼接最终请求地址时,会在你配置的base-url后面追加/v1/chat/completions这类路径。所以你的base-url应该配到/api这一层,而不是配到/api/v1,否则会变成/api/v1/v1/chat/completions,直接 404。这个细节我在第一次配的时候也栽过,日志里看到重复的/v1才反应过来。

3. 可复制配置:application.yml 与 OpenAiApi Bean 的完整写法

这一节给你两份可直接用的配置,一份是纯 yml 方式,一份是 Java Config 方式。你可以根据项目习惯二选一,但不要同时用,否则 Bean 覆盖顺序会让你怀疑人生。

先看 yml 方式。假设你用的是 Spring Boot 3.4.x 加 SpringAI 1.1.x,依赖里引入的是spring-ai-starter-model-openai:

spring: ai: openai: base-url: https://taotoken.net/api api-key: sk-你的TaoToken密钥 chat: options: model: gpt-4o-mini temperature: 0.7 embedding: options: model: text-embedding-3-small

这里base-url写https://taotoken.net/api,不要带结尾斜杠,也不要带/v1。api-key直接填你从控制台复制的那串。model填你要用的模型 ID,具体支持哪些可以在模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 里试出来,或者查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

如果你更喜欢用 Java Config 显式构造,可以这样写:

@Configuration public class OpenAiConfig { @Bean public OpenAiApi openAiApi() { return OpenAiApi.builder() .baseUrl("https://taotoken.net/api") .apiKey(System.getenv("TAOTOKEN_API_KEY")) .build(); } @Bean public ChatClient chatClient(OpenAiChatModel chatModel) { return ChatClient.builder(chatModel).build(); } }

注意这里我把 Key 放在环境变量TAOTOKEN_API_KEY里,而不是硬编码在代码中。这样做的原因是:一旦你把 Key 提交到 Git,哪怕后来删掉,它也可能留在历史记录里。环境变量方式在本地开发时用 IDE 的运行配置注入,在服务器上用 systemd 或容器环境变量注入,都比重写代码安全。

对应的pom.xml依赖片段如下,重点是 BOM 统一管理版本,避免各模块版本打架:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.1.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies>

配好之后先别急着写 Controller,用下一节的 curl 命令确认链路通了,再回到代码层,能省掉大量“到底是配置错还是代码错”的纠结。

4. 验证请求:用 curl 确认请求真正到达目标服务并返回 choices

配置写完,最忌讳的就是直接启动 Spring Boot 然后对着 401 发呆。更高效的做法是先用 curl 在命令行里把请求打一遍,确认 Base URL、Key、模型 ID 这三者组合是通的。这样如果 curl 通了而 SpringAI 不通,问题就锁定在框架配置层;如果 curl 也不通,那就是 Key 或地址本身的问题。

打开终端,执行下面这条命令。把sk-你的TaoToken密钥替换成你实际的 Key:

curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的TaoToken密钥" \ -d '{ "model": "gpt-4o-mini", "messages": [ {"role": "user", "content": "用一句话说明什么是SpringAI"} ], "temperature": 0.7 }'

如果一切正常,你会看到类似这样的返回:

{ "id": "chatcmpl-xxx", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "SpringAI 是 Spring 生态中用于集成大模型能力的应用框架。" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 18, "completion_tokens": 24, "total_tokens": 42 } }

看到choices数组里有内容,说明请求已经真正到达目标服务,鉴权也通过了。这时候你再启动 Spring Boot 应用,调用/ai/chat接口,理论上应该能拿到同样的结果。

如果 curl 返回的是 401,先检查Authorization头里的 Key 有没有多余空格,Bearer和 Key 之间是一个空格。如果返回 404,检查 URL 路径是不是写成了/api/v1/chat/completions,注意/api后面直接跟/v1,不要重复。如果返回local proxy failed这类错误,通常是你本机设置了 HTTP 代理环境变量,curl 把请求发给了代理而不是直连。可以用env | grep -i proxy看一下,如果有http_proxy或https_proxy,临时unset掉再试。

curl 通了之后,回到 SpringAI 这边,启动应用并访问你的接口。如果这时报 401,而 curl 是通的,那基本可以断定是 SpringAI 读取配置的优先级问题——比如你环境变量里有一个旧的OPENAI_API_KEY覆盖了 yml 里的值。用System.getenv("OPENAI_API_KEY")打印一下确认,或者干脆在 yml 里显式指定并重启。

5. 常见错排查:401、local proxy failed、reading choices、OAuth 逐个对照

这一节把几个高频报错和真实日志对照着说,你可以直接拿自己的异常栈来比对。

401 Unauthorized:最典型。日志里通常伴随WWW-Authenticate: Bearer响应头。排查顺序是:先 curl 确认 Key 本身有效;再检查 SpringAI 配置里base-url和api-key是否成对出现;最后检查环境变量有没有覆盖。有一个隐蔽情况是 Key 复制时带了换行符,yml 里看不出来,但请求头里会多一个\n,服务端解析失败返回 401。用echo -n "sk-xxx" | wc -c确认长度和预期一致。

local proxy failed:这个报错不是 SpringAI 抛的,而是底层 HTTP 客户端(通常是 JDK 的 HttpClient 或 Reactor Netty)在尝试走系统代理时失败。常见于公司内网环境或者你之前配过代理工具。解决方式是显式禁用代理,或者在 JVM 启动参数里加-Dhttp.proxyHost=和-Dhttps.proxyHost=置空。如果你用的是 Reactor Netty,也可以在OpenAiApi构造时传入自定义的WebClient.Builder,把代理配置清掉。

reading choices 相关异常:完整报错通常是Cannot deserialize value of type ... from Array value ... reading choices或者Error while extracting response for type [ChatCompletion]。这说明请求发出去了,服务端也返回了,但返回的 JSON 结构和你期望的不一致。常见原因是base-url配错了层级,比如配到了官网首页,返回的是 HTML,反序列化自然失败。另一个原因是模型 ID 写错,服务端返回了一个错误对象而不是正常的choices数组。用 curl 打一遍同样的请求,看返回体长什么样,就能定位。

OAuth 相关报错:如果你在日志里看到OAuth、token endpoint、invalid_client这类字样,说明你的 SpringAI 配置里混入了 OAuth2 的自动配置。Spring Security 的 OAuth2 Client 会自动拦截带有Authorization头的请求并尝试刷新 token。解决办法是在 Security 配置里对/ai/**路径放行,或者把spring.security.oauth2.client相关配置移除。这个坑比较隐蔽,因为报错信息不会直接说“SpringAI 配置错了”,而是把你引向 OAuth 排查。

如果你在项目里用了 CC Switch、Cline MCP 或者 Codex 的auth.json来管理多个模型的接入信息,那要特别注意三件套必须写全:Base URL、Key、Model ID。缺任何一个,工具链在切换时都可能回退到默认值,表现就是“明明配了却还是 401”。以auth.json为例,结构大致是:

{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoToken密钥", "model": "gpt-4o-mini" }

三个字段名要和工具要求的一致,大小写敏感。CC Switch 这类工具在切换配置时如果发现字段缺失,有的版本会静默使用上一次的值,导致你以为切过去了其实没有。

6. 把链路固定下来:从模型对话验证到长期编码的接入建议

排查完一轮之后,建议你把验证过的配置固化下来,避免下次换环境又重新踩一遍。我的做法是在项目根目录放一个.env.example,把需要的变量名列出来,但不填真实值:

TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-your-key-here TAOTOKEN_MODEL=gpt-4o-mini

然后在application.yml里用占位符引用:

spring: ai: openai: base-url: ${TAOTOKEN_BASE_URL} api-key: ${TAOTOKEN_API_KEY} chat: options: model: ${TAOTOKEN_MODEL}

这样本地开发时用 IDE 注入环境变量,CI/CD 里用流水线变量注入,配置本身不进版本库,安全性和可移植性都兼顾了。

如果你只是想在本地快速验证模型对话效果,可以直接用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 试几个 prompt,确认模型 ID 和返回格式符合预期,再写进 SpringAI 配置。如果你打算把 SpringAI 用在长期的编码辅助或者 Agent 场景里,比如让模型帮你生成代码、调用工具链,那建议了解一下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它在配额和模型调度上更适合持续性的开发任务,而不是一次性的对话测试。

最后说一个我自己的习惯:每次改完base-url或api-key,先跑一遍第 4 节的 curl 命令,再启动 Spring Boot。这个顺序看起来多了一步,但能帮你把“配置问题”和“代码问题”彻底分开。我试过好几次,curl 通而应用不通,最后发现都是环境变量覆盖或者 Bean 构造顺序的问题,跟 Key 本身一点关系都没有。把验证步骤前置,排查时间能从半小时压缩到两分钟。

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

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

立即咨询