让大模型操作工业相机:工具怎么设计、循环跑在哪个线程
给相机客户端加自然语言控制,想法很直接:说一句「画面有点暗」,它自己去查画面指标、改曝光、回读确认。
真正动手才发现,接大模型本身是最简单的一步——难的是给它什么工具、循环跑在哪个线程、以及它看不见图像这件事怎么绕过去。
这一层是纯增量的:不碰契约层、实现层和现有交互层。删掉它就是原来的客户端。
一句话原理
用户:画面有点暗
→ 面板把问题 + 工具清单交给大模型
→ 模型回:先调 get_frame_stats 看看
→ 程序真的用 OpenCV 算了亮度和过曝占比
→ 结果回灌给模型
→ 模型回:调 set_param('ExposureTime', 8000)
→ 程序真的写了相机参数
→ 模型总结成一句人话
大模型不执行任何代码,它只输出「我要调谁、传什么」。真正碰相机的是 CameraContext,和界面点按钮走的是同一条路。
这一层插在交互层和门面层之间,只允许调 CameraContext,不得出现任何厂商类型。和原有架构的依赖规则保持一致。
工具粒度:10 个通用工具,不是 37 个参数
第一版想得很美:每个参数一个工具,模型想调哪个调哪个。
问题在于这台相机有 37 个参数。37 个工具塞进 context,模型光看清单就晕了,实测选错率很高。
改成通用工具 + 参数名:
| 工具 | 参数 | 返回 |
|---|---|---|
list_cameras |
— | 序列号 / 品牌 / 是否连接 / 是否拉流 |
get_current_camera |
— | 当前选中相机的序列号 |
connect_camera / disconnect_camera |
serial? |
连接结果 |
list_params |
group? |
参数名 / 当前值 / 可读可写 / 取值范围 |
get_param |
name |
单个参数的值与范围 |
set_param |
name, value |
写入结果 + 写后回读的实际值 |
start_grab / stop_grab |
serial? |
拉流状态 |
get_frame_stats |
serial? |
亮度 / 过曝占比 / 欠曝占比 / 对比度 / 清晰度 + 语义判定 |
10 个工具,参数名作为字符串传进去,模型不用记 37 个函数签名。
set_param 那个「写后回读」是个小设计:设备可能对写入的值做钳位(要 8000 实际只给到 6000),把真实值返回给模型,它下一轮推理才不会建立在错误前提上。
模型看不见图像,这是最大的坑
最开始的设想是让模型判断画面。但它看不到图像——你问它「画面暗不暗」,它只能瞎猜,而且会猜得很有自信。
绕过去的办法是先让程序把图像变成数字:
| 指标 | 算法 | 给模型的字段 |
|---|---|---|
| 平均亮度 | cv::mean |
brightness: 0.31 |
| 过曝占比 | 灰度 > 250 的像素比例 | overexposed_ratio: 0.02 |
| 欠曝占比 | 灰度 < 20 的像素比例 | underexposed_ratio: 0.41 |
| 对比度 | 灰度标准差 | contrast: 18.3 |
| 清晰度 | Laplacian 方差 | sharpness: 12.7 |
| 判定 | 上述指标合成 | assessment: "underexposed" |
最后一行是关键。只给数字,模型还得自己判断「brightness 0.31 算不算暗」——不同场景阈值不一样,它猜不准。所以工具直接返回一句语义判定,模型拿到的就是结论。
把推理前置到确定性的代码里,模型只负责决策下一步做什么。 这是整个集成里最重要的一条经验。
线程模型
工具调用必须切回主线程执行。
主线程(UI) 后台线程
────────── ──────────
点击发送
└─ ask() ─────────────────→ QtConcurrent::run
Agent::Run() 循环问模型
└─ 调工具 ──┐
│
←──── BlockingQueuedConnection ─────────────┘
真正操作相机(主线程执行)
└─ 结果返回后台线程
Run() 返回
←──── 信号 finished(答复) ──────┘
原因很实在:CameraContext 和它背后的 CameraInterface 都不是线程安全的,而且用户可能同时在点界面。把所有相机操作串行到 UI 线程,天然避免了并发冲突,不用给整个相机层加锁。
代价是 AI 调参数期间界面事件队列被占用。但每次工具调用只有毫秒级,真正耗时的部分(等模型回复)在后台线程,所以界面不会卡。
API Key 不放代码里
| 项 | 做法 |
|---|---|
| Key 存储 | QSettings 存本机注册表,绝不进源码和仓库 |
| 传给库 | 库只接受环境变量名而不是 Key 本身,所以得先 qputenv() 注进去 |
| 换服务商 | Base URL 和模型名做成设置项,默认 DeepSeek,也支持智谱和本地 Ollama |
| 未配置时 | 直接禁用发送按钮并提示先配置,而不是让你对着没反应的按钮发呆 |
依赖降级照抄海康那一套
大模型运行时库是闭源商业授权的,而这个客户端是 MIT 开源项目。两者不能混在一起,所以沿用接海康 SDK 时的同一套策略:
- 库的 dll / lib / 头文件不提交进仓库
find_package(... QUIET)找不到时,AI 相关的源文件整体不参与编译- 没有它的时候
cmake --build必须照样成功,程序退化为纯相机客户端
结果是所有可选依赖走同一条路:缺任何一个,都能构建出一个能跑的、少了对应功能的版本。 没有「必须先装齐才能编译」的硬门槛。