← 返回博客
·AI技术

用 RAG 做了一个内部知识库,上线一周后被产品骂惨了

RAG 系统从 Demo 到上线的真实踩坑记录:语义漂移导致幻觉、4秒延迟的性能瓶颈、Embedding模型选型难题与混合检索方案。

#RAG#LLM#向量数据库#LangChain#生产实践

# 用 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+。每个查询涉及:

  • 文本切分 → embedding 计算
  • 2. 向量相似度搜索 → KNN

    3. 检索结果重排序

    4. LLM 生成回答

    我把查询量测了一下,平均每次 RAG 查询耗时 **4.2 秒**。其中:

  • Embedding: ~300ms
  • ChromaDB 搜索: ~50ms
  • 重排序 (Cross-Encoder): ~800ms
  • LLM 生成: ~3s
  • 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,中文效果好是好的,但有两个致命问题:

  • **句子太短的话效果很差。** 客服的问题很多只有 5-8 个字,比如 "怎么重置密码"。bge 在这种短 query 上的嵌入质量远低于长文档,导致检索的召回率只有 60% 左右。
  • 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 |

    改善主要来自于两件事:

  • 加了 Query 改写和混合检索,召回质量提升了
  • 2. 在前端加了一个 "这个回答准确吗?" 按钮,收集到的差评反馈自动进了重新索引队列

    但还有一个我至今没想通的问题:**为什么我们做的 RAG 系统永远缺不了人工审核?**

    可能的答案是:RAG 的本质不是替代人,而是给人一个更好的起点。客服不需要系统给她完美的答案,她需要的是一个能快速缩小搜索范围的助手,然后她自己判断哪些信息适用。我们一开始想做全自动,反而把系统做成了一个容易出错的玩具。


    *如果你也在做 RAG 项目,踩过哪些你以为是 bug 其实是 design problem 的情况?*