Function Call、Tool Call、MCP

发布时间:

被反复问一个问题:Function Call、Tool Call、MCP,不都是让 AI 调工具吗,只是换了名字,旧瓶装新酒? 纯文字解释确实容易绕。把同一个需求分别用三种方式过了一遍代码,很容易看代码就懂了。

就和当初学 transformers 架构的时候,看那个图看一天也是理解到一定程度就卡住了,但是当你打开哈佛的The Annotated Transformer网站,然后看代码的运行的时候,很容易就知道架构和数据的流向了 。

先给结论

它们不是互斥关系,而是分层关系。Tool Call 是要做什么动作,MCP 是这动作通过什么标准接出去。 Function Call和Tool Call是模型的调用能力层,AI 决定该调用什么工具,而MCP是工具接入标准层,定义了工具如何被发现、描述、调用。

用同一个需求对比:创建 GitHub Issue

我用同一个任务来对比:用户说给 repo 创建一个 bug issue。

1)纯 Function Call(手写工具 + 手写分发)

# 伪代码:手动注册工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "create_github_issue",
            "description": "Create an issue in GitHub repository",
            "parameters": {
                "type": "object",
                "properties": {
                    "repo": {"type": "string"},
                    "title": {"type": "string"},
                    "body": {"type": "string"}
                },
                "required": ["repo", "title"]
            }
        }
    }
]

resp = llm.chat(messages=messages, tools=tools)
call = resp.tool_calls[0]

# 关键点:需要我们自己写分发逻辑
if call.name == "create_github_issue":
    args = call.arguments
    run(["gh", "issue", "create",
         "--repo", args["repo"],
         "--title", args["title"],
         "--body", args.get("body", "")])

这套方式的问题是:工具一多,if/elif 会越来越长;每加一个工具,主程序就要改。

2)Tool Call(并行调用能力)

# 伪代码:模型一次返回多个 tool calls
resp = llm.chat(messages=messages, tools=tools)

for call in resp.tool_calls:
    # 我们仍然要自己做分发,但可并发处理
    dispatch(call.name, call.arguments)

它不是完全不同的技术,而是OpenAI后来的标准能力升级:模型可在一个回合里给出多个工具调用请求,减少往返。 这个是背景:

2023 年 11 月 6 日(OpenAI 首届 DevDay 开发者大会)当时和 gpt-4-1106-preview 模型一起作为重磅更新发布。官方博客文章: 标题是 “New models and developer products announced at DevDay”。文章中有一节专门叫 “Function calling updates”,里面明确指出了弃用旧模式,推出支持并行调用 (Parallel function calling) 的新架构。假设你的系统需要对比三个不同数据库表的数据来生成一份报告。在 Function Call 时代,LLM 只能请求查表 A -> 等待代码返回结果 -> LLM 再思考 -> 请求查表 B -> 等待结果 -> 请求查表 C。这是极度低效的串行阻塞。每一次往返(Round-trip)都在消耗几十到几百毫秒的网络延迟。 引入 Tool Calling 后,模型可以一次性输出一个包含 3 个调用的 JSON 数组。代码拿到这个数组,可以直接起多个并发线程/协程,同时去查表 A、B、C,然后把三个结果一次性塞回给模型。

整体耗时从 T_a + T_b + T_c 骤降到 max(T_a, T_b, T_c)。

3)MCP 模式(工具发现和调用标准化)

# 伪代码:client 只认 MCP,不写死具体工具
mcp = MCPClient("github-mcp-server")

# 动态拿工具定义,而不是手写 tools 列表
tools = await mcp.list_tools()
resp = llm.chat(messages=messages, tools=tools)

for call in resp.tool_calls:
    # 通用转发,不再写 if/elif,这个是关键
    result = await mcp.call_tool(call.name, call.arguments)
    feed_back_to_llm(result)

这里的关键变化是:主程序和具体工具解耦。新增工具通常改 MCP Server 端,不用频繁改客户端分发逻辑。

看代码会豁然开朗

因为差别不在有没有调用工具,而在谁负责适配: 纯 Tool Call是我们自己适配每个工具,MCP是协议帮我们做了通用适配层 。所以看起来都在调工具,但工程成本完全不是一个量级。总结下,Tool Call 解决模型怎么发起工具请求,MCP 解决工具怎么标准化接入并跨客户端复用。