一、先看最终效果
问 AI:「3 号机现在正常吗?」
它自己去调 get_status,返回:运行中 / 自动模式 / 通讯正常 / 4 个点位其中 1 个越限 / 近 24 小时 2 条报警,然后用中文答出「压力 0.95 MPa 高于上限 0.8,建议检查」。
这件事的技术本质是:模型没有连数据库,它只是请求调用一个 C# 方法。
两条链路、两个进程、一个数据库文件。这个设计的价值在于:前端改的数据,AI 立刻能看到,不需要任何同步逻辑。
二、环境与版本
后端 NuGet 包(三个项目共用版本,避免还原冲突):
|
包
|
版本
|
用在哪
|
| --- | --- | --- |
|ModelContextProtocol
|
2.2.0
|
MCP Server
|
|SqlSugarCore
|
5.1.4.221
|
Shared(两个后端都用)
|
|Microsoft.Data.Sqlite
|
10.0.13
|
Shared(SQLite 驱动)
|
|Microsoft.Extensions.Hosting
|
10.0.13
|
MCP Server
|
|Microsoft.AspNetCore.OpenApi
|
10.0.13
|
Web API
|
前端:react 19.2.8 / antd 6.6.6 / vite 8.3.0 / typescript 6.0.2 / axios 1.20.0。
SQLite 不需要单独装数据库服务,devicehub.db 这个文件就是数据库。
三、项目结构
关键点:DeviceQueryService(只读)和 DeviceHubService(可写)是刻意分开的。MCP Server 只注入前者,从代码层面保证 AI 改不了数据。等以后要开放写操作,再改注入后者,那时所有改动会自动落进操作日志。
四、三条命令跑起来
1. 起后端 API
成功会打两行:
首次启动会自动建表并灌入示例数据:3 个型号、5 台设备、16 个点位、9 条报警。想重灌就删掉 data/devicehub.db* 三个文件(含 -wal、-shm)再启动。
2. 起前端
打开 http://localhost:5173。
3. 验证 MCP Server(可选)
MCP Server 走 stdio,不是双击运行的,它由客户端(Claude Desktop / Cursor / 各类 Agent 框架)当子进程拉起:
它会停在等待 stdin 的状态——这是正常的。Ctrl+C 退出。
客户端配置指向编译产物:
五、五张表:为什么这么分
|
表
|
说明
|
| --- | --- |
|device_model
|
型号字典。设备只引用字典里的型号,不允许自由填写
|
|device
|
设备台账。编码、名称、型号、位置、状态、运行模式
|
|point
|
点位(测点)。当前值、单位、上下限、采集地址、采集周期
|
|alarm
|
报警流水。只增不改,恢复填 ClearedAt,确认填 IsAcknowledged
|
|operation_log
|
操作日志。每次写操作自动追加,记录改动前后完整 JSON
|
三个设计决定值得说:
1. device 表把编号和名称拆成两个字段。 DeviceCode 是给系统用的稳定标识(M-003),DeviceName 是给人用的(3号机)。现场师傅嘴里说的永远是后者,而且设备改造后可能改名,所以 AI 查询要按名称模糊匹配,日志和关联要用编号。
2. point 表必须存上下限。 只有 Value 没有 LowerLimit/UpperLimit,AI 就只能念数字;有了上下限,它才能回答「正常吗」。这是从「能查数」到「能判断」的关键一步。
3. alarm 表只增不改。 报警是历史事实,恢复和确认都是补字段,不做物理删除。否则半年后想复盘「那天为什么停机」就没有原始记录了。
实体写法(以 Point 为例):
主键一律用 int,不要用 long——原因见第八节坑 ①。
六、双进程共享一个 SQLite:两个必做配置
这是整个项目最容易翻车的地方。
1. 用 SqlSugarScope,不要用 SqlSugarClient
SqlSugarClient 不是线程安全的。Web API 并发处理请求时,多个请求共用一个 Client 会出现数据串台(A 请求拿到 B 请求的结果)。SqlSugarScope 是官方为多线程准备的,内部按线程隔离。
注册成单例,不要每次 new。
2. 必须开 WAL 模式
两个进程(MCP Server + Web API)同时读写同一个 .db 文件,默认的 delete 日志模式下写操作会互相阻塞,直接抛 database is locked。WAL 允许多读一写并发,这个场景够用。
InitTables 是幂等的,每次启动都可以调,表已存在时只比对字段差异做增量变更,不会清空数据。
3. 数据库路径必须统一解析
这是双进程架构最容易掉的坑:MCP Server 和 Web API 是两个项目,各自的 bin 目录不同。如果直接用 AppContext.BaseDirectory 拼相对路径,会生成两个 .db 文件,你会以为「数据没同步」,实际是两个进程各写各的。
解决办法是从程序目录往上找到解决方案根目录:
两个进程都调这个方法,就一定指向同一个文件。部署到别的机器时用环境变量覆盖。
七、MCP Server 的三个关键写法
1. stdio 铁律:stdout 只能走协议
stdio 的通信通道就是标准输入输出。任何一句 Console.WriteLine 都会污染 JSON-RPC 消息流,客户端会报解析错误,而且报错位置离真正原因很远,极难排查。
所有日志必须重定向到 stderr:
2. 注册与工具发现
ServerInstructions 在握手时发给客户端,客户端会当成系统提示注入,对之后每次对话都生效。它和工具的 Description 分工不同:工具描述回答「这个工具是什么」,ServerInstructions 回答「什么时候必须用它、不许绕过它」。
注意它只在握手时发送一次,改完必须重启客户端才会重新下发。多数客户端只把前 512 个字符喂给模型,硬规则放最前面。
3. 工具写法:[Description] 不是注释
这段中文是模型决定调不调用这个工具的唯一依据。 写含糊了,模型就直接不调,然后自己编一个答案——这是「AI 答非所问」最常见的原因,比代码写错更致命。
所以每段描述必须回答三件事:
这个工具能做什么
什么情况下必须调它(用用户可能说出口的词写,如「正常吗」「什么情况」)
边界:查不到怎么办
第 3 条很多人会漏。模型遇到「查不到」非常容易直接回用户一句「系统里没有这台设备」,用户当场认为这个工具没用。
参数描述同理,hours 写成「用户说『今天』填 24,『这周』填 168」,比写「回溯小时数」有效得多。
4. UseStructuredContent = true 到底给什么
开了它,MCP 协议的返回会多两样东西:
tools/list里多出outputSchema——由返回类型自动推导的 JSON Schema,说明「这个工具返回什么结构」tools/call里多出structuredContent——可以直接反序列化成对象的干净 JSON
不开的话 Tool.OutputSchema 就是 null。
但要注意一个和直觉相反的事实:content(给模型读的那份)在开与不开时内容完全一样,都是整个返回值序列化成的 JSON 字符串,中文还会被转义成 \uXXXX。开关的增量只有 outputSchema + structuredContent 这两样。
八、越限判断放服务端,别交给模型
这是我认为整个项目里最值钱的一个决定。
如果只返回 {"压力": 0.62},模型不知道 0.62 是好是坏——它没有工艺区间这个概念,最后要么瞎猜、要么反问、要么编一句「正常」。
所以在 DTO 转换时就把结论算好:
三个收益:
模型不需要比大小,几乎不会算错
规则集中在一个方法里,将来改(比如停机不判越限)只改一处,不用去改 prompt
返回值里再附带一句人话摘要(
"3号机 · 压力 = 0.62 MPa(正常区间 0.5–0.8),当前正常"),模型直接引用就不会说错
模型擅长语言,不擅长你那套工艺规则。判断权别交给它。
九、设备名解析:把最高频的失败兜住
现场最常见的失败不是模型不会答,而是名字对不上——人说「三号机」,库里存的是「3号机」。
查不到时不返回错误,返回可用清单:
模型看到这句,下一轮就会自己改对重试。用户看到的是正确答案,而不是「查不到」。
只多写了十几行代码,但把最高频的失败场景从「失败」变成了「自动纠正」。
十、怎么验证 MCP Server 真的在干活
不用装任何客户端,直接用 stdio 喂它 JSON-RPC。MCP 就是拿标准输入当对话通道:
三条请求:握手 → 要工具清单 → 真正调一次。id=2 的回复里是 5 个工具的完整定义(含 outputSchema),id=3 的回复里是真实数据。
这个办法在排查「工具没被调用」「返回结构不对」时极其有用,比在客户端里猜快得多。
十一、这套东西能用在什么场景
它的定位不是「炫技 demo」,而是断网内网环境下的一份最小可运行骨架:
全程不联网,不需要云服务,一台普通工控机就能跑
数据落在本地 SQLite 文件里,可以直接拷走备份
MCP 是标准协议,工具写完,Claude Desktop / Cursor / 各类 Agent 框架都能直接接
AI 只读不写,从依赖注入那层就限制住了,不会误改现场数据
还没做的部分(也是下一步):写操作工具(要解决「AI 改了参数,操作日志记谁」)、离线打包(dotnet publish 单文件 + 配置外置)、登录与权限。
十三、代码
完整项目在 GitHub,仓库名 device-hub:包含 5 张表实体、5 个只读工具、Web API、React 前端,以及上面的所有踩坑记录。
如果这篇文章帮你省下了排查 MSB3026 或 AUTOINCREMENT 的那两个小时,欢迎在评论区说一声。有做现场的朋友想聊聊「AI 读实时数据」在产线上的实际接受度,也欢迎留言。