RAG 知识库优化:别让 AI 一本正经地胡说八道
Posted on 五 08 5月 2026 in Journal
| Abstract | RAG 知识库优化:别让 AI 一本正经地胡说八道 |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Status | v1.0 |
| Updated | 2026-05-10 |
| License | CC-BY-NC-ND 4.0 |
引言:RAG 最怕一本正经地错
你有没有遇到过这样的场景:
花了两周搭了一套 RAG 系统,接上公司知识库,兴冲冲演示给老板看。老板随口问了一句:"我们 Q1 的营收是多少?" 系统很快回答了一个数字,语气坚定,格式漂亮。问题是,数字是错的。
这不是段子,这是无数 RAG 项目的真实写照。
RAG (Retrieval-Augmented Generation,检索增强生成) 的原理不复杂:先从知识库里找材料,再让 LLM 基于材料回答。听起来很像开卷考试。
可是开卷考试也会翻错页、抄错段,甚至没看书就开始发挥。RAG 也一样——分块不当、检索不准、上下文塞太多、Prompt 没约束、引用没溯源、上线后不评估,每一项都能把"知识库助手"变成"知识库造谣机"。
我把 RAG 优化拆成四件事:
数据准备 → 检索优化 → 生成约束 → 评估监控
这四件事做扎实,RAG 才有资格谈"可用"。否则 Demo 再漂亮,也只是一个会说漂亮话的概率玩具。
一、RAG 架构回顾
先把基本流程摆出来。流程不复杂,复杂的是每一步都可能埋坑。
@startuml
top to bottom direction
skinparam defaultTextAlignment center
skinparam shadowing false
skinparam rectangle {
RoundCorner 15
}
rectangle "用户提问" as User
rectangle "Query 理解\n& 改写" as QueryRewrite
rectangle "检索/召回\n(Retrieval)" as Retrieval
rectangle "重排序\n(Reranking)" as Reranking
rectangle "上下文组装\n& Prompt" as PromptAssembly
rectangle "LLM 生成\n(Generation)" as Generation
rectangle "后处理 &\n引用溯源" as PostProcess
User --> QueryRewrite
QueryRewrite --> Retrieval
Retrieval --> Reranking
Reranking --> PromptAssembly
PromptAssembly --> Generation
Generation --> PostProcess
@enduml
每个环节都有优化空间,也都有翻车机会。RAG 的麻烦就在这里:它不是一个单点模型问题,而是一条链路问题。链路上任何一环松了,最后都会体现在答案质量上。
二、数据准备:垃圾进,垃圾出
先把材料收拾干净
1. 分块策略是基石
分块是 RAG 里最容易被低估、却最影响体验的环节。很多系统不是模型不行,是把知识切碎的时候就已经切坏了。
固定长度分块省事,但它不关心句子、段落、标题和上下文。就像切菜只看尺子不看菜,最后切出来能不能下锅,全凭运气。
更靠谱的做法是按语义边界切:
# 固定长度分块——省事但粗暴
chunks = [text[i:i+512] for i in range(0, len(text), 512)]
# 按语义边界分块——多花几行代码,少踩很多坑
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n## ", "\n### ", "\n\n", "\n", "。", ".", " "],
length_function=len
)
chunks = splitter.split_text(text)
先按这几条原则做:
| 原则 | 说明 |
|---|---|
| 语义完整性 | 一个 chunk 应该包含一个完整的语义单元 |
| 适当重叠 | 10-20% 的重叠率,避免上下文断裂 |
| 保留元数据 | 每个 chunk 附带来源文档、章节、页码 |
| 大小适中 | 通常 200-800 tokens,太小缺上下文,太大噪声多 |
2. 文档预处理别偷懒
PDF 解析、网页抓取、Office 文档导入,看起来都是"把文档变成文本",实际效果差很多。页眉页脚、水印、目录、乱码、断行、表格错位,这些都会进入检索链路。
脏数据进了向量库,不会因为套了一层 AI 就自动变干净。
def preprocess_document(doc):
doc = remove_headers_footers(doc)
tables = extract_tables(doc)
images = extract_and_describe_images(doc)
metadata = extract_metadata(doc)
return doc, tables, images, metadata
3. 不要只建一个大索引
不要把所有内容一股脑塞进一个向量索引。文档、章节、句子、表格、图片,信息形态不同,检索方式也该不同。
一种常见做法是父子文档索引 (Parent-Child):
文档层 (Document) → 摘要索引 (用于粗召回)
│
├── 段落层 (Section) → 主索引 (用于精确检索)
│ │
│ └── 句子层 (Sentence) → 细粒度索引 (用于精确匹配)
│
└── 表格/图片 → 结构化索引 (单独处理)
容易踩的坑
坑 1:不洗数据,直接灌
PDF 原样解析,页眉页脚、目录、版权声明全进了向量库。检索时这些噪声频繁被召回,严重拉低质量。典型情况是:用户问一个具体流程,top-5 里却混进两条页眉里的 "Confidential - Do Not Distribute"。
清洗数据可以按四步走。不要一开始就上"全家桶",先把最脏、最重复、最影响检索的东西干掉:
| 步骤 | 具体怎么做 | 常用工具 |
|---|---|---|
| 去模板噪声 | 统计每页重复出现的文本,删除页眉页脚、水印、版权声明、导航菜单、重复目录;网页内容先做正文抽取,别把侧边栏和广告一起塞进去 | PyMuPDF / pdfplumber、trafilatura、readability-lxml、BeautifulSoup |
| 修版式问题 | 合并异常断行,修复乱码和全半角混用,去掉多余空格,恢复项目符号;PDF 里跨页断开的句子要重新拼起来 | ftfy、正则、unstructured、Docling、MarkItDown |
| 保留结构信息 | 把标题层级、表格、代码块、图片说明、来源页码保存到 metadata;表格不要粗暴拍平成一坨文本,最好转成 Markdown 表格或结构化 JSON | pandas、camelot / tabula、markdownify |
| 抽样验收 | 随机抽几十个 chunk,看语义是否完整、来源是否清楚、噪声是否重复;再用几条典型问题做 smoke test,看召回结果是不是"看起来就靠谱" | 自己写脚本、pytest、简单的检索评测集 |
坑 2:分块大小一刀切
所有文档统一用 512 tokens 分块。FAQ 类文档需要小块 (100-200),技术手册需要大块 (500-800),一刀切顾此失彼。
分块策略也可以做成一张配置表,不要把所有文档都塞进同一个 splitter:
| 文档类型 | 具体怎么做 | 常用工具 |
|---|---|---|
| FAQ / 问答 | 一问一答尽量保持在同一个 chunk,chunk 可以小一点,通常 100-200 tokens 就够;不要把多个无关问题硬拼在一起 | RecursiveCharacterTextSplitter、自定义 Q/A parser |
| 技术手册 / 设计文档 | 按标题层级、段落和代码块切,保留 10-20% overlap;代码块不要从中间切断 | MarkdownHeaderTextSplitter、RecursiveCharacterTextSplitter |
| 长 PDF / 规章制度 | 先按章节切,再在章节内按段落切;每个 chunk 带上章节名、页码、版本号 | LangChain splitters、LlamaIndex node parser |
| 表格 / 配置项 | 不要按 token 硬切,优先按行、按字段或按业务实体切;必要时转成结构化 JSON 单独入库 | pandas、camelot / tabula、自定义 parser |
坑 3:忽略文档之间的关系
公司制度文档 A 引用了文档 B 的条款,但分块后这种引用关系丢失了。用户问到相关问题,系统只能给出片面回答。
文档关系要在入库时就显式保存,否则检索阶段很难凭空猜出来:
| 关系类型 | 具体怎么做 | 常用工具 |
|---|---|---|
| 引用关系 | 解析"见第 X 条"、"参考文档 B"、URL 链接、附件名,把被引用文档 ID 写进 metadata | 正则、BeautifulSoup、自定义 link extractor |
| 层级关系 | 保存 document -> section -> chunk 的父子结构;召回子 chunk 后,可以把父章节一起带出来补上下文 |
LlamaIndex Parent-Child retriever、LangChain Parent Document Retriever |
| 版本关系 | 保存文档版本、发布日期、失效日期;检索时优先召回最新版,避免旧政策压过新政策 | metadata filter、向量库过滤条件 |
| 主题关系 | 给文档打业务标签,比如产品线、部门、系统、模块;用户问题先缩小范围,再做向量检索 | embedding 聚类、人工标签、spaCy / KeyBERT |
三、检索优化:找到对的内容
先别急着让模型回答
1. 混合检索
很多 RAG 项目一开始只上向量检索,觉得语义相似就够了。实际并不够。
向量检索擅长找"意思接近"的内容,BM25 这类关键词检索擅长打中精确词。有人问 "ISO 27001",有人问 "报销制度 v2.1",这种问题如果只靠语义相似,很容易把路走偏。
所以更稳妥的方式是两条路一起走:
from typing import List
def hybrid_search(query: str, top_k: int = 10) -> List[dict]:
vector_results = vector_store.similarity_search(query, k=top_k * 2)
bm25_results = bm25_index.search(query, k=top_k * 2)
# Reciprocal Rank Fusion
fused = reciprocal_rank_fusion(
[vector_results, bm25_results],
weights=[0.6, 0.4]
)
return fused[:top_k]
def reciprocal_rank_fusion(result_lists, weights=None, k=60):
scores = {}
if weights is None:
weights = [1.0] * len(result_lists)
for results, weight in zip(result_lists, weights):
for rank, doc in enumerate(results):
doc_id = doc["id"]
if doc_id not in scores:
scores[doc_id] = 0
scores[doc_id] += weight * (1.0 / (k + rank + 1))
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
2. Query 改写
用户的问题是面向人的,不一定适合检索系统。人会省略上下文,会用简称,会问得很口语。检索系统可没那么善解人意。
可以把一个原始问题改写成几个检索友好的 query:
def query_rewrite(original_query: str, llm) -> List[str]:
prompt = f"""请将以下用户问题改写为3个不同角度的检索查询:
原始问题:{original_query}
要求:
1. 一个保持原意但更精确的版本
2. 一个使用同义词/近义词的版本
3. 一个更宽泛的版本
"""
queries = llm.generate(prompt)
return [original_query] + queries
3. Reranking 往往最划算
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
def rerank(query: str, candidates: List[str], top_k: int = 5):
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs)
ranked = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)
return ranked[:top_k]
经验法则:先粗召回 20-50 条,再用 Reranker 精排到 3-5 条。很多时候这一步比盲目换大模型更划算。
如果前面用了 Hybrid Retrieval,通常会先分别跑两路召回:
BM25 / 关键词检索 → 擅长命中专有名词、编号、错误码
向量检索 / Dense → 擅长命中语义相近的表达
两路结果合并时,一个常用办法是 RRF(Reciprocal Rank Fusion)。它不直接比较 BM25 分数和向量相似度,因为这两个分数不是一个量纲;它只看排名:
RRF_score(d) = Σ 1 / (k + rank_i(d))
其中 d 是某个文档或 chunk,rank_i(d) 是它在第 i 路检索结果里的排名,k 是平滑参数,常见取值是 60。一个 chunk 如果在 BM25 和向量检索里都排得靠前,RRF 分数就会更高;如果只在一路里偶然靠前,分数就不会太夸张。
这招朴素,但很实用。它像开会时听两个人投票:关键词检索说"这个很像",向量检索也说"这个也像",那就优先拿出来给 reranker 精排。
4. 元数据过滤
不要什么问题都去全库里搜。用户问财务制度,就先限定部门;问最新政策,就过滤更新时间;问某个产品线,就缩到对应文档集合。
不花哨,但很管用。
results = vector_store.similarity_search(
query,
k=10,
filter={
"department": "finance",
"doc_type": "policy",
"updated_after": "2025-01-01"
}
)
容易踩的坑
坑 4:只用向量检索
用户问 "ISO 27001 认证流程",向量检索返回一堆关于"认证"和"流程"的无关内容——语义相似但主题不同。加上 BM25 关键词匹配后,"ISO 27001" 这种关键字才能被精确命中。
混合检索不要只喊口号,可以按问题类型拆:
| 场景 | 具体怎么做 | 常用工具 |
|---|---|---|
| 有明确关键词 | 对产品名、标准号、错误码、人名、工单号走 BM25 或精确匹配,先保证关键字不丢 | Elasticsearch / OpenSearch、PostgreSQL full-text search |
| 语义描述类问题 | 用户说的是"怎么申请权限"、"系统为什么变慢"这类自然语言问题,用向量检索找相近语义 | FAISS、Milvus、Qdrant、pgvector |
| 两者都有 | BM25 和向量检索各召回一批,再用 RRF 或加权分数合并,避免一边独大 | Reciprocal Rank Fusion、Elasticsearch hybrid search、LlamaIndex retrievers |
| 带结构条件 | 先用 metadata 过滤部门、产品线、版本、时间,再做混合检索,别在全库里大海捞针 | 向量库 metadata filter、where 条件、业务标签 |
坑 5:不做 Reranking
向量检索的 top-10 结果中,真正相关的可能排在第 5-8 位。不做重排序直接取 top-3,大概率丢失关键信息。
Reranking 的核心是:召回阶段宁可多捞一点,排序阶段再精挑细选。
| 场景 | 具体怎么做 | 常用工具 |
|---|---|---|
| 通用文本重排 | 先召回 20-50 条,再用 reranker 精排到 3-5 条;这一步通常比盲目增大 top_k 更有效 | BAAI/bge-reranker-large、Cohere Rerank、Jina Reranker |
| 中文知识库 | 选中文或多语言 reranker,不要默认拿英文 cross-encoder 硬套 | bge-reranker-v2-m3、bge-reranker-large |
| 专业领域文档 | 准备一小批真实问答对,用人工标注的相关性样本评估 reranker;效果不够再考虑微调 | sentence-transformers CrossEncoder、评测集、pytest |
| 防止结果太单一 | top-5 里不要全是同一章节的近似 chunk,可以加 MMR 或按文档去重 | MMR、按 doc_id 去重、自定义 post-rank 规则 |
坑 6:Embedding 模型选错
用英文 Embedding 模型处理中文知识库,或者用通用模型处理专业领域文档。要选与语言和领域匹配的模型,必要时做 fine-tuning。
选模型别只看榜单,先看你的语料和问题长什么样:
| 场景 | 推荐模型 | 注意点 |
|---|---|---|
| 中文通用 | BAAI/bge-large-zh-v1.5 |
适合中文知识库的基线模型,先用它跑一版评测再说 |
| 英文通用 | text-embedding-3-large |
效果好,但要考虑 API 成本、数据出境和隐私要求 |
| 多语言 | BAAI/bge-m3 |
中英混合、跨语言检索时更稳,适合国际化文档 |
| 代码 | voyage-code-3 |
代码检索不要只靠自然语言 embedding,最好保留函数名、类名、文件路径等 metadata |
| 垂直领域 | 通用模型 + 小评测集,必要时再 fine-tuning | 先做 50-100 条真实查询评测,别上来就训练模型 |
四、生成优化:让 LLM 少发挥
规则要写清楚
1. Prompt 不是装饰,是边界
RAG 里的 Prompt 不是为了让回答更"优雅",是给模型划边界:哪些能答,哪些不能答,引用怎么给,资料不够时怎么说。
尤其是企业知识库,宁可回答"根据现有资料无法回答",也不要编一个听起来像真的答案。编出来的答案如果被人当真,后果比不回答严重得多。
RAG_SYSTEM_PROMPT = """你是一个专业的知识库问答助手。请严格基于以下检索到的参考资料回答用户问题。
## 规则
1. **只基于参考资料回答**,不要使用你的训练知识
2. 如果参考资料不足以回答问题,明确说"根据现有资料无法回答"
3. 回答中引用来源,格式为 [来源: 文档名称]
4. 如果多个来源有矛盾,指出差异并说明各自来源
5. 保持回答简洁、结构化
## 参考资料
{context}
## 用户问题
{question}
"""
2. 上下文不是越多越好
调 RAG 时有个常见冲动:怕漏信息,那就多塞点上下文。听起来合理,实际很危险。上下文越多,噪声也越多,模型越容易抓不住重点。
def build_context(chunks: List[dict], max_tokens: int = 4000) -> str:
context_parts = []
current_tokens = 0
for i, chunk in enumerate(chunks):
chunk_tokens = count_tokens(chunk["text"])
if current_tokens + chunk_tokens > max_tokens:
break
context_parts.append(
f"[参考{i+1}] (来源: {chunk['source']}, "
f"更新: {chunk['date']})\n{chunk['text']}"
)
current_tokens += chunk_tokens
return "\n\n---\n\n".join(context_parts)
3. 引用溯源不是锦上添花
RAG 系统和普通聊天机器人最大的区别,是它该让用户追到来源。答案后面没有引用,用户就只能选择信或不信——这不是知识库系统该有的样子。
CITATION_PROMPT = """回答问题时,请在每个关键信息后标注来源编号。
格式示例:
公司的年假政策规定,入职满1年的员工享有5天年假[1],
满5年的员工享有10天年假[2]。
最后列出参考来源:
[1] 《员工手册v3.2》第四章第二节
[2] 《2025年度假期政策更新》
"""
容易踩的坑
坑 7:不限制 LLM 的"创造力"
Prompt 里没要求"只基于检索内容回答",LLM 就开始脑补,把训练数据里的过时信息混入答案。这就是所谓的幻觉——它不是故意骗你,它是真觉得自己说得对。
生成阶段要把边界写死,尤其是企业知识库,别让模型自由发挥:
| 风险场景 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| 资料不足 | 明确要求"资料不足就说无法回答",不要让模型凭常识补全 | System Prompt、拒答模板 |
| 来源混乱 | 要求每个关键结论都带引用编号,没有引用的句子不输出或标记为不确定 | Citation Prompt、后处理校验 |
| 多来源冲突 | 如果多个来源说法不一致,要求模型列出差异,而不是自行裁判 | Prompt 规则、conflict detection |
| 输出跑偏 | 限定回答格式,比如结论、依据、注意事项、来源;复杂场景用 JSON Schema 约束 | structured output、Pydantic、Guardrails |
坑 8:上下文塞太多
把检索到的 20 个 chunk 全塞进 Prompt,LLM 反而被噪声干扰,抓不住重点。实践中,3-5 个高质量 chunk 往往优于 10+ 个中等质量 chunk。
上下文组装要像打包行李:该带的带上,"也许有用"的先放下:
| 场景 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| chunk 太多 | 先 rerank,再只取 top 3-5 个高质量 chunk;不要把 top 20 原样塞进 Prompt | reranker、top_k 控制 |
| chunk 太长 | 对长 chunk 做摘要或二次切分,只保留与问题相关的段落 | map-reduce summarize、自定义 trimmer |
| 信息重复 | 同一文档连续命中的多个相似 chunk,按 doc_id 和相似度去重 |
MMR、dedup by metadata |
| token 超限 | 给上下文设置 token budget,超过预算就按相关性和新鲜度裁剪 | tiktoken、token counter、context budget |
坑 9:忽略 "Lost in the Middle"
LLM 对上下文中间部分的注意力较弱。最匹配的内容应该放在上下文的开头和结尾,不是中间。
上下文排序不是排队买奶茶,最重要的内容不要站在中间被淹没:
| 场景 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| 最相关 chunk 很少 | 把最高分 chunk 放在上下文开头,必要时在结尾再放一次简短摘要 | attention-aware reorder |
| 多个来源都重要 | 开头放主证据,结尾放补充证据,中间放背景材料 | 自定义 context assembler |
| 长上下文模型 | 即使用长上下文,也按相关性排序,不要把原文顺序当成唯一顺序 | rerank score、position strategy |
| 需要引用溯源 | 保留 chunk 编号和来源编号,重排后不要丢失引用关系 | source map、citation metadata |
def reorder_for_attention(chunks: List[dict]) -> List[dict]:
"""最匹配的放开头和结尾,次匹配的放中间"""
if len(chunks) <= 2:
return chunks
sorted_chunks = sorted(chunks, key=lambda x: x["score"], reverse=True)
result = []
left, right = [], []
for i, chunk in enumerate(sorted_chunks):
if i % 2 == 0:
left.append(chunk)
else:
right.append(chunk)
return left + list(reversed(right))
五、评估与监控:别靠感觉上线
Demo 好看不等于系统可用
1. 先有评估数据集
RAG 项目最容易犯的错,是 Demo 能跑就上线。Demo 里问的十个问题,往往都是开发者自己挑的——怎么挑怎么准。真用户的问题一来,表达方式、背景信息、边界条件全变了。
所以要先有一套评估数据集。不需要一开始很完美,但至少要覆盖常见场景、关键业务和容易出错的问题。
eval_dataset = [
{
"question": "公司的报销流程是什么?",
"expected_answer": "提交申请→主管审批→财务审核→打款",
"expected_sources": ["报销制度v2.1"],
"category": "policy"
},
# ... 至少 50-100 条覆盖不同场景
]
2. 检索和生成分开评估
RAG 答错了,不一定是模型生成错,也可能是检索没召回;检索召回了,也可能是重排序丢了;上下文都对,也可能是 Prompt 没约束住。
所以评估指标要拆开看:
class RAGEvaluator:
def evaluate(self, question, generated, expected, retrieved_docs):
return {
# 检索质量
"retrieval_precision": self.calc_precision(retrieved_docs, expected_sources),
"retrieval_recall": self.calc_recall(retrieved_docs, expected_sources),
# 生成质量
"answer_relevance": self.llm_judge_relevance(question, generated),
"faithfulness": self.llm_judge_faithfulness(generated, retrieved_docs),
"correctness": self.llm_judge_correctness(generated, expected),
# 实用指标
"has_citation": bool(re.search(r'\[.*?\]', generated)),
"response_length": len(generated),
"latency_ms": self.last_latency
}
3. 线上要盯这些数
| 指标 | 目标 | 告警阈值 |
|---|---|---|
| 检索召回率 | > 85% | < 70% |
| 答案准确率 | > 80% | < 65% |
| 幻觉率 | < 5% | > 15% |
| 用户满意度 (好评率) | > 75% | < 60% |
| P95 延迟 | < 5s | > 10s |
| "无法回答"率 | < 20% | > 40% |
容易踩的坑
坑 10:没评估就上线
"Demo 看着挺好的,上线吧!"——这是 RAG 项目挂掉的头号原因。没有系统评估,你不知道系统在哪些场景下会出错,上线就是盲人骑瞎马。
上线前至少做一轮小而硬的评估,不求完美,但要能暴露问题:
| 评估对象 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| 检索质量 | 准备 50-100 条真实问题,标注期望来源,计算 recall、precision、MRR | RAGAS、TruLens、自定义 pytest |
| 生成质量 | 检查答案是否基于来源、是否答到问题、是否有幻觉 | LLM-as-judge、人工抽检 |
| 引用质量 | 验证引用是否存在、是否支持对应结论,别只看有没有 [1] |
citation checker、自定义脚本 |
| 线上风险 | 用边界问题、过期政策、冲突文档做回归测试 | regression test set、CI job |
坑 11:只评估一次
知识库在更新,用户问法在变,模型在迭代。评估应该是持续的,不是一次性的。
RAG 的评估要接进日常流水线,不然一次评估只能证明"当时没坏":
| 变化来源 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| 文档更新 | 每次知识库重建索引后跑一遍核心评测集,观察召回率和答案准确率是否下降 | CI/CD、scheduled eval |
| 模型升级 | Embedding、reranker、LLM 版本变化时做 A/B 对比,不要凭感觉切换 | experiment tracking、A/B test |
| 用户问法变化 | 定期抽样线上问题,把高频问法加入评测集 | query log sampling |
| 指标漂移 | 监控幻觉率、无法回答率、差评率、P95 延迟,超过阈值就回滚或降级 | dashboard、alerting |
坑 12:不看用户反馈
用户点了"差评"但没人分析原因。每一个差评都在告诉你系统哪里不对——可能是检索不准,可能是分块不当,也可能是 Prompt 有漏洞。这些信息不用花钱买,但很多团队就是不看。
用户反馈不是客服噪声,是最便宜的线上评测数据:
| 反馈类型 | 具体怎么做 | 常用工具 / 机制 |
|---|---|---|
| 点赞 / 点踩 | 点踩必须记录问题、答案、召回 chunk、模型版本,方便复盘 | feedback log、trace ID |
| 用户改写问题 | 用户连续追问或换问法,说明第一次没答好;把这些 query 加进评测集 | conversation log、query mining |
| 人工纠错 | 允许用户标注"正确来源"或"正确答案",沉淀成训练和评估样本 | review queue、labeling workflow |
| 高频差评主题 | 按部门、产品线、文档类型聚合差评,定位到底是数据问题、检索问题还是生成问题 | analytics dashboard、issue tracker |
六、进阶技巧
1. Agentic RAG
基础 RAG 跑稳之后,可以考虑 Agentic RAG:让 LLM 先分析问题,再决定检索策略。适合复杂问题,但也会增加延迟、成本和不可控性。不要一上来就用它解决所有问题——先把基础链路做到 80 分再说。
def agentic_rag(question: str):
# Step 1: LLM 分析问题,决定检索策略
plan = llm.generate(f"""分析这个问题需要什么信息:
问题:{question}
输出:
- 需要检索的子问题列表
- 每个子问题的检索策略 (向量/关键词/结构化查询)
""")
# Step 2: 执行多轮检索
all_contexts = []
for sub_query in plan.sub_queries:
results = search(sub_query)
all_contexts.extend(results)
# Step 3: 判断信息是否充分
if llm.judge_sufficient(question, all_contexts):
return llm.generate_answer(question, all_contexts)
else:
additional = llm.generate_followup_queries(question, all_contexts)
# ... 继续检索
2. 知识图谱增强
对于实体关系很强的知识库,可以抽取实体和关系,构建知识图谱作为向量检索的补充。
组织架构、产品依赖、权限关系、合同条款——这些内容只靠向量相似度往往不够。
向量检索 → 找到相关段落
+
知识图谱 → 找到关联实体和关系
↓
更完整的上下文
3. 缓存
RAG 的成本不低,延迟也不低。高频问题、稳定知识库、固定答案,都可以缓存。但缓存一定要带失效策略,否则知识库更新了,系统还在回答旧答案。
import hashlib
class RAGCache:
def __init__(self, ttl=3600):
self.cache = {}
self.ttl = ttl
def get_or_compute(self, query, search_fn, generate_fn):
key = hashlib.md5(query.encode()).hexdigest()
if key in self.cache and not self.is_expired(key):
return self.cache[key]
results = search_fn(query)
answer = generate_fn(query, results)
self.cache[key] = {"answer": answer, "sources": results}
return self.cache[key]
七、检查清单
□ 数据准备
□ 文档清洗 (去噪声、格式统一)
□ 语义分块 (非固定长度)
□ 分块重叠 (10-20%)
□ 元数据保留 (来源、日期、分类)
□ 多层索引架构
□ 检索优化
□ 混合检索 (向量 + BM25)
□ Query 改写与扩展
□ Reranking 重排序
□ 元数据过滤
□ 匹配的 Embedding 模型
□ 生成优化
□ 严格的 Prompt 约束
□ 上下文数量控制 (3-5 个)
□ 注意力友好的排列顺序
□ 引用溯源
□ 兜底策略 (无法回答时的处理)
□ 评估监控
□ 评估数据集 (50-100 条+)
□ 多维度指标 (检索+生成+实用)
□ 持续评估 pipeline
□ 用户反馈收集与分析
□ 线上监控告警
最后说一句不中听但有用的话:RAG 不是一个项目,是一个产品。
项目可以交付,产品要持续运营。知识库会更新,用户问法会变,模型会升级,业务规则也在变。你不能指望一次上线,从此岁月静好。
如果只记住一件事:RAG 的质量不只取决于 LLM,而取决于整条链路。数据要干净,检索要准,生成要有边界,答案要能溯源,评估要持续跑。
无他,少一点玄学,多一点工程。
参考资料
- RAG Optimization Best Practices - LangChain Blog
- Twelve RAG Pain Points and Solutions
- Lost in the Middle: How Language Models Use Long Contexts
- BAAI BGE Embedding Models
- Reciprocal Rank Fusion
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。