# zm-rag 未提交改动审查记录

审查日期：2026-03-18

审查范围：当前未提交的 Guide、ACL、Research、前端导航相关改动。

## 结论

本轮未提交代码里，我确认了 3 个高置信度问题，优先级依次是：

1. Guide 抽取失败分支被错误记成 skipped，并会删除已有 Guide 数据。
2. Guide 引用和普通文档引用都按 doc_id 去重，导致两类证据无法共存。
3. 深度研究里同时带 explicit docs 和 matter scope 时，Guide 检索被错误收紧成交集过滤。

除这 3 项外，我没有再记录更多高置信度 findings；其余看起来更像实现取舍、低风险防御不足，或者需要额外产品语义才能定性。

## Findings

### 1. High: Guide 抽取失败会被错误标记为 skipped，并清掉已有结构化结果

涉及位置：

- [backend/app/core/ingest_pipeline.py](../backend/app/core/ingest_pipeline.py#L1149-L1167)
- [backend/app/core/service_guide_extractor.py](../backend/app/core/service_guide_extractor.py#L177-L222)

问题说明：

- 抽取器已经把“识别到标准办事指南，但结构化抽取失败”定义成 detected=true 且 profile=None。
- 但入库阶段没有区分这个分支，而是统一落成 service_guide_status = skipped，并记录“未识别为标准办事指南，跳过结构化抽取”。
- 同一分支里还会执行 delete_service_guides_by_doc_id(doc_id)，把该文档已有的 Guide 结构化结果删掉。

影响：

- 一次临时的抽取异常、字段校验失败，都会把原本已经可用的 Guide 数据删掉。
- doc meta、trace、后续运营排查都会把这类失败误判成“不是 Guide”，导致状态语义回退。
- 这和抽取器已经建立好的 detected=true, profile=None 契约不一致。

建议：

- 对 detected=true 且 profile=None 单独保留失败状态，例如 failed 或 degraded。
- 这类分支不要复用“未识别为标准办事指南”的 summary。
- 除非明确确认文档已经不再是 Guide，否则不要在该分支删除历史 Guide 文档。

### 2. High: Guide 引用和普通文档引用都只按 doc_id 去重，导致 Guide 证据在常见场景下被吞掉

涉及位置：

- [backend/app/core/research_formatter.py](../backend/app/core/research_formatter.py#L73-L82)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L747)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L951)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L1195)
- [frontend/src/stores/qa.ts](../frontend/src/stores/qa.ts#L147-L153)
- [frontend/src/stores/research.ts](../frontend/src/stores/research.ts#L451-L457)

问题说明：

- Guide profile 和它的原始文档天然共享同一个 doc_id。
- 后端在输出 reference_docs 时先把 all_docs 和 guide_docs 按 doc_id 合并去重，Guide 命中会被普通搜索/显式纳入/图谱补证命中的同 doc 文档覆盖掉。
- 即使后端后续修成允许两条引用并存，前端 QA/Research store 也仍然会再次按 doc_id 合并，把后一条覆盖到前一条上。

影响：

- 用户在最常见的“既命中文档，又命中对应 Guide”的场景下，看不到 source_group=guide 的引用卡片。
- Guide 专有的 profile_id 路由、字段级标签、Guide 说明文案都会一起丢失。
- 表面上 LLM 上下文已经吃到了 Guide 结构化证据，但用户侧引用展示却退化成普通文档引用，和本轮前端增强目标相冲突。

建议：

- 引用唯一键至少要区分 doc_id + source_group；Guide 还应纳入 profile_id。
- 后端 reference_docs 合并策略不要把 Guide 视为普通 doc 的重复项。
- 前端 store 的 addReference 也要同步改唯一键，否则后端修完仍会被前端覆盖。

### 3. Medium: 深度研究同时传入 explicit docs 和 matter_ids 时，Guide 检索被错误收紧成交集

涉及位置：

- [backend/app/infrastructure/es_client.py](../backend/app/infrastructure/es_client.py#L847-L890)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L388-L400)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L496-L499)
- [backend/app/core/research_engine.py](../backend/app/core/research_engine.py#L709-L712)

问题说明：

- search_service_guides 在同时收到 doc_ids 和 matter_ids 时，会把两者都放进 filter 子句里。
- 这等价于“Guide 文档必须同时满足指定 doc_id 和 linked_matter_ids”，实际是交集语义。
- 深度研究场景里，如果用户显式纳入的是普通文档，而研究范围同时带了事项 ID，这个交集通常为空。
- 当前 fallback 只去掉了 matter_ids，没有去掉 doc_ids，因此仍然会继续漏召回。

影响：

- 用户一旦同时带显式资料和事项范围，Guide 结构化证据会比预期更容易消失。
- 这会直接削弱 Research 里 Guide 作为“补充证据源”的效果，而且问题只会出现在复合输入场景，靠手工点查不容易发现。

建议：

- 明确 doc_ids 和 matter_ids 的组合语义；如果目标是“任一约束命中即可纳入候选”，就应该改成并集或分两路召回再合并。
- fallback 至少要有一轮彻底去掉 doc_ids 的重试，否则当前重试没有真正放宽约束。

## 测试缺口

以下分支当前没有看到对应回归覆盖：

- [backend/tests/test_ingest_pipeline_service_guide_unit.py](../backend/tests/test_ingest_pipeline_service_guide_unit.py)
  目前只覆盖了 completed 路径，没有覆盖 detected=true 且 profile=None 的入库语义。

- [backend/tests/test_research_engine_unit.py](../backend/tests/test_research_engine_unit.py)
  目前没有覆盖“同一个 doc_id 同时作为普通文档命中和 Guide 命中”的引用展示场景。

- [backend/tests/test_research_engine_unit.py](../backend/tests/test_research_engine_unit.py)
  目前也没有覆盖“deep research 同时带 explicit docs 和 matter_ids”时的 Guide 召回组合语义。

## 建议处理顺序

1. 先修 Finding 1。它会直接影响数据状态正确性，而且会删已有 Guide 数据。
2. 再修 Finding 2。它会让本轮 Guide 前端曝光能力在常见查询里失效。
3. 最后修 Finding 3，并补组合场景回归。