MCP Sampling:让 Server 反过来"请教" LLM

周五下午的监控大屏上,一条红色告警闪烁了三分钟。老李的文件管理 MCP Server 删了一个不该删的文件——那是一份被五个部门共同维护的接口规范文档,因为文件名匹配了“临时文件_*”的清理规则,在凌晨的自动任务中被移除了。

恢复花了运维半小时。老李把保温杯往桌上一顿,火气直冒:“这 Server 是瞎了吗?删之前就不能动动脑子想想这东西重不重要?”

小王端着咖啡走过来,看了眼日志:“老李,Server 确实没脑子。你给它写的规则是‘匹配 临时文件_* 就删’,它看到匹配了,就删了。它不会想。”

“那能不能让它想一下?”

“能。MCP 有个功能叫 Sampling——让 Server 在关键时刻反过来请教 LLM。”

老李一愣:“Server 请教 LLM?不都是 LLM 调用 Server 吗?”

“老李你想啊,你家扫地机器人撞到障碍物,它只是机械地换个方向。但如果它撞到的是一个在地上睡觉的猫,它是不是应该‘想一下’再决定怎么绕?Sampling 就是给 Server 装了个猫识别器——让它在不确定的时候,能停下来问一问更聪明的大脑。”


Sampling 是什么:Server 的“反向求助”

小王走到白板前,画了一个与以往相反的箭头。

“咱们之前聊的所有 MCP 通信,都是 Client 发起请求,Server 被动响应。Client 说‘给我查数据库’,Server 就查。但有些场景下,Server 遇到了它判断不了的事情——比如这个文件该不该删、这堆数据要不要汇总、这个错误该怎么恢复——它需要反过来问 LLM 的意见。”

“Sampling 就是 Server 通过 Client 向 LLM 发送请求的能力。注意,Server 不能直接调 LLM。它只能向 Client 发一个采样请求,Client 决定要不要转发给 LLM,LLM 生成结果后 Client 再把响应传回 Server。这一来一回,控制权始终在 Client 手里。”

图:Sampling 的逆调用流程——Server 发起,Client 控制,LLM 提供智能

“这个设计很精巧,”小王强调,“Server 没有 LLM 的 API key,不能绕开 Client 偷偷调模型。Client 是唯一的 LLM 访问通道,这保证了安全边界。”

老李若有所思:“那我那个文件清理 Server,可以在删之前让 LLM 判断一下?”


实战:让文件清理 Server “三思而后删”

“对。我们来给你那个清理规则加一层 Sampling 保护。”小王打开代码编辑器。

“原来你的清理逻辑是:匹配规则 → 直接删。现在改成:匹配规则 → 请求 Sampling → LLM 评估风险 → Server 根据评估决定删不删。”

typescript
// 文件清理 Server 中的 Sampling 调用
async function shouldDeleteFile(filePath: string, stats: FileStats) {
  // 构造一个请求 LLM 帮忙判断的提示
  const samplingRequest = await server.createMessage({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `我准备删除文件:${filePath}
该文件的信息:
- 创建时间:${stats.createdAt}
- 最后修改:${stats.modifiedAt}
- 文件大小:${stats.size}KB
- 最近7天被 ${stats.recentEditors.length} 个不同用户编辑过

请判断删除风险(高/中/低),并给出简短理由。
如果风险为高,请建议保留。`
      }
    }],
    maxTokens: 200  // 限制 LLM 回答长度,控制延迟
  });
  
  const llmResponse = samplingRequest.content.text;
  if (llmResponse.includes("高风险") || llmResponse.includes("建议保留")) {
    return false;  // LLM 认为不该删
  }
  return true;  // 低风险,正常清理
}

老李盯着代码:“这个 maxTokens: 200 是控制 LLM 回答长度的?”

“对。Sampling 请求也有成本——每次调 LLM 都要消耗 Token,而且有延迟。所以你要设置合理的 token 上限,让 LLM 只做快速判断,不要长篇大论。这个清理场景,200 个 token 足够 LLM 判断风险高低了。”

“那如果 LLM 判断错了呢?”老李追问。

“这正是 Sampling 设计中最重要的原则——Server 永远保留最终决定权。LLM 返回的是建议,不是命令。你的 Server 代码里可以再加一层规则:比如 LLM 说低风险,但你还可以检查文件是否被 git 跟踪、是否有最近的备份——多层判断,LLM 只是其中一层。”


三个典型的 Sampling 应用场景

小王在白板上写下三个场景,一一展开。

智能文本摘要:知识库 Server 接到一个大查询,返回了 30 页搜索结果。它可以把这些结果发给 LLM,请求生成一个 200 字的摘要,然后把这个摘要作为 Resource 返回给 Client。这样就避免了把 30 页全文全部塞进 AI 的上下文窗口。

typescript
// Server 用 Sampling 做大数据量摘要
const summaryRequest = await server.createMessage({
  messages: [{
    role: "user",
    content: {
      type: "text",
      text: `请将以下 ${searchResults.length} 条搜索结果总结为 200 字以内的摘要:
${searchResults.map(r => r.title + ": " + r.snippet).join("\n")}`
    }
  }],
  maxTokens: 300
});
const summary = summaryRequest.content.text;
return { type: "resource", text: summary }; // 返回摘要而非全文

动态工具生成:数据库 Server 接收到一个自然语言查询——“上周注册的新用户里,有多少完成了首次购买?”它不知道该怎么转换成 SQL。于是它请求 LLM 帮忙生成 SQL,拿到 SQL 后在自己的沙箱里执行,返回结果。

错误恢复建议:API 网关 Server 调用某个接口时收到了 500 错误和一堆乱码。它把错误日志发给 LLM,LLM 分析出“这看起来是数据库连接池耗尽,建议检查慢查询日志”。Server 把这个建议反馈给运维,而不是默默地重试。


Sampling 的权限与成本:不是所有 Server 都能随便“想”

老李突然想起一个问题:“那是不是所有 Server 都能随便调 LLM?万一有个恶意 Server 疯狂发 Sampling 请求,把 API 额度烧光怎么办?”

“这就是 Sampling 的权限控制。”小王在白板上画了一个审批流程。

“Client 在收到 Server 的 Sampling 请求时,可以选择拒绝。比如你可以在 Claude Desktop 里设置:‘文件操作 Server 的 Sampling 请求需要人工确认’——这样每次删文件前,不仅 LLM 会判断,还会弹个框让你亲自看一眼。另外,你还可以限制单个 Server 每分钟最多发多少次 Sampling 请求,防止失控。”

老李点点头:“那这样延迟不会很高吗?尤其是人工确认那一步。”

“所以 MCP 允许 Client 对 Sampling 请求做自动决策。对于低风险操作,可以自动放行;对于高风险操作(删除、修改权限),可以要求人工确认或直接拒绝。策略你自己定——这跟银行的交易风控一模一样:小额免密,大额人工审核。”

老李沉默了一会儿,盯着白板上那个逆向箭头看了很久。

“嗯……还有点意思。这样 Server 就不是一个只会执行命令的工具人了,它有了一个可以‘商量’的大脑。”

“老李,你这就是——”

“下不为例啊。”老李赶紧绷住脸,但表情已经松弛了,“对了,你刚才说 maxTokens 控制回答长度,那如果 LLM 的回答被截断了怎么办?Server 怎么处理不完整的建议?给我看看你这边的错误处理逻辑。”

小王笑着把代码补丁调出来。窗外夕阳正好,白板上那个从 Server 指向 LLM 的逆向箭头,像一条刚刚被打通的新神经通路,让那个曾经只会执行死规则的 Server,第一次学会了“在动手前想一想”。

真实世界的 MCP:把 Claude 接入你的数据库、API 和文件系统
MCP 安全指南:OAuth 舞蹈、权限迷宫和信任链