chore(docs): 归档 ISS-001 session-dedup-knowledge-map + ISS-002 mvp 文档
- 移动 session-dedup-knowledge-map OpenSpec 到 archive 目录 - 提交 ISS-001 遗留的 devflow 档案文件 - 更新 ISS-002 状态为已修复 - 新增 mvp/architecture/action-memory-relevance.md 设计文档 - 更新 mvp/README.md 文档导航 - 更新 devflow/index.md OpenSpec 链接指向 archive
This commit is contained in:
@@ -0,0 +1 @@
|
||||
committed
|
||||
@@ -0,0 +1,157 @@
|
||||
# Design: session-dedup-knowledge-map
|
||||
|
||||
## 1. 整体架构
|
||||
|
||||
本 change 包含两个独立但互补的部分:
|
||||
|
||||
```
|
||||
Part A: 工具层去重
|
||||
LookupKnowledgeTool
|
||||
├── 维护 ConcurrentHashMap<sessionId, Set<filePath>>(JVM 内)
|
||||
├── 每次检索前过滤已召回文档
|
||||
└── SessionContextHolder.clear() 时同步清理
|
||||
|
||||
Part B: 知识图谱
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 文档上传 (DocumentManagementService) │
|
||||
│ → LLM 生成 doc.covers + doc.when_to_retrieve │
|
||||
│ → 存入 api_document.metadata │
|
||||
│ → 触发域级重算 (KnowledgeDomainService) │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 域级聚合 (KnowledgeDomainService) │
|
||||
│ → 读取同域所有文档的 when_to_retrieve │
|
||||
│ → LLM 生成 domain.when_to_retrieve │
|
||||
│ → 存入 knowledge_domain 表 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 启动 (KnowledgeIndexService.loadIndex) │
|
||||
│ → 加载 knowledge_domain 表 │
|
||||
│ → 某域无记录则触发域级生成 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Planner prompt (ChatService) │
|
||||
│ → 注入 knowledge map(域级) │
|
||||
│ → Planner 做粗粒度检索决策 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据结构定义
|
||||
|
||||
### 2.1 Frontmatter 新增字段
|
||||
|
||||
```yaml
|
||||
# 新增两个字段,其余不变
|
||||
covers: ["支付失败排查", "扣款无回调"] # List<String>:业务场景标签,Planner 决策用
|
||||
when_to_retrieve: "用户描述支付失败、超时时" # String:文档级检索时机,LLM 上传时生成
|
||||
```
|
||||
|
||||
对应 `Frontmatter.java` 新增两个字段:
|
||||
- `List<String> covers`
|
||||
- `String whenToRetrieve`
|
||||
|
||||
对应 `KnowledgeEntry.java` 新增两个字段(同上)。
|
||||
|
||||
### 2.2 knowledge_domain 表(新表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE knowledge_domain (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
domain_id VARCHAR(64) NOT NULL UNIQUE, -- category 值,如 "payment"
|
||||
description VARCHAR(256), -- 域描述(聚合自文档 summary)
|
||||
when_to_retrieve TEXT, -- 域级检索时机(LLM 生成)
|
||||
document_count INT DEFAULT 0, -- 该域当前文档数
|
||||
updated_at DATETIME,
|
||||
created_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
### 2.3 knowledge map 结构(注入 Planner 的 YAML 文本)
|
||||
|
||||
```yaml
|
||||
available_knowledge_domains:
|
||||
- domain_id: "payment"
|
||||
description: "支付链路问题排查"
|
||||
when_to_retrieve: "用户问题涉及支付、退款、对账时检索;优先检索一次,勿重复"
|
||||
documents:
|
||||
- title: "支付失败排查手册"
|
||||
covers: ["支付超时", "扣款无回调"]
|
||||
- title: "退款处理指南"
|
||||
covers: ["退款未到账", "退款状态异常"]
|
||||
- domain_id: "infrastructure"
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增组件
|
||||
|
||||
### 3.1 KnowledgeDomainService(新类)
|
||||
|
||||
职责:域级聚合与存储
|
||||
|
||||
```
|
||||
buildDomainSummary(category)
|
||||
→ 读取同域所有 KnowledgeEntry(含 when_to_retrieve)
|
||||
→ 拼装 prompt,调用 LLM
|
||||
→ 写入 knowledge_domain 表
|
||||
|
||||
buildKnowledgeMap()
|
||||
→ 读取所有 knowledge_domain 记录
|
||||
→ 拼装 YAML 文本(含 documents 列表)
|
||||
→ 返回 String(供 Planner prompt 注入)
|
||||
|
||||
onDocumentChange(category)
|
||||
→ 调用 buildDomainSummary(category)(只重算受影响域)
|
||||
```
|
||||
|
||||
### 3.2 RetrievedDocTracker(新类,或内联入 LookupKnowledgeTool)
|
||||
|
||||
职责:session 级已召回文档追踪
|
||||
|
||||
```
|
||||
ConcurrentHashMap<String, Set<String>> retrieved
|
||||
key: sessionId
|
||||
value: Set<filePath>
|
||||
|
||||
isAlreadyRetrieved(sessionId, filePath) → boolean
|
||||
markRetrieved(sessionId, filePath)
|
||||
clearSession(sessionId) ← 由 SessionContextHolder.clear() 触发
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 改动文件清单
|
||||
|
||||
| 文件 | 改动类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `LookupResult.java` | 修改 | 新增 `message` 字段(去重提示文本) |
|
||||
| `Frontmatter.java` | 修改 | 新增 `covers`、`whenToRetrieve` |
|
||||
| `KnowledgeEntry.java` | 修改 | 新增 `covers`、`whenToRetrieve` |
|
||||
| `FrontmatterParser.java` | 修改 | 解析新字段 |
|
||||
| `DocumentManagementService.java` | 修改 | upload 时调 LLM 生成文档级字段;upload/delete 后触发域级重算 |
|
||||
| `KnowledgeIndexService.java` | 修改 | loadIndex 时加载域级数据;若域无记录则触发生成 |
|
||||
| `KnowledgeDomainService.java` | 新增 | 域聚合、LLM 调用、DB 读写、buildKnowledgeMap |
|
||||
| `KnowledgeDomain.java`(entity) | 新增 | knowledge_domain 表映射 |
|
||||
| `KnowledgeDomainRepository.java` | 新增 | JPA Repository |
|
||||
| `LookupKnowledgeTool.java` | 修改 | 集成 RetrievedDocTracker,检索前过滤,检索后标记 |
|
||||
| `SessionContextHolder.java` | 修改 | clear() 时通知 RetrievedDocTracker |
|
||||
| `RetrievedDocTracker.java` | 新增 | session 级去重状态管理 |
|
||||
| `ChatService.java` | 修改 | buildChatPlannerAgent 注入 knowledge map |
|
||||
| `chat-planner-prompt.md` | 修改 | 添加 knowledge map 使用规则 |
|
||||
| `V009__add_knowledge_domain.sql` | 新增 | Flyway 建表脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键决策记录
|
||||
|
||||
1. **域级 when_to_retrieve 存 DB**:避免每次重启调 LLM;文档变更时只重算受影响域
|
||||
2. **文档级 when_to_retrieve 存 metadata JSON**:沿用现有 frontmatter 存储路径,无需新字段
|
||||
3. **RetrievedDocTracker 独立于 SessionContextHolder**:SessionContextHolder 只持有 sessionId,Tracker 是业务状态,职责分离;clear() 时通过 Tracker.clearSession() 联动
|
||||
4. **Planner 只看域级**:文档级 when_to_retrieve 留 Executor 筛选(Phase 2),MVP 不暴露给 Planner
|
||||
5. **LLM 调用同步执行**:上传时同步生成,接受约 1-2s 延迟,保证数据库和 L0 索引立即一致
|
||||
@@ -0,0 +1,61 @@
|
||||
# Proposal: session-dedup-knowledge-map
|
||||
|
||||
## 问题
|
||||
|
||||
1. **ISS-001 重复召回**:`LookupKnowledgeTool` 每次调用完全无状态,同一 session 中同一文档可被重复召回 13+ 次,浪费 token、压缩上下文窗口、导致 `tool_call_count` 虚高。
|
||||
|
||||
2. **Planner 缺少全局视野**:Planner 不知道知识库里有哪些域,只能靠 Executor 反复试探,导致低效的"盲目检索"模式。
|
||||
|
||||
## 建议方案
|
||||
|
||||
### Part A:工具层去重(彻底修复 ISS-001)
|
||||
|
||||
在 `LookupKnowledgeTool` 的 session 维度维护已召回文档 ID 集合。
|
||||
每次检索时,过滤掉已召回的文档;相同 query 命中相同文档则直接跳过(返回"已在上下文中"提示)。
|
||||
|
||||
状态存储:`ConcurrentHashMap<sessionId, Set<docKey>>`,生命周期随 session(`SessionContextHolder.clear()` 时清理)。
|
||||
|
||||
### Part B:知识图谱注入 Planner
|
||||
|
||||
启动时(`KnowledgeIndexService.loadIndex()` 完成后),将 L0 索引中的所有 `KnowledgeEntry` 聚合为域级摘要(knowledge map)。
|
||||
每次构建 Planner prompt 时(`buildChatPlannerAgent()`),将 knowledge map 注入 system prompt,让 Planner 有"知识边界"。
|
||||
|
||||
聚合策略:按 `category` 字段分组,生成结构:
|
||||
```
|
||||
available_knowledge_domains:
|
||||
- domain_id: "payment"
|
||||
description: "..."
|
||||
covers: [...]
|
||||
document_count: N
|
||||
when_to_retrieve: "..."
|
||||
```
|
||||
|
||||
知识图谱的 `description` / `when_to_retrieve` 字段来源于:
|
||||
- 选项 1:直接聚合 KnowledgeEntry 的 title/summary
|
||||
- 选项 2:文档 frontmatter 中新增 `domain_description` / `when_to_retrieve` 字段
|
||||
- 选项 3:上传时 LLM 自动生成这两个字段
|
||||
|
||||
## 范围
|
||||
|
||||
**In scope**:
|
||||
- `LookupKnowledgeTool`:添加 session 级去重状态管理
|
||||
- `KnowledgeIndexService`:添加 `buildKnowledgeMap()` 方法
|
||||
- `ChatService.buildChatPlannerAgent()`:注入 knowledge map 到 prompt
|
||||
- `chat-planner-prompt.md`:添加如何使用 knowledge map 的指令
|
||||
|
||||
**Out of scope**(本次不做):
|
||||
- `EvaluationService.tool_call_count` 的统计口径调整(去重后虚高问题自然消失,但评分规则不改)
|
||||
- RRF 混合重排
|
||||
- 文档 frontmatter 自动生成(上传时 LLM 生成,留 Phase 2)
|
||||
|
||||
## 风险
|
||||
|
||||
- Part A 引入 JVM 内存 Map,高并发时多 session 并发需线程安全
|
||||
- Part B knowledge map 注入 Planner prompt 会增加每次请求的 token 消耗(固定开销)
|
||||
- 文档 `category` 字段缺失或不规范时,聚合结果可能混乱
|
||||
|
||||
## 上下文约束
|
||||
|
||||
- `SessionContextHolder` 是 ThreadLocal,异步路径不安全(已知限制,Part A 需确认同步路径)
|
||||
- `EvaluationService` 依赖 `tool_call_count`,去重会降低此值(是修复,不是回归)
|
||||
- `KnowledgeEntry` 已有 `category` 字段,但当前数据库中的文档是否都有 `category` 需确认
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Functional Spec: session-dedup-knowledge-map
|
||||
|
||||
## REQ-01:工具层去重(Part A)
|
||||
|
||||
**触发**:`LookupKnowledgeTool.lookupKnowledge(query)` 被调用
|
||||
|
||||
**行为**:
|
||||
1. 从 `SessionContextHolder.getSessionId()` 获取当前 sessionId;若为 null(非会话上下文)跳过去重逻辑,正常检索
|
||||
2. L0+L1 检索完成后,将结果中已在 `RetrievedDocTracker` 中标记的 filePath 过滤掉
|
||||
3. 若过滤后 L0 结果为空、L1 结果也为空(全部已召回),返回 `LookupResult.found=false`,并在结果中附带提示文本:"以下文档已在本会话中检索过:[列表],无需重复召回"
|
||||
4. 未被过滤的文档正常返回后,将其 filePath 写入 `RetrievedDocTracker`
|
||||
5. `SessionContextHolder.clear()` 调用时,`RetrievedDocTracker.clearSession(sessionId)` 同步清理
|
||||
|
||||
**验收**:
|
||||
- 同一 session 内同一文档第二次命中时,返回去重提示而非完整文档内容
|
||||
- 不同 session 之间互不影响
|
||||
- sessionId 为 null 时不影响正常检索流程
|
||||
|
||||
---
|
||||
|
||||
## REQ-02:文档级 LLM 字段生成(Part B - 文档级)
|
||||
|
||||
**触发**:`DocumentManagementService.uploadDocument()` 完成 frontmatter 解析后
|
||||
|
||||
**行为**:
|
||||
1. 若文档 frontmatter 中已包含 `covers` 和 `whenToRetrieve`,跳过 LLM 生成(作者手动填写优先)
|
||||
2. 否则,调用 LLM,输入为文档 title + summary + 正文前 1000 字符
|
||||
3. Prompt 要求 LLM 返回 JSON:`{"covers": [...], "whenToRetrieve": "..."}`
|
||||
4. 解析结果,回填到 `Frontmatter` 对象
|
||||
5. 序列化存入 `api_document.metadata`;同步更新 `KnowledgeEntry` 写入 L0 索引
|
||||
6. LLM 调用失败时,`covers` 置为空列表,`whenToRetrieve` 置为 summary(降级),不阻断上传流程
|
||||
|
||||
**验收**:
|
||||
- 上传后 `api_document.metadata` 中包含 `covers` 和 `whenToRetrieve` 字段
|
||||
- frontmatter 已有这两个字段时不覆盖
|
||||
- LLM 调用异常时文档仍上传成功,字段降级填充
|
||||
|
||||
---
|
||||
|
||||
## REQ-03:域级聚合与存储(Part B - 域级)
|
||||
|
||||
**触发**:文档上传成功后;文档删除后;`loadIndex()` 时发现某域在 `knowledge_domain` 表无记录
|
||||
|
||||
**行为**:
|
||||
1. `KnowledgeDomainService.onDocumentChange(category)` 读取该 category 下所有 `KnowledgeEntry` 的 title + covers + whenToRetrieve
|
||||
2. 调用 LLM,生成域级 `when_to_retrieve`(要求 LLM 识别域内文档边界,输出含区分语义的路由描述)
|
||||
3. 写入 `knowledge_domain` 表(upsert by domain_id),同时更新 `document_count`
|
||||
4. LLM 调用失败时,`domain.when_to_retrieve` 保留上次 DB 记录;若无历史记录则置为空字符串
|
||||
|
||||
**验收**:
|
||||
- 上传文档后,对应 category 的 `knowledge_domain` 记录被更新
|
||||
- 删除文档后,对应 category 的 `document_count` 减少,`when_to_retrieve` 重新生成
|
||||
- `loadIndex()` 时无 DB 记录的域自动触发生成
|
||||
|
||||
---
|
||||
|
||||
## REQ-04:knowledge map 注入 Planner(Part B - 注入)
|
||||
|
||||
**触发**:`ChatService.buildChatPlannerAgent()` 调用时
|
||||
|
||||
**行为**:
|
||||
1. 调用 `KnowledgeDomainService.buildKnowledgeMap()` 生成 YAML 文本
|
||||
2. YAML 结构:域列表,每个域包含 domain_id、description、when_to_retrieve、documents(title + covers)
|
||||
3. 若 knowledge_domain 表为空(无任何域记录),跳过注入,不修改 prompt
|
||||
4. 注入位置:Planner system prompt 末尾,独立区块
|
||||
|
||||
**Planner prompt 附加规则**:
|
||||
- 制定步骤时,先查看 `available_knowledge_domains`,按 `when_to_retrieve` 判断是否需要检索该域
|
||||
- 每个域最多指示 Executor 检索一次;已检索过的域不再安排检索步骤
|
||||
|
||||
**验收**:
|
||||
- Planner prompt 包含 `available_knowledge_domains` 区块
|
||||
- 无域记录时 prompt 不包含该区块(不注入空结构)
|
||||
- knowledge map 文本长度 < 1000 字符(6 个文档场景下)
|
||||
|
||||
---
|
||||
|
||||
## REQ-05:Frontmatter 字段扩展
|
||||
|
||||
**行为**:
|
||||
- `Frontmatter.java` 新增 `List<String> covers` 和 `String whenToRetrieve`
|
||||
- `KnowledgeEntry.java` 新增同名字段
|
||||
- `FrontmatterParser.java` 解析 `covers`(YAML 数组)和 `when_to_retrieve`(YAML 字符串)
|
||||
|
||||
**验收**:
|
||||
- 现有文档(无新字段)上传/解析不报错,字段为 null 或空列表
|
||||
- 含新字段的文档正确解析
|
||||
@@ -0,0 +1,74 @@
|
||||
# Tasks: session-dedup-knowledge-map
|
||||
|
||||
## T1:数据层基础
|
||||
|
||||
**T1-1:新增 knowledge_domain 表** ✅
|
||||
- 创建 `src/main/resources/db/migration/V009__add_knowledge_domain.sql`
|
||||
- 字段:id、domain_id(unique)、description、when_to_retrieve(TEXT)、document_count、created_at、updated_at
|
||||
|
||||
**T1-2:新增 KnowledgeDomain 实体和 Repository** ✅
|
||||
- `KnowledgeDomain.java`:JPA 实体,对应 knowledge_domain 表
|
||||
- `KnowledgeDomainRepository.java`:`findByDomainId(String)` + save
|
||||
|
||||
---
|
||||
|
||||
## T2:Frontmatter 扩展
|
||||
|
||||
**T2-1:Frontmatter.java / KnowledgeEntry.java 新增字段** ✅
|
||||
- `Frontmatter`:新增 `List<String> covers`、`String whenToRetrieve`
|
||||
- `KnowledgeEntry`:新增 `List<String> covers`、`String whenToRetrieve`
|
||||
|
||||
**T2-2:FrontmatterParser 解析新字段** ✅
|
||||
- 解析 YAML 中的 `covers`(List)和 `when_to_retrieve`(String)
|
||||
|
||||
**T2-3:KnowledgeIndexService 替换为 Jackson 解析** ✅
|
||||
- 全量替换手写 extractJsonValue/extractJsonArray 为 `objectMapper.readValue(metadata, Frontmatter.class)`
|
||||
|
||||
**T2-4:LookupResult 新增 message 字段** ✅
|
||||
- 新增 `String message` 字段,去重时填入提示
|
||||
|
||||
---
|
||||
|
||||
## T3:工具层去重(Part A)
|
||||
|
||||
**T3-1:新增 RetrievedDocTracker** ✅
|
||||
- `RetrievedDocTracker.java`:Spring `@Component`,`ConcurrentHashMap<String, Set<String>>`
|
||||
- 方法:`isAlreadyRetrieved`、`markRetrieved`、`clearSession`
|
||||
|
||||
**T3-2:ChatService.finally 联动 Tracker** ✅
|
||||
- `executeChat` / `executeChatComplex` 的 finally 块显式调用 `retrievedDocTracker.clearSession(sessionId)`
|
||||
|
||||
**T3-3:LookupKnowledgeTool 集成去重** ✅
|
||||
- 注入 `RetrievedDocTracker`,检索后过滤已召回文档,全部已召回时返回去重提示
|
||||
|
||||
---
|
||||
|
||||
## T4:文档级 LLM 生成(Part B 文档级)
|
||||
|
||||
**T4-1:新增 DocumentFieldEnricher + 上传时调用** ✅
|
||||
- `DocumentFieldEnricher.java`:调用 LLM 生成 covers / whenToRetrieve
|
||||
- `DocumentManagementService.uploadDocument()` frontmatter 解析后调用 enrich
|
||||
- 失败时降级(covers=空列表,whenToRetrieve=summary),不阻断上传
|
||||
|
||||
---
|
||||
|
||||
## T5:域级聚合(Part B 域级)
|
||||
|
||||
**T5-1:新增 KnowledgeDomainService** ✅
|
||||
- `buildDomainSummary`:同域文档聚合 → LLM → upsert knowledge_domain
|
||||
- `buildKnowledgeMap`:全量 knowledge_domain → YAML 字符串
|
||||
- `onDocumentChange`:触发 buildDomainSummary
|
||||
|
||||
**T5-2:loadIndex 触发域生成 + 文档变更触发域重算** ✅
|
||||
- `KnowledgeIndexService.loadIndex` 末尾:无 DB 记录的域自动触发生成
|
||||
- `DocumentManagementService.uploadDocument` / `deleteDocument` 末尾:调用 `onDocumentChange`
|
||||
|
||||
---
|
||||
|
||||
## T6:Planner 注入(Part B 注入)
|
||||
|
||||
**T6-1:ChatService 注入 knowledge map** ✅
|
||||
- `buildChatPlannerAgent()` 注入 `KnowledgeDomainService.buildKnowledgeMap()` 到 prompt
|
||||
|
||||
**T6-2:chat-planner-prompt.md 新增规则** ✅
|
||||
- 新增知识库检索规则区块,要求 Planner 按 when_to_retrieve 决策、每域最多一次
|
||||
Reference in New Issue
Block a user