1. 从一次“工具明明注册了却调不到”说起
如果你正在用 Deep Agents 搭一个能自己拆任务、调工具、写文件的智能体,大概率会在某个时刻卡在同一个地方:create_agent和create_deep_agent这两个函数名字太像,参数表又都挺长,复制示例跑起来之后,工具到底有没有被真正注入、子代理有没有被触发,心里没底。更麻烦的是,当你把MultiServerMCPClient.get_tools拉回来的工具列表直接塞进create_deep_agent,有时候能跑,有时候静默失败,日志里只留下一句“no tool calls”,排查起来非常费劲。
这篇就按源码调用链把这几件事串起来:MultiServerMCPClient.get_tools负责把外部 MCP 服务器的工具“借”进来,create_agent是轻量构造器,create_deep_agent是带规划、文件系统、子代理的重型构造器。三者不是替代关系,而是流水线上的不同工位。同时我会给出一套可以直接复制的config.toml/settings.json骨架,以及用 TaoToken 统一 Key 接入的步骤,最后给一个验证函数调用是否真正生效的检查动作。适合已经能跑通基础 Agent、但想搞清楚参数差异和 MCP 注入细节的开发者。
2. TaoToken 前置:把模型入口统一成一个 Key
在拆函数之前,先把模型入口固定下来。Deep Agents 本身不绑定某一家模型,但你在create_deep_agent里写model="claude-sonnet-4-5-20250929"或者model="openai:gpt-4o"的时候,底层还是要走一个兼容 OpenAI 协议的端点。TaoToken 在这里的角色就是统一入口:一个 Key、一个 Base URL,模型名按需切换,省得在多个平台之间来回改环境变量。
接入动作分三步。第一步,去控制台创建一个 API Key,地址是https://taotoken.net/api-keys,注意这个 deep link 已经带了utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite,打开就是 Key 管理页。第二步,把 Key 写进环境变量,不要硬编码在代码里。第三步,把 Base URL 指向https://taotoken.net/api,这个地址不加 UTM,直接作为base_url使用。
export TAOTOKEN_API_KEY="sk-你的key" export OPENAI_BASE_URL="https://taotoken.net/api" export OPENAI_API_KEY="$TAOTOKEN_API_KEY"这样设置之后,LangChain 的ChatOpenAI和 Deep Agents 内部走的 OpenAI 兼容客户端都会自动读取这两个变量。如果你用的是 Claude 系列模型,同样通过这个端点转发,模型名写对即可。需要确认模型列表或者临时对话测试,可以打开模型对话页https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite,先手动发一条消息确认 Key 有效,再回到代码里跑 Agent。
注意:环境变量名用
OPENAI_API_KEY是为了兼容 LangChain 默认读取逻辑,实际值填 TaoToken 的 Key。不要同时保留其他平台的同名变量,否则会互相覆盖。
3. 可复制配置:config.toml 与 settings.json 骨架
Deep Agents 项目里通常会有两层配置:一层是项目级的config.toml,用来声明 MCP 服务器和默认模型;另一层是编辑器或运行时的settings.json,用来固定环境变量和启动参数。下面这两份骨架可以直接改路径使用。
config.toml负责描述 MCP 服务器连接方式。stdio适合本地起的 Python 脚本服务,streamable_http或sse适合远程服务。注意command和args要写绝对路径,相对路径在不同工作目录下会找不到文件。
[model] default = "claude-sonnet-4-5-20250929" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" [mcp.servers.math] transport = "stdio" command = "python" args = ["/abs/path/to/math_server.py"] [mcp.servers.search] transport = "streamable_http" url = "http://localhost:8000/mcp" [agent] system_prompt = "你是项目经理,把复杂任务拆给子代理执行" max_iterations = 25settings.json负责把环境变量和运行参数固定下来,避免每次开终端都要重新 export。如果你用 VS Code 或类似编辑器,可以放在.vscode/settings.json;如果是独立运行时,放在项目根目录由启动脚本读取。
{ "env": { "TAOTOKEN_API_KEY": "sk-你的key", "OPENAI_BASE_URL": "https://taotoken.net/api", "OPENAI_API_KEY": "${TAOTOKEN_API_KEY}" }, "deepagents": { "checkpoint_backend": "memory", "interrupt_on": { "delete_file": { "allowed_decisions": ["approve", "reject"] } } } }这两份配置合起来解决一个问题:模型入口、MCP 服务器、中断审批策略都不散落在代码里。后面无论你调create_agent还是create_deep_agent,都从配置读取,改一处即可全局生效。
4. 调用链拆解:get_tools、create_agent、create_deep_agent 的参数差异
4.1 MultiServerMCPClient.get_tools 返回的到底是什么
MultiServerMCPClient初始化时接收一个字典,key 是服务器别名,value 是连接参数。调用get_tools()之后返回的是一个BaseTool列表,LangChain 和 Deep Agents 都能直接消费。关键点在于:这个列表是异步拉取的,如果你在同步函数里直接调用会报错,必须放在async def里await。
import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient async def load_tools(): client = MultiServerMCPClient({ "math": { "command": "python", "args": ["/abs/path/to/math_server.py"], "transport": "stdio", }, "search": { "url": "http://localhost:8000/mcp", "transport": "streamable_http", }, }) all_tools = await client.get_tools() print([t.name for t in all_tools]) return all_tools tools = asyncio.run(load_tools())如果你只想拿某个服务器的工具,传server_name="math"即可。性能敏感场景可以用client.session("math")手动管理会话生命周期,避免每次调用重建连接。这一步的输出是后续两个构造函数的共同输入。
4.2 create_agent 的参数面
create_agent是轻量构造器,核心参数就三个:llm、tools、prompt。它适合任务边界清晰、工具调用轮次少的场景。所有工具调用的中间结果都会堆进上下文,任务一长 token 消耗会明显上升。
from langchain.agents import create_react_agent, AgentExecutor from langchain_openai import ChatOpenAI model = ChatOpenAI(model="claude-sonnet-4-5-20250929") agent = create_react_agent(llm=model, tools=tools, prompt=prompt) executor = AgentExecutor(agent=agent, tools=tools, verbose=True) executor.invoke({"input": "算一下 23 乘以 47"})注意create_react_agent和create_openai_tools_agent是两种不同范式,前者用 ReAct 提示词模板,后者依赖模型的 function calling 能力。选哪个取决于你的模型是否稳定支持工具调用。
4.3 create_deep_agent 的参数面与增量能力
create_deep_agent在tools之外多了几个关键参数:subagents用来挂载子代理,interrupt_on用来定义需要人工审批的工具,checkpointer用来支持中断恢复。它的内部机制是把大模型当调度器,文件系统当工作内存,子代理当隔离进程。
from deepagents import create_deep_agent from langgraph.checkpoint.memory import MemorySaver research_subagent = { "name": "research-agent", "description": "专门负责深度研究的子代理", "prompt": "你是个专业研究员,会进行深入资料搜集", "tools": [internet_search], "model": "openai:gpt-4o", } agent = create_deep_agent( model="claude-sonnet-4-5-20250929", tools=tools, subagents=[research_subagent], system_prompt="你是项目经理,把复杂任务拆给子代理做", interrupt_on={"delete_file": {"allowed_decisions": ["approve", "reject"]}}, checkpointer=MemorySaver(), )参数差异的本质是:create_agent只解决“模型 + 工具”的绑定,create_deep_agent额外解决“任务规划 + 上下文隔离 + 人工介入”。当你把MultiServerMCPClient.get_tools的结果传给create_deep_agent的tools参数时,MCP 工具和本地工具在框架眼里没有区别,都是BaseTool,所以可以混用。
5. 验证请求:确认函数调用真的生效
写完代码最怕的是“看起来跑了,其实工具没被调用”。下面这个检查动作分三层,从工具列表到实际调用链都能覆盖。
第一层,打印工具名和描述,确认 MCP 工具已经注入。
for t in tools: print(t.name, "|", t.description[:60])第二层,用astream观察事件流,看是否有tool_calls字段出现。如果模型只是普通回复而没有工具调用,说明提示词或工具描述不够明确。
async for chunk in agent.astream({ "messages": [{"role": "user", "content": "帮我查一下 MCP 协议的最新进展"}] }): if chunk.get("messages"): msg = chunk["messages"][-1] if getattr(msg, "tool_calls", None): print("触发工具调用:", [c["name"] for c in msg.tool_calls]) msg.pretty_print()第三层,在 MCP 服务器端加一行日志,确认请求真的到达了服务。本地stdio服务可以直接在工具函数里print,远程服务看访问日志。三层都通过,说明从get_tools到create_deep_agent再到实际执行的链路是通的。
如果你在验证过程中需要临时切换模型对比效果,可以打开模型对话页https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite手动测一条,确认模型本身对工具调用的支持程度,再回到代码里排查。
6. 本篇常见错排查
报错一:TypeError: object list can't be used in 'await' expression原因是在同步上下文里调用了get_tools()。解决方式是把它包进async def,用asyncio.run()或await执行。如果你在 Jupyter 里,直接用await client.get_tools()即可。
报错二:工具列表为空,get_tools()返回[]先检查 MCP 服务器的command和args路径是否为绝对路径,再确认transport类型和服务器实际协议一致。stdio服务如果启动失败,get_tools不会抛异常,只会返回空列表。可以在终端手动执行python /abs/path/to/math_server.py看是否正常启动。
报错三:create_deep_agent报checkpointer is required when interrupt_on is setinterrupt_on依赖检查点来保存中断状态,必须同时传checkpointer。用MemorySaver()做本地测试即可,生产环境换成持久化后端。
报错四:子代理没有被触发检查subagents里每个字典是否包含name、description、prompt、tools四个字段。description是主代理决定是否委派任务的依据,写得太模糊会导致主代理自己干活而不调用子代理。
报错五:模型返回 401 或invalid api key确认OPENAI_API_KEY和OPENAI_BASE_URL同时设置,且没有其他平台的同名变量覆盖。TaoToken 的 Key 在控制台https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite创建后立即生效,如果仍然报错,检查是否有多余空格或换行。
报错六:长任务跑到一半上下文爆炸这是create_agent的固有限制。把构造器换成create_deep_agent,让文件系统和子代理分担上下文压力。如果任务涉及多轮工具调用,优先用重型构造器。
7. 接入文档与 Coding Plan:把骨架落到项目里
配置骨架和验证动作都跑通之后,下一步是把它固化到项目结构里。建议把 MCP 服务器定义、模型入口、中断策略分别放在独立配置文件,代码里只做读取和组装。这样换模型或加工具时不需要改业务逻辑。
如果你打算长期用 Deep Agents 做编码类或 Agent 类任务,可以看一下 Coding Plan 页面https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,里面有针对长周期任务的额度方案。接入细节和参数说明在文档页https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,遇到get_tools或create_deep_agent的参数疑问可以先查那里。控制台入口在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,Key 管理和用量查看都在里面。
最后留一个实操建议:每次改完config.toml里的 MCP 服务器配置,先单独跑一遍get_tools打印工具名,确认注入成功再启动 Agent。这个习惯能省掉大量“Agent 不调工具”的排查时间。