传统RAG过时了?从RAG到RAG Flow的架构演进与技术实现 |LLM应用探讨

技术

点击上方蓝字关注我们

picture.image

大模型发展到现在,随着下游应用特别是B端应用的不断拓展与验证,不管是知识密集型的RAG类搜索与问答应用,还是更复杂的AI Agents,在架构上都在不断创新演进,跳出简单的提示工程与传统“链”式架构,以适应更高的任务能力要求。本文结合当前最常见的RAG(检索增强生成)应用的发展,来简单认识其最新的架构演进与技术实现。

picture.image

  • LLM应用架构的演进
  • RAG应用面临的挑战
  • 从RAG到RAG Flow
  • 实现一个RAG Flow

picture.image

01

LLM应用架构的演进

SPRING HAS ARRIVED

这里探讨的应用是 以LLM为核心驱动,能够自主迭代完成一系列设定的工作步骤的“原生“LLM应用。 当前这一类应用的最主要形式就是AI Agents智能体与RAG类应用(两者常常也会融合),也是很多商业大模型应用开发平台或开发框架最关注的领域,应用架构也不再是一个简单的Re-Act范式或者Retrieve-Augment可概括,而是体现出一些新的特征与趋势:

  • 从依赖于单一模型到多模型的协作。 随着大模型在很多领域的专业化,以及更多“小”模型的出现,让“合适的模型做合适的事”会成为一项技术考量。比如有的模型擅长某领域知识推理,有的模型针对RAG场景做了微调,有的模型则在Text2SQL任务上表现更优。
  • 从“黑盒子”转向更开放、可编排、可跟踪。 大模型本身是一种缺乏可解释性的一种“黑盒”,如果LLM应用完全依赖于大模型自身的决策与思维链(COT),就会极大的增加不确定性。这一点在一些Agent项目如AutoGPT的早期版本中深有体会,遇到复杂任务时只能是“开盲盒”。

picture.image

  • 从顺序式为主的简单架构走向复杂的WorkFlow 。随着应用场景的复杂与更多优化方法的出现,几个简单的顺序执行步骤已经无法达到最佳的任务效果。借助于分支、并行、迭代、循环等更复杂的工作流程,可以充分挖掘大模型的潜能,并使得应用效果达到最优化。

picture.image

还记得LangGraph吗?

在前几篇文章中剖析过的LangGraph,其推出的根本动机正是为了适应越来越复杂Agent工作流。现有的Chain与黑盒模式的Agent组件无法满足新的要求。LangGraph采用Graph这种更灵活的算法结构来定义Agent工作流的任务节点、关系与状态迁移,几乎可以满足无限场景下的复杂Agent需求,极大地提升了开发效率。具体请参考:

彻底搞懂LangGraph:构建强大的Multi-Agent多智能体应用的LangChain新利器 【1】

picture.image

picture.image

START

picture.image

  • 多种模型、技术、算法与范式的融合架构。 单一的Prompt工程加大模型显然已经无法满足要求。在一个优化的Agent应用中,可能会融合到更多类型的AI模型与技术,单模态、多模态、预测模型、视觉模型、向量搜索、关键字搜索、重排算法等多种AI技术的结合;再比如,在一个Text2SQL的分析助手中可以结合RAG的思想以更好地增强SQL的生成。

我们从最常见的LLM RAG应用来认识这种架构转变与相关技术。

picture.image

02

RAG应用面临的挑战

SPRING HAS ARRIVED

RAG的基本思想已经为人熟知: 借助于检索技术将外部知识与上下文补充给大模型,帮助大模型生成更准确更可靠的响应内容,尽可能地规避大模型的“幻觉”问题。

picture.image 传统经典RAG架构面临挑战

但在实际应用中,RAG往往是上手容易但是要做到尽可能完美,特别是达到企业生产应用条件却很难。这其中一些原因来自于企业运营,比如用来“增强”的企业私有知识的管理与维护;另一方面的原因则来自于技术本身,这源于自然语言的复杂性,给检索与理解带来的挑战。其中最主要的一些挑战来自于:

  • 知识召回的精准度

如何保证检索召回的外部知识有足够的相关性与覆盖性就是生成阶段(Generation)的重要保障。如果召回的信息带有大量的无用、噪声甚至矛盾信息,或者最重要的信息不在大模型的主要“关注区域”,就会干扰到最终大模型的输出质量。

召回的精准度通常涉及到索引与检索两大环节:

  • 索引: 这包括如何对原始知识数据(文档、数据库、网站等)进行加载、识别、切分、索引等处理。到微观层面,又会涉及到多模态文档的处理、切分块大小的选择、索引机制的选择、Embedding模型的选择等一系列技术问题。

  • 检索: 在实际应用中,尽管当前基于Embedding模型的向量技术对语义检索能力有更好的支撑与提升,但诸如索引块的大小、Embedding 算法、原始的“脏”知识等都可能导致检索结果质量不高。

  • 大模型自身的生成能力

检索产生的相关知识形成的上下文最后需要LLM来理解并生成,所以LLM的生成能力无疑是最重要的另外一个环节。

对不同的场景来说,如何选择合适的模型、需要怎样的参数尺寸,是在实施RAG应用最先困惑与面临的选择。比如简单的知识问答,可能在6B,13B也能有较好的效果;但是对于SQL生成、Tools使用的生成,可能千亿级大模型,也很难达到80%以上的准确率。

此外,检索产生的相关知识的排序、干扰信息、多余信息、矛盾信息等需要LLM自身能够尽量地识别与区分,因此模型本身的“抗干扰”能力大小以及“注意力”的范围也是影响最终生成的重要因素。所以在一些实际应用中,会针对RAG场景微调大模型,以使其更适合RAG应用的场景。

  • 可能的技术限制与难点

此外,RAG应用还会面临其他的一些可能的技术限制与难点,比如携带过多的关联知识可能会突破大模型上下文窗口大小的限制;比如过多的处理步骤可能会影响到端到端的响应性能;比如在典型的多轮对话中,一个输入问题的完整语义可能分布在多轮交互中,如何进行合并这些语义表达来形成完整的检索输入。

picture.image

03

从RAG到RAG Flow

SPRING HAS ARRIVED

正因为这些RAG应用中面临的挑战,也促成了其架构的不断优化与演进。这里我们借助同济大学等团队最近的一篇调查报告 《Retrieval-Augmented Generation for Large Language Models: A Survey》 中的观点来认识RAG最新架构的发展: 从最早的顺序式RAG范式(即输入->检索->生成)发展成了更具灵活性的模块化RAG(Modular RAG)架构

picture.image

图片来自原论文

这里的基本思想是:

将RAG应用中的各个步骤细分成了多个 模块类 (代表RAG应用中的一个核心流程,比如预检索)、 模块 (代表一个核心流程中的功能,比如预检索中的查询重写)与 运算符/算法 (代表模块的一种实现方法,比如查询重写可以有普通的重写、HyDE重写等)。 这些模块与算法不再有固定的选择与流程,而是由开发者根据场景灵活组合与编排,构建适合自己的RAG系统,这种灵活编排的工作流程可以称之为RAG Flow。

picture.image

picture.image

此处我们不对其中每个模块及实现的典型算法进行逐个介绍,具体可以参考原Github项目(Github中搜索RAG-Survey项目)

picture.image

在上面的图中可以看到, 推理阶段的RAG Flow分成四种主要的基础模式:顺序、条件、分支与循环 ,这里做简单分析(图片均来自原论文)。

【顺序范式】

这是经典的RAG范式,当然,与简单的query-retrieve-generate的过程相比,这里加上了Pre-Retrieval和Post-Retrieval的模块,而其中常用的算法比如Pre-Retrieval的Query Rewrite(查询重写),以及Post-Retrieval的Rerank(重排序):

picture.image

【条件范式】

顾名思义,条件范式的RAG Flow就是会根据不同的条件选择不同的后续RAG路径。这里的条件判断就需要增加路由模块,可以根据关键词或者语义来路由到不同的后续路径,不同的路径可以有不同的流程、模型、提示词、算法等。

这在构建RAG应用中是比较常见的一种范式。 比如,你可以在构建企业的一个增强知识助手时,会根据语义路由到不同的知识库检索,甚至要求LLM采用不同的语气回答。

picture.image

【分支范式】

分支范式就是在流程中会出现多个并行的分支,其中并行的部分可能是Retrieve检索环节,也可能是Generate生成环节。

分支也是常见的一种RAG优化范式, 比如以下是一个在查询开始阶段通过LLM对输入问题进行扩展(比如扩充3-5个新的相关/相似问题),再用相似问题进行多次检索,最后将检索出来的知识再次排序后交给LLM做生成。

picture.image

【循环范式】

在循环范式中,具有更加复杂的推理过程与逻辑,根据场景的不同,在循环范式中可以出现多次 迭代、递归、自适应检索 等方法。下面是一个迭代模式的RAG Flow:

picture.image

在该RAG Flow的每次迭代中,会利用前一次迭代的模型输出作为特定上下文来帮助检索更多相关知识,再利用新的知识做进一步的生成,而循环的终止由预定义的迭代次数确定。很显然, 这种RAG流的好处是通过多次迭代,尽可能地检索出更多相关的知识,并由LLM负责整理生成。

循环范式的一个知名案例是Self-RAG,一种借助LLM进行自我反思与按需检索,以提高生成准确性与质量的框架项目(具体可以在Github搜索Self-RAG)。

picture.image

04

实现一个RAG Flow

SPRING HAS ARRIVED

实现相对复杂的RAG Flow架构,建议基于当下主流的两种开发框架来完成 :LangChain或者LlamaIndex。

两种框架各具优势,相对来说LangChain更面向通用的LLM应用而设计,功能更强大但使用相对更复杂;另外,在推出了最新的 LangGraph 以后,在构建复杂Workflow的RAG或者Agent上获得了极大的增强;而LlamaIndex则更加针对RAG类应用而设计,预置了大量上述模块化RAG架构中的算法实现,包括各种高级检索、模型微调等。

这里以上面分支范式中的RAG Flow作为样例实现,这也是常见的一种用于优化RAG输出的方法,常用在基于LLM的精准搜索应用中。由于这个流程中不涉及较复杂的“循环”,此处我们直接采用Chain实现,而非LangGraph。

其工作机制如下:

picture.image

  1. 借助于LLM将原始查询扩展转换为多个相似但又不同的问题。 注意相似问题尽量能够针对原始问题提供不同的视角,以拓展输入问题的广度与深度。

picture.image

为什么需要扩展查询

通常我们在使用一个问答或者搜索系统时,只会习惯于用单个的输入查询。但是单个的问题可能无法完整的或者更深入细致的表达使用者真正的意图,这可能会导致检索的关联知识也无法更好地覆盖需要了解的内容。比如你想了解“个人所得税专项附加扣除的相关规定”,那可以生成类似“个税专项附加扣除的扣除标准”,”个税专项附加扣除的在线操作流程”,“个税专项附加扣除的主要类型”等相似问题,这些问题可以更好地帮助检索相关知识,并提供不同的视角,生成更全面与深入的答案。

picture.image

picture.image

picture.image

  1. 对原始输入与新的问题执行基于向量的语义搜索,获得搜索结果。

  2. 对多个搜索结果进行结果重排名,这里使用倒数排名融合算法(RRF)。

picture.image

关于RRF

倒数排名融合 (RRF) 是一种将多个搜索结果列表的排名组合起来以生成单个统一排名的技术。通过组合不同查询的排名,可以增加最相关的文档/知识出现在最终排名顶部的机会,从而帮助LLM提高生成响应结果的质量。如果对RRF的细节感兴趣,可以阅读:https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf

picture.image

picture.image

picture.image

  1. 将重新排序的相关知识与输入问题交给LLM进行生成。

以下是主要的代码实现(仅展现核心过程):

【构建向量库与检索器】


        
            

          #加载知识文档  
loader = TextLoader(file\_path="test.txt")  
document = loader.load()  
  
#分割文档  
text\_splitter = CharacterTextSplitter(separator="\n",chunk\_size=500,chunk\_overlap=0)  
data = text\_splitter.split\_documents(document)  
  
#嵌入并存储向量库  
db=Chroma.from\_documents(documents=data,embedding\_function=OpenAIEmbeddings(),persist\_directory="./chrom\_db")  
  
#检索器  
retriever=db.as\_retriever(k=5)
        
      

【查询扩展】

使用Langchain构建查询扩展chain


          
llm=ChatOpenAI(temperature=0.0,model_name='gpt-3.5-turbo-1106')
          
template = """你是一个聪明的AI助手,能够根据输入的单个查询生成多个相关的查询问题.请根据如下查询内容生成5个相关的搜索查询: {query}
          
"""prompt = ChatPromptTemplate.from_template(template)
          
#LCEL语言构建一个Chaingenerate_queries = (    prompt | llm | StrOutputParser() | (lambda x: x.split("\n")))
      

【RRF排序】


        
            

          #RRF实现算法:对每个相关问题搜索出来的文档列表依次处理  
#重新计算每个文档的Score,最后形成重新排序的文档结果  
def reciprocal\_rank\_fusion(results: list[list], k=60):  
    fused\_scores = {}  
    for docs in results:  
        for rank, doc in enumerate(docs):  
            doc\_str = dumps(doc)  
            if doc\_str not in fused\_scores:  
                fused\_scores[doc\_str] = 0  
            previous\_score = fused\_scores[doc\_str]  
            fused\_scores[doc\_str] += 1 / (rank + k)  
  
    reranked\_results = [  
        (loads(doc), score)  
        for doc, score in sorted(fused\_scores.items(), key=lambda x: x[1], reverse=True)  
    ]  
    return reranked\_results
        
      

【Chain:生成->检索->重排】


        
            

          #构造一个rag fusion的chain  
#处理过程:生成相似问题 => 检索问题相关文档 => 文档重排序  
ragfusion\_chain = generate\_queries | retriever.map() | reciprocal\_rank\_fusion
        
      

【Chain:生成最终结果】


        
            

          template = """  
基于如下上下文回答问题:  
{context}  
===  
问题: {question}  
"""  
prompt = ChatPromptTemplate.from\_template(template)  
#构建生成器的Chain  
#输入:retriever返回的检索结果与输入问题  
final\_chain = ({"context": rag\_fusion\_chain, "question": RunnablePassthrough ()}  
| prompt  
| llm  
| StrOutputParser ()  
)
        
      

【测试】


        
            

          final\_chain.invoke({question:"请介绍个人所得税专项附加扣除的政策"})
        
      

picture.image

05

结束语

SPRING HAS ARRIVED

前文简单探讨了当前LLM原生应用为了适应更复杂的业务环境与更苛刻的工程要求,在架构的演进上体现出的一些新的特征与范式,特别是更灵活的基于WorkFlow的架构,极大地扩充了LLM的应用场景,提升了任务准确性与可靠性。

【参考链接】

picture.image

END

点击下方关注我,不迷路

交流请识别以下名片并说明来源

picture.image

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论