Deep Agents 3. 内置工具、自定义 Tool 与 MCP


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 提供能力。

Deep Agents 中的能力来源

能力来源 接入方式 典型能力 是否天然是 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

需要注意:

  1. 每次调用提交的是完整 Todo 列表,不是增量追加;
  2. 同一轮不要并行写入多个完整列表,否则可能互相覆盖;
  3. Todo 完成不等于用户产物已经交付,Agent 仍需生成报告或结论;
  4. 简单问答不必为了“像 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 契约

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 会进入消息历史。返回一万行记录会抬高后续每次模型调用的输入成本,因此应:

  1. 只返回下一步需要的字段;
  2. 列表使用分页;
  3. 大结果写文件,只返回路径、摘要、行数和校验值;
  4. 二进制数据使用内容块或文件引用;
  5. 保留稳定 ID,让后续 Tool 精确查询;
  6. 限制错误内容长度。

在本文使用的 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 初始化、发现与调用生命周期

FastMCP Session 与调用生命周期

await client.get_tools() 背后会经历:

  1. Host 启动 stdio Server;
  2. Client 创建 ClientSession;
  3. 双方执行 initialize 和能力协商;
  4. Client 发送 tools/list;
  5. Server 返回 Tool 名称、描述和输入 Schema;
  6. Adapter 创建 LangChain Tool;
  7. 模型产生结构化 Tool Call;
  8. Adapter 建立 Session 并发送 tools/call;
  9. Server 返回 CallToolResult;
  10. Adapter 转换为内容块、artifact 或 ToolMessage;
  11. 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 错误传播分层

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”,两条路径覆盖的变量不同。

远程 Streamable HTTP 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!

默认测试中的十项成功用例覆盖:

  1. 纯函数正常计算;
  2. 非法收入抛出明确错误;
  3. 自定义 Tool 返回紧凑成功和失败结构;
  4. Tool Schema 具有两个必填数值字段和有效描述;
  5. 原始 MCP 层检查文本、structuredContent 和 isError;
  6. Adapter 层检查成功/失败 ToolMessage,并验证关闭错误处理时抛异常;
  7. 不存在的 Server Command 作为 Transport 启动错误直接抛出;
  8. 远程 URL 必须存在并使用 HTTPS;
  9. 保存结果时删除远程部署路径;
  10. 确定性模型的 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 能执行、业务系统
能治理的稳定契约。

本篇可以归纳为五条原则:

  1. Deep Agents 的 Harness、自定义 Tool、MCP 和 Skills 是不同能力来源,边界不能混淆;
  2. execute 是否对模型可见取决于 Backend 执行协议,生产执行环境还必须隔离和审计;
  3. MCP 业务错误、Adapter 错误状态和 Transport 异常要分层处理;
  4. 先测试纯业务函数,再测试 Tool Schema、原始 MCP、Adapter,最后才使用真实模型验收。
  5. 远程 MCP 必须分别验证初始化、tools/list 和真实 tools/call,连接失败时保留失败边界,不能用本地 Tool 冒充远程结果。

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录