Files
mengyanote-rag/docs/RAG优化方案.md
shumengya c410d69970 feat: 初始提交 mengya-rag 知识库项目
轻量级 Obsidian Markdown RAG 系统,包含:
- Markdown 结构感知分块(标题层级 + 代码块/表格整体保留)
- FastEmbed + BAAI/bge-small-zh-v1.5 本地向量化
- SQLite + sqlite-vec 向量库(无需外部服务)
- BM25 + 向量混合检索,RRF 融合
- 盘点模式(有哪些/列表/目录类问题)
- DeepSeek API 生成回答
- mengya-rag CLI 工具(ask/search/context/read/sync/index/status)
- docs/RAG优化方案.md 待实施优化计划

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:12:06 +08:00

263 lines
7.5 KiB
Markdown
Raw Permalink 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.
# mengya-rag 知识库优化方案
> 记录日期2026-06-19
> 当前状态:待实施
> 目标:低成本 · 低幻觉 · 高命中
---
## 一、现状诊断
### 基础信息
| 项目 | 当前值 |
|------|--------|
| 笔记数量 | 495 篇 Markdown |
| Embedding 模型 | `BAAI/bge-small-zh-v1.5`90MB512 dim |
| 检索方式 | HybridBM25 + 向量RRF 融合 |
| LLM | DeepSeekdeepseek-v4-flash |
| 中文分词 | 自制 N-gram 滑窗 |
---
### 问题 1中文分词太弱最核心问题
当前 `chinese_tokenizer` 用的是 N-gram 滑窗,对技术笔记效果极差。
**示例**:搜索"GMP调度系统"
```
N-gram 分词结果:["Go", "Gol", "Gola", "olan", "lang", "中的", "的G", "GMP"]
jieba 分词结果:["Golang", "中", "GMP", "调度", "系统"]
```
N-gram 把"GMP"和"调度"拆散BM25 根本匹配不上完整关键词召回率大幅下降。495篇涵盖 Golang、Docker、C++ 等技术笔记,这个问题影响所有技术查询。
---
### 问题 2文件级强去重损失上下文
`_fuse_results` 里每个文件严格只返回 **1 个 chunk**
- "字节Agent中台一面凉经.md" 有 1459 行,答案往往跨多个章节
- 只拿最高分那一个 chunk丢失了大量关联内容
- 对长篇面经、长教程类笔记尤其严重
---
### 问题 3Prompt 没有引用约束
当前 Prompt 只说"根据以下内容回答"没有强制引用来源编号。DeepSeek 在上下文不完整时会"补充说明"而不是说"笔记中没有",导致幻觉。
---
### 问题 4注入上下文无 token 预算
所有检索到的 chunk 全量拼入 prompt。若 `top_k=6` 且每块 512 token注入了 3000+ token低相关内容稀释关键信息模型注意力分散精度反而下降同时增加 API 费用。
---
### 问题 5嵌入模型偏小
`BAAI/bge-small-zh-v1.5` 对纯中文 ok但笔记大量混合英文技术词汇Docker、Golang、nginx……小模型语义泛化能力有限向量召回对中英混合查询效果较差。
---
## 二、优化方案
### 方案总览
| # | 改动 | 影响文件 | 预期收益 | 优先级 |
|---|------|----------|----------|--------|
| ① | 中文分词换 jieba | `indexing.py``pyproject.toml` | BM25 关键词召回大幅提升 | 🔴 高 |
| ② | 文件去重改为最多 2 chunk/文件 | `retrieval.py` | 长文多节答题完整度提升 | 🟠 中 |
| ③ | QA Prompt 加编号引用 + 严格反幻觉 | `qa.py` | 幻觉率明显下降 | 🟠 中 |
| ④ | 注入上下文加 token 预算(≤ 2000 token | `qa.py` | 精度提升 + 节省 API 费用 | 🟡 中 |
| ⑤ | 换 `BAAI/bge-m3` 嵌入模型(可选) | `.env` | 中英混合语义理解提升 | 🟢 低 |
---
### ① jieba 中文分词(最高优先级)
**依赖**`pyproject.toml` 加入 `jieba>=0.42`
**改动位置**`src/mengya_rag/indexing.py``chinese_tokenizer()`
```python
# 旧实现N-gram 滑窗
def chinese_tokenizer(text: str) -> list[str]:
for part in re.findall(r"[A-Za-z0-9_#+.-]+|[一-鿿]+", text.lower()):
# N-gram 2~4 滑窗...
# 新实现jieba 分词
import jieba
STOPWORDS = {"", "根据", "笔记", "简要", "回答", "哪些", "什么", "内容", "一下", "我的", "有哪些", "是什么"}
def chinese_tokenizer(text: str) -> list[str]:
tokens = []
for part in re.findall(r"[A-Za-z0-9_#+.-]+|[一-鿿]+", text.lower()):
if re.fullmatch(r"[A-Za-z0-9_#+.-]+", part):
if part not in {"md", "markdown"} and part not in STOPWORDS:
tokens.append(part)
else:
for word in jieba.cut(part):
if len(word) >= 2 and word not in STOPWORDS:
tokens.append(word)
return tokens
```
**可选增强**:在 `data/` 目录放 `userdict.txt`,补充专有名词:
```
萌芽笔记 5
bigmengya 5
smallmengya 5
GMP调度 5
```
**注意**:改分词后必须重建索引(`mengya-build-index`)。
---
### ② 文件级去重放宽(最多 2 chunk/文件)
**改动位置**`src/mengya_rag/retrieval.py``HybridRetriever._fuse_results()`
```python
# 旧:严格 1 chunk/文件
seen_files: set[str] = set()
...
if rel_path in seen_files:
continue
seen_files.add(rel_path)
# 新:最多 2 chunk/文件
from collections import Counter
seen_files: Counter[str] = Counter()
...
if seen_files[rel_path] >= 2:
continue
seen_files[rel_path] += 1
```
---
### ③ QA Prompt 强化(反幻觉 + 强制引用)
**改动位置**`src/mengya_rag/qa.py``QA_PROMPT` + `format_context()`
```python
QA_PROMPT = """你是萌芽 RAG 知识库助手。请严格根据下方标有编号的笔记片段回答问题。
规则:
1. 回答中必须用 [来源N] 标注依据,例如"根据 [来源1] 可知……"
2. 若所有片段均无法支撑某个细节,必须明确说"笔记中未找到相关记录",不得凭己见补充
3. 回答要准确、完整,优先使用中文
笔记内容:
---------------------
{context}
---------------------
问题:{question}
回答:"""
def format_context(items: list[RetrievedNode]) -> str:
parts: list[str] = []
for index, item in enumerate(items, start=1):
rel_path = item.node.metadata.get("rel_path", "未知来源")
parts.append(
f"[来源{index}]《{rel_path}\n"
f"{item.node.get_content().strip()}"
)
return "\n\n".join(parts)
```
---
### ④ 上下文 Token 预算(硬限 2000 token
**改动位置**`src/mengya_rag/qa.py``format_context()``ask_question()`
```python
CONTEXT_TOKEN_BUDGET = 2000 # 可通过 env 配置
def format_context(items: list[RetrievedNode], budget: int = CONTEXT_TOKEN_BUDGET) -> str:
from .markdown_chunker import estimate_tokens
parts: list[str] = []
used = 0
for index, item in enumerate(items, start=1):
rel_path = item.node.metadata.get("rel_path", "未知来源")
content = item.node.get_content().strip()
block = f"[来源{index}]《{rel_path}\n{content}"
cost = estimate_tokens(block)
if used + cost > budget and parts:
break
parts.append(block)
used += cost
return "\n\n".join(parts)
```
---
### ⑤ 换更大嵌入模型(可选,需重建索引)
修改 `.env`
```diff
-EMBED_MODEL=BAAI/bge-small-zh-v1.5
+EMBED_MODEL=BAAI/bge-m3
```
| 对比项 | bge-small-zh | bge-m3 |
|--------|-------------|--------|
| 大小 | ~90 MB | ~570 MB |
| 维度 | 512 | 1024 |
| 语言 | 纯中文 | 中英多语言 |
| 混合技术词召回 | 一般 | 好 |
改完后执行:
```bash
mengya-build-index
```
---
## 三、实施顺序建议
```
第 1 步:换 jieba 分词(改代码 + 重建索引) ← 收益最大
第 2 步:放宽文件去重 + 加 token 预算 ← 改代码,立即生效
第 3 步:加强 Prompt ← 改代码,立即生效
第 4 步(可选):换 bge-m3 嵌入模型 ← 改配置 + 重建索引
```
---
## 四、改动文件清单
```
pyproject.toml ← 加 jieba 依赖
src/mengya_rag/indexing.py ← chinese_tokenizer 换 jieba
src/mengya_rag/retrieval.py ← _fuse_results 去重策略
src/mengya_rag/qa.py ← QA_PROMPT + format_context token 预算
data/userdict.txt ← 可选jieba 自定义词典
.env / .env.example ← 可选EMBED_MODEL 升级
```
---
## 五、重建索引命令
改完代码后执行:
```bash
# 只重建索引(笔记已在本地)
mengya-build-index
# 或先同步笔记再重建
mengya-rag reindex
```