From ec7f11103875f35634249b9288830a21cbc58bdf Mon Sep 17 00:00:00 2001 From: ssurui Date: Thu, 3 Sep 2026 21:22:49 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E8=A1=A5=E9=BD=90=E6=8B=8D=E7=85=A7?= =?UTF-8?q?=E6=8F=90=E9=97=AE=E3=80=81=E6=B5=81=E5=BC=8F=E6=92=AD=E6=8A=A5?= =?UTF-8?q?=E4=B8=8E=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md: - 文件说明补 visionHandler.ts / CameraCapture.vue / utils/tts.ts, 并修正 generator.ts 的过时描述(早就不接受 llamacppServer 配置了) - AppConfig 补 vision 字段,并写明"改 DEFAULT_CONFIG 只对新配置生效" 这个部署坑(已有设备的 config.json 会盖掉新默认值) - 新增"拍照提问"章节:三段式流程、"检索用组合文本+生成用原始问题" 这个分工的理由、视觉能力探测的判据顺序(含 er_backend 日志只留 200 条 导致 allow_image 会被挤掉、以及为什么不用 config.modelType)、 摄像头 deviceId 跨会话失效和画面倒置影响识别质量两个坑 - 关键技术约定新增语音流式播报(append 必须累加、加粗重复朗读的成因) 和系统提示词(为什么"每次换措辞"只写提示词无效、为什么不调 temperature) README.md:新增"功能"和"启用拍照提问"两节,说明拍照提问是接入知识库 检索而非让模型凭空描述图片,以及需要多模态模型+设置开关两个前提。 文档里引用的文件、函数、IPC 事件已逐一核对与代码一致。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN --- CLAUDE.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---- README.md | 20 +++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5c50b34..22d41e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,8 @@ 服务进程管理(llama-server / embed-server 的启动停止)**完全委托给 er_backend**(HTTP 17700),前端不直接 spawn 任何推理进程。设置页面不再提供计算卡型号、嵌入模型、生成模型路径等配置入口,这些均由 er_backend Web 管理界面(端口 17700)配置。 +输入方式有三种:打字、语音输入(ASR)、**拍照提问**(用摄像头拍一张照片,先由多模态模型转成一句话描述,再接入同一套知识库检索流程,见"拍照提问"章节)。 + 本仓库从 monorepo(electronrag)通过 `scripts/sync-elec-rag-ui.sh` 单向同步而来,历史不保留。 ## 技术栈 @@ -31,6 +33,7 @@ src/ │ ├── index.ts # 注册所有 IPC 处理器 │ ├── indexHandler.ts # 文件选择、文档入库(分块→向量化→写入LanceDB) │ ├── queryHandler.ts # 查询(向量化→检索→流式生成),query:done 含 GeneratorStats +│ ├── visionHandler.ts # 拍照提问(图像理解→拼检索查询→复用检索+生成) │ ├── statusHandler.ts # 数据库状态、配置读写、测试连接(无 Ollama 分支) │ ├── erBackendHandler.ts # er_backend HTTP 17700 代理 IPC(启动/停止/状态) │ ├── hardwareHandler.ts # 硬件探测:hardware:get-detected / hardware:detected @@ -39,7 +42,7 @@ src/ │ ├── splitter.ts # 递归收集 .md/.txt,自实现递归字符分块 │ ├── embedder.ts # 向量化路由:local / llamacpp(无 Ollama 分支) │ ├── localEmbedder.ts # 本地 CPU 向量化(Transformers.js 单例,懒加载) -│ ├── generator.ts # 流式生成:仅 llamacpp(接受 llamacppServer 配置) +│ ├── generator.ts # 流式生成 + describeImage 图像理解,统一走 er_backend 代理 │ ├── hardwareDetector.ts # 探测 hm_smi / rpp-info 判断 er160/er25/none │ └── ollamaModels.ts # (保留文件,此项目不使用 Ollama) ├── preload/index.ts # contextBridge 暴露 electronAPI(无 llamaServer IPC) @@ -48,11 +51,13 @@ src/ ├── App.vue # 根布局:顶栏、主区;服务状态由 SSE 维护 ├── components/ │ ├── IndexPanel.vue # 文档入库面板 - │ ├── ChatPanel.vue # 问答气泡、Think 块、引用徽章、批量发送 + │ ├── ChatPanel.vue # 问答气泡、Think 块、引用徽章、批量发送、拍照入口 + │ ├── CameraCapture.vue # 拍照弹层:取景/拍照/重拍/切换摄像头(纯交互,不含业务逻辑) │ ├── ContextDrawer.vue # 检索片段抽屉 - │ ├── SettingsModal.vue # 设置抽屉(仅 RAG 参数 + 语音 + 批量测试) + │ ├── SettingsModal.vue # 设置抽屉(RAG 参数 + 语音 + 拍照开关 + 批量测试) │ └── ServiceControl.vue # 顶栏"启用/停止"按钮(走 er_backend HTTP) - ├── stores/rag.ts # Pinia store + ├── utils/tts.ts # TTS 播放器:流式分句播报、自适应语速、原生/Web 双后端 + ├── stores/rag.ts # Pinia store(含 visionAvailable:后端是否支持图像输入) └── types/index.ts # TS 接口 + Window.electronAPI 类型声明 ``` @@ -85,11 +90,17 @@ interface AppConfig { } tts: { enabled: boolean; voiceURI: string; pitch: number; rate: number; volume: number } } + vision: { + enabled: boolean // 拍照提问入口的总开关,默认关闭 + cameraDeviceId: string // 上次选用的摄像头,空=用第一个可用设备(该 id 可能失效,见"拍照提问") + } } ``` **注意**:本项目无 `chat` 字段(生成固定走 llamacppServer)。`embed.baseUrl` 字段存在但不对用户暴露,由 `applyPlatformOverrides` 固定推导。 +**改 `DEFAULT_CONFIG` 只对新配置生效**:`getConfig()` 是 `{ ...DEFAULT_CONFIG.rag, ...raw.rag }` 这样合并的,已部署设备上 `config.json` 里存过的字段会盖掉新默认值。所以调整默认系统提示词这类改动,已有设备不会自动生效,需要删掉 `config.json` 里对应字段(删掉后会回落到新默认值)或在设置页重新保存一次。 + ## applyPlatformOverrides 逻辑 `getConfig()` 末尾调用。**自 `836d25c` 起,推理请求(生成 + 嵌入)统一走 er_backend 代理**(`ER_BACKEND_URL = 'http://127.0.0.1:17700'`),不再由前端按 cardType 自己推导 llama-server/embed-server 端口——服务实际监听哪个端口完全由 er_backend 后端配置决定,前端只认这一个代理入口: @@ -126,6 +137,39 @@ NDrawer (width=900) **erBackendHandler.ts** 提供对应 IPC,渲染进程通过 `window.electronAPI.erBackend.*` 调用。 +## 拍照提问 + +用摄像头拍照代替打字提问。**不是把图片直接丢给模型问答,而是先把图像转成文字,再走同一套知识库检索流程**——这样答案依然基于手册、带引用标号,跟文字问答一致。 + +### 三段式流程(visionHandler.ts) + +``` +① 图像理解 describeImage(图片) → 一句话描述 caption 推送 vision-query:caption +② 检索 embedOne(caption + 用户问题) → search() 取 topK 推送 vision-query:context +③ 生成 streamGenerate(用户原始问题, chunks, systemPrompt) 推送 vision-query:delta/done +``` + +②③ 直接复用文字问答的 `embedOne`/`search`/`streamGenerate`,**没有平行实现一套**;图片只参与第①步,不会传给最终生成模型。事件通道与 `query:*` 平行(`vision-query:stage` / `:caption` / `:context` / `:delta` / `:done` / `:error`)。 + +两个刻意的取舍: + +- **检索用 `caption + 问题` 拼接,生成只用用户原始问题**。检索侧拼接是为了扩大召回;生成侧只用原问题是为了让回答聚焦。实测拍无关物体 + 问"如何更换前大灯",答案准确命中前大灯文档且没被图像描述带偏,证明这个分工是必要的。 +- **第①步用固定提示词而非用户问题**,且明确要求"一句话、不展开"。实测输出 45 tokens 左右、耗时 4.0–5.3 秒;对比让视觉模型直接完整作答的 454 tokens / 31 秒,差一个数量级。 + +### 视觉能力探测(ServiceControl.vue → store.visionAvailable) + +后端加载的不是多模态模型时,拍照按钮置灰;换回多模态模型后下一轮轮询(5 秒)自动恢复,不用重启应用。判据顺序: + +1. 日志里的 `allow_image = 1` —— 最准确,但**er_backend 日志缓冲只留最近 200 条,服务跑一会儿这行就被推理日志挤掉**,所以只在还翻得到时采信 +2. 启动命令 `lastCommand` 含 `--mmproj` —— 不随日志滚动,长期可靠的兜底 + +**不要改用 `config.generate.modelType === 'vlm'`**:那是配置意图,用户改了配置但没重启服务时与实际运行的模型不符。 + +### 摄像头的两个坑 + +- **deviceId 跨会话会失效**:Web 标准不保证 `deviceId` 稳定,配置里存的旧 id 换会话后常常已不存在,拿它做 `getUserMedia` 的 `exact` 约束会抛 `OverconstrainedError`,整个拍照功能不可用。`CameraCapture.vue` 的 `openStream()` 因此带了回退:指定 id 打不开就退回默认摄像头,并把实际拿到的设备 id 回写。 +- **画面倒置会影响识别质量,不只是观感问题**:这批设备摄像头原始画面是倒的。实测把未旋转的原始帧送去识别,模型描述里会明确写"倒置的室内场景"——它把方向当成了图像特征。所以预览的 CSS 旋转和 `capture()` 里 canvas 的旋转**必须成对出现**,只转预览会变成"看着是正的、存下来的图是倒的"。 + ## 关键技术约定 ### LanceDB 余弦检索(lancedb.ts) @@ -150,6 +194,26 @@ const vectors = await Promise.all(texts.map((t, i) => fetchOne(t, i))) ``` 每条 60 秒超时,失败重试 2 次。 +### 语音输出:流式播报(utils/tts.ts) + +答案边生成边播报,不等整段生成完。`ChatPanel` 在 `onDelta` 里实时 `ttsPlayer.append(delta)`,`onDone`/`onError` 里 `flush()` 收尾;`TtsPlayer` 内部按句子边界切分入队,播放跟不上文字时会自适应降速。 + +三个踩过的坑: + +- **`append()` 里必须是 `this.buffer += pending` 而不是 `=`**。LLM 流式增量很碎(常几个字一个 chunk),用赋值会把上一次还没凑够一句、尚未送去朗读的残留直接冲掉,实际效果是整段正文都丢了、只念出 `flush()` 时残留的一个句号。 +- **加粗文本会被念两遍**,根因是 `markdownToPlain()` 每次都从头重新解析整段 `rawText`,而 `**` 闭合前后同一段文字的纯文本结果不一样(未闭合时星号当普通字符,闭合后星号被去掉)。提前把这段还不稳定的文字记为"已朗读",闭合符号一到就会被当成新内容重念。`findStableBoundary()` 因此只处理到最后一个已闭合的 `**` 为止,未闭合的尾巴留到下次;`flush()` 时生成已结束,不再有这顾虑,直接处理完。 +- **"开始播报"提示音要单独入队**(`announceStart()`),不能碰流式缓冲区状态。 + +### 系统提示词:查不到内容时的答复 + +提示词要求"说明手册中暂未查到,并给一条可操作的建议",而不是硬邦邦一句"我不知道"。 + +**光在提示词里写"每次换一种措辞"是无效的**——模型每次请求都是无状态的,不知道上次说了什么,实测同样输入连发三次答复一字不差,而且会照抄提示词里给的示例。所以措辞变化由代码侧提供:`generator.ts` 的 `withToneHint()` 每次请求随机挑一条引导追加到系统提示词末尾,让输入本身产生差异。 + +**没有采用调高 temperature 的做法**:那会同时影响命中文档的正常回答,而维修手册问答更看重稳定准确。 + +另外,`queryHandler`/`visionHandler` 里 `chunks.length === 0` 的兜底文案说的是"知识库还没有文档,请先导入"而不是"我不知道"——`search()` 是全表余弦排序后截断,只要库里有文档就一定返回结果,走到那个分支只可能是库为空。 + ### Vue Proxy IPC 序列化 Pinia 响应式对象直接传给 `ipcRenderer.invoke()` 会触发结构化克隆错误。**所有传入 IPC 的响应式对象必须先序列化**: diff --git a/README.md b/README.md index 35143b8..727a090 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ **仅支持 Linux(银河麒麟 Kylin V10 / Ubuntu)**,适配 ER160(揽月 M50 NPU)和 ER25(芯动力 RPP)计算卡。服务进程管理委托给 [er_backend](https://git.drumtm.com/EdgeRhythm/er_backend)(HTTP 17700),前端专注 RAG 逻辑与用户交互。 +## 功能 + +- **文档问答**:导入 Markdown/文本手册,提问后检索相关片段并生成带引用标号的回答,点引用可查看原文片段 +- **语音输入**:Paraformer 流式识别,说话即转文字填入输入框 +- **语音输出**:边生成边播报,不用等整段答案写完 +- **拍照提问**:用摄像头拍下实物,先由多模态模型转成一句话描述,再接入同一套知识库检索——所以答案同样基于手册内容、带引用,而不是模型凭空描述图片。需要 er_backend 当前加载的是多模态模型,否则按钮会自动置灰(换回多模态模型后自动恢复) +- **批量测试**:按预设问题列表循环提问,用于压测和效果回归 + ## 技术栈 - **框架**:Electron + electron-vite + Vue3 + TypeScript @@ -37,8 +45,9 @@ elec_rag_ui/ │ ├── renderer/ # Vue3 前端 │ │ └── src/ │ │ ├── App.vue # 根布局(顶栏 + 主区) -│ │ ├── components/ # ChatPanel / IndexPanel / SettingsModal 等 +│ │ ├── components/ # ChatPanel / IndexPanel / CameraCapture / SettingsModal 等 │ │ ├── stores/ # Pinia store +│ │ ├── utils/ # TTS 播放器、音频采集、Markdown 处理 │ │ └── types/ # TypeScript 类型定义 │ └── batch_questions.txt # 批量测试问题示例 ├── models/ # 本地模型(onnx 由 Git LFS 管理) @@ -67,6 +76,15 @@ systemctl status er-backend er_backend 管理界面:`http://:17700`(配置模型路径、启动/停止服务) +## 启用拍照提问 + +需要两个条件同时满足: + +1. **er_backend 加载的是多模态模型**——在管理界面把生成模型配成带 mmproj 的视觉模型(启动命令里应带 `--mmproj`)。应用会自动探测:不是多模态模型时拍照按钮置灰,换回多模态模型后 5 秒内自动恢复可用,不用重启应用。 +2. **在「设置 → 基本设置 → 拍照提问」里打开开关**(默认关闭,因为不是所有部署都会配视觉模型)。 + +摄像头用标准 USB UVC 设备即可,无需额外驱动。有多个摄像头时,拍照弹层右上角可以切换。 + ## 模型文件 `models/` 目录中的 `.onnx` 文件通过 **Git LFS** 管理。克隆后拉取实体文件: