资讯详情

资讯详情

工具与 MCP:让 Agent 长出手脚

大模型天生只有一张嘴——吐 token。它能写诗、能推导却摸不到数据库、点不了按钮、发不出邮件。问题不在模型笨在于它和外部世界之间没有接口。工具Tool与 MCP 就是给 agent 接上的手脚前者定义模型能调什么后者定义这些能力跑在哪、怎么被发现。这篇文章把这两层机制拆到底让你看清 function calling 究竟怎么发生以及为什么把工具做对比把模型调大更重要。工具的本质模型只看到签名执行发生在别处很多人以为「给模型接个工具」就是在代码里塞个 if 判断去分流。这是最根本的误解。工具的本质是给模型一份结构化函数签名加一块独立的执行环境。模型这一侧永远只接收到 schema函数的名字、一句话描述、以及参数用 JSON Schema 写的类型和约束。它基于这些信息决定「调还是不调」「传什么参数」然后产出一个调用意图——一段包含函数名和参数的结构化数据。到这里为止模型一行工具代码都没碰过。真正的执行发生在 agent 进程之外的某台机器、某个服务里。查数据库、调第三方 API、读写文件系统这些副作用全在外部进程完成跑完把结果回填给模型。模型是「大脑与嘴」执行环境是「手与脚」二者通过 schema 这份契约隔离。隔离模型不读函数体看不见实现只能依据 schema 文字下判断委托真正干活的是外部进程模型只负责产生调用意图契约schema 是双方唯一接口定义了能做什么、不暴露怎么做图1工具的本质——模型只看到 schema执行落在外部进程tool 装饰器让 Python 函数自动编译成 JSON Schema在 LangChain 里把函数变成工具只要一个装饰器。你写一个普通 Python 函数带上类型注解和 docstringtool会在加载时把它自动编译成模型能消费的 JSON Schema——你不必手写一份重复的接口描述。from langchain_core.tools import tool tool defsearch_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 长这样print(search_flights.to_json()) # { # __STR0__: __STR1__, # __STR0__: __STR1__, # __STR0__: { # __STR0__: __STR1__, # __STR0__: { # __STR0__: {__STR1__: __STR2__, __STR3__: __STR4__上海\__STR5__}, # __STR0__: {__STR1__: __STR2__, __STR3__: __STR4__北京\__STR5__}, # __STR0__: {__STR1__: __STR2__, __STR3__: __STR4__} # }, # __STR0__: [__STR1__, __STR2__, __STR3__] # } # }注意三个关键点。第一参数类型直接来自函数的类型注解str 映射 string、int 映射 integer少写一处类型schema 就少一份约束。第二docstring 的第一句话会成为 description模型就是靠这行字理解「这个工具干嘛用」的。第三参数说明也来自 docstring 里的 Args 段写得细模型传参越准。schema 不是文档是契约。它从代码派生所以代码改了schema 跟着改不会出现文档和接口两张皮。图2tool 把 Python 函数自动编译成 JSON Schemaschema 质量即调用准确率描述写不清模型就调错这里有个反直觉的事实模型不会去读你的源码。它唯一的判断依据就是 schema 里的文字。所以 description 写得多含糊模型就调得多离谱。举同一个函数的两种写法。反例里 description 只写「处理数据」date 参数没有任何格式说明类型还标成 any。模型面对这样的 schema会把 date 传成时间戳、把城市传成缩写接口十次有六次报错。正例里 description 写清「查询指定日期从出发城市到目的城市的航班列表」date 标注为 string 且注明 YYYY-MM-DD 格式模型传参一次到位。准确率绑定描述精度schema 是模型唯一信息源含糊等于高误调率类型约束不能省该 string 就 string标 any 等于把判断推给模型瞎猜示例值放进去在 description 里给「如 2024-05-30」比单纯说格式更有效图3schema 描述精度直接决定模型调用准确率MCP把工具执行从 agent 进程拆成独立服务工具写好了下一个问题是它该跑在哪。最朴素的做法是函数直接 import 进 agent 进程。但这样工具就被语言和进程锁死了换个项目、换个语言全部重写。MCPModel Context Protocol要解决的正是这件事。MCP 把「工具的执行」从 agent 进程里抽出来变成独立的 server 进程彼此用一套统一协议通信。agent 作为 Host 内嵌一个 MCP Client通过 stdio 或 SSE 这类传输层用 JSON-RPC 和 server 对话先 list 对端暴露了哪些工具再 call 其中某一个。from langchain_mcp_adapters.client import MultiServerMCPClient asyncdefbuild_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 一侧。图4MCP 把工具执行从 agent 进程解耦成独立 server反例与反模式硬编码工具与不设防的副作用讲完正路说几个我见过的真坑。第一个坑是把工具实现硬编码进 agent 主进程。函数直接 import和业务逻辑揉在一起。改一个工具的描述要重发整个 agent想用另一种语言重写某个能力没法单拆工具之间的依赖关系变成一坨意大利面。正确做法是把工具外推成 server见上节agent 只管编排。第二个坑更危险把复杂副作用当普通工具不设权限、不做确认。看这段反例——# 反例把发邮件写成一个普通工具没有权限闸门 tool def send_email(to: str, subject: str, body: str) - str: 发邮件 smtp.send(to, subject, body) # 模型一句话邮件就真发出去了 return sent模型一旦产生调用意图这封邮件就真的从服务器飞出去了。没有二次确认、没有白名单、没有审批闸门。在自动化 agent 里这等于把炸药和最灵巧的手放在一个房间。正确做法写库、发邮件、删资源这类高危操作必须在 MCP server 侧加权限校验和人审节点把「模型想调」和「系统真执行」之间的一堵墙砌实。第三个坑是忽略工具返回值的结构。模型拿到一段非结构化的长文本很难从中抽取它要的字段下一步推理就容易跑偏。工具返回尽量给结构化、带字段名的数据而不是一段散文。硬编码工具揉进主进程耦合重、难复用、语言锁死无闸门高危副作用直接执行缺权限与确认误操作难挽回弱返回返回非结构化长文本模型二次解析成本高、易出错最后说一句技术成长不只是写代码职业规划和自我包装同样重要。我整理了一份简历、面试和职业规划的学习资料适合想在职场上走得更远的朋友看看。资料展示下面是我整理的AI大模型学习资料和工具包预览适合收藏后按主题逐步学习。如果你想看完整资料目录可以在评论区留言「资料」也欢迎告诉我你更关注AI大模型里的哪类内容。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →