一个远程 MCP Server 提供“日志分析”工具。用户传入一段服务日志,工具需要先做本地统计,再让大模型写一段趋势摘要。Server 部署在别人的机器上,它拿不到用户公司的模型密钥,也不该拿到。把 OpenAI 或 Anthropic 的 API Key 硬编码进 Server 是最直接的做法,但密钥会随镜像分发、随日志泄露,用户想换模型时也无从选择。
MCP 的 Sampling 机制把这次模型调用反过来:Server 不调用模型,而是向客户端发一个 sampling/createMessage 请求,说清“我需要一次文本生成”,由客户端决定用哪个模型、是否让用户确认、最终返回什么。协议文档把这条路径描述为“服务端无需 API Key 即可借助 AI 能力”。本文以日志摘要这个场景贯穿,说明请求怎么构造、结果怎么回来、权限边界在哪、什么时候它会失效。
反方向的能力声明
MCP 里 Tools、Resources、Prompts 都是 Server 向 Client 暴露能力,方向是 Server 到 Client。Sampling 反过来,它是 Client 的能力,Server 在执行过程中可以请求 Client 帮忙调一次大模型。协议文档明确写着,这条流程让客户端保持对模型访问、模型选择和权限的控制。
能力声明是这条反向路径的前提。支持 Sampling 的客户端必须在初始化时声明 sampling 能力,请求里写成:
{
"capabilities": {
"sampling": {}
}
}
只有客户端声明了这项能力,Server 才应该发起采样请求。如果客户端没有声明,Server 的日志摘要工具就只能退回本地规则或直接报错,不能假设对方一定能调模型。
这里有一个容易忽略的版本事实。协议 2026-07-28 版本把 Sampling 标记为废弃(SEP-2577),按特性生命周期策略,它会在该修订发布后至少保留十二个月才可能被移除。文档同时写明:新实现不应再采用它,已有实现应迁移到直接集成各家大模型厂商 API。这意味着 Sampling 目前仍可运行,但选型时要把迁移成本算进去。
一次摘要请求的参数构成
Server 发起的是 JSON-RPC 方法 sampling/createMessage。以日志摘要为例,请求体大致如下:
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请用一句话总结以下日志的核心趋势。"
}
}
],
"modelPreferences": {
"hints": [{ "name": "claude-3-sonnet" }],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "你是一个简洁的日志分析助手。",
"maxTokens": 100
}
}
messages 是对话内容,角色和普通聊天一致。systemPrompt 给模型设定身份。maxTokens 限制生成长度,摘要任务通常不需要很大。modelPreferences 是这套机制里最需要理解的部分,下一节单独展开。
客户端返回的结果形如:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": { "type": "text", "text": "日志显示错误率在午后持续上升。" },
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}
结果里带 model 字段,告诉 Server 实际用了哪个模型。Server 可以据此记录“这次摘要由哪个模型生成”,便于审计和复现。stopReason 说明生成为什么结束,例如正常结束还是触发了长度上限。
模型偏好为什么不能直接指定模型名
Server 和 Client 可能用不同厂商的模型。Server 不能简单指定某个模型名,因为 Client 未必有那个模型,或者更愿意用别家的等价模型。协议因此设计了一套偏好系统,把抽象的能力优先级和可选的模型提示结合起来。
三个优先级取值在 0 到 1 之间。costPriority 越高越倾向便宜模型,speedPriority 越高越倾向低延迟模型,intelligencePriority 越高越倾向能力更强的模型。日志摘要需要理解长文本,intelligencePriority 可以调高;如果是给每条日志打一个固定标签,speedPriority 更重要。
hints 是模型名或模型族的子串提示,按优先级排序,Client 可以把它映射到不同厂商的等价模型。文档举的例子是:Client 没有 Claude 模型但有 Gemini,可以把 sonnet 提示映射到能力相近的 Gemini 模型。hints 只是建议,最终选型由 Client 决定。
请求与结果的完整流动
把日志摘要场景展开,一次工具调用内部的顺序是这样的。
flowchart TD
A[用户调用日志分析工具] --> B[Server 本地统计日志]
B --> C[Server 发 sampling/createMessage]
C --> D[Client 展示请求供用户确认]
D -->|用户拒绝| E[返回错误 Server 走降级路径]
D -->|用户同意| F[Client 按偏好选模型并调用]
F --> G[Client 审查生成结果]
G --> H[结果回传 Server]
H --> I[Server 组合统计与摘要返回用户]
关键转折点在 D 和 G 两处。D 是人工确认环节,用户可以在请求真正发往模型前拒绝或修改。G 是结果审查,客户端在把生成内容交回 Server 之前先过目。这两个环节决定了 Sampling 和“Server 自己调模型”在信任模型上的根本差别。
协议文档对人工在环的要求用的是 SHOULD:出于信任与安全,应该始终有人在环并能拒绝采样请求。应用应该提供便于审查请求的界面,允许用户在发送前查看和编辑提示词,并在交付前展示生成结果供审查。注意这是“应该”而非“必须”,实现可以自行决定交互形式,协议本身不强制某种用户交互模型。
权限边界与拒绝路径
Sampling 的安全模型围绕“控制权在 Client”展开。Server 全程不接触 API Key,不接触模型选择,连最终返回什么内容都由 Client 审查后决定。
拒绝路径是这条边界的具体体现。当用户拒绝采样请求,Client 返回一个 JSON-RPC 错误,例如:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -1,
"message": "User rejected sampling request"
}
}
Server 必须把“用户拒绝”当成一种正常结果来处理,而不是异常崩溃。日志摘要工具在收到拒绝后,可以只返回本地统计,并注明“摘要未生成”。如果 Server 把拒绝当成致命错误,整个工具调用就会失败,用户看到的是一个坏掉的工具,而不是一个降级但可用的结果。
协议还列出几条安全考量:Client 应该实现用户审批控制;双方都应该校验消息内容;Client 应该尊重模型偏好提示;Client 应该实现限流;双方必须妥善处理敏感数据。限流这条尤其重要,因为 Server 可以在一次工具执行里反复发起采样请求。
一个具体的失败模式是 Server 端循环调用采样而没有终止条件。工具在循环里每轮都请求一次生成,循环没有退出条件,就会持续消耗客户端的 token 配额。工程上通常的做法是在 Client 侧对单次工具调用内的采样次数计数,超过阈值直接拒绝,同时在 Server 侧给循环设置明确的终止条件。
与服务端自持 API Key 的对比
两种方案都能让 Server 拿到模型生成结果,差别在密钥归属、模型选择权和审计位置。
| 维度 | Sampling 反向调用 | 服务端自持 API Key |
|---|---|---|
| 密钥存放 | Server 不持有任何密钥 | 密钥存在 Server 侧,随部署分发 |
| 模型选择 | Client 按偏好和可用模型决定 | Server 代码写死,用户无法更换 |
| 用户确认 | 可在请求和结果两处介入 | 无介入点,用户看不到发往模型的提示 |
| 成本归属 | 计入 Client 的模型配额 | 计入 Server 运营方账单 |
| 限流控制 | Client 侧可拒绝和限流 | 依赖 Server 自身实现 |
| 部署复杂度 | 需要 Client 声明并实现 sampling | Server 独立可用,不依赖 Client 能力 |
| 审计位置 | 提示与结果都经过 Client | 日志留在 Server,用户难以核查 |
| 适用前提 | Client 支持 Sampling 且用户同意 | Server 能安全保管密钥 |
从这张表能看出取舍的核心。Sampling 把密钥风险和控制权都转移给了 Client,代价是 Server 失去了对模型和成本的直接掌控,并且强依赖 Client 的能力声明。自持密钥的 Server 独立性强、行为可预测,但把密钥保管责任和用户信任成本留给了自己。
对于面向多租户的远程 Server,Sampling 的收益更明显:它不必为每个用户配置密钥,也不必承担用户模型调用的账单。对于完全在受控环境内、Client 不支持 Sampling 的场景,自持密钥反而是唯一可行路径。
可观测指标与失效条件
要让这条链路在生产里可运维,需要观察几类信号。
采样请求量与拒绝率。 统计单位时间内 sampling/createMessage 的发起次数,以及其中被用户拒绝的比例。拒绝率突然升高,通常意味着 Server 的提示词让用户觉得可疑,或者请求里带了不该带的敏感内容。
模型映射结果。 结果里的 model 字段记录了实际使用的模型。如果 Server 请求的是高智能模型,Client 却长期映射到小模型,摘要质量会下降,需要和 Client 侧确认偏好映射逻辑。
停止原因分布。 stopReason 为长度上限的比例偏高,说明 maxTokens 设置过小,摘要被截断。
单次工具调用的采样次数。 这个指标能直接暴露循环失控。次数异常增长时,先检查 Server 侧循环的终止条件。
端到端延迟。 Sampling 比 Server 直接调模型多出人工确认和结果审查两个环节。如果确认界面是阻塞式的,用户不在电脑前时工具调用会一直挂起。工程上通常需要给确认环节设置超时,超时后按拒绝处理。
失效条件可以归纳成几条。Client 没有声明 sampling 能力时,请求不应发出。用户拒绝或超时未确认时,Server 必须走降级路径。Client 侧限流触发时,Server 会收到错误而非结果。协议版本进入废弃周期后,新实现不应再采用这条路径。
适用边界与迁移考量
Sampling 适合这样的场景:Server 需要模型能力但不应持有密钥,用户希望自己控制模型选择和成本,并且能接受人工确认带来的额外延迟。日志摘要、文本分类、结果润色这类“生成内容不直接触发副作用”的任务比较契合,因为即使结果被用户拒绝,工具也只是少了一段摘要。
反过来,如果工具的执行强依赖模型输出才能继续,比如模型生成的参数要直接写入数据库,人工确认环节就会成为阻塞点。这类场景要么把确认做成异步,要么重新评估是否该用 Sampling。
最后是版本层面的判断。协议文档已经把 Sampling 标记为废弃,建议新实现直接集成各家模型厂商 API。这不代表 Sampling 立刻不可用,它仍会在规范里保留至少十二个月。但对于现在就要选型的新项目,需要把“未来迁移到直连厂商 API”作为已知成本纳入设计,而不是把它当成一条长期稳定的路径。
一个折中做法是:把模型调用封装在 Server 内部的一个接口后面,当前通过 Sampling 实现,迁移时替换成直连厂商 API 的实现。这样工具逻辑不必改写,变的只是模型访问这一层。