目录

一个专注音视频领域的小圈子

前面几篇已经把 RAG 的基本链路跑通了。

但只要真正试几次就会发现:RAG 的关键不只是“能检索”,而是“检索得准不准”。

同样一个问题,top_k 不同、chunk 大小不同、是否按分类过滤,最后给模型的上下文都会不一样,回答质量也就不一样。

这一篇就专门做 RAG 检索质量调优。

本文对应代码位于 code/langchain-demo/chapter07

本篇目标

这一篇不引入新的大组件,主要围绕三个参数做实验:

  1. category:只在某个博客分类下检索。
  2. top_k:控制返回多少个片段。
  3. chunk_sizechunk_overlap:控制文档切分方式。

运行命令类似这样:

uv run python -m chapter07.quality_rag \
  "FFmpeg 硬解码相关内容有哪些?" \
  --category ffmpeg \
  --top-k 4

它会输出问题、调参信息、命中的关键词、来源文章和最终回答。

为什么要加分类过滤

博客文章数量多了以后,不同分类里可能有相同关键词。

比如“函数”“模板”“编译”“解码”这些词,在不同技术方向里都可能出现。如果不加范围限制,检索结果就可能混入不相关内容。

第七篇在读取文档时,把分类也写进 metadata:

metadata={
    "source": str(path.relative_to(REPO_ROOT)),
    "title": title,
    "category": path.relative_to(BLOG_POST_ROOT).parts[0],
}

这里的 category 来自路径:

blog/content/post/ffmpeg/...
blog/content/post/android/...
blog/content/post/code/...

这样提问 FFmpeg 相关问题时,可以指定:

--category ffmpeg

让检索范围更聚焦。

top_k 的影响

top_k 表示返回前几个检索结果。

它不是越大越好。

如果太小,可能漏掉重要资料;如果太大,prompt 里会塞入太多噪声,模型反而容易被干扰。

第七篇把它做成命令行参数:

parser.add_argument("--top-k", type=int, default=4)

然后传给检索函数:

relevant_chunks = retrieve(
    args.question,
    chunks,
    top_k=args.top_k,
    category=args.category or None,
)

对于教程阶段,top_k=4 是一个比较容易观察的默认值。

chunk_size 和 chunk_overlap

RAG 里还有两个很容易被忽略的参数:

parser.add_argument("--chunk-size", type=int, default=900)
parser.add_argument("--chunk-overlap", type=int, default=150)

chunk_size 太小,单个片段信息不完整。

chunk_size 太大,检索命中后会带入很多无关内容。

chunk_overlap 的作用是让相邻片段之间保留一部分重复内容,避免关键句子刚好被切开。

切分代码如下:

splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap,
    separators=["\n\n", "\n", "。", ",", " ", ""],
)

这里的 separators 也做了中文场景的适配,优先按段落、换行、句号、逗号切分。

标题加权和来源去重

第七篇的检索函数比第四篇稍微加强了一点。

标题命中权重更高,路径命中也会加分:

title_score = sum(title.count(token) for token in tokens) * 5
source_score = sum(source.count(token) for token in tokens) * 2
content_score = sum(content.count(token) for token in tokens)
score = title_score + source_score + content_score

另外还做了来源去重:

return _dedupe_by_source([chunk for _, chunk in scored_chunks], top_k)

这个很重要。

如果不去重,很容易出现同一篇文章的多个 chunk 占满所有结果。模型看到的上下文虽然相关,但覆盖面太窄。

去重后,优先让不同文章都有机会进入上下文。

输出调试信息

为了方便观察检索质量,demo 会打印调参信息:

Tuning:
- category: ffmpeg
- top_k: 4
- chunk_size: 900
- chunk_overlap: 150
- matched_keywords: 解码:45, ffmpeg:25, 硬解码:3

这里的 matched_keywords 不是给用户看的,而是给开发者调试用的。

如果发现回答不对,可以先看:

  1. 命中了哪些关键词。
  2. 来源文章是否符合预期。
  3. 是否需要调整分类或 top_k。

这比直接盯着最终回答猜原因要高效很多。

运行 demo

code/langchain-demo 目录下运行:

uv run python -m chapter07.quality_rag \
  "FFmpeg 硬解码相关内容有哪些?" \
  --category ffmpeg \
  --top-k 4

也可以尝试修改参数:

uv run python -m chapter07.quality_rag \
  "FFmpeg 硬解码相关内容有哪些?" \
  --category ffmpeg \
  --top-k 6 \
  --chunk-size 1200 \
  --chunk-overlap 200

对比不同输出,就能直观看到参数对检索结果的影响。

小结

这一篇没有引入新概念,而是把 RAG 里最容易影响效果的几个参数拿出来调了一遍。

对于一个 RAG 应用来说,最基本的排查顺序可以是:

  1. 先看检索结果对不对。
  2. 再看 chunk 是否完整。
  3. 再看 top_k 是否过多或过少。
  4. 最后再调整 prompt。

不要一上来就怪模型。

很多时候,模型回答不好,是因为我们给它的上下文本来就不够好。

参考

  • code/langchain-demo/chapter07
  • LangChain RecursiveCharacterTextSplitter
  • RAG 检索参数调优

原创文章,转载请注明来源:    LangChain 入门学习第七篇-RAG 检索质量优化