
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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 注册并登录然后进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。在控制台里找到 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 创建一个新的 Key。创建时建议给它起个能认出来的名字比如springai-local-dev方便以后区分环境。Key 只在创建时完整显示一次复制下来存到安全的地方。接下来是理解 SpringAI 的鉴权读取顺序这一步决定了你 401 到底该改哪个配置。以spring-ai-starter-model-openai为例它读取 API Key 的优先级大致是构造OpenAiApiBean 时显式传入的apiKey参数application.yml里spring.ai.openai.api-key的值环境变量OPENAI_API_KEY系统属性openai.api.key。Base URL 的读取顺序类似对应spring.ai.openai.base-url、环境变量OPENAI_BASE_URL等。问题就出在这里如果你在 yml 里只配了api-key没配base-urlSpringAI 会默认用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-openaispring: 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_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里试出来或者查接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你更喜欢用 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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.1.4/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-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密钥替换成你实际的 Keycurl -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_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-your-key-here TAOTOKEN_MODELgpt-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_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 试几个 prompt确认模型 ID 和返回格式符合预期再写进 SpringAI 配置。如果你打算把 SpringAI 用在长期的编码辅助或者 Agent 场景里比如让模型帮你生成代码、调用工具链那建议了解一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它在配额和模型调度上更适合持续性的开发任务而不是一次性的对话测试。最后说一个我自己的习惯每次改完base-url或api-key先跑一遍第 4 节的 curl 命令再启动 Spring Boot。这个顺序看起来多了一步但能帮你把“配置问题”和“代码问题”彻底分开。我试过好几次curl 通而应用不通最后发现都是环境变量覆盖或者 Bean 构造顺序的问题跟 Key 本身一点关系都没有。把验证步骤前置排查时间能从半小时压缩到两分钟。