Agent开发(三) — MCP与Function call 与Struct output

40 阅读22分钟

Agent开发(三) — MCP与Function call 与Struct output


前言

在具体深入到MCP协议之前,我想先来捋一捋来龙去脉完整的展现MCP 协议出现的必要原因和目的。一切要从 OpenAI 描述的通向通用人工智能(AGI)的五个阶段开始,分别如下:

  1. Level 1: Conversational AI (对话式AI / Chatbot)
    • 专注于自然语言理解和生成,能够进行流畅的对话。
  2. Level 2: Reasoning AI (推理式AI / Reasong 推理者)
    • 具备更强的逻辑推理、问题解决和复杂任务处理能力。
  3. Level 3: Autonomous AI (自主式AI)
    • AI系统能够设定目标、规划行动、并独立执行复杂任务。
  4. Level 4: Innovating AI (创新式AI)
    • AI能够进行科学发现、技术突破或产生新知识,具备高度的创造性。
  5. Level 5: Organizational AI (组织式AI)
    • AI能够进行跨领域、大规模的协调和管理,甚至能有效运行大型组织或经济体。

来自openai的 www.forbes.com/sites/jodie…

Level 1: Conversational AI 很好理解,chatgpt 3.5 出来的时候就是就是该阶段,相信也是很多人的AI启蒙时刻

image.png Level 2: Reasoning AI (推理式AI / Reasong 推理者) 的里程碑就是今年年初引起AI狂热的DeepSeek-R1,一个能够读懂用户心理且具备自我深度思考的大模型

image 1.png

Level 3: Autonomous AI (自主式AI):接下来就是本文的重点,如何给AI 动手动脚 的能力呢?

发展

结构化输出(Structured model outputs)

如果你还有早期AI使用习惯的话,想要从对话里面获取Json或者Xml此类信息是非常困难的一件事情,经常会输出不符合要求的格式出来,为此,早期开发者不得不依赖复杂的后处理逻辑、正则表达式,甚至人工校验来“清洗”模型输出,以确保下游系统能正确解析。

这种不可靠的结构化输出严重限制了AI在自动化流程中的应用——毕竟,如果连“告诉AI去调用哪个API、传什么参数”都无法稳定实现,又谈何“自主执行任务”?

为了解决这个问题,业界逐步引入了 Structured Output(结构化输出) 技术。其核心思想是:在模型生成阶段就强制约束输出格式,而非依赖事后修正。例如Openai 的结构化输出例子

import json
from openai import OpenAI

client = OpenAI(
    api_key="<your-api-key>",
    base_url="https://api.deepseek.com",
)

system_prompt = """
The user will provide some exam text. Please parse the "question" and "answer" and output them in JSON format. 

EXAMPLE INPUT: 
Which is the highest mountain in the world? Mount Everest.

EXAMPLE JSON OUTPUT:
{
    "question": "Which is the highest mountain in the world?",
    "answer": "Mount Everest"
}
"""

user_prompt = "Which is the longest river in the world? The Nile River."

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    response_format={
        'type': 'json_object'
    }
)

print(json.loads(response.choices[0].message.content))

返回Json大概如下

{'question': 'Which is the longest river in the world?', 'answer': 'The Nile River'}

函数调用(Function calling)

接下来就是本文章的主角也是奠定了大模型对外的 操作的行为逻辑,函数调用(也称为工具调用)主要是给大模型提供了一种强大且灵活的方式,以与外部系统交互并访问其训练数据之外的数据。

一个函数由其 schema 定义,该 schema 告知模型它的功能以及它期望的输入参数。一个函数定义具有以下属性:

FieldDescription
typeThis should always be function这个应该始终是 function
nameThe function's name (e.g. get_weather)函数的名称(例如 get_weather )
descriptionDetails on when and how to use the function何时以及如何使用该函数的详细信息
parametersJSON schema defining the function's input arguments定义函数输入参数的 JSON schema
strictWhether to enforce strict mode for the function call是否强制启用函数调用的严格模式

如果你刚刚有尝试过上面这个例子的话,请求体里面的 response_format={"type": "json_object"}`` 就是明确要求 JSON 输出,而Tool则需要进行 **注册** 到LLM的上下文,在LLM需要的时候会自动掉用并转门返回以 tool` 为角色的消息

from openai import OpenAI

def send_messages(messages):
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        tools=tools
    )
    return response

client = OpenAI(
    api_key="<your-api-key>",  # 这里填入deepseek的API密钥
    base_url="https://api.deepseek.com",
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather of a location, the user should supply a location first.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    }
                },
                "required": ["location"]
            },
        }
    },
]

messages = [{"role": "user", "content": "How's the weather in Hangzhou, Zhejiang?"}]
message = send_messages(messages)
print(message.json())

大概返回结果如下

{
  "id": "9ac1395d-f190-4cc0-a7de-3ca1fa3a6493",
  "choices": [
    {
      "finish_reason": "tool_calls",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": "I'll check the weather in Hangzhou, Zhejiang for you.",
        "refusal": null,
        "role": "assistant",
        "annotations": null,
        "audio": null,
        "function_call": null,
        "tool_calls": [
          {
            "id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw",
            "function": {
              "arguments": "{\"location\": \"Hangzhou, Zhejiang\"}",
              "name": "get_weather"
            },
            "type": "function",
            "index": 0
          }
        ]
      }
    }
  ],
  "created": 1760591188,
  "model": "deepseek-chat",
  "object": "chat.completion",
  "service_tier": null,
  "usage": {
    "completion_tokens": 33,
    "prompt_tokens": 181,
    "total_tokens": 214,
    "completion_tokens_details": null,
    "prompt_tokens_details": {
      "audio_tokens": null,
      "cached_tokens": 128
    },
    "prompt_cache_hit_tokens": 128,
    "prompt_cache_miss_tokens": 53
  }
}

假如已经执行完成了之后,以 tool 角色组装成回复内容

{
    "role": "tool",
    "tool_call_id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // 必须匹配第一轮的 tool_calls[0].id
    "name": "get_weather",
    "content": "The current temperature in Hangzhou, Zhejiang is 25°C, with light rain." // 工具的实际输出
}

加入到 chat message 组成下面对话

[
  // 1. 用户的原始请求
  {
    "role": "user",
    "content": "What is the weather in Hangzhou, Zhejiang?"
  },

  // 2. 模型在第一轮返回的工具调用请求
  //    (这是从你提供的JSON响应中提取的 message 对象)
  {
    "role": "assistant",
    "content": "I'll check the weather in Hangzhou, Zhejiang for you.", // 模型的过渡文本 (可选, 有些模型不返回此内容)
    "tool_calls": [
      {
        "id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // 必须使用模型返回的ID
        "function": {
          "arguments": "{\"location\": \"Hangzhou, Zhejiang\"}",
          "name": "get_weather"
        },
        "type": "function"
      }
    ]
  },

  // 3. 你的应用程序执行完工具函数后,返回给模型的结果
  {
    "tool_call_id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // ❗ 必须匹配上面的 tool_calls[0].id
    "role": "tool",
    "name": "get_weather", // ❗ 必须匹配调用的函数名
    "content": "The current temperature in Hangzhou, Zhejiang is 25°C, with light rain." // ❗ 工具函数的实际输出结果 (字符串)
  }
]

这就是一轮完整的 工具调用(Function Calling) 交互流程:模型识别出需要外部信息 → 主动请求调用指定工具 → 应用执行工具并返回结果 → 模型基于结果生成最终回答。

strict Mode

因为LLM本质上还是一个 预测下一个Token 的作为驱动,即使使用Json-Schema来限定还有其他原因导致的并不会进行正确的tool_call,比如下面这个来自 Qwen/Qwen3-VL-235B-A22B-Instruct 的模型要求输出 tool_call的时候反而直接将 tool_call写在了reasoning_content 中

{
    "id": "0199ef984be17de0da6bbd2cdaf17736",
    "object": "chat.completion",
    "created": 1760661360,
    "model": "Qwen/Qwen3-VL-235B-A22B-Instruct",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "",
                "reasoning_content": "<tool_call>\n{\"name\": \"navigate\", \"arguments\": {\"url\": \"https://www.bing.com\"}}\n</tool_call>"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 6385,
        "completion_tokens": 253,
        "total_tokens": 6638,
        "completion_tokens_details": {
            "reasoning_tokens": 0
        }
    },
    "system_fingerprint": ""
}

开启的方式也非常简单,直接在tool里面新增一行 "strict": true 即可

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "strict": true,  // 输入这一行即可
        "description": "Get weather of a location, the user should supply a location first.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
            "additionalProperties": false
        }
    }
}

不过要注意并不是每个模型都支持的,具体还得看各个厂家的说明,比如说 Deepseek 就明确说明支持 https://api-docs.deepseek.com/guides/function_calling#strict-mode-beta

MCP

虽然函数调用极大拓展了大模型的“行动边界”,但它仍存在一个关键瓶颈:缺乏统一标准。不同厂商(如 OpenAI、Anthropic、DeepSeek)对工具的定义格式、调用方式、认证机制、错误处理等各不相同。

开发者若想让一个 AI Agent 同时支持多个模型或接入多种企业系统(如 Slack、Notion、Salesforce、内部数据库),就必须为每种组合编写定制化适配层——这不仅成本高昂,也阻碍了 AI Agent 的规模化部署与互操作性。

正是在这一背景下,Model Context Protocol(MCP,模型上下文协议) 应运而生。

Model Context Protocol(MCP) 由 Anthropic 于 2024 年 11 月正式推出,是一项开源、开放标准协议,旨在解决当前 AI 工具调用生态中“碎片化集成”的核心痛点。其官方目标是“用单一协议取代零散的集成方式”,让 AI 助手能够以统一方式访问任意外部工具或数据源 。

MCP 的本质不是取代函数调用,而是为其提供标准化的通信层。正如 HTTP 之于 Web,MCP 希望成为 LLM 与外部世界交互的“通用语言” 。


技术架构

MCP 采用清晰的客户端-服务器(Client-Server)架构,包含四个核心组件 :

  1. AI 应用(App / Agent Host):用户交互界面,如聊天机器人、自动化工作流引擎;
  2. MCP 客户端(MCP Client):嵌入在 AI 应用中,负责与 MCP 服务器通信,将工具请求转换为 MCP 协议格式;
  3. MCP 服务器(MCP Server):由工具提供方部署,将本地能力(如数据库查询、API 调用)暴露为符合 MCP 规范的接口;
  4. 外部服务(External Service):实际执行操作的系统,如 Slack、Notion、企业 ERP 等。

如何搭建一个最简的 MCP Client/Server

这里涉及到几个关键点:

  • MCP 注册中心和发现
  • MCP 服务执行模块

前者需要将MCP进行注册和发现,后者则是具体的功能的实现

接下来让我们一步步来搭建,首先是下载开源项目 https://github.com/charSLee013/mcp-transport-demo

git clone https://github.com/charSLee013/mcp-transport-demo
cd mcp-transport-demo

安装依赖 (推荐虚拟环境)

uv sync --frozen --all-extras --dev

运行下面的脚本会自动启动 clientserver 用来网络通信

bash scripts/run_sse_demo.sh

运行完成后会有两个文件分别是

  • /tmp/sse_server.log 显示 Uvicorn 启动、监听 8000 端口、处理初始化和两次工具调用。
  • /tmp/sse_client.log 包含客户端发起的工具调用与 SSE 回传的数据,例如 tools/call.result 的文本内容。

下面就是 sse_client.log 来从客户端视角来看看 SSE 运行的全流程

SSE 协议流传输解析

打开 sse_client.log后,下面将一步步拆解整个 SSE 启动与通信流程,并用清晰说明每个阶段的作用和设计意图。

image 2.png


阶段一:建立SSE连接并发现通信端点

这是所有通信的起点,目的是建立一个由服务器到客户端的持久消息通道。

1. 客户端发起SSE连接

>>> REQUEST GET <http://127.0.0.1:8000/sse>
Headers (filtered — hint):
  connection: keep-alive — keep-alive keeps TCP open for streaming
  cache-control: no-store — no-store disables caching for streams

解说:

  • 客户端向服务器的 /sse 端点发送 GET 请求,这是一个标准的SSE握手。
  • connection: keep-alive 请求保持TCP连接,为后续的数据流传输做准备。
  • cache-control: no-store 确保中间节点不会缓存这个实时流。

2. 服务器接受连接并返回SSE流

<<< RESPONSE 200 OK (GET <http://127.0.0.1:8000/sse>)
Headers (filtered  hint):
  connection: keep-alive
  transfer-encoding: chunked  chunked = streaming
  cache-control: no-store  disable caching for streaming
  content-type: text/event-stream; charset=utf-8  payload media type
  x-accel-buffering: no  no disables proxy buffering (SSE)

解说:

  • 服务器返回 200 OK,表示同意建立SSE连接。
  • content-type: text/event-stream 是关键,它告诉客户端这是一个SSE流,而不是普通的HTTP响应。
  • transfer-encoding: chunked 表示数据将以分块的形式发送,适合内容长度未知的流式数据。

3. 服务器推送 endpoint 事件,指定后续通信地址

SSE << event: endpoint
SSE << data: /messages/?session_id=1c75ff593002412a88b5a9ac39f80648
SSE <<

解说:

  • 这是MCP协议的核心设计之一。SSE连接建立后,服务器立刻通过这个连接发送一个名为 endpoint 的特殊事件。
  • data 字段的内容 /messages/?session_id=1c75ff593002412a88b5a9ac39f80648 包含了两个关键信息:
    1. 通信路径: 客户端后续所有请求(initialize, tools/list等)都应发送到 /messages/ 路径。
    2. 会话标识: session_id 用于唯一标识当前的会话,确保服务器能将所有相关请求与这个SSE流正确关联。

阶段二:初始化会话

客户端现在已经知道了如何向服务器发送消息,接下来执行MCP的正式握手。

1. 客户端发送 initialize 请求

>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=1c75ff593002412a88b5a9ac39f80648>
Headers:
  content-type: application/json
Body:
{
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "mcp",
      "version": "0.1.0"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}

解说:

  • 客户端向 /messages/ 端点发送一个 POST 请求,并附上上一步获取的 session_id
  • 这是一个标准的JSON-RPC 2.0请求。methodinitializeid0
  • params 对象详解:
    • protocolVersion: 声明客户端希望使用的MCP协议版本。
    • capabilities: 客户端所支持的能力集,这里为空对象 {},表示使用默认能力。
    • clientInfo: 客户端的身份信息,包括名称 mcp 和版本 0.1.0

2. 服务器响应 202 Accepted

<<< RESPONSE 202 Accepted (POST <http://127.0.0.1:8000/messages/?session_id=>...)
Body: Accepted

解说:

  • 202 Accepted 状态码是异步通信模式的关键。它表示服务器已成功接收并排队处理该请求,但处理结果尚未准备好。客户端应继续监听SSE流以获取最终结果。

3. 服务器通过SSE返回 initialize 结果

SSE << event: message
SSE << data: {
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "experimental": {},
      "prompts": {
        "listChanged": false
      },
      "resources": {
        "subscribe": false,
        "listChanged": false
      },
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "SSE Progress Demo",
      "version": "1.18.1.dev3+40acbc5"
    }
  }
}
SSE <<

解说:

  • 服务器通过SSE的 message 事件返回了完整的JSON-RPC响应。id: 0 与客户端请求对应。
  • result 对象详解:
    • protocolVersion: 服务器同意使用的协议版本。
    • capabilities: 服务器的能力集。这里详细列出了服务器支持的功能领域,如 promptsresourcestools,以及在这些领域中是否支持某些高级特性(如 listChanged, subscribe)。
    • serverInfo: 服务器的身份信息,包括名称和版本。

阶段三:工具调用

会话初始化后,客户端开始与服务器提供的工具进行交互。

3.1 发送初始化完成通知并获取工具列表

在MCP协议中,客户端在收到 initialize 响应后,需要发送一个 notifications/initialized 通知,告诉服务器它已完成初始化。

>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
  "method": "notifications/initialized",
  "jsonrpc": "2.0"
}

解说: 这是一个JSON-RPC 通知(没有id字段),表示客户端已准备就绪。

随后,客户端立即请求工具列表:

>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
  "method": "tools/list",
  "jsonrpc": "2.0",
  "id": 1
}

服务器通过SSE返回了工具列表的完整定义:

SSE << event: message
SSE << data: {
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "calculator",
        "title": "Calculator",
        "description": "Execute a basic calculation.",
        "inputSchema": {
          "type": "object",
          "title": "calculatorArguments",
          "properties": {
            "a": {"title": "A", "type": "number"},
            "b": {"title": "B", "type": "number"},
            "operation": {
              "title": "Operation",
              "type": "string",
              "enum": ["add", "subtract"]
            }
          },
          "required": ["a", "b", "operation"]
        },
        "outputSchema": {
          "type": "object",
          "title": "calculatorOutput",
          "properties": {
            "result": {"title": "Result", "type": "string"}
          },
          "required": ["result"]
        }
      },
      {
        "name": "stream_demo",
        "title": "Stream Demo",
        "description": "Emit log + progress notifications over SSE before returning a result.\\n\\n    Args:\\n        seconds: total duration (simulated)\\n        steps: number of progress steps\\n    ",
        "inputSchema": {
          "type": "object",
          "title": "stream_demoArguments",
          "properties": {
            "seconds": {"title": "Seconds", "type": "number", "default": 2.0},
            "steps": {"title": "Steps", "type": "integer", "default": 5}
          }
        }
      }
    ]
  }
}
SSE <<

解说: result 对象包含一个 tools 数组,每个元素都是一个工具的完整元数据定义,包括 namedescription 以及至关重要的 inputSchema(输入参数的JSON Schema)和 outputSchema(输出的JSON Schema)。

3.2 调用流式工具 stream_demo

1. 客户端调用 stream_demo

>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
  "method": "tools/call",
  "params": {
    "name": "stream_demo",
    "arguments": {
      "seconds": 1.5,
      "steps": 4
    },
    "_meta": {
      "progressToken": 2
    }
  },
  "jsonrpc": "2.0",
  "id": 2
}

解说:

  • params 对象详解:
    • name: 指定要调用的工具。
    • arguments: 传递给工具的参数,这里指定总时长为1.5秒,分为4步。
    • _meta: 元数据对象。progressToken: 2 是一个客户端定义的令牌,用于将后续的进度通知与这次特定的调用关联起来。

2. 服务器通过SSE推送一系列通知和最终结果

服务器在任务执行期间,通过SSE持续推送消息。

  • 开始通知:

    {"method":"notifications/message","params":{"level":"info","data":"stream started: 4 steps / 1.5s total"},"jsonrpc":"2.0"}
    
    
  • 循环推送进度(以第1步为例):

    // 日志消息
    {"method":"notifications/message","params":{"level":"info","data":"step 1/4"},"jsonrpc":"2.0"}
    // 进度通知
    {"method":"notifications/progress","params":{"progressToken":2,"progress":25.0,"total":100.0,"message":"step 1"},"jsonrpc":"2.0"}
    
    

    解说:

    • notifications/message: 这是一个通用的日志通知。
    • notifications/progress: 这是专门的进度通知。其 params 中包含的 progressToken: 2 正好与客户端调用时传入的_meta.progressToken匹配,客户端可以据此识别这是哪个任务的进度。
  • 结束通知和最终结果:

    // 结束日志
    {"method":"notifications/message","params":{"level":"info","data":"stream finished"},"jsonrpc":"2.0"}
    // 最终的JSON-RPC响应
    {
      "jsonrpc":"2.0",
      "id":2,
      "result": {
        "content": [
          {
            "type": "text",
            "text": "{\\n  \\"status\\": \\"done\\",\\n  \\"steps\\": 4,\\n  \\"seconds\\": 1.5,\\n  \\"elapsed\\": 1.51\\n}"
          }
        ],
        "isError": false
      }
    }
    
    

    解说:

    • 最后,服务器发送了一个与请求id: 2匹配的JSON-RPC响应,表示任务已彻底完成。
    • result 对象详解:
      • content: 一个包含结果的数组,每个元素可以有不同类型。这里是一个 type: "text" 的文本内容。
      • isError: 布尔值,明确指出此调用是否成功。

3.3 调用常规工具 calculator

这是一个快速、非流式的调用,但其内部机制与流式调用完全一致。

1. 客户端调用 calculator

>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
  "method": "tools/call",
  "params": {
    "name": "calculator",
    "arguments": {
      "a": 7,
      "b": 4,
      "operation": "add"
    }
  },
  "jsonrpc": "2.0",
  "id": 3
}

解说: 调用 calculator 工具,提供加法操作所需的参数。

2. 服务器通过SSE返回结果

SSE << event: message
SSE << data: {
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "结果: 11.0"
      }
    ],
    "structuredContent": {
      "result": "结果: 11.0"
    },
    "isError": false
  }
}
SSE <<

解说:

  • 服务器同样通过SSE返回了id: 3对应的响应。
  • result 对象详解:
    • content: 包含人类可读的文本结果。
    • structuredContent: 这是一个额外的字段,提供了结构化的数据(这里是JSON对象),方便程序直接解析和使用,而无需从文本中提取。
    • isError: false 表示调用成功。

关于通信中的ID(额外补充内容)

如果你足够细心的话,会发现建立连接后,每次请求都会记上 id ,那这是什么呢?

简单来说,id 的核心作用是:关联请求与响应,实现异步通信

下面我们从三个层面来深入理解它的作用。


层面一:基础作用 —— 关联请求与响应

这是 id 最直接的功能。在一个理想化的、一问一答的同步世界里,你可能不需要 id,因为你发送一个请求,然后等待一个响应。但在MCP over SSE的异步模型中,情况完全不同。

工作原理

  1. 客户端生成唯一ID:当客户端发起一个需要响应的请求时(如 initialize, tools/list, tools/call),它必须生成一个唯一的 id(如 id: 0)并放入JSON-RPC请求对象中。
  2. 服务器必须原样返回:服务器在处理完该请求后,其返回的JSON-RPC响应对象中,必须包含与请求完全相同的 id
  3. 客户端进行匹配:客户端接收到通过SSE流推送过来的响应时,会检查响应中的 id,并与它之前发送的请求进行匹配,从而知道“这个响应对应的是我之前发的哪个请求”。

示例分析(来自你的日志)

  • 客户端发送 id: 0initialize 请求。
  • 之后,客户端通过SSE收到 id: 0 的响应,就知道这是 initialize 的结果。
  • 客户端发送 id: 1tools/list 请求。
  • 之后,客户端收到 id: 1 的响应,就知道这是工具列表。

层面二:核心机制 —— 支持高并发与乱序响应

这是 id 最重要的价值所在,也是异步通信模式能够成功的关键。

设想一个没有 id 的混乱场景

  1. 客户端快速连续发送了两个请求:
    • 请求A(tools/call stream_demo),一个耗时1.5秒的慢任务。
    • 请求B(tools/call calculator),一个毫秒级的快任务。
  2. 服务器异步处理它们。请求B先完成,请求A后完成。
  3. 服务器通过SSE流把结果推回给客户端。先推送了B的结果,然后推送了A的结果。
  4. 问题来了:客户端先收到一个结果,然后又收到一个结果。它怎么知道哪个结果是 stream_demo 的,哪个是 calculator 的?它无法区分,整个通信模型就崩溃了。

id 如何解决这个问题id 为每个请求-响应对提供了一个独一无二的“身份证”。

  1. 客户端发送请求A,带上 id: 2
  2. 客户端发送请求B,带上 id: 3
  3. 服务器先完成B,通过SSE推送一个 id: 3 的响应。
  4. 客户端收到 id: 3 的响应,立刻知道:“哦,这是 calculator 的结果,处理一下。”
  5. 服务器后完成A,通过SSE推送一个 id: 2 的响应。
  6. 客户端收到 id: 2 的响应,知道:“这是 stream_demo 的最终结果。”

结论id 使得响应可以乱序返回,客户端依然能正确地处理,这正是实现高并发和非阻塞交互的基础。客户端不必傻等一个慢请求完成,才能发下一个请求。


层面三:语义区分 —— 区分“请求”与“通知”

在JSON-RPC协议中,id 的存在与否,定义了消息的根本类型。

  • id 的消息是“请求”:表示客户端期望得到一个响应。例如 {"method": "tools/list", "id": 1}
  • 没有 id 的消息是“通知”:表示客户端只是在告知服务器某件事,完全不期望得到任何响应。例如日志中的 notifications/initialized
{
  "method": "notifications/initialized",
  "jsonrpc": "2.0"
}

为什么需要通知? 有些事件只是“告知”,不需要回复。比如“我(客户端)已经初始化完了”,服务器收到后无需专门回复一个“收到”,这样减少了不必要的网络往返,提高了效率。


通俗比喻:餐厅点餐

把整个过程想象成你在一家餐厅点餐:

  • 你(客户端)服务员(SSE连接) 点了两道菜:
    1. 慢炖的佛跳墙(stream_demo),服务员给了你一张 #A号 牌(id: 2)。
    2. 快手的凉拌黄瓜(calculator),服务员给了你一张 #B号 牌(id: 3)。
  • 后厨(服务器) 同时制作。
  • 凉拌黄瓜先做好了,服务员端过来,喊道:“#B号的菜好了!” 即使这不是你先点的,但你看了一眼牌子就知道是你的,于是开吃。
  • 过了很久,佛跳墙做好了,服务员喊道:“#A号的菜好了!” 你从容地享受你的大餐。
  • 如果你只是跟服务员说“麻烦帮我加点茶水”(通知),你就不需要拿号牌,服务员也不会特地跑回来告诉你“茶水已加好”。

在这个比喻里,号牌就是 id。它让点餐(请求)和上菜(响应)过程可以高效、有序、并发地进行。


总结:从“能说”到“能做”,再到“能自主”

回顾全文,我们沿着一条清晰的技术演进路径,逐步为大模型赋予了越来越强的“行动能力”:

  1. 结构化输出(Structured Outputs) 解决了“说清楚”的问题。通过强制模型输出合法、可靠的 JSON,我们让 AI 的语言具备了机器可解析的确定性,为自动化奠定了第一块基石。
  2. 函数调用(Function Calling) 解决了“能做事”的问题。通过将外部工具的能力以 JSON Schema 的形式注册给模型,AI 不再局限于其训练数据,而是能够主动调用 API、查询数据库、执行计算,真正开始与外部世界互动。
  3. 严格模式(Strict Mode) 解决了“做对事”的问题。它将函数调用从“概率性输出”提升为“契约式交付”,确保模型生成的调用指令 100% 符合开发者定义的规范,为生产环境的可靠性提供了保障。

然而,当我们将视野从单个工具扩展到整个企业生态时,一个新的瓶颈出现了:碎片化的集成方式。每个模型、每个服务都有自己的“方言”,这让构建一个能同时操作 Slack、Notion、Salesforce 和内部系统的通用 AI Agent 变得异常复杂。

MCP(Model Context Protocol)的出现,正是为了解决这个规模化难题。它没有发明新的能力,而是为已有的能力提供了一个标准化的“神经接口”

  • 对工具提供方而言,只需实现一次 MCP Server,其能力就能被所有兼容 MCP 的 AI 应用无缝调用。
  • 对 AI 应用开发者而言,只需集成 MCP Client,就能接入一个不断增长的、标准化的能力生态,无需再为每个服务编写定制胶水代码。

更重要的是,MCP 的设计(如 SSE 流式传输、id 驱动的异步通信、Resources/Tools 的语义区分)完美契合了 Level 3: Autonomous AI 的核心需求:

  • 持续感知:通过 Resources 和流式通知,Agent 能实时掌握环境状态;
  • 可靠执行:通过 Tools 和严格模式,Agent 能安全、准确地执行动作;
  • 自主协同:通过统一协议,Agent 能自由组合不同领域的工具,完成复杂任务。

因此,MCP 不仅仅是一个技术协议,更是通向自主智能体时代的关键基础设施。它标志着 AI 开发范式正从“手工作坊”迈向“工业化标准”,让“能连续数日代表用户自主完成任务”的 AI Agent 从概念走向现实。

未来已来,只是尚未均匀分布。而 MCP,正是加速这一分布的催化剂。