返回顶部
热门问答 更多热门问答
技术文章 更多技术文章

别只顾着卷检索了!真正决定RAG上限的,是这四个“后处理”工程

[复制链接]
链载Ai 显示全部楼层 发表于 2 小时前 |阅读模式 打印 上一主题 下一主题
引言
在上一篇文章中告别“搜不到、搜不准”:用这套查询优化,让你的RAG检索召回率飙升,我们探讨了多种提升RAG系统检索阶段性能的策略,包括索引优化、查询转换、混合搜索及QA对生成。这些方法旨在从源头提高信息检索的召回率与准确性。


获得了初步的检索结果后,本篇文章将聚焦于后续的关键环节,即如何将这些信息转化为高质量、可靠的最终答案。内容将围绕以下几个核心主题展开:
  • 结果精炼:对初步检索到的文档进行重排序、压缩与筛选,提升上下文的信噪比。
  • 架构优化:引入查询路由等模式,构建更具弹性和智能化的系统。
  • 生成控制:通过有效的Prompt工程,确保语言模型能忠实、准确地生成回答。
  • 系统性防范:建立一套事实护栏,系统性地防范与应对“幻觉”问题。
接下来,我们将逐一深入分析。

一:检索结果后处理:提升上下文的信噪比
核心痛点:初步检索返回的Top-K文档,虽然大体相关,但依然包含噪音,且并非所有文档都同等重要。直接将它们全部喂给LLM,既浪费Token,也可能干扰模型的最终判断。

1.1 重排序:将最相关的推向前列
  • 原理: 这是提升RAG精准度的必备环节。它引入一个独立的、通常更轻量的重排序模型(如bge-reranker-base等),对初步检索到的文档列表进行二次打分和排序。这个模型的唯一任务就是更精细地判断“查询”和“文档”之间的相关性,比向量相似度的初步排序更可靠。
  • 优点: 显著将最相关的文档置于结果列表的顶端,是解决“找得准”问题的关键一步。
#示例:使用Flashrank进行重排序fromlangchain.retrievers.document_compressorsimportFlashrankRerankfromlangchain.retrieversimportContextualCompressionRetriever#1.定义一个重排序压缩器#top_n是重排序后返回多少个文档rerank_compressor=FlashrankRerank(model="miniReranker_arabic_v1",top_n=3)#2.创建一个ContextualCompressionRetriever,使用Flashrank作为压缩器rerank_retriever=ContextualCompressionRetriever(base_compressor=rerank_compressor,base_retriever=vectorstore.as_retriever(search_kwargs={"k":5})#先检索5个再重排)#3.使用print("\n---Reranking(Flashrank)示例---")query_rerank="LangChain的最新功能是什么?"retrieved_reranked_docs=rerank_retriever.invoke(query_rerank)print(f"对查询'{query_rerank}'的重排序检索结果({len(retrieved_reranked_docs)}个文档):")fori,docinenumerate(retrieved_reranked_docs):print(f"文档{i+1}(分数:{doc.metadata.get('relevance_score','N/A')}):\n{doc.page_content[:100]}...")print("-"*30)

1.2 上下文压缩:聚焦核心,降低成本
  • 原理: 在检索到文档后,再用一个LLM或特定模型,把每个文档块中与用户查询直接相关的句子或片段给筛选出来,丢掉无关的部分。
  • 优点: 大幅减少送入LLM的Token,直接降低API成本,同时帮助LLM更好地聚焦关键信息。
fromlangchain.retrieversimportContextualCompressionRetrieverfromlangchain.retrievers.document_compressorsimportLLMChainExtractorfromlangchain_openaiimportChatOpenAI#1.定义一个基础检索器(先多检索一些,再压缩)base_retriever_for_comp=vectorstore.as_retriever(search_kwargs={"k":5})#2.定义一个LLMChainExtractor(压缩器)compressor=LLMChainExtractor.from_llm(llm=llm)#3.创建ContextualCompressionRetrievercompression_retriever=ContextualCompressionRetriever(base_compressor=compressor,base_retriever=base_retriever_for_comp)#4.使用query_comp="LangChain的调试工具叫什么?它的主要作用是什么?"retrieved_compressed_docs=compression_retriever.invoke(query_comp)print(f"对查询'{query_comp}'的ContextualCompressionRetriever检索结果:")fori,docinenumerate(retrieved_compressed_docs)riginal_len=len(doc.metadata.get('original_content',doc.page_content))compressed_len=len(doc.page_content)print(f"文档{i+1}(原始长度:{original_len},压缩后长度:{compressed_len}):")print(doc.page_content)print("-"*30)

1.3 拐点法则:动态决定上下文数量
这是一个与重排序紧密配合的高级技巧。
  • 原理: 在重排序之后,根据文档的相关性分数曲线,自动找到那个从“高度相关”到“一般相关”的“拐点”,然后只截取拐点前的文档作为上下文。这避免了使用固定的Top-K,让上下文数量变得智能和自适应。
  • 此方法效果高度依赖重排序分数的质量和区分度。如果分数分布很平滑,没有明显“拐点”,则该方法可能失效。
fromtypingimportList,Tupleimportnumpyasnpfromlangchain_core.documentsimportDocumentdeffind_elbow_point(scores: np.ndarray) ->int: """  使用点到直线最大距离的纯几何方法。  返回的是拐点在原始列表中的索引。  """  n_points =len(scores) ifn_points <3:   returnn_points -1# 返回最后一个点的索引 # 创建点坐标 (x, y),x是索引,y是分数  points = np.column_stack((np.arange(n_points), scores)) # 获取第一个点和最后一个点  first_point = points[0]  last_point = points[-1] # 计算每个点到首末点连线的垂直距离 # 使用向量射影的方法  line_vec = last_point - first_point  line_vec_normalized = line_vec / np.linalg.norm(line_vec)
vec_from_first = points - first_point
# scalar_product 是每个点向量在直线方向上的投影长度 scalar_product = np.dot(vec_from_first, line_vec_normalized)
# vec_parallel 是投影向量 vec_parallel = np.outer(scalar_product, line_vec_normalized)
# vec_perpendicular 是垂直向量,它的模长就是距离 vec_perpendicular = vec_from_first - vec_parallel
dist_to_line = np.linalg.norm(vec_perpendicular, axis=1) # 找到距离最大的点的索引 elbow_index = np.argmax(dist_to_line) returnelbow_indexdeftruncate_with_elbow_method_final( reranked_docsist[Tuple[float, Document]]) ->List[Document]: ifnotreranked_docsorlen(reranked_docs) <3: print("文档数量不足3个,无法进行拐点检测,返回所有文档。") return[docfor_, docinreranked_docs] scores = np.array([scoreforscore, _inreranked_docs]) docs = [docfor_, docinreranked_docs]
# 调用我们验证过有效的拐点检测函数 elbow_index = find_elbow_point(scores)
# 我们需要包含拐点本身,所以截取到 elbow_index + 1 num_docs_to_keep = elbow_index +1 final_docs = docs[:num_docs_to_keep]
print(f"检测到分数拐点在第{elbow_index +1}位。截断后返回{len(final_docs)}个文档。") returnfinal_docsprint("\n--- 拐点检测示例 ---")# 假设 reranked_docs 是你的输入数据reranked_docs = [ (0.98,"文档1"), (0.95,"文档2"), (0.92,"文档3"), (0.75,"文档4"), (0.5,"文档5"), (0.48,"文档6")]final_documents = truncate_with_elbow_method_final(reranked_docs)print(final_documents)# 输出前三个文档

二:架构优化:用查询路由实现智能分发
核心痛点:真实场景,我们面对的问题多种多样,试图用“一套万金油”的RAG链路来解决所有问题,往往效率低下且效果不佳。

  • 原理
    : 在RAG流程的最前端,设置一个由LLM驱动的“查询路由器”。它的任务是分析用户的输入,就像智能导航一样,决定接下来该将这个请求“路由”到哪条最合适的处理链路。
  • 可能的处理链路:
    • 向量检索链路: 处理常规的语义相似性查询。
    • 摘要总结链路: 当用户意图是总结长文时,绕过检索,直接调用摘要模型。
    • 结构化查询链路: 当查询包含元数据过滤条件(如“查找2025年之后关于LCEL的文档”)时,路由到能处理结构化查询的检索器。
    • 无需检索,直接回答: 处理闲聊、问候等,直接由LLM回答。
  • 优点: 让RAG系统更具适应性和效率,是构建复杂、多功能AI助手的关键架构模式。

三:生成端控制:Prompt工程的最佳实践
核心痛点:即使我们提供了完美的上下文,一个模糊、无约束的Prompt也会让LLM“自由发挥”,导致回答偏离、甚至再次产生幻觉。

一个强大的RAG Prompt,至少应包含以下要素:
  1. 清晰的角色设定:“你是一名专业的[领域]知识库助手...”
  2. 严格的约束与底线:“请只根据提供的上下文回答。如果信息不足,请明确回答‘根据现有信息,我无法回答’。严禁使用你的内部知识或进行任何形式的编造。”
  3. 强制引用与溯源:“在你的回答结尾,必须以列表形式注明答案所参考的所有上下文文档来源...”
  4. 结构化输出要求: 要求LLM以JSON等固定格式输出,便于程序解析和后续处理。在LangChain中,使用.with_structured_output()是最可靠的方法。

四:系统性“幻觉”防范:构建AI的“事实护栏”
“幻觉”是RAG的天敌。防范它,绝不是单点技术,而是一个贯穿始终的系统工程。

  • 优质的检索与精炼 (根本): 垃圾进,垃圾出。前面所有的检索优化、重排序、压缩等技术,是防幻觉的第一道,也是最重要的防线。
  • 清晰的指令 (约束): 通过严格的Prompt工程,为LLM设定明确的行动边界,强制其成为“阅读理解者”而非“创作者”。
  • 严格的审核 (后处理): 在答案输出前,设立一个自动化的“事实核查员”,检查最终生成的答案中的关键陈述是否都能在原始上下文中找到依据。
  • 强大的基座 (模型): 通常,更新、更强大的模型(如GPT-4系列、Claude 3系列)其“遵从指令”的能力和“事实性”会更强。

本篇文章聚焦于检索之后的关键步骤。我们探讨了如何通过结果精炼、架构优化生成控制,将初步的检索结果,系统性地转化为高质量、可信赖的最终答案。掌握这些技巧,是实现RAG系统从“能用”到“可靠”的质变核心。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

链载AI是专业的生成式人工智能教程平台。提供Stable Diffusion、Midjourney AI绘画教程,Suno AI音乐生成指南,以及Runway、Pika等AI视频制作与动画生成实战案例。从提示词编写到参数调整,手把手助您从入门到精通。
  • 官方手机版

  • 微信公众号

  • 商务合作

  • Powered by Discuz! X3.5 | Copyright © 2025-2025. | 链载Ai
  • 桂ICP备2024021734号 | 营业执照 | |广西笔趣文化传媒有限公司|| QQ