1. 多轮对话里 MCP Tools 为什么会断在半路
Agent 多轮对话调用 MCP Tools 这件事,真正跑起来之后你会发现,最难的从来不是写循环,而是循环跑到第三、第四轮时突然断掉。前端还在转圈,后端日志里要么是一行401 Unauthorized,要么是local proxy failed,要么是reading 'choices'这种一看就知道是空指针的报错。我试过把同一个 Agent 放在 Spring Boot 里跑,MCP Server 用 STREAMABLE 协议挂在 8088,LLM 走 OpenAI 兼容格式,结果第一轮工具调用成功,第二轮开始就各种鉴权失败。
先说清楚这套东西是什么。MCP 是 Model Context Protocol,你可以把它理解成「给大模型用的 USB 接口」——每个 MCP Server 暴露一组 Tools,Agent 负责把用户问题翻译成工具调用指令,拿到结果后再喂回模型,形成多轮对话链。适合谁?适合已经在写 Agent、想让模型真正去查数据/调接口,而不是只聊天的开发者。核心检索词就是 Agent 多轮对话、MCP Tools、401 鉴权失败、local proxy failed。
问题出在哪?多轮对话和单轮最大的区别是:每一轮你都要重新发一次 HTTP 请求给 LLM,而每一次请求都带着Authorization: Bearer <key>。单轮时 key 是对的,多轮时如果中间某一轮走了不同的 endpoint、或者本地代理把请求拦下来改了 header,就会在第二轮之后开始 401。更隐蔽的是local proxy failed——它不是你的代码错,而是请求根本没出去,被本机某个代理层挡了。
我踩过的坑是这样的:Agent 的callLlm方法里apiUrl配的是http://localhost:8080/v1/chat/completions,但那个 8080 其实是另一个 MCP Server 的端口,不是 LLM 网关。第一轮因为工具描述里没匹配到工具,直接走 LLM 聊天,恰好那个端口有个 mock 服务返回了合法 JSON,看起来正常;第二轮真正要调工具时,请求打到了错误的地址,返回 401。这种「第一轮正常、第二轮崩」的现象,几乎都是 endpoint 配错或鉴权头丢失导致的。
所以排查顺序应该是:先确认每一轮请求实际打到了哪个 URL,再确认 header 里的 key 有没有被覆盖,最后才看 MCP Server 本身。下面我把 TaoToken 作为统一网关接进来,把 Base URL、Key、Model ID 三件套固定住,让多轮对话的每一轮都走同一条路,问题就收敛了。
2. TaoToken 前置:把 Base URL 和 Key 固定成三件套
在动手改代码之前,先把「三件套」这个概念立住:Base URL、API Key、Model ID。任何一次 LLM 调用,只要这三样在每一轮都一致,401 和 proxy failed 的概率会大幅下降。TaoToken 在这里扮演的角色是统一入口——你不需要在 Agent 里维护多个 endpoint,所有模型请求都指向同一个 Base URL,Key 也只用一份。
官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。注意 API 这个地址后面不加任何参数,直接作为 Base URL 使用。模型对话页面在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,你可以先在那里手动发一条消息,确认 Key 是活的,再去改代码。
为什么强调「先手动验证」?因为多轮对话的报错会掩盖最基础的问题。如果 Key 本身就是错的,你在 Agent 里看到的是第二轮 401,但根因其实是 Key 从来没对过。手动在模型对话页发一条「你好」,能返回内容,说明 Key 和 Base URL 这一层是通的,接下来所有问题都只可能出在代码或本地代理上。
接下来是 Coding Plan 和 API Keys 两个入口。如果你打算长期跑 Agent、频繁多轮调用,Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 里有适合持续编码场景的额度说明;而真正要拿到 Key,去 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建。创建完复制那串sk-开头的字符串,先存到环境变量里,别直接写进代码。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面写了 OpenAI 兼容格式的调用方式。对于 Spring Boot 这套 Agent,你只需要记住:Base URL 填https://taotoken.net/api,请求路径拼/v1/chat/completions,header 里Authorization: Bearer <你的Key>。Model ID 填你在模型对话页选中的那个模型名,比如gpt-4o或claude-3-5-sonnet之类,具体以页面显示为准。
这里有个容易忽略的点:TaoToken 的 Base URL 是https://taotoken.net/api,不是https://taotoken.net/api/v1。很多 OpenAI SDK 会自动在 Base URL 后面拼/v1/chat/completions,如果你手动拼了/v1,就会变成/api/v1/v1/chat/completions,返回 404 而不是 401,但日志里看起来也像鉴权问题。所以统一约定:Base URL 只写到/api,路径部分由代码拼。
把这三件套固定下来之后,Agent 的application.properties里就不要再出现localhost:8080这种地址了。所有 LLM 请求走 TaoToken,MCP Server 的地址单独用mcp.server.urls配置,两者彻底分开。这样多轮对话里每一轮 LLM 调用都走同一条路,401 的排查范围就缩小到「Key 是否被覆盖」和「本地代理是否拦截」两件事上。
3. 可复制配置:auth.json、settings 与 application.properties
这一节直接给可复制的片段。先说你最可能用到的两个场景:CC Switch 和 Cline MCP。这两个工具在多轮对话里经常和 Agent 混用,配置写错就会互相污染。
CC Switch 的配置文件通常放在用户目录下的.cc-switch/config.json,或者项目根目录的settings.json。核心是baseUrl和apiKey两个字段。下面这段可以直接改:
{ "provider": "openai-compatible", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoTokenKey", "model": "gpt-4o", "timeout": 60000, "maxRetries": 2 }注意baseUrl结尾没有斜杠,也没有/v1。model字段填你在模型对话页确认过的 Model ID。maxRetries设成 2 是为了多轮对话时偶发的网络抖动,但不要设太大,否则 401 会被重试掩盖,日志里看不到真实错误。
Cline MCP 的配置在 VS Code 的settings.json里,或者 Cline 自己的 MCP 配置面板。它需要同时配 LLM 和 MCP Server 两块:
{ "cline.mcpServers": { "my-talk": { "url": "http://localhost:8088/mcp", "transport": "streamable-http" } }, "cline.apiProvider": "openai", "cline.openaiBaseUrl": "https://taotoken.net/api", "cline.openaiApiKey": "sk-你的TaoTokenKey", "cline.openaiModelId": "gpt-4o" }这里cline.openaiBaseUrl同样只写到/api。MCP Server 的url是你本地 Spring Boot 起的 8088 端口,和 TaoToken 完全无关。很多人 401 的原因就是把cline.openaiBaseUrl写成了 MCP Server 的地址,导致 LLM 请求打到了 MCP 端口上。
如果你用的是 Codex 风格的auth.json,路径一般在~/.codex/auth.json,内容长这样:
{ "OPENAI_API_KEY": "sk-你的TaoTokenKey", "OPENAI_BASE_URL": "https://taotoken.net/api", "OPENAI_MODEL": "gpt-4o" }auth.json里的 Key 不要带Bearer前缀,只放sk-开头的原始字符串。有些工具会自动加Bearer,你手动加了就会变成Bearer Bearer sk-xxx,直接 401。
回到 Spring Boot 的application.properties,把 excerpt 里的空值填上:
server.port=8088 spring.ai.mcp.server.name=my-talk spring.ai.mcp.server.version=0.0.1 spring.ai.mcp.server.protocol=STREAMABLE logging.file.name=./logs/agent-server.log llm.api-key=sk-你的TaoTokenKey llm.api-url=https://taotoken.net/api/v1/chat/completions llm.model=gpt-4o mcp.server.urls=http://localhost:8080,http://localhost:8081注意llm.api-url这里是完整路径,包含了/v1/chat/completions,因为 Agent 的callLlm方法里是直接URI.create(apiUrl)用的,不会再拼路径。而 CC Switch 和 Cline 的baseUrl是基础地址,由工具自己拼路径。这两个不要搞混,搞混就是 404 或 401。
还有一个细节:mcp.server.urls里的地址是 MCP Server,不是 LLM。如果你只有一个 MCP Server,就写一个;如果有多个,用逗号分隔。Agent 的AgentService.init()会遍历这个数组,为每个 URL 建一个McpSyncClient。多轮对话时,callMcpTool会遍历所有 server 找到能处理该工具的那个。如果某个 MCP Server 连不上,init()里会打印MCP 服务器连接失败,但不会中断启动,所以你要在日志里留意这行。
最后强调一遍三件套的对应关系:Base URL 是https://taotoken.net/api,Key 是sk-开头那串,Model ID 是模型对话页显示的名字。这三样在 CC Switch、Cline、Codex、Spring Boot 里必须完全一致,任何一处写错都会在多轮对话的第二轮之后暴露成 401 或 proxy failed。
4. 验证请求:从 curl 到多轮工具调用成功
配置改完,先别急着启动 Spring Boot。用 curl 打一发,确认 TaoToken 这一层是通的:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的TaoTokenKey" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "你好,请回复ok"} ] }'如果返回 JSON 里有choices[0].message.content,说明 Key、Base URL、Model ID 三件套都对。如果返回 401,先检查 Key 有没有多余空格;如果返回 404,检查路径是不是/api/v1/chat/completions;如果返回local proxy failed,说明你本机有代理层在拦截,需要把taotoken.net加入直连白名单。
curl 通了之后,启动 Spring Boot:
mvn spring-boot:run启动日志里会打印 MCP Server 连接情况和工具列表。看到类似这样的输出就对了:
MCP 服务器共 2 台, 注册工具共 5 个 可用工具列表: - get_weather: 查询指定城市天气 - query_order: 根据订单号查询订单状态然后调 Agent 的 chat 接口,发一个需要多轮工具调用的问题:
curl -X POST http://localhost:8088/api/agent/chat \ -H "Content-Type: application/json" \ -d '{"message": "帮我查一下北京天气,然后根据天气推荐穿什么"}'这个请求会触发多轮:第一轮 LLM 判断需要调get_weather,返回工具调用 JSON;Agent 解析后调 MCP Server 拿到天气;第二轮把天气结果喂回 LLM,让它推荐穿搭。如果两轮都成功,返回的data.reply里会有完整回答。
验证多轮是否真的走了多轮,看日志里的[Agent] 第 N 轮推理...。如果只打印了第 1 轮就返回,说明 LLM 没返回工具调用 JSON,可能是ToolRegistry.buildToolsDescription()里的工具描述没被模型理解,或者parseToolCall没提取到 JSON。如果打印了第 1 轮、第 2 轮然后报 401,那就是第二轮 LLM 请求的鉴权出了问题,回到三件套检查。
成功的结果长这样:
{ "code": 200, "data": { "reply": "北京今天晴,气温 18-26 度,建议穿薄外套加长裤。" } }到这一步,Agent 多轮对话调用 MCP Tools 的主链路就通了。接下来是排错,因为实际跑起来你大概率会遇到下面几种报错。
5. 本篇常见错排查:401、local proxy failed、reading choices
先说 401。多轮对话里 401 几乎只有三个来源:Key 写错、Key 被覆盖、请求打到了错误的 endpoint。Key 写错最好查,把application.properties里的llm.api-key复制出来,和 API Keys 页面上的对比,注意有没有换行符或空格。Key 被覆盖常见于 CC Switch 和 Cline 同时配置的情况,两个工具都往环境变量里写OPENAI_API_KEY,后写的覆盖先写的。解决办法是只保留一个工具的配置,或者给 Spring Boot 单独用llm.api-key这个自定义字段,不读环境变量。
请求打到错误 endpoint 是最隐蔽的。比如llm.api-url写成了http://localhost:8080/v1/chat/completions,而 8080 是某个 MCP Server 的端口。第一轮如果没触发工具调用,可能碰巧返回了东西;第二轮触发工具调用后,请求还是打到 8080,MCP Server 不认识/v1/chat/completions,返回 401 或 404。排查方法是在callLlm里加一行日志,打印实际请求的 URL:
System.out.println("[Agent] LLM 请求 URL: " + apiUrl);每次多轮推理都打印,一眼就能看出哪一轮打错了地址。
再说local proxy failed。这个报错不是 TaoToken 返回的,是你本机网络层返回的。常见原因是系统代理或某个本地代理工具把taotoken.net的请求拦截了,但代理本身没配好。表现是 curl 也失败,报Failed to connect to taotoken.net port 443或local proxy failed。解决办法是检查系统代理设置,把taotoken.net加入直连列表,或者临时关闭本地代理再试。注意这里不要用任何绕过网络管理的方式,只是把域名加入直连白名单,让请求正常出去。
第三个是reading 'choices'。这个报错来自 Agent 的callLlm方法:
JSONArray choices = resultMap.getJSONArray("choices"); if (choices != null && !choices.isEmpty()) { JSONObject message = choices.getJSONObject(0).getJSONObject("message"); return message.getString("content"); }如果返回的 JSON 里没有choices字段,getJSONArray返回 null,后面的choices.isEmpty()不会执行,但如果你在别处直接choices.getJSONObject(0)就会空指针。更常见的是返回了choices但里面是空数组,或者message字段不存在。根因通常是 LLM 返回了错误结构,比如 401 时返回的是{"error": {"message": "..."}},没有choices。所以看到reading 'choices'不要只改代码,先看response.body()里到底返回了什么。在callLlm里加一行:
System.out.println("[Agent] LLM 原始响应: " + response.body());打印出来,如果是{"error":...},那就是鉴权或参数问题,回到三件套。
还有一个 OAuth 相关的报错,出现在你用某些需要 OAuth 的工具时。表现是OAuth token expired或invalid_grant。TaoToken 的 API Key 是静态的sk-字符串,不涉及 OAuth 刷新,所以如果你看到 OAuth 报错,说明请求根本没走 TaoToken,而是走了别的 provider。检查 CC Switch 或 Cline 里有没有残留的 OAuth 配置,把它删掉,统一用 API Key。
最后是 MCP Server 连接失败。日志里出现[Agent] MCP 服务器连接失败: Connection refused,说明mcp.server.urls里的地址没有服务在监听。先确认 MCP Server 是否启动,端口是否对。如果是 STREAMABLE 协议,URL 要带/mcp路径,比如http://localhost:8080/mcp,不带路径可能连不上。这个报错不会导致启动失败,但会导致工具列表为空,多轮对话时isRelevant返回 false,直接走 LLM 聊天,看起来像「工具没被调用」。
排查顺序建议固定成:先 curl 验证 TaoToken 三件套,再看 Spring Boot 日志里的 LLM 请求 URL,再看原始响应 body,最后看 MCP Server 连接日志。按这个顺序,90% 的多轮中断都能定位到具体哪一层。
6. 把多轮链路固定下来之后
多轮对话调用 MCP Tools 的稳定性,本质上不取决于 Agent 循环写得多漂亮,而取决于每一轮 LLM 请求是否走同一条路。把 Base URL 固定成https://taotoken.net/api,Key 固定成一份,Model ID 固定成一个,CC Switch、Cline、Codex、Spring Boot 四处配置保持一致,401 和 proxy failed 就会从「随机出现」变成「可定位」。
如果你还在调 MCP Server 本身,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有 OpenAI 兼容格式的完整说明。想先手动验证模型连通性,去模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 发一条消息最快。长期跑 Agent、需要稳定额度的,看 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。Key 在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建。
下一篇会讲多轮对话里工具结果太长导致上下文超限、以及maxRounds设成 10 之后怎么避免死循环。那又是另一个坑了。