160 lines
12 KiB
Markdown
160 lines
12 KiB
Markdown
# 阶段 F:Embedding 与知识库问题与解决方案
|
||
|
||
> 记录日期:2026-09-04。涉及分支:`feat/multimodal-pipeline`,前序功能提交:`6eb97bf`。
|
||
> 本文按既有复盘格式记录原因、后果、解决思路、实际方案和验证结果。修复随当前分支提交;合并状态以 Git 与 PR 记录为准。
|
||
|
||
## 1. 背景
|
||
|
||
阶段 F 将占位向量替换为真实本地 Embedding,并加入 API 路由、CPU/CUDA 模型子进程、转写笔记和聊天知识库上下文。联调问题跨越运行环境、索引、持久化与本地处理约束,不能仅根据“模型已下载”判断整条链路正常。
|
||
|
||
```text
|
||
前端 / 后续 Tauri WebView → 后端 API → 模型路由
|
||
→ 带模型空间标识的向量索引 → 知识检索 / 聊天来源
|
||
```
|
||
|
||
## 2. 问题总览
|
||
|
||
| 编号 | 问题 | 后果 | 实际方案 |
|
||
| --- | --- | --- | --- |
|
||
| F-01 | 配置、推理与索引错误共用提示 | 已有模型却被提示未配置 | 区分推理错误与索引错误 |
|
||
| F-02 | Windows 热重载事件循环不支持异步子进程 | 命令行成功,HTTP 失败 | 线程管道子进程兼容路径 |
|
||
| F-03 | 重建忽略向量失败,异常游标未关闭 | 虚报成功或阻塞重建 | 严格校验、回滚和显式关闭 |
|
||
| F-04 | 搜索记录仅保存在内存或浏览器 | 刷新丢失,桌面无法统一管理 | SQLite 历史与 API |
|
||
| F-05 | 聊天忽略 `use_rag` | 没有知识库内容 | 真实 Block 上下文与来源事件 |
|
||
| F-06 | 本地转写导出未传递限制 | 正文可能发送给远程 Embedding | 持久化本地索引标记 |
|
||
| F-07 | 推理结束才读取当前模型标识 | 向量与空间错配 | 冻结模型、revision 和设备配置 |
|
||
| F-08 | 将不同处理策略误判为配置漂移 | 普通与仅本地笔记共存时不能重建 | 按策略校验覆盖,独立检索并融合排名 |
|
||
| F-09 | 简单字符串比较忽略 YAML 语法 | 注释等合法写法可能关闭本地限制 | 解析 YAML 节点,非法策略明确拒绝 |
|
||
| F-10 | 迁移 DDL 与版本号分开提交 | 中断后重启报重复列 | 原子迁移、并发重检及旧半迁移恢复 |
|
||
|
||
## 3. F-01 / F-03:模型可用不等于索引可用
|
||
|
||
### 原因与后果
|
||
|
||
`embed_remote` 捕获异常后返回 `None`,上层将调用失败与索引缺失统一显示为“请配置 Embedding”。默认库曾有 29 个 Block 和模型元信息,但没有向量侧表。普通保存允许降级保留正文,这一策略又被用于严格重建,导致没有向量也可能报告完成。异常回溯保留查询游标时,还会影响后续写事务。
|
||
|
||
### 解决思路与实际方案
|
||
|
||
纯向量查询使用严格错误处理:模型失败保留安全错误码;模型可用但索引缺失时返回 `SEMANTIC_INDEX_UNAVAILABLE`,明确提示重建。混合检索仍可退到全文检索。重建先准备向量,再进入事务,检查空间一致和完整覆盖,失败保留旧索引。查询游标在 `finally` 中关闭。
|
||
|
||
前序真实本地验证:重建后 29 个 Block 对应 29 条向量,查询返回 20 条结果。这是当时样本库的历史记录,不代表全部环境与规模。
|
||
|
||
## 4. F-02:Windows 热重载下本地模型无法启动
|
||
|
||
### 原因与后果
|
||
|
||
Windows 下 `uvicorn --reload` 使用的事件循环可能不支持 `asyncio.create_subprocess_exec`,抛出 `NotImplementedError`。模型与依赖均已安装,普通 `asyncio.run` 冒烟成功,但实际 HTTP 请求失败。第一轮只修提示和索引,未覆盖此启动方式。
|
||
|
||
### 实际方案与验证
|
||
|
||
优先保留异步子进程,仅在不支持时使用 `ThreadedProcess`。同步创建进程以避免取消时失去进程归属;管道读写与等待在线程中执行,保留输出上限、隐藏窗口、超时、取消和回收逻辑。
|
||
|
||
修复后通过前端实际连接的 `/api/search` 验证成功;测试覆盖真实小子进程的结果读取与取消回收。重新安装权重不能解决此类事件循环兼容问题。
|
||
|
||
## 5. F-04 / F-05:搜索记录与聊天知识库
|
||
|
||
### 原因与后果
|
||
|
||
历史最初包含硬编码示例并只在内存更新;第一轮改成 `localStorage` 虽解决刷新丢失,却不符合后续 Tauri 统一管理应用数据的要求。聊天只有 `use_rag` 字段,没有执行检索。
|
||
|
||
### 实际方案
|
||
|
||
SQLite v5 增加 `search_history`,保留最近 10 条去重查询,重复项置顶。记录属于配置的 `APP_DB_PATH`,Tauri 可复用后端;这不等于已实现 Rust 原生存储。此前浏览器记录没有自动迁入 SQLite。
|
||
|
||
| 方法 | 路径 | 行为 |
|
||
| --- | --- | --- |
|
||
| POST | `/api/search` | 记录提交的非空查询,再执行检索 |
|
||
| GET | `/api/search/history` | 返回 `{"queries": [...]}`,最近项在前 |
|
||
| DELETE | `/api/search/history` | 清空历史,返回空数组 |
|
||
|
||
前端通过 API 加载与清空,失败显示错误。聊天开启知识库时最多取 6 个来源,每段正文最多 3000 字符、合计最多 12000 字符;资料明确标为非指令,SSE 返回 `Citation` 供定位。关闭时不附加笔记,无命中时不生成来源。请求与事件测试不等于外部模型回答质量验收。
|
||
|
||
## 6. F-06:仅本地转写的笔记索引
|
||
|
||
### 原因与后果
|
||
|
||
`create_transcript_note` 调用通用 `create_note`,后者默认执行 API Embedding。转写本身遵守 `local_only`,导出索引却可能上传正文。只增加临时调用标记也无法覆盖后续重建。
|
||
|
||
### 实际方案
|
||
|
||
仅本地任务新生成的 Markdown 写入 frontmatter:
|
||
|
||
```yaml
|
||
---
|
||
embedding_local_only: true
|
||
---
|
||
```
|
||
|
||
解析器读取标记,索引向路由传入 `local_only=True`,跳过远程绑定与凭据解析。普通笔记保持 API 优先和本地回退。本地向量失败时,普通保存仍可保留正文与全文索引;严格重建报错并保留旧索引。
|
||
|
||
标记随 Vault 持久化,编辑保留标记和重建时继续生效。此标记约束 Embedding,不是笔记的通用联网权限;显式开启远程聊天知识库仍可能提供相关片段。旧导出笔记不会自动补标记,必要时应人工补入;删除标记恢复普通索引路由。
|
||
|
||
## 7. F-07:异步推理中的模型空间一致性
|
||
|
||
### 原因与后果
|
||
|
||
请求开始使用 Bekko,推理期间切到 Granite,结束时重新读取 `model_id` 就可能把旧向量标为新模型。两者都是 384 维,维度校验无法发现错误。
|
||
|
||
### 实际方案
|
||
|
||
进入本地路径时调用 `LocalEmbedding.snapshot()` 复制模型和设备配置。通过请求级 `ContextVar` 将相同配置传给 Runtime,结束或异常时恢复上下文;返回模型标识取自同一快照,下一请求使用新设置。
|
||
|
||
快照仅在实际进入本地路径时创建,避免正常 API 请求额外依赖本地配置。API 的 URL、模型、维度和请求扩展冻结规则保持不变。
|
||
|
||
## 8. 回退行为与验证
|
||
|
||
| 场景 | 预期 |
|
||
| --- | --- |
|
||
| 普通请求,API 有效 | 使用 API,不执行本地推理 |
|
||
| 普通请求,无 API | 使用本地模型 |
|
||
| 普通请求,API 失败或响应无效 | 回退本地,保留 `fallback_reason` |
|
||
| 本地限定索引,存在 API | 不请求 API、不解析远程凭据 |
|
||
| 本地限定后再发普通请求 | API 仍可调用,不泄漏临时限制 |
|
||
| 推理期间修改设置 | 当前向量与身份一致,下一请求采用新设置 |
|
||
| 普通与仅本地笔记共存 | 分区重建,各自空间内检索,再融合排名 |
|
||
| 同一策略内空间漂移或覆盖不完整 | 严格重建拒绝提交,整体回滚 |
|
||
|
||
### F-08:混合处理策略的重建与检索
|
||
|
||
提交审阅时用隔离数据库复现:一篇普通 API 笔记与一篇本地限定笔记共存,配置未变化,全量重建仍返回 `EMBEDDING_SPACE_CHANGED`。原因是重建将全部笔记约束到一个空间,查询也要求单个空间覆盖全部 Block,未区分处理策略。
|
||
|
||
本轮追加 SQLite v6,为 Block 保存 `embedding_local_only` 策略。重建分别检查普通和仅本地策略的空间一致性及完整覆盖,仍在同一事务提交;同一策略内模型改变、缺失向量或存储失败仍整体回滚。正常 API 回退不受跨策略差异影响。
|
||
|
||
查询按策略生成对应查询向量,在同一个数据库快照中检查两个分区。各分区独立计算相似度,再用 RRF 融合排名,不直接比较不同模型的向量或余弦分数。仅含本地限定笔记时,查询也不请求远程 Embedding;分区失效时纯向量明确报错,混合查询仍可退到全文。
|
||
|
||
已有数据库升级后应重建一次索引,将 Vault 中的策略标记同步至 Block。新增和更新笔记自动同步。回归覆盖混合策略、全部本地回退、仅本地查询、单分区缺失和跨分区写入失败回滚;此项合并阻碍已修复。
|
||
|
||
本轮在 `backend/` 执行:
|
||
|
||
```powershell
|
||
.venv/Scripts/python.exe -m pytest tests/test_model_routing.py tests/test_media_jobs.py tests/test_routed_retrieval.py tests/test_local_models.py -q -p no:cacheprovider
|
||
```
|
||
|
||
结果:132 项通过,覆盖 API 回退、本地限定导出和重建、模型切换及 Windows 子进程路径,不调用真实外部 API;有既有 Starlette/httpx 弃用提示。
|
||
|
||
前序持久化修复记录:后端搜索历史、聊天与媒体相关 9 项,前端搜索与聊天 5 项及类型检查通过。各轮结果是针对性验证,不相加当作全仓测试数。
|
||
|
||
## 9. 工程经验
|
||
|
||
### F-09:本地限制标记的 YAML 解析
|
||
|
||
再次审阅复现:`embedding_local_only: true # keep local` 被旧字符串比较解析为 `False`。加注释没有改变用户意图,却可能使保存或重建发送正文到远程 Embedding。
|
||
|
||
实际方案:使用 PyYAML SafeLoader 解析 frontmatter 节点,不构造任意对象;读取布尔节点,支持注释、带引号的键、缩进、多行布尔值和布尔锚点。普通的 `true/false` 与 YAML 布尔别名 `yes/no/on/off` 均按布尔值处理。字符串 `"true"`、数字、空值、非法值及重复声明返回 `INVALID_EMBEDDING_POLICY`,不静默转为普通索引。
|
||
|
||
无效 YAML、非映射 frontmatter 和 YAML 合并键也明确拒绝;合并键应展开为显式声明,以避免遗漏继承的限制。标题与标签的既有提取方式保持不变。回归包含添加行尾注释后保存笔记、重建仍只走本地索引。
|
||
|
||
### F-10:数据库迁移中断恢复
|
||
|
||
旧 `executescript` 先提交 DDL,随后才写入 `schema_migrations`。若 v6 新增列后中断,重启会再次执行 `ALTER TABLE`,产生 `duplicate column name: embedding_local_only`。
|
||
|
||
实际方案:用 SQLite 的完整语句检测拆分静态迁移脚本,逐句执行,避免 `executescript` 隐式提交。每个版本在 `BEGIN IMMEDIATE` 事务内执行 DDL 和版本写入;异常包括中断均回滚。获取写锁后重新检查版本,防止多个连接重复迁移。连接初始化失败时主动关闭连接。
|
||
|
||
兼容恢复仅针对旧版已发生的 v6 半迁移:确认现有列为预期的 `INTEGER NOT NULL DEFAULT 0` 后补记版本,不再重复新增列;形状不符合预期则报错,不擅自更改数据。测试注入写版本失败和中断,验证列与版本同时回滚、重新连接升级成功,并覆盖旧半迁移与并发连接升级。
|
||
|
||
F-09/F-10 修复后完整后端回归:516 项通过,新增 21 个参数化用例;仍仅有既有 Starlette/httpx 弃用提示。测试均使用隔离数据,不调用外部模型 API。
|
||
|
||
F-08 修复后完整后端回归:495 项通过;新增 5 个用例覆盖混合策略重建与查询、全部本地回退、仅本地查询不访问 API、跨分区回滚和不完整分区。现有同策略空间漂移拒绝用例仍通过。
|
||
|
||
区分配置、权重安装、推理运行、索引覆盖四种状态;按用户实际启动方式验证;跨异步边界冻结身份;持久化处理限制;增加限制时也验证普通 API 回退没有被破坏。
|