用Langchain+FastMCP搭建带上下文记忆的ReAct Agent:TaoToken统一Key接入实战 1. 多工具 Agent 的 Key 分散问题与上下文记忆场景做 ReAct Agent 最烦的不是写 prompt而是工具一多Key 就开始满天飞。我最近在搭一个带上下文记忆的 Agent需要同时调用天气查询、数据库查询、网页抓取三个 MCP Server每个 Server 背后可能连的是不同厂商的模型或服务结果就是.env里塞了四五个 API Key鉴权逻辑散落在各个文件里改一个 Base URL 要翻三个目录。这种场景下Langchain 负责编排 Agent 的思考-行动循环FastMCP 负责把本地函数暴露成标准工具而模型调用这一层如果继续用分散的 Key整个骨架就变得很难复用。这篇文章要解决的就是这个问题用 TaoToken 的统一 Key 和 API 通道把模型鉴权收敛到一个auth.json或环境变量里让 Langchain 的 ChatOpenAI 和 FastMCP 的工具调用都走同一条出口。目标很明确——你跟着配完能跑出一个带多轮记忆、能调用多个 MCP 工具、并且换模型时只改一个 Model ID 的 ReAct Agent 骨架。适合谁看如果你已经写过基础的 Langchain Chat Model 调用知道SystemMessage、HumanMessage、AIMessage的区别也试过用 FastMCP 把函数变成工具但卡在“多个工具 多轮对话 统一鉴权”这一步那这篇就是给你准备的。如果你还没接触过 MCP也没关系我会把 FastMCP 的 Server 端和 Langchain 的 Client 端都拆开讲代码可以直接复制。核心检索词先摆出来Langchain ReAct Agent 上下文记忆、FastMCP 多工具调用、TaoToken 统一 Key 接入。这三个词贯穿全文你可以在每个章节里找到对应的落地步骤。先说一下整体架构。Agent 的骨架分三层最底层是模型通道用 TaoToken 的 API Base URL 和统一 KeyLangchain 的ChatOpenAI指向这个地址中间层是工具层用 FastMCP 写两个本地工具比如查天气和查时间通过MultiServerMCPClient以 stdio 方式挂载最上层是 Agent 循环用create_react_agent把模型和工具绑在一起维护一个chat_history列表来保存上下文。这样每次用户输入Agent 会先看历史消息再决定调哪个工具工具返回结果后继续推理直到给出最终回答。为什么强调“上下文记忆”因为 ReAct Agent 默认每次invoke如果只传当前问题它会忘记上一轮调过什么工具、用户之前说过什么偏好。比如用户先说“查一下北京天气”再说“那上海呢”如果没有历史消息Agent 不知道“那”指代的是天气查询。所以chat_history必须把HumanMessage、AIMessage、ToolMessage都存下来下一轮一起传给 Agent。Langgraph 的create_react_agent返回的result[messages]已经包含了完整的消息链直接替换chat_history就行不用手动拼。这里有个容易踩的坑很多人用agent.invoke而不是agent.ainvoke结果在异步的 MCP Client 里报错。因为MultiServerMCPClient的get_tools()是异步的整个 Agent 调用链必须放在async def main()里用asyncio.run(main())启动。如果你在同步函数里调会看到RuntimeError: no running event loop或者coroutine was never awaited。这个后面排障章节会细说。再说 Key 分散的具体痛点。假设你有三个工具天气查询走一个免费 API数据库查询走内部服务网页抓取走另一个模型。传统做法是每个工具内部自己读环境变量或者每个 MCP Server 启动时传不同的 Key。但 Agent 的模型调用本身也需要 Key如果模型和工具用的不是同一个厂商你就得在 Langchain 和 FastMCP 两边分别配置。TaoToken 的统一 Key 解决的是模型调用这一层——不管底层实际路由到哪个模型Langchain 的ChatOpenAI只需要一个openai_api_key和一个openai_api_base。工具层的 Key 如果也是模型驱动的比如工具内部调 LLM 做摘要同样可以复用这个 Key。这样你的配置文件从“N 个 Key 对应 N 个服务”变成“一个 Key 对应一个 API 通道通道内部按 Model ID 路由”。接下来我会先讲 TaoToken 的前置准备包括 Base URL 和auth.json的写法然后给出完整的可复制配置包括 FastMCP Server 代码和 Langchain Agent 代码接着验证多轮对话和工具调用链最后把常见的 401、local proxy failed、OAuth 报错逐个拆解。你按顺序操作半小时内能跑通。2. TaoToken 统一 Key 与 API 通道前置配置在写 Agent 代码之前先把模型通道配好。TaoToken 的官网是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址是https://taotoken.net/api注意 API 地址后面不加 UTM 参数直接用于代码里的openai_api_base。你需要先去控制台创建一个 API Key然后拿到两个东西Base URL 和 Key 字符串。控制台入口在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite进去之后左侧菜单找 API Keys点创建复制生成的 Key。这个 Key 就是后面auth.json里的OPENAI_API_KEY。模型 ID 可以在模型对话页面查看或者直接看文档里的模型列表。常用的 Model ID 格式类似deepseek/deepseek-chat-v3-0324或gpt-4o-mini具体以你控制台看到的为准。为什么用auth.json而不是纯.env因为 Langchain 的ChatOpenAI支持从环境变量读但 FastMCP 的 Server 如果也要调模型用 JSON 文件更统一。而且auth.json可以放在项目根目录被多个脚本共享避免每个文件都写一遍load_dotenv()。当然你也可以继续用.env两者不冲突。下面给出两种写法你选一种。先看.env写法适合快速测试# .env OPENAI_API_KEYsk-你的TaoTokenKey OPENAI_API_BASEhttps://taotoken.net/api MODEL_IDdeepseek/deepseek-chat-v3-0324然后在 Python 里用load_dotenv()加载ChatOpenAI的参数这样写from dotenv import load_dotenv from langchain_openai import ChatOpenAI import os load_dotenv() llm ChatOpenAI( modelos.getenv(MODEL_ID), openai_api_keyos.getenv(OPENAI_API_KEY), openai_api_baseos.getenv(OPENAI_API_BASE), temperature0 )再看auth.json写法适合多脚本共享{ OPENAI_API_KEY: sk-你的TaoTokenKey, OPENAI_API_BASE: https://taotoken.net/api, MODEL_ID: deepseek/deepseek-chat-v3-0324 }读取的时候用json.loadimport json with open(auth.json, r) as f: auth json.load(f) llm ChatOpenAI( modelauth[MODEL_ID], openai_api_keyauth[OPENAI_API_KEY], openai_api_baseauth[OPENAI_API_BASE], temperature0 )注意openai_api_base的值必须是https://taotoken.net/api不要在后面加/v1或者斜杠否则可能报 404。如果你用的是 OpenAI 官方 SDK 的某些版本它可能会自动补/v1但 Langchain 的ChatOpenAI不会所以直接写完整路径。实测下来https://taotoken.net/api这个地址在 Langchain 里能正常路由到模型。如果你要用 Claude Code 或者 Cline 这类工具配置方式类似但需要填三个东西Base URL、API Key、Model ID。比如在 Claude Code 的配置里Base URL 填https://taotoken.net/apiAPI Key 填你的 KeyModel ID 填claude-3-5-sonnet之类的。Cline 的 MCP 配置也是同理后面章节会给出具体的 JSON 片段。这里提醒一下不要把 Key 硬编码在代码里提交到 Git。用.env或者auth.json的时候记得加到.gitignore。如果你在团队里共享可以用环境变量注入或者用 TaoToken 控制台的多 Key 管理功能给不同项目分配不同 Key。配置完模型通道后先跑一个最简单的 Chat 测试确认 Key 和 Base URL 没问题from dotenv import load_dotenv from langchain_openai import ChatOpenAI import os load_dotenv() llm ChatOpenAI( modelos.getenv(MODEL_ID), openai_api_keyos.getenv(OPENAI_API_KEY), openai_api_baseos.getenv(OPENAI_API_BASE), temperature0 ) result llm.invoke(你好请回复一句测试成功) print(result.content)如果输出类似“测试成功”的内容说明模型通道通了。如果报 401检查 Key 是否复制完整如果报Connection error检查 Base URL 是否写错。这一步过了再往下走否则后面 Agent 报错你分不清是模型问题还是工具问题。3. 可复制配置FastMCP Server 与 Langchain Agent 骨架这一章给出完整的可复制代码。分两部分先写两个 FastMCP Server一个查天气模拟一个查时间再写 Langchain Agent 主程序用MultiServerMCPClient挂载这两个 Server并用create_react_agent创建带记忆的 ReAct Agent。先看 FastMCP Server 的代码。新建目录mcp_servers/里面放两个文件weather_server.py和time_server.py。天气 Server 用模拟数据避免依赖外部 API时间 Server 返回当前时间。两个 Server 都用stdio传输因为本地调用不需要网络。weather_server.pyfrom mcp.server.fastmcp import FastMCP from loguru import logger mcp FastMCP(weather_server, log_levelERROR) mcp.tool() async def get_weather(city: str) - str: 获取指定城市的天气信息。 Args: city (str): 城市名称例如 北京、上海。 Returns: str: 天气描述字符串。 logger.info(f查询天气: {city}) # 模拟数据实际可替换为真实 API 调用 weather_data { 北京: 晴25°C微风, 上海: 多云28°C东南风3级, 广州: 雷阵雨30°C湿度80% } return weather_data.get(city, f未找到 {city} 的天气数据) if __name__ __main__: mcp.run(transportstdio)time_server.pyfrom mcp.server.fastmcp import FastMCP from datetime import datetime from loguru import logger mcp FastMCP(time_server, log_levelERROR) mcp.tool() async def get_current_time(timezone: str Asia/Shanghai) - str: 获取指定时区的当前时间。 Args: timezone (str): 时区名称默认 Asia/Shanghai。 Returns: str: 当前时间字符串。 logger.info(f查询时间: {timezone}) now datetime.now() return f{timezone} 当前时间: {now.strftime(%Y-%m-%d %H:%M:%S)} if __name__ __main__: mcp.run(transportstdio)注意mcp.tool()装饰器下面的函数必须有清晰的 Docstring因为 Langchain 会把 Docstring 作为工具描述传给 LLM。函数名和参数名也要有意义比如get_weather和city不要用func1和arg1。log_levelERROR是为了避免 FastMCP 在 stdio 模式下输出日志到 stdout干扰 MCP 协议通信。如果你需要调试可以把日志写到文件不要打印到控制台。接下来是 Langchain Agent 主程序agent_main.py。这个文件负责加载模型、挂载 MCP Server、创建 ReAct Agent、维护上下文记忆。import asyncio import json from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.prebuilt import create_react_agent import os load_dotenv() system_prompt 你是一个智能助手可以查询天气和时间。 当用户询问天气时调用 get_weather 工具。 当用户询问时间时调用 get_current_time 工具。 如果用户的问题需要多个工具请依次调用。 回答时请结合工具返回的结果不要编造数据。 async def main(): # 1. 初始化模型走 TaoToken 统一通道 llm ChatOpenAI( modelos.getenv(MODEL_ID), openai_api_keyos.getenv(OPENAI_API_KEY), openai_api_baseos.getenv(OPENAI_API_BASE), temperature0 ) # 2. 挂载两个 FastMCP Server client MultiServerMCPClient( { weather: { command: python, args: [mcp_servers/weather_server.py], transport: stdio }, time: { command: python, args: [mcp_servers/time_server.py], transport: stdio } } ) tools await client.get_tools() print(f已加载工具: {[t.name for t in tools]}) # 3. 创建 ReAct Agent agent create_react_agent(llm, tools) # 4. 初始化上下文记忆 chat_history [SystemMessage(contentsystem_prompt)] while True: user_input input(\n请输入问题输入 exit 退出: ) if user_input.lower() exit: break chat_history.append(HumanMessage(contentuser_input)) # 5. 调用 Agent传入完整历史 result await agent.ainvoke({messages: chat_history}) # 6. 提取最终回复并更新历史 final_message result[messages][-1] print(f\nAgent: {final_message.content}) # 用 Agent 返回的完整消息链替换历史保留 ToolMessage chat_history result[messages] if __name__ __main__: asyncio.run(main())这段代码的关键点有几个。第一MultiServerMCPClient的配置里command用pythonargs是 Server 脚本的相对路径。如果你用uv管理环境可以改成command: uv, args: [run, mcp_servers/weather_server.py]。第二tools await client.get_tools()是异步的必须在async def main()里调用。第三create_react_agent(llm, tools)返回的是一个 Langgraph 的 Agent 对象调用方式是await agent.ainvoke({messages: chat_history})。第四result[messages]包含了本轮所有消息包括HumanMessage、AIMessage、ToolMessage直接赋值给chat_history就能保留上下文。如果你要用 Cline 或者 Claude Code 的 MCP 配置格式类似这样{ mcpServers: { weather: { command: python, args: [mcp_servers/weather_server.py], transport: stdio }, time: { command: python, args: [mcp_servers/time_server.py], transport: stdio } } }注意 Cline 的 MCP 配置里transport字段可能叫type具体看版本。但核心三件套不变Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 填你的 TaoToken KeyModel ID 填你控制台选的模型。依赖安装方面你需要这些包pip install langchain langchain-openai langchain-mcp-adapters langgraph mcp fastmcp loguru python-dotenv httpx如果你用uv可以uv add同样的包。安装完后先单独跑一下python mcp_servers/weather_server.py如果它卡住不输出说明 stdio 模式正常它在等 MCP 协议输入。按 CtrlC 退出然后跑python agent_main.py。4. 验证多轮对话记忆与工具调用链代码写完后跑起来验证。启动agent_main.py你会看到“已加载工具: [get_weather, get_current_time]”然后进入输入循环。下面是我实测的对话流程你可以照着输入观察 Agent 是否保持记忆并正确调用工具。第一轮输入“北京天气怎么样” Agent 应该调用get_weather返回“北京晴25°C微风”。控制台输出类似请输入问题输入 exit 退出: 北京天气怎么样 Agent: 北京当前天气为晴气温25°C微风。第二轮输入“那上海呢” 这里的关键是“那”指代天气。如果上下文记忆生效Agent 会知道上一轮在问天气于是调用get_weather并传入“上海”。输出请输入问题输入 exit 退出: 那上海呢 Agent: 上海当前天气为多云气温28°C东南风3级。第三轮输入“现在几点了” Agent 应该调用get_current_time返回当前时间。输出请输入问题输入 exit 退出: 现在几点了 Agent: Asia/Shanghai 当前时间: 2025-01-15 14:30:22第四轮输入“北京和上海哪个更热” 这一轮需要 Agent 结合前两轮的天气数据做比较。如果记忆完整它会从历史消息里找到北京25°C和上海28°C然后回答“上海更热”。输出请输入问题输入 exit 退出: 北京和上海哪个更热 Agent: 根据之前的查询北京25°C上海28°C所以上海更热。如果第四轮 Agent 回答“我不知道”或者重新调用工具说明chat_history没有正确保留ToolMessage。检查代码里chat_history result[messages]这一行确保它替换了旧历史。另外create_react_agent默认会把所有消息传给模型包括工具调用的中间结果所以模型能看到之前的天气数据。为了更直观地验证工具调用链你可以在agent_main.py里加一行打印result await agent.ainvoke({messages: chat_history}) for msg in result[messages]: print(f[{msg.__class__.__name__}] {msg.content[:100]})这样你能看到完整的消息序列比如[HumanMessage] 北京天气怎么样 [AIMessage] [ToolMessage] 北京晴25°C微风 [AIMessage] 北京当前天气为晴气温25°C微风。AIMessage的空内容表示模型决定调用工具ToolMessage是工具返回结果最后一个AIMessage是最终回答。这个链条就是 ReAct 的“思考-行动-观察”循环。如果你要验证多工具混合调用可以输入“查一下广州天气然后告诉我现在时间。” Agent 应该先调get_weather再调get_current_time最后合并回答。输出类似Agent: 广州当前天气为雷阵雨30°C湿度80%。当前时间为 Asia/Shanghai 2025-01-15 14:32:10。实测下来只要system_prompt里写清楚工具用途模型基本能正确选择工具。如果模型选错工具比如问天气却调了时间检查 Docstring 是否清晰或者把temperature设为 0 减少随机性。还有一个验证点换模型。把.env里的MODEL_ID从deepseek/deepseek-chat-v3-0324改成gpt-4o-mini重启 Agent其他代码不变。如果一切正常说明 TaoToken 的统一通道确实做到了“换模型只改一个 ID”。这个特性在多工具场景下特别有用因为你可以让不同 Agent 用不同模型但共享同一个 Key 和 Base URL。5. 常见报错排查401、local proxy failed、OAuth 与 reading choices这一章把我在搭建过程中遇到的报错逐个拆解。你如果卡在某一步先对照这里的错误信息找原因。401 Unauthorized最常见。报错信息通常是openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}。原因有三个Key 复制不完整、Key 被删除、Base URL 写错导致请求发到了错误的服务。检查.env里的OPENAI_API_KEY是否以sk-开头有没有多余空格。然后确认OPENAI_API_BASE是https://taotoken.net/api不是https://taotoken.net或https://taotoken.net/v1。如果你用auth.json检查 JSON 格式是否正确有没有漏引号。local proxy failed报错类似httpx.ConnectError: [Errno 111] Connection refused或openai.APIConnectionError: Connection error。这通常是因为你的网络环境无法直连taotoken.net或者本地设置了代理但代理没启动。先检查是否能 ping 通taotoken.net如果不行检查你的网络设置。注意不要使用任何非官方的网络工具直接用系统默认网络即可。如果你在公司内网可能需要配置HTTP_PROXY环境变量但确保代理地址正确。另外openai_api_base如果写成https://taotoken.net/api/带尾部斜杠某些版本会拼接成//v1/chat/completions导致 404去掉斜杠即可。OAuth 相关报错如果你用 Claude Code 或 Cline 接入可能会看到OAuth token expired或invalid_grant。这是因为这些工具默认走 OAuth 流程但 TaoToken 的 API Key 是直接鉴权。解决方法是在配置里把认证方式改成 API Key不要选 OAuth。具体操作Claude Code 的settings.json里apiKey字段填你的 TaoToken KeybaseUrl填https://taotoken.net/api。Cline 的 MCP 配置里env字段加OPENAI_API_KEY和OPENAI_API_BASE。如果你看到OAuth callback failed说明工具在尝试浏览器授权直接跳过用 Key 模式。reading choices 报错报错信息类似KeyError: choices或IndexError: list index out of range。这通常是因为模型返回的 JSON 结构不符合 OpenAI 格式或者请求被中间层拦截返回了 HTML。检查openai_api_base是否指向了正确的 API 路径。另外如果你用的模型 ID 不存在TaoToken 可能返回错误信息而不是标准 choices 数组。去控制台确认 Model ID 拼写正确比如deepseek/deepseek-chat-v3-0324不要写成deepseek-chat。如果问题依旧把temperature设为 0并打印完整响应result llm.invoke(测试) print(result)MCP 工具加载失败报错Failed to load tools或MCP server not responding。检查MultiServerMCPClient里的command和args路径是否正确。如果你用相对路径确保从项目根目录运行python agent_main.py。另外FastMCP Server 的log_level必须设为ERROR否则日志会输出到 stdout干扰 MCP 的 JSON-RPC 通信。如果你看到json.decoder.JSONDecodeError就是这个问题。上下文记忆丢失Agent 不记得上一轮对话。检查chat_history result[messages]是否在循环内执行。如果你写成chat_history.append(result[messages][-1])只会保留最后一条 AIMessage丢失 ToolMessage 和之前的 HumanMessage。正确做法是用完整消息链替换。另外create_react_agent默认的messages状态会保留所有消息但如果你手动截断就会丢记忆。异步调用报错RuntimeError: no running event loop或coroutine get_tools was never awaited。这是因为你在同步函数里调用了异步的client.get_tools()或agent.ainvoke()。确保整个流程在async def main()里并用asyncio.run(main())启动。如果你在 Jupyter Notebook 里跑直接用await main()不要用asyncio.run()。模型返回空内容Agent 输出空白。检查system_prompt是否太长导致模型忽略或者temperature太高。把temperature设为 0并简化 prompt。另外如果工具返回的结果太长模型可能截断可以在工具函数里限制返回长度。排障时建议打开 Langchain 的调试日志import langchain langchain.debug True这样你能看到每次模型调用的完整 prompt 和响应快速定位是模型问题还是工具问题。6. 接入文档与 API Key 获取入口如果你还没创建 TaoToken 的 API Key直接去控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite。创建后复制 Key填到.env或auth.json里。API Keys 管理页面在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite可以随时查看和重置 Key。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有各个模型 ID 和 Base URL 的说明。如果你要验证模型是否可用可以用模型对话页面直接测试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。长期编码或 Agent 项目建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。Claude Code 的接入配置参考https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite。Cline MCP 的配置类似核心三件套是 Base URLhttps://taotoken.net/api、API Key 你的 Key、Model ID 你选的模型。把这三点填对Agent 就能跑起来。最后说一个实用技巧把agent_main.py里的chat_history持久化到文件下次启动时加载这样 Agent 能跨会话记住上下文。用json序列化消息列表注意ToolMessage需要特殊处理。如果你不需要跨会话每次启动重置历史即可。这个骨架可以直接复用到客服机器人、数据分析助手等场景换工具只需改 FastMCP Server 的代码模型通道不用动。