拿到板子回家第一件事我不是去点亮 LED而是先问了自己一个很没出息的问题当我把一个 GPIO 引脚的状态通过 MCP 暴露给大模型的时候我到底是在写驱动还是在写 API这个问题困扰了我很久也基本贯穿了 DingOS 探索系统级 MCP 的第二阶段。上一篇我讲过为什么 DingOS 要盯上 MCP 这条协议而不是自己发明一套 Agent 调用硬件的规范。今天这篇我想更具体一点聊一聊真正把系统级 MCP 落地时的架构取舍、权限模型、事件通道和一路踩过的坑。这篇文章不会只给你看成功了的部分更多是那些返工了三遍才想明白的设计。如果你是做 Android/Linux 系统、嵌入式硬件或者对 AI Agent 怎么控制真实设备感兴趣这篇应该能给你一些不一样的参考。我们先把一个前提彻底说清楚系统级 MCP 不是把每个驱动都包成一个 JSON-RPC 服务那只是换了个壳核心问题在于——操作系统到底该把硬件的哪些语义交给 AI。1. 为什么把 MCP 下沉到系统层而不是继续在应用层打补丁先说一个我在逛各种 MCP 生态库时的感受。当前流行的 MCP Server像 Playwright MCP、Chrome DevTools MCP、Burpsuite MCP本质都是在和软件世界打交道网页 DOM、测试请求、浏览器调试协议。它们解决的是让大模型能够操作数字工具这当然很有价值但落到硬件场景里你会发现一个很尴尬的事实所有应用层 MCP 都默认操作系统已经稳定运转设备已经枚举完成驱动已经就绪。而真正做嵌入式、做 OS 的人都知道设备管理恰恰是最不稳定的那一层。DingOS 把 MCP 下沉到系统层核心动机其实特别朴素如果 Agent 要控制的设备连系统都还没有完成初始化那任何应用层的 Agent 框架都是空中楼阁。比如一块需要加载固件的 FPGA 板卡或者要通过安全启动校验才能接入的外设这种设备在传统 Linux 里可能到 late init 阶段才会以 char device 的形式出现在/dev下。Agent 如果想在更早阶段就参与设备配置就必须让 MCP 服务本身成为 OS 初始化管线的一部分。这里有一个反直觉的结论系统级 MCP 的价值不在于让大模型多了一个查 CPU 温度的工具而在于它迫使我们把整个设备管理流程重新拆解成 AI 可理解、可操作、可审计的语义单元。传统内核驱动的接口设计面向的是程序员——你要 read、write、ioctl得自己去查芯片手册。而 MCP 工具的设计面向的是意图——你要读取当前温度或者是把风扇转速调到 60%。这完全是两种抽象层次。下面这张表是我在 DingOS 内部分享时常用的对比它解释了为什么应用层 MCP 的经验不能直接搬到系统层对比维度应用层 MCP系统级 MCP资源对象网页、接口、文件、会话设备树节点、总线、寄存器、设备状态安全模型API Key、OAuth、租户隔离能力边界、审计、物理操作回滚状态性无状态请求为主强状态涉及设备独占和生命周期失败模式超时、限流、接口变更总线错误、设备热拔插、驱动崩溃部署单元独立进程或容器与内核模块、设备事件总线协作这张表里最扎眼的是安全模型那一行。应用层你泄露一个 API Key最多是数据被拉了一遍系统层你要是让大模型能无限制地操作 GPIO 或者继电器那真的能把一盏灯点烧把一台电机转冒烟。所以这个问题必须在协议设计之初就正视而不是等等再去补。我自己的结论是DingOS 做系统级 MCP不是为了新而是因为传统的那套sysfs ioctl交互方式在 AI Agent 时代已经变成了瓶颈。Agent 天然不擅长从设备手册里推理该怎么办它需要的是清晰、稳定、自带语义边界的接口。2. 设备抽象层与 MCP 的映射一条 I2C 总线如何变成一组工具系统级 MCP 的第一步是确定设备在 MCP 的模型里长什么样。MCP 官方模型里有三个核心概念Resources、Tools、Prompts。DingOS 对应关系很简单直接Resources映射设备的状态与配置比如当前温湿度读数、固件版本、寄存器 dumpTools映射设备的可执行操作比如读取传感器、设置 GPIO 输出、校准零点Prompts映射的是设备的使用模式比如帮我诊断为什么 I2C 通信不稳定这类预设流程。这个映射看起来平平无奇但真正的难点在设备抽象层怎么划分。2.1 设备分类别让每个驱动各写各的我们见过太多的 Linux 驱动每个人对设备能力的描述都不一样。有人用 sysfs 暴露属性有人在 debugfs 里放寄存器有人干脆只提供一个用户态 SDK。这种各自为政的状态到了 Agent 时代就是灾难——大模型没法预测你下一个设备长什么样。DingOS 的做法是先把设备按能力分类。基础类型包括 GPIO、PWM、ADC、I2C、SPI、UART、CAN、USB HID、传感器节点再加上一大类复合设备比如带 ISP 的摄像头模组、电机驱动器、读卡器。每一类设备在 DingOS 里都对应一份固定的 capability schema而不是任意的属性集合。举个例子一个 GPIO 设备的 CAPABILITY schema 至少要有这么几项方向输入/输出/双向逻辑电平语义高有效还是低有效是否支持中断触发去抖时间参数当前状态读取方式为什么要把逻辑电平语义单独列出来因为实际硬件设计中开关量输入经常经过光耦隔离光耦一接原来开关闭合时可能是低电平也可能是高电平完全取决于电路设计。如果你只给 Agent 一个gpio.read接口它读到的 raw level 其实是没有意义的。在 DingOS 里我们在 MCP 工具描述里直接写清楚该输入经光耦反相断开时为低电平闭合时为高电平Agent 就不用去猜硬件设计。2.2 一条 I2C 总线到一个 Sensor Tool Group 的映射拿最常见的温湿度传感器 SHT30 来说。它在 DingOS 里不是一个孤立工具而是一组以设备 ID 为前缀的 Tool Groupsht30.read_temperature_humidity— 一次性读取温湿度sht30.convert_raw_value— 把某个原始寄存器值换算成物理量sht30.set_heater— 控制传感器内置加热器用于高湿环境sht30.reset— 软复位清除异常状态每个工具的 inputSchema 和 outputSchema 都是系统生成的不是驱动作者手写的。下面的伪代码展示了 DingOS 内部如何从一个 I2C 设备节点自动推导出 Tool Schema{ name: sht30.read_temperature_humidity, description: 通过 I2C 地址 0x44 读取 SHT30 的温湿度数据返回已换算的物理量。设备读时序为 0x2C 0x06Clock Stretching 模式。, inputSchema: { type: object, properties: { timeout_ms: { type: integer, minimum: 100, maximum: 2000, default: 500 }, use_cache: { type: boolean, default: true, description: 距上次成功采样小于 1s 时直接返回缓存结果 } } }, outputSchema: { type: object, properties: { temperature_c: { type: number }, humidity_rh: { type: number }, sample_ts: { type: integer } } } }你可能会问为什么工具参数里要暴露timeout_ms和use_cache这种东西因为 LLM 的推理耗时是不可控的。Agent 在决定要不要再次读取时如果每次都真正发起一次 I2C 事务总线带宽和传感器功耗都会浪费。DingOS 的做法是在设备抽象层做好缓存和滤波MCP Server 只是把设备运行时的结果翻译给 Agent。2.3 MCP Server 不该独占设备这条我建议每个做设备 MCP 的人都记下来MCP Server 是设备驱动和寄存器世界的翻译官它不是设备的主人。在 DingOS 里真正持有设备文件描述符的是设备运行时Device RuntimeMCP Server 只是通过 IPC 和运行时通信。哪怕 MCP Server 进程崩溃底层驱动和设备状态都不受影响。Linux KVM 虚拟化的思路很值得借鉴客户机崩溃不影响宿主机虚拟机里的系统调用不会直接打到物理设备上。DingOS 的 MCP Server 也是这个定位它是一个无特权翻译层只负责把 Agent 的意图转换成设备运行时能执行的指令。这么做还有一个实际好处一个物理设备可以被多个 MCP Client 同时查看只读状态而写操作则被权限模块严格管控。如果你允许每个 Agent 各起一个 MCP Server 直接操作 GPIO那你迟早会碰到两个进程争抢同一个设备 fd 的惨案。3. 权限边界系统级 MCP 真正难啃的硬骨头我上一篇说过权限是安全的核心这一篇我想把具体的思路展开。很多人一开始把精力全花在协议交互上进程一跑起来发现大模型连继电器都能随便拨才意识到权限模型根本没设计。DingOS 的设备操作权限分四个等级能力级别权限含义典型工具风险等级read读取设备状态/配置sht30.read_temperature_humidity低listen订阅设备事件door_sensor.subscribe低write修改设备运行状态relay.set_target_state高config修改配置或升级固件flash.upgrade_firmware最高这个分级本身不复杂复杂的是工具可见性和调用权限要同时生效。很多系统只做调用时校验Agent 发出工具请求网关检查用户 token 里有没有权限。这有一个大问题——大模型的上下文窗口是有限的如果把当前用户无权使用的所有工具都塞给它不仅浪费 token还容易诱导模型去尝试越权操作。DingOS 的做法是最小工具暴露每个 Agent 会话在启动时只能从 MCP Registry 拿到它权限范围内允许的工具列表。权限之外的工具对它来说根本不存在。说白了不是做了坏事被抓而是从物理上就看不到作案工具。3.1 硬件操作没有撤回键软件世界里删库跑路后还能靠备份恢复硬件操作就不一样。你把继电器信号给反了电机烧了就是烧了你把固件刷挂了可能整个设备都要返厂。所以在 write 和 config 两种操作上DingOS 增加了额外的意图复核机制。具体做法是两步执行。Agent 先申请一个操作意图{ intent: relay.set_target_state, args: { channel: 0, target_state: on }, reason: 检测到温度超限需要开启通风阀 }系统把这个意图发给用户确认用户批准之后Agent 才能拿到一个有效期很短的 execution token用它去执行真正的工具调用。这套机制更像 Android 的运行时权限——你不只是批准这个应用可以打开摄像头你还能看到它当前这一次操作的上下文。说实话这套流程对 Agent 来说确实增加了延迟但它建立起了一个根关键的信任基础用户知道 AI 在动硬件之前会先问一声。如果这个信任基础不建立起来用户永远不会放心让 Agent 碰自己家里的智能家居更别说工业现场的 CAN 设备了。3.2 审计追踪与信任根DingOS 里每一次工具调用都会记录一条不可篡改的审计日志内容包括Agent ID、会话 ID、工具名、参数、调用结果、设备操作前后状态。我们不只记录做了什么还记录操作前设备的什么状态变成了操作后的什么状态这样出了问题才能回溯到底是 Agent 的意图错了还是驱动执行错了。信任根也是我在这个阶段才真正理解透彻的点。硬件工程师聊信任根往往想到安全芯片软件工程师聊信任根往往想到 Secure Boot。在系统级 MCP 里信任根还要回答一个问题这个 MCP Server 的二进制和 tool schema 是不是可信的如果 Agent 被诱导加载了一个来路不明的 MCP Server它就有机会伪装成合法设备工具把物理操作包装成无害调用。DingOS 目前的方案是MCP Server 进程由系统拉起二进制校验依赖启动时的安全引导信任链tool schema 变更需要签名授权。国产密码算法里 SM2/SM3 完全可以用于这种场景——签一个设备描述文件和处理器的固件包开销完全可以接受。这里不展开讲算法细节但请记住系统级 MCP 的安全底线不能只停留在应用层的 API Key它必须和系统信任根绑定。4. 一次完整调用从 /dev 节点到 Agent 工具消费的全链路理论讲再多不如走一遍完整的链路。这一节我用一个智能门锁的门磁传感器作为例子把 DingOS 里一次真实的 MCP 工具调用拆开来看。4.1 设备发现与注册当门磁传感器通过 GPIO 扩展芯片接入系统后DingOS 的 Device Registry 会经历这三个阶段硬件探测GPIO 事件触发内核识别到新设备分配设备节点设备注册设备运行时读取设备树配置确认门磁传感器的方向为输入、逻辑语义为打开时高电平标记支持中断触发MCP 上架根据 GPIO 类 capability schema 自动生成door_sensor.gpio.read和door_sensor.gpio.subscribe两个工具注册到 MCP Registry。在调试阶段我经常用命令行直接看这些工具dingos-mcpctl device list dingos-mcpctl tools list --device door_sensor输出大概是这样device: door_sensor ├── status: online ├── bus: gpiochip2 ├── line: 7 └── tools: ├── door_sensor.gpio.read └── door_sensor.gpio.subscribe请特别注意工具名不是随便取的。设备 ID 在前设备类型在中间具体操作在后。看似笨拙但对大模型非常友好因为它能一眼看出这个工具归属于哪个设备、属于哪类能力。我最开始图省事把工具名写成read_door后来 Agent 在处理多个设备时经常语义混乱还是老老实实改了回来。4.2 从 MCP Client 发出调用Agent 侧使用标准的 MCP Client 发起调用。下面是一个用 Python MCP SDK 写的最小示例import asyncio from mcp import ClientSession, StdioServerParameters async def read_door(): server StdioServerParameters( command/usr/lib/dingos-device-runtime, args[--device, door_sensor] ) async with ClientSession(server) as session: tools await session.list_tools() assert len(tools) 2, 门磁传感器应该只有两个工具 result await session.call_tool( door_sensor.gpio.read, {timeout_ms: 200} ) for block in result.content: print(block.text) asyncio.run(read_door())这段代码看着简单实际在 DingOS 内部至少经过五层处理MCP Client 发送 JSON-RPC 请求到网关网关做会话身份校验权限过滤器确认该 Agent 有 read 权限设备运行时拿到参数执行实际的 GPIO 读取最后结果按规范化格式返回。4.3 参数校验把错误拦截在驱动之外这一层我强烈建议做在 MCP 网关侧而不是做在设备驱动里。否则 Agent 可能在一次会话里用一百种错误参数轰炸你的驱动代码。DingOS 的网关对所有工具请求做 JSON Schema 校验类型不对、范围越界、必填缺失的请求直接返回错误。这里有个细节很多人想不到默认参数的安全值设计要和硬件风险挂钩。同样一个timeout_ms对于只读传感器默认给 500ms 完全没问题如果是继电器的hold_time_ms默认值必须要结合硬件最大允许通电时间来设计甚至强制要求显式传入不允许用默认值。权限模型解决谁能用参数校验解决怎么用安全两者缺一不可。5. 事件通道设计让大模型看见硬件状态变化工具调用解决的是让 Agent 主动读取硬件但真实的硬件世界大部分时间是事件驱动的——门被打开、温度越限、按钮被按下。如果这些变化都要靠 Agent 轮询那大模型每次回复都要等好几轮工具调用效率和实时性都一塌糊涂。DingOS 第二阶段做得比较深的一块就是这个设备事件通道。5.1 为什么标准 MCP 协议里事件不能走普通请求响应MCP 的普通工具调用是典型的请求-响应模型Agent 发起调用Server 返回结果然后 Agent 再思考、再调用。这个循环里如果 Agent 在等待一个门被打开的事件它总不能每秒钟调一次door_sensor.gpio.read吧既浪费 token又把硬件中断系统的意义给废掉了。DingOS 的做法是基于订阅的事件流。设备运行时内部维护一个事件订阅表Agent 可以通过subscribe类工具订阅特定设备的事件。底层依赖内核的eventfd和中断线程事件从硬件中断产生到进入 MCP 的消息通道实际延迟在亚毫秒级别。一条典型的事件消息长这样{ event_id: evt_1832, device: door_sensor, ts: 1738915200123, data: { state: open } }5.2 去抖、限流和状态快照硬件事件最大的麻烦不是少而是多。一个机械按钮按下如果不做去抖一次按压可能触发几十个边沿中断。如果在驱动层不做处理把这些原始事件全部推给大模型Agent 会非常困惑这个门到底开了一次还是开了五十次DingOS 的规则很简单能去抖的去抖能聚合的聚合能降噪的降噪全部在设备运行时完成不要让 Agent 看到原始中断洪流。比如门磁传感器驱动层做 20ms 软件去抖后一次开门的物理动作在 Agent 看来就是一条干净的open事件。持续事件场景则更复杂。比如读一个转速传感器每秒产生几千个脉冲但 Agent 真正需要的只是转速值或者超速告警。DingOS 在这个场景会做数据降噪聚合把高频脉冲流变成低频的状态更新或告警事件。订阅机制还附带一个关键策略新订阅者在建立订阅后会先收到一份当前状态快照后续只收到增量事件。这样 Agent 即使中途接入也能立刻知道设备当前是什么状态不会因为错过事件而对世界产生错误认知。5.3 Agent 慢思考的实时性挑战有一个场景让我意识到事件通道和 LLM 交互之间天然存在张力Agent 收到门被打开事件后需要调用大模型进行推理这个推理可能耗时两三秒甚至更久。这期间设备可能又产生了三次状态变化如果每个变化都推给 AgentAgent 还在上一个推理循环里事件就会被丢弃或堆积。DingOS 的方案是状态快照为准事件流为辅。Agent 每次完成一轮推理后不必去补齐漏掉的事件而是直接拉取一份最新状态快照和它记忆里的旧状态做对比。事件流负责告诉它发生改变了状态快照负责告诉它现在是什么样。这个组合既避免了 Agent 被历史事件淹没又保证了它掌握的是最新状态。6. 实测填坑会话残留、工具幂等与慢思考超时所有架构设计在真机跑起来之前都只是纸面文章。这一节是我在 DingOS 第二阶段真刀真枪踩过的一些坑直接给你可复现的教训。6.1 会话残留把设备锁死的故障第一次联调时我开了一堆调试客户端反复连接那个门磁传感器结果某次异常退出后设备运行时提示 device busy任何工具调用都失败。排查了半天发现是前一个会话的 MCP Server 进程没有正常退出它还死死攥着/dev/gpiochip2的文件描述符。这个问题在应用层 MCP 里几乎不存在因为谁也不会去独占一个 web API。但在系统级 MCP 就很致命设备独占生命周期和会话生命周期必须解耦。DingOS 最终的方案是给设备运行时加了 idle TTL会话断开后如果设备没有活动请求超过 5 分钟自动释放设备句柄同时增加 session heartbeat客户端活着的话可以续租。这个设定非常值因为它同时解决了 Agent 掉线重连后设备死锁的问题。6.2 工具幂等性继电器不能翻转这个坑我印象最深。最初我把继电器控制工具设计成了relay.toggle()看着挺简洁但实际测试里出过一次事故Agent 在网络抖动后重试了同一请求结果继电器被翻转了两次执行器状态和预期完全相反。从那以后 DingoOS 的设计原则变成了所有控制类工具必须面向目标状态而不是面向状态变化。relay.set_target_state(on)是幂等的——不管你调用一次还是三次只要当前已经是 on后续调用就是空操作。而toggle()天然不具备幂等性网络重试一次就是灾难。同理调整电机转速要用set_speed(60)不要用speed_up()设置空调温度要用set_target_temperature(26)不要用increase_temperature()。这个经验不仅适用于 DingOS任何想用大模型控制硬件的团队都应该当成铁律。大模型应用的网络重试是无法避免的工具设计必须保证重试安全。6.3 慢思考导致的硬件事务超时还有一次踩坑也很有意思。Agent 设计了一个流程读取称重传感器的当前值根据值决定是否开启进料阀。听起来很合理但实际跑起来Agent 在读取称重传感器的 5Hz 连续采样数据后开始调用大模型进行推理推理花了将近 3 秒。等它终于决定要开阀时传感器那边的采样缓冲区早就被新数据覆盖了读到的值已经是三秒前的老数据决策依据直接失效。这个问题的本质是硬件实时性要求和 LLM 推理的非实时性是天然的矛盾。DingOS 最终用后台采样缓冲 时间戳标注的模式解决。对于需要连续采样的设备设备运行时按固定频率把数据写入环形缓冲区Agent 随时可以读取到带时间戳的最新样本Agent 要做的不是实时控制而是基于最近一个样本窗口做决策。理解了这一点Agent 的硬件控制流程才真正可靠而不是靠运气。6.4 热拔插与枚举变化对 MCP 工具列表的影响最后提一个还没有完全解决的问题USB 设备热拔插时MCP 工具列表是动态变化的。Agent 会话开始时如果只拉取了一次工具列表那么当某个 USB 转串口设备被拔掉时它手里的工具就变成了悬空引用。调用时返回的是DEVICE_GONE错误但很多时候 Agent 并不能智能地重新枚举。DingOS 目前的缓解方案是向订阅事件的 Agent 推送工具列表变更通知。但说实话到这里我发现这已经不只是协议层的问题它还牵扯到 Agent 自身的规划能力也留到第三个阶段继续做深。回到文章最开头那个问题我到底是在写驱动还是写 API做完第二阶段我的答案是两者都是但又不完全是。系统级 MCP 让我重新审视了驱动接口应该长什么样也让我意识到AI 与硬件协作这件事真正难的不是让大模型学会调用工具而是让操作系统敢于把这些工具交给大模型。信任模型、幂等设计、事件通道、状态快照这些都是用一次次翻车换回来的。希望这篇记录能帮后来的人少踩几个坑。