Files
zhuyongxin e438df4355 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
2026-07-01 18:24:41 +08:00

7.7 KiB
Raw Permalink Blame History

Design: executor-action-memory-relevance

架构设计

整体数据流

用户问题
  → Supervisor → Planner(规划查哪些域)
  → Supervisor → Executor(自主调用 lookup_knowledge)
       ↓
  LookupKnowledgeTool
    ├─ L0 精确匹配 → l0Matches (含 category)
    ├─ L1 语义检索 → l1Results (含 L2 score)
    ├─ 归一化层 → computeRelevanceLevel(l0Count, l1TopScore)
    │    L2 距离 → similarity = 1 - min(score, 2.0) / 2.0
    │    L0 唯一匹配 → PRECISE
    │    L0 命中 + L1 similarity ≥ 0.75 → HIGHLY_RELEVANT
    │    仅 L1 similarity ≥ 0.75 → HIGHLY_RELEVANT
    │    L0 多匹配 + L1 similarity [0.5, 0.75) → REFERENCE
    │    仅 L1 similarity [0.5, 0.75) → REFERENCE
    ├─ 域级行动记忆 → RetrievedDocTracker.markRetrieved(sessionId, domain, filePath)
    │    getRetrievedDomains(sessionId) → retrievedDomainsThisSession
    ├─ 文档级去重 → 保留现有逻辑
    └─ 组装 LookupResult(含 relevanceLevel, completenessHint, retrievedDomainsThisSession)
       ↓
  LLM 看到:
    relevanceLevel: PRECISE
    completenessHint: "知识库中不存在比上述结果更精准的文档"
    retrievedDomainsThisSession: ["infrastructure", "api"]

Agent 边界(保持清晰)

Agent 知道什么 不知道什么
Planner 全域知识边界(knowledge map) 执行细节、检索结果
Executor 自己的行动记忆(已检索域列表) 全域知识边界(不注入 knowledge map)

行动记忆通过工具返回值传递,不通过 prompt 注入。

数据结构设计

1. RetrievedDocTracker 升级

// 现有:sessionId → Set<filePath>(文档级)
ConcurrentHashMap<String, Set<String>> retrieved

// 新增:sessionId → { domain → Set<filePath> }(域级 + 文档级)
ConcurrentHashMap<String, Map<String, Set<String>>> sessionRetrievals

方法列表:

  • markRetrieved(sessionId, domain, filePath) — 一次记录两层
  • isDocRetrieved(sessionId, filePath) → boolean — 文档级去重(替代现有 isAlreadyRetrieved)
  • isDomainRetrieved(sessionId, domain) → boolean — 域级检查(Phase 2 硬限制用)
  • getRetrievedDomains(sessionId) → List — 行动记忆(返回给 LLM)
  • clearSession(sessionId) — 清理(不变)

2. LookupResult 扩展

@Data @Builder
public class LookupResult {
    boolean found;
    PrimaryResult primary;                    // 不变,不暴露原始分数
    SupplementResult supplement;              // 不变,不暴露原始分数
    // ---- 新增 ----
    String relevanceLevel;                    // PRECISE / HIGHLY_RELEVANT / REFERENCE
    String completenessHint;                  // 兜底信号
    List<String> retrievedDomainsThisSession; // 行动记忆
    String message;                           // 不变
}

PrimaryResult 和 SupplementResult 不加任何分数字段。原始分数在归一化层内部消化。

3. 归一化计算

RelevanceNormalizer(LookupKnowledgeTool 内部静态方法):

输入:l0MatchCount, l1TopScore (L2 距离)
输出:RelevanceAssessment { relevanceLevel, completenessHint }

归一化公式(BGE-M3 输出 L2 归一化单位向量,已实测验证):
  similarity = 1 - min(l2Score, maxL2Distance) / maxL2Distance
  maxL2Distance 默认 2.0,yml 可覆盖

判定逻辑:
  if l0MatchCount == 1 → PRECISE
  if l0MatchCount > 1 && l1Similarity >= highlyRelevantThreshold → HIGHLY_RELEVANT
  if l0MatchCount == 0 && l1Similarity >= highlyRelevantThreshold → HIGHLY_RELEVANT
  if l0MatchCount > 1 && l1Similarity >= referenceThreshold → REFERENCE
  if l0MatchCount == 0 && l1Similarity >= referenceThreshold → REFERENCE
  else → 无结果

completenessHint 映射:
  PRECISE → "知识库中不存在比上述结果更精准的文档"
  HIGHLY_RELEVANT → "当前结果已高度相关,继续检索不太可能找到更精准的文档"
  REFERENCE → "当前结果为相关参考,如需更精准信息请明确缺少的具体维度"

配置项(application.yml):

retrieval:
  normalization:
    max-l2-distance: 2.0              # L2 距离上界(单位向量 = 2.0)
    highly-relevant-threshold: 0.75   # similarity ≥ 0.75 → HIGHLY_RELEVANT
    reference-threshold: 0.5          # similarity ≥ 0.5 → REFERENCE

4. 入库记录扩展

tool_invocation 表新增列:

列名 类型 说明
relevance_level VARCHAR(20) PRECISE / HIGHLY_RELEVANT / REFERENCE / DEDUPED
dedup_reason VARCHAR(32) doc_retrieved / domain_retrieved / null

retrieval_details JSON 扩展:

{
  "l0_match_count": 2,
  "l0_titles": ["MySQL连接池配置", "HikariCP参数调优"],
  "l1_top_score": 0.52,
  "l1_top_similarity": 0.74,
  "l1_match_count": 3,
  "l1_scores": [0.52, 0.68, 0.91],
  "relevance_level": "HIGHLY_RELEVANT",
  "completeness_hint": "当前结果已高度相关...",
  "retrieved_domains": ["infrastructure"],
  "dedup_reason": null
}

原始 L2 score 和归一化后的 similarity 都入库,保留可观测性。

Executor Prompt 设计

不加 knowledge map,只加基于行动记忆的行为规则:

## 检索约束

### 1. 判断重复:基于已检索上下文
每次 lookup_knowledge 返回值中包含 retrievedDomainsThisSession,
表示本次会话已检索过的知识域。如果当前问题与已检索域语义重叠,
**禁止再次调用 lookup_knowledge**。

### 2. 重复了该怎么办
如果当前想检索的内容与【已检索上下文】语义相似:
- 禁止换关键词重新检索
- 直接基于已有事实回答
- 如果信息不足,先明确指出缺少什么具体维度
  (如:"缺少 HikariCP 具体配置参数"、"缺少连接池耗尽的日志样例"),
  再针对该维度进行一次定向补充检索——而非盲目换词重查

### 3. 合法出口:允许信息不全时给出结论
如果你认为已有信息足以回答核心问题,即使细节不全,
也请直接给出结论并说明局限性(如:"基于已有信息,连接池配置建议如下,
但具体参数值需结合实际负载调整")。
**不查全不会被追责,重复检索才会被惩罚。**

### 4. 利用质量信号判断
- relevanceLevel=PRECISE → 信息精准,直接使用,不再检索
- relevanceLevel=HIGHLY_RELEVANT + 域已在 retrievedDomainsThisSession → 禁止再次调用
- relevanceLevel=REFERENCE → 先指出缺什么维度,再定向补充一次
- completenessHint 是知识库给你的天花板信号,信任它

关键决策

  1. L0/L1 原始分数不暴露给 LLM — 在归一化层内部消化,避免 LLM 混淆尺度
  2. BGE-M3 L2 归一化已实测验证 — 范数 1.00000002,maxL2Distance=2.0 是数学硬上界
  3. 行动记忆通过工具返回值传递 — 不通过 prompt 注入,不修改 ReactAgent prompt 构建方式
  4. 不给 Executor knowledge map — 保持 Agent 边界:Planner 知道全域,Executor 只知道自己做了什么
  5. Phase 2 域级硬限制暂不实施 — 先观察 prompt 约束 + 归一化信号的效果

接口影响分级

变更 级别 说明
RetrievedDocTracker 数据结构升级 L2 内部接口 消费者只有 LookupKnowledgeTool,在同一实现范围内
LookupResult 新增 3 个字段 L2 内部接口 消费者是 LLM(工具返回值),无跨模块调用方
tool_invocation 表新增 2 列 L2 内部接口 Flyway 迁移,nullable,不影响现有查询
chat-executor-prompt.md 更新 L1 内部实现 Prompt 文本变更,不改变接口