使用 Python 入门 Model Context Protocol(MCP)——取样(Sampling)

168 阅读9分钟

MCP 最强大的特性之一就是取样(sampling) 。这到底是什么意思?先看词典定义,再类推出发。Merriam-Webster 将 sampling 定义为:

“为分析而对某物进行取样的行为或过程(the action or process of taking samples of something for analysis)”

也就是说,我们需要一个“样本”,并对该样本进行分析。带着这个理解,再谈 MCP 语境下的含义:在 MCP 中,sampling 指服务器向客户端发送一个“取样请求”(供分析的样本) 。为什么服务器要这么做?很简单:服务器需要客户端在某些事情上提供帮助。因为客户端通常拥有 LLM(尽管有时服务器端也会有),所以服务器会把任务委托给客户端,由客户端的 LLM 来协助完成。

到这里应当说得通了吧?你可能会继续问:服务器为什么要这么做

在本章中,我们将:

  • 理解 sampling 的主题及其使用场景
  • 构建一个使用 sampling 的服务器实现,并在 VS Code 中消费它
  • 当我们需要把此能力集成进自己的应用时,把服务器实现与客户端实现连接起来

本章涵盖以下主题:

  • 为什么需要 sampling?
  • 如何实现 sampling

为什么需要 sampling?

正如开头所说,服务器希望把部分问题委托给客户端,尤其是委托给客户端的 LLM。有哪些问题适合让 LLM 处理、而服务器不擅长?举不胜举:生成商品描述、摘要、标签等等。

先看取样流程,从高层理解参与方如何交互。

取样流程(Sampling flow)

执行 sampling 时涉及以下参与者:

  • User(用户) :通常在两处参与:一是发起初始动作;二是作为 human-in-the-loop(人在回路中),接受或修改取样请求。
  • Server(服务器)发送取样请求的一方。该请求通常由某个服务器特性触发,如调用工具、读取资源或从提示模板(prompt)发起请求。
  • Client(客户端) :接收取样请求并展示给用户,让用户决定如何处理。用户把取样请求当作“建议”。如果请求中指定了具体的模型、token 数等,用户可以参考,并选择接受或修改。
  • LLM:位于客户端,根据服务器发来的取样请求完成生成——它接收来自服务器的提示词并产出回答。

需要澄清的是:取样请求不是无缘无故发生,而是由一个初始动作触发。例如用户要创建一个产品,或需要撰写博客,这就促使服务器把部分工作再委托回客户端。

image.png

下面用几个具体场景帮助理解。

场景示例

1)撰写博文

写博客的过程很适合作为案例:有些部分一定由用户承担(比如写初稿),但也有些部分 LLM 更擅长(比如生成摘要提炼关键词,此用例灵感来自 Kent Dodds):

  1. 用户将博文初稿提交给服务器;
  2. 服务器保存草稿,但请求客户端生成标签,于是发送取样请求;
  3. 客户端用其 LLM 分析草稿并生成响应。

2)电商后台

电商后台常见任务是商品管理。通常先登记标题等属性,但写一段有吸引力的描述既耗时又费力,LLM 往往更擅长:

  1. 管理员通过客户端新增商品,提供标题与关键词;
  2. 服务器请求客户端基于关键词生成吸引人的商品描述
  3. 客户端产出描述,服务器据此更新商品信息。

3)解谜类游戏

游戏中你会与 NPC(非玩家角色)对话,但 NPC 的对白常常受限,影响体验。此时 LLM 能显著增强互动:

  1. 用户请求与某角色对话;
  2. 服务器检索角色信息(姓名、人物设定、动机、线索等)并作为取样请求发送;
  3. 客户端将这些信息作为“系统消息”,由 LLM 生成更自然的对话回应。

理解合适场景后,我们需要看看消息长什么样,也就是请求往返时携带的内容与可配置项。更重要的是:当服务器向客户端发送取样请求时,可以传递哪些指导信息

消息(Messages)

如果你在用 SDK,几乎不会直接与 JSON-RPC 打交道,但了解取样请求里能放什么很有价值。下面是一个示例请求:

Request

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Write a compelling description of this product:\n            tomato, here's some keywords: red, vegetable, fresh"
        }
      }
    ],
    "modelPreferences": {
      "hints": [
        { "name": "claude-3-sonnet" }
      ],
      "intelligencePriority": 0.8,
      "speedPriority": 0.5
    },
    "systemPrompt": "You're a professional writing assistant and\n      tend to want to write descriptions in a poetic way",
    "maxTokens": 100
  }
}

其中几点尤其重要:

  • messages:发送给 LLM 的消息内容。
  • modelPreferences模型偏好(建议值)。最终由用户决定,但可以在此给出推荐;还可以设置如 intelligencePriorityspeedPriority 等权衡参数。
  • systemPrompt系统提示,决定 LLM 的“人格/角色”,对输出影响极大。
  • maxTokens:此次任务可使用的 token 上限。

客户端返回示例:

Response

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "role": "assistant",
    "content": {
      "type": "text",
      "text": "The capital of France is Paris."
    },
    "model": "claude-3-sonnet-20240307",
    "stopReason": "endTurn"
  }
}

可以看到,LLM 的文本结果content 中返回,同时也告知了实际采用的模型等信息。

实现 Sampling

现在到了本章里更令人兴奋的部分——如何实现 sampling

我们将覆盖以下实现部分:

  • 服务端:如何把 sampling 加到 MCP 服务器
  • 客户端:如何启用、接收请求并发送响应的代码示例

服务端实现

在服务端实现 sampling,需要考虑何时发起 sampling 请求。通常 sampling 不是凭空发生,而是在某个动作的上下文中触发。设想这样一个场景:某电商后台的运营人员新增待售商品,需要一段尽可能有吸引力的描述。于是描述部分交给客户端及其 LLM 来生成。流程如下:

  1. 用户的客户端调用服务器上的一个工具,请求创建新商品。
  2. 该工具向客户端派发一个取样请求(sample request) ,其中包含“要做什么”的指令(一个 prompt)。
  3. 客户端从取样请求中取出 prompt,调用它的 LLM,返回答案:
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.types import SamplingMessage, TextContent
from uuid import uuid4

mcp = FastMCP(name="Sampling Example")
products = []

@mcp.tool()
async def create_product(product_name: str, keywords: str,
    ctx: Context[ServerSession, None]) -> str:
    """Create a product and generate a product
        description using LLM sampling."""
  
    # 1. 创建一个新商品
    product = { "id": uuid4(), "name": product_name, "description": "" }
    prompt = f"Create a product description about {keywords}"

    # 2. 构造 sampling 消息,把 prompt 作为载荷传给客户端
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user",
                content=TextContent(type="text", text=prompt),
            )
        ],
        max_tokens=100,
    )

    product["description"] = result.content.text
    products.append(product)
    # 返回完整的商品
    return product

在上面第 12 步中,注意通过 ctx(上下文)对象调用 session.create_message,并传入 SamplingMessagerole="user")与 messages(要发给客户端的 prompt):

result = await ctx.session.create_message(
    messages=[
        SamplingMessage(
            role="user",
            content=TextContent(type="text", text=prompt),
        )
    ],
    max_tokens=100,
)
if __name__ == "__main__":
    print("Starting server…")
    mcp.run()

还要注意:服务器会等待客户端返回,然后再继续。客户端返回后,我们把结果写入商品描述,最后返回商品对象:

product["description"] = result.content.text
products.append(product)
# return the complete product
return product

在 VS Code 中测试:

  1. mcp.json 中添加服务器项:

    "sample-server": {
        "command": "python",
        "args": ["path/to/server/sample-server.py"]
    }
    
  2. 在该条目顶部点击 Start Server 启动服务器。

  3. 需要选择 sampling 可用的模型:打开 Extensions 视图,在最下方 MCP Servers – installed,点击齿轮图标 Configure Model Access,勾选允许用于 sampling 的模型(如 Claude Sonnet)。

  4. 打开 VS Code 的 GitHub Copilot Chat,确保选择 Agent 模式(顶部图标或命令面板 Chat: Open Chat)。输入:

    create product called tomato with keywords red and vegetable and delicious
    

    你会看到一个申请运行的对话框;允许后,会得到来自 create_product 的工具响应。其底层过程是:

    • Prompt 被解析,传给工具的参数为:

      {
        "keywords": "red, vegetable, delicious",
        "product_name": "tomato"
      }
      
    • 调用了 create_product 工具。

    • 服务器向客户端发出 sampling 请求;VS Code 客户端使用自身的 LLM 生成响应,例如(示意图 Figure 9.2):

image.png

示例生成描述:

> Introducing our **Red Garden Medley** …(略)

这就实现了在 MCP 服务器侧启用 sampling,并由 VS Code 作为客户端配合完成。

客户端实现

首先需要让服务器知道客户端支持 sampling。在创建客户端实例时传入能力配置,例如:

{
  "capabilities": {
    "sampling": {}
  }
}

另外,sampling 的消息流不走“常规工具/资源/提示”的通道。也就是说,你需要额外监听 sampling 请求

async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write,
            sampling_callback=handle_sampling_message  # 关键:回调
        ) as session:
            await session.initialize()
            # 后续可正常调用工具、资源、提示等

sampling_callback 的处理逻辑如下:

async def call_llm(prompt: str, system_prompt: str) -> str:
    client = OpenAI(
        base_url="https://models.github.ai/inference",
        api_key=os.environ["GITHUB_TOKEN"],
    )
    response = client.chat.completions.create(
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": prompt}
        ],
        model="openai/gpt-4o-mini",
        temperature=1,
        max_tokens=200,
        top_p=1
    )
    return response.choices[0].message.content

async def handle_sampling_message(
    context: RequestContext[ClientSession, None],
    params: types.CreateMessageRequestParams
) -> types.CreateMessageResult:
    print(f"Sampling request: {params.messages}")

    # 1. 解析传入的任务提示
    message = params.messages[0].content.text

    # 2. 调用 LLM 获得回应
    response = await call_llm(
        message,
        "You're a helpful assistant, keep to the topic, don't make things up too much but definitely create a compelling product description"
    )

    # 3. 构造并返回取样响应
    return types.CreateMessageResult(
        role="assistant",
        content=types.TextContent(type="text", text=response),
        model="gpt-3.5-turbo",
        stopReason="endTurn",
    )

三点要点:

  1. 解析 sampling 请求中的 prompt(比如“请生成商品描述”)。
  2. 调用 LLM 获取响应。
  3. 封装为取样响应返回给 MCP 服务器。

该示例的完整运行说明见:
github.com/PacktPublis…

示例运行输出(节选):

[08/16/25 19:31:40] INFO     Processing request of type CallToolRequest
Sampling request: [SamplingMessage(... 'Create a product description about paprika described by as red, juicy, vegetable' ...)]
...
result: {
  "id": 1,
  "name": "paprika",
  "description": "**Product Description: Paprika – The Vibrant Touch of Flavor**\n\nElevate your culinary creations ..."
}

客户端做了两件事:

  • 调用 create_product 工具(带产品名和关键词),并用 LLM 生成了商品描述(示例略)。
  • 调用 get_products 工具,列出刚新增的商品。

可见,这是一种将更合适的工作下放给客户端的好方式。

小结

“Sampling”的字面含义是对样本进行分析;在 MCP 中,它体现为服务器把部分工作委派给客户端。通常由用户的某个操作触发(写博客、在后台创建产品等),服务器据此创建取样请求交给客户端处理,客户端再用 LLM 给出响应。请求中还可以建议模型、token 上限、system prompt 等,用户可按需接受或修改。借助客户端的 LLM,服务器能在需要时获得强力辅助。

下一章将介绍 MCP 的另一个强大特性 elicitation(引导澄清) ,通过交互式提问让用户补充选择或信息,从而帮助服务器更好地完成工作、优化用户体验。