点点开黑交友陪玩
262.4MB · 2026-04-23
想象一下,你要做一个“AI 点外卖助手”。
用户说一句:
大模型 只是一个语言模型,它不知道怎么和外部交流,这时候Tool就出来了,Tool让大模型知道附近有哪些商家,菜单长什么样,配送系统怎么查。
这里可以先建立一个直观的类比:
所以,Function Calling 解决的是“这次怎么点单”;而 MCP 解决的是“平台怎么把大量商家用统一方式接进来”。Anthropic 对 MCP 的官方定义就是:它是一个开放标准,用来在数据源和 AI 工具之间建立安全的双向连接;其公开介绍里也明确采用了 MCP clients 和 MCP servers 的架构表达。 (Anthropic)
在 function calling 出现之前,行业里最常见的做法其实很朴素,也很混乱:
本质上就是:
没有统一 schema,没有稳定的参数结构,也没有真正意义上的标准工具调用流程。OpenAI 后来对 function calling 的官方表述,恰恰说明了这一点:开发者可以先描述应用中的函数,模型再输出一个带参数的 JSON 对象来调用函数。换句话说,在此之前,开发者普遍都得自己解决“如何让模型稳定地产出可执行参数”这个问题。 (OpenAI)
# function calling 之前的常见写法:靠 prompt 约定格式
prompt = f"""
你是助手。
如果用户要查天气,请输出:
WEATHER(city=城市,date=日期)
否则正常回答。
用户:{user_input}
"""
reply = llm(prompt)
if reply.startswith("WEATHER("):
city = parse_between(reply, "city=", ",")
date = parse_between(reply, "date=", ")")
result = get_weather(city, date)
answer = llm(f"天气结果是:{result}。请整理成自然语言。")
else:
answer = reply
这段代码的问题不是“不能跑”,而是太脆:
weather(...)只要模型输出稍微偏一点,后端就得补一堆 if/else 和容错逻辑。
prompt = f"""
你可以调用两个工具:
1. search_restaurant(query, budget)
2. estimate_delivery(store_id)
如果需要调用工具,请严格输出 JSON:
{{"tool": "...", "args": {{...}}}}
用户:{user_input}
"""
reply = llm(prompt)
obj = try_parse_json(reply)
if obj and obj["tool"] == "search_restaurant":
stores = search_restaurant(**obj["args"])
elif obj and obj["tool"] == "estimate_delivery":
eta = estimate_delivery(**obj["args"])
else:
# 解析失败,回退成普通回答
pass
这比上一种写法好一点,但问题依旧明显:
这套 JSON 结构是你自己定的,不是一个通用的、模型原生围绕它优化的工具调用机制。
你这次写 tool + args,别人那次写 name + parameters,另一个团队又写 action + payload。看起来都像 JSON,但其实完全不是一回事。
所以在那个阶段,行业的真实状态可以概括成一个词语:群魔乱舞
后面 OpenAI 推出了 function calling。它做的事情并不困难,但非常关键,它规定了Tool怎么定义:
OpenAI 官方对这一能力的描述是:开发者可以描述应用中的函数,模型会智能地输出一个包含参数的 JSON 对象来调用这些函数。这个能力在 2023 年 6 月 13 日正式发布。 (OpenAI)
tools = [
{
"name": "get_weather",
"description": "查询指定城市和日期的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"date": {"type": "string"}
},
"required": ["city", "date"]
}
}
]
resp = llm.chat(
messages=[{"role": "user", "content": user_input}],
tools=tools
)
if resp.tool_call:
name = resp.tool_call.name
args = resp.tool_call.arguments
tool_result = call_tool(name, args)
final = llm.chat(
messages=[
{"role": "user", "content": user_input},
{"role": "tool", "name": name, "content": str(tool_result)}
]
)
else:
final = resp.content
这一步真正解决了三个问题。
第一,它把**“调用哪个工具、参数长什么样”**这件事,从“靠 prompt 猜”变成了“靠 schema 约束”。
第二,它让工具调用进入了更稳定的运行时模式:模型负责决策,程序负责执行。
第三,它显著减少了开发者自己发明半吊子调用格式的成本。OpenAI 官方后来也把 function calling 直接描述为一种让模型连接到外部工具和训练数据之外信息的能力。 (OpenAI)
Function calling 出来之后,大家开始围绕“模型如何发起工具调用”形成相对统一的做法,然后再在这个基础上做各自的封装和扩展。
所以更准确地说,Function Calling 并不是发明了“模型调工具”这个想法,而是把这件事行业统一化了。 (OpenAI)
讲到这里,大家可能会自然产生一个疑问:
答案是:
OpenAI 的 function calling 文档重点就是:你定义工具 schema,模型输出结构化参数,再由你的应用去执行实际调用。也就是说,它主要统一的是模型和你自己的应用之间这一小段。 (OpenAI)
但模型输出完以后,后面的世界还是乱的。
假设你要接 GitHub 、Notion、数据库。
你可能要自己写:
{"name":"search_prs","parameters":{...}}
{"name":"search_pages","parameters":{...}}
{"name":"run_sql","parameters":{...}}
这些 schema 都能塞给模型,但仍然是你自己手工整理的。
Function calling 不会自动告诉你:
换句话说,“工具长什么样”这件事,还是每个接入方自己决定。 (OpenAI)
就算模型已经返回:
{"name":"search_prs","arguments":{"repo":"foo/bar","author":"alice"}}
你后面仍然得自己处理:
换成 Notion、Postgres、本地文件系统,这套适配逻辑又完全不同。
所以重复写的,不是“模型怎么开口”,而是:
大家可能会想,那每个厂家都直接官方提供function calling以及对应的外部适配系统就行呀,比如搞个SDK,比如搞个包等等等,欸,问题就出现在这里,不同的厂家做了不同的适配系统,所以MCP就规定了不同的厂家怎么提供统一的接口。
这才是最痛的地方。
假设你已经给“桌面助手”写好了 GitHub 集成。
现在你要把同样的 GitHub 能力再接到另一个“IDE 助手”里。
在没有更高层 协议 时,常见情况通常是:
因为前一套往往是“某个宿主应用内部的私有接法”,不是一个别的 AI 客户端也能直接复用的统一接口。
Anthropic 后来在工程文章里把这个痛点说得很直接:把 agents 连接到工具和数据,传统上需要为每个配对编写定制集成,这会造成碎片化和重复劳动。MCP 的价值之一,就是让开发者实现一次通用协议,再连接一个更大的集成生态。 (Anthropic)
只有 function calling 时,更像平台只是规定了:
比如:
{"dish":"宫保鸡丁","spicy":"extra","budget":50}
这很好,订单格式统一了。
但每一家商家,平台仍然要自己单独处理:
所以:
这就是为什么在没有更高一层协议时,大家还得“再写一套”。
这时候,MCP 要解决的问题就变得很清楚了。
Anthropic 在 2024 年 11 月公开发布 MCP,并把它定义为一个开放标准,用来让 AI 工具和数据源之间建立安全的双向连接。它的官方介绍中明确写到:开发者可以暴露 MCP servers,AI 应用则可以作为 MCP clients 去连接这些 servers 。 (Anthropic)
如果沿用前面的比喻,那么:
它不只关心“怎么下单”,还关心:
所以,MCP 的价值不在于“替代 function calling”,而在于:
下面这段不是某个特定 SDK 的精确代码,而是为了表达 MCP 的思想:
# Host 应用里有一个 MCP Client
client = MCPClient()
# 连接多个 MCP Server
client.connect("mcp://filesystem-server")
client.connect("mcp://github-server")
client.connect("mcp://delivery-server")
# 统一发现可用能力:tools / resources / prompts
capabilities = client.list_capabilities()
# 把发现到的能力提供给模型
resp = llm.chat(
messages=[{"role": "user", "content": user_input}],
available_tools=capabilities.tools,
available_resources=capabilities.resources
)
# 模型决定调用哪个能力
if resp.tool_call:
result = client.invoke_tool(
server=resp.tool_call.server,
name=resp.tool_call.name,
arguments=resp.tool_call.arguments
)
final = llm.chat(
messages=[
{"role": "user", "content": user_input},
{"role": "tool", "name": resp.tool_call.name, "content": result}
]
)
你会发现,这里关心的已经不只是一个 get_weather() 这种单函数,而是:
这就是为什么大家会说,MCP 处理的是 client/server 层面的接入问题;而 function calling 处理的是 运行时的一次工具调用问题。Anthropic 官方对 MCP 的公开定义里,client/server 架构以及连接外部工具、数据源的能力,就是最核心的部分。 (Anthropic)
总的来说,就是function calling解决了怎么统一工具,MCP解决了不同厂家怎么统一的提供自家的工具的一种通信协议。