Files
SuperBizAgent-java/openspec/changes/archive/2026-07-01-executor-action-memory-relevance/design.md
T
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

190 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 升级
```java
// 现有: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<String> — 行动记忆(返回给 LLM)
- `clearSession(sessionId)` — 清理(不变)
#### 2. LookupResult 扩展
```java
@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):
```yaml
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 扩展:
```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,只加基于行动记忆的行为规则:
```markdown
## 检索约束
### 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 文本变更,不改变接口 |