diff --git a/docs/feature-pic-design.md b/docs/feature-pic-design.md index 3db3207..7aacb94 100644 --- a/docs/feature-pic-design.md +++ b/docs/feature-pic-design.md @@ -1,7 +1,8 @@ # 拍照提问模式 —— 设计方案 -状态:**MVP 已实现并通过真机端到端验证**(2026-09-03)。类型检查、构建、真实摄像头拍照到 -真实视觉模型作答的完整链路已跑通,细节见 §10。 +状态:**已实现并通过真机端到端验证**(2026-09-03)。当前生效的是 §11 的二次架构 +(拍照 → 图像理解转述 → 接入本地知识库检索 → 生成),验证记录见 §12。 +§1–§10 保留第一版"纯视觉直接问答"的设计与验证过程作为历史记录,**已被 §11 推翻,不再是当前实现**。 ## 1. 背景与目标 @@ -269,10 +270,11 @@ vision: { **这次验证没有覆盖到的**:正式打包(deb)流程、设置页手动切换开关的 UI 操作(是直接改 `config.json` 打开的 `vision.enabled`,没有走设置页表单)、长时间使用的稳定性。 -## 11. 架构调整:接入本地知识库检索(2026-09-03 二次设计,未实现) +## 11. 架构调整:接入本地知识库检索(2026-09-03 二次设计) -**状态:设计已定稿,代码尚未开始改。** 本节内容推翻 §3 "不做检索" 的原始决定,是当前的 -权威设计,实现时以本节为准,§3/§5.2/§5.3/§6 保留作为"第一版为什么这么想"的历史记录。 +**状态:已实现并通过真机端到端验证**(commit `687e0ca`,验证记录见 §12)。本节内容推翻 +§3 "不做检索" 的原始决定,是当前的权威设计,§3/§5.2/§5.3/§6 保留作为"第一版为什么这么想" +的历史记录。 ### 11.1 为什么要改 @@ -339,3 +341,64 @@ vision: { (拍照、缩略图、发送按钮)不用动 - 是否需要给"识别图片"这一步单独设一个超时/失败兜底(比如识别失败了要不要还能继续走检索, 还是直接报错终止)——待实现时决定,本节暂不展开 + +## 12. 二次架构(接入检索)的实现与验证记录(2026-09-03) + +代码:commit `687e0ca`。实现完全按 §11 的设计,没有中途改方向。 + +### 12.1 真机验证结果 + +验证环境:pad(当时地址 `192.168.1.37`,注意这台设备 IP 会变,`192.168.31.48` 那个地址 +当时能 ping 通但 SSH 被拒,实际是别的设备占用),`npx electron-vite preview` 跑编译产物, +`xdotool` 模拟点击 + `ffmpeg -f x11grab` 截图取证。 + +类型检查 `npx vue-tsc --noEmit` 零错误,`npx electron-vite build` 构建成功。 + +两次真实拍照问答的完整链路数据: + +| 场景 | 图像理解 | 检索 | 生成 | 结果 | +|---|---|---|---|---| +| 未输入文字(拍室内台灯托盘) | 4.0s | 召回 20 片段 | 0.1s / 3 tokens | 正确回答"我不知道" | +| 输入"如何更换汽车前大灯。" | 5.3s | 召回 20 片段 | 17.5s / 233 tokens | 正确答出换灯步骤,带引用 [18] | + +**图像理解稳定在 4.0–5.3 秒**,落在 §11 定的 5–10 秒目标内。固定提示词里"一句话""不展开" +的约束确实生效:输出 45 tokens 上下,对比旧方案(让视觉模型直接完整作答)的 454 tokens / 31 秒, +差了一个数量级。 + +### 12.2 验证中确认为正确的两个行为 + +**其一,第二个场景恰好验证了 §11.3 那个关键决策是对的**:拍的是保温杯/厨房桌面,跟"更换前大灯" +毫无关系,但因为检索用的是 `caption + 用户问题` 拼接(扩大召回),生成用的是**用户原始问题** +(保持聚焦),最终答案准确命中前大灯文档并带正确引用,完全没被"保温杯、厨房环境"这些无关的 +图像描述带偏。如果当初决定生成也用组合文本,这个场景大概率会跑偏。 + +**其二,第一个场景回答"我不知道"是正确行为不是 bug**:图片内容跟知识库(汽车维修手册)无关, +检索虽然照常返回 topK=20 个片段(暴力余弦排序总会返回够数),但内容都不相关,模型遵守 +systemPrompt 里"上下文找不到就回答我不知道"的规则。这跟文字问答问无关问题时的行为一致。 + +### 12.3 验证中发现并修复的真 bug:摄像头 deviceId 跨会话失效 + +首次点开拍照弹层直接报 **`OverconstrainedError`**、预览全黑,整个功能不可用。 + +根因:`CameraCapture.vue` 把上次选用的 `deviceId` 持久化到 config.json,下次启动时拿它做 +`getUserMedia({ video: { deviceId: { exact: ... } } })` 的约束。但 **deviceId 在 Web 标准里 +不保证跨会话稳定**,换一次会话后旧 id 往往已经不存在,`exact` 约束匹配不上就直接抛错。 + +修复:抽出 `openStream()`,指定 id 打不开时自动回退到 `{ video: true }` 默认摄像头,并把 +实际拿到的设备 id(`getVideoTracks()[0].getSettings().deviceId`)回写到 `selectedDeviceId`, +让"自动切到后置摄像头"的逻辑在回退场景下同样能继续工作(原来的写法只在 `initialDeviceId` +为空时才找后置摄像头,回退场景会被漏掉)。 + +### 12.4 顺带确认的一件事:画面方向会影响识别质量 + +用 `ffmpeg` 直接从 `/dev/video2` 抓的**原始帧**(不经过前端旋转)送去识别,模型的描述里明确 +写了"图片显示一个**倒置的**室内场景"。这说明摄像头原始画面确实是倒的,而且**视觉模型会把 +"倒置"当成图像的一个特征去描述**——即 commit `0d19be5` 那个旋转修复不只是预览好看的问题, +它直接影响识别质量。经过前端旋转后拍的照片(见 §12.1 两次实测),描述里再没出现"倒置"字样。 + +### 12.5 这次没有覆盖到的 + +- 正式 deb 打包(走的是 `electron-vite preview` 跑编译产物) +- "识别图片"这一步失败时的兜底行为(§11.4 最后一条列为待定,实现时也没专门处理, + 目前是整个 vision-query 抛错走 `vision-query:error`,前端弹错误提示) +- 长时间连续使用的稳定性