# RAG v2: BM25 + Embedding + Rerank 混合检索方案

## Context

当前 RAG 系统使用 BM25 关键词检索，存在核心缺陷：搜索"海上交通安全法与水污染防治法"时检索到行政检查清单而非法律原文，因为 BM25 只看关键词共现频率，无法理解语义。用户提议引入 qwen-embedding-v4 + qwen-rerank-8B + RRF 混合检索，预期显著提升检索准确性。

## 方案概览

```
查询
  ├─► BM25 检索 (现有 search_engine.py) → 排名列表 A
  ├─► 向量检索 (qwen-embedding-v4) → 排名列表 B
  └─► RRF 融合 (k=60) → top-20 候选
       └─► Rerank (qwen-rerank-8B) → 过滤 threshold=0.5 → top-5 结果
```

## 新增/修改文件清单

### 新增 3 个文件

| 文件 | 用途 |
|------|------|
| `qaagent67/scripts/search_engine_v2.py` | 混合检索引擎 (BM25+向量+RRF+Rerank) |
| `qaagent67/scripts/build_vector_index.py` | 向量索引构建 (重新分块+调 embedding API) |
| `qaagent67/scripts/rebuild_index_v2.py` | BM25 索引重建 (适配新分块参数) |

### 修改 2 个文件

| 文件 | 改动 |
|------|------|
| `qaagent67/agent-config.yml` | 新增 `rag.hybrid/embedding/rerank` 配置段 |
| `qademo/app.py` | 改 1 行 import: `from search_engine` → `from search_engine_v2` |

### 不动的文件 (保留)

| 文件 | 说明 |
|------|------|
| `qaagent67/scripts/search_engine.py` | 现有 BM25 引擎，被 v2 内部调用 |
| `qaagent67/scripts/rebuild_index.py` | 现有索引构建，保留为参考 |
| `knowledge/maritime/rag_index.pkl` | 现有 BM25 索引，保留为 fallback |
| `knowledge/maritime/rag_keywords.pkl` | 现有关键词索引，保留为 fallback |

## 详细设计

### 1. build_vector_index.py — 向量索引构建

**分块参数:**
- 分段符: `\n\n`
- 最大长度: 1024 token (≈1024 中文字符)
- 重叠: 200 token
- 保留表格完整性 (`[TABLE_START]...[TABLE_END]`)

**流程:**
1. 读 processed/*.txt → 用新参数分块 → ~50-60K chunks
2. 每 16 个 chunk 一批调 SiliconFlow embedding API
3. 保存: `rag_vectors_v2.npy` (numpy float32 [N, dim]) + `rag_vectors_meta_v2.pkl`
4. 同时构建新 BM25 索引: `rag_index_v2.pkl` + `rag_keywords_v2.pkl`
5. 支持断点续传 (checkpoint 每 500 批)

**API 调用:**
```
POST https://api.siliconflow.cn/v1/embeddings
{"model": "Pro/Qwen/Qwen3-Embedding-0.6B", "input": ["text1", ...], "encoding_format": "float"}
```

**预估:**
- ~55K chunks × 500 tokens = 27.5M tokens ≈ $0.30 一次性成本
- 3,437 批 × 0.1s = ~6 分钟

### 2. search_engine_v2.py — 混合检索引擎

**核心函数:**

```python
def search(query, top_k=5):
    # 1. BM25 检索 (调现有 search_engine.search)
    bm25_results = bm25_search(query, top_k=20)
    
    # 2. 向量检索 (embed query → cosine similarity)
    query_vec = _embed_query(query)
    vector_results = _vector_search(query_vec, top_k=20)
    
    # 3. RRF 融合
    fused = _rrf_fuse(bm25_results, vector_results, k=60)  # top-20
    
    # 4. Rerank
    reranked = _rerank(query, fused[:20])
    
    # 5. 过滤 threshold + 返回 top-k
    return [r for r in reranked if r['score'] >= 0.5][:top_k]
```

**降级策略:**
- 向量索引不存在 → 纯 BM25
- Embedding API 超时 → 纯 BM25
- Rerank API 超时 → 跳过 rerank，直接用 RRF 结果

**返回格式:** 与现有 `search()` 完全一致: `{source, chunk_id, score, text}`

### 3. agent-config.yml 新增配置

```yaml
rag:
  enabled: true
  collection: maritime_rag
  chunkSize: 1024
  chunkOverlap: 200
  chunkSeparator: "\n\n"
  topK: 5
  
  hybrid:
    enabled: true
    rrfK: 60
    rerankCandidates: 20
    similarityThreshold: 0.5
  
  embedding:
    model: Pro/Qwen/Qwen3-Embedding-0.6B
    baseUrl: https://api.siliconflow.cn/v1
    apiKeyEnv: SILICONFLOW_API_KEY
    batchSize: 16
  
  rerank:
    model: Pro/Qwen/Qwen3-Reranker-0.6B
    baseUrl: https://api.siliconflow.cn/v1
    apiKeyEnv: SILICONFLOW_API_KEY
    topN: 5
```

### 4. app.py 改动 (1 行)

```python
# Before:
from search_engine import search

# After:
try:
    from search_engine_v2 import search
except ImportError:
    from search_engine import search
```

### 5. 存储格式

| 文件 | 格式 | 大小估算 |
|------|------|---------|
| `rag_vectors_v2.npy` | numpy float32 [N, dim] | ~225 MB (55K×1024×4) |
| `rag_vectors_meta_v2.pkl` | pickle list of (source, chunk_id, text) | ~50 MB |
| `rag_index_v2.pkl` | pickle BM25 索引 | ~30 MB |
| `rag_keywords_v2.pkl` | pickle 关键词倒排 | ~20 MB |

**不依赖向量数据库** — numpy brute-force cosine similarity 对 55K chunks 只需 <100ms。

### 6. 依赖

仅新增 `numpy`，已在 Docker 镜像的 pip install 中。无需 chromadb/faiss 等。

### 7. Docker 部署

```bash
# 构建索引 (一次性)
docker exec qademo python3 /app/agentexample/qaagent67/scripts/build_vector_index.py

# 重启 Web 服务 (自动使用 v2)
docker restart qademo
```

环境变量: `SILICONFLOW_API_KEY=<your-key>` （申请: https://cloud.siliconflow.cn/account/ak ）

## 影响分析

| 方面 | 影响 |
|------|------|
| 现有 BM25 代码 | 不动，被 v2 内部调用 |
| Web 前端 | 无改动 |
| Wiki 模式 | 不受影响 |
| 查询延迟 | +200-500ms (embed+rerank API) |
| 首次加载 | ~2s (numpy mmap) vs 6s (pickle BM25) |
| 索引构建 | 一次性 ~6 分钟 + ~$0.30 |
| 每次查询成本 | ~$0.001 (1次embed + 1次rerank) |

## 验证方式

1. **单元测试:** `python search_engine_v2.py "海上交通安全法与水污染防治法"` — 应检索到法律原文
2. **对比测试:** 同一问题 v1 vs v2 结果对比
3. **Web 测试:** 在 http://qa.lambdagent.cn:8080 提问验证
4. **降级测试:** 停掉 SiliconFlow API，验证 fallback 到 BM25

## 实施顺序

1. 先验证 SiliconFlow embedding/rerank API 可用性 (curl 测试)
2. 更新 agent-config.yml
3. 编写 build_vector_index.py + 构建索引
4. 编写 search_engine_v2.py
5. 修改 app.py (1 行)
6. 部署到 Docker + 验证
