LangChain 学习笔记 · Day 11 检索器进阶(Retriever)
系统掌握 similarity / MMR / similarity_score_threshold 三种检索模式与常用参数(k、fetch_k、lambda_mult、score_threshold),用 langchain_chroma + Qwen 向量做可切换实验,并为 Day 12 的 RAG 闭环做准备。
18 分钟阅读
LangChainPythonRetrieverMMRsimilarity_score_thresholdChromaQwenRAG
🎯 学习目标
- 搞清三种检索模式的原理与差异:
similarity(相似度前 k)mmr(Maximal Marginal Relevance,多样性重排)similarity_score_threshold(相似度阈值过滤)
- 熟练掌握
as_retriever()的常用参数:k / fetch_k / lambda_mult / score_threshold。 - 知道什么时候用谁,如何快速调参。
- 为 Day 12 的 RAG 闭环 打基础(检索更稳,回答才稳)。
🧭 与 Day 10 的衔接
- Day 10:完成向量化 → Chroma 持久化 → 基础检索,并体验了 MMR。
- Day 11:把 检索策略吃透,补齐阈值过滤与参数取值经验。
- 你的观察:MMR 的 Top-k 更分散(覆盖更多小节/角度),这是正确直觉 ✅。
🧠 三种检索模式一览
1) similarity(kNN)
- 选“与问题最相似”的前 k 个文档。
- 优点:定位单一事实很准。
- 缺点:容易扎堆在同一片段附近,覆盖面窄。
2) mmr(Maximal Marginal Relevance)
- 在“相关性”与“多样性”之间找平衡:
- 既像问题,又彼此不太像 → 信息更全,答案更稳。
- 关键参数:
lambda_mult(0.3–0.7 常用,0.5 起步),fetch_k(候选池大小,4–8×k)。
3) similarity_score_threshold(阈值过滤)
- 先按相似度排序,再丢掉低于阈值的结果。
- 适合“宁缺毋滥”的场景(如行业问答,误召回代价高)。
- 关键参数:
score_threshold(0.3–0.5 常用,分数越接近 1 越相关)。
⚙️ 依赖与准备
沿用 Day 10 环境(已具备以下依赖与数据):
pip install -U langchain langchain-community langchain-text-splitters \
langchain-chroma chromadb dashscope python-dotenv
# 向量库目录:chroma_qwen_day10;集合:notes_day10_qwen
🧪 实战:可切换的检索实验脚本
保存为
day11_retriever_lab.py;复用 Day10 的向量库(无需重建)。
# -*- coding: utf-8 -*-
"""
Usage:
python day11_retriever_lab.py sim "Day9 的切分参数怎么选?" --k 4
python day11_retriever_lab.py mmr "Day9 的切分参数怎么选?" --k 4 --fetch_k 24 --lambda_mult 0.5
python day11_retriever_lab.py thresh "Day9 的切分参数怎么选?" --k 8 --score_threshold 0.35
"""
import os, argparse
from typing import List, Tuple
from dotenv import load_dotenv; load_dotenv()
from langchain_chroma import Chroma
from langchain_community.embeddings import DashScopeEmbeddings
CHROMA_DIR = "chroma_qwen_day10"
COLLECTION = "notes_day10_qwen"
EMBED_MODEL = "text-embedding-v2"
def _emb():
key = os.getenv("DASHSCOPE_API_KEY")
if not key:
raise RuntimeError("缺少 DASHSCOPE_API_KEY")
return DashScopeEmbeddings(model=EMBED_MODEL, dashscope_api_key=key)
def _db():
return Chroma(
persist_directory=CHROMA_DIR,
collection_name=COLLECTION,
embedding_function=_emb(),
)
def _print_with_scores(items: List[tuple], max_len=100):
for i, (doc, score) in enumerate(items, 1):
src = doc.metadata.get("source", "unknown")
tx = doc.page_content[:max_len].replace("\n", " ")
print(f"{i}. score={score:.3f} | {src} | {tx}…")
def _print_docs(items: List, max_len=100):
for i, doc in enumerate(items, 1):
src = doc.metadata.get("source", "unknown")
tx = doc.page_content[:max_len].replace("\n", " ")
print(f"{i}. {src} | {tx}…")
def run_similarity(q: str, k: int):
db = _db()
items = db.similarity_search_with_relevance_scores(q, k=k)
print(f"🔎 similarity | k={k}")
_print_with_scores(items)
def run_mmr(q: str, k: int, fetch_k: int, lambda_mult: float):
db = _db()
retriever = db.as_retriever(
search_type="mmr",
search_kwargs={"k": k, "fetch_k": fetch_k, "lambda_mult": lambda_mult},
)
docs = retriever.invoke(q) # retriever: 不带分数
print(f"🔎 mmr | k={k}, fetch_k={fetch_k}, lambda={lambda_mult}")
_print_docs(docs)
def run_threshold(q: str, k: int, score_threshold: float):
db = _db()
retriever = db.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"k": k, "score_threshold": score_threshold},
)
docs = retriever.invoke(q)
print(f"🔎 similarity_score_threshold | k={k}, score_threshold={score_threshold}")
_print_docs(docs)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mode", choices=["sim", "mmr", "thresh"])
ap.add_argument("question", type=str)
ap.add_argument("--k", type=int, default=4)
ap.add_argument("--fetch_k", type=int, default=24)
ap.add_argument("--lambda_mult", type=float, default=0.5)
ap.add_argument("--score_threshold", type=float, default=0.35)
args = ap.parse_args()
if args.mode == "sim":
run_similarity(args.question, args.k)
elif args.mode == "mmr":
run_mmr(args.question, args.k, args.fetch_k, args.lambda_mult)
else:
run_threshold(args.question, args.k, args.score_threshold)
if __name__ == "__main__":
main()
🔧 调参与选型指南
- 单一事实/定位明确 →
similarity - 概览/跨小节/对比类 →
mmrlambda_mult = 0.5起步;更分散降到0.3,更集中升到0.7fetch_k = 4~8×k:先“高回收”,再“多样性重排”
- 宁缺毋滥 →
similarity_score_threshold(0.3~0.5试探) - k 值:2~8 常用;
k↑覆盖更全但噪声变多 - 前提:嵌入质量与切分仍是地基(Day 10 的 Qwen v2 + RCTS 参数)
🧯 常见问题
- 为什么 MMR 没有分数?
- MMR 是在候选集中做多样性重排,关注“集合的互补性”,不是单点打分。
- 阈值过滤后返回 0 条怎么办?
- 适当降低
score_threshold或把k提高;也可先用similarity打印分数分布再设阈值。
- 适当降低
- 返回片段仍有重复信息?
- 降低
lambda_mult(如 0.3),或增加fetch_k。
- 降低
- 跨文件检索时来源混乱?
- 在入库前把
metadata补充完整(title/page/section),检索时一并打印核对。
- 在入库前把
🧪 今日练习
- 针对同一问题分别跑
sim / mmr / thresh,对比 Top-k 的“分散度”和覆盖面。 - 调整
lambda_mult(0.3/0.5/0.7)与fetch_k(16/24/40),观察变化。 - 用
similarity_search_with_relevance_scores打印分数分布,确定一个合适的score_threshold。 - 给检索结果补打 来源引用(
[n])并和生成式回答串起来(承接 Day 10 的问答链)。
✅ 今日收获(TL;DR)
- similarity:最相关的前 k,精准但易扎堆。
- MMR:相关性 × 多样性 → 更稳定、覆盖更广。
- similarity_score_threshold:阈值过滤,宁缺毋滥。
as_retriever(...).invoke()是新推荐写法;k/fetch_k/lambda_mult/score_threshold是四个关键旋钮。
🔮 明日预告(Day 12)
- 把“检索→重排(可选)→引用→回答格式化”串成稳定的 RAG Pipeline:
- 引用对齐、片段拼接策略、答复模板化与评测小脚本(命中率/覆盖度)。