彻底搞懂LangGraph【之二】:构建一个可自我纠正的增强知识库RAG应用

技术

picture.image

picture.image

点击上方

蓝字

关注我们

picture.image

picture.image

让我们简单回顾 LangGraph

  • LangGraph从LangChain最近的0.1版本开始引入,用于开发更强大的AI Agent的库。
  • LangGraph诞生的目的是为了解决LLM应用中的复杂“循环”问题与Agent开发过于“黑盒化”的问题。
  • LangGraph把一个Single-Agent或者Multi-Agent系统用Graph来表示,从而能够支持最复杂的任务节点与关系。
  • LangGraph开发最重要的是定义Graph(包括任务节点Node与边Edge)与状态(state,随着任务执行而变化的状态信息)。

picture.image

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

从本篇开始,我们会陆续剖析几个我们认为最重要或最有趣的LangGraph应用案例,并尽量确保即使你没有LangChain经验,也能了解到LangGraph的魅力所在。

我们从目前最常见的知识库RAG应用开始。

picture.image

picture.image

文中代码部分参考LangChain官方案例进行修改与解读,读者可使用官方提供的Jupyter Notebook自行实验。

picture.image

PART 01

picture.image

picture.image

自纠正的RAG:C-RAG

RAG(检索增强生成)是目前LLM应用领域最为人熟知也相对成熟的一种解决方案,在构建基于私有知识库的LLM应用上表现出了较好的适应性。但做过RAG应用的朋友应该对此深有体会: RAG应用的输出效果在极大的程度上依赖于其中Retrieve这一环节召回的知识文档(也叫知识块)的相关性与精确性, 而一些不相关的文档召回甚至可能误导LLM激发“幻觉”问题。(关于RAG应用的知识召回,我们曾经介绍过一些优化方向,具体请阅读文章:深度|基于大模型的RAG应用中的四个常见问题及方案探讨【上】)。

因此,在此基础上有研究者提出了一种“自纠错的RAG”方案(Corrective-RAG,论文参考:https://arxiv.org/pdf/2401.15884.pdf):

picture.image

这个图看上去复杂,但C-RAG的核心思想是简洁的:

借助一个 轻量级的评估器 (通常也是借助LLM),评估召回的相关文档质量,将其分为 相关、存疑、不相关 ,并根据评估结果做相应的后续优化:

【针对相关文档】

  • 如果至少有一个检索文档是相关的,则该文档会交给LLM用来生成
  • 在LLM生成答案之前,还将对知识细化;并 进一步过滤无关的部分

【针对存疑/不相关文档】

  • 使用网络搜索或其他方式寻找相关知识文档来做补充
  • 在通过其他途径寻求补充知识之前对输入问题做重写(Re-write)

简而言之,C-RAG就是通过对检索出的文档做关联性评估,去除不相关的知识文档,并尝试借助其他途径补充相关知识,从而提高输入的关联知识(即LLM回答问题时需要参考的上下文)质量,让回答更准确。

PART 02

picture.image

picture.image

LangGraph实现C-RAG的关键设计

如前文所说,用LangGraph实现一个LLM应用,在开始前,你需要设计好这个应用的 “工作流程图”,也就是“Graph” ;以及在工作过程中的 “状态”信息,也就是“State”。

【Graph设计】

如我们日常在软件编码时的简单流程图类似,Graph其实代表了LLM应用在接收到一个输入时的处理流程。在这里的C-RAG的应用中, 首先考虑其存在哪些任务节点(Node),节点通常代表一个可以执行的、相对独立的流程动作,在技术上可以是一个可运行的Chain、Agent或者一个函数。 这里设计如下任务节点:

  • 检索关联文档(retrieve)

从向量库中根据输入问题搜索语义相近的知识文档

  • 文档关联性评估(grade_documents)

对搜索出的知识文档进行量化评估其关联性

  • 大模型生成答案(generate)

将问题与关联的知识文档交给LLM学习并生成答案

  • 输入问题重写(transform_query)

当retrieve的知识文档不相关时,对输入问题进行改写

  • Web搜索(web_search)

    借助在线Web搜索来补充关联知识文档

picture.image

picture.image

为了简化处理过程,此处不考虑原论文中对关联的知识文档进一步的细化(strip)、过滤与重组动作。

picture.image

因此可以定义如下的Graph,来实现一个基本的C-RAG应用:

picture.image

【State设计】

LangGraph中需要定义的State,用来在每个节点动作之间保存与传递必须的信息 。这样每个节点有State这个统一的数据访问对象,这里我们把任务过程中的以下信息保存到State:

  • question :输入问题,或者改写后的问题
  • documents :所有检索或web搜索的关联知识文档
  • run_web_search :文档评估结果,确定是否需要Web搜索
  • generation :LLM最后生成的输出答案

定义如下的State,用Dict字典对象来保存这些信息即可:


        
            

          class GraphState(TypedDict):  
    keys: Dict[str, any]
        
      

PART 03

picture.image

picture.image

代码实现与测试

设计完Graph与State以后,剩下的任务是用代码来实现Graph中的各个节点(Node)并定义节点间的关系(Edge),这里我们简单描述与解释示例代码。如果你有过RAG的初步经验,理解起来应该不困难。

Node:检索关联文档

案例中我们用百度最新产品Comate的两篇在线文档构建一个简单私有知识库,并做拆分与嵌入,构建向量存储。这部分过程与普通RAG应用一样,此处不做介绍。完成后就可以创建retrieve这个节点(注意节点的输入输出就是State):


        
            

          def retrieve(state):  
'''  
从state中取出问题 => 调用检索器获取相关文档  
把获取的文档添加到state中,并返回新的状态即可  
'''  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    documents = retriever.get\_relevant\_documents(question)  
    return {"keys": {"documents": documents, "question": question}}
        
      

Node:文档关联性评估

这个节点构建一个轻量级的文档相关性评估工具,调用这个工具可以对召回的文档做评估,得出结论:


        
            

          def grade\_documents(state):  
  
    #取出state中的问题和检索的文档  
    print("---CHECK RELEVANCE---")  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    documents = state\_dict["documents"]  
  
    #此处省略定义llm\_with\_tool/parser\_tool工具,可参考langchain文档  
  
    #Prompt提示模版  
    prompt = PromptTemplate(  
        template="""您是一个评分人员,评估检索到的文档与用户问题   
                    的相关性。以下是检索到的文档:  
        {context}  
        以下是用户的问题:{question}  
        如果文档包含与用户问题相关的关键词或语义意义,请将其评为相关。给出一个yes或no来表明文档是否与问题相关。""",  
        input\_variables=["context", "question"],  
    )  
  
    #构建一个评估的Chain  
    chain = prompt | llm\_with\_tool | parser\_tool  
  
    #对文档做评估  
    filtered\_docs = []  
    search = "No"   
    for d in documents:  
        score = chain.invoke({"question": question, "context": d.page\_content})  
        grade = score[0].binary\_score  
  
        #如果相关,则添加到关联文档  
        if grade == "yes":  
            print("---GRADE: DOCUMENT RELEVANT---")  
            filtered\_docs.append(d)  
  
        #如果不相关,则要求进行web搜索  
        else:  
            print("---GRADE: DOCUMENT NOT RELEVANT---")  
            search = "Yes"  # Perform web search  
            continue  
  
    #将过滤后的文档、是否需要web搜索保存到state返回  
    return {  
        "keys": {  
            "documents": filtered\_docs,  
            "question": question,  
            "run\_web\_search": search,  
        }  
    }
        
      

Node:LLM生成答案

这部分非常简单,即把问题和关联知识丢给LLM生成答案:


        
            

          def generate(state):  
  
    print("---GENERATE---")  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    documents = state\_dict["documents"]  
  
    # 获取一个现成的Prompt(借助langchain hub)  
    prompt = hub.pull("rlm/rag-prompt")  
  
    # 模型  
    llm = ChatOpenAI(model\_name="gpt-3.5-turbo", temperature=0, streaming=True)  
  
    #Chain:提示词=>llm=>输出解析  
    rag\_chain = prompt | llm | StrOutputParser()  
  
    #调用  
    generation = rag\_chain.invoke({"context": documents, "question": question})  
  
    #结果放入state,返回  
    return {  
        "keys": {"documents": documents, "question": question, "generation": generation}  
    }
        
      

Node:输入问题重写

这个节点是在发现召回的文档相关性不够时,使用LLM优化输入问题:


        
            

          def transform\_query(state):、  
  
    print("---TRANSFORM QUERY---")  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    documents = state\_dict["documents"]  
  
    #提示模版  
    prompt = PromptTemplate(  
        template="""你需要生成针对检索优化的问题。请根据输入内容,尝试推理其中的语义意图/含义。这是初始问题:  
               {question}  
                 请提出一个改进的问题:""",  
                 input\_variables=["question"],  
    )  
  
    # 模型  
    model = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-1106", streaming=True)  
  
    # 链  
    chain = prompt | model | StrOutputParser()  
  
    # 调用获得改进的问题  
    better\_question = chain.invoke({"question": question})  
  
    # 放入state返回  
    return {"keys": {"documents": documents, "question": better\_question}}
        
      

Node:Web搜索

这个节点我们借助Bing搜索,来搜索问题的网络相关知识:


        
            

          def web\_search(state):  
    print("---WEB SEARCH---")  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    documents = state\_dict["documents"]  
  
    #bing搜索  
    tool = BingSearchAPIWrapper(k=3)  
    docs = tool.run(question)  
    web\_results = "\n".join([d for d in docs])  
    web\_results = Document(page\_content=web\_results)  
  
    #搜索结果添加到已经有的documents后面返回  
    documents.append(web\_results)  
  
    return {"keys": {"documents": documents, "question": question}}
        
      

其他:辅助方法

上一篇介绍LangGraph的边(edge)时说过 , 对于条件边,需要有一个辅助判断的函数,用来帮助决定走向哪个节点。 这里由于需要根据文档相关性评估的结果来决定是生成答案,还是进行web搜索。因此需要这样一个辅助方法,很好理解:


        
            

          def decide\_to\_generate(state):  
    print("---DECIDE TO GENERATE---")  
    state\_dict = state["keys"]  
    question = state\_dict["question"]  
    filtered\_documents = state\_dict["documents"]  
  
    #取出评估节点放入的评估结果:是否需要web搜索  
    search = state\_dict["run\_web\_search"]  
  
    #如果需要,返回下一个节点:transform\_query  
    if search == "Yes":  
        print("---DECISION: TRANSFORM QUERY and RUN WEB SEARCH---")  
        return "transform\_query"  
  
    #如果不需要,返回下一个节点:generate  
    else:  
        # We have relevant documents, so generate answer  
        print("---DECISION: GENERATE---")  
        return "generate"
        
      

构建Graph,创建APP

在节点(node)与辅助函数构建完成后,就可以构建Graph:


        
            

          #定一个一个Graph  
workflow = StateGraph(GraphState)  
  
#添加上面准备的Node  
workflow.add\_node("retrieve", retrieve) # retrieve  
workflow.add\_node("grade\_documents", grade\_documents) # grade documents  
workflow.add\_node("generate", generate) # generatae  
workflow.add\_node("transform\_query", transform\_query) # transform\_query  
workflow.add\_node("web\_search", web\_search) # web search  
  
#特殊边:入口  
workflow.set\_entry\_point("retrieve")  
  
#普通边:检索完成后,进入文档评估  
workflow.add\_edge("retrieve", "grade\_documents")  
  
#条件边:借助辅助方法决定进入哪一个节点  
workflow.add\_conditional\_edges(  
    "grade\_documents",  
    decide\_to\_generate,  
    {  
        "transform\_query": "transform\_query",  
        "generate": "generate",  
    },  
)  
  
#普通边:改写问题后总是去搜索  
workflow.add\_edge("transform\_query", "web\_search")  
  
#普通边:搜索后,生成最后答案  
workflow.add\_edge("web\_search", "generate")  
  
#特殊边:生成答案后结束  
workflow.add\_edge("generate", END)  
  
#编译成应用,完成!  
app = workflow.compile()
        
      

测试APP

  1. 我们首先测试一个知识库能够完美回答的问题:

        
            

          inputs = {"keys": {"question": "百度的comate是什么?有哪些特点?"}}  
for output in app.stream(inputs):  
  
    #output中保存了每一步节点完成信息,key是节点名,value是state  
    for key, value in output.items():  
        pprint.pprint(f"Node '{key}':")  
        pprint.pprint("\n---\n")  
  
# 打印最后的State中的generation,就是最终答案  
pprint.pprint(value["keys"]["generation"])
        
      

查看输出日志,可以看到 这次任务经过了retrieve,grade_documents,generate三个节点,而没有经过web_search,原因是因为召回的文档都相关(document relevant)。

picture.image

  1. 但如果把上面的输入问题换一个知识库无法完全回答的问题,比如:

picture.image

观察输出日志,可以 发现任务多经过了transform_query与web_search两个新的节点,原因是开始召回的知识文档经过评估后,发现无法回答输入问题(NOT RELEVANT),因此需要借助Web搜索来补充知识

picture.image

至此,我们就完成了这个基础的C-RAG应用。

PART 04

picture.image

picture.image

结束语

以上部分我们用LangGraph来实现了一种优化的RAG方案:通过对召回的关联知识文档进行评估,并根据评估结果去除不相关的干扰知识,同时借助其他手段(改写问题并Web搜索)来补充知识文档,从而让最终结果更准确。 在这个例子中,展现了LangGraph在构建复杂LLM应用时的灵活与强大的能力。由于采用Graph这种支持更复杂关系的结构来定义任务过程,也就具备了构建超级AI Agent的底层基础。

后续文章中将继续分享与剖析其他LangGraph使用案例。

picture.image

END

点击下方关注我,不迷路

与作者交流请识别以下名片

picture.image

HAS ARRIVED

picture.image

点个在看你最好看

0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
如何利用云原生构建 AIGC 业务基石
AIGC即AI Generated Content,是指利用人工智能技术来生成内容,AIGC也被认为是继UGC、PGC之后的新型内容生产方式,AI绘画、AI写作等都属于AIGC的分支。而 AIGC 业务的部署也面临着异构资源管理、机器学习流程管理等问题,本次分享将和大家分享如何使用云原生技术构建 AIGC 业务。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论