☰
LangChain RAG实战:从组件选型到混合检索与调优
2026/10/23 5:15:06 网站建设 项目流程

我们做知识库问答类应用的时候,最常见的一句话是:"文档我都给模型了,它怎么还在瞎编?"刚开始接触RAG(检索增强生成,Retrieval-Augmented Generation)的时候,我也天真地以为,只要把PDF扔给LangChain,接上向量数据库,大模型就能一本正经地引用我的内部资料。实际跑完才知道,这条链路上每一个环节都有它自己的脾气,稍有不慎,召回的内容和最终回答就会跑偏。

这篇文章我想以"AI大模型之旅——LangChain RAG"为主线,完整记录我在这套技术栈上的真实实践:从组件选型、参数设置、代码实现,到踩坑排错的完整链路。适合刚学会LangChain基本调用、正准备搭建私有知识库问答系统的开发者,也适合已经跑通Demo、但想让回答更稳定的同学。文章不涉及复杂的数学推导,重点放在"当时为什么这么做"和"换一个场景该怎么做"上。

1. 为什么有知识库还要引入RAG:大模型原生能力的边界

先聊一个基础但必须想清楚的问题:既然大模型参数里已经装了大量知识,为什么我们还要多此一举,先把文档切片、向量化、再检索一遍?这个问题想不通,后面的架构设计就容易走极端。

1.1 大模型的"记忆"不是实时数据库

大模型的知识截止到训练数据那一刻,它不知道你昨天刚更新的产品文档,也不了解你公司的内部流程手册。就算你用提示词把资料贴进上下文,上下文窗口再长也有限,而且塞满垃圾信息后,模型回答的准确性和条理性会明显下降。你可以把大模型看成一位知识渊博但从不查资料的同学,你问它历史事件它能答得不错,但问它你们团队新定的接口规范,它就只能靠猜。

RAG的思路很朴素:先到外部知识库里把相关内容"找"出来,再让大模型基于这些找到的内容来回答。它改变的并不是大模型本身,而是给大模型配了一个"随用随查"的资料助手。这样知识可以随时更新,不用重新训练模型,成本低得多,也解决了"模型知识停留在过去"的致命伤。

我见过不少团队一上来就问:"那直接微调模型是不是更好?"我的经验是,如果知识库里的内容每周都在变,或者你压根没有足够的高质量语料去微调,RAG几乎是唯一理性选择。微调适合的是"说话风格、输出格式、领域术语习惯"这类稳定的能力迁移,不适合频繁变化的事实类知识。

1.2 直接微调与RAG的分工:不要一上来就微调

这里有一个容易被忽略的常识:微调一个7B参数量的模型,即使只是LoRA(低秩适配),也需要清理数据、准备训练脚本、调节点,一整套下来至少要一两周。而RAG从零搭起来,一个下午能把Demo跑通。更关键的是,微调并不能保证模型记住文档里的精确数字和条款,它学的是"统计规律",不是"查字典"。

所以我在实际项目中给团队的默认建议是:事实类问答,先上RAG;稳定风格的生成任务,再考虑微调;两者也可以共存,用RAG拿到事实,再让微调过的模型用特定口吻输出。RAG负责"查得到",模型负责"说得好"。

1.3 RAG的一条数据流转主线

RAG的链路听起来简单,但每个环节都会影响最终效果。通俗点说,整条主线是:

  1. 文档加载:把PDF、Word、网页、数据库里的内容读进来,转成纯文本。
  2. 文本切分:把长文本切成一块块适合检索和送入模型的小段。
  3. 向量化:用Embedding模型把每个小段变成一串浮点数,也就是向量。
  4. 存储入库:把向量和原文一起存进向量数据库。
  5. 检索召回:把用户问题也转成向量,在库里找最相似的一批片段。
  6. 合成回答:把召回的片段拼进提示词,让大模型据此生成答案。

这六步每一环都影响最后的质量。比如切分不合理,信息被"拦腰斩断",后面的检索和生成怎么努力都白搭。后面我会按这条主线逐一展开,把我踩过的坑一并写出来。

2. LangChain RAG的五个核心组件与取舍逻辑

LangChain在这里的作用,是把上面六步的代码包装成一个个可组合的模块。你不用自己写PDF解析器和向量检索逻辑,但它也有一个"坑",就是文档太抽象,容易让人只调API不知道底层发生了什么。所以我会把五个核心组件逐个讲清楚,重点说为什么这么选。

2.1 文档加载器:不止是读文件,还涉及源格式解析

LangChain的DocumentLoader看似简单,就是把文件读进来变成Document对象,里面是page_content和metadata。但真正干活的时候,你会发现难点全在"文件本身"。

我实测过的几个场景供参考:

  • PDF:如果文字是真实文本,用PyPDFLoader可以;如果是扫描件,必须走OCR,否则切出来的内容全是乱码。
  • Word文档:Docx2txtLoader能提取段落,但表格内容经常被打散,建议先把表格区域转成Markdown表格,再交给切分器。
  • 网页:WebBaseLoader方便,但页面里的导航、广告、脚本文字都会被带进来。这类噪音比想象中影响大,最好自己加一层正文提取逻辑。
  • Markdown/代码仓库:可以直接用TextLoader,但要留意编码问题,统一转成UTF-8。

选择加载器的原则只有一个:想清楚"检索时希望命中什么"。如果希望用户问"报销流程"时能召回那段流程描述,就别让页面标题和页脚噪音占掉宝贵的chunk额度。

2.2 切分器:chunk size与overlap的两难

切分是我认为整条链路里"下限感"最强的环节。LangChain默认推荐的RecursiveCharacterTextSplitter是目前最稳妥的选择,它按一组分隔符优先顺序切分:先按段落级分隔符,再按句子级,最后按字符级。好处是不会生硬地把一个句子切断,还允许用户自己设定分隔符优先级。

关键参数是chunk_size和chunk_overlap。前者决定每段的字符数,后者是在相邻段之间保留的一小段重复内容。为什么要重复?因为如果某个知识点恰好落在两段中间,没有overlap的话,这个知识点会被切成两半,各自都不完整,检索时两头都命中不全。

我做过一个实测,某企业内部培训手册里有一个操作步骤:"先打开设置页,然后点击数据同步,最后等待日志出现SYNC_OK。"在这个句子中间切开的场景下,无论怎么调Embedding模型,回答都很难还原完整的操作顺序。加上overlap之后,这个句子的关键信息会同时出现在相邻两段里,命中概率大幅提升。

切分长度没有银弹。如果你做的是开放域问答,512到768个字符往往是起步参考值;如果是面向窄领域的精确问答,比如合同条款、技术FAQ,建议用256字符以下的小块,召回更精准。但小块也会带来另一个问题:召回数量需要增加,否则上下文碎片化严重。

2.3 向量化:Embedding模型选型的现实考量

Embedding模型负责把文本变成向量,它的质量直接决定"相似"到底像不像。LangChain里最常用的是OpenAI的text-embedding-3-small,维度小、效果好、成本低。但如果你处理的是中文场景或专业领域,我建议多对比bge-m3这类开源模型,尤其是当你有一定GPU资源的时候。

我的选型经验可以总结成三点:

  1. 先用通用Embedding模型跑通流程,不要一开始追求领域模型。
  2. 准备一批应该在语义上相近的测试问题,比如"怎么重置密码"和"密码忘记如何找回",看它们在向量空间里的距离是否低于阈值。
  3. 关注维度与实际存储成本。比如128维和1024维在千万级数据量下的内存差距不是一星半点。

顺便提醒一句,向量化的对象不只是文档切片,用户的查询语句也要用同一个模型转向量。所以之后如果更换Embedding模型,所有入库的向量都得重新生成,否则新旧向量根本不在同一个空间里,检索效果会直接崩掉。

2.4 向量库与检索器:召回质量决定回答下限

向量数据库我先后用过Chroma、FAISS、Milvus。简单说,做原型验证用Chroma最舒服,起步快,无需额外服务,所有数据存在本地;到了中等规模以上、需要并发查询时,再迁移到Milvus这类专业向量库里。

但无论用哪个库,真正决定效果的其实是检索策略。默认采用相似度检索的top_k,比如返回最相近的4段内容。实际用下来,我觉得有两个参数值得折腾:

  • k值:k太小,可能漏掉正确答案;k太大,噪音上下文太多,大模型容易被干扰。我第一次做客服知识库时k设为2,回答经常残缺;调到5之后明显改善,但超过8之后,回答开始出现"不相关内容导致的幻觉"。
  • score_threshold:很多向量库支持设定相似度阈值,低于阈值的chunk直接丢弃。这看起来能过滤噪音,但风险也很高——不同Embedding模型的相似度分布差异很大,它不像是一个稳定可跨场景复用的数字。

我的习惯是:先调k,再用一批代表性测试问题人工观察召回的"命中率",最后才考虑加阈值或rerank。

2.5 生成环节:把检索结果变成可回答的上下文

检索完成之后,LangChain的RetrievalQA或StuffDocumentsChain负责把文档片段变成上下文,再交给大模型生成答案。这里有个细节很容易被忽略:给模型提示词里的文档片段,建议按相关度排序,并带上元信息,比如文档来源和页码。

为什么?因为大模型对上下文的注意力并不是均匀分配的。排序靠前的内容更容易被参考,把最相关的答案片段放在前面,能有效降低"模型自己编"的概率。我在实践里会在每个片段前加上"[来源] 产品手册V3 / 第12页"这样的标记,让模型在回答时天然倾向引用这些来源;即使回答不到位,用户也能顺着来源去核实。

生成温度也要注意。知识库问答是典型的事实性任务,温度尽量设到0到0.3之间,不要开高,否则模型会在事实基础上自由发挥。

3. 一套可复现的RAG代码实践(附参数说明)

前面讲了一堆原理,接下来我们把链条串起来。我用一套简单但完整的Python代码,展示如何从零搭建RAG,并说明每一步背后的参数选择。以下代码基于LangChain 0.1.x版本,其他版本部分API有差异,需要以官方文档为准。

3.1 环境准备与版本选择

首先安装依赖。这里不推荐无脑装最新版,因为LangChain的API变更很快,社区里很多教程跑不通,多半是版本问题。

pip install langchain langchain-openai langchain-community chromadb bge-m3 pip install pypdf docx2txt tiktoken

我当前使用的版本组合可以作为参考:

  • langchain: 0.1.16
  • langchain-openai: 0.1.9
  • langchain-community: 0.2.4
  • chromadb: 0.5.5

如果你要处理扫描版PDF,还需要额外装OCR组件。我项目里用的是paddleocr,它对中文扫描件的识别效果比常规OCR库好不少。

3.2 建立知识库的完整流程

这里我用某个模拟项目X的资料文件举例,它是一份包含产品介绍、操作说明和常见问题的PDF。完整入库代码如下:

from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载PDF loader = PyPDFLoader("项目X产品手册.pdf") documents = loader.load() print(f"加载到 {len(documents)} 页") # 2. 切分 text_splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=64, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], length_function=len, ) chunks = text_splitter.split_documents(documents) print(f"切分得到 {len(chunks)} 个片段") # 3. 向量化 embeddings = HuggingFaceBgeEmbeddings( model_name="BAAI/bge-m3", model_kwargs={"device": "cpu"}, encode_kwargs={"normalize_embeddings": True}, ) # 4. 存入库 vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", ) print("向量库构建完成")

这里有几个参数我特意解释一下。separators里的中文标点很重要,默认的RecursiveCharacterTextSplitter对中文支持不够友好,如果不在分隔符里加上句号、感叹号、问号,它会把一大段中文按字符硬切。normalize_embeddings=True会对向量做归一化,计算余弦相似度更快也更稳定。

3.3 QA链路与流式输出

库建好之后,就可以搭问答链路了。我这里结合检索器和LLM,写成一个完整的QA函数:

from langchain_openai import ChatOpenAI from langchain.chains import RetrievalQA from langchain.prompts import ChatPromptTemplate import os os.environ["OPENAI_API_KEY"] = "your-api-key" # 检索器 retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5}, ) # 提示词模板 prompt_template = """你是一个企业内部知识库助手,请根据以下资料回答问题。 资料: {context} 如果资料中没有相关内容,请直接说"根据现有资料无法回答",不要编造。 问题:{question} 答案:""" prompt = ChatPromptTemplate.from_template(prompt_template) # LLM llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, max_tokens=1024, ) # 组装QA链路 qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True, chain_type_kwargs={"prompt": prompt}, ) # 提问 result = qa_chain.invoke({"query": "如何开启日志审计功能?"}) print(result["result"]) print("来源片段:") for doc in result["source_documents"]: print(f" 来自 {doc.metadata.get('source', '未知')}") print(doc.page_content[:120])

chain_type我选的是stuff,它会将所有检索到的片段一次性塞进提示词。这么做逻辑最简单,适合片段数量少的场景。如果检索结果太多,后续可以换成map_reduce或refine等更复杂的链型,但它们在响应速度和实现复杂度上都有代价,早期追求"跑通"时不必急着上。

return_source_documents=True一定要开,因为它会把你召回的原始文本带回来,这是排查问题的重要抓手。没有它,你看到模型答错,根本不知道是检索漏了还是生成瞎编。

3.4 检索效果的量化观察

我建议在跑完整QA链路之前,先单独测试检索器这一步。因为生成结果不理想时,十有八九问题出在检索。单独观察检索结果,能快速区分问题来自哪一段链路。测试代码很简单:

test_query = "日志审计功能的开启步骤" docs = retriever.invoke(test_query) for i, doc in enumerate(docs): similarity = doc.metadata.get("score", "未知") print(f"Top {i+1}, 命中片段开头: {doc.page_content[:80]}") print(f" 得分或距离信息: {similarity}")

我习惯把测试问题提前写成清单,一类是"能直接从文档中找到答案的问题",另一类是"文档之外的问题"。第一类测试集用来观察召回率,如果明确存在的内容却召回不到,说明切分或Embedding有问题;第二类测试集用来观察系统的"拒答能力",如果模型在文档没有相关内容时还在硬编,可能要调整提示词或降低温度。

4. 实测中Top5的"答非所问"原因与排查链路

代码跑通只是第一步。实际做知识库项目时,我遇到的最多的疑问是:"为什么召回的片段看起来相关,但模型给出的答案明显不对?"这一节我记录五类高频问题的排查链路,每条都是我在项目里真实经历过的。

4.1 相似度分数高但语义不对齐

第一次碰到的情况是:检索出来的Top1片段相似度分数很高,内容却只是"关键词碰巧一样"。比如用户问"如何关闭广告推荐",Top1命中的却是"推荐算法如何增加广告投放"。这里面有Embedding模型的局限性:它抓的是整体语义,但对专有名词和状语细节的区分度不够。

排查链路是这样走的:

  1. 先打印所有召回片段,看Top N里是否至少有一片段确实与问题对齐。如果没有,是检索问题;如果有,是生成问题。
  2. 针对检索问题,检查Embedding模型规模。小模型对语义细节的捕捉较弱,可以在资源允许时换参数量更大的Embedding模型。
  3. 再检查切分粒度。如果chunk包含太多主题,向量往往被"平均化"了,导致与具体问题的匹配度被稀释。

一个有效的优化是:把chunk里的关键信息提取出来做成摘要,让Embedding针对摘要建索引,原始全文用于生成。这样检索命中"更准",生成引用"更全"。我后来在很多项目里都采用了这个"双文本"设计,效果提升很明显。

4.2 切分把一句话拦腰斩断

前面提到过overlap,这里我讲讲实际排查到的案例。某次知识库问答中,用户问"退款到账需要多久",系统回答的居然是"请联系商务经理确认",内容倒是不算错,但明显答非所问。我把召回片段打印出来一看,发现命中的片段恰好被切分成两半,一半停在"退款到账需要",另一半从"联系商务经理"开始,答案被拦腰斩断。

排查时我是这样验证的:手动搜索原始文档,确认完整句子是否存在。确认存在后,再查切分后的片段,发现chunk_overlap=0把句子切开了。解决办法是把chunk_overlap调大到64到128,同时增加中文标点分隔符,避免在句子中途断开。

这个坑的教训是:切分器不是凭感觉调的,最好拿你的知识库里十句"最容易被问到的内容"做回归测试。所有回答不理想的case,先查"这一段原文是不是被切碎了"。

4.3 多轮对话的query重写缺失

第一次做客服机器人时,我在多轮对话场景里遇到了一个很典型的问题。用户先问"怎么申请发票",系统回答了;接着用户追问"那需要多久",系统在向量库里检索"那需要多久",自然什么都检索不到。原因是,这个问题缺少了上文信息,本质上需要被重写成"申请发票需要多久"。

LangChain社区早期版本里没有为RetrievalQA自动做query重写的环节,需要自己处理。我的做法是用一个轻量LLM调用做对话历史压缩:

from langchain_core.messages import HumanMessage, SystemMessage history_text = "用户: 怎么申请发票\n助手: 在财务系统提交申请单即可。\n" rewrite_prompt = f"""根据对话历史,把用户最新问题改写成一个不依赖上下文的独立问句。 历史: {history_text} 最新问题:那需要多久? 改写结果只输出问句本身:""" messages = [ SystemMessage(content="你是一个查询改写助手。"), HumanMessage(content=rewrite_prompt), ] rewritten = llm.invoke(messages).content print(rewritten) # 输出:申请发票需要多久?

把改写后的query传给检索器,多轮问答的召回准确率提升非常明显。如果你不想额外调用一次LLM,也可以用langchain.chains.history_aware_retriever里的create_history_aware_retriever,它会自动处理这部分逻辑,但你需要审慎地告诉它如何引用历史消息。

4.4 排序只看向量相似度的局限

向量相似度反映的是语义上"接近",但它对关键实体的匹配能力不够敏感——比如型号"RX-204"和"RX-240"在向量空间里可能非常接近,看起来相似,实际上是完全不同的产品。结果就是:检索器把"RX-240的参数"召回给问"RX-204"的用户。

解决这类问题,我做的最有效的一件事是引入关键词过滤。在调用向量检索之前,先用规则或NER(命名实体识别)抽取出问题中的型号、编号,然后在metadata里做精确匹配过滤。比如:

def filter_by_keyword(retriever, query, keyword): docs = retriever.invoke(query) filtered = [d for d in docs if keyword.lower() in d.page_content.lower()] if not filtered: return docs # 关键词过滤为空时回退到纯向量结果 return filtered

实体型问题尤其适合"关键词过滤+向量检索"的组合方案。向量负责语义上的大致候选集,关键词负责精确筛选。这是低成本、高回报的优化手段。

4.5 幻觉并未被完全消除

不少人以为RAG能彻底消除幻觉,这其实是误解。RAG能显著降低幻觉,但无法根除。原因在于:即使检索到了正确内容,如果提示词里没有明确约束,大模型仍然会习惯性"顺着问题编下去"。

我在提示词里加了三条硬约束后,幻觉明显减少:

  1. 如果资料里没有相关内容,直接说"根据现有资料无法回答",不要编造。
  2. 回答时引用资料中的原句,不要自行扩展。
  3. 如果资料之间有冲突,明确说明存在不一致,并分别列出两方的观点。

温度调低到0也是关键。我在测试中发现,温度高于0.7的时候,即使提示词有明确约束,模型依然会冒出"资料里没有的细节"。

5. 检索增强的进阶策略:混合检索、rerank与路由

如果你已经把基础RAG跑通,并且发现"偶尔答得好、偶尔崩溃",那就要进入进阶阶段了。这节讲三个我实测有效的主流策略:混合检索、rerank排序、查询路由。

5.1 混合检索:向量召回与关键词召回互补

纯向量检索有一个软肋:对精确术语不敏感。比如用户问"API超时阈值怎么配",向量可能召回大量"API调用""超时重试"的泛泛内容,但配置值那一行反而没召回。反过来,纯关键词搜索又对同义词束手无策。两者结合就是混合检索。

LangChain里我用的最简单做法是向量检索和BM25检索并行,然后合并结果按自定义分数排序。BM25是传统的信息检索算法,对"词义匹配"不敏感但能精准匹配关键词,正好和向量互补。

from langchain.retrievers import BM25Retriever, EnsembleRetriever # 假设texts是所有切分片段 bm25_retriever = BM25Retriever.from_texts(texts, k=5) vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.3, 0.7], )

权重这里我一般把向量放重一点,语义匹配为主,关键词匹配为辅。具体比例要看知识库的内容形式:如果里面大量是代码、型号、变量名,关键词权重可以提高到0.4甚至0.5。

5.2 rerank模型:第二次排序的价值

混合检索召回的结果往往有10个以上,但能塞进提示词的只有四五个。这时候直接从低到高截断不理智,应该引入rerank模型做第二遍精排。rerank模型会把人机交互时query和document一起送入模型,输出一个相关度分数,比纯向量距离要准不少。

我用的是bge-reranker-base,可以直接在本地跑,效果对我来说够用:

from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain.retrievers import ContextualCompressionRetriever reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compressor = CrossEncoderReranker(model=reranker, top_n=4) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever, )

加了rerank之后,我在某电商客服模拟数据集上的Top1命中率,从61%提升到78%左右。代价是每轮查询多了几十到几百毫秒,但换来的是回答稳定性的明显提升,这比单纯调大k值划算得多。

5.3 查询路由:不同问题不同链路

不是所有问题都适合走同一条RAG链路。比如"报销流程是什么"适合走文档检索,"本季度销售额是多少"可能需要走数据库查询。LangChain的RouterRetriever或LLMRouterChain可以按类别路由到不同的处理方式上。

我的设计思路是:先让一个轻量分类器判断问题类型,然后分发到不同的retriever。简单实现可以这样:

def route_query(query): if any(kw in query for kw in ["销售额", "订单量", "统计"]): return "sql_retriever" elif any(kw in query for kw in ["手册", "流程", "规范"]): return "doc_retriever" return "general_llm"

这类路由规则写起来不难,难的是想清楚"哪些问题在知识库里有答案,哪些需要走数仓"。我的经验是:先观察线上真实问题分布,把高频且能以规则命中的问题先分流;数量少、频次低的问题,用不精准的通用链路兜住。

5.4 缓存与多路召回的组合拳

RAG链路里,Embedding调用和LLM生成都有成本和延迟。对于频繁被问到的高频问题,尤其是客服场景,建缓存能省下大量开支。做法很简单:把"问题+检索结果"或"问题+最终回答"存下来,命中时直接返回。

我建议至少做两级缓存:

  1. 回答级缓存:同一问题直接返回历史答案,延迟最低。
  2. 检索级缓存:同一query直接取历史召回片段,跳过向量检索,再走LLM生成。

第二级的价值在于,用户可能换了个问法,但召回片段相同,这时候缓存检索结果可以省下Embedding调用,同时保持答案的灵活性。这个方案在服务内部知识库时效果很明显,因为内部问题分布高度集中,头部几百个问题覆盖了80%以上的流量。

6. RAG上线前的评估意识:用指标代替感觉

很多团队跑通RAG之后,试了二三十个问题觉得"看起来还行",就准备上线。但"看起来还行"这个标准非常危险,因为RAG的表现不仅在问题之间波动很大,在同义词改写之下也会剧烈变化。我建议上线前至少做一轮系统化评估。

6.1 召回质量评估

召回质量不靠人工看几屏,而是靠"标准答案集"。我实践中的做法是:准备50到100个问题,每个问题标注"文档里对应的标准片段ID"。然后看系统召回Top5里是否包含标准片段,计算召回率(Recall@5)。

这里有一个建议:这类标注不用一次做太多。50个覆盖高频场景的问题已经能发现大部分系统性问题,后续再根据线上日志扩充。每轮调整完切分参数或Embedding模型之后,跑一遍同一套测试集,对比召回率和答案准确率,才能知道改动到底是变好还是变坏。

我见过不少团队调整参数全凭感觉,今天调大chunk,明天换模型,却说不清楚有效无效。没有量化评估,等于闭着眼睛开车。

6.2 生成质量评估

生成质量比较难量化,我用两个维度:忠实度(Faithfulness)和答案相关性(Answer Relevance)。忠实度看"答案里的内容是否能在提供的上下文里找到依据",相关性看"答案是否回答了用户的问题,没有跑题"。

如果不想上复杂的RAGAS评估框架,可以先做人工打分。我习惯用的评分标准是:

  • 3分:答案完全正确,依据充分,可以直接给用户。
  • 2分:答案基本正确,但缺少部分细节或语气可以再优化。
  • 1分:答案有部分信息错误或者答非所问。
  • 0分:完全错误或编造。

每次调整后,抽30条问题重新打分,只要平均分下降,就说明改动值得怀疑。我用这个方法挡下过好几次"看似优化实则退化"的改动。

6.3 一套轻量评估脚本思路

这里给出一段简化的自动评估思路,用LLM帮我们判断答案是否忠实于上下文,避免一次性人工看太多:

evaluate_prompt = """给定用户问题、检索到的上下文和模型回答,判断回答中的每句话是否都能从上下文中找到依据。 问题:{question} 上下文:{context} 回答:{answer} 如果回答中的所有关键信息都在上下文中有对应依据,输出"忠实";否则输出"不忠实",并给出原因。 """ def evaluate_faithfulness(question, context, answer): response = llm.invoke(evaluate_prompt.format( question=question, context=context, answer=answer )) return response.content

这本质上是"用LLM评估LLM",不完全可靠,但对快速迭代很有价值。至少它能抓出"回答虽然流畅但内容在上下文里根本不存在"的严重幻觉case,替你省掉大量人工筛查时间。

7. 我踩过的坑和目前常用的两组参数

最后分享一些具体的经验数字和参数组合,这是我在多个项目里反复调试后的"起手式",未必适合所有场景,但如果你想少走弯路,可以参考这个起点再微调。

首先说坑。最疼的一次是:某次我把知识库文档从PDF换成Word,骨架代码没变,结果所有问题都答得像"阅读理解缺失"——追问发现,Word里的表格被加载成了挤在一块的纯文本,切分后全是碎片。后来我给加载器加了专门的表格提取逻辑,先把表格转成Markdown再切分,效果立刻正常。

第二个坑来自metadata过滤。我一度把文档来源作为过滤条件,结果测试时发现有的片段metadata里source是绝对路径,有的是相对路径,过滤条件根本匹配不上。从那以后,我在入库前会统一校准metadata格式,用规则把路径和版本号清洗干净,再做过滤。

现在我用得最多的两组参数组合如下:

对照组(快速验证):

  • 切分:chunk_size=512,chunk_overlap=64
  • 检索:k=5,纯向量检索,无rerank
  • Embedding:bge-m3
  • 生成:温度0,RetrievalQA的stuff模式

优化组(知识库问答已上线):

  • 切分:chunk_size=256,chunk_overlap=32
  • 检索:混合检索(向量0.7 + BM25 0.3),召回12条
  • Rerank:bge-reranker-base,top_n=4
  • 查询改写:多轮对话场景开启query重写
  • 生成:温度0,提示词显式要求"无资料不答"
  • 评估:50条标准问题集 + 忠实度自动评估

这两组之间差别最大的是chunk size。为什么优化组反而用了更小的256?因为从客服问答的实际日志来看,问题大多指向具体的操作步骤或参数,小块命中精准,配合混合检索和rerank后,召回相同内容只需要更少的上下文,大模型的干扰也少。

RAG这条路没有一劳永逸的配置,核心是把可控的评估集建起来,让每次调整都有据可依。找到你场景里"问题分布最密的内容",用这一小部分内容把切分、检索、提示词打磨到稳定,再横向扩展到整个知识库。我自己的经验是,先花两天把评估集建好,之后的调参效率会翻倍;反过来,先急着调模型换参数,大概率会一直在原地打转。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询