工具与 MCP:让 Agent 长出手脚

大模型天生只有一张嘴------吐 token。它能写诗、能推导,却摸不到数据库、点不了按钮、发不出邮件。问题不在模型笨,在于它和外部世界之间没有接口。工具(Tool)与 MCP 就是给 agent 接上的手脚:前者定义模型能调什么,后者定义这些能力跑在哪、怎么被发现。这篇文章把这两层机制拆到底,让你看清 function calling 究竟怎么发生,以及为什么把工具做对,比把模型调大更重要。

工具的本质:模型只看到签名,执行发生在别处

很多人以为「给模型接个工具」就是在代码里塞个 if 判断去分流。这是最根本的误解。工具的本质,是给模型一份结构化函数签名加一块独立的执行环境。

模型这一侧,永远只接收到 schema:函数的名字、一句话描述、以及参数用 JSON Schema 写的类型和约束。它基于这些信息决定「调还是不调」「传什么参数」,然后产出一个调用意图------一段包含函数名和参数的结构化数据。到这里为止,模型一行工具代码都没碰过。

真正的执行发生在 agent 进程之外的某台机器、某个服务里。查数据库、调第三方 API、读写文件系统,这些副作用全在外部进程完成,跑完把结果回填给模型。模型是「大脑与嘴」,执行环境是「手与脚」,二者通过 schema 这份契约隔离。

  • 隔离 模型不读函数体,看不见实现,只能依据 schema 文字下判断
  • 委托 真正干活的是外部进程,模型只负责产生调用意图
  • 契约 schema 是双方唯一接口,定义了能做什么、不暴露怎么做

@tool 装饰器:让 Python 函数自动编译成 JSON Schema

在 LangChain 里,把函数变成工具只要一个装饰器。你写一个普通 Python 函数,带上类型注解和 docstring,@tool 会在加载时把它自动编译成模型能消费的 JSON Schema------你不必手写一份重复的接口描述。

python 复制代码
from langchain_core.tools import tool

@tool
def search_flights(
    origin: str,
    destination: str,
    date: str,
) -> list:
    """查询指定日期从出发城市到目的城市的航班列表。

    Args:
        origin: 出发城市,如 "上海"
        destination: 目的城市,如 "北京"
        date: 出发日期,格式 YYYY-MM-DD,如 "2024-05-30"
    """
    # 真实代码运行在外部进程,模型看不到这里
    return external_api.query(origin, destination, date)

装饰器扫一遍函数,吐出的 schema 长这样:

python 复制代码
print(search_flights.to_json())
# {
#   "name": "search_flights",
#   "description": "查询指定日期从出发城市到目的城市的航班列表。",
#   "parameters": {
#     "type": "object",
#     "properties": {
#       "origin": {"type": "string", "description": "出发城市,如 \"上海\""},
#       "destination": {"type": "string", "description": "目的城市,如 \"北京\""},
#       "date": {"type": "string", "description": "出发日期,格式 YYYY-MM-DD"}
#     },
#     "required": ["origin", "destination", "date"]
#   }
# }

注意三个关键点。第一,参数类型直接来自函数的类型注解(str 映射 string、int 映射 integer),少写一处类型,schema 就少一份约束。第二,docstring 的第一句话会成为 description,模型就是靠这行字理解「这个工具干嘛用」的。第三,参数说明也来自 docstring 里的 Args 段,写得细,模型传参越准。

schema 不是文档,是契约。它从代码派生,所以代码改了,schema 跟着改,不会出现文档和接口两张皮。

schema 质量即调用准确率:描述写不清,模型就调错

这里有个反直觉的事实:模型不会去读你的源码。它唯一的判断依据就是 schema 里的文字。所以 description 写得多含糊,模型就调得多离谱。

举同一个函数的两种写法。反例里 description 只写「处理数据」,date 参数没有任何格式说明,类型还标成 any。模型面对这样的 schema,会把 date 传成时间戳、把城市传成缩写,接口十次有六次报错。正例里 description 写清「查询指定日期从出发城市到目的城市的航班列表」,date 标注为 string 且注明 YYYY-MM-DD 格式,模型传参一次到位。

  • 准确率绑定描述精度 schema 是模型唯一信息源,含糊等于高误调率
  • 类型约束不能省 该 string 就 string,标 any 等于把判断推给模型瞎猜
  • 示例值放进去 在 description 里给「如 2024-05-30」,比单纯说格式更有效

MCP:把工具执行从 agent 进程拆成独立服务

工具写好了,下一个问题是:它该跑在哪。最朴素的做法是函数直接 import 进 agent 进程。但这样工具就被语言和进程锁死了,换个项目、换个语言,全部重写。MCP(Model Context Protocol)要解决的正是这件事。

MCP 把「工具的执行」从 agent 进程里抽出来,变成独立的 server 进程,彼此用一套统一协议通信。agent 作为 Host 内嵌一个 MCP Client,通过 stdio 或 SSE 这类传输层,用 JSON-RPC 和 server 对话:先 list 对端暴露了哪些工具,再 call 其中某一个。

python 复制代码
from langchain_mcp_adapters.client import MultiServerMCPClient

async def build_tools():
    client = MultiServerMCPClient({
        "filesystem": {
            "command": "python",
            "args": ["mcp_server_filesystem.py", "/data"],
            "transport": "stdio",
        },
        "github": {
            "url": "http://localhost:8000/mcp",
            "transport": "streamable_http",
        },
    })
    # 协议层自动发现对端暴露的工具
    # agent 进程内无需写任何工具实现
    tools = await client.get_tools()
    return tools

这套机制带来三个实打实的好处。其一是跨语言:server 用 Python、Go、Node 随便写,只要说 MCP 的协议,agent 照单全收。其二是跨进程:server 可以跑在另一台机器上,它崩了不影响 agent。其三是可复用:一个写好的 filesystem server,全公司的 agent 都能接,不用每人重写一遍。

重点在于:agent 这边不需要进程内写任何工具实现,工具的发现与调用全走协议。解耦之后,工具的生死、升级、权限,都收敛到 server 一侧。

反例与反模式:硬编码工具与不设防的副作用

讲完正路,说几个我见过的真坑。

第一个坑是把工具实现硬编码进 agent 主进程。函数直接 import,和业务逻辑揉在一起。改一个工具的描述要重发整个 agent;想用另一种语言重写某个能力,没法单拆;工具之间的依赖关系变成一坨意大利面。正确做法是把工具外推成 server(见上节),agent 只管编排。

第二个坑更危险:把复杂副作用当普通工具,不设权限、不做确认。看这段反例------

python 复制代码
# 反例:把发邮件写成一个普通工具,没有权限闸门
@tool
def send_email(to: str, subject: str, body: str) -> str:
    """发邮件"""
    smtp.send(to, subject, body)   # 模型一句话,邮件就真发出去了
    return "sent"

模型一旦产生调用意图,这封邮件就真的从服务器飞出去了。没有二次确认、没有白名单、没有审批闸门。在自动化 agent 里,这等于把炸药和最灵巧的手放在一个房间。正确做法:写库、发邮件、删资源这类高危操作,必须在 MCP server 侧加权限校验和人审节点,把「模型想调」和「系统真执行」之间的一堵墙砌实。

第三个坑是忽略工具返回值的结构。模型拿到一段非结构化的长文本,很难从中抽取它要的字段,下一步推理就容易跑偏。工具返回尽量给结构化、带字段名的数据,而不是一段散文。

  • 硬编码 工具揉进主进程,耦合重、难复用、语言锁死
  • 无闸门 高危副作用直接执行,缺权限与确认,误操作难挽回
  • 弱返回 返回非结构化长文本,模型二次解析成本高、易出错
相关推荐
努力的小郑24 分钟前
数据查到了,回答去哪儿了?一次百炼 + LangChain 排查实录
后端·langchain·agent
DigitalOcean1 小时前
Y Combinator 开源 Agent 运行框架 QM,现已支持在 DigitalOcean 一键部署
agent
赢乐2 小时前
Agent 聊到第 20 轮就“失忆“?——长对话上下文丢失的排查与修复完全指南
agent·长对话·上下文丢失·失忆·关键信息·滑动窗口截断·摘要压缩
阿里云云原生2 小时前
让 Agent 成为企业生产力丨Alibaba Cloud AgentCore 企业智能体构建与治理沙龙·杭州站精彩回顾 & PPT 下载
agent
lucas_AI2 小时前
AGENTS.md 烂了没人管?732 次实测:Coding Agent 每个会话都在替你付「文档债」利息
agent
泯泷2 小时前
AI 最坑的失败:不是不会做,而是"假装做完了"
人工智能·llm·agent
阿里云云原生2 小时前
满帮基于阿里云 AgentCore 打造生产级 SRE 稳定性数字人
agent
去伪存真3 小时前
稠密检索负责脑补,稀疏检索负责兜底:拆解 RAG 倒排索引的“查字典”艺术
前端·agent
跨境Jacky5 小时前
选品数据采集: MCP 比爬虫稳在哪? 实测 46/47
跨境电商·mcp·sorftime
喵个咪6 小时前
GoWind Admin 拆解:AI 能力层——工具循环、pgvector 知识库与机器凭证
人工智能·后端·agent