我最近在一间不算大的实验室里做了一件事:把一台倒置荧光显微镜的 MCP server 写了出来,然后让 Claude 通过这个 server 自动完成“移动载物台、换物镜、对焦、采图”这一套动作。听着像是科幻,但真正跑起来后我发现,问题根本不在模型能不能理解“对焦”这件事,而在于硬件世界和软件世界对“调用”的定义完全不一样。
这篇文章想聊的正是这层差异:Anthropic 把 MCP 从软件工具一路推进到了物理设备,MCP 生态开始长出一种新的“模型硬件标准”(Model Hardware Standard,简称 MHS)。我先说明一下,MHS 并不是凭空发明的一套新协议,更像是 MCP 延伸到物理世界时,大家发现必须补上的那套语义契约。下面会结合显微镜、机械臂和量子激光实验台,把 MHS 的底层逻辑拆开讲清楚。
1. 我第一次让模型控制显微镜翻车后,才理解 MHS 的出现
1.1 表面上是接口问题,实际上是语义鸿沟
最早我的想法很简单:显微镜厂商提供了 Python SDK,那我不就是把 SDK 里的几个函数包成 MCP tools 吗?这在软件侧明明是已经被验证过无数次的做法——Git 操作可以包成 MCP,浏览器可以包成 MCP,Figma 也能通过 MCP 被 Claude 调用。显微镜无非就是把move_stage(x=100, y=100)变成一个 JSON-RPC 调用而已。
结果第一轮实测就翻车了。模型很自然地调用了move_stage,返回也是{"ok": true},但实际载物台纹丝不动。排查半天才发现,这台显微镜重启之后必须先做原点回归,否则驱动器会拒绝所有绝对定位指令。SDK 函数没有报错,只返回了一个“预期会被忽略”的状态码,模型当然不知道发生了什么。
这个故事特别典型:软件 API 里的“调用成功”通常意味着操作已经发生,但物理设备里的“调用成功”只代表指令被设备接收了,不一定代表动作真正完成了,更不代表设备已经进入你预期的状态。这是模型控制物理世界时遇到的第一道坎,而 MCP 原生协议并没有专门定义这些语义。
1.2 MHS 不是 MCP 的替代品,而是一层物理语义标准
如果把 MCP 比作快递物流的标准包装箱,不管里面装的是什么,只要用标准箱封装就能在物流网络中流转。MCP 定义了工具如何被描述、参数如何传递、结果如何返回,但它不关心这个工具背后是读数据库,还是驱动一个大功率激光器。对于软件工具来说这没问题,因为软件函数大多无状态、副作用可控、可重复执行,出错最多就是报个异常。
物理设备就不一样了。设备有状态,动作有惯性,连续执行会产生互相干扰,更别说还有安全边界。你没办法把“打开激光器”当成一个普通函数反复重试,也不能在设备还没停稳时就把下一个动作发进去。MHS 做的事,就是把这些物理语义补进了 MCP 的工具描述、资源定义和调用流程里,让模型看得懂设备状态、知道哪些动作有风险、明白动作之后需要等待什么反馈。一句话总结:MCP 解决“模型怎么调用工具”,MHS 解决“模型怎么安全地与物理世界打交道”。
理解了这一点再去看 Anthropic 推动的 MCP 生态,会发现它其实不是要取代硬件控制层,而是想在模型和硬件控制层之间放一个统一、可解释、可审计的接口。设备本身的 PID 控制环、电机伺服、实时逻辑,都不归 MHS 管;MHS 管的是更上层的行为编排和状态仲裁。
2. MHS 到底要约定什么:设备说明书、动作字段、实时状态
过去几个月我看了不少团队为 MHS 写的草案,也自己设计过一套。不同草案细节差别很大,但共识出奇一致:模型硬件标准需要同时覆盖三个东西——能力描述、动作定义、状态通道。
2.1 把“设备说明书”变成模型能读的资源
以前我们给 Claude 写 MCP server,通常只需要在 tool description 里写几句话,比如“移动载物台到指定位置,单位是微米”。对软件工具来说这种描述可能够了,但硬件设备的信息密度远高于此。
一个实际显微镜的 MHS capability 描述通常长这样:
{ "mhs_version": 1, "device_id": "scope-01", "device_class": "inverted_microscope", "axes": [ { "name": "x_stage", "unit": "um", "min": -12500, "max": 12500, "soft_limit": 12000, "max_speed": 8000, "homing_required": true } ], "objectives": [10, 20, 40, 100], "laser": { "wavelengths_nm": [405, 488, 561], "max_power_mw": 50, "interlock_required": true }, "actions": [ "home", "move_stage_absolute", "switch_objective", "autofocus", "acquire_image", "run_laser_exposure" ] }这串 JSON 不是为了给前端展示的,而是放在 MCP resourcemhs://device/capabilities下面,让模型在开始操作前先读一遍。它相当于把一份设备说明书的关键页直接送进模型的上下文,里面包含坐标范围、单位、安全限位、是否需要回归原点这类直接影响决策的信息。
我在实现时会把这份能力描述缓存在 server 端,并按设备状态动态更新。比如显微镜当前处于BUSY状态时,描述里会明确标记available_actions: [],模型读到后就不会继续发指令。这个细节很重要:让模型直接从能力描述里“看到”当前可执行动作,比让它从几十轮历史对话里倒推状态可靠得多。
2.2 给动作补上四类物理字段
MCP 对工具的描述其实已经很标准了:name、description、input schema。但物理设备需要一个动作在“能不能做、做多久、做了之后能不能撤销”这些维度上给出明确标记。我自己归纳了四类必须补充的字段:
| 字段 | 含义 | 例子 |
|---|---|---|
precondition | 执行前必须成立的条件 | must_be_homed、chamber_closed |
duration_ms | 动作预计持续时长 | 500、3000 |
reversible | 是否可逆 | false、true |
require_operator | 是否需要人类确认 | false、true |
比如“开启激光曝光”这个动作,MHS 描述里必须写明precondition: light_curtain_closed、reversible: false、require_operator: true。模型不是真能理解这些字段背后的安全意义,但这些字段会约束它能构建出的操作序列。
另一个容易被忽略的字段是idempotency_key。软件 API 可以容忍重复调用,但物理动作不一定。比如“关闭激光快门”重试两次问题不大,“移动载物台”如果因为网络超时被重复提交,载物台就可能多跑一段距离,轻则错过目标视场,重则撞坏样本。所以 MHS server 通常会要求客户端每次请求带一个唯一动作 ID,server 端只执行一次,后续相同 ID 的请求直接返回已经执行过的结果。这种设计思路是从支付系统学来的,在硬件控制里反而比在软件 API 里更重要。
2.3 遥测与事件:把状态从返回值升级成资源
普通 MCP 工具调用是“你问我答”:调一次函数,拿到一个结果。物理世界不是这样,一个动作可能持续几百毫秒,设备状态在动作中还会发生变化。如果模型只能靠工具返回值猜状态,那等于让一个近视眼通过拍照片判断跑步运动员的实时位置。
MHS 的解决办法是把状态流也变成 MCP resource 和 notification。设备会维护一个mhs://device/state资源,当前处于什么状态、XYZ 坐标是多少、激光器是否 ready,全部实时刷新。动作结束后 server 不是简单地返回一串文本,而是主动发一个状态变更通知,比如:
{ "event": "state_changed", "device_id": "scope-01", "state": "IDLE", "position_um": {"x": 120.5, "y": 880.0, "z": 0.0}, "timestamp": "2025-06-11T10:24:31Z" }这样的好处有两个。第一,模型收到通知后知道“动作现在真正完成了”;第二,状态以资源形式存在,模型随时可以重新读取,不需要靠纯文本日志推断。显微镜实验中,模型下一步该做什么,往往取决于当前载物台是否已经稳定、焦点质量分数是否够高,这些信息必须从状态通道拿,而不是从聊天记录里翻。
3. 显微镜、机械臂、量子激光:三种设备的接入方式完全不同
MHS 给了统一语义,但不同设备对实时性的要求、对安全模型的依赖、对控制粒度的需求差异极大。我建议不要把 MHS 理解成一个万能抽象层,它更像一套公共词汇表,不同设备在这套词汇表里填不同的内容。
3.1 自动调焦显微镜:动作慢,状态要等
显微镜看起来是三个场景里最温和的,但它特别适合用来验证 MHS 的基本流程。它的动作时间尺度是百毫秒到秒级,正好落在模型推理的节奏里:模型发一个指令,等设备响应,通过状态变化拿到新状态,然后再决定下一步。
我第一次做自动对焦时,第一版方案是让模型自己不断调用read_focus_score、move_z,用类似二分法的方式找焦点。模型确实能写出正确的二分逻辑,但每轮推理都要等设备响应,整套流程拖得很慢。后来我把“自动对焦”这个动作升级成了 MHS server 端的高层工具:模型只需要调autofocus,server 内部执行对焦算法,完成后把焦点位置和置信度返回给模型。这个调整让整条链路快了好几倍。
这其实是 MHS 设计里很反直觉的一点:硬件标准不是让模型掌握所有低层控制,而是让模型尽量使用高层、安全、经过验证的设备动作。模型的任务是看懂实验目标、选择正确的工作流、处理异常;电机怎么转、对焦算法怎么收敛,这些应该留在设备端的控制器里。
3.2 机械臂作业:坐标、原点、互斥一个都不能少
机械臂比显微镜复杂在两点:运动学和并发。机械臂的坐标系通常有多个:基座坐标系、末端夹具坐标系、视觉标定坐标系。模型如果不懂这些坐标系,很容易计算出某个目标位置但在错误的坐标系里执行。MHS 的能力描述里必须明确写上当前动作使用的坐标系、单位、以及是否需要先做归零校准。
我在仿真环境里测过一组很经典的抓取任务:让模型把一个方块从位置 A 搬到位置 B。模型生成的计划是合理的——先移动到 A 上方、下降、夹爪闭合、抬起、移动到 B、放下。问题出在动作之间的“状态互斥”:夹爪没有完全闭合时,机械臂不能立刻抬起,否则物体会掉。真实机器人控制器通常有内部状态机保证这一点,但模型侧的 MHS server 不能假设模型清楚这些状态转换规则。
所以机械臂的 MHS 适配器里,我还会额外加一层互斥锁逻辑:同一时间只允许一个动作在执行队列里,前一个动作未收到done通知,后一个动作直接返回device_busy。这些规则和模型是否聪明无关,纯粹是物理世界的基本约束:机器不能同时执行两个需要同一关节的动作。
3.3 量子激光实验台:模型只做编排,实时控制仍归底层
听起来最“科幻”的量子激光场景,反而最能说明 MHS 的边界。量子光学实验里确实有一些激光器、衰减器、移相器和单光子探测器,它们之间有时序要求,某些延迟要到纳秒甚至皮秒量级。这种控制不可能经过一个跑在普通服务器上的 MCP tool 再回传,一个大语言模型的推理延迟足够让实验失败一万次。
所以真正合理的架构是分层控制。底层用 FPGA 或者专门的时序控制器完成高精度闭环,上层实验编排系统负责“什么时候开始、参数怎么组合、数据怎么落盘”。MHS 和模型只接在上层编排系统上。Claude 能做的事情是读取实验配置、决定要不要切换功率档位、在预设的操作序列里选一组执行,而不是直接去给激光器发实时控制信号。
连接量子激光实验台时,MHS server 的核心价值是安全边界和流程审计。比如“把激光功率从 1mW 调整到 3mW”这个动作,必须在硬件层面上经过安全互锁确认,由独立于模型的安全守卫检查光路是否封好、操作者是否在安全区域、功率设定值是否在允许范围内。模型可以提出请求,但真正执行按钮必须被一套固定规则锁住。MHS 在这里更像一个“权限代理”,而不是智能决策核心。
4. 一个最小 MHS 适配器怎么搭:从设备类到 MCP Server
如果你手上也有一台可以通过 Python SDK 控制的设备,我建议按下面这个顺序搭最小适配器。我以显微镜为例,但流程对所有设备通用。
4.1 先写设备驱动类,再暴露成 MCP
最容易犯的错是直接拿厂商 SDK 的函数名去开一个@mcp.tool()。厂商 SDK 是为写桌面软件的人设计的,变量名、单位、错误处理都不是为模型准备的。
正确顺序是先写一个干净的 Python 设备类,内部统一单位,做状态管理:
class MicroscopeDevice: def __init__(self): self.position_um = {"x": 0.0, "y": 0.0, "z": 0.0} self.busy = False self.homed = False self.objective = 10 self.laser_enabled = False self.laser_power_mw = 0.0 def home(self): self.busy = True # 调用厂商SDK执行归零 self.position_um = {"x": 0.0, "y": 0.0, "z": 0.0} self.homed = True self.busy = False return self.get_state() def move_stage_absolute(self, x_um: float, y_um: float): if not self.homed: return {"ok": False, "reason": "must_home_first"} if not (-12000 <= x_um <= 12000 and -12000 <= y_um <= 12000): return {"ok": False, "reason": "target_out_of_soft_limit"} # 调用厂商SDK执行运动 self.position_um["x"] = x_um self.position_um["y"] = y_um return self.get_state()这个类的好处是让你在一个没有 MCP 干扰的环境里先验证设备的真实行为。你可以在命令行里反复测试move_stage_absolute,确认边界条件,然后再考虑怎么把动作暴露给模型。
4.2 capability 资源和动作实现
设备类写好后,用 FastMCP 包一层就很直接:
from fastmcp import FastMCP mcp = FastMCP("microscope-mhs") device = MicroscopeDevice() @mcp.resource("mhs://device/capabilities") def capability() -> str: return json.dumps(CAPABILITY_JSON, ensure_ascii=False) @mcp.resource("mhs://device/state") def state() -> str: return json.dumps(device.get_state(), ensure_ascii=False) @mcp.tool() def home_and_wait() -> dict: result = device.home() return result @mcp.tool() def move_stage_absolute(x_um: float, y_um: float) -> dict: result = device.move_stage_absolute(x_um, y_um) return result这里的CAPABILITY_JSON就是前面那段设备说明书。第一版不需要实现完整 MHS 标准,只需要做到三件事:模型能读能力描述、模型能读实时状态、每个动作执行后返回完整状态。设置好之后,用 MCP 客户端连上去,Claude 就能根据当前状态决定该调哪个动作。
4.3 本地安全守卫必须独立于模型
在 MHS 适配器里我认为最不能省略的部分是一个独立于模型调用的安全守卫函数。它的作用是执行硬校验,不接收任何模型改写后的逻辑。用代码表达就是:
class LaserGuard: HARD_MAX_POWER_MW = 20.0 @staticmethod def can_enable(target_power_mw: float) -> bool: return target_power_mw <= LaserGuard.HARD_MAX_POWER_MW @mcp.tool() def enable_laser(power_mw: float) -> dict: if not LaserGuard.can_enable(power_mw): return {"ok": False, "reason": "request_exceeds_hard_limit"} # 再执行真实硬件控制 ...千万不要把这个校验逻辑放在提示词或者工具描述里让模型“自觉遵守”。大模型可能因为上下文太长而忽略一条限制,也可能会在极端情况下生成绕过限制的参数。唯一可靠的做法是在设备驱动之前加一层由普通代码实现的硬校验,最好连设备厂商 SDK 本身也再有一道独立物理互锁。安全层设计的原则永远是:模型犯错也不至于造成事故。
5. 实测中反复遇到的五个坑,每个都值得写成排错日志
5.1 单位缺失让模型把微米当毫米
显微镜载物台的运动距离通常用微米,但模型在对话中看到细胞尺寸、样本容器这些信息时,很容易自行脑补毫米单位。第一次实测我让模型把载物台从当前视野向右移动“大约两个细胞直径”,结果模型直接传了一个 2000 的值,看起来像微米,其实它想表达的是 2000 微米,但它没有意识到这个范围已经接近载物台行程上限。
这里不能指望模型“聪明”到会自动换算,必须靠 MHS 字段把单位写死。更保险的做法是:server 端只接受x_um这种自带单位的参数名,并且把范围min/max明确暴露给模型。如果参数超范围,直接返回错误并附带当前合法范围,模型通常看到错误信息后会自行修正。我们后来在所有动作参数里都加了unit字段,这种错误基本绝迹。
5.2 动作返回后设备还在运动,模型以为万事大吉
很多硬件 SDK 的动作调用都是“提交后立刻返回”,真实运动在后台持续几百毫秒。第一版 server 在move_stage_absolute里直接返回了 SDK 的提交结果,模型立刻发起下一个采图动作,结果图拍下来时载物台还没到位。这个问题的修复不是让动作函数强制 sleep,而是让设备类维护一个motion_done事件,等驱动器确认到位后再返回状态。
在 MHS 层面,这意味着每个耗时动作必须设置明确的duration_ms和完成事件。模型不需要知道硬件底层怎么判断到位,但 server 返回给模型的状态必须保证是动作完成后的真实状态,而不是动作提交后的中间态。
5.3 “返回 ok”不等于“动作有效”
这个坑在显微镜原点回归那次也出现过。SDK 函数返回了一个非零状态码,但我当时没有解析,直接把它当成成功处理。教训是:硬件厂商 SDK 的返回值往往包含警告位、错误码、校准状态,不能只判断函数没有抛异常。
我后来给 MHS server 加了一个强制约定:所有动作返回值必须是结构化状态,而不是一串自然语言“success”。如果设备还有未处理的报警,状态字段必须带上alarms数组。模型只有看到完整的结构状态,才能发现“位置变了但是激光器温度异常”这类跨模块问题。
5.4 图像和点云塞进工具返回值会撑爆上下文
显微镜采图后,如果直接把 base64 图片塞进 MCP tool 的返回文本,会让上下文变得非常大,也会增加模型解析的负担。MHS 场景里,二进制数据应该走资源通道,而不是工具返回通道。比如acquire_image动作只返回frame_id,真正的图像放在mhs://device/frames/{frame_id}这个 MCP 资源里,模型需要时再按 URI 读取。
在需要视觉判断的任务里,我会再配合 MCP 的图片内容块把压缩后的预览图传给模型,让模型可以“看”到当前视野,同时保留原始分辨率的 frame_id 供后续算法处理。这个分离在软件 MCP server 里很少见,但在物理设备场景里几乎不可避免——显微镜一帧 2000 万像素的原始数据很容易到几十 MB,不可能作为普通 tool 返回值。
5.5 重复请求是比超时更隐蔽的危险
网络请求可能超时,客户端为了恢复可能会重试同一个动作。对于读操作毫无问题,但对于写操作就可能是灾难。MHS 的解决方式是强制每个动作请求携带action_id,server 端在收到重复 ID 时不重新执行,而是把第一次执行的结果原样返回。这要求设备动作都必须有幂等语义。
我一开始嫌这个字段多余,直到有次模拟弱网环境测试,载物台因为同一个移动指令被重发了三次,硬生生往一个方向多跑了三倍距离。那次之后我把idempotency_key当成了所有 MHS 动作的必填项,就像数据库写操作必须处理重复提交一样。
6. MHS 往前走的几个方向,以及我现在会怎么选型
6.1 设备目录与统一网关会是第一个成熟层
现在每个团队接一台设备就写一个 MCP server,动作命名五花八门。有的叫moveTo,有的叫move_to_abs,这种碎片化无法支撑大规模物理 AI 应用。我判断 MHS 下一步会先把“能力描述格式”和“设备目录协议”固定下来,类似 USB 的设备描述符:设备插入后,主机能自动识别它是什么、支持哪些能力。
放在 MHS 场景里,就是每台设备在接入网络时把自己注册到一个设备网关,网关能读取 capability、聚合状态、统一鉴权。模型不需要关心后台是一台显微镜还是一台机械臂,只需要问网关“当前可用的物理资源有哪些”,网关返回一套标准化能力清单。这个抽象对实验室自动化特别有价值:同一个实验流程可以在不同设备组合上跑,只要它们都符合 MHS。
6.2 物理动作评测会成为训练和验收的硬指标
软件 MCP tool 做评测可以靠单元测试和端到端断言,物理设备动作却很难评判“这次执行得好不好”。同样是抓取一个零件,机械臂可能碰到了边缘但最终还是成功了,也可能表面成功了但力控曲线异常。MHS 的结构化状态流天然可以产生大量时序数据:动作前状态、动作后状态、中间事件、报警记录。
这些数据回收以后,能直接用于构建物理 AI 数据工厂。大家现在都在提 data factory blueprint,本质就是让真实设备把每一次动作都自动变成带标签的训练样本。我现在会把action_id、状态变化、传感器读数、最终结果全部落盘,而不是只记录模型的文本输出。这些数据既是评测集,也是后续调优的原料。
6.3 如果团队准备尝试 MHS,我的建议是从最小闭环开始
不要一上来就设计一个覆盖所有设备的完整标准,那是标准组织做的事,不是做应用的人该做的事。先选一台低风险、可重复、状态容易观测的设备,比如一个桌面级的显微镜、一台小型的教学机械臂,把前面说的能力描述、状态通道、安全守卫跑通,让它连续稳定运行一整天不出问题,再考虑扩展到更复杂的设备。
我在实验室里最深的体会是:物理 AI 的难点从来不在模型不够聪明,而在于可靠的设备抽象、可信的状态反馈、可执行的安全边界。MHS 能不能统一成最终标准,现在没人能打包票,但把设备说明书变成模型能读的 JSON、把动作结果变成结构化状态、把安全校验交给硬代码,这些做法无论标准怎么演进都不会白做。等标准真正成熟的那天,你已经攒下了大量可迁移的适配经验,这才是比协议本身值钱的东西。