feat(knowledge): Executor 行动记忆 + 归一化质量等级解决 ISS-002 重复检索

- RetrievedDocTracker 升级为域级+文档级双层记录(Map<sessionId, Map<domain, Set<filePath>>>)
- LookupKnowledgeTool 新增 Min-Max 归一化层(BGE-M3 L2 距离→[0,1] similarity)
- 三等级 relevanceLevel:PRECISE / HIGHLY_RELEVANT / REFERENCE + completenessHint 兜底信号
- LookupResult 新增 relevanceLevel、completenessHint、retrievedDomainsThisSession
- Executor prompt 重写:4 条检索约束 + 合法出口不查全不追责,重复检索才惩罚
- 入库可观测性:V010 迁移 + retrieval_details JSON 扩展
- 归档 executor-action-memory-relevance change
This commit is contained in:
zhuyongxin
2026-07-01 18:24:41 +08:00
parent e4f37cb9e6
commit e438df4355
21 changed files with 1076 additions and 88 deletions
+1
View File
@@ -11,3 +11,4 @@
| 2026-06-26 | session-storage | 会话存储/可观测 | diagnosis_session, agent_step, tool_invocation, token追踪, 多Agent路由 | openspec/changes/session-storage | archived |
| 2026-06-29 | confidence-feedback | 质量评估/反馈机制 | evidence_score, selfEvaluation, feedback, useful, not_useful, case_library, BAD_CASE, tool_invocation规则引擎, 反馈按钮, sessionId回传 | openspec/changes/confidence-feedback | archived |
| 2026-06-30 | session-dedup-knowledge-map | 去重/知识图谱 | RetrievedDocTracker, KnowledgeDomainService, knowledge_domain, covers, whenToRetrieve, Planner注入, ISS-001 | openspec/changes/session-dedup-knowledge-map | archived |
| 2026-07-01 | executor-action-memory-relevance | 检索质量/行动记忆 | relevanceLevel, completenessHint, Min-Max归一化, RetrievedDocTracker域级记录, Executor检索约束, ISS-002 | openspec/changes/archive/2026-07-01-executor-action-memory-relevance | archived |
@@ -0,0 +1,70 @@
# Acceptance: executor-action-memory-relevance
## 分档
standard
## 任务完成状态
| 任务 | 状态 | 说明 |
|------|------|------|
| T1: RetrievedDocTracker 域级升级 | ✅ 完成 | 双层 Map 结构,域级+文档级记录 |
| T2: LookupResult 新增字段 | ✅ 完成 | relevanceLevel / completenessHint / retrievedDomainsThisSession |
| T3: 归一化计算逻辑 | ✅ 完成 | Min-Max 归一化 + 三等级判定 |
| T4: LookupKnowledgeTool 集成 | ✅ 完成 | 归一化层 + 行动记忆注入 + 域拦截 |
| T5: Executor Prompt 重写 | ✅ 完成 | 4 条检索约束,无 knowledge map |
| T6: 入库可观测性 | ✅ 完成 | V010 + Entity + JSON 扩展 |
| T7: BGE-M3 归一化验证测试 | ✅ 完成 | 范数=1.00000002,测试通过 |
## 静态验证
- [x] **语法/编译检查**: 所有 Java 文件编译通过
- [x] **Impact Analysis**: LookupKnowledgeTool、RetrievedDocTracker 变更范围经 `gitnexus_impact` 检查,均为 L2 内部接口影响
- [x] **Cross-artifact 对齐检查**: brief → proposal → design → specs → tasks 闭环,无 gap
- [x] **Prompt 约束检查**: chat-executor-prompt.md 不包含 knowledge map,包含 4 条检索约束
## 脚本验证
- [x] **V010 Flyway 迁移**: 迁移成功,`relevance_level` 和 `dedup_reason` 列已添加
```sql
ALTER TABLE tool_invocation
ADD COLUMN relevance_level VARCHAR(20),
ADD COLUMN dedup_reason VARCHAR(32);
```
- [x] **FullPipelineSmokeTest**: BGE-M3 归一化测试通过(范数=1.00000002)
- [x] **数据库数据校验**:
- `relevance_level` 列已写入 HIGHLY_RELEVANT / REFERENCE
- `dedup_reason` 列已写入 doc_retrieved / null
- `retrieval_details` JSON 包含 l1_top_similarity、completeness_hint、retrieved_domains、dedup_reason
## 浏览器/人工验证
- [x] **应用启动验证**: Spring Boot 应用正常启动,端口 9900
- [x] **Chat API 调用验证**: 通过 curl 测试 chat 接口,lookup_knowledge 调用链完整
```
curl -X POST "http://localhost:9900/api/chat/send" \
-H "Content-Type: application/json" \
-d '{"sessionId": "b66d799e", "question": "..."}'
```
- [x] **日志验证**: 应用日志可观察到 relevanceLevel、retrievedDomainsThisSession 输出
- [x] **归一化数学验证**: l1_top_score=0.383 → l1_top_similarity=0.8085(`1 - 0.383/2.0 = 0.8085`)✅
- [x] **域追踪验证**: `[infrastructure]` → `[infrastructure, api]` 域列表正常扩展
## 未验证
| 场景 | 原因 | 风险 | 补验建议 |
|------|------|------|---------|
| PRECISE 等级(L0 唯一精确匹配) | 测试会话无精确匹配场景 | 低 — L0 matchCount=1 的判断逻辑与 HIGHLY_RELEVANT 共用,实现确定性强 | 构造一条 L0 精确匹配的知识库文档后测试 |
| domain_retrieved 域级去重 | 需要同一域全部文档已检索再查该域才触发 | 低 — isDomainRetrieved 逻辑简单,与 isDocRetrieved 等价 | Phase 2 启用域级硬限流时测试 |
| DEDUPED 等级 | 当前 code path 去重时仍写 REFERENCE,DEDUPED 未被使用 | 低 — 设计预留,当前未启用 | Phase 2 若启用 DEDUPED 等级时验证 |
| Phase 2 域级硬限流 | 非本次范围 | 中 — 当前仅有软约束(prompt),LLM 仍可能在 REFERENCE 下继续检索 | 实测观察,如果 lookup 调用仍偏高,启动 Phase 2 |
## 剩余风险
1. **Prompt 软约束局限性**:实测 10 次调用中 9 次为 REFERENCE,说明 LLM 仍倾向于继续检索。如果 prompt 约束效果不足,需启用 Phase 2 域级硬限流。
2. **L1 Metadata 解析兼容性**:L1 domain 兜底路径解析 metadata JSON,如果知识库文档 frontmatter 格式不一致可能解析失败,已有 try-catch 兜底。
## 归档状态
- [ ] OpenSpec change 尚未归档
- [ ] devflow/index.md 状态为 `implemented`,待改为 `archived`
@@ -0,0 +1,35 @@
# Brief: executor-action-memory-relevance
## 背景
ISS-002:Executor 在单次会话中调用 `lookup_knowledge` 20+ 次,大部分是同域换变体的冗余调用。前序 change `session-dedup-knowledge-map` 解决了文档级重复召回(ISS-001),但未解决 Executor 重复调用问题。
## 目标
- Executor 获得行动记忆(知道自己本次会话已检索了哪些域)
- 检索结果提供归一化质量等级(PRECISE/HIGHLY_RELEVANT/REFERENCE)+ 兜底信号
- Executor prompt 提供明确的检索约束和"放弃检索"的合法出口
- 原始分数入库保留可观测性,但不暴露给 LLM
## 范围
- `RetrievedDocTracker`:域级 + 文档级双层记录
- `LookupKnowledgeTool`:归一化层 + 行动记忆注入
- `LookupResult`:新增 relevanceLevel / completenessHint / retrievedDomainsThisSession
- `chat-executor-prompt.md`:检索约束重写
- `ToolInvocation` + V010:入库可观测性
## 非目标
- 不给 Executor 注入 knowledge map(保持 Agent 边界)
- 不修改 Planner prompt 或 Planner 逻辑
- 不修改 PrimaryResult / SupplementResult 的字段(不暴露原始分数)
- Phase 2 域级硬限制暂不实施
## 分档
standard
## 关联 OpenSpec change
openspec/changes/executor-action-memory-relevance
@@ -0,0 +1,83 @@
# Decisions: executor-action-memory-relevance
## 过程日志
### Clarify 阶段
**入口摘要**:ISS-002 Executor 无约束重复调用 lookup_knowledge(单会话 20+ 次),需要行动记忆 + 归一化质量等级 + prompt 约束来解决。
**slug**: `executor-action-memory-relevance`
**规模分档**: `standard`(涉及 7 个文件,跨 DTO/工具层/持久化/Prompt,有设计决策需澄清)
### Context 阶段
**devflow/index.md 使用状态**: 已命中。前序 change `session-dedup-knowledge-map`(archived)提供了 RetrievedDocTracker、KnowledgeDomainService、ISS-002 文档。
**相关 ADR**: 无直接 ADR,但 `session-dedup-knowledge-map` 的 decisions.md 和 evidence.md 记录了文档级去重和 knowledge map 注入的决策。
**不能违反的历史决策**:
1. RetrievedDocTracker 的文档级去重必须保留
2. knowledge map 只注入 Planner,不注入 Executor(本次讨论确认)
3. L0/L1 原始分数不暴露给 LLM,只在归一化层内部使用(本次讨论确认)
**需进入 OpenSpec 的上下文点**:
1. L1 score 是 L2 距离(值域 [0,+∞)),不是归一化分数——阈值设计需基于实际分布
2. L0 的 category 可从 KnowledgeEntry.getCategory() 直接获取;L1 需解析 metadata JSON
3. ReactAgent 是自主决策工具调用的 Agent,Prompt 约束是软约束
### Grill 阶段 — Question Pool
**维度:术语**
1. [evidence-driven] `relevanceLevel` 三个等级(PRECISE/HIGHLY_RELEVANT/REFERENCE)的边界是否清晰,是否存在 LLM 误解的可能? → **已查证**:三个等级语义明确,PRECISE=唯一匹配、HIGHLY_RELEVANT=高分命中、REFERENCE=低置信度参考。LLM 理解风险低。
**维度:边界**
2. [evidence-driven] L1 score 是 L2 距离(值域 [0,+∞)),当前代码无阈值判断。归一化阈值如何设计? → **已查证**:L2 距离典型范围取决于 BGE-M3 1024 维 embedding 的尺度,需从 `tool_invocation.retrieval_details` 中查询实际 `l1_scores` 分布才能定阈值。当前先以常量定义,标记为"需实测校准"。
3. [evidence-driven] L1 结果的 category 提取需要解析 metadata JSON 字符串,当前 `SearchResult.metadata` 是 `toString()` 的结果。归一化层是否需要 L1 的 domain? → **已查证**:L1 的 domain 主要用于 RetrievedDocTracker 的域级记录。如果 L0 已命中且包含 category,可直接用 L0 的 category;如果仅 L1 命中,需解析 metadata 提取 category。当前知识库中 L0 大概率先命中,L1 domain 提取作为兜底路径。
4. [user-interview] 归一化阈值(L1 score 分界线)在实测数据不足时,是否接受先用保守初始值 + 后续调优的策略? → **用户待确认**
**维度:验收**
5. [evidence-driven] 现有 `tool_invocation` 表 `retrieval_details` JSON 中 `l1_scores` 存的是 L2 距离原始值,新增的 `relevance_level` 和 `completeness_hint` 入库后是否需要回填历史数据? → **已查证**:不需要回填历史数据,新列 nullable 即可,历史记录 relevance_level=null。
### Grill 结论
**evidence-driven 汇报**:
- E1: relevanceLevel 三等级语义清晰,LLM 误解风险低
- E2: L1 score 是 L2 距离,值域不固定,阈值需实测校准
- E3: L0 category 直接可用,L1 category 需解析 metadata(兜底路径)
- E4: 历史数据不回填,新列 nullable
**user-interview 已确认**:
- Q4: 归一化阈值先用保守初始值 + 后续调优 → **用户已确认**,并建议用 Min-Max 归一化到 [0,1]
### Specify 阶段补充
**BGE-M3 L2 归一化实测验证**:
- FullPipelineSmokeTest.embeddingBgeM3Works() 新增 L2 范数断言
- 结果:范数=1.00000002,误差 < 0.01,测试通过
- 结论:BGE-M3 输出为 L2 归一化单位向量,L2 距离数学硬上界 = 2.0
- Min-Max 归一化公式:`similarity = 1 - min(l2Score, 2.0) / 2.0`
**Cross-artifact 对齐检查**:
| 对齐项 | 状态 |
|--------|------|
| brief 目标/范围/非目标 → proposal 覆盖 | 已对齐 |
| proposal 范围/约束 → design 覆盖 | 已对齐 |
| design 归一化/行动记忆/接口影响 → specs 覆盖 | 已对齐 |
| specs 可观察行为 → tasks 覆盖 | 已对齐 |
**接口影响分级**:
- RetrievedDocTracker 数据结构升级 → L2(内部接口,消费者只有 LookupKnowledgeTool)
- LookupResult 新增 3 字段 → L2(工具返回值,无跨模块调用方)
- tool_invocation 新增 2 列 → L2(Flyway nullable,不影响现有查询)
- chat-executor-prompt.md 更新 → L1(Prompt 文本变更)
### Audit 阶段
**架构风险评估**(5 句以内):
1. 归一化层嵌入 LookupKnowledgeTool 内部(静态方法),无跨模块耦合风险。
2. RetrievedDocTracker 升级为双层结构,数据量级不变(文档数 × session 数),内存无风险。
3. L1 metadata 解析 category 是兜底路径,如果 JSON 格式不一致可能解析失败——已有 try-catch 兜底。
4. 归一化阈值 yml 配置化,运行时调优不需要改代码和重启——运维友好。
5. Prompt 约束仍依赖 LLM 遵守——如果 Phase 1 效果不足,Phase 2 域级硬限制的 isDomainRetrieved 已就绪,无需额外改造。
@@ -0,0 +1,65 @@
# Evidence: executor-action-memory-relevance
## Evidence-driven 结论
### E1: relevanceLevel 三等级语义清晰度
- **来源**: Grill 阶段 Question Pool #1
- **查证结果**: 三个等级语义明确,边界清晰:
- PRECISE:L0 唯一精确匹配,LLM 应直接使用
- HIGHLY_RELEVANT:归一化 similarity ≥ 0.75,高度相关
- REFERENCE:归一化 similarity ≥ 0.5,相关参考
- **结论**: LLM 误解风险低,语义边界足够清晰
### E2: L1 Score 值域与归一化阈值
- **来源**: Grill 阶段 Question Pool #2
- **查证结果**:
- L1 score 是 L2 距离,值域 [0, +∞)
- BGE-M3 输出为 L2 归一化单位向量(实测范数=1.00000002),L2 距离数学硬上界 = 2.0
- Min-Max 归一化公式:`similarity = 1 - min(l2Score, 2.0) / 2.0`
- **结论**: 使用 `maxL2Distance=2.0` 作为归一化上界,阈值 yml 可配置
### E3: L1 Domain 提取兜底路径
- **来源**: Grill 阶段 Question Pool #3
- **查证结果**:
- L0 的 domain 可从 `KnowledgeEntry.getCategory()` 直接获取
- L1 结果的 domain 需解析 `SearchResult.metadata` JSON 字符串
- 当前知识库设计下 L0 大概率先命中,L1 domain 提取作为兜底
- **结论**: 先尝试 L0 category,失败时解析 L1 metadata JSON(try-catch 兜底)
### E4: 历史数据不回填
- **来源**: Grill 阶段 Question Pool #5
- **查证结果**: 新列 `relevance_level` 和 `dedup_reason` 均为 nullable,不影响现有查询
- **结论**: 历史记录保持 null,不需要回填迁移
### E5: BGE-M3 L2 归一化实测验证
- **来源**: Specify 阶段 + FullPipelineSmokeTest
- **查证结果**:
- embeddingBgeM3Works() 测试新增 L2 范数断言
- 实测范数 = 1.00000002,误差 < 0.01
- 测试通过,BGE-M3 输出确认为 L2 归一化单位向量
- **结论**: L2 距离上界 = 2.0 的数学依据成立
### E6: V010 迁移验证
- **来源**: Apply 阶段运行时验证
- **查证结果**:
- Flyway V010 迁移成功执行
- `relevance_level` VARCHAR(20) 列可空,已正确写入
- `dedup_reason` VARCHAR(32) 列可空,已正确写入
- `retrieval_details` JSON 扩展字段(l1_top_similarity、relevance_level、completeness_hint、retrieved_domains、dedup_reason)全部写入
- **结论**: 入库可观测性符合设计
### E7: 数据库数据校验
- **来源**: Apply 阶段运行时验证
- **查证结果**:
- session `b66d799e` 共 10 条 lookup_knowledge 调用
- id=138: L2=0.383 → similarity=0.8085 → HIGHLY_RELEVANT(符合预期)
- id=139-147: 主要为 REFERENCE,doc_retrieved 去重正常触发
- retrieved_domains 域追踪:`[infrastructure]` → `[infrastructure, api]` 正常扩展
- **结论**: 归一化、行动记忆、去重机制数据层面全部验证通过