AI

LangChain 避坑手册:RAG 幻觉、检索失效、组件兼容,这些问题怎么解?

转载:小红书 AI产品赵哥

前言🔖


一路走来,我们聊了组件、聊了数据库、聊了架构优化。已经有产品同学搞出了 AI 应用产品的 MVP。

然后,你们就迎来了最 “酸爽” 的阶段:验收与排障。你发现你的 AI 经常一本正经地胡说八道(幻觉),明明文档里的内容它死活搜不到(检索失效),昨天还能跑的代码今天升级个库就报错(兼容性崩塌)。

很多产品经理在这个阶段会非常焦虑,觉得是模型不够聪明,或者技术选型错了。其实,大部分问题都是工程实现的细节没处理好。

今天这篇笔记,我把过去一年在几个项目中遇到的最高频的坑汇总出来,并给出基于 LangChain 的具体解法。这是一本给产品经理的《LangChain 避坑参考书》。

  

一、RAG 幻觉:如何让 AI 别那么放肆?🔖


现象描述:

  • 你上传了一份公司《员工手册》,里面规定 “病假只发 80% 工资”。
  • 用户问:“病假工资怎么算?”
  • AI 回答:“根据规定,病假期间全额发放工资,并有额外慰问金。”

这是最可怕的错误。AI 用极其自信的语气说了假话。在金融、医疗、法律领域,这种错误是致命的。

根因分析:

  1. 知识混淆:模型底座(比如 GPT-4)训练时看过太多 “良心企业” 的语料,它把通用知识和你的私有知识搞混了。
  2. 注意力涣散:检索回来的文档片段太长,包含无关信息,模型没有抓到重点。

LangChain 解法:引用源(Citations)与思维链验证

不要只给开发提需求说 “别让它乱说”,你要给出具体的工程指令:“必须实现引用来源功能”

🔹1. 强制要求 Citations(引用)

在 Prompt 中强制要求模型列出它引用的文档 ID 或页码。如果模型列不出来,就说明它在瞎编。

LangChain 提供了 create_citation_fuzzy_match_chain 等工具,但目前最好的方式是利用 Tool Calling(工具调用) 结构。

# 定义一个带有"引用字段"的结构
class AnswerWithCitation(BaseModel):
    answer: str = Field(description="针对用户问题的回答")
    citation: str = Field(description="回答依据的原文句子")
    source_id: int = Field(description="来源文档的ID")

# 强制模型输出这种结构
llm_with_tools = llm.with_structured_output(AnswerWithCitation)

# 效果:
# 如果模型想瞎编, 它会发现填不出 citation 字段,
# 这时通常会触发模型的自我修正, 或者直接返回"未找到相关信息"。

  

🔹2. IDK 策略 (I Don’t Know)

在 System Prompt 中,必须把 “不知道” 的优先级提至最高。

错误示范:“请尽可能回答用户问题。”

正确示范:“你是一个基于检索内容的回答助手。只允许使用下文中提供的【Context】来回答。如果【Context】中没有答案,必须直接回复‘文档中未提及’,严禁使用你自带的知识补充。”

具象化细节:

我们在做某法律助手时发现,仅仅写 “严禁编造” 是不够的。我们后来改成了:“如果你的回答在上下文中找不到原文作为证据,你的系统积分将被扣除。” 对,PUA 大模型有时候真的管用。

  

二、检索失效:明明有,为什么搜不到?🔖


现象描述:

  • 文档里明明写了 “错误代码 503 代表服务过载”。
  • 用户问:“报错 503 是什么意思?”
  • 检索系统返回了一堆 “502 错误”、“服务部署流程” 的文档,偏偏把关于 503 的那一段漏掉了。

根因分析:

这是 纯向量检索(Dense Retrieval)的硬伤。

向量检索是基于 “语义相似度” 的。在向量空间里,“503” 和 “502” 可能挨得很近(都是错误码),或者 “503” 这个数字被 Embedding 模型忽略了。

对于专有名词、精确数字、缩写词,向量检索的效果往往不如传统的关键词搜索。

LangChain 解法:混合检索(Hybrid Search)

这是目前生产环境的标配。不要只用向量搜索。

🔹1. EnsembleRetriever(集成检索器)

LangChain 提供了一个神器叫 EnsembleRetriever。它的作用是把 ** 关键词搜索(BM25)向量搜索(Vector Store)** 的结果加权合并。

  • BM25:就像以前的 Google,擅长精确匹配关键字(503、X-2000 型号)。
  • Vector:擅长理解语义(“服务器崩了”=“服务不可用”)。
from langchain.retrievers import BM25Retriever, EnsembleRetriever

# 1. 建立关键词检索器
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 5  # 取前5个

# 2. 建立向量检索器(假设 vector_store 已经建好)
vector_retriever = vector_store.as_retriever(search_kwargs={"k": 5})

# 3. 混合!
# weights=[0.5, 0.5] 表示两者权重相等
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

# 现在的 ensemble_retriever 既能搜到语义相关的,也能搜到精确匹配的

产品经理 Insight:

当测试反馈 “搜不到精准词” 时,不要让开发去换 Embedding 模型,那个成本太高。直接要求加上 BM25 混合检索,立竿见影。

  

🔹2. 上下文重排序(Re-ranking)

有时候不是没搜到,而是搜到了排在第 10 位。模型通常只看前 5 个文档。

这时候需要引入 Reranker(重排序模型)。它像一个严格的考官,把检索回来的 10 个粗糙结果,精细地打分重排。

在 LangChain 中,可以使用 ContextualCompressionRetriever 配合 CohereRerankBGE-Reranker。这能让检索准确率提升 20% 以上

  

三、Lost in the Middle:长上下文的陷阱🔖


现象描述:

  • 为了让 AI 掌握更多信息,你把检索到的 20 段文档(Total 10k Tokens)全塞给了 GPT-4。
  • 结果发现,AI 只记得第一段和最后一段的内容,中间文档里提到的关键信息被它无视了。

根因分析:

这是 LLM 的固有缺陷。研究表明,大模型对 Context 的首尾部分关注度最高,中间部分容易 “注意力坍塌”。

LangChain 解法:

LangChain 提供了一个专门的工具来解决这个问题:长上下文重排序(LongContextReorder)

它的逻辑很简单但很有效:把检索到的最相关的文档,物理移动到 Context 的头部和尾部,把不太相关的文档放在中间。

from langchain_community.document_transformers import LongContextReorder

# 假设 docs 是检索回来的 10 个文档,按相关性从高到低排序
reordering = LongContextReorder()

# 执行重排序
# 结果会变成: [第1相关, 第3相关, ... 第10相关 ..., 第4相关,
reordered_docs = reordering.transform_documents(docs)

# 这样,最重要的第1和第2个文档,分别占据了 Prompt 的开头和结尾

产品经理 Insight:

如果你的应用场景涉及长文档综述或者大量碎片信息整合,必须在这个环节做优化,否则 Token 费花了,效果却出不来。

  

四、组件兼容性:代码昨晚还能跑。。。我真是疯了我。。。🔖


现象描述:

  • 周五下班前 Demo 跑通了,周一早上演示时报错 ImportError 或者 Method deprecated
  • 开发一查,说是 LangChain 周末自动升级了个小版本,API 变了。

根因分析:

LangChain 是目前迭代速度最快的开源项目之一(有时一天发三个版本)。为了追求新功能,它经常做破坏性更新(Breaking Changes)。而且,LangChain 生态极其庞大,依赖包(OpenAI SDK、Pydantic、NumPy)之间的版本冲突如家常便饭

LangChain 解法:LCEL 与版本锁定

🔹1. 拥抱 LCEL (LangChain Expression Language)

在之前的笔记里我反复提到 LCEL。它不仅仅是语法糖,它是稳定性的保障。

LangChain 旧版的许多 “黑盒链”(如 RetrievalQAConversationalRetrievalChain)封装太重,一旦底层逻辑变动,上层就挂。

而 LCEL 使用的是原子操作(prompt | model | parser),这些基础原语非常稳定,极少变动。

建议:新项目全部使用 LCEL 写法,尽量少用 langchain.chains 下那些封装好的类。

  

🔹2. 锁定 requirements.txt

这对产品经理来说是管理要求。不要让开发写 langchain 这种不带版本号的依赖。

必须锁死主版本号和次版本号:

langchain==1.1.0
langchain-community==0.4.1
langchain-openai==1.1.0

  

🔹3. 使用 LangSmith 进行回归测试

LangChain 官方的 LangSmith 平台不仅能看日志,还能做测试。

你可以录制一组 50 个 “黄金问答对”。每次升级库版本之前,先跑一遍这 50 个 Case。如果准确率从 95% 掉到了 80%,坚决不升级。

  

五、结构化输出的不稳定🔖


现象描述:

  • 你要求 AI 返回 JSON 格式:{ 'name': 'Old K', 'age': 30 }
  • AI 有时候心情好会返回 JSON。
  • 有时候会返回:“好的,这是你要的 JSON:json { …} ”。
  • 有时候会返回:{ 'name': 'Old K', 'age': 30, }(注意那个多余的逗号)。

这会导致你的后端 JSON 解析器报错,整个服务崩溃。

根因分析:

大模型本质是生成文本的,它不懂 JSON 语法。早期的做法是用正则表达式去修补,这非常脆弱。

LangChain 解法:Pydantic Output Parser & Tool Binding

现在解决这个问题的标准答案只有一个字:Pydantic。

LangChain 深度集成了 Pydantic 库。你只需要定义好 Python 的类,LangChain 会自动生成 Prompt 指令,并在模型输出错误时自动重试。

更高级的做法是使用模型原生的 Tool Calling(在 LangChain 中通过 .with_structured_output() 实现)。

代码块

# 再次强调,这是目前最稳的写法
class UserInfo(BaseModel):
    name: str
    age: int

# 大部分现代模型(GPT-3.5+,Claude 3)都支持这种原生模式
# 模型不会输出废话,直接输出符合 Schema 的对象
chain = prompt | llm.with_structured_output(UserInfo)

产品经理 Insight:

如果你的产品经常因为 “格式解析错误” 而报错,去检查代码。如果开发还在用 RegexParser 或者在 Prompt 里写 “请不要输出 Markdown 符号”,请让他们立刻重构,改用 with_structured_output

  

六、来,总结一下吧🔖


这一篇笔记里,我们不再谈论那些令人兴奋的新概念,而是面对冷冰冰的现实问题。

  • RAG 幻觉 →→ 用引用(Citations)和工具绑定来约束。
  • 检索失效 →→ 用 ** 混合检索(Ensemble)重排序(Rerank)** 来补救。
  • 长文遗忘 →→ 用 ** 重排序(Reorder)** 把关键信息顶到两头。
  • 代码崩坏 →→ 拥抱 LCEL锁死版本

其实,AI 应用开发的门槛不仅在于写出第一行代码,更在于如何处理这 1% 的 Edge Case(边缘情况)。

当你能熟练地指挥团队使用这些手段去排坑时,你手里的产品才能真正从 Demo 走向 Production。