徐仁康首页项目文章标签关于

让大模型操作工业相机:工具怎么设计、循环跑在哪个线程

2026-09-28 · C++ · Qt · LLM · 架构设计

给相机客户端加自然语言控制,想法很直接:说一句「画面有点暗」,它自己去查画面指标、改曝光、回读确认。

真正动手才发现,接大模型本身是最简单的一步——难的是给它什么工具、循环跑在哪个线程、以及它看不见图像这件事怎么绕过去。

这一层是纯增量的:不碰契约层、实现层和现有交互层。删掉它就是原来的客户端。

一句话原理

用户:画面有点暗
  → 面板把问题 + 工具清单交给大模型
  → 模型回:先调 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 必须照样成功,程序退化为纯相机客户端

结果是所有可选依赖走同一条路:缺任何一个,都能构建出一个能跑的、少了对应功能的版本。 没有「必须先装齐才能编译」的硬门槛。

← 返回文章列表