# 法规对话模块优化设计文档

**日期**: 2026-05-20  
**状态**: 已批准，实施中

---

## 背景

当前法规对话模块存在以下问题：
1. `compliance.py` `/compliance/chat/{segment_id}` 返回硬编码 Mock 数据
2. `rag.py` `/rag/chat` 返回硬编码 Mock 数据（前端实际调用 `/agent/chat/stream`，此路由可统一）
3. 前端快速问题列表硬编码在 `rag.ts`，未调用后端
4. 仅有 Dense 向量检索（COSINE），无 BM25 混合检索与精排
5. LLM 输出的 `[1][2]` 引用编号未在前端内联高亮
6. 会话存储仅为内存（100条上限，30分钟过期）

---

## 方案：分层优先（4个阶段）

### Phase 1 — 接入真实服务（Week 1）

**目标**：消灭所有 Mock，让系统真正可用。

#### 后端变更

**`backend/app/schemas/compliance.py`**
- `ComplianceChatRequest` 新增 `segment_context: str | None = None`

**`backend/app/api/routes/compliance.py`**
- 移除 `get_mock_compliance_chat_response` 导入
- `/compliance/chat/{segment_id}` 接入 `get_agent_conversation_service().stream_chat()`
- 将 `segment_context` 拼接到 query 前缀作为上下文
- 将 agent `content` 事件翻译为 `{"type":"chunk","text":"..."}` 格式，保持前端兼容

**`backend/app/api/routes/rag.py`**
- 移除 `get_mock_retrieval`、`get_mock_rag_answer` 导入
- `/rag/chat` 接入 `get_agent_conversation_service().stream_chat()`
- 翻译 agent 事件为 rag 格式（`retrieved`/`chunk`/`done`）

**`backend/app/schemas/rag.py`**
- `RagChatRequest` 新增 `session_id: str | None = None`，`filters: str | None = None`

#### 前端变更

**`frontend/src/api/rag.ts`**
- `getQuickQuestions()` 改为真实调用 `GET /api/v1/rag/quick-questions`，失败时降级为本地数组

**`frontend/src/api/compliance.ts`**
- `complianceChat()` 新增第三个参数 `segmentContext: string | undefined`，传入 request body

**`frontend/src/pages/Compliance/CompliancePage.tsx`**
- `sendChatMessage()` 中构建 `segmentContext`（intent + content 摘要 + 法规名）
- 传给 `complianceChat()`

---

### Phase 2 — 混合检索 + Reranking（Week 2-3）

**目标**：提升召回质量（BM25 + dense RRF融合）+ 精排。

#### 2a — Cross-Encoder Reranking（优先，无需 schema 变更）

- 新增端口 `backend/app/domain/retrieval/ports.py`: `Reranker` ABC
- 新增适配器 `backend/app/infrastructure/vectorstore/cross_encoder_reranker.py`
  - 调用 OpenAI-compatible reranker API（BAAI/bge-reranker-v2-m3）
- 修改 `KnowledgeRetrievalService`：先 retrieve top-20，再 rerank 到 top-5
- 新增 settings: `reranker_enabled: bool = False`，`reranker_model: str`，`reranker_top_k: int = 5`

#### 2b — Milvus Sparse BM25（需 schema 迁移）

- Milvus collection 新增 `sparse_embedding SPARSE_FLOAT_VECTOR` 字段
- 新增端口 `SparseEmbeddingProvider`（sparse embed 接口）
- 适配器优先使用 BGE-M3 API（同时输出 dense + sparse），不可用时降级为 TF-IDF keyword weights
- `MilvusVectorIndex.upsert()` 同时写入 sparse 向量
- `MilvusVectorIndex.search()` 改为 hybrid search（`WeightedRanker` 或 `RRFRanker`）
- 提供一次性迁移脚本：dump 所有 chunks → recreate collection → re-embed → re-insert

---

### Phase 3 — 引用溯源 + 筛选 UI（Week 4）

**目标**：答案文本中 `[1][2]` 可点击跳转原文片段；法规类型/版本可筛选。

#### 引用内联解析

- 新增 React 组件 `CitedAnswer`：接受 `text` + `sources[]`
  - 用正则 `/\[(\d+)\]/g` 拆分文本，将 `[N]` 渲染为可点击 `<cite>` 元素
  - 点击高亮/滚动到来源面板对应条目
- `RagChatPage.tsx` 将 assistant 消息改用 `CitedAnswer` 组件渲染
- 来源面板各条目加 `id="source-N"` 属性

#### 法规筛选 UI

- `RagChatPage.tsx` 在输入框上方加筛选栏：`regulation_type` 下拉 + `version` 输入
- 筛选值作为 `filters` 参数传到 `/agent/chat/stream`
- `MilvusVectorIndex.search()` 解析 `filters` 字符串为 Milvus expr（如 `regulation_type == "GB"`）

#### 快速问题动态化

- 后端 `/rag/quick-questions` 改为从 settings 或配置文件加载，不硬编码
- 前端已在 Phase 1 中改为调用后端

---

### Phase 4 — 会话持久化 + 上下文压缩（Week 5）

**目标**：长对话不丢失；上下文过长时智能压缩。

#### PostgreSQL 会话存储

- 新增 `backend/app/infrastructure/session/postgres_conversation_store.py`
  - 实现 `ConversationStore` port
  - 复用现有 PostgreSQL 连接
  - DDL: `conversation_sessions` + `conversation_messages` 两张表
- 新增 settings: `conversation_store_backend: str = "memory"`
- `bootstrap.py` 按 settings 切换实现

#### 上下文压缩

- 在 `AgentConversationService` 中，当对话轮数 > N 时，将早期消息用 LLM 摘要
  - 摘要替换为一条 `system` 消息 `"对话摘要：..."`
- 新增 `AnswerGenerator.summarize(messages) -> str` 方法

---

## 关键设计决策

| 决策 | 选择 | 原因 |
|------|------|------|
| BM25 实现 | Milvus sparse vectors | 不引入新服务，Milvus 2.4+ 原生支持 |
| Reranking | API-based cross-encoder | 复用现有 OpenAI-compatible 接口 |
| 会话持久化 | PostgreSQL | 复用现有 PostgreSQL 基础设施 |
| 引用解析 | 前端纯客户端 | LLM 已输出 [N] 编号，无需后端改动 |
| segment_context | 前端构建传后端 | 合规结果存在前端状态，无需后端持久化 |

---

## 验证标准

- Phase 1: 合规对话面板返回真实法规检索结果，不再固定响应
- Phase 2: 相同 query 在 hybrid 模式下比 dense-only 召回更相关结果
- Phase 3: 点击答案中的 `[1]` 能跳转到来源面板第一条
- Phase 4: 刷新页面后历史对话仍可恢复
