1. Tool Calling 的执行边界
1.1 模型只生成 Tool Call
大模型不会直接执行 Python、访问数据库或启动 Shell。应用把 Tool 名称、描述和 JSON Schema
发给模型,模型返回结构化调用意图,真正执行代码的是 Agent Runtime 或 MCP Server 。 模型可能在拿到结果后继续调用其他工具,因此这是一个循环,而不是一次固定的函数调用。
1.2 Runtime 执行代码
同一个 Tool Call 在不同层看到的对象并不相同:
| 层级 | 看到的对象 | 负责什么 |
|---|---|---|
| 模型 | Tool Schema、Tool Call、Tool Result | 决定是否调用以及如何继续推理 |
| LangGraph Runtime | AIMessage.tool_calls、ToolMessage | 校验参数、执行 Tool、维护消息状态 |
| MCP Adapter | LangChain Tool 与 MCP Content 的转换 | 建立 Session、发送 tools/call、转换结果 |
| MCP Server | 业务参数与 CallToolResult | 执行业务代码、返回内容或错误状态 |
| 业务系统 | 用户、租户、资源和事务 | 最终认证、授权、幂等与审计 |
Tool Description 可以帮助模型做选择,但不能承担权限控制。只要 Tool 能写文件、发邮件、修改
数据库或执行命令,服务端就必须独立完成身份校验、授权和审计。
1.3 框架事实与安全建议要分开
“Backend 支持执行”是能力事实,“是否应该把执行能力交给模型”是安全决策。二者不能混为一谈。
例如 LocalShellBackend 实现了执行协议,但它直接访问宿主机文件和 Shell。它适合明确受控的
本地开发,不代表生产系统可以无条件暴露 execute。生产环境应优先使用隔离、有限权、可审计、
可销毁的 Sandbox。
2. Deep Agents 的内置工具
2.1 能力来源与分层
Deep Agents 的能力不只来自 create_deep_agent(tools=[…])。默认 Middleware、应用代码、
MCP Server 和 Skills 都可能为 Agent 提供能力。

| 能力来源 | 接入方式 | 典型能力 | 是否天然是 Tool |
|---|---|---|---|
| Harness | 默认 Middleware | write_todos、文件工具、task | 是 |
| 自定义 Tool | tools=[…] | 查询订单、计算指标、发送通知 | 是 |
| MCP Server | MCP Client 动态加载 | 数据库、搜索、内部平台 | MCP Tool 会转换成 LangChain Tool |
| Skill | SKILL.md、脚本、资料和资源 | 可复用工作方法与确定性脚本 | 不一定 |
Skill Script 不能与 Tool 并列理解。Skill 首先是按需加载的说明和资源;脚本只有通过 execute
或某个专用 Tool 才能运行,而且 Skill 文件所在 Backend 与实际执行 Sandbox 也可能不同。
2.2 write_todos:显式维护计划
TodoListMiddleware 提供 write_todos,用结构化列表保存任务状态:
pending -> in_progress -> completed
需要注意:
- 每次调用提交的是完整 Todo 列表,不是增量追加;
- 同一轮不要并行写入多个完整列表,否则可能互相覆盖;
- Todo 完成不等于用户产物已经交付,Agent 仍需生成报告或结论;
- 简单问答不必为了“像 Agent”而强制规划。
2.3 文件系统工具
在本文锁定的 deepagents==0.6.12 中,FilesystemMiddleware 提供六个基础文件工具:
| Tool | 作用 | 典型用途 |
|---|---|---|
| ls | 查看目录内容和文件信息 | 了解工作区结构 |
| read_file | 分段读取文本或多模态文件 | 读取资料和中间产物 |
| write_file | 创建文件 | 保存报告、脚本或大结果 |
| edit_file | 使用旧字符串进行精确替换 | 更新代码和文档 |
| glob | 按模式查找路径 | 查找 Python、Markdown 等文件 |
| grep | 搜索文件内容 | 定位符号、错误和数据 |
这些工具不只是 I/O 接口,也承担上下文卸载。大结果可以写入虚拟文件,后续只通过 grep
和分段 read_file 取回必要片段,避免每次模型调用都携带完整结果。
2.4 execute 与 SandboxBackendProtocol
从模型可见的稳定契约看,只有 Backend 实现 SandboxBackendProtocol 时,模型才应该看到并使用
execute;默认 StateBackend 只有虚拟文件能力,没有 Shell 执行能力。
这里还要区分公开行为与内部实现。deepagents==0.6.12 的当前源码会在
FilesystemMiddleware 初始化时构造 execute Tool,并在每次模型请求前检查 Backend,
不支持执行时从 request.tools 中过滤。API 使用者不应依赖这个内部实现,也不能据此认为可以
绕过检查调用 Shell。对应用而言,可靠结论仍然是:模型能否看到 execute 由 Backend 的
执行协议能力决定。
从安全角度看,即使 Backend 实现了协议,也应继续检查:
- 是否真正隔离宿主机;
- 文件系统和网络权限是否最小化;
- 是否限制命令、运行时间、CPU、内存和输出大小;
- 是否记录执行者、命令摘要和退出状态;
- Sandbox 是否能在任务结束后销毁。
2.5 task 与 SubAgent 的配置边界
SubAgentMiddleware 提供 task。在 0.6.12 中,它的核心输入 Schema 只有两个业务字段:
{
"description": "读取 /reports/q2.csv,计算异常项,并返回 Markdown 表格",
"subagent_type": "general-purpose"
}
| 字段 | 含义 |
|---|---|
| description | 完整描述目标、背景、输入位置和期望输出 |
| subagent_type | 从已经注册的 SubAgent 类型中选择一个 |
输入文件路径、输出格式和“只返回摘要”等要求属于 description 中的委派提示,不是独立的
结构化字段。SubAgent 能使用哪些模型、Tools、Skills、Middleware 和权限,则在定义 SubAgent
时确定,不能由主 Agent 在每次 task 调用中任意扩权。
SubAgent 使用隔离上下文执行,并把最终报告返回给主 Agent。它适合多步骤、输出较多且可以独立
完成的任务,不适合把两行计算机械拆成多智能体流程。
2.6 Filesystem Permissions 的边界
permissions= 约束的是内置文件工具,不会自动保护:
- 自定义 Tool 内部调用的 open();
- MCP Server 读取的文件或数据库;
- Sandbox 中执行的任意命令;
- 业务 API 内部的间接资源访问。
例如禁止 /secrets/** 只能约束内置 read_file 等文件 Tool。MCP Tool 和业务 Tool 必须在
自己的服务端再次进行授权。权限不能只存在于 Prompt 中。
3. 设计可靠的自定义 Tool
3.1 从纯业务函数开始
比较容易测试的结构是:
纯业务函数 -> Tool 适配层 -> Agent
examples/p03_tools_mcp/calculator.py 中的业务函数不依赖模型、LangChain 或网络:
def gross_margin(revenue: float, cost: float) -> float:
if revenue <= 0:
raise ValueError("revenue must be greater than zero")
return round((revenue - cost) / revenue, 4)
收入 120、成本 75 时:
(120 - 75) / 120 = 0.375
同一函数之后既可以包装成 LangChain Tool,也可以注册为 MCP Tool,而公式测试不需要启动 Agent。
3.2 名称、描述和输入 Schema
模型看不到函数实现,选择 Tool 主要依赖名称、描述和 Schema:
@tool
def calculate_margin(revenue: float, cost: float) -> dict[str, bool | float | str]:
"""Calculate gross margin from revenue and cost using the business formula."""
try:
margin = gross_margin(revenue, cost)
except ValueError as exc:
return {"ok": False, "error": str(exc)}
return {"ok": True, "margin": margin}
calculate_margin 比 run、process 更容易选择;Docstring 应说明“做什么”和“何时使用”;
revenue: float、cost: float 会生成两个必填的 number 字段。
输入 Schema 应尽量收窄:
- 固定集合使用 Enum 或 Literal;
- 标识符使用稳定字符串,不让模型猜数据库对象;
- 时间范围拆成开始和结束字段;
- 分页大小、字符串长度和数值范围由服务端限制;
- 避免用一个无约束的 payload: dict 接收全部内容。
3.3 完整 Tool 契约

可靠契约不只有输入和输出,还应该覆盖:
Name + Description + Input Schema
+
Execution Policy(权限、超时、并发、幂等)
+
Output Contract(最小结果、稳定字段、分页或文件引用)
+
Error Contract(业务错误、协议错误、基础设施错误)
+
Audit(调用者、租户、参数摘要、Trace ID)
模型可见的 Schema 与服务端执行策略是两层。Schema 能减少无效参数,却不能替代授权、限流和事务。
3.4 输出与错误分类
| 错误类型 | 示例 | 推荐处理 |
|---|---|---|
| 参数校验 | 缺少 revenue、类型错误 | Schema/Pydantic 在执行前拒绝 |
| 预期业务错误 | 收入不大于 0、状态不允许修改 | 返回稳定错误码和可行动说明 |
| 权限错误 | 无权读取客户数据 | 服务端拒绝并审计,不泄露资源细节 |
| 临时基础设施错误 | 429、超时、连接重置 | 有上限重试,标明可恢复状态 |
| 程序缺陷 | KeyError、断言失败 | 记录 Trace ID,只返回脱敏错误 |
failed 不可行动,revenue must be greater than zero 则告诉模型应修改哪个条件。完整堆栈、
SQL、宿主机路径和密钥不应作为 Tool Result 交给模型。
3.5 上下文成本与大结果卸载
Tool Result 会进入消息历史。返回一万行记录会抬高后续每次模型调用的输入成本,因此应:
- 只返回下一步需要的字段;
- 列表使用分页;
- 大结果写文件,只返回路径、摘要、行数和校验值;
- 二进制数据使用内容块或文件引用;
- 保留稳定 ID,让后续 Tool 精确查询;
- 限制错误内容长度。
在本文使用的 deepagents==0.6.12 默认配置中,FilesystemMiddleware 的
tool_token_limit_before_evict 为 20000 Token。这个值属于版本和 Middleware 配置,不是协议
常量,更不能在业务代码中假定永远固定。应用仍应优先分页和聚合,而不是依赖事后卸载。
3.6 幂等、审批与审计
模型、网络或 Runtime 都可能重试。发送邮件、创建订单、扣款和发布报告至少要考虑:
- idempotency_key:同一业务请求只执行一次;
- dry_run:先展示将要发生的变更;
- 乐观锁或资源版本号:防止覆盖并发修改;
- Human-in-the-loop:高风险动作批准后执行;
- 审计记录:调用者、参数摘要、结果和 Trace ID;
- 补偿流程:失败后怎样撤销或转人工。
System Prompt 中写“不要重复发送”不能代替幂等约束。
4. MCP 的架构与核心概念
4.1 什么时候需要 MCP
进程内自定义 Tool 简单、延迟低,但以下场景更适合 MCP:
- 多个 Agent 需要复用同一能力;
- Tool 使用 Java、Go 等其他语言实现;
- 工具需要独立发布、扩缩容和升级;
- 工具拥有自己的认证、限流和审计边界;
- Agent 环境不适合安装重依赖;
- 企业需要统一工具目录。
MCP 标准化的是 AI 应用与外部能力之间的交换方式,不负责主 Agent 如何规划,也不替代业务授权。
4.2 Host、Client 与 Server
| 参与者 | 本文对象 | 职责 |
|---|---|---|
| MCP Host | Deep Agents 应用 | 管理模型、用户会话和多个 Client |
| MCP Client | MultiServerMCPClient | 连接 Server、发现能力并发送调用 |
| MCP Server | finance-tools | 暴露 Tools、Resources 或 Prompts |
MCP 数据层使用 JSON-RPC 表达初始化、能力协商、调用和通知;传输层决定消息通过 stdio 还是
HTTP 传递。协议不规定 Host 必须使用哪个大模型。
4.3 Tools、Resources 与 Prompts
| Primitive | 典型控制方 | 适用语义 | 示例 |
|---|---|---|---|
| Tools | 模型 | 需要模型自主决定执行的动作 | 查询数据库、创建工单、运行计算 |
| Resources | 应用 | 应用选择并注入的上下文 | 文件、记录、知识库内容 |
| Prompts | 用户或应用 | 显式选择的交互模板 | 审核模板、分析模板、Few-shot 示例 |
这是典型控制模型,不限制 Host 设计其他交互界面。数据库读取既可能是 Resource,也可能是
Tool:如果应用预先选定记录并注入上下文,更像 Resource;如果需要模型决定查询条件并发起动作,
更像 Tool。
4.4 stdio 与 Streamable HTTP
| 对比项 | stdio | Streamable HTTP |
|---|---|---|
| Server 位置 | 通常是本机子进程 | 本机或远程服务 |
| 启动方式 | Client 执行命令 | Server 独立运行 |
| 端口 | 不需要 | 需要 HTTP 地址 |
| 认证边界 | 进程、用户和文件权限 | TLS、Bearer、OAuth 或自定义认证 |
| 生命周期 | 与进程和 Session 相关 | 可供多个 Client 访问 |
| 典型场景 | CLI、桌面应用、本地开发 | 企业服务、跨主机共享 |
MCP 规范已经把旧 SSE Transport 标记为 deprecated,新项目通常优先使用 Streamable HTTP。
LangChain Adapter 仍可能为兼容旧 Server 保留 SSE 配置,这不表示现有 SSE Server 会立即失效。
不同客户端对配置字段的命名可能不同。用户提供的 JSON 使用:
{
"type": "streamable_http",
"url": "https://mcp.example.com/<deployment-id>/mcp"
}
本文锁定的 Python MultiServerMCPClient 使用 transport:
{
"transport": "streamable_http",
"url": remote_url,
}
二者表达的是同一种 Transport,但不是可以互换粘贴的同一份配置 Schema。接入前应以当前 Host
或 Adapter 的类型定义为准。
5. 使用 FastMCP 接入 Deep Agents
5.1 固定 FastMCP 来源
本文锁文件使用:
mcp==1.28.1
并采用 MCP Python SDK 内的导入路径:
from mcp.server.fastmcp import FastMCP
一些当前教程使用独立的 fastmcp 包和 from fastmcp import FastMCP。两种依赖来源和导入路径
不能随意混用。本系列没有安装独立 fastmcp 包,所有代码以锁文件中的 mcp==1.28.1 为准。
5.2 创建 stdio Server
examples/p03_tools_mcp/server.py:
"""A local FastMCP server transported over stdio."""
from mcp.server.fastmcp import FastMCP
from p03_tools_mcp.calculator import gross_margin
mcp = FastMCP("finance-tools")
mcp.tool()(gross_margin)
if __name__ == "__main__":
mcp.run(transport="stdio")
mcp.tool()(gross_margin) 根据函数名、Docstring 和类型注解生成 Tool 定义。Server 不包含模型,
也不会主动决定何时计算毛利率。
stdio 的 stdout 用于传输协议消息,业务日志不能随意 print 到 stdout。日志应写 stderr 或独立
日志文件,并进行脱敏。stdio 配置中的 command 会在 Host 上启动真实进程,因此配置来源必须可信。
5.3 加载 MCP Tool
client_agent.py 使用当前虚拟环境解释器启动 Server:
EXAMPLES_DIR = Path(__file__).resolve().parent.parent
client = MultiServerMCPClient(
{
"finance": {
"transport": "stdio",
"command": sys.executable,
"args": ["-m", "p03_tools_mcp.server"],
"cwd": str(EXAMPLES_DIR),
}
}
)
tools = await client.get_tools()
agent = create_deep_agent(model=build_model(), tools=tools)
sys.executable 保证 Client 与 Server 使用同一虚拟环境。-m 要求目标包能从 sys.path 找到;
cwd 则决定子进程当前工作目录,并在本例中让 examples 成为导入起点。更稳定的长期工程方式是
使用标准 pyproject.toml 并 editable 安装,而不是把导入永久绑定在某个工作目录上。
5.4 初始化、发现与调用生命周期

await client.get_tools() 背后会经历:
- Host 启动 stdio Server;
- Client 创建 ClientSession;
- 双方执行 initialize 和能力协商;
- Client 发送 tools/list;
- Server 返回 Tool 名称、描述和输入 Schema;
- Adapter 创建 LangChain Tool;
- 模型产生结构化 Tool Call;
- Adapter 建立 Session 并发送 tools/call;
- Server 返回 CallToolResult;
- Adapter 转换为内容块、artifact 或 ToolMessage;
- Agent 把结果交回模型。
MultiServerMCPClient 默认按无状态方式使用 Session,Tool 调用时可创建并清理 Session。需要跨调用
保存状态时,应显式使用 client.session()。stdio 子进程连接的存活时间与 Adapter 的逻辑 Session
不是完全相同的概念,不能仅看到进程仍在就假设会话状态一定保留。
5.5 内容块与 structured content
在本文版本和只返回标量的 gross_margin 示例中,原始 MCP 结果同时包含文本和结构化内容:
result = await session.call_tool(
"gross_margin", {"revenue": 120, "cost": 75}
)
assert result.isError is False
assert result.content[0].text == "0.375"
assert result.structuredContent == {"result": 0.375}
经过 Adapter,并以 LangChain Tool Call 形态调用时,可以看到:
assert result.status == "success"
assert result.content[0]["text"] == "0.375"
assert result.artifact == {
"structured_content": {"result": 0.375}
}
这不是所有 MCP Tool 的固定返回形式。MCP 可以返回文本、图片、Resource Link、嵌入资源以及
structuredContent;Adapter 会把结构化内容放入 ToolMessage.artifact。直接使用普通参数调用
转换后的 Tool、通过 ToolNode 调用以及不同 response_format,观察到的 Python 返回对象也可能不同。
5.6 错误传播的三层边界
适用版本:langchain-mcp-adapters==0.3.0。

MCP Tool 的业务执行错误和 Transport 故障走不同路径:
业务函数异常
-> CallToolResult(isError=True)
-> Adapter 默认生成 ToolMessage(status="error")
-> 模型读取错误并尝试恢复
Transport / Session / Content Conversion Error
-> Python Exception
-> 应用决定重试、降级或终止
各层应检查的对象是:
| 层级 | 检查方式 |
|---|---|
| 原始 MCP ClientSession | CallToolResult.isError 和 content |
| LangChain Adapter / Agent | ToolMessage.status == “error” 和内容块 |
| Transport、Session、内容转换 | Python 异常 |
默认 handle_tool_errors=True 时,isError=True 不会让整个 Agent Run 崩溃;设置
handle_tool_errors=False 才会抛出 ToolException。这个开关只覆盖 MCP Tool 的执行错误,
不存在的 Server 命令、连接中断和内容转换错误仍然直接抛出。
5.7 将 Tool 交给 Agent
async def main() -> None:
client = MultiServerMCPClient(
{
"finance": {
"transport": "stdio",
"command": sys.executable,
"args": ["-m", "p03_tools_mcp.server"],
"cwd": str(EXAMPLES_DIR),
}
}
)
tools = await client.get_tools()
agent = create_deep_agent(model=build_model(), tools=tools)
result = await agent.ainvoke(
{
"messages": [
{"role": "user", "content": "收入120、成本75,毛利率是多少?"}
]
}
)
print(result["messages"][-1].content)
MCP Tool 发现和协议调用可以离线测试;只有“让模型决定是否调用”这一步需要真实模型凭据。
5.8 连接远程 Streamable HTTP MCP
远程服务 URL 不直接写入代码,而是放在 Git 忽略的 .env:
RUN_REMOTE_MCP_TESTS=0
P03_RAILWAY_MCP_URL=
P03_STOCK_MCP_URL=
remote_demo.py 只接受绝对 HTTPS URL,并且在结果中删除真实部署路径。部分托管 MCP URL 的
路径包含不可猜测的部署标识,可能具有 bearer locator 属性;把它完整提交到仓库,会扩大任何人
直接调用服务的范围。
12306 Client 的创建代码如下:
railway_client = MultiServerMCPClient(
{
"railway": {
"transport": "streamable_http",
"url": railway_url,
}
}
)
tools = await asyncio.wait_for(railway_client.get_tools(), timeout=30)
发现 Tool 后,仍然像本地 MCP 一样交给 Deep Agent:
agent = create_deep_agent(
model=RemoteMCPRuleModel(),
tools=tools,
system_prompt="必须通过 search-stations MCP Tool 搜索北京车站,最多返回 3 项。",
)
state = await agent.ainvoke(
{"messages": [{"role": "user", "content": "通过 MCP 搜索北京车站"}]}
)
这里使用 RemoteMCPRuleModel 固定生成 search-stations Tool Call,目的是单独检验远程 MCP 的
初始化、工具发现、Schema 转换、Agent Tool 循环和结果转换,不让外部 LLM 的随机工具选择干扰协议
回归。5.7 节的在线示例继续验证“真实模型 + 本地 stdio MCP”,两条路径覆盖的变量不同。

6. 测试与故障排查
6.1 测试环境
| 项目 | 验证环境 |
|---|---|
| 操作系统 | macOS 15.7.3,arm64 |
| Python | 3.12.11 |
| Deep Agents | deepagents==0.6.12 |
| LangChain | langchain==1.3.13、langchain-core==1.4.9 |
| MCP Adapter | langchain-mcp-adapters==0.3.0 |
| MCP Python SDK | mcp==1.28.1 |
| MCP 协议协商结果 | 2025-11-25 |
| 在线端点 | Endpoint A,第三方 OpenAI-compatible 网关 |
| 在线模型别名 | qwen3.7-plus,可能是网关别名 |
模型别名和兼容结果只适用于该端点,不代表模型厂商公开 API 存在同名模型。
6.2 十项离线测试
运行:
cd source/_posts/deepagent/examples
.venv_deepagent/bin/python -m pytest p03_tools_mcp -q
.venv_deepagent/bin/python -m ruff check p03_tools_mcp
结果:
..........s [100%]
All checks passed!
默认测试中的十项成功用例覆盖:
- 纯函数正常计算;
- 非法收入抛出明确错误;
- 自定义 Tool 返回紧凑成功和失败结构;
- Tool Schema 具有两个必填数值字段和有效描述;
- 原始 MCP 层检查文本、structuredContent 和 isError;
- Adapter 层检查成功/失败 ToolMessage,并验证关闭错误处理时抛异常;
- 不存在的 Server Command 作为 Transport 启动错误直接抛出;
- 远程 URL 必须存在并使用 HTTPS;
- 保存结果时删除远程部署路径;
- 确定性模型的 Tool 绑定返回隔离快照,不修改共享实例。
末尾的 s 是显式跳过的远程网络集成测试。只有设置 RUN_REMOTE_MCP_TESTS=1 才会访问外部
MCP Server,默认 CI 不依赖第三方网络状态。
测试真实启动 stdio Server,并出现 ListToolsRequest 和 CallToolRequest,没有通过直接导入
Server 函数冒充 MCP 测试。
6.3 原始协议与 Adapter 测试
原始 MCP 层:
error = await session.call_tool(
"gross_margin", {"revenue": 0, "cost": 75}
)
assert error.isError is True
assert "revenue must be greater than zero" in error.content[0].text
Adapter 层:
error = await margin.ainvoke(
{
"type": "tool_call",
"name": "gross_margin",
"args": {"revenue": 0, "cost": 75},
"id": "call-error",
}
)
assert error.status == "error"
assert "revenue must be greater than zero" in error.content[0]["text"]
这样可以明确知道错误是在业务执行、Adapter 转换还是 Transport 层发生。
6.4 真实模型在线验证
执行:
.venv_deepagent/bin/python -m p03_tools_mcp.live_demo
脱敏结果与 p03_tools_mcp/live_result.json 保持一致:
{
"scene": "p03_fastmcp_gross_margin",
"status": "passed",
"model": "qwen3.7-plus",
"latency_ms": 8424,
"mcp_tools": ["gross_margin"],
"tool_calls": [
{"name": "gross_margin", "args": {"revenue": 120, "cost": 75}}
],
"tool_results": [
{"name": "gross_margin", "status": "success", "content": "0.375"}
],
"usage": {
"input_tokens": 12614,
"output_tokens": 187,
"total_tokens": 12801
},
"final_answer": "收入 120、成本 75,毛利率 37.5%"
}
0.375 是 MCP Tool 机器结果,37.5% 是模型格式化后的回答。在线断言检查实际
gross_margin Tool Call、参数和 Tool Result,不断言随机自然语言全文。
12614 个输入 Token 不是毛利率函数自身的成本,而是整个 Deep Agents Harness Prompt、内置
文件/Todo/SubAgent 工具 Schema、MCP Tool Schema 和消息历史的共同输入成本。评估生产成本时,
必须观察完整 Run,而不能只计算用户问题长度。
6.5 远程 Streamable HTTP MCP 真实验证
使用配置的两个 ModelScope MCP 地址执行:
set -a && source .env && set +a
RUN_REMOTE_MCP_TESTS=1 .venv_deepagent/bin/python -m pytest \
p03_tools_mcp/test_remote_mcp.py -m remote_mcp -vv
结果为 1 passed。随后执行完整演示:
.venv_deepagent/bin/python -m p03_tools_mcp.remote_demo
脱敏结果保存在 p03_tools_mcp/remote_result.json:
{
"scene": "p03_remote_streamable_http_mcp",
"status": "passed",
"agent_model": "p03-remote-mcp-rule-model",
"transport": "streamable_http",
"latency_ms": 10135,
"railway": {
"endpoint": "https://mcp.api-inference.modelscope.net/<redacted>/mcp",
"status": "connected",
"tool_count": 7,
"tools": [
"get-current-time",
"get-train-no-by-train-code",
"get-train-route-stations",
"query-ticket-price",
"query-tickets",
"query-transfer",
"search-stations"
]
},
"agent_tool_call": {
"name": "search-stations",
"args": {"query": "北京", "limit": 3}
},
"agent_tool_result": {
"name": "search-stations",
"status": "success",
"contains_station_code": "BJP"
},
"stock": {
"endpoint": "https://mcp.api-inference.modelscope.net/<redacted>/mcp",
"status": "failed",
"errors": [
{"type": "McpError", "message": "Session terminated"}
]
}
}
12306 服务成功完成初始化和 tools/list,共发现 7 个 Tool。Deep Agent 随后真正调用
search-stations(query=”北京”, limit=3),Adapter 得到成功 ToolMessage,结果中包含北京站代码
BJP。这不是直接请求 Server 函数,也不是伪造一个 LangChain Tool。
股票服务没有通过初始化。用户提供的地址在域名后直接出现 //mcp,缺少与第一个地址类似的
部署标识;HTTP 规范化后实际路径相当于 /mcp,MCP Client 收到 Session terminated。因此本文
只记录失败事实,不展示虚构的股票 Tool 列表或调用结果。拿到完整有效的部署地址后,需要重新执行
同一探测流程。
远程结果会随服务版本、网络和实时数据变化。自动测试只断言 Tool 名、Schema 参数、调用状态和
可验证车站代码,不断言完整自然语言内容或固定延迟。
6.6 常见错误
| 现象 | 原因 | 处理方式 |
|---|---|---|
| No module named p03_tools_mcp | 包搜索根目录不正确 | 使用 python -m、固定 cwd,长期改为标准项目安装 |
| MCP Connection closed | Server 启动失败或 stdout 被污染 | 检查 stderr、Import 错误和 Server 退出码 |
| 找不到 Tool | 未注册或能力协商失败 | 单独执行 get_tools() 并检查名称与 Schema |
| 参数验证失败 | 类型注解或模型参数错误 | 查看自动生成的 JSON Schema |
| 返回内容块而非标量 | Adapter 正在保留 MCP Content | 按 type 读取内容,并检查 artifact |
| 错误没有抛异常 | 默认转换为错误 ToolMessage | 检查 status;确需异常时关闭 handle_tool_errors |
| 每次调用状态丢失 | 默认无状态 Session | 显式使用 client.session() |
| HTTP 401 | 认证 Header 或 Token 错误 | 在 Client Auth 层检查,不打印 Token |
| Session terminated | URL 缺少部署标识、Session 被服务端关闭或协议不兼容 | 先检查完整 URL,再单独执行初始化和 tools/list |
| Agent 不选择 Tool | 描述不清或模型 Tool Calling 不稳定 | 先验证 Schema,再检查 AIMessage.tool_calls |
| 上下文快速膨胀 | Tool 返回原始大结果 | 分页、聚合或写文件后返回引用 |
7. 安全、治理与选型
7.1 MCP 安全边界
MCP 统一了调用协议,但不会自动提供业务安全:
- FilesystemPermission 不检查 MCP Server 内部访问的路径;
- Streamable HTTP 仍需 TLS、认证、租户隔离、限流和审计;
- 托管服务的不可猜测部署路径可能等同访问凭据,不应提交到 Git 或文章;
- Tool Result 可能携带 Prompt Injection,应作为不可信数据处理;
- 数据库等凭据应由 Server 管理,不应放进 Tool 参数让模型填写;
- stdio Command 等同于在 Host 上运行程序,配置来源和可执行文件必须可信。
7.2 生产 Tool 治理
| 治理项 | 需要解决的问题 |
|---|---|
| Tool Registry | 当前有哪些工具、负责人和版本 |
| Schema Review | 参数是否明确、是否泄露敏感信息 |
| 权限模型 | 哪个用户、租户和 Agent 可以调用 |
| 超时与重试 | 慢调用和临时故障如何有界恢复 |
| 幂等与审批 | 副作用怎样避免重复和越权 |
| 可观测性 | 调用次数、延迟、错误率和 Token 影响 |
| 兼容管理 | Schema 或 Description 变化是否破坏行为 |
| 下线流程 | 移除 Tool 前怎样发现依赖方 |
Tool Description 也要版本管理。即使 API 没变,修改描述也可能改变模型的工具选择,需要使用
回归数据集重新评估。
7.3 自定义 Tool、MCP、Skill 与 Agent Protocol 的选型
| 场景 | 建议方式 |
|---|---|
| 单个 Agent 使用的简单纯函数 | 自定义 Tool |
| 同一 Python 服务中的复杂业务逻辑 | 业务 Service + Tool 适配层 |
| 多 Agent 或多语言复用能力 | MCP Server |
| 远程企业平台提供统一能力 | Streamable HTTP MCP |
| 本地 CLI 或桌面工具 | stdio MCP |
| 固定流程、脚本、参考资料和模板 | Skill |
| 简单封装一次远程 Agent 能力 | 可以使用 MCP Tool |
| 长期任务、异步状态和 Agent 间多轮协作 | Agent Protocol、A2A 或异步 SubAgent |
MCP Tool 可以封装远程 Agent 调用,但不擅长天然表达长任务状态、取消、双向协作和多轮任务生命周期。
只有当复用、隔离或独立部署的收益高于进程边界成本时,才值得引入 MCP。
8. 总结
Tool 的核心不是在函数上增加一个装饰器,而是建立一份模型能选择、Runtime 能执行、业务系统
能治理的稳定契约。
本篇可以归纳为五条原则:
- Deep Agents 的 Harness、自定义 Tool、MCP 和 Skills 是不同能力来源,边界不能混淆;
- execute 是否对模型可见取决于 Backend 执行协议,生产执行环境还必须隔离和审计;
- MCP 业务错误、Adapter 错误状态和 Transport 异常要分层处理;
- 先测试纯业务函数,再测试 Tool Schema、原始 MCP、Adapter,最后才使用真实模型验收。
- 远程 MCP 必须分别验证初始化、tools/list 和真实 tools/call,连接失败时保留失败边界,不能用本地 Tool 冒充远程结果。