高级 📋 5 个步骤 第 469 / 470 篇

用 MCP Sampling 让 Server 反向调用大模型(sampling/createMessage 实操)

MCP Sampling 实操:用官方 mcp 包的 FastMCP 写会调用 ctx.session.create_message 的服务器,配带 sampling_callback 的客户端,跑通 sampling/createMessage 反向请求宿主模型,并说明弃用状态与安全边界。

2026.09.24· 18 分钟阅读· 约 1552 字· 🔌 MCP / 🤖 大模型

标准 MCP 是"客户端调用服务端的工具";而 Sampling(采样)把方向反过来——服务端在一个工具执行到一半时,向客户端发一个 sampling/createMessage 请求,请客户端用自己的模型推理一次,再把结果送回服务端。这样服务器本身不用持有任何 API Key,也能"借用"宿主的模型能力。本教程用官方 mcp 包的 FastMCP 写一个会反问模型的服务器,并配一个带 sampling 回调的客户端把它跑通,全程可复现。

🔁 本教程适合:已经写过或跑通过 MCP Server / Client 的开发者。如果你还没接触 MCP,先看本中心《从零手撸 ReAct》或 MCP 入门类教程。Sampling 属于进阶协议特性。

先搞懂:Sampling 把请求方向反了过来

普通流程:客户端 → 调用服务端工具 → 服务端返回结果。Sampling 流程:客户端调用工具 → 服务端执行中途 → 服务端向客户端请求"帮我用模型推理一下" → 客户端用自己的 Key 跑模型 → 结果回传服务端 → 服务端继续干完活返回。

方向谁发起谁付模型费用
普通工具调用客户端不涉及模型
Sampling服务端(中途)客户端(宿主模型)

版本红线:自 2026-07-28 起,sampling/createMessage 被标记为 deprecated(SEP-2577),但在 2025-11-25 版本下仍完全可用。新服务器建议直接调 LLM 提供方 API;只有在"不想让服务器持有密钥"的场景才用 Sampling。客户端若在现代化连接上声明 sampling 能力,会收到弃用警告。

Step 1:装 mcp 包并建项目

1 建项目与依赖

用官方 mcp 包(含 FastMCP)。注意:还有一个独立的 fastmcp v2 包,API 不同,本教程统一用 mcp.server.fastmcp

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "mcp[cli]"
💡 一条命令起服务器:fastmcp.run(transport="stdio")。调试时加 mcp dev server.py 可用 Inspector 查看 sampling 请求是否真的发出。

Step 2:写会 Sampling 的 Server

2 工具里调用 ctx.session.create_message

新建 server.py。工具的参数里加一个 ctx: Context,框架会自动注入;在工具内部用 await ctx.session.create_message(...) 反向请求一次模型推理。下面这个工具让服务器"借"客户端的模型把一段文本总结一下。

from mcp.server.fastmcp import FastMCP, Context
from mcp.types import SamplingMessage, TextContent

mcp = FastMCP(name="sampling-demo")

@mcp.tool()
async def summarize(text_to_summarize: str, ctx: Context) -> str:
    """借用宿主模型总结一段文本,服务端本身不持有任何 LLM Key。"""
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"请用一句话总结以下内容:\n{text_to_summarize}",
                ),
            )
        ],
        max_tokens=4000,
        system_prompt="你是一个严谨的研究助手,只输出总结,不发挥。",
    )
    if result.content.type == "text":
        return result.content.text
    raise ValueError("Sampling 返回内容不是文本")

if __name__ == "__main__":
    mcp.run(transport="stdio")

不可信来源勿开 Sampling:一个恶意或被攻破的服务器可以在 messages / systemPrompt 里塞入提示注入,借你的模型外泄上下文或篡改行为。只在你审查过的服务器上允许 sampling 能力。

Step 3:写带 sampling 回调的 Client

3 客户端实现 handle_sampling

客户端要提供一个 sampling 回调,真正去跑模型。下面用 OpenAI 兼容接口举例;回调签名在新版为 (context, params) 形式。

import os
from openai import OpenAI
from mcp import ClientSession, types

client = OpenAI()   # 读环境变量 OPENAI_API_KEY

async def handle_sampling(context, params: types.CreateMessageRequestParams):
    # 客户端用自己的 Key 跑推理,返回结果给服务器
    resp = client.chat.completions.create(
        model="gpt-4.1-mini",
        max_tokens=params.max_tokens or 1000,
        messages=[m.model_dump() for m in params.messages],
    )
    return types.CreateMessageResult(
        role="assistant",
        content=types.TextContent(type="text", text=resp.choices[0].message.content),
        model="gpt-4.1-mini",
        stop_reason="endTurn",
    )
🔑 模型决定权在客户端:服务器给的 modelPreferences 只是"建议"(hints),客户端可忽略。响应里的 model 字段返回实际跑的模型名,服务器可据此感知差异。

Step 4:串起来跑通

4 初始化时必须声明 sampling 能力

客户端在 initialize 握手时,必须在 capabilities 里声明 sampling: {},否则服务器发 sampling/createMessage 会报错。用 stdio 拉起服务器并传入回调:

async with sdio_server.server() as (read, write):
    async with ClientSession(read, write, sampling_callback=handle_sampling) as session:
        await session.initialize()           # 握手声明 sampling 能力
        result = await session.call_tool("summarize", {"text_to_summarize": "一段很长的原文..."})
        print(result.content[0].text)

human-in-the-loop 是规范要求的闸:宿主在真正跑推理前应弹出审批,让用户允许或拒绝这次 sampling 请求。这既是协议 SHOULD 级要求,也是防止恶意服务器滥用的主要防线。

Step 5:给 Sampling 加结构与约束

5 systemPrompt / modelPreferences / includeContext

create_message 还支持系统提示、模型偏好和上下文范围。把一次需要的判断尽量放进同一条请求,减少来回推理轮次。

result = await ctx.session.create_message(
    messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
    max_tokens=512,
    system_prompt="你是安全审查员,只返回与鉴权、输入校验、数据暴露相关的发现。",
    model_preferences=types.ModelPreference(
        hints=[types.ModelHint(name="claude")],
        intelligence_priority=0.8,
        speed_priority=0.2,
        cost_priority=0.0,
    ),
    include_context="thisServer",
)
💡 includeContext 控制是否把 MCP 服务器上下文注入提示;多数场景用 nonethisServer。每次 sampling 都加至少一轮推理延迟,能批量问的就别拆成多次。

什么时候该用 Sampling

Sampling 适合"工具本身需要判断、而非确定性逻辑"的场景:给抓回的网页做分类、在构建工具里判断哪些编译错误值得处理、给研究工具把原始结果先总结、监控工具决定日志归哪类告警。它能把这些"判断"路由到宿主模型,而不必让服务器自带 LLM 客户端。

与 Elicitation 区分:Elicitation 是向"用户"要结构化输入;Sampling 是向"模型"要推理。两者都会打断确定性工具执行,但目的不同——一个要人的判断,一个要模型的判断。需要确定性结果时,直接写代码,别用 Sampling。

常见问题

现象原因与处理
报错 sampling 能力未声明initialize 的 capabilities 里漏了 sampling: {}
返回的不是文本检查 result.content.type,按需处理 image/audio
同一个工具结果不一致不同宿主模型能力不同,这是预期;用返回 model 名做适配
← 返回教程中心