用 RAG 做了一个内部知识库,上线一周后被产品骂惨了
RAG 系统从 Demo 到上线的真实踩坑记录:语义漂移导致幻觉、4秒延迟的性能瓶颈、Embedding模型选型难题与混合检索方案。
# 用 RAG 做了一个内部知识库,上线一周后被产品骂惨了
上个月我们决定给客服团队搭一个 RAG 系统。需求很简单:把过往的工单、FAQ、产品文档扔进去,让客服问问题时系统能给出参考答案。产品经理拍了胸口说 "这还不简单?Embedding + 向量数据库 + LLM,两天就能搞定"。
我信了他的邪。
第一周:Demo 跑起来之后所有人都很开心
用了 LangChain 的 RetrievalQA 链,ChromaDB 做向量存储,模型选的本地部署的 Qwen2.5-7B。数据源是 3000 多份 Markdown 格式的 FAQ 和 200 多 MB 的产品文档 PDF。
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_qwen import ChatQwen
embeddings = OllamaEmbeddings(model="bge-large-zh")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
)
docs = text_splitter.split_documents(raw_documents)
vectorstore = Chroma.from_documents(docs, embeddings, collection_name="kb")
qa_chain = RetrievalQA.from_chain_type(
llm=ChatQwen(),
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 5})
)
跑 Demo 的时候确实很惊艳。你问 "退款流程是什么?",它从文档里找出一段话,组织成清晰的回答。客服主管当场拍板:"就这个!周一上线。"
我没说话。因为我知道有些东西在这个 Demo 里不会出现。
第一天的线上事故:幻觉之王
上午十点,一个用户投诉说 "你们的产品 A 支持七天无理由退款"。我们的客服按照系统给的回答回复了客户,结果客户截图发到了社交媒体——我们的产品 A **根本没有** 七天无理由政策。
我去查了系统的检索日志。原因很典型:向量数据库里找不到精确匹配的条款(因为我们从来没写过 "七天无理由"),但模型在检索到的一些类似退换货政策文档中找到了模糊相关信息,然后自作主张把 "部分品类支持退货" 解读成了 "支持七天无理由"。
这就叫 **语义漂移**。嵌入模型把 "退货政策" 和 "退款政策" 的向量靠得很近,检索返回了不相关的文档,LLM 又擅长编故事。两个错误的叠加 = 一个看起来很可信的谎言。
**教训 #1:RAG 系统的召回质量取决于文档质量,而不是向量搜索的质量。** 如果你的原始文档本身就含糊不清,再牛的 Embedding 也救不回来。
第二周:性能瓶颈比预期严重十倍
我们的客服团队大概有 80 个人,峰值时段同时在线提问的可能有 20+。每个查询涉及:
2. 向量相似度搜索 → KNN
3. 检索结果重排序
4. LLM 生成回答
我把查询量测了一下,平均每次 RAG 查询耗时 **4.2 秒**。其中:
3 秒的 LLM 延迟是最痛的。客服每问一个问题,要等 4 秒才能看到答案。如果这个问题需要追问一次(比如 "那具体的操作流程呢?"),用户就要等 8 秒。
我尝试了几个优化方案:
方案 A:缓存热门问题
import hashlib
from functools import lru_cache
class CacheAwareRetriever:
def __init__(self, vectorstore, cache_ttl=3600):
self.vectorstore = vectorstore
self.cache = {}
self.cache_ttl = cache_ttl
def search(self, query: str) -> List[Document]:
q_hash = hashlib.md5(query.encode()).hexdigest()
if q_hash in self.cache:
entry = self.cache[q_hash]
if time.time() - entry["ts"] < self.cache_ttl:
return entry["docs"]
docs = self.vectorstore.similarity_search(query, k=5)
self.cache[q_hash] = {"docs": docs, "ts": time.time()}
return docs
效果不错——大约 35% 的查询命中缓存,响应时间降到 0.8 秒以内。但问题是,热门问题的命中率有限。客服问的问题五花八门,很少完全相同的 query。
方案 B:预计算高频问题的答案
我把前一周客服问过的问题做了词频分析,发现大约 20% 的问题集中在 50 个核心 topic 上。于是做了一个预计算缓存:
TOPIC_CENTERS = [
"退款流程", "发票开具", "版本升级", "API限流",
"权限申请", "数据导出", "账号注销", ...
]
async def precompute_answers():
for topic in TOPIC_CENTERS:
docs = await retriever.search(topic)
answer = await llm.ainvoke(f"根据以下文档回答问题 '{topic}':{format_docs(docs)}")
db.save("precomputed", topic, answer, docs) # 存答案和原始文档
这样高频问题可以直接返回预计算的结果,连 LLM 调用都省了。加上缓存后,峰值时段整体 P95 延迟压到了 1.2 秒。
踩坑最深的细节:Embedding 模型选型
我最初选的是 bge-large-zh,中文效果好是好的,但有两个致命问题:
2. **领域术语映射不到向量空间。** 我们的系统有一个自定义模块叫 "工单流转引擎",这在通用训练语料里没有对应的概念。向量化的时候它被映射到了一个完全不相关的方向。
换用 QAnything 的专用 embedding 模型后,召回率提到了 78%,但还是不理想。
最终解决方案是 **Query 重写 + 混合检索**:
from langchain_core.prompts import ChatPromptTemplate
query_rewriter = ChatPromptTemplate.from_template(
"你是一个知识库搜索助手。将用户的简短问题改写为更利于检索的形式。"
"不要改变原意,可以适当展开。只输出改写后的句子。\n\n"
"用户问题:{question}\n改写:"
)
rewriter_chain = query_rewriter | ChatQwen()
def enriched_search(query: str):
# Step 1: 改写 query
expanded_query = rewriter_chain.invoke({"question": query}).content
# Step 2: 向量检索(语义匹配)
semantic_results = vectorstore.similarity_search(expanded_query, k=5)
# Step 3: BM25 全文检索(关键词匹配)
keyword_results = bm25_store.search(expanded_query, k=5)
# Step 4: 去重合并,语义结果权重 0.7,BM25 结果权重 0.3
merged = deduplicate_and_merge(semantic_results, keyword_results,
semantic_weight=0.7, keyword_weight=0.3)
return merged[:5]
查询改写让短 query 的召回率从 60% 提到了 85%。BM25 弥补了向量检索在处理精确术语时的短板。两条腿走路比单靠 embedding 靠谱得多。
上线两周后的总结
我们做 RAG 的内部知识库上线了,但客服主管反馈 "回答质量不稳定"。我看了完整的数据:
| 指标 | 第 1 周 | 第 2 周 |
|------|---------|---------|
| 人工采纳率 | 45% | 67% |
| 幻觉率(人工标注) | 18% | 7% |
| 平均响应时间 | 4.2s | 1.1s |
| P95 响应时间 | 6.8s | 2.3s |
改善主要来自于两件事:
2. 在前端加了一个 "这个回答准确吗?" 按钮,收集到的差评反馈自动进了重新索引队列
但还有一个我至今没想通的问题:**为什么我们做的 RAG 系统永远缺不了人工审核?**
可能的答案是:RAG 的本质不是替代人,而是给人一个更好的起点。客服不需要系统给她完美的答案,她需要的是一个能快速缩小搜索范围的助手,然后她自己判断哪些信息适用。我们一开始想做全自动,反而把系统做成了一个容易出错的玩具。
*如果你也在做 RAG 项目,踩过哪些你以为是 bug 其实是 design problem 的情况?*
VkingAI