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

7.5 KiB
Raw Permalink Blame History

mengya-rag 知识库优化方案

记录日期2026-06-19
当前状态:待实施
目标:低成本 · 低幻觉 · 高命中


一、现状诊断

基础信息

项目 当前值
笔记数量 495 篇 Markdown
Embedding 模型 BAAI/bge-small-zh-v1.590MB512 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.pypyproject.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.pychinese_tokenizer()

# 旧实现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.pyHybridRetriever._fuse_results()

# 旧:严格 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.pyQA_PROMPT + format_context()

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.pyformat_context()ask_question()

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

-EMBED_MODEL=BAAI/bge-small-zh-v1.5
+EMBED_MODEL=BAAI/bge-m3
对比项 bge-small-zh bge-m3
大小 ~90 MB ~570 MB
维度 512 1024
语言 纯中文 中英多语言
混合技术词召回 一般

改完后执行:

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 升级

五、重建索引命令

改完代码后执行:

# 只重建索引(笔记已在本地)
mengya-build-index

# 或先同步笔记再重建
mengya-rag reindex