目录
前面几篇已经把 RAG 的基本链路跑通了。
但只要真正试几次就会发现:RAG 的关键不只是“能检索”,而是“检索得准不准”。
同样一个问题,top_k 不同、chunk 大小不同、是否按分类过滤,最后给模型的上下文都会不一样,回答质量也就不一样。
这一篇就专门做 RAG 检索质量调优。
本文对应代码位于 code/langchain-demo/chapter07。
本篇目标
这一篇不引入新的大组件,主要围绕三个参数做实验:
category:只在某个博客分类下检索。top_k:控制返回多少个片段。chunk_size和chunk_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 不是给用户看的,而是给开发者调试用的。
如果发现回答不对,可以先看:
- 命中了哪些关键词。
- 来源文章是否符合预期。
- 是否需要调整分类或 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 应用来说,最基本的排查顺序可以是:
- 先看检索结果对不对。
- 再看 chunk 是否完整。
- 再看 top_k 是否过多或过少。
- 最后再调整 prompt。
不要一上来就怪模型。
很多时候,模型回答不好,是因为我们给它的上下文本来就不够好。
参考
code/langchain-demo/chapter07- LangChain
RecursiveCharacterTextSplitter - RAG 检索参数调优
原创文章,转载请注明来源: LangChain 入门学习第七篇-RAG 检索质量优化
相关文章
- LangChain 入门学习第六篇-持久化索引和本地知识库问答
- LangChain 入门学习第五篇-使用本地 Embeddings 实现向量检索 RAG
- LangChain 入门学习第四篇-基于本地 Markdown 的 RAG 问答
- LangChain 入门学习第三篇-工具调用和 Agent
- LangChain 入门学习第二篇-PromptTemplate 和结构化输出
- LangChain 入门学习第一篇-环境准备和模型调用
- 使用 Cursor 进行 AI 编程的年度总结
- 真的,AI 可能就是新时代的信息差
- 充值 Cursor 之后,工作有了哪些变化?🤔
- 个人'蒸馏'大模型能做哪些有意思的事情