尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

[RAG在LangChain中的实现-06]利用LangGraph实现基于“交谈历史”的RAG应用

[RAG在LangChain中的实现-06]利用LangGraph实现基于“交谈历史”的RAG应用 利用LangGraph编写RAG应用有一个问题它将LLM完全视为一个完全无状态的函数在用因为调用的输入都只有一条单一的消息并没有之前对话历史这样的好处是减少输入降低成本。但这将导致LLM因缺乏上下文不能从之前的“失败”中学习我们可以将create_agent默认采用的面向对话历史的实现方式应用到LangGraph的编程模式上。步骤一 检索上下文我们首先通过如下的步骤创建作为上下文检索器的Retriever对象我们利用WebBaseLoader加载博文内容并转换成Document列表后者被RecursiveCharacterTextSplitter切割之后生成的切片被添加到创建的InMemoryVectorStore中最后调用InMemoryVectorStore的as_retriever方法得到所需的检索器。loaderWebBaseLoader(web_paths(https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html,),bs_kwargs{parse_only:SoupStrainer(class_(postBody))},)documentsloader.load()splitterRecursiveCharacterTextSplitter(chunk_size1000,chunk_overlap200,add_start_indexTrue,)retriever(InMemoryVectorStore.from_documents(documentssplitter.split_documents(documents),embeddingOpenAIEmbeddings(modeltext-embedding-3-small),).as_retriever())我们定义了如下的State作为整个执行流程的状态Schema类型。为了让大家了解整个流程的执行情况我们会将每个步骤的执行信息以日志的形式写入log变量表示的列表。log[]classState(TypedDict):messages:Annotated[list[AnyMessage],operator.add]generate_times:Annotated[int,operator.add]定义generate_times对查询再生成进行计数之外新的State只包含一个返回消息历史的messages字段之前单独定义的查询文本、上下文和最终答案都存储在某个消息中。如下所示的retrieve_context为上下文检索节点函数只需要直接调用Retriever对象将格式化后的检索内容封装成HumanMessage并添加到消息列表中。在方法返回之前我们将检索信息写入日志。defretrieve_context(state:State)-dict:query_msgstate[messages][-1]querycast(str,query_msg.content)retrieved_docsretriever.invoke(query)context\n\n.join((f来源:{doc.metadata}\n内容:{doc.page_content})fordocinretrieved_docs)log.append(f检索上下文: 检索到{len(retrieved_docs)}个相关文档\n)return{messages:[HumanMessage(contentcontext)]}步骤二相关性评估我们使用LLM评估查询文本与检索内容的相关性并利用模型的结构化输出得到一个确定的二元结果评估成功或者失败如下所示的QueryEvaluationResult为绑定的结构化输出类型两个字段成员is_relevant和reason为评估结果和理由。我们使用的模型组件为ChatOpenAI并调用with_structured_output方法完成结构化输出Schema的绑定。classQueryEvaluationResult(TypedDict):对查询与上下文相关性的评估结果is_relevant:bool查询与上下文相关性的判断结果reason:str对判断结果的简要说明llmChatOpenAI(modelgpt-5.2-chat)如下所示的evaluate_query_relevance为相关性评估函数返回的字符串表示在评估成功和失败情况下的路由节点。为了因总是评估失败而导致的无限循环我们将查询文本的最大生成次数设置为3。defevaluate_query_relevance(state:State)-Literal[generate_response,regenerate_query]:if(state.get(generate_times)3):returngenerate_responsequery_msgstate[messages][-2]context_msgstate[messages][-1]evaluation_prompt(fQuery:{query_msg.content}\nfContext:{context_msg.content}\n\n请判断这个查询与上下文的相关性返回格式为{is_relevant: bool, reason: str}其中reason是对判断结果的简要说明。)responsellm.with_structured_output(QueryEvaluationResult).invoke([*state[messages],HumanMessage(contentevaluation_prompt)])is_relevantresponse[is_relevant]pathgenerate_responseifis_relevantelseregenerate_querylog.append(f查询相关性评估: 结果{不相关ifnotis_relevantelse相关}, 路由{path}, 原因{response[reason]}. )returnpath我们为当前评估任务创建了提示词但是作为输入的不仅仅是据此创建的单一HumanMessage我们还添加了之前的交谈历史。在使用查询文本和上下文格式化提示词后我们调用ChatOpenAI对象得到评估结果和理由并据此返回最终的路由节点名称。在方法返回之前我们将评估信息写入日志。步骤三查询文本再生成如下所示的查询文本再生成节点函数regenerate_query它会根据当前状态提供的查询文本和上下文构建提示词并调用ChatOpenAI对象得到由LLM生成的高质量的查询文本,输入依然携带真完整的交谈历史。我们将返回的AIMessage置于交谈历史之前会将查询再生成的信息写入日志。表示查询再生成次数的generate_times加1。defregenerate_query(state:State)-dict:context_msgstate[messages][-2]query_msgstate[messages][-1]regeneration_prompt(f根据以下上下文信息重新生成一个与查询相关的新查询。\n\nfContext:{context_msg.content}\n\nfOriginal Query:{query_msg.content}\n\n请直接回复新查询的内容不要任何额外的说明和前后缀。)new_query_msgllm.invoke([*state[messages],HumanMessage(contentregeneration_prompt)])log.append(f重新生成查询 原始查询: {query_msg.content}, 新查询: {new_query_msg.content} )return{messages:[new_query_msg],generate_times:1}步骤四生成答案最终得到的答案由如下的节点函数generate_response生成它利用状态提供的查询和上下文生成提示词,比携带之前的交谈历史调用ChatOpenAI对象。在利用返回的AIMessage写入消息历史之前我们也会将相关执行信息写入日志。defgenerate_response(state:State)-dict:context_msgstate[messages][-2]query_msgstate[messages][-1]response_prompt(f根据以下上下文信息使用精炼的语言回答查询尽量控制在100个字以内。\n\nfContext:{context_msg.content}\n\nfQuery:{query_msg.content}\n\n)ai_msgllm.invoke([*state[messages],HumanMessage(contentresponse_prompt)])log.append(f生成最终回答:{ai_msg.content})return{messages:[ai_msg]}步骤五图的构建和编译我们创建了一个StateGraph对象并将添加了上面定义的三个节点其中retrieve_context和generate_response作为入口和完成节点。我们在retrieve_context和generate_respons/regenerate_query节点之间添加了一个“条件边”条件分支函数为evaluate_query_relevance。regenerate_query和retrieve_context之间的边确保查询再生成后流程再次启动。builder(StateGraph(State).add_node(retrieve_context,retrieve_context)# type: ignore.add_node(generate_response,generate_response).add_node(regenerate_query,regenerate_query).set_entry_point(retrieve_context).set_finish_point(generate_response).add_conditional_edges(retrieve_context,evaluate_query_relevance).add_edge(regenerate_query,retrieve_context))agentbuilder.compile().with_config(recursion_limit10)payloadagent.get_graph().draw_mermaid_png()PILImage.open(io.BytesIO(payload)).show()在将StateGraph编译成Agent之后我们调用其get_graph方法得到Graph对象并将其转换成PNG图片呈现出来如下图所示。步骤六调用Agent由于State不再通过单独的query字段表示查询文本所以我们需要将查询封装成HumanMessage并添加到messages列表中如下是新的调用方式。resultagent.invoke({messages:[HumanMessage(content比较一下ASP.NET Core和Express)]})# type: ignoreformessageinresult[messages]:message.pretty_print()print(\n\nExecution Log:)fori,entryinenumerate(log):print(f{i1}.{entry})由于携带了完整的交谈历史而且整个流程又会进行多轮迭代输出的内容会很长所以下面只给出记录的日志1. 检索上下文: 检索到 4 个相关文档 2. 查询相关性评估: 结果不相关, 路由regenerate_query, 原因上下文内容主要介绍的是 ASP.NET Core 的内部原理、设计思想和 Mini 版本实现没有涉及 Express 或两者的对比信息因此无法直接支持“比较 ASP.NET Core 和 Express”的查询。. 3. 重新生成查询 原始查询: 来源: {source: https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html, start_index: 210} 内容: 2019年1月19日微软技术苏州俱乐部成立我受邀在成立大会上作了一个名为《ASP.NET Core框架揭秘》的分享。在此次分享中我按照ASP.NET Core自身的运行原理和设计思想创建了一个 “迷你版” 的ASP.NET Core框架并且利用这 个 “极简” 的模拟框架阐述了ASP.NET Core框架最核心、最本质的东西。整个框架涉及到的核心代码不会超过200行涉及到7个核心的对象。PPT下载源代码下载目录1. 从Hello World谈起2. ASP.NET Core Mini3. Hello World 24. 第一个对象HttpContext5. 第二个对象RequetDelegate6. 第三个对象Middleware7. 第四个对象ApplicationBuilder8. 第五个对象Server9. HttpContext和Server之间的适配10. HttpListenerServer11. 第六个对象WebHost12. 第七个对象WebHostBuilder13. 回顾一下Hello World 214. 打个广告《ASP.NET Core框架揭秘》1、从Hello World谈起当我们最开始学习一门技术的时候都喜欢从Hello World来时貌似和我们本篇的主题不太搭。但事实却非如此在我们看来如下这个Hello World 是对ASP.NET Core框架本质最好的体现。public class Program { public static void Main() new WebHostBuilder() .UseKestrel() .Configure(app app.Run(context context.Response.WriteAsync(Hello World!))) .Build() .Run(); 来源: {source: https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html, start_index: 1879} 内容: World应用那么可以基本上证明你已经基本理解了这个框架最本质的东西。虽然ASP.NET Core目前是一个开源的项目我们可以完全通过源码来学习它但是我相信这对于绝大部分人来说是有难度的。为此我们将ASP.NET Core最本质、最核 心的部分提取出来重新构建了一个迷你版的ASP.NET Core框架。ASP.NET Core Mini具有如上所示的三大特点。第一、它是对真实ASP.NET Core框架的真实模拟所以在部分API的定义上我们做了最大限度的简化但是两者的本质是完全一致的。如果你能理解ASP.NET Core Mini意味着你也就是理解了真实ASP.NET Core框架。第二、这个框架是可执行的我们提供的并不是伪代码。第三、为了让大家能够在最短的时间内理解ASP.NET Core框架的精髓ASP.NET Core Mini必需足够简单所以我们整个实现的核心代码不会超过200行。3、Hello World 2既然我们的ASP.NET Core Mini是可执行的意味着我们可以在上面构建我们自己的应用如下所示的就是在ASP.NET Core Mini上面开发的Hello World可以看出它采用了与真实ASP.NET Core框架一致的编程模式。public class Program 来源: {source: https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html, start_index: 2} 内容: 2019年1月19日微软技术苏州俱乐部成立我受邀在成立大会上作了一个名为《ASP.NET Core框架揭秘》的分享。在此次分享中我按照ASP.NET Core自身的运行原理和设计思想创建了一个 “迷你版” 的ASP.NET Core框架并且利用这 个 “极简” 的模拟框架阐述了ASP.NET Core框架最核心、最本质的东西。整个框架涉及到的核心代码不会超过200行涉及到7个核心的对象。 来源: {source: https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html, start_index: 1059} 内容: }如上这个Hello World程序虽然人为地划分为若干行但是整个应用程序其实只有一个语句。这个语句涉及到了ASP.NET Core程序两个核心对象WebHost和WebHostBuilder。我们可以将WebHost理解为寄宿或者承载Web应用的宿主应用的启动 可以通过启动作为宿主的WebHost来实现。至于WebHostBuilder顾名思义就是WebHost的构建者。在调用WebHostBuilder的Build方法创建出WebHost之前我们调用了它的两个方法其中UseKestrel旨在注册一个名为Kestrel的服务器而Configure方法的调用则是为了注册一个用来处理请求的中间件后者在响应的主体内容中写入一个“Hello World”文本。当我们调用Run方法启动作为应用宿主的WebHost的时候后者会利用WebHostBuilder提供的服务器和中间件构建一个请求处理管道。这 个由一个服务器和若干中间件构成的管道就是ASP.NET Core框架的核心我们接下来的核心任务就是让大家搞清楚这个管道是如何被构建起来的以及该管道采用怎样的请求处理流程。2、ASP.NET Core Mini在过去这些年中我不断地被问到同一个问题如何深入地去一个开发框架。我知道每个人都具有适合自己的学习方式而且我觉得我个人的学习方法也算不上高效所以我很少会正面回应这个问题。不过有一个方法我倒很乐意与大家分享那就是当你在学习一个开发框架的时候不要只关注编程层面的东西而应该将更多的精力集中到对架构设计层面的学习。针对某个框架来说它提供的编程模式纷繁复杂而底层的设计原理倒显得简单明了。那么如何检验我们对框架的设计原理是否透彻呢我觉得最好的方式就是根据你的理解对框架进行“再造”。当你按照你的方式对框架进行“重建”的过程中你会发现很多遗漏的东西。如果被你重建的框架能够支撑一个可以运行的Hello World应用那么可以基本上证明你已经基本理解了这个框架最本质的东西。虽然ASP.NET Core目前是 一个开源的项目我们可以完全通过源码来学习它但是我相信这对于绝大部分人来说是有难度的。为此我们将ASP.NET Core最本质、最核心的部分提取出来重新构建了一个迷你版的ASP.NET Core框架。ASP.NET Core, 新查询: 从框架核心设计与运行原理角度对比ASP.NET Core与Express重点比较两者的中间件管道模型、请求处理流程、启动与宿主机制如WebHost/Kestrel vs Node.js/HTTP Server、HttpContext/Request-Response抽象以及在可扩展性与性能上的差异。 4. 检索上下文: 检索到 4 个相关文档 5. 查询相关性评估: 结果相关, 路由generate_response, 原因查询关注ASP.NET Core与Express的核心设计与运行原理对比其中上下文详细介绍了ASP.NET Core的核心对象WebHost、WebHostBuilder、Kestrel、中间件管道等和请求处理流程能够直接支撑对ASP.NET Core一侧的分析。虽然上 下文未涉及Express但仍与查询主题中的关键一半高度相关因此整体上是相关的。. 6. 生成最终回答: ASP.NET Core 通过 WebHost/WebHostBuilder 启动Kestrel 强类型中间件构建请求管道抽象清晰、性能高、扩展性强Express 基于 Node.js HTTP Server函数式中间件串联请求模型简单灵活但类型与性能控制较弱。对于这种无脑地将整个交谈历史发送给LLM的做法会导致随着“交谈”的深入进行输入越来越多甚至会产出LLM上下文窗口限制。目前常用的解决方案是对消息历史进行摘要压缩根据当前消息历史的长度对较早的消息列表的内容提取摘要转换成一条单一消息。下面给出完整的代码fromdotenvimportload_dotenv load_dotenv()fromtypingimportTypedDict,Literal,Annotated,castfromlangchain_openaiimportChatOpenAI,OpenAIEmbeddingsfromlangchain_core.vectorstoresimportInMemoryVectorStorefromlangchain_core.messagesimportHumanMessage,AnyMessagefrombs4.filterimportSoupStrainerfromlangchain_community.document_loadersimportWebBaseLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlanggraph.graphimportStateGraphfromPILimportImageasPILImageimportio,operator loaderWebBaseLoader(web_paths(https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html,),bs_kwargs{parse_only:SoupStrainer(class_(postBody))},)documentsloader.load()splitterRecursiveCharacterTextSplitter(chunk_size1000,chunk_overlap200,add_start_indexTrue,)retriever(InMemoryVectorStore.from_documents(documentssplitter.split_documents(documents),embeddingOpenAIEmbeddings(modeltext-embedding-3-small),).as_retriever())log[]classState(TypedDict):messages:Annotated[list[AnyMessage],operator.add]generate_times:Annotated[int,operator.add]classQueryEvaluationResult(TypedDict):对查询与上下文相关性的评估结果is_relevant:bool查询与上下文相关性的判断结果reason:str对判断结果的简要说明defretrieve_context(state:State)-dict:query_msgstate[messages][-1]querycast(str,query_msg.content)retrieved_docsretriever.invoke(query)context\n\n.join((f来源:{doc.metadata}\n内容:{doc.page_content})fordocinretrieved_docs)log.append(f检索上下文: 检索到{len(retrieved_docs)}个相关文档\n)return{messages:[HumanMessage(contentcontext)]}llmChatOpenAI(modelgpt-5.2-chat)defevaluate_query_relevance(state:State)-Literal[generate_response,regenerate_query]:if(state.get(generate_times)3):returngenerate_responsequery_msgstate[messages][-2]context_msgstate[messages][-1]evaluation_prompt(fQuery:{query_msg.content}\nfContext:{context_msg.content}\n\n请判断这个查询与上下文的相关性返回格式为{is_relevant: bool, reason: str}其中reason是对判断结果的简要说明。)responsellm.with_structured_output(QueryEvaluationResult).invoke([*state[messages],HumanMessage(contentevaluation_prompt)])is_relevantresponse[is_relevant]pathgenerate_responseifis_relevantelseregenerate_querylog.append(f查询相关性评估: 结果{不相关ifnotis_relevantelse相关}, 路由{path}, 原因{response[reason]}. )returnpathdefgenerate_response(state:State)-dict:context_msgstate[messages][-2]query_msgstate[messages][-1]response_prompt(f根据以下上下文信息使用精炼的语言回答查询尽量控制在100个字以内。\n\nfContext:{context_msg.content}\n\nfQuery:{query_msg.content}\n\n)ai_msgllm.invoke([*state[messages],HumanMessage(contentresponse_prompt)])log.append(f生成最终回答:{ai_msg.content})return{messages:[ai_msg]}defregenerate_query(state:State)-dict:context_msgstate[messages][-2]query_msgstate[messages][-1]regeneration_prompt(f根据以下上下文信息重新生成一个与查询相关的新查询。\n\nfContext:{context_msg.content}\n\nfOriginal Query:{query_msg.content}\n\n请直接回复新查询的内容不要任何额外的说明和前后缀。)new_query_msgllm.invoke([*state[messages],HumanMessage(contentregeneration_prompt)])log.append(f重新生成查询 原始查询: {query_msg.content}, 新查询: {new_query_msg.content} )return{messages:[new_query_msg],generate_times:1}builder(StateGraph(State).add_node(retrieve_context,retrieve_context)# type: ignore.add_node(generate_response,generate_response).add_node(regenerate_query,regenerate_query).set_entry_point(retrieve_context).set_finish_point(generate_response).add_conditional_edges(retrieve_context,evaluate_query_relevance).add_edge(regenerate_query,retrieve_context))agentbuilder.compile().with_config(recursion_limit10)payloadagent.get_graph(xrayTrue).draw_mermaid_png()PILImage.open(io.BytesIO(payload)).show()resultagent.invoke({messages:[HumanMessage(content比较一下ASP.NET Core和Express)]})# type: ignoreformessageinresult[messages]:message.pretty_print()print(\n\nExecution Log:)fori,entryinenumerate(log):print(f{i1}.{entry})
返回列表