Sampling原语:让Server反向请求LLM

📅 2026/8/14 15:18:10
Sampling原语:让Server反向请求LLM
摘要MCP Sampling原语让Server反向请求Client端的LLM能力实现服务端AI推理。本文详解采样请求流程、模型偏好设置、安全审批机制和典型应用场景。Sampling原语让Server反向请求LLM前段时间我做一个数据分析工具工具内部需要让大模型对查询结果做一轮摘要再返回。我一开始的方案是在Server里硬塞一个OpenAI的API Key直接调GPT。结果上线第二天安全团队找上门说Server里不该存API Key而且不同用户想用不同模型也没法满足。后来我改用MCP的Sampling原语Server不持有任何Key需要LLM时反向请求Client由Client用自己的模型能力生成结果。这篇我把这个逆向通信机制讲清楚。Sampling的逆向通信机制前面三篇讲的Tools、Resources、Prompts都是Server向Client暴露能力方向是Server到Client。Sampling正好反过来它是Client的能力Server在执行过程中可以请求Client帮忙调一次LLM。方向变成了Server请求Client。这个设计解决了一个核心矛盾。Server经常需要在工具执行中借助LLM做推理比如分析一段数据、生成一段摘要、判断一个分类。但Server不应该自己持有模型API Key那样既不安全又限制了用户选模型的自由。Sampling让Server说我需要一次LLM生成具体用哪个模型、用什么Key全由Client决定。协议上这走的是sampling/createMessage这个JSON-RPC方法由Server发起请求。请求里带上messages对话内容、modelPreferences模型偏好、systemPrompt系统提示、maxTokens等参数。Client收到后调自己的LLM把生成结果返回给Server。整个流程有个重要的安全设计规范要求必须有人类在环。Client在调LLM前应该让用户审查和编辑请求生成结果也要给用户过目再返回。我实际用下来这个审查环节在开发阶段特别有用能直接看到Server到底给LLM发了什么。Server如何请求Client的LLM能力在FastMCP里Server端通过Context对象的sample方法发起请求。你在工具函数里拿到ctx调ctx.sample()框架自动帮你构造sampling/createMessage请求发给Client。下面是协议层面的请求结构。{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}}modelPreferences是Sampling的精华设计。Server不能直接指定模型名因为Client未必有那个模型。于是Server用hints给提示用三个优先级表达需求。costPriority越高越想省钱speedPriority越高越想要快intelligencePriority越高越想要强。Client综合这些偏好从自己可用的模型里挑一个合适的。比如Server说我想要claude-3-sonnet这个级别的速度优先Client如果只有Gemini可以映射到一个速度快的Gemini模型。hints是子串匹配多个hint按优先级排序Client尽量满足。安全考量Sampling的安全模型围绕控制权在Client展开。Server全程不接触API Key不接触模型选择甚至连最终返回什么内容都是Client审查后决定的。我总结了几条安全要点。第一Client必须实现用户审批Server发来的sampling请求要先给用户看用户同意才转发给LLM。第二用户可以编辑请求内容防止Server通过精心构造的prompt做坏事。第三生成结果也要审查避免Server拿到不该拿的信息。第四Client应该做限流防止Server疯狂发sampling请求烧token。我踩过一个真实的坑。有个Server工具在循环里调ctx.sample()每轮都生成一段分析循环没设退出条件token哗哗地烧。后来我在Client端的handler里加了调用计数超过5次直接拒绝问题才控制住。Server端的循环一定要有明确的终止条件。完整代码下面是完整示例。Server提供一个数据分析工具内部用ctx.sample()请求Client的LLM做摘要。Client端配置一个自定义sampling_handler为了不依赖真实API Key我用了一个mock handler返回模拟结果真实场景换成OpenAI等内置handler即可。server.py# server.py MCP Sampling原语完整示例# 运行方式 python server.py# 依赖安装 pip install fastmcpfromfastmcpimportFastMCP,Context# 创建服务器实例mcpFastMCP(nameSamplingDemoServer)mcp.toolasyncdefanalyze_and_summarize(data:str,ctx:Context)-str:分析数据并用客户端的LLM生成摘要. 这个工具先做本地处理, 再请求Client的LLM做摘要, 整个过程Server不持有任何模型API Key. # 第一步, 本地预处理, 统计数据基本特征linesdata.strip().split(\n)local_statsf共{len(lines)}行数据# 第二步, 通过ctx.sample请求Client的LLM生成摘要# 这里Server只表达需求, 具体用哪个模型由Client决定resultawaitctx.sample(messagesf请用一句话总结以下数据的核心趋势.\n\n{data},system_prompt你是一个简洁的数据分析助手, 只输出结论.,max_tokens80,# hints告诉Client倾向用什么级别的模型model_preferences[claude-3-sonnet,gpt-4o],)# result.text是LLM生成的文本summaryresult.textor摘要生成失败# 组合本地统计和LLM摘要一起返回returnf本地统计{local_stats}\nLLM摘要{summary}mcp.toolasyncdefclassify_text(text:str,ctx:Context)-str:用客户端LLM对文本做情感分类. 演示Sampling在分类场景的应用, Server定义分类规则, LLM负责判断. resultawaitctx.sample(messages(f判断以下文本的情感倾向, 只回复 正面/负面/中性 三个词之一.\n\nf文本{text}),system_prompt你是一个情感分析器, 严格只输出一个分类标签.,max_tokens10,# 分类任务速度优先, 不需要最强模型model_preferences[gpt-4o-mini,claude-3-haiku],)returnresult.textor分类失败if__name____main__:mcp.run()client_test.py# client_test.py 带sampling_handler的客户端测试# 运行方式 python client_test.py# 这个脚本连接server.py, 并提供一个mock的sampling_handlerimportasynciofromfastmcpimportClientfromfastmcp.client.samplingimportSamplingMessage,SamplingParams,RequestContext# 自定义sampling_handler, 处理Server发来的LLM生成请求# 真实场景替换成OpenAI或Anthropic的内置handlerasyncdefmock_sampling_handler(messages:list[SamplingMessage],params:SamplingParams,context:RequestContext,)-str:模拟LLM生成的handler, 不依赖真实API Key. 真实项目中用内置handler替代, 例如 from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler sampling_handlerOpenAISamplingHandler(default_modelgpt-4o) 这里为了演示可独立运行, 返回模拟文本. # 调用计数, 防止Server死循环烧请求# 生产环境也应该做限流countgetattr(mock_sampling_handler,_count,0)1mock_sampling_handler._countcountifcount10:raiseRuntimeError(sampling调用次数超限, 疑似死循环)# 提取最后一条用户消息的文本last_msgmessages[-1]# SamplingMessage的content可能是TextContent, 有text属性user_textlast_msg.content.textifhasattr(last_msg.content,text)elsestr(last_msg.content)# 根据system_prompt决定返回什么, 模拟不同LLM行为systemparams.systemPromptorif情感insystemor分类inuser_text:return正面# 默认返回一个模拟摘要returnf[模拟摘要] 该数据呈现稳定上升趋势, 共涉及{len(user_text)}个字符.asyncdefmain():# 创建带sampling_handler的Client# Client会自动声明sampling能力, Server就能发createMessage请求了asyncwithClient(server.py,sampling_handlermock_sampling_handler,)asclient:# 测试数据分析工具, 它内部会触发samplingprint( 调用 analyze_and_summarize )resultawaitclient.call_tool(analyze_and_summarize,{data:1月销量100\n2月销量120\n3月销量150\n4月销量180},)print(f 结果{result.structured_content})print()# 测试分类工具, 同样内部触发samplingprint( 调用 classify_text )resultawaitclient.call_tool(classify_text,{text:今天天气真好, 心情特别愉快!},)print(f 分类{result.structured_content})if__name____main__:asyncio.run(main())效果验证装好fastmcp后跑client_test.py。因为用了mock handler不需要任何API Key就能看到完整流程。输出大致如下。 调用 analyze_and_summarize 结果 {本地统计: 共 4 行数据, LLM摘要: [模拟摘要] 该数据呈现稳定上升趋势, 共涉及38个字符.} 调用 classify_text 分类 正面可以看到Server的工具在执行中调用了Client的LLM能力Client的handler处理后把结果返回给ServerServer再组合成最终结果。整个过程中Server没接触任何模型API Key。真实场景下把mock_sampling_handler换成内置的OpenAISamplingHandler或AnthropicSamplingHandler就行。装好对应扩展后一行代码切换。Sampling与Tools的对比Sampling和Tools经常被放一起讨论因为它们都涉及调用但方向完全相反。我做了个对比。维度SamplingTools通信方向Server请求ClientClient请求Server谁发起Server在工具执行中发起Client/模型发起能力归属Client的LLM能力Server的函数能力API KeyServer不需要, Client持有Server自己执行逻辑典型场景Server需要AI推理时借力模型需要执行外部操作控制方Client控制模型选择和审批Server控制工具逻辑简单记Tools是模型要干活问Server要工具Sampling是Server要思考问Client借大脑。两者经常配合使用Server工具内部用Sampling做推理推理结果再返回给模型。我做过一个最典型的组合场景。一个数据分析Server工具先查数据库拿到原始数据再用Sampling让Client的LLM分析趋势最后把分析结果返回给用户。Server只负责数据获取AI推理交给Client职责分得清清楚楚。常见问题与避坑坑1循环里调sample导致token爆炸。Server工具在while循环里反复ctx.sample()没设退出条件每轮都烧token。我亲历过一晚上烧了几十刀。Server端循环必须有明确终止条件Client端handler也要加调用计数限流。坑2客户端没声明sampling能力直接报错。Sampling是可选的Client能力Client没配sampling_handler时Server调ctx.sample()会失败。用sampling_handler_behaviorfallback配一个兜底handlerClient不支持时自动走自己的LLM。坑3modelPreferences的hints写得太具体匹配不到。hints是子串匹配写一个完整版本号可能Client没有完全一致的模型。写模型族名比如claude-3-sonnet比写claude-3-sonnet-20240229更容易匹配到Client会映射到同级别的模型。坑4忽略了人类在环的审批延迟。规范要求Client在调LLM前让用户审批这会引入延迟。如果你的工具对延迟敏感在prompt里把审批必要的信息写清楚让用户快速判断要不要批准。别在时序要求极高的场景盲目用Sampling。坑5把敏感数据塞进sampling请求。Server把数据库里的用户隐私数据原样发给Client的LLM可能违反数据合规要求。发送前做脱敏处理或者只发聚合统计结果让LLM分析趋势别发原始明细。小结Sampling原语实现了MCP里唯一的逆向通信Server在执行中请求Client的LLM能力。核心要点有三个通信方向是Server请求ClientmodelPreferences用hints和优先级表达模型偏好不强制指定安全模型把控制权完全交给Client包括模型选择和人类审批。它和Tools形成对偶关系Tools是Client借Server的能力Sampling是Server借Client的大脑。下一篇我们看最后一个原语Elicitation它让Server能在执行中向用户要输入。相关推荐Prompts原语标准化提示词模板Elicitation原语Server向Client请求用户输入MCP协议全景Host、Client、Server架构详解