ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于MCP服务端让AI自动分析IoT功耗计数据

基于MCP服务端让AI自动分析IoT功耗计数据

1. 从一个折腾的下午说起:为什么要让 AI 自己看功耗计

去年冬天我在调一块低功耗传感器板子,目标是让它在纽扣电池上撑够两年。理论上算下来休眠电流应该压在 8 微安以内,可实测数据总是飘,一会儿 12 微安,一会儿又跳到 30 微安。问题在于,我手上那台 IoT Power 功耗计虽然能记录电流曲线,但每次改完固件、烧录、跑测试、导出 CSV、再拿脚本分析,这一套流程走下来少说十几分钟,一天下来光在"看数据"这件事上就耗掉大半天。

更烦的是,功耗异常往往出现在某个特定的时间窗口——比如射频模块唤醒的那 200 毫秒,或者某个外设在特定状态下没进休眠。这些瞬态尖峰靠人眼盯曲线基本抓不住,得写脚本去筛。可每换一个测试场景,脚本就得改一遍,改到最后我自己都记不清哪个版本对应哪次实验。

后来我接触到 MCP(Model Context Protocol),也就是模型上下文协议,思路一下子打开了。MCP 本质上是一套让 AI 模型能够调用外部工具、读取外部资源的标准化协议。它把"AI 能做什么"和"外部世界有什么"解耦开来:AI 负责理解和决策,MCP 服务端负责把真实世界的设备能力暴露成 AI 能调用的工具。那我完全可以给 IoT Power 写一个 MCP 服务端,让 AI 直接去读功耗计的实时数据、去筛选异常区间、去对比不同固件版本的功耗表现。

这篇文章就是把这个想法落地的完整记录。我会讲清楚 MCP 服务端到底是怎么回事、IoT Power 这类功耗计的数据怎么接进来、工具函数怎么设计、AI 调用时有哪些坑。适合已经有一点嵌入式或 IoT 测试经验、想把手上的仪器接进 AI 工作流的同学。哪怕你用的是别的功耗计、别的数据采集设备,这套思路也能直接迁移。

2. 先搞明白 MCP 服务端到底在干什么

2.1 MCP 协议的核心抽象:工具、资源、提示

很多人第一次听到 MCP 会以为是什么高深的模型内部机制,其实它非常朴素。你可以把它理解成一份"AI 和外部程序之间的接口约定"。这份约定里主要有三类东西:

  • 工具(Tools):AI 可以主动调用的函数。比如read_power_now()读当前功耗,list_capture_sessions()列出所有采集会话。工具是有副作用的、需要参数的、会返回结果的。
  • 资源(Resources):AI 可以读取的数据。比如某次采集的原始电流曲线、某个固件版本的功耗报告。资源通常是只读的、通过 URI 定位的。
  • 提示(Prompts):预定义的提示模板,让用户或 AI 快速发起某类任务。比如"分析这次采集里所有超过阈值的尖峰"。

这三者的分工很关键。我一开始图省事,把所有能力都塞进工具里,结果 AI 每次分析曲线都要先调一个工具把整条曲线拉回来,token 消耗巨大还容易超上下文。后来把"原始曲线"改成资源,AI 只在需要时按 URI 读取片段,效率立刻上来了。

提示:工具适合"动作",资源适合"数据"。分不清的时候问自己一句——这个操作会不会改变外部状态?会就是工具,不会就是资源。

2.2 为什么是服务端而不是插件

有同学会问,直接写个 Python 脚本让 AI 调用不就行了,为什么要搞个服务端?这里有个实际考量。功耗测试往往是长周期的,一次采集可能跑几个小时甚至过夜。如果 AI 和采集程序在同一个进程里,AI 一崩采集就断了。做成独立的服务端之后,采集进程稳定跑着,AI 只是通过协议来查询,两者生命周期解耦。

另一个原因是复用。我实验室里不止一台功耗计,还有电子负载、温箱、示波器。如果每台设备都写一套 AI 对接逻辑,维护成本爆炸。统一走 MCP 服务端之后,AI 侧只需要知道"有这么几个工具可用",底层换设备、加设备都不用改 AI 的调用逻辑。

从架构上看,典型的部署是这样:IoT Power 通过 USB 或串口连到一台常开的采集主机,采集主机上跑着数据落库程序和一个 MCP 服务端。AI 客户端(可以是桌面应用、也可以是命令行 agent)通过标准输入输出或本地网络连到这个服务端。整个链路里,AI 永远不直接碰硬件,只碰服务端暴露的抽象接口。

2.3 数据流的关键设计:采样、落库、查询三段分离

我在实际做的时候踩过一个坑:一开始让 MCP 服务端直接去读功耗计的串口。结果 AI 一发起查询,服务端就去抢占串口,采集程序那边就丢数据。后来改成三段分离:

  1. 采样层:一个常驻进程独占串口,以固定频率(我用的 1kHz)读取电流电压,写入本地时序数据库。
  2. 存储层:时序数据库按会话(session)组织数据,每个会话对应一次固件版本加一组测试条件。
  3. 查询层:MCP 服务端只读数据库,永远不碰硬件。

这个分离带来的好处是,AI 查询再频繁也不会影响采集质量。而且历史数据可以反复分析,不用重新跑实验。代价是多了一个数据库组件,但用 SQLite 加时间索引就够用了,单机场景完全扛得住。

3. 把 IoT Power 的数据接进来:采样与存储实操

3.1 采样频率与量程的选择逻辑

功耗计的参数选择直接决定后面 AI 能不能分析出有用结论。这里给几个我实测下来比较稳的配置思路。

采样频率方面,如果你只关心平均功耗和休眠电流,100Hz 足够。但如果要抓射频唤醒、外设瞬态这类毫秒级事件,至少得上 1kHz,我一般用 2kHz 留余量。频率再高,数据量会爆炸——2kHz 跑一小时就是 720 万个采样点,SQLite 单表就有点吃力了,得考虑分表或降采样存储。

量程方面,IoT Power 这类设备通常有多个电流档位。低功耗场景一定要用最小档位,否则小电流的分辨率不够。我遇到过 8 微安和 12 微安分不出来的情况,就是因为档位选大了,ADC 的有效位数被浪费掉。切换档位时会有短暂的量程切换毛刺,分析时要记得把这几十毫秒的数据标记为无效。

测试场景建议采样率建议档位单小时数据量
休眠电流评估100Hz最小档约 36 万点
射频瞬态分析2kHz中档约 720 万点
整机平均功耗10Hz自动约 3.6 万点
电池续航预估1Hz自动约 3600 点

3.2 落库表结构设计

数据库表我设计得很简单,但有几个字段是必须的,缺了后面 AI 分析会很痛苦。

CREATE TABLE sessions ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, firmware_version TEXT, test_condition TEXT, started_at INTEGER NOT NULL, ended_at INTEGER, sample_rate INTEGER NOT NULL, current_range TEXT ); CREATE TABLE samples ( session_id INTEGER NOT NULL, ts INTEGER NOT NULL, current_ua REAL NOT NULL, voltage_mv REAL, valid INTEGER DEFAULT 1 ); CREATE INDEX idx_samples_session_ts ON samples(session_id, ts);

几个设计要点值得展开说。firmware_version和test_condition这两个字段是我后来加的,一开始没加,结果 AI 分析时没法把功耗差异归因到具体改动上,只能干巴巴地说"这次比上次高"。加上之后,AI 就能说出"v1.2.3 相比 v1.2.2 在射频唤醒阶段平均电流高了 15%",这种结论才有指导意义。

valid字段用来标记量程切换、设备重连等异常区间的数据。AI 分析时默认只读valid=1的点,避免被毛刺误导。这个字段看起来不起眼,但没有它,AI 会把量程切换的尖峰当成真实的功耗异常,给出完全错误的结论。

3.3 采集进程的稳定性处理

采集进程要能扛住各种意外。我踩过的坑包括:USB 线接触不良导致串口断开、功耗计固件偶发死机、主机休眠导致采集中断。处理方式是在采集进程里加心跳检测和自动重连。

def acquire_loop(device, db, session_id): while running: try: frame = device.read_frame(timeout=1.0) if frame is None: reconnect(device) continue db.insert_sample(session_id, frame.ts, frame.current_ua, frame.voltage_mv, valid=frame.valid) except DeviceError: mark_invalid_window(db, session_id, window_ms=200) reconnect(device)

注意mark_invalid_window这一步。设备重连期间的数据是缺失的,如果不标记,AI 会以为那段时间功耗为零,得出"设备进入了超低功耗状态"的荒谬结论。把重连前后的 200 毫秒标记为无效,是保证数据可信的关键细节。

4. MCP 服务端的工具与资源设计

4.1 工具函数清单与参数设计

工具设计的原则是:粒度适中,参数明确,返回结构化。我最终定下来的工具集大概是这些:

  • list_sessions(limit, firmware_version):列出采集会话,支持按固件版本过滤。
  • get_session_summary(session_id):返回某会话的平均电流、峰值、最小值、时长。
  • find_current_spikes(session_id, threshold_ua, min_duration_ms):找出超过阈值的电流尖峰。
  • compare_sessions(session_id_a, session_id_b, metric):对比两个会话的指定指标。
  • get_power_profile(session_id, start_ts, end_ts, bucket_ms):返回指定时间窗口的降采样曲线。

参数设计上有个经验:阈值类参数一定要带单位后缀。我一开始写的是threshold,AI 调用时传了个 100,我以为是微安,结果它理解成毫安,找出来的尖峰全是噪声。改成threshold_ua之后,AI 再也没传错过。这种命名上的小细节,能省掉大量调试时间。

返回结构统一用 JSON,字段名也带单位。比如{"avg_current_ua": 8.3, "peak_current_ua": 45200, "duration_ms": 3600000}。AI 拿到这种结构,几乎不会误读。

4.2 资源 URI 的设计

资源这块我用 URI 来定位数据,格式是power://session/{id}/raw和power://session/{id}/summary。AI 需要原始曲线时读 raw,需要概览时读 summary。

这里有个坑:原始曲线可能几百万个点,直接作为资源返回会撑爆上下文。我的做法是 raw 资源默认只返回前 10000 个点,并在返回里带上truncated: true和total_points字段。AI 看到截断标记后,会改用get_power_profile工具去取降采样版本。这个"资源有上限、工具做聚合"的分工,是我试了好几版才定下来的。

4.3 提示模板的实用化

提示模板我做了三个,都是高频场景:

  • analyze_sleep_current:分析休眠电流是否达标,给出与目标值的差距。
  • diagnose_spike:针对某个尖峰,结合时间戳分析可能的原因。
  • regression_check:对比新旧固件,判断是否有功耗回归。

模板里预置了分析步骤和输出格式,AI 调用后能直接产出结构化报告。这比让用户每次手打一长串提示要省事得多,也保证了分析口径的一致性。

5. 完整实操:从零跑通一次 AI 功耗分析

5.1 环境准备与依赖

我用的技术栈是 Python 加 SQLite 加官方 MCP SDK。依赖不多,但版本要对齐。

pip install mcp pyserial

MCP SDK 的版本更新比较快,接口偶有变动,建议锁定一个稳定版本。我用的版本在工具注册的装饰器写法上和早期版本有差异,如果你照抄网上的老教程发现跑不起来,先检查 SDK 版本。

采集主机上要确保串口权限正确,Linux 下把用户加进dialout组,Windows 下确认驱动装好、端口号没被占用。这些基础问题看似简单,但我见过太多人卡在"设备连不上"上耗掉一整天。

5.2 服务端骨架代码

服务端主体结构如下,我做了精简,保留核心逻辑:

from mcp.server import Server from mcp.server.stdio import stdio_server import sqlite3 app = Server("iot-power-mcp") DB = "power.db" @app.tool() def list_sessions(limit: int = 20, firmware_version: str = None) -> list: conn = sqlite3.connect(DB) sql = "SELECT id, name, firmware_version, started_at FROM sessions" params = [] if firmware_version: sql += " WHERE firmware_version = ?" params.append(firmware_version) sql += " ORDER BY started_at DESC LIMIT ?" params.append(limit) rows = conn.execute(sql, params).fetchall() conn.close() return [{"id": r[0], "name": r[1], "fw": r[2], "started_at": r[3]} for r in rows] @app.tool() def get_session_summary(session_id: int) -> dict: conn = sqlite3.connect(DB) row = conn.execute( "SELECT AVG(current_ua), MAX(current_ua), MIN(current_ua), " "COUNT(*), (MAX(ts)-MIN(ts)) FROM samples " "WHERE session_id = ? AND valid = 1", (session_id,) ).fetchone() conn.close() return { "avg_current_ua": round(row[0], 2), "peak_current_ua": round(row[1], 2), "min_current_ua": round(row[2], 2), "sample_count": row[3], "duration_ms": row[4] } async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())

这段代码里,@app.tool()装饰器把普通函数注册成 MCP 工具,类型注解会被自动转成工具的输入 schema。所以注解一定要写全、写准,AI 是靠这个 schema 来决定怎么传参的。

5.3 一次真实的 AI 分析过程

环境跑通后,我在 AI 客户端里发了一句:"帮我看看 session 42 的休眠电流有没有达标,目标是 8 微安。"

AI 的调用链是这样的:先调get_session_summary(42),拿到平均电流 9.7 微安、峰值 45 毫安、最小值 6.2 微安。然后它判断平均值超标,接着调find_current_spikes(42, threshold_ua=1000, min_duration_ms=1),发现有三个持续 2 到 5 毫秒的尖峰,分别在采集开始后第 12 分钟、第 34 分钟、第 51 分钟。

AI 接着调get_power_profile(42, start_ts, end_ts, bucket_ms=10)把这三个尖峰附近 200 毫秒的曲线拉出来,发现尖峰形态一致,都是先冲高再缓慢回落。它给出的结论是:这三个尖峰很可能是同一个周期性任务触发的,建议检查定时器周期和射频唤醒逻辑。

整个过程我一句话没多说,AI 自己完成了从概览到定位到归因的完整链路。这就是 MCP 服务端的价值——把仪器的能力变成 AI 的"感官"。

5.4 参数计算:怎么定尖峰阈值

find_current_spikes的阈值不是拍脑袋定的。我的做法是先算基线:取会话里电流的中位数作为基线,然后阈值设为基线的 10 倍,或者绝对值的 1 毫安,取两者较大值。

def auto_threshold(session_id): conn = sqlite3.connect(DB) row = conn.execute( "SELECT current_ua FROM samples WHERE session_id=? AND valid=1 " "ORDER BY current_ua LIMIT 1 OFFSET " "(SELECT COUNT(*)/2 FROM samples WHERE session_id=? AND valid=1)", (session_id, session_id) ).fetchone() conn.close() median = row[0] return max(median * 10, 1000)

用中位数而不是平均值,是因为平均值会被尖峰本身拉高,导致阈值定得过高、漏掉真实异常。这个细节我在第一版里没注意,结果 AI 老是报"未发现异常",其实异常就在眼前。

6. 踩坑记录与常见问题排查

6.1 工具调用超时与大数据量返回

最常见的坑是工具返回数据太大导致超时。我一开始让get_power_profile不限制返回点数,AI 要一小时的数据就返回几百万个点,序列化加传输直接卡死。

解决办法是强制降采样:工具内部根据请求的时间窗口自动计算 bucket 大小,保证返回点数不超过 5000。如果用户或 AI 要更细的粒度,就缩小时间窗口再请求。这个"窗口换精度"的策略,比单纯限制点数更符合分析习惯。

6.2 AI 误读单位与量纲

前面提过阈值单位的问题,这里再补充一个。电流值我用微安存储,但 AI 有时会在报告里写成毫安。根源是返回字段名不够明确。后来我把所有返回字段都带上单位后缀,avg_current_ua、peak_current_ua,AI 就再没搞错过。字段命名这件事,多花五分钟,省下五小时。

6.3 会话对比时的条件不一致

compare_sessions最容易出的问题是拿两个测试条件不同的会话去比。比如一个跑在 25 度、一个跑在 60 度,功耗差异可能全来自温度。我的处理是在对比工具里先检查两个会话的test_condition是否一致,不一致就返回警告,让 AI 知道这个对比有前提。

问题现象可能原因排查方向
AI 报未发现异常阈值定得过高检查基线计算是否用了中位数
工具调用超时返回数据量过大加降采样,限制返回点数
单位读错字段名无单位后缀统一加_ua、_mv后缀
对比结论不可信测试条件不一致对比前校验 test_condition
数据有零值段设备重连未标记检查 valid 字段标记逻辑

6.4 采集与查询的并发冲突

如果偷懒让 MCP 服务端直接读串口,并发冲突几乎必然发生。SQLite 在并发读写时也可能报database is locked。我的做法是采集进程用 WAL 模式写库,查询进程只读,并且设置合理的 busy timeout。

PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;

WAL 模式下读写可以并发,查询不会阻塞采集。这个设置对长周期采集特别重要,否则每次 AI 查询都可能让采集丢一小段数据。

7. 这套方案还能怎么扩展

跑通基础版本之后,我陆续加了几个扩展,都挺实用。

一个是自动回归检测。每次新固件采集完,服务端自动触发一次与基线会话的对比,如果平均功耗或峰值超标,就在会话记录里打上标记。AI 查询时能直接看到哪些版本有回归。

另一个是多设备聚合。实验室里几台功耗计同时跑不同板子,服务端按设备 ID 分区存储,AI 可以跨设备对比同一固件在不同硬件上的表现。这个在选型阶段特别有用。

还有就是与固件仓库联动。把会话的firmware_version和代码仓库的 commit 关联起来,AI 分析出功耗回归后,能直接定位到是哪次提交引入的。这一步需要额外的集成工作,但对定位问题帮助巨大。

我个人在实际操作中的体会是,MCP 服务端的价值不在于让 AI "更聪明",而在于把仪器的数据变成 AI 能理解的结构化信息。工具和资源的边界划清楚、字段命名带单位、异常数据标记好,这三点做到位,AI 分析功耗的准确率会远超预期。至于具体用哪台功耗计、哪个数据库,反而是次要的,思路对了,换什么设备都能套。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进