cover_image

既生Function Call,何生MCP

李宏建 蚂蚁技术AntTech
2025年04月11日 10:46
图片


文|李宏建(蚂蚁集团技术专家)


先看网上广传的MCP的科普图:

图片


接下来我们会慢慢解开它的面纱,聊MCP,离不开聊Function Call。其实Function Call仍然是大模型调用工具的基本协议,MCP更像是我们在架构系统时,提出的一个更完整的工程方案。









智能体的手脚-Function Call









图片


在智能体中我们利用大模型非常核心的一个能力就是选对工具,这个方式就是Function Call。其需要我们提供已有的工具列表给到大模型,让模型针对当前上下文,选择对工具,并提取正确的参数。然后交给一个容器去执行我们的工具,工具执行的结果会继续作为大模型下一轮的上下文。









Function Call API









Function Call来源于2023年的OpenAI。我们详细看下OpenAI的标准调用协议,其中Function Call对应json中的tools部分,另外多轮对话放在messages中。

POST /v1/chat/completions{  "model""gpt-4o-2024-08-06",  "messages": [    {      "role""system",      "content""You are a helpful assistant. The current date is August 6, 2024. You help users query for the data they are looking for by calling the query function."    },    {      "role""user",      "content""look up all my orders in may of last year that were fulfilled but not delivered on time"    }  ],  "tools": [    {      "type""function",      "function": {        "name""query",        "description""Execute a query.",        "strict"true,        "parameters": {          "type""object",          "properties": {            "table_name": {              "type""string",              "enum": ["orders"]            },            "columns": {              "type""array",              "items": {                "type""string",                "enum": [                  "id",                  "status",                  "expected_delivery_date",                  "delivered_at",                  "shipped_at",                  "ordered_at",                  "canceled_at"                ]              }            },            "conditions": {              "type""array",              "items": {                "type""object",                "properties": {                  "column": {                    "type""string"                  },                  "operator": {                    "type""string",                    "enum": ["="">""<"">=""<=""!="]                  },                  "value": {                    "anyOf": [                      {                        "type""string"                      },                      {                        "type""number"                      },                      {                        "type""object",                        "properties": {                          "column_name": {                            "type""string"                          }                        },                        "required": ["column_name"],                        "additionalProperties"false                      }                    ]                  }                },                "required": ["column""operator""value"],                "additionalProperties"false              }            },            "order_by": {              "type""string",              "enum": ["asc""desc"]            }          },          "required": ["table_name""columns""conditions""order_by"],          "additionalProperties"false        }      }    }  ]}

上下/左右滑动查看









Function Call怎么work









这和我们通常认识的只给大模型一个prompt,或者用户的prompt 拼接上系统提示词和大模型对话的方式看起来有了gap。其实这里可以理解为在真正请求大模型之前,大模型额外提供了一个api,接收用户传进来的json,最终会按照一个模板格式化成最终的prompt。


图片


我们用qwen qwq 32b的这个模板具体看下(源码tokenizer_config.json 的chat_template这个字段):

"chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- '' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures withinXML tags:\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\\n\\nFor each function call, return a json object with function name and arguments withinXML tags:\\n\\n{\\\"name\\\":, \\\"arguments\\\":}\\n<|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" and not message.tool_calls %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content.split('')[-1].lstrip('\\n') %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- message.content }}\n {{- '\\n' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n\\n' }}\n{%- endif %}\n",

上下滑动查看


我们用一个具体的例子详细介绍一下这个过程,如果我们给到的是一个查询天气的工具,输入的JSON:


{    "messages": [        {            "role": "user",            "content": "请查询杭州的天气"        }    ],    "tools": [        {            "name": "getWeather",            "description": "获取指定城市的天气",            "parameters": {                "type": "object",                "properties": {                    "city": {                        "type": "string",                        "description": "城市名称"                    },                    "date": {                        "type": "string",                        "description": "请求天气的日期"                    }                },                "required": ["city"]            }        }    ],   }

上下滑动查看


最终经过模板加工,最终给到大模型的prompt大概是这样的:

<|im_start|>system
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:<tools>{"name""getWeather""description""获取指定城市的天气""parameters": {"type""object""properties": {"city": {"type""string""description""城市名称"}, "date": {"type""string""description""请求天气的日期"}}, "required": ["city"]}}</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:<tool_call>{"name""<function-name>""arguments": <args-json-object>}</tool_call><|im_end|>
<|im_start|>user请查询杭州的天气<|im_end|>

上下滑动查看


是不是看起来,特别符合我们的认知了,告诉模型可以选那些工具,历史的对话是什么,让模型选择工具,返回的工具格式包含工具名称和提取的工具参数。









Function Call为什么能work









special_tokens


上边的例子我们发现了一个新的细节,<|im_start|>、<|im_end|> 这种比较奇怪的tokens是什么?大模型中称他们为special tokens。它们其实是后训练的关键,模型是在后训练的过程中,才第一次见到这些 tokens(预训练不可见),然后对微调样本的学习过程中,才理解了它们。


想象一下我们在教一个小朋友写故事,我们会告诉他:“故事开始时,你需要写‘开始’。”“故事结束时,要写‘结束’。”“如果你有两个人说话,中间要加一个分隔‘:’来区分。”然后小朋友看着我们写的几个示范,慢慢就学会了这些规则。在每次写故事时,他都会记得这些特殊标记是哪里需要用的。


qwen qwq 的special_tokens有下边这些:

{"additional_special_tokens": ["<|im_start|>","<|im_end|>","<|object_ref_start|>","<|object_ref_end|>","<|box_start|>","<|box_end|>","<|quad_start|>","<|quad_end|>","<|vision_start|>","<|vision_end|>","<|vision_pad|>","<|image_pad|>","<|video_pad|>"],"eos_token": {"content": "<|im_end|>","lstrip": false,"normalized": false,"rstrip": false,"single_word": false},"pad_token": {"content": "<|endoftext|>","lstrip": false,"normalized": false,"rstrip": false,"single_word": false}}

上下滑动查看


Function Call 的能力来源于大模型的后训练,详细来说,就是给大模型一些选择工具的样本,让模型学会在合适的时机决策需要调用工具。


假设我们现在需要微调一个具有点餐能力的智能体,代替餐厅的服务员。我们需要让模型学会在用户让推荐热饮的时候,决策调用商品列表的接口:query_product_list。当调用完成返回了热饮餐品后,这个列表的数据会作为模型对话的下一轮的上下文继续使用,如下图的蜀实耙耙柑(热饮)等饮品。模型在获得这个列表后,可以决定怎么和用户对话。


下图可见,微调阶段可以会将对话样本用相同的special_tokens预处理。

<|im_start|>system

你是一个奶茶店的智能点单机器人,负责帮助顾客浏览商品、查看商品详情、定制饮品规格,并完成下单过程。<|im_end|>

<|im_start|>user

帮我看看有什么热饮吧<|im_end|>

<|im_start|>assistant

Action: query_product_list

Action Input: {"category": "热饮", "sort_by": "price"}

<|im_end|>

<|im_start|>tool

[{"id": "P1010", "name": "蜀实耙耙柑(热饮)", "description": "臻选优质产区自然熟成的春见耙耙柑,现剥果肉厚实脆嫩。", "price": 16}, {"id": "P1011", "name": "百香凤梨(热饮)", "description": "热饮果茶温暖来袭,满满维C添活力!", "price": 12}, {"id": "P1012", "name": "杨枝甘露(热饮)", "description": "暖心热饮,酸甜适中。", "price": 17}]<|im_end|>

<|im_start|>assistant

以下是当前可用的热饮列表(按价格排序):

1. 百香凤梨(热饮) - 12元

2. 蜀实耙耙柑(热饮) - 16元

3. 杨枝甘露(热饮) - 17元

请问您需要查看哪个商品的详情?<|im_end|>

<|im_start|>user

我要查看百香凤梨(热饮)的详情<|im_end|>









不是标准的标准









到这里我们思考下为什么要有Function Call,按照我们对话模板那块讲的最终拼接出来的就是一个包含了工具列表上下文的prompt,我们完全可以直接自己写一个few-shot prompt 让大模型实现工具的选择。


但是不同用户的工具定义格式是不同的,为了把用户的请求和大模型厂商微调的格式对上,所以才有了Function Call。Function Call本质是就是每个大模型厂家的事实标准。


OpenAI虽然最有行业影响力,但是当初Function Call那套协议没有按照标准推广,其他大厂可以选择用,也可以自己定义,比如你叫tool我可以叫function。


标准的事情非常适合OpenAI来做,但是它没做,不然估计也不会有MCP。Function Call不是一个行业标准会让开发者面临开发问题,假如我们换了一个新模型,又假如JSON格式是不一样的,那我们就需要定制化再适配一遍。


此时我们作为一个开发者会发现,非常头疼的一个点主要在工具的可复用性上,我们开发的工具没法做到一次开发多次复用,工具无法变成万能的可插拔插件。


我们想到了两种方式解决这个问题:


撂挑子不干了,这个代码让模型自己生成,然后只需要额外提供一个代码编译运行环境就可以了。 

1















2

遇事不决加一层,包一下Function Call,用一个service包装一下。更进一步我们把这个service我们做成标准,并把提供service的服务叫server。


前者是codeAct,后者是最近大火的MCP。









CodeAct









2025年初Manus爆火,很关键的原理就是,借助大模型针对具体的任务生成个性化的python等代码。


图片


CodeAct(下图右)的和functioncall(下图左)类似push和pull的关系,前者把更多更核心的工作交给了大模型。


图片


按照论文的指标,codeact的方式该好的地方好(成功率更高),可能不好地方也好(互动轮次更少,代表效率更高)。当然需理性看待,局限还是挺明显的,局限就在它的写代码本身,制作个pdf啥的可以,复杂场景不一定写的好。


图片









MCP









Anthropic是一家成立于2021年的人工智能初创公司,由OpenAI的前研究副总裁达Dario Amodei和GPT-3论文的一作Tom Brown等人共同创立。该公司非常有名的产品是大模型-Claude。24年11月,Anthropic推出MCP协议,并在自家的Claude客户端做了支持。MCP真正火起来是因为Cursor,AI coding届扛把子的插件集成支持了MCP。


不多久后起了个大早赶了晚集的OpenAI也支持了MCP。


一个用了MCP的智能体的动图:


图片


如上图上半部分,MCP的核心包括host-client-server三元素。


1

host

通义千问APP、豆包APP


2

server

工具、资源(比如mongodb,这个比较有趣,有点像文本转sql的方式),就是我们原来的工具的增强版本


3

client

host需要借助一个client和server通信,暂且理解为一个线程。


例子:我们在通义千问问了一个今天蚂蚁A空间天气的问题,host会启一个查询天气的client和查询天气的server进行通信。绕来绕去,其实就是多了client一个角色,就是我们常说的中间层,不过大模型是Function Call的client,server屏蔽了funtion的差异性。 


MCP 和 Function Call的核心差异

图片


关于MCP的更多细节可以参考官网:https://modelcontextprotocol.io/introduction









快速开始









真实的server有很多类型,可以把它理解成一个spring mvc服务。


@GetMapping = @mcp.resource,

@PostMapping = @mcp.tool()


我们用fast-mcp 直觉感受下如何快速开发MCP。

pip install fastmcp
from fastmcp import FastMCP

# Create an MCP servermcp = FastMCP("Demo")

@mcp.tool()def get_current_weather(city: str) -> str:    """Get the current weather for the specified city"""    # 这里模拟一个天气查询    # 实际代码中,应该调用真实的天气 API 获取数据    return f"Current weather in {city}: Sunny, 25°C"  # 假设的天气信息

@mcp.resource("weather://forecast/{city}")def get_weather_forecast(city: str) -> str:    """Get the weather forecast for the specified city"""    # 模拟天气预报    # 实际代码中,调用天气 API 获取天气预报    return f"Weather forecast for {city}: Rainy in the next 3 days"  # 假设的天气预报信息

上下/左右滑动查看









SOFAAI现已集成MCP Server









MCP Server


1. 添加依赖

Spring MVC项目,添加以下依赖:

<dependency>    <groupId>com.alipay.sofa</groupId>    <artifactId>sofa-ai-mcp-server-webmvc-sofa-boot-starter</artifactId></dependency>


2. 提供天气查询Tools 

@Configuration(proxyBeanMethods = false)public class ToolsConfiguration {
    @Bean    public ToolCallbackProvider myTools() {        List<ToolCallback> tools = List.of(ToolCallbacks.from(new WeatherTools()));        return ToolCallbackProvider.from(tools);    }
}
public class WeatherTools {
    @Tool(description = "Get the current weather for the specified city")    public String getCurrentWeather(@ToolParam(description = "City name"String city) {        // 这里模拟一个天气查询        // 实际代码中,你应该调用真实的天气 API 获取数据        return "Current weather in " + city + ": Sunny, 25°C"// 假设的天气信息    }
    @Tool(description = "Get the weather forecast for the specified city")    public String getWeatherForecast(@ToolParam(description = "City name"String city) {        // 模拟天气预报查询        // 实际代码中,调用天气 API 获取天气预报        return "Weather forecast for " + city + ": Rainy in the next 3 days"// 假设的天气预报信息    }}


上下/左右滑动查看


MCP Client

MCP Client 集成了 Tracer组件

1. 添加依赖

<dependency>            <groupId>com.alipay.sofa</groupId>            <artifactId>sofa-ai-mcp-client-sofa-boot-starter</artifactId>           </dependency>

2. 配置相关配置

//目前只支持openai chatmodel sofa.ai.openai.chat.base-url=https://antchat-prepub.alipay.comsofa.ai.openai.chat.api-key=xxxsofa.ai.openai.chat.options.model=大模型
sofa.ai.mcp.client.enabled=truesofa.ai.mcp.client.name=test-mcp-clientsofa.ai.mcp.client.version=1.0.0sofa.ai.mcp.client.request-timeout=30ssofa.ai.mcp.client.type=ASYNCsofa.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
3. 调用 MCP Server@SpringBootApplicationpublic class SofaBootMcpClientApplication {    public static void main(String[] args) {        System.setProperty("spring.profiles.active""mcp-client");        SpringApplication.run(SofaBootMcpClientApplication.class, args);    }
    @Bean    public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,                                                 ConfigurableApplicationContext context) {
        return args -> {
            var chatClient = chatClientBuilder                    .defaultTools("test-mcp-client-server1-getCurrentWeather",                                  "test-mcp-client-server1-getWeatherForecast")                    .build();
                        System.out.println("\n>>> ASSISTANT: " + chatClient.prompt("查询未来3天的杭州天气").call().content());
            context.close();        };    }}

上下/左右滑动查看

图片

继续滑动看下一个
蚂蚁技术AntTech
向上滑动看下一个