1. 401 到底卡在哪一层:LangChain Agent 调用 MCP 的鉴权链路拆解
LangChain Agent 调用 MCP 报 401,最让人抓狂的地方在于:报错信息只有一句401 Unauthorized,但你根本不知道是模型这一层被拒了,还是 MCP 工具服务这一层被拒了。我先把整条链路拆开,你对着自己的工程一眼就能定位。
一个典型的 LangChain Agent + MCP 调用,请求会经过三个独立的鉴权关口:
第一层是Agent 到模型服务。LangChain 的create_agent在构造时会读取OPENAI_API_BASE和OPENAI_API_KEY,这一步如果 Key 无效、Base URL 写错、或者环境变量没被进程读到,就会在模型推理阶段直接 401。注意,这一层的 401 往往发生在 Agent 还没开始调用任何工具之前。
第二层是Agent 到 MCP Server。MultiServerMCPClient里配置的url指向你的 MCP 服务(比如http://localhost:8000/mcp),如果这个服务本身要求鉴权头,而客户端没带,或者带了但格式不对,就会在client.get_tools()或工具调用时抛 401。
第三层是MCP Server 内部再去调用外部 API。你的 MCP 工具函数里如果又去请求了某个需要 Key 的第三方服务,那这个 401 是工具内部产生的,会被包装成 ToolMessage 返回,而不是直接抛异常。
这三层的 401 表现完全不同。第一层通常在agent.ainvoke()一开始就炸;第二层在get_tools()阶段或工具调用瞬间炸;第三层不会炸,而是工具返回一段错误文本,Agent 拿到后可能自己"编"一个回答。
我实测下来,绝大多数人遇到的 401 其实是第一层和第二层的配置错位:把 MCP 的 endpoint 和模型的 endpoint 混在同一个环境变量里,或者 Key 只配了一边。下面这张对照表可以先帮你快速判断:
| 报错出现时机 | 大概率层级 | 典型原因 |
|---|---|---|
create_agent后首次ainvoke立即 401 | 模型层 | OPENAI_API_KEY无效或OPENAI_API_BASE指向错误 |
client.get_tools()阶段 401 | MCP 层 | MCP Server 要求鉴权,客户端未带 header |
| 工具调用返回文本含 401 | 工具内部 | MCP 工具函数请求外部 API 时 Key 缺失 |
| 偶发 401,重试有时成功 | 模型层 | Key 额度耗尽或并发限流被拒 |
搞清楚这三层,你就不用在代码里到处加 print 了。接下来我把 endpoint 统一到 TaoToken 的 Key/API 通道上,让模型层和工具层的鉴权来源一致,这样排查面直接缩小一半。
2. 把 endpoint 统一到 TaoToken:前置准备与 Key 获取
在动手改配置之前,先把"鉴权来源"这件事想清楚。401 的本质是"服务端不认识你",而服务端认识你的唯一凭证就是 Key + Base URL 这一对组合。如果你的模型走一个通道、MCP 工具走另一个通道,两边的 Key 格式、鉴权头写法、Base URL 路径规则都不一样,出错概率自然翻倍。
TaoToken 在这里的作用,是给你一个统一的 API 通道:模型对话、Coding Plan、以及通过 API 转发的请求,都走同一套 Key 和同一个 Base URL 规则。这样你在 LangChain 里只需要维护一份凭证,MCP 工具内部如果需要调用模型能力,也复用同一份,排查时只需要验证"这一个 Key 是否有效"。
前置准备分三步。
第一步,拿到 API Key。访问控制台创建:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite创建后你会得到一串以sk-开头的 Key。注意,这个 Key 只在创建时完整显示一次,复制后立刻存到环境变量或密钥管理里,别直接硬编码进 Git 仓库。
第二步,确认 Base URL。TaoToken 的 API 入口是:
https://taotoken.net/api这个地址是给 OpenAI 兼容客户端用的,LangChain 的ChatOpenAI或create_agent底层走的就是 OpenAI 协议,所以直接把OPENAI_API_BASE指向它即可。注意末尾不要多加/v1,具体路径规则以接入文档为准,写错路径也会返回 401 或 404。
第三步,确认你要用的 Model ID。不同模型在 TaoToken 上的标识名不一样,别想当然写gpt-4。去模型对话页面确认可用模型:
https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite如果你是要长期跑编码类 Agent,可以顺带看下 Coding Plan 的额度说明:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewriteKey、Base URL、Model ID 这三件套凑齐,后面所有配置都围绕它们展开。我建议你把它们写进一个.env文件,而不是散落在代码各处,这样 401 排查时只需要检查一个地方。
3. 可复制的 endpoint 与鉴权配置片段
这一节是重点,我给出可以直接抄的配置。先看环境变量文件.env:
# .env OPENAI_API_BASE=https://taotoken.net/api OPENAI_API_KEY=sk-你的TaoToken密钥 OPENAI_MODEL=gpt-4.1-2025-04-14 MCP_SERVER_URL=http://localhost:8000/mcp注意这里我把模型 endpoint 和 MCP endpoint 分成了两个变量。很多人 401 就是因为把MCP_SERVER_URL也写成了OPENAI_API_BASE,结果 MCP 客户端拿着模型 Key 去请求本地 MCP 服务,服务端当然不认识。
然后是 LangChain Agent 的构建代码,把模型层配置显式写出来:
import os import asyncio from dotenv import load_dotenv from langchain.agents import create_agent from langchain_openai import ChatOpenAI from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_mcp_adapters.interceptors import MCPToolCallRequest load_dotenv() # 模型层:显式传入 base_url 和 api_key,避免依赖隐式环境变量 llm = ChatOpenAI( model=os.environ["OPENAI_MODEL"], base_url=os.environ["OPENAI_API_BASE"], api_key=os.environ["OPENAI_API_KEY"], timeout=60, max_retries=2, ) async def logging_interceptor( request: MCPToolCallRequest, handler, ): print(f"[MCP] calling tool: {request.name} args: {request.args}") result = await handler(request) print(f"[MCP] tool {request.name} returned: {result}") return result client = MultiServerMCPClient( { "weather": { "transport": "http", "url": os.environ["MCP_SERVER_URL"], } }, tool_interceptors=[logging_interceptor], ) async def main(): tools = await client.get_tools() agent = create_agent( model=llm, tools=tools, system_prompt="你是个很好的助手,调用工具后请基于工具返回结果回答。", ) result = await agent.ainvoke( {"messages": [{"role": "user", "content": "广州天气如何?"}]} ) for msg in result["messages"]: print(type(msg).__name__, getattr(msg, "content", "")) if __name__ == "__main__": asyncio.run(main())这里有两个关键改动,直接决定 401 会不会出现。
第一个改动:ChatOpenAI显式传base_url和api_key。原示例里用的是os.environ["OPENAI_API_BASE"]这种隐式读取,一旦你的进程没加载.env,或者被其他库覆盖了环境变量,就会静默走到默认的 OpenAI 官方地址,然后拿着 TaoToken 的 Key 去请求官方,必然 401。显式传参让配置来源唯一。
第二个改动:MCP 客户端的url单独从MCP_SERVER_URL读取,和模型 endpoint 彻底解耦。这样即使你后面把 MCP 服务部署到远程,也不会误改模型配置。
如果你用的是 Claude Code 或 Cline 这类工具,配置文件的写法略有不同。以 Claude Code 的 settings 为例,需要写全三件套:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的TaoToken密钥", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }注意这里的变量名是ANTHROPIC_前缀,因为 Claude Code 走的是 Anthropic 协议。如果你把它和 OpenAI 协议的变量混用,同样会 401。Cline 的 MCP 配置里,Base URL、Key、Model ID 三件套一个都不能少,缺任何一个都会在连接阶段被拒。
配置写完后,先别急着跑完整 Agent,用下面这个最小验证脚本单独测模型层:
import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() llm = ChatOpenAI( model=os.environ["OPENAI_MODEL"], base_url=os.environ["OPENAI_API_BASE"], api_key=os.environ["OPENAI_API_KEY"], ) print(llm.invoke("只回复两个字:正常").content)如果这一步就 401,那问题 100% 在模型层,和 MCP 无关,先解决 Key 和 Base URL。如果这一步通过,再往下测 MCP 层。
4. 逐步验证:从模型层到 MCP 层的成功请求长什么样
验证要分层做,一层通过再进下一层,否则你永远不知道 401 是哪来的。
第一层验证:模型层连通性。跑上面那个最小脚本,预期输出是"正常"两个字。如果输出正常,说明 TaoToken 的 Key、Base URL、Model ID 三件套没问题。如果这里报 401,检查三件事:Key 是否复制完整(有没有漏字符)、Base URL 是否是https://taotoken.net/api(别加/v1)、Model ID 是否是模型对话页面里列出的可用名称。
第二层验证:MCP 工具发现。单独跑client.get_tools(),不接 Agent:
async def check_tools(): tools = await client.get_tools() for t in tools: print(t.name, t.description) asyncio.run(check_tools())预期输出类似:
get_weather 获取指定城市的天气信息, 参数为城市名称如果这一步 401,说明 MCP Server 本身要求鉴权,而你的客户端没带 header。检查你的 MCP Server 是否在启动时配置了 token 校验,如果有,需要在MultiServerMCPClient的配置里加上 headers:
client = MultiServerMCPClient( { "weather": { "transport": "http", "url": os.environ["MCP_SERVER_URL"], "headers": {"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"}, } }, tool_interceptors=[logging_interceptor], )第三层验证:完整 Agent 调用。前两层都通过后,跑完整流程,预期看到拦截器打印:
[MCP] calling tool: get_weather args: {'city': '广州'} [MCP] tool get_weather returned: ...广州今日晴,28℃...然后 Agent 的最终回答应该基于工具返回结果,比如"广州今日晴,28℃"。这里有个坑:原示例里 Agent 拿到工具结果后,最终回答却是"当前无法获取广州的天气信息",这是因为模型没有正确理解 ToolMessage 的结构。解决办法是在 system_prompt 里明确要求"必须基于工具返回的 structuredContent 回答",或者检查create_agent的版本是否支持工具结果回传。
成功请求的特征是:拦截器打印了工具调用、工具返回了结构化内容、Agent 最终回答引用了工具结果。三者缺一,说明链路某处断了。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节我把真实遇到过的报错逐个拆开,你对着自己的日志找。
报错一:401 Unauthorized且发生在ainvoke最开始。这是模型层鉴权失败。最常见原因是.env没被加载,load_dotenv()写在了ChatOpenAI初始化之后。检查顺序:先load_dotenv(),再读环境变量。另一个原因是 Key 前后有空格或换行,复制时带进去了,用print(repr(os.environ["OPENAI_API_KEY"]))看一眼。
报错二:local proxy failed或连接被拒。这个通常不是 401,而是 MCP Server 没启动或端口不对。检查MCP_SERVER_URL里的端口是否和mcp.run(transport="streamable-http")实际监听的端口一致。FastMCP 默认端口可能不是 8000,启动日志里会打印实际地址,以日志为准。
报错三:Error reading choices或response parsing failed。这个报错说明请求发出去了、鉴权也过了,但返回的响应格式不符合 OpenAI 协议。常见于 Base URL 写成了非 OpenAI 兼容的路径,或者 Model ID 写错导致服务端返回了错误页面的 HTML。检查 Base URL 是否是https://taotoken.net/api,Model ID 是否在可用列表里。
报错四:OAuth相关错误或invalid_grant。如果你用的是 Claude Code 或某些需要 OAuth 的工具,报这个错说明你用了 OAuth 流程但没走通。解决办法是改用 API Key 方式,在 settings 里配置ANTHROPIC_API_KEY而不是依赖 OAuth 登录。三件套(Base URL + Key + Model ID)写全,OAuth 报错自然消失。
报错五:工具调用返回文本里含 401。这是 MCP 工具函数内部请求外部 API 失败。检查你的工具函数里是否有硬编码的第三方 Key,或者是否复用了OPENAI_API_KEY去请求了别的服务。工具内部的鉴权要单独配置,不能和模型层混用。
排查时有个通用技巧:在拦截器里打印完整的 request 和 response,包括 headers。401 的根因往往就藏在 headers 里——要么 Authorization 头缺失,要么格式不是Bearer sk-xxx。
6. 把调用链路固定下来:长期编码与 Agent 场景的配置建议
排查完 401 只是第一步,真正省心的是把配置固定成一套可复用的模板,下次新建 Agent 工程直接抄。
我的建议是维护一个config.py,把所有 endpoint 和 Key 的读取集中在一处:
import os from dotenv import load_dotenv load_dotenv() class Config: OPENAI_BASE_URL = os.environ["OPENAI_API_BASE"] OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] OPENAI_MODEL = os.environ["OPENAI_MODEL"] MCP_SERVER_URL = os.environ["MCP_SERVER_URL"] MCP_TOKEN = os.environ.get("MCP_TOKEN", "") @classmethod def validate(cls): missing = [k for k in ["OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_MODEL"] if not getattr(cls, k)] if missing: raise ValueError(f"缺少配置: {missing}")启动时先调Config.validate(),缺配置直接报错,而不是等到 401 才发现。
对于长期跑的编码类 Agent,建议把模型层指向 TaoToken 的 Coding Plan 通道,额度更稳定,不会因为单次请求限流导致偶发 401。配置入口:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite如果你需要管理多个 Key(比如开发和生产分开),去 API Keys 页面创建独立的 Key:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite每个 Key 单独配额度,出问题时能快速定位是哪个环境的 Key 失效。
最后,接入文档里有完整的协议说明和路径规则,遇到 Base URL 路径不确定时以文档为准:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite把这三件套(Base URL + Key + Model ID)固定成模板,MCP 的 endpoint 单独管理,401 这类问题基本就绝迹了。真遇到时,按第 4 节的三层验证法逐层跑一遍,五分钟内就能定位到具体是哪一层被拒。