docs(design): add multi-turn dialogue optimization system design (report 03)

This commit is contained in:
WeiCHEN
2026-09-13 02:50:20 +00:00
committed by weichen
parent e5e80a6fc6
commit e590dcfd7b
2 changed files with 813 additions and 2 deletions
+24 -2
View File
@@ -47,6 +47,26 @@
---
### 3. 多轮对话优化
| 项 | 链接 |
|-----|------|
| 主报告 | [multi-turn-dialogue/03-多轮对话优化-设计-架构-实现-难点-解决方案-亮点与优势.md](multi-turn-dialogue/03-多轮对话优化-设计-架构-实现-难点-解决方案-亮点与优势.md) |
| 一句话总结 | 断链修复 + 三层记忆(工作/短期/长期)+ 滑动窗口双预算 + 滚动摘要 + DST 状态跟踪 + 对话向量库按需召回 + 规则查询改写 + 十段式 Prompt + 多级缓存 |
**核心要点**:
- **P0 断链修复**:`RecentDialogue` / `PersonalizedGuidance` 生产链路从未填充、`RestoreSessionContext` 零调用——三大断点先接线,既有部件(三分支/黑板/偏好反射/pgvector)即刻协同
- **三层记忆模型**:工作记忆(`task_state` DST)/ 短期记忆(轮数+token 双预算滑动窗口)/ 长期记忆(用户画像 + `chat_history` 向量域)
- **历史压缩**:滚动摘要(LLM 生成 + 规则降级),硬事实区永不压缩,黑板 SSOT 优先级兜底
- **按需检索**:意图触发的记忆召回(TopK≤3 + 篇幅闸门),SQL 权限过滤 + Prompt 隐私条款双闸门
- **查询改写**:5 条确定性规则(省略补全/代词消解/主题合并/槽位补全/概念合成),永不进入意图识别链路
- **十段式 Prompt 规范**:分段硬分隔 + 逐轮编号 + 来源标注 + 优先级声明 + CiteKey 引用回溯闭环
- **多级缓存 C1~C5**:检索/改写/embedding/上下文组装/摘要,写入口主动失效,一键可关
- **五阶段渐进路线**:P0 修复 → P1 上下文 → P2 检索 → P3 改写 → P4 系统层,全部可独立回滚
---
## 🏗️ 在整体架构中的位置
```
@@ -99,8 +119,10 @@
├── README.md ← 本文件(顶层索引)
├── intent-recognition/
│ └── 01-意图识别框架-设计-架构-实现-难点-解决方案-亮点与优势.md
└── agent-squad/
└── 02-智能体小队编排-设计-思路-架构-实现-难点-解决方案-亮点与优势.md
├── agent-squad/
│ └── 02-智能体小队编排-设计-思路-架构-实现-难点-解决方案-亮点与优势.md
└── multi-turn-dialogue/
└── 03-多轮对话优化-设计-架构-实现-难点-解决方案-亮点与优势.md
.tmp/ ← 中间分析文件
├── intent-recognition/
@@ -0,0 +1,789 @@
# 多轮对话优化:设计、架构、实现、难点、解决方案、亮点与优势
> 适用范围:`internal/chat`、`internal/coordinator`、`internal/ctxmgr`、`internal/intent`、`internal/kb`、`internal/server`(多轮对话链路全栈)
> 报告版本:v1.0
> 日期:2026-09-10
> 工程根目录:`/Volumes/MacBook Pro's Data Drive/ShuiMuDongFang-Projects/2026-09-07 Chat-Agent-System`
> 上游文档:[01-意图识别框架](../intent-recognition/01-意图识别框架-设计-架构-实现-难点-解决方案-亮点与优势.md)、[02-智能体小队编排](../agent-squad/02-智能体小队编排-设计-思路-架构-实现-难点-解决方案-亮点与优势.md)、[架构总览](../architecture-design.md)
---
## 一、现状盘点与差距分析
### 1.1 现有多轮对话链路全景
一轮对话的完整数据流(现状):
```
HTTP/SSE: POST /api/v1/chat(/stream)
│
▼
Server.getOrCreateSession (internal/server/server.go:196)
│ 内存 map[string]*chat.Session(无 TTL 淘汰)
│ AssembleFromWorkspace / 默认上下文组装
▼
chat.Session.sendMessageInternal (internal/chat/session.go:88)
│ 1) append 用户消息到 s.Messages
│ 2) CtxMgr.RecordUserMessage 落库 + 偏好反射(manager.go:67)
│ 3) Recognizer.Recognize —— 纯规则四级漏斗 + 三分支消解
│ 4) CoordinatorEngine.ProcessIntent —— 意图分发(engine.go:75)
▼
Phase A 对话 LLM(CASUAL / AMBIGUOUS / QUERY 降级路径)
generateDialogueReply (coordinator/dialogue_llm.go:29)
│ buildDialoguePrompt(userInput, userCtx)
│ 【项目上下文】【近期对话】← RecentDialogue(实际恒为空!)
│ 【本轮用户】
│ pi-agent 生成 reply.md ──失败──▶ 模板兜底
▼
回包:append 助手消息 + 落库 + 更新 PendingExpectation / 卡片生命周期
持久层:chat_sessions / chat_messages / user_preferences / context_memory_chunks(pgvector)
```
### 1.2 已有能力与优化思路的映射矩阵
| 优化思路 | 项目现有实现 | 差距评级 |
|---|---|---|
| **上下文-滑动窗口** | `dialogueHistoryMax = 8` 轮数截断已定义(`dialogue_llm.go:18`),仅按轮数、无 token 预算;且输入源 `UserProjectContext.RecentDialogue` **在生产链路从未被填充** | 🔴 断链 |
| **上下文-历史压缩** | 无任何摘要机制。`RestoreSessionContext` 一次拉 200 条全量消息(`chat_history.go:141`);`GetUserContext` 固定 `LIMIT 30`(`manager.go:214`) | 🔴 缺失 |
| **上下文-状态跟踪** | 已有项目级黑板事实(`BlackboardFacts` → `project_blackboards`)、挂起预期(`PendingExpectation`)、交互卡片 TTL;但**会话级任务参数跟踪**(多轮收集中的参数字段、缺槽清单、主题锚点)无显式建模 | 🟡 半成品 |
| **检索-向量存储** | `context_memory_chunks` + pgvector 已就绪,但**仅收录终审批准文档**(`domain='approved_case'`,`case_indexing.go:86`);历史对话不入向量库 | 🟡 半成品 |
| **检索-按需检索** | `SearchSimilarCases` 仅用于参谋锦囊增强(`engine.go:209`,Limit=2);对话链路无历史片段召回注入 | 🟡 半成品 |
| **检索-分层记忆** | 结构雏形已有:`SingleUserContext` / `ProjectCollectiveContext` / `BlendedSquadContext`(仅 L3 小队链路);L1/L2 对话链路无"工作/短期/长期"三层注入编排 | 🟡 半成品 |
| **改写-补全/概念合成** | 完全没有。`Recognizer.Recognize` 直接消费 `rawInput`;省略式追问("上海呢")会落入 `AMBIGUOUS_FALLBACK` 或错误意图 | 🔴 缺失 |
| **提示词-身份角色** | `dialogue_llm.go:61-77` 已有完整人设 instructions("水木东方项目助手"语气、行为边界) | ✅ 基本齐备 |
| **提示词-历史格式规范** | `buildDialoguePrompt` 有分段骨架,但缺轮次编号、摘要段、检索片段段、任务状态段;且近期对话段恒空 | 🟡 半成品 |
| **提示词-行为约束/隐私** | 已有安全红线拦截与"不得编造/不得伪造执行结果"约束;**跨用户隐私防护缺失**(项目协同上下文含其他成员活动,注入时无脱敏边界声明) | 🟡 半成品 |
| **提示词-缓存复用** | 无任何缓存。每轮新建 `turn_%d` 工作目录、全量重组装上下文、重复检索、重复 embedding | 🔴 缺失 |
### 1.3 十项关键缺口(证据定位)
| # | 缺口 | 证据 | 影响 |
|---|---|---|---|
| G1 | `RecentDialogue` 断链 | 仅 `dialogue_llm.go:186`、`router.go:112` 读取;全仓无生产代码赋值(唯一填充在 `dialogue_llm_test.go:55` 手工构造) | 对话 LLM 每轮"失忆";router 的"多轮追问深化"信号(`RecentDialogue >= 2`)永不触发 |
| G2 | `PersonalizedGuidance` 断链 | `coordinator/context.go:72` 字段定义;`ctxmgr` 的 `GeneratePromptGuidance` 只进 L3 小队 memBank(`bizdev/service.go:421`),未进对话链路 | 用户偏好画像对 Phase A 对话完全无效 |
| G3 | 服务重启会话失忆 | `server.sessions` 为纯内存 map;重启后同 sessionID 重建空上下文,`RestoreSessionContext` 存在但**无人调用**(`chat_history.go:135`) | 长会话跨进程不可续 |
| G4 | 无查询改写 | `recognizer.go:25` 直接 `Recognize(ctx, input, snap)` | "上海呢""它的预算呢"式追问答非所问 |
| G5 | 无历史压缩 | 无摘要表、无摘要代码 | 长会话 token 无界膨胀;30/200 条硬编码拉取 |
| G6 | 对话不入向量库 | `IndexApprovedDocument` 仅 `domain='approved_case'` | 跨会话/长程历史无法按需召回 |
| G7 | 法规查询硬编码 | `handleQueryRegulations`(`engine.go:1379`)返回固定 3 条标准,未调 `kb.Retriever`(该能力在任务执行链路已验证可用,`bizdev/service.go:177`) | 法规问答复读老三样,KB 能力闲置 |
| G8 | 无缓存 | `agent/backend.go` 仅并发限流包装;全链路无 query/embedding/组装缓存 | 重复问题重复付费、重复时延 |
| G9 | sessions map 泄漏 + turn 目录堆积 | `server.go:29` map 只增不减;`dialogue_llm.go:49` 每轮 `MkdirAll` 新目录 | 长期运行内存/磁盘增长 |
| G10 | 跨用户隐私边界缺失 | `GetProjectContext` 聚合全部成员活动流直接注入 prompt;prompt 无隐私隔离指令 | 其他成员沟通习惯/私人事实可能被复述 |
### 1.4 差距结论
本项目的多轮对话能力**"骨架先进、神经断裂"**:意图三分支、黑板 SSOT、时间衰减、pgvector、RAG 重排、偏好反射等部件全部齐备且质量很高,但**部件之间的接线在 L1/L2 对话链路上大面积缺失**。因此本方案的核心策略不是推翻重建,而是——
> **先接神经(P0),再强器官(P1~P4)**:修复断链 → 补齐上下文管理 → 打通检索增强 → 引入查询改写 → 规范提示词与缓存。
---
## 二、总体方案架构
### 2.1 目标架构图
```
┌────────────────────────────────────────────────────────────────────────┐
│ L1 对话层(internal/chat + intent) │
│ │
│ 用户输入 rawInput │
│ ├─▶ [新增] QueryRewriter 查询改写器(规则补全/代词消解/概念合成) │
│ │ → rewrittenQuery(只服务检索与 Prompt,不改变意图识别输入) │
│ ├─▶ Recognizer.Recognize(rawInput)(保持纯规则确定性,不动) │
│ └─▶ [新增] 会话任务状态 DST 更新(active_task / slots / 锚点) │
└────────────────────────────┬───────────────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────┐
│ L2 协调层(internal/coordinator) │
│ │
│ ProcessIntent → handler 分发(现状保留) │
│ ▼ │
│ [重构] buildDialoguePrompt —— 分层记忆装配(MemoryComposer) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 【工作记忆】 任务状态 + 挂起预期 + 交互卡片(本轮正在处理什么) │ │
│ │ 【短期记忆】 滑动窗口近 M 轮完整对话(轮次编号,双预算裁剪) │ │
│ │ 【压缩记忆】 早期轮次滚动摘要(LLM 生成,规则降级兜底) │ │
│ │ 【长期记忆】 用户画像偏好(MemoryFacts + 风格/格式注入语) │ │
│ │ 【按需检索】 向量召回相关历史片段(≤3 条,带来源/时间/权限) │ │
│ │ 【知识注入】 法规 KB 命中片段(QUERY_REGULATIONS 等意图触发) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ├─▶ [新增] 多级缓存(检索/改写/组装/embedding 四级缓存) │
│ └─▶ pi-agent / 模板兜底(现状保留) │
└────────────────────────────┬───────────────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 持久层与记忆库(internal/ctxmgr + storage) │
│ chat_sessions(+task_state,+turn_count) chat_messages │
│ chat_summaries(新表: 滚动摘要) │
│ context_memory_chunks(domain 扩展: chat_history + approved_case) │
│ user_preferences(MemoryFacts) project_blackboards(SSOT) │
└────────────────────────────────────────────────────────────────────────┘
```
### 2.2 设计原则(继承并扩展既有"防回归三原则")
1. **默认即现状**:所有新模块默认关闭或等价于现状行为,配置开关逐项启用;既有 18 类意图识别与三分支消解测试必须全绿。
2. **确定性优先**:改写、摘要、检索注入均为"增强项",任一环节失败都降级到现状路径;意图识别输入永远使用 `rawInput`。
3. **升级需确认 + 冷却**:继承路由层原则,记忆与检索的主动注入控制篇幅(≤3 条片段),不堆砌上下文。
4. **隐私内建(新增)**:跨用户数据在 SQL 层强制过滤 + Prompt 层约束声明,双闸门。
5. **异步化(新增)**:向量入库、偏好反射、摘要生成一律不阻塞回复主链路。
### 2.3 三层记忆模型定义(与优化思路逐条对齐)
| 记忆层 | 生命周期 | 承载内容 | 本系统落点 |
|---|---|---|---|
| **工作记忆** Working | 单轮~单任务 | 本轮任务正在处理的状态:活跃任务、已收集槽位、缺槽、挂起预期、交互卡片 | `chat_sessions.task_state`(JSONB)+ `UserProjectContext` 内存态 |
| **短期记忆** Short-term | 当前会话 | 最近 M 轮完整对话(滑动窗口双预算:轮数 + token) | `Session.Messages` 尾部切片 → `RecentDialogue`(修复断链) |
| **长期记忆** Long-term | 跨会话永久 | 用户画像(偏好/习惯/事实)、项目黑板、历史会话片段、标杆案例 | `user_preferences.MemoryFacts` + `project_blackboards` + `context_memory_chunks(domain=chat_history)` |
---
## 三、P0 阶段:断链修复(让既有设计真正跑通)
> 工作量约 0.5~1 天;不新增任何表与配置,只接线。这是全部后续工作的前提。
### 3.1 修复 G1:RecentDialogue 填充
**现状**:`Session.sendMessageInternal` 把消息 append 到 `s.Messages`(内存),但从不同步到 `s.UserCtx.RecentDialogue`。
**修复方案**(`internal/chat/session.go`,在构造本轮 `userMsg` 之前调用):
```go
// syncRecentDialogue 将会话内存消息同步到 UserCtx(供对话 LLM 与路由画像使用)
func (s *Session) syncRecentDialogue() {
if s.UserCtx == nil {
return
}
const maxTurns = 12 // 略宽于 dialogueHistoryMax=8,截断细节交给 prompt 层统一裁剪
msgs := s.Messages
if len(msgs) > maxTurns {
msgs = msgs[len(msgs)-maxTurns:]
}
turns := make([]coordinator.DialogueTurn, 0, len(msgs))
for _, m := range msgs {
if strings.TrimSpace(m.Content) == "" {
continue
}
turns = append(turns, coordinator.DialogueTurn{Role: m.Role, Content: m.Content})
}
s.UserCtx.RecentDialogue = turns
}
```
**连带收益**:`router.go:112` 的 `ProfileTask`"多轮追问深化"信号自动开始生效。注意该信号 +10 分可能改变升级建议触发频率,**需回归 `router_test.go` 与 `engine_test.go`**,必要时将信号阈值从 `>= 2` 提高到 `>= 4`(配置化)。
### 3.2 修复 G2:PersonalizedGuidance 填充
**现状**:`ctxmgr.PreferenceReflector.GeneratePromptGuidance`(`preference.go:87`)只被 `BuildBlendedSquadContext`(L3 链路)调用。
**修复方案**(`server.getOrCreateSession`,`ctxmgr` 需暴露公共封装方法 `CompilePromptGuidance(ctx, userID)`):
```go
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
if s.svc != nil {
sess.CtxMgr = s.svc.ContextManager()
if sess.UserID > 0 && sess.CtxMgr != nil { // 新增:注入偏好画像编译结果
if guidance, err := sess.CtxMgr.CompilePromptGuidance(ctx, sess.UserID); err == nil {
userCtx.PersonalizedGuidance = guidance
}
}
}
```
**刷新策略**:每 10 轮刷新一次,或用户显式表达定制要求("记住…/以后请…",`PreferenceReflector` 已能捕获此类信号)后的下一轮刷新;避免每轮查库。
### 3.3 修复 G3:服务重启会话恢复
**现状**:`RestoreSessionContext`(`chat_history.go:135`)功能完整但零调用;`server.sessions` 纯内存。
**修复方案**(`server.getOrCreateSession`):内存 map 未命中时,先查 `chat_messages` 是否已有该 sessionID 记录;有则:
1. `ctxMgr.RestoreSessionContext(ctx, sessionID)` 拉取历史(P1 完成后改走"摘要 + 窗口"混合拉取,避免 200 条全量);
2. 返回的 `Messages` 回填 `sess.Messages` 与 `userCtx.RecentDialogue`;
3. `Facts` 合入 `userCtx.BlackboardFacts`;
4. 从 `chat_sessions.task_state`(P1 新增列)恢复工作记忆。
同时顺带处理 G9 会话淘汰:`sessions` map 增加 LRU + 空闲 TTL(默认 120 分钟);被淘汰会话仅丢内存态,下次命中走恢复路径,行为等价。
### 3.4 P0 验收标准
- [ ] 连续 3 轮对话后,pi-agent prompt 的【近期对话】段非空(debug API 验证);
- [ ] 用户说"以后答复请简短一点",后续轮 prompt 出现偏好注入语;
- [ ] 重启服务后同 sessionID 继续对话,助手能引用重启前轮次内容;
- [ ] `go test ./internal/...` 全绿(含 router / engine 回归)。
---
## 四、P1 阶段:上下文管理(滑动窗口 + 历史压缩 + 状态跟踪)
### 4.1 滑动窗口装配器(SlidingWindowAssembler)
**新建** `internal/ctxmgr/window.go`。核心语义:**双预算裁剪**——`maxTurns`(轮数上限)与 `tokenBudget`(字符预算)同时生效;从最新一轮向前收纳,任一预算触顶即停。
```go
// WindowConfig 滑动窗口配置
type WindowConfig struct {
MaxTurns int // 窗口内最大消息条数(一问一答计 2 条),默认 16
TokenBudget int // 窗口内字符预算(近似 token),默认 3000
ReserveTail int // 为【本轮用户】与固定段预留预算,默认 800
}
// AssembleWindow 从完整消息序列裁出窗口
// 返回:窗口内消息 + 是否发生截断(截断则上层补【压缩记忆】摘要段)
func AssembleWindow(msgs []ChatMessageRecord, cfg WindowConfig) (window []ChatMessageRecord, truncated bool)
```
**token 估算约定(中文场景,零依赖)**:不引入 tokenizer 依赖,采用保守 rune 计数 `1 rune ≈ 1 token`(中文单字实际 1~2 token,配置默认值已含 1.5 倍冗余),比较 `Σ len([]rune(content))` 与 `TokenBudget` 即可。该约定写入代码注释,保证可解释、可单测。
**接线点**:`buildDialoguePrompt`(`dialogue_llm.go:186-199`)现有 `dialogueHistoryMax` 截断逻辑替换为 `AssembleWindow`,并输出**逐轮编号**:
```
【近期对话】(第 15~22 条,更早内容见上方摘要)
[21] 用户: 帮我看看当前项目的整体进展如何?
[22] 助手: 项目当前处于 V0.1 三角验证阶段……
```
轮次编号来源:DB 模式用 `chat_messages.id`,内存模式用 `Session.Messages` 索引。
### 4.2 历史压缩器(RollingSummarizer)
**新建** `internal/ctxmgr/summarizer.go` + 新表 `chat_summaries`(见 §8.1 迁移)。
**触发条件**(满足其一,在回复完成后的**异步钩子**中判断,不阻塞本轮回复):
- 会话消息总数超过 `SUMMARY_TRIGGER_MSGS`(默认 20 条 ≈ 10 轮);
- `AssembleWindow` 报告 truncated 且该会话尚无摘要(首次越界)。
**滚动摘要算法**:
```
输入: 旧摘要 S_old(可为空) + 本批被淘汰出窗口的消息 E(按时间序)
1. 硬事实提取:复用 intent 词典逻辑(device_type / target_market / budget /
timeline 等槽位)从 E 中抽取键值对 → 并入结构化事实区,永不参与压缩、永不丢失
2. 叙事摘要生成:
- 首选 LLM:pi-agent 小 prompt(输入 S_old + E → 输出 ≤200 字),
要求保留:讨论主题、已达成结论、未决问题、用户表达过的偏好
- 规则降级(LLM 不可用/30s 超时):抽取式——每条消息取首个分句 +
事实槽位清单拼接,硬截断 300 字
3. UPSERT chat_summaries(by session_id),更新 covered_count / covered_until
```
**摘要存储与注入格式一致**:
```json
{
"narrative": "本会话围绕XX项目立项摸底展开:确认器械类型为超声手术设备、目标市场国内+欧洲CE;讨论了GB 9706.1适用性;用户倾向先做V0.5方案框架。",
"facts": {"device_type": "超声手术设备", "target_market": "国内+欧洲CE"},
"open_questions": ["型检周期未确认"],
"covered_count": 14
}
```
```
【早期对话摘要】(第 1~14 条,已压缩)
主题与结论:……
已确认事实:device_type=超声手术设备; target_market=国内+欧洲CE
未决问题:型检周期未确认
```
**一致性保障**:黑板 `BlackboardFacts` 是唯一 SSOT;摘要中 `facts` 区仅作 prompt 展示,注入顺序上黑板置于摘要之后并声明"黑板事实优先级最高",冲突自动消解。
### 4.3 会话级任务状态跟踪(DST)
**新增列** `chat_sessions.task_state JSONB`,**新建** `internal/ctxmgr/taskstate.go`。
```go
// ConversationTaskState 会话级工作记忆。
// 与项目级黑板的分工:黑板是全项目多方共享 SSOT;
// task_state 是"本次对话任务进行到哪一步"的会话私有状态。
type ConversationTaskState struct {
ActiveTask string `json:"active_task,omitempty"` // 当前对话任务类型(如 case_compare / quote_refine / doc_reject_consult)
TopicAnchor string `json:"topic_anchor,omitempty"` // 主题锚点:最近一轮完整问句(供改写器引用)
TopicEntities []string `json:"topic_entities,omitempty"` // 锚点实体(项目名/器械/文档名,供代词消解)
CollectedSlots map[string]string `json:"collected_slots,omitempty"` // 已收集参数(复用 intent 槽位常量)
MissingSlots []string `json:"missing_slots,omitempty"` // 待收集参数(驱动澄清追问)
OpenQuestions []string `json:"open_questions,omitempty"` // 未决问题(摘要器同步)
UpdatedAt time.Time `json:"updated_at"`
}
```
**每轮更新时机**(`session.sendMessageInternal`,意图识别结果产出之后):
| 输入来源 | 更新动作 |
|---|---|
| `recRes.TopIntent.FilledSlots` | 合入 `CollectedSlots`(意图槽位是最可靠的状态增量) |
| `recRes.Branch == BranchExpectationHit` | 清理对应 `MissingSlots`,`ActiveTask` 转入执行态 |
| 改写器(P3)产出 | 更新 `TopicAnchor` / `TopicEntities` |
| 摘要器(§4.2) | 同步 `OpenQuestions` |
| `handleSupplementInfo` 黑板写入 | 双写 `CollectedSlots`(以黑板为准) |
**注入格式**(Prompt 的【工作记忆】段):
```
【本轮任务状态】
- 当前任务: 案例对比咨询
- 主题锚点: "对比A项目和B项目的报价策略"
- 已收集: target_stage=V0.5; budget=15万
- 待确认: 型检周期
```
**与既有 `PendingExpectation` 的关系**:`PendingExpectation` 管"上轮我建议了什么、本轮用户是否兑现"(短期挂起,已有 TTL 机制);`task_state` 管"这场对话的任务推进到哪一步"(全程状态)。两者并存、互不替代。
---
## 五、P2 阶段:检索增强(向量存储 + 按需检索 + 分层记忆)
### 5.1 对话记忆向量化入库(向量存储)
**复用** `context_memory_chunks` 表与 `IndexApprovedDocument` 的切片-嵌入-落库管线,**新增 domain = `chat_history`**。
**新建** `internal/ctxmgr/chat_memory.go`:
```go
// RecordTurnMemory 将一轮"问-答"对作为一条记忆切片异步入库
// 触发点:session.sendMessageInternal 回复完成后 go func()(不阻塞回复)
// 切片构造:user 问句 + assistant 答复首 300 字 拼接为 content;
// intent/branch 写入 metadata,供召回时按意图过滤
// chunk_hash = sha256(session_id + turn_no),天然幂等去重
func (m *ContextManager) RecordTurnMemory(ctx context.Context, sessionID string,
userID, projectID int64, userQuery, assistantReply, intentName, branch string)
```
**关键决策——Embedder 统一接线**:`ctxmgr` 当前默认 `DeterministicEmbedder`(离线兜底,`manager.go:46`)。改造:`bizdev.NewService` 中若 `cfg.EmbeddingEndpointURL` 非空,构造 `ctxmgr` 适配器包裹 `kb.Embedder`(同 bge-m3/1024 维),`SetEmbedder` 注入;KB 不可用时自动回落 Deterministic——保证向量库内域间向量空间一致(`approved_case` 与 `chat_history` 可同库检索)。
**重要性权重**:入库时 `importance_weight` 按 `CalculateDecayedWeight` 思路初始化——含槽位事实/决策类轮次 `raw_weight=1.2` 且 `is_milestone=true`(不衰减),普通问答 `raw_weight=0.6` 随半衰期衰减;检索排序时 `score = cosine × decayed_weight`,复用既有 `decay.go`。
### 5.2 按需检索与精准注入
**新建** `SearchChatMemory`(与 `SearchSimilarCases` 同构,`case_indexing.go:150` 可参考):
```go
type ChatMemorySearchOptions struct {
Query string
UserID int64 // 必填:数据权限闸门
ProjectID int64 // 可选:同项目公开记忆
SessionID string // 可选:排除当前会话(避免与窗口重复)或限定本会话
TopK int // 默认 3
MinScore float64 // 默认 0.25(Deterministic 兜底嵌入阈值需实测标定)
}
// SQL 层强制权限过滤(隐私第一闸门):
// WHERE (user_id = $1 OR (project_id = $2 AND visibility = 'project_public'))
```
**触发意图(按需,非每轮)**:
| 意图/分支 | 检索 query | 注入位置 |
|---|---|---|
| `QUERY_PROGRESS` / `QUERY_SIMILAR_CASES` | 改写后的完整问句(P3) | 【相关历史片段】段 |
| `AMBIGUOUS_FALLBACK`(模糊分忧) | 改写问句 | 同上,帮 LLM 理解语境 |
| `CASUAL_CHAT` 中命中升级画像信号 | rawInput | 同上(轻量,TopK=2) |
| `TRIGGER_ADHOC_TASK`(升级后) | task_brief | 小队上下文 memBank 追加 |
**注入格式(含来源与免责声明)**:
```
【相关历史片段】(按语义相关度召回,仅供参考,其中标注他项的内容禁止复述细节)
- [本会话·第9轮] 用户曾问"GB 9706.1 对超声设备适用吗",答复要点:适用,另需满足 YY 9706.102 EMC…
- [历史会话·2026-09-08] 与本主题相关的讨论:客户预算通常按千万级申报(来自用户本人历史)
- [项目公开·2026-09-06] 项目里程碑:完成V0.1需求收集表终审(作者:李工)→ 仅可引用结论,不得评价个人
```
**篇幅闸门**:每片段 ≤150 字摘要化注入,TopK ≤3,总预算 ≤600 字(计入 Prompt 总预算);超出即按分数截断。
### 5.3 三层记忆协同总编排(分层记忆落地)
**新建** `internal/coordinator/memory_composer.go`,替代 `buildDialoguePrompt` 中的散装拼接:
```
composeMemories(userCtx, session, taskState, summary, recalled, kbHits) → PromptSections
注入优先级(预算冲突时从下往上挤占):
1.【工作记忆】 task_state + PendingExpectation 摘要 + InteractiveCard 状态 ~200 字
2.【短期记忆】 滑动窗口近 M 轮(逐轮编号) ~3000 字
3.【压缩记忆】 早期轮次滚动摘要 ~300 字
4.【长期记忆-画像】 PersonalizedGuidance(偏好注入语) ~300 字
5.【长期记忆-检索】 相关历史片段 TopK≤3 ~600 字
6.【知识注入】 法规 KB 命中(仅特定意图) ~800 字
7.【项目上下文】 现有项目段(黑板/待办/阶段)——黑板事实声明为最高优先级
```
**与 L3 小队上下文的关系**:`BlendedSquadContext`(L3)维持现状;P2 的分层编排只作用于 L1/L2 对话链路。未来如需统一,可将 `MemoryComposer` 输出桥接进 `BuildBlendedSquadContext`,本期不做(防回归边界)。
**隐私第二闸门(Prompt 层)**:instructions 追加条款——"【相关历史片段】与【项目上下文】中涉及其他成员姓名、个人沟通习惯、私人事实的内容,仅可用于理解语境,**不得在答复中复述、评价或转述给当前用户**;引用项目事件只允许引用客观结论。"
---
## 六、P3 阶段:查询改写(改写补全 + 完整概念合成)
### 6.1 核心约束:改写不进意图识别链路
**第一原则**:三分支消解与 18 类意图识别的**确定性绝对不动**——`Recognizer.Recognize` 永远吃 `rawInput`。改写产物 `rewrittenQuery` 只流向三处:
1. KB 检索 / 记忆召回的 query(提升召回质量);
2. Prompt 的【本轮用户】段(`原文 + (补全后:…)` 双呈现);
3. `task_state.TopicAnchor` 更新(供下一轮改写引用)。
这直接继承意图识别框架报告的"规则识别 + 上下文约束 + LLM 仅作重写"哲学,也符合 router 层"默认即现状"防回归原则。
### 6.2 规则改写器(第一版,零 LLM、零时延)
**新建** `internal/coordinator/rewriter.go`(挂靠 coordinator,因其需要 UserProjectContext 与 task_state):
```go
// RewriteResult 查询改写产物
type RewriteResult struct {
Rewritten string // 补全后的完整问句(空 = 无需改写,用原文)
AppliedRules []string // 命中规则名(可解释、可观测)如 ["ellipsis_completion", "pronoun_resolution"]
}
// RewriteQuery 规则改写主入口
// 输入: rawInput + task_state(TopicAnchor/TopicEntities/CollectedSlots) + 最近一轮窗口
func RewriteQuery(rawInput string, st *ctxmgr.ConversationTaskState,
recent []DialogueTurn, proj ProjectInfo) RewriteResult
```
**规则集(首版 5 条,全部可单测)**:
| 规则 | 触发特征(确定性词典/模式) | 改写动作 | 例 |
|---|---|---|---|
| R1 省略补全 | 输入 ≤12 rune 且含"呢/也/还是/那/另外"或为纯实体名词 | `TopicAnchor 的谓语框架 + 本轮实体` 合成 | 上轮"北京天气怎么样"→本轮"上海呢" ⇒ "上海天气怎么样"(本项目对应形态:上轮"GB 9706.1 适用吗"→本轮"YY 0505 呢" ⇒ "YY 0505 适用吗") |
| R2 代词消解 | 含"它/他们/这个项目/该文档/那份报告/上次说的" | 指代替换为 `TopicEntities`(项目名/器械类型/文档标题,取最近距离实体) | "它的预算是多少" ⇒ "「超声手术设备项目」的预算是多少" |
| R3 主题延续合并 | 输入为短句且 `Branch=EXPECTATION_HIT` 或延续同主题追问 | `TopicAnchor + 本轮增量` 拼接为 standalone 问句 | 上轮"对比A和B的报价"→本轮"再加上C呢" ⇒ "对比A、B、C三家的报价" |
| R4 槽位上下文补全 | 检索类意图且 `CollectedSlots` 非空 | 追加已知约束后缀 | 检索 query 尾部拼 `(器械类型=超声手术设备; 市场=国内+CE)` |
| R5 完整概念合成 | 面向检索 query 的兜底:`rewritten := TopicAnchor + " " + rawInput` 去重词 | 检索专用,不进 Prompt | —— |
**实现要点**:
- 词典与 `intent` 包的既有模式词典风格一致(`recognizer.go` 的 `deliverableDocWords` 等),避免引入 NLP 依赖;
- `TopicAnchor` 维护规则:每轮若 `rawInput` ≥ 16 rune 且非纯代词/省略句,则更新为该轮完整问句;否则保持——这保证锚点始终是"最近一个完整表达";
- 命中 R1~R4 任一条时,Prompt 中【本轮用户】段输出双呈现,让 LLM 同时看到原文与补全(防止改写错误硬覆盖原意)。
### 6.3 LLM 改写器(第二版,可选增强)
规则改写覆盖高频形态;长尾复杂省略(如"那如果把预算砍一半呢,还是说先等等")由 LLM 改写兜底:
- 复用 `dialogueBackend`(pi-agent),独立小 prompt:输入 `TopicAnchor + 滑动窗口近 4 轮 + rawInput`,输出 standalone 问句(≤80 字);
- **超时 8s 降级**:放弃改写用原文(改写是增强项,绝不可阻塞回复);
- 结果进改写缓存(§8.3,key = `sha256(rawInput + TopicAnchor)`,上下文指纹变化自动失效);
- 开关 `REWRITE_LLM_ENABLED` 默认关闭,规则版先行验证收益后再开启。
### 6.4 与三分支消解的融合行为表
| 本轮输入形态 | 意图识别(不变) | 改写动作 | 对答复的影响 |
|---|---|---|---|
| "上海呢"式省略追问 | 若词典不中 → `AMBIGUOUS_FALLBACK` | R1 补全 → 检索/记忆召回 query 用补全句 | 模糊分忧回复能围绕正确主题引导,不再答非所问 |
| "它的预算呢"代词句 | 可能误中 `QUERY_PROGRESS` | R2 消解 → Prompt 双呈现 | LLM 答复引用正确实体 |
| "好,但市场改成欧洲" 复合补丁 | 现有 `PatchSlots` 机制正确处理 | 不改写(`EXPECTATION_HIT` 优先,改写器让位) | 行为与现状完全一致 |
| 完整新问题 | 正常分支 | 不改写(锚点更新) | 与现状一致 |
### 6.5 验收标准
- [ ] "GB 9706.1 适用吗"→"YY 0505 呢":第二轮答复围绕 YY 0505(E2E 断言);
- [ ] "它的预算是多少":答复中出现项目名实体而非"它";
- [ ] 改写命中率/规则分布可观测(debug API 返回 AppliedRules 统计);
- [ ] 改写器关闭(`REWRITE_ENABLED=false`)时全链路行为与 P2 完全一致。
---
## 七、P4 阶段:提示词与系统层(格式规范 + 行为约束 + 缓存复用)
### 7.1 对话 Prompt 结构化模板规范
**重构** `buildDialoguePrompt` → `MemoryComposer.Render()`,最终 prompt 布局(自上而下):
```
┌─────────────────────────────────────────────────────┐
│ ①【角色与边界】(instructions 固定段,现状保留增强) │ ← 身份/语气/行为方式/禁止事项
├─────────────────────────────────────────────────────┤
│ ②【本轮任务状态】工作记忆(§4.3) │
├─────────────────────────────────────────────────────┤
│ ③【项目上下文】(现状段落保留:项目/用户/客户/器械/ │
│ 阶段/待办/挂起建议)+ 黑板事实(声明最高优先级) │
├─────────────────────────────────────────────────────┤
│ ④【早期对话摘要】压缩记忆(§4.2,仅长会话出现) │
├─────────────────────────────────────────────────────┤
│ ⑤【近期对话】短期记忆(§4.1,逐轮编号 + 范围标注) │
├─────────────────────────────────────────────────────┤
│ ⑥【相关历史片段】按需检索(§5.2,≤3条,带来源/隐私 │
│ 标注/时间,仅特定意图出现) │
├─────────────────────────────────────────────────────┤
│ ⑦【参考知识】法规 KB 命中(§7.2,带 CiteKey 引用键) │
├─────────────────────────────────────────────────────┤
│ ⑧【用户偏好画像】长期记忆(PersonalizedGuidance) │
├─────────────────────────────────────────────────────┤
│ ⑨【本轮用户】原文 + (补全后:…)双呈现(P3) │
├─────────────────────────────────────────────────────┤
│ ⑩【答复要求】输出格式/篇幅/引用规范/隐私条款 │
└─────────────────────────────────────────────────────┘
```
**规范细则**(对应"历史对话格式规范"思路):
1. **分段硬分隔**:每段以 `【段名】` 起头 + 空行分隔,杜绝原文堆叠;
2. **逐轮编号 + 范围标注**:`[21] 用户: …`;段头注明 `(第 15~22 条,更早见摘要)`,让 LLM 明确知道"哪些是完整的、哪些是压缩的";
3. **来源标注强制**:检索片段必须带 `[来源·时间]` 前缀;KB 片段必须带 `CiteKey`(复用 `kb.formatCiteKey` 的 `〔GB 9706.1-2020 §8.4〕` 格式),并要求答复沿用同格式引用;
4. **优先级声明**:黑板事实 > 本轮用户输入 > 近期对话 > 摘要 > 检索片段(写入⑩段),防止 LLM 在冲突信息间随机取信;
5. **空段省略**:任一段无内容时整段省略(不留空标题),控制无效 token。
### 7.2 法规查询接入真 KB(修复 G7)
`handleQueryRegulations`(`engine.go:1379`)改造:
```
1. 检索 query = rewrittenQuery(P3)+ device_type/target_market 槽位(R4)
2. kbClient.Search(query, SearchOptions{Domains: [regulatory], Limit: 5})
—— Server 持有 svc,engine 需增挂 kb 检索能力(经 bizdevSvc 暴露或直接注入 kb.Client)
3. 命中 → ⑦【参考知识】段注入(每条 ≤200 字 + CiteKey)
LLM 生成答复(generateDialogueReply 复用)
4. 未命中 / KB 不可用 → 现有硬编码模板兜底(保留,作为降级路径)
```
同时 `AdvisorBrief.RecommendedCases` 之外的 KB 命中也走⑦段注入,答复要求"引用格式与 CiteKey 一致"——复用既有 `LookupCite`(`kb/cite_lookup.go`)可回查原文,形成"生成-引用-可回溯"闭环,直接强化"不得编造"约束的可验证性。
### 7.3 行为与隐私约束体系(提示词层)
**instructions 增补**(在现有 7 条基础上追加,保持编号连续):
```
8. 事实与引用:涉及法规标准、项目数据、历史结论时,优先依据【参考知识】与
【项目上下文】作答并沿用〔引用键〕格式;两者未覆盖且无把握时,明确告知
"暂无依据",不得编造标准编号、条款内容或项目事实。
9. 隐私与跨用户边界:【相关历史片段】【项目上下文】中涉及其他成员姓名、
个人沟通习惯、私人事实的内容仅用于理解语境,不得在答复中复述、评价
或转述;引用项目事件仅限客观结论(谁做了什么交付物可以,个人风格不行)。
10. 身份一致性:只以「项目助手」自称(现有条款重申),不得扮演医生/监管
审评员等法定角色给出诊疗或注册裁决性结论,法规解读须提示"以官方发文为准"。
```
**SQL 层隐私闸门**(§5.2 已定义)+ **Prompt 层闸门**(本节)双保险;另在 `chat_history` 域入库时即写入 `visibility`(默认 `private`,仅里程碑/客观交付结论类标记 `project_public`)。
### 7.4 多级缓存设计(缓存复用)
**新建** `internal/ctxmgr/cache.go`:统一 LRU + TTL 内存缓存(零依赖实现,容量与 TTL 可配置):
| 层级 | 缓存对象 | Key | TTL | 失效策略 | 预期收益 |
|---|---|---|---|---|---|
| C1 检索缓存 | `kb.Search` / `SearchChatMemory` 结果 | `sha256(domain+query+opts)` | 10min | TTL 自然过期;黑板写入不失效(KB 与黑板无关) | 重复问题零检索开销 |
| C2 改写缓存 | `RewriteResult` | `sha256(rawInput+TopicAnchor)` | 30min | 锚点变化 ⇒ key 变化自动失效 | 省改写时延(LLM 版尤甚) |
| C3 Embedding 缓存 | 文本→向量 | `sha256(model+text)` | 60min | TTL;模型切换清空 | 同 query 二次 embedding 归零 |
| C4 上下文组装缓存 | `UserProjectContext` 组装产物 | `user_id+project_id+session_id` | 5min | **黑板写入 / task_state 更新主动失效** | 每轮省 3~5 次 SELECT |
| C5 摘要读取缓存 | `chat_summaries` 行 | `session_id` | 5min | 摘要 UPSERT 主动失效 | 长会话每轮省 1 读 |
**实现要点**:
- 命中率与节省量指标接入 `stream.EmitDetail("cache_stats", …)` 与 `/health` 报表(`CacheHits/CacheMisses/SavedMs`);
- C4 的主动失效挂在 `UpdateProjectBlackboard` / `UpdateTaskState` 两个写入口(本包内回调,无循环依赖);
- 全部缓存可经 `CACHE_ENABLED=false` 一键关闭,行为退回现状;
- 并发安全:`sync.RWMutex` + 双检(与 `ContextManager` 既有风格一致)。
**不缓存项**(明确边界):对话 LLM 的最终回复不缓存(自然语言答复需结合最新状态,缓存易造成"复读旧结论"的合规风险;仅 C1~C4 为安全缓存面)。
---
## 八、数据模型与配置变更
### 8.1 SQL 迁移(`deploy/postgres/migrations/2026-09-10-chat-memory-and-dialogue-optimize.sql`)
```sql
-- =============================================================================
-- 多轮对话优化:滚动摘要 + 会话任务状态 + 对话记忆向量索引
-- 环境流转:dev → test → main(按三环境规范分别执行)
-- =============================================================================
-- 1. 会话滚动摘要表
CREATE TABLE IF NOT EXISTS chat_summaries (
id BIGSERIAL PRIMARY KEY,
session_id TEXT NOT NULL UNIQUE,
summary_text TEXT NOT NULL, -- narrative 叙事摘要
facts JSONB NOT NULL DEFAULT '{}'::jsonb, -- 结构化事实(永不压缩丢失)
open_questions JSONB NOT NULL DEFAULT '[]'::jsonb,
covered_count INT NOT NULL DEFAULT 0, -- 已覆盖的 chat_messages 条数
covered_until TIMESTAMPTZ,
model_source TEXT NOT NULL DEFAULT 'rule', -- rule | llm(可审计摘要来源)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 2. 会话表扩展工作记忆列
ALTER TABLE chat_sessions
ADD COLUMN IF NOT EXISTS task_state JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS turn_count INT NOT NULL DEFAULT 0;
-- 3. 对话记忆向量域支撑索引(context_memory_chunks 复用,新增 domain='chat_history')
CREATE INDEX IF NOT EXISTS idx_mem_chunks_user_domain
ON context_memory_chunks(user_id, domain);
CREATE INDEX IF NOT EXISTS idx_mem_chunks_chat_sess
ON context_memory_chunks(reference_id)
WHERE domain = 'chat_history';
-- 消息表会话时间索引(恢复路径高频查询)
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_time
ON chat_messages(session_id, created_at DESC);
```
**兼容性**:全部 `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`,可重复执行;不影响既有表数据;`context_memory_chunks` 无需结构变更(`domain`/`metadata` 字段现成)。
### 8.2 配置项(`internal/config/config.go` + `.env.example`)
```bash
# ---- 多轮对话记忆(新增区块)----
HISTORY_MAX_TURNS=16 # 滑动窗口消息条数上限
HISTORY_TOKEN_BUDGET=3000 # 滑动窗口字符预算(rune 近似 token)
SUMMARY_TRIGGER_MSGS=20 # 触发滚动摘要的消息条数阈值
CHAT_MEMORY_EMBED_ENABLED=true # 对话记忆向量化入库开关
CHAT_MEMORY_RECALL_TOPK=3 # 按需检索召回条数
CHAT_MEMORY_MIN_SCORE=0.25 # 召回相似度阈值
REWRITE_ENABLED=true # 规则改写器总开关
REWRITE_LLM_ENABLED=false # LLM 改写增强(默认关)
CACHE_ENABLED=true # 多级缓存总开关
CACHE_TTL_SEARCH_SECONDS=600
CACHE_TTL_REWRITE_SECONDS=1800
SESSION_IDLE_TTL_MINUTES=120 # 内存会话空闲淘汰
```
加载方式与既有 `MAX_CONCURRENT_AGENTS` 一致(`.env` 文件解析 + 环境变量覆盖双通道,`config.go:215` 同款 case 分支)。
### 8.3 代码落点清单(按包)
| 包 | 变更 | 类型 |
|---|---|---|
| `internal/ctxmgr` | `window.go`(新)/ `summarizer.go`(新)/ `taskstate.go`(新)/ `chat_memory.go`(新)/ `cache.go`(新)/ `manager.go`(+`CompilePromptGuidance`、+`UpdateTaskState`、+异步钩子挂点) | 新增为主 |
| `internal/coordinator` | `memory_composer.go`(新)/ `rewriter.go`(新)/ `dialogue_llm.go`(prompt 重构 + instructions 增补)/ `engine.go`(`handleQueryRegulations` 接 KB)/ `router.go`(多轮信号阈值配置化) | 新增+改造 |
| `internal/chat` | `session.go`(`syncRecentDialogue` + task_state 更新 + 异步入库/摘要钩子 + 恢复逻辑) | 改造 |
| `internal/server` | `server.go`(会话恢复 + LRU/TTL 淘汰 + guidance 注入)/ `handlers.go`(`GET /api/v1/chat/context-debug`) | 改造 |
| `internal/bizdev` | `service.go`(embedder 统一接线、kb 检索暴露给 coordinator) | 小改 |
| `web/src` | (可选)会话恢复提示、debug 面板 | 后置 |
---
## 九、实施路线图(五阶段,对齐"按功能层次拆分,过程逐步提交"规范)
> 全程在 `dev` 分支(或其 `feature/*` 子分支)进行;每阶段完成后跑全量 `go test ./internal/...`;阶段完成后合并 `dev`,累积至 `rc-vX.Y.Z` 进 `test` 环境实测。
| 阶段 | 工期 | 提交序列(对齐 README §四 提交规范) | 交付物 | 依赖 |
|---|---|---|---|---|
| **P0 断链修复** | 0.5~1 天 | ①`feat(agent): 会话近轮对话同步至对话LLM上下文` ②`feat(agent): 用户偏好画像注入对话链路` ③`feat(api): 服务重启会话恢复与内存淘汰` ④`test(agent): 多轮上下文接线回归` | RecentDialogue/PersonalizedGuidance/恢复链路全通 | 无 |
| **P1 上下文管理** | ~2 天 | ①`feat(db): 会话摘要与任务状态表迁移` ②`feat(agent): 双预算滑动窗口装配器` ③`feat(agent): 滚动摘要压缩器(LLM+规则降级)` ④`feat(agent): 会话任务状态跟踪DST` ⑤`feat(api): 上下文观测调试接口` ⑥`test(agent): 窗口/摘要/DST 单测与E2E` | 窗口+摘要+task_state | P0 |
| **P2 检索增强** | ~2 天 | ①`feat(db): 对话记忆向量索引迁移` ②`feat(agent): 对话记忆异步入库与统一Embedder` ③`feat(agent): 历史记忆按需检索与权限过滤注入` ④`feat(agent): 分层记忆编排器MemoryComposer` ⑤`test(agent): 记忆召回与隐私闸门测试` | chat_history 域 + 召回注入 | P1 |
| **P3 查询改写** | ~1.5 天 | ①`feat(agent): 规则查询改写器五规则` ②`feat(agent): 检索与Prompt接入改写产物` ③`test(agent): 改写规则矩阵单测` | rewriter + 行为表验证 | P1(锚点) |
| **P4 提示词+缓存+系统** | ~2 天 | ①`feat(agent): 对话Prompt十段式模板重构` ②`feat(agent): 法规查询接入知识库检索` ③`feat(agent): 多级缓存层C1-C5` ④`chore(agent): 会话TTL与turn目录清理` ⑤`test(agent): 缓存命中与失效测试` ⑥`docs(...): 本文档随实现修订` | 模板+KB接入+缓存 | P2、P3 |
**建议里程碑**:P0+P1 合并为第一个可发布增量(`rc-v1.1.0`:多轮连续性);P2+P3+P4 为第二个增量(`rc-v1.2.0`:记忆与检索智能)。
---
## 十、测试与验收标准
### 10.1 单元测试清单(新增)
| 模块 | 用例要点 |
|---|---|
| `window_test.go` | 双预算边界:轮数触顶、token 触顶、双触顶、空序列、单条超预算(截断该条并标 truncated);rune 计数含中文/emoji |
| `summarizer_test.go` | 触发阈值;LLM 成功路径(stub backend);LLM 失败→规则降级路径产物结构(narrative/facts/open_questions);滚动合并幂等(重复批次不重复计数);黑板事实冲突以黑板为准 |
| `taskstate_test.go` | 槽位合入/清理;EXPECTATION_HIT 后状态迁移;序列化往返(JSONB ↔ struct);旧数据兼容(空 task_state) |
| `chat_memory_test.go` | 入库幂等(同 turn_no 重放不重复);异步不阻塞(goroutine 完成断言);Decay 权重初始化(milestone 豁免);权限过滤 SQL(构造他用户记忆断言不可见) |
| `rewriter_test.go` | R1~R5 规则矩阵(每规则 ≥3 正例 + ≥2 反例);锚点维护(短句不更新锚点);EXPECTATION_HIT 让位;开关关闭等价现状 |
| `cache_test.go` | 命中/过期/主动失效(黑板写入后 C4 失效);LRU 容量淘汰;并发读写安全(`-race`) |
| `memory_composer_test.go` | 十段顺序与空段省略;预算挤占顺序(优先级 7→1);隐私标注存在性断言 |
| `session_test.go`(扩展) | `syncRecentDialogue` 截断长度;恢复路径回填;偏好注入触发 |
### 10.2 E2E 场景(扩展 `multi_turn_e2e_test.go` 风格,形成 `memory_e2e_test.go`)
1. **多轮连续性**:3 轮对话后第 4 轮引用第 1 轮内容(pi-agent 实机或 stub 断言 prompt 含【近期对话】非空段);
2. **重启恢复**:写库→重建 Server→同 sessionID 对话→答复引用重启前结论;
3. **长会话压缩**:模拟 30+ 条消息→断言 prompt 含【早期对话摘要】且窗口 ≤ 预算;关键事实(device_type)在摘要 facts 区保留;
4. **跨会话记忆召回**:会话 A 沉淀记忆→新会话 B 同主题提问→【相关历史片段】命中 A 的内容;
5. **省略追问改写**:"GB 9706.1 适用吗"→"YY 0505 呢"→答复围绕 YY 0505;
6. **隐私闸门**:用户 B 提问用户 A 的私人历史→【相关历史片段】不含 A 的 private 记忆;prompt 含隐私条款;
7. **法规查询真检索**:QUERY_REGULATIONS → 答复含 `〔引用键〕` 且可在 KB 回查(`LookupCite`);
8. **缓存收益**:同问题二连发→第二次 `cache_stats` 命中、检索调用计数为 0。
### 10.3 防回归红线(每阶段必跑)
- `go test ./internal/intent/... ./internal/coordinator/... ./internal/chat/... ./internal/ctxmgr/...` 全绿;
- 既有 18 意图 + 三分支用例**零修改**(若必须调整,须在 PR 说明中给出行为差异论证);
- `router_test.go`:`RecentDialogue` 修复后"多轮追问深化"信号首次真实生效——**预期需更新**该用例的输入构造(手工填充 `RecentDialogue` → 改为真实多轮驱动),属合法测试演进,需在提交信息标注;
- 三环境部署冒烟:`./scripts/env-service.sh dev start` → health → 5 轮对话 → 重启 → 续聊。
---
## 十一、风险与对策
| # | 风险 | 概率 | 影响 | 对策 |
|---|---|---|---|---|
| R1 | 改写破坏意图确定性(用户真实意图与补全句冲突) | 中 | 高 | 改写永不进识别链路;Prompt 双呈现原文+补全;`REWRITE_ENABLED` 一键关闭;R1~R4 全部可解释(AppliedRules 落 debug API) |
| R2 | 摘要丢失关键细节导致 LLM 误答 | 中 | 中 | 硬事实区永不压缩;黑板 SSOT 兜底;摘要注明覆盖范围轮次,LLM 可声明"早期细节已压缩" |
| R3 | 向量入库拖慢对话主链路 | 低 | 中 | 全异步 goroutine + 失败静默重试 1 次;`CHAT_MEMORY_EMBED_ENABLED` 可关 |
| R4 | 缓存脏数据(黑板已变、缓存仍旧) | 中 | 高 | C4 挂写入口主动失效;TTL 兜底;缓存键含上下文指纹;最终回复不缓存 |
| R5 | 跨用户隐私泄露 | 低 | 极高 | SQL 强制过滤 + visibility 标记 + Prompt 条款三层;E2E 场景 6 专项回归;审计日志记录召回来源 |
| R6 | pi-agent 摘要/改写调用失败或超时 | 中 | 低 | 摘要规则降级、改写直接用原文、8s/30s 硬超时——全部增强项可失联 |
| R7 | rune≈token 估算偏差导致 prompt 超限 | 中 | 低 | 预算默认值含 1.5x 冗余;`context-debug` 暴露实际 prompt 字数;超限时按优先级从第 7 段向上挤占 |
| R8 | 会话淘汰误伤活跃用户 | 低 | 中 | TTL 仅淘汰"空闲"会话;淘汰即落盘(消息已实时写库);恢复路径无缝续接(P0 已保障) |
| R9 | `context_memory_chunks` 双域数据量增长影响检索延迟 | 低 | 中 | `(user_id, domain)` 索引 + 部分索引;TopK 严格 ≤3;重量级召回走 pgvector 原生 ORDER BY(case_indexing 已验证模式) |
| R10 | 三环境迁移遗漏(dev 执行、test/main 未执行) | 中 | 中 | 迁移纳入 `deploy/postgres/migrations/` 既有流程;`schema_migrations` 记录;`env-service.sh` 启动时提示未执行迁移 |
---
## 十二、亮点与优势总结
1. **修复优先于新建**:P0 用 ~1 天接线让既有高质量部件(三分支、黑板、偏好反射、pgvector、RAG)真正协同——投入产出比最高的"神经手术",而非器官再造。
2. **确定性不妥协**:查询改写、摘要、检索注入全部定位为"增强层",意图识别输入永远是原文;每个增强项都有规则降级路径,pi-agent 失联时系统退化为"现状行为 + 窗口记忆",永不比今天更差。
3. **三层记忆语义清晰**:工作(task_state)/ 短期(滑动窗口)/ 长期(画像 + 向量库)职责分明,且与既有 `BlackboardFacts`(项目 SSOT)、`PendingExpectation`(挂起预期)既有概念不打架、可叠加。
4. **隐私双闸门内建**:SQL 权限过滤(第一闸)+ Prompt 边界条款(第二闸)+ visibility 数据分级,跨用户隔离不是事后补丁而是检索协议的一部分。
5. **全链路可解释可观测**:改写命中规则名、摘要来源(rule/llm)、召回片段来源与分数、缓存命中率——`context-debug` API + stream 事件让每一次"助手为什么这么说"都可回放。
6. **引用可回溯闭环**:法规答复强制 `〔CiteKey〕` 引用 + `LookupCite` 回查,"不得编造"从口号变成可机器验证的约束。
7. **零新依赖**:全部实现基于 Go 标准库 + 既有 pgx/pgvector;token 估算、LRU 缓存、规则改写均为自研轻量实现,延续本项目"零外部依赖、可 git 管理"的工程传统。
8. **渐进可回滚**:五阶段各自独立开关与迁移;任一阶段出问题可单独关闭回滚,不影响其余层。
---
## 十三、优化思路 → 方案条目总索引(速查表)
| 用户提出思路 | 对应方案条目 | 阶段 |
|---|---|---|
| 上下文管理:滑动窗口(轮数 + token) | §4.1 SlidingWindowAssembler 双预算 | P1 |
| 历史压缩(摘要保留语义丢弃细节) | §4.2 RollingSummarizer(硬事实区永不压缩) | P1 |
| 状态跟踪(参数字段追踪 + 每轮更新) | §4.3 ConversationTaskState DST | P1 |
| 向量存储(历史对话进外部记忆库) | §5.1 RecordTurnMemory(domain=chat_history) | P2 |
| 按需检索(每轮只召回最相关片段精准注入) | §5.2 SearchChatMemory + 意图触发表 + 篇幅闸门 | P2 |
| 分层记忆(工作/短期/长期) | §2.3 模型定义 + §5.3 MemoryComposer 总编排 | P1/P2 |
| 改写补全("上海呢"→"上海天气怎么样") | §6.2 R1 省略补全规则 | P3 |
| 完整概念(代词消解 + 意图合并为完整表达) | §6.2 R2 代词消解 / R3 主题延续合并 / R5 概念合成 | P3 |
| 身份角色 / 说话方式 / 行为方式 | §7.1 ①段(现状保留)+ §7.3 第 10 条身份一致性 | P4 |
| 历史对话格式规范(明确信息 + 分段分隔) | §7.1 十段式模板 + 逐轮编号 + 范围标注 + 优先级声明 | P4 |
| 行为约束(不编造 / 结合项目上下文) | §7.2 KB 接入 + §7.3 第 8 条事实与引用 | P4 |
| 隐私保护(区分不可泄露的其他用户内容) | §5.2 SQL 闸门 + §7.3 第 9 条 + visibility 分级 | P2/P4 |
| 缓存复用(缓存过往结果省时省算力) | §7.4 C1~C5 多级缓存 + 观测指标 | P4 |
---
## 附:本文档撰写依据的代码实勘清单
| 文件 | 关键证据 |
|---|---|
| `internal/coordinator/context.go:69-74` | `RecentDialogue`/`PersonalizedGuidance` 字段定义(无生产赋值方) |
| `internal/coordinator/dialogue_llm.go:15-20,61-77,118-205` | `dialogueHistoryMax=8`、instructions 人设、prompt 拼接 |
| `internal/coordinator/router.go:112` | `RecentDialogue >= 2` 多轮信号(断链未生效) |
| `internal/coordinator/engine.go:209,1379` | 案例检索注入;法规查询硬编码 |
| `internal/chat/session.go:88-190` | 单轮处理主流程(无 RecentDialogue 同步) |
| `internal/server/server.go:29,196-284` | sessions 内存 map(无 TTL/恢复) |
| `internal/ctxmgr/manager.go:46,67-144,194-268,412-437` | DeterministicEmbedder 默认、RecordUserMessage、GetUserContext、BlendedContext |
| `internal/ctxmgr/chat_history.go:82-200` | GetChatHistory / RestoreSessionContext(零调用) |
| `internal/ctxmgr/case_indexing.go:20-246` | 切片-嵌入-落库-召回完整范式(可复制到 chat_history 域) |
| `internal/ctxmgr/decay.go` / `preference.go` / `embed.go` | 时间衰减 / 偏好编译 / 向量接口 |
| `internal/intent/recognizer.go:25,838-` | 纯规则识别入口 / extractFactSlots 词典 |
| `internal/kb/retrieve.go,client.go,cite_lookup.go` | RAG 检索范式 / Client / 引用回查 |
| `internal/bizdev/service.go:52,65-102,160-198` | ctxMgr 构建 / 批准入库钩子 / KB 注入任务链路 |
| `deploy/postgres/context_init.sql` | 全部既有表结构 |
| `internal/agent/backend.go` | Backend 抽象 + 并发限流(无缓存) |
> 实勘时间:2026-09-10;基线 commit:`747cd39`(dev 分支)。