找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4274

积分

0

好友

592

主题
发表于 4 小时前 | 查看: 4| 回复: 0

学员成功获得大模型独角兽公司月之暗面offer的聊天记录

最近,我用 LangChain 搭建了一个面向“业务知识库”场景的 RAG 系统。这个系统并非泛泛的搜索引擎或演示版聊天机器人,而是更贴近企业内部的实际需求,比如客服 SOP、退款审批规则、OpenAPI 对接手册这类文档的精准问答。

最终,项目具备了以下核心能力:

  • 支持文档上传、按路径导入
  • 支持多轮对话和代词指代
  • 回答严格基于上下文,不知道就明确说不知道
  • 返回引用溯源,能看到答案来自哪个文档哪一段
  • 检索层支持 Dense Recall + BM25 + Cross-Encoder reranking
  • 用 LangChain InMemoryCache 做请求级缓存
  • 用 RAGAS 做回归评估

本文我将重点分享三方面的内容:这套 RAG 的架构设计思路、为何单一的“向量检索 + 大模型回答”模式远不足以应对实际业务,以及在实现过程中踩过的那些“坑”及其解决方案。

01 系统架构

先来看整体的架构设计图:

基于LangChain和FastAPI的业务知识库RAG系统架构图

这张架构图清晰地展示了从用户提问到获取答案的完整流程。其中最核心的设计理念是将复杂的问答过程进行分层解耦:

  • 将“检索前的问题改写”和“最终生成回答”拆分为两个独立阶段。
  • 将“粗粒度召回”和“精粒度排序”拆开。
  • 将“在线问答服务”和“离线效果评估”拆开。

这样做的好处是每一层都可以独立调优和替换,而不是把所有复杂性都推给大语言模型(LLM)去处理,使整个系统更可控、更易维护。

02 为什么要先改写问题再检索?

很多入门级的 RAG 教程都忽略了一个现实问题:在实际对话中,用户的第二句、第三句提问往往不是完整的独立问题。

例如:

Q1: 退款金额高于 200 元怎么办?
Q2: 那它需要谁二次确认?

第二句中的“它”对人类来说指代明确,但对于仅依赖当前句子进行向量相似度计算的检索器而言,却十分不友好,极易导致召回无关内容。

因此,我在检索步骤之前专门加入了一层“问题改写”(Question Rewriting)。其作用是将依赖上下文的追问(如Q2),补充、重写成一个语义完整的独立查询语句。

关键代码位于 app/rag_chain.py 中:

standalone_question = self._rewrite_question(llm, chat_history, question)
source_documents = self.vector_index.search(
    session_id=session.session_id,
    query=standalone_question,
    top_k=self.settings.top_k,
)

这一步的目标并非让最终生成的答案“看起来更好”,而是为了确保“检索资料”这一步能够更加精准,为后续流程打下坚实基础。

03 为什么我最终做成了混合召回?

项目初期仅使用了向量召回(Dense Retrieval),在处理“语义相近”的问题时表现尚可。然而,一旦问题中出现以下元素,效果就变得极不稳定:

  • 专有名词(如内部系统名称、产品代号)
  • 金额阈值(如“高于200元”)
  • 规则编号(如“条款第3.2条”)
  • 明确的关键词

例如,对于“退款金额高于 200 元”这类问题,BM25 这类基于关键词匹配的检索方法其实更具优势。

因此,我对召回层进行了改造,采用了混合召回策略:

  • Dense Retrieval(向量召回):主要负责处理语义近似、表达方式多样的查询。
  • BM25 Retrieval(关键词召回):主要负责处理包含明确关键词、数字、条件判断的查询。

关键实现代码在 app/vector_store.py 中:

dense_results = self._dense_search(session_id, query, candidate_k)
keyword_results = self._keyword_search(session_id, query, candidate_k)
merged_results = self._merge_results(dense_results, keyword_results, candidate_k)
return self._rerank_results(query, merged_results, top_k)

这一步改造价值显著,因为业务文档问答往往既需要深度的语义理解,也离不开对关键词和精确条件的匹配。

04 为什么还要再加 Cross-Encoder 重排序?

引入混合召回后,我发现新的问题出现了:候选文档片段“能被召回”,并不代表它们的“排序顺序就是正确的”。

尤其是在以下场景中,召回的前 Top-K 个结果里常常会混杂一些看似相关、实则并非答案核心证据的段落:

  • 同一主题但涉及不同角色或权限的段落。
  • 同样讨论“退款”,但处理条件和流程截然不同的段落。
  • 同一文档中相邻、且都包含关键词的段落。

为此,我在召回层之后又添加了一层 Cross-Encoder reranking(交叉编码器重排序):

return self.reranker.rerank(query, candidates, top_k)

它与普通的向量检索(Embedding)核心区别在于:

  • Embedding:是“先将查询和文档分别编码成向量,再计算向量间的相似度”。
  • Cross-Encoder:是“将查询和文档片段一起送入模型,让模型直接判断两者的相关性得分”。

Cross-Encoder 的计算成本更高,但判断也更为精准,因此特别适合作为第二阶段的精排器,对少量高质量候选进行最终排序。

05 严格回答和结构化输出

对于业务问答系统而言,最可怕的不是“答不上来”,而是“答错了却显得十分自信”。

因此,我在设计提示词(Prompt)时加入了两条硬性约束:

  1. 回答必须严格基于提供的上下文。
  2. 如果上下文没有相关信息,必须明确回答“我不知道”。

同时,我没有让模型自由发挥地进行文本输出,而是采用了 Pydantic 结构化输出(Structured Output):

class StructuredAnswer(BaseModel):
    answer: str
    grounded: bool
    citations: List[Citation] = Field(default_factory=list)

这样做有两个明显好处:

  1. 前端展示稳定:无需再猜测或解析模型返回的非结构化文本,直接使用定义好的字段即可。
  2. 引用关系可校验:可以通过程序检查 citations 字段与上下文的对应关系,减少模型“虚构引用”的情况。

06 缓存优化:我为什么选择 InMemoryCache

在调试提示词、反复运行相同问题、进行回归测试时,最耗时的往往是重复调用大模型。

为了提升开发与评估效率,我集成了一层 LangChain 的 InMemoryCache:

from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
set_llm_cache(InMemoryCache())

这里有一个至关重要的点需要澄清:LangChain 的 InMemoryCache 不是基于向量相似度的语义缓存(semantic cache),它是“仅当完全相同的 Prompt 再次出现时才会命中”的精确缓存。

为了避免误解,我后来还在系统中添加了简单的缓存状态观测功能,记录并展示 hits(命中)、misses(未命中)、writes(写入)等指标,方便在 Web 界面上直观了解缓存的使用情况。

07 RAGAS 评估如何集成

仅仅靠人工查看几轮问答结果,几乎不可能稳定地优化一个 RAG 系统。

因此,我为项目集成了一套内置的评估流程,使用 RAGAS 框架来量化以下几个关键指标:

  • Faithfulness(忠实度):答案是否严格基于给定的上下文,有无幻觉。
  • Answer Relevancy(答案相关性):答案是否直接回答了问题。
  • Context Recall(上下文召回率):给定的上下文是否包含了回答所需的所有信息。
  • Context Precision(上下文精确率):给定的上下文是否都是与问题高度相关的。
  • Answer Correctness(答案正确性):对比参考答案,答案的准确程度。

核心评估流程如下:

  1. 准备一组带有标准答案(ground truth)的业务测试问题集。
  2. 使用当前的 RAG 系统对这些问题进行实际问答,并记录生成的答案以及检索到的上下文(contexts)。
  3. questionanswercontextsground_truth 喂给 RAGAS 进行评估计算。

关键代码位于 app/evaluation.py

result = evaluate(
    Dataset.from_list(dataset_rows),
    metrics=[
        faithfulness,
        answer_relevancy,
        context_recall,
        context_precision,
        answer_correctness,
    ],
    llm=LangchainLLMWrapper(self.rag_service._build_llm()),
    embeddings=LangchainEmbeddingsWrapper(EmbeddingAdapter(self.embedding_service)),
    raise_exceptions=False,
)

在 Web 界面中,我将 RAGAS 的总体评分以及每个问题的细分分数都展示出来。这样,每次调整系统参数(如召回数量、Prompt 等),都可以立即从数据上看出是“检索效果变差了”还是“答案生成跑偏了”,让优化工作有据可依。

08 我踩过的几个坑

坑 1:RAGAS 安装成功,但在 Python 3.8 环境导入时报错

最初我安装了 ragas 0.2.x 版本,安装过程顺利,但在代码中 import 时直接抛出 SyntaxError
原因:新版本的 RAGAS 内部使用了 Python 3.9+ 的语法特性(如字典合并运算符 |),而我本地的项目环境是 Python 3.8。
解决方式

  • 没有选择强行升级整个项目的 Python 版本。
  • 而是将 ragas 的版本锁定在兼容 Python 3.8 的 0.1.21
  • 并在项目的 README 中明确说明了版本依赖关系。
    这种方式比“为了一个评估库而推翻整个项目环境”要稳健得多。

坑 2:误将 InMemoryCache 当作语义缓存使用

正如前文强调的,很多人看到“Cache”会想当然地认为它能缓存“语义相似”的问题。
实际上,LangChain 的 InMemoryCache 只进行字符串级别的精确匹配。
解决方式

  • 在代码中按照其真实功能接入 InMemoryCache
  • 在界面说明和 README 中明确标注:“此为精确请求缓存,非语义缓存”。
    技术命名的准确性远比营销式的模糊表述更重要。

坑 3:评估时污染了对话历史记忆(Memory)

如果直接使用同一个对话会话(Session)来运行批量评估,其中的 ConversationBufferMemory 会不断累积历史对话。这会导致后面的评估问题受到前面问题的影响,使评估结果失真。
解决方式

  • 评估时,复用了当前会话加载的文档知识库。
  • 但为每一个测试问题都创建一个全新的、隔离的 memory 对象。
    这样既保证了评估基于同一套知识数据,又避免了测试用例间的相互干扰。

坑 4:仅使用向量召回时,金额阈值类问题效果不稳定

在业务规则文档中,“高于 200 元”、“连续三次失败”、“24 小时内”这类包含明确条件和数字的信息至关重要。
仅依赖语义检索,经常会把主题相关但条件不符的段落排在前面。
解决方式

  • 引入 BM25 进行关键词召回,确保精确条件能被匹配。
  • 引入 Cross-Encoder 进行精排,对混合召回的结果进行更准确的排序。
    这是整个项目迭代中,效果提升最显著的一次架构改造。

09 如果让我再做一次,我会优先补什么?

如果你已经跟着教程跑通了 RAG 的基础流程,我建议接下来优先补全以下三个模块:

  1. 混合召回:结合向量与关键词检索,覆盖更全面的查询意图。
  2. Reranking:对召回结果进行精细排序,提升最终输入给 LLM 的上下文质量。
  3. Benchmark + 回归评估:建立可量化的评估体系,告别“凭感觉调参”。

真正让一个 RAG 系统变得“工程化”的,往往不是更精巧的 Prompt,而是:

  • 检索的稳定性:能否持续、准确地命中正确答案所在的证据片段。
  • 评估的持续性:能否通过数据指标驱动迭代,而非盲目调整。

最后

这个项目我已经完成了 Web 界面、缓存观测、RAGAS 评估和引用溯源等功能的集成。代码已在 GitHub 开源,地址如下:

https://github.com/wilsonIs/langchain-business-rag

如果你也在构建面向中文业务场景的知识库 RAG 系统,我的核心建议是:不要把所有问题都归咎于“模型不够强大”。很多时候,性能瓶颈在于工程架构的细节。优先补齐以下环节,通常比单纯更换更强大的模型能带来更显著的稳定性提升:

  • 检索前改写:让查询更完整。
  • 混合召回:让检索更全面。
  • 精排:让证据更精准。
  • 评估:让优化有方向。
  • 观测:让系统更透明。

希望这篇来自真实项目的 开源实战 经验总结,能帮助你在云栈社区探索 RAG 技术的道路上少走弯路。




上一篇:MuleRun vs OpenClaw: 从个人AI到跨境电商,我为何最终选了这头“骡子”?
下一篇:利用股票截面数据构建股息期限结构:GKK(2024)模型解析与资产定价应用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-17 07:09 , Processed in 0.477941 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表