1. LCEL与AgentExecutor的局限性分析 在大语言模型(LLM)应用开发领域,LangChain表达式语言(LCEL)和AgentExecutor一直是开发者的得力助手。然而,随着应用场景的复杂化,这些工具的局限性也日益凸显。本文将深入探讨LCEL和AgentExecutor的不足,并引入一个新的解决方案。
1.1 LCEL链表达式的局限性 LangChain表达式语言(LCEL)提供了一种便捷的方式来创建链式应用,将知识库、LLM、prompt、工具调用和输出解析器等组件串联成一个有向无环图。尽管LCEL极大地降低了LLM应用的开发难度,但在处理复杂、动态的对话流程时,它仍然存在一些明显的局限性:
线性流程: LCEL链通常是线性的,只能按照预定义的顺序执行步骤。这种线性结构限制了在对话中进行动态路由和条件分支的能力,使得处理复杂对话场景变得困难。
状态管理复杂: 在处理多轮对话时,LCEL的状态管理变得非常复杂。每次调用链时,都需要手动传递和更新状态,这不仅增加了代码的复杂性,也提高了出错的可能性。
工具集成不直观: 虽然LCEL链可以调用外部工具,但在链的内部结构中集成和协调多个工具的使用并不直观,尤其是在需要根据对话上下文动态选择工具时。
举个例子,在构建一个问题分解策略的并行子问题优化链时,LCEL表达式的复杂性就会显现出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 decomposition_chain = ( {"question" : RunnablePassthrough()} | decomposition_prompt | ChatOpenAI(model="gpt-4o-mini" , temperature=0 ) | StrOutputParser() | (lambda x: x.strip().split("\n" )) ) sub_question_chain = ( {"context" : retriever, "question" : RunnablePassthrough()} | sub_question_prompt | ChatOpenAI(model="gpt-4o-mini" ) | StrOutputParser() ) chain = ( { "question" : RunnablePassthrough(), "context" : decomposition_chain | { "questions" : RunnablePassthrough(), "answers" : sub_question_chain.map () } | RunnableLambda(format_qa_pairs) } | prompt | llm_output_str )
这个例子展示了当工具嵌套层级稍微多一些时,LCEL链的构建就会变得相当复杂。
1.2 AgentExecutor的局限性 AgentExecutor的出现在一定程度上解决了LCEL的部分缺陷,它允许智能体根据输入动态选择工具和操作。然而,AgentExecutor也存在一些值得注意的局限性:
复杂性: AgentExecutor的配置和使用相对复杂,特别是在处理复杂的对话流程和多轮对话时。需要手动管理智能体的状态和工具调用,这增加了开发的难度。
动态路由能力有限: 尽管AgentExecutor支持动态选择工具,但在处理复杂的条件分支和动态路由时仍然不够灵活。缺乏一种直观的方式来定义和执行复杂的对话流程。
状态持久性不足: AgentExecutor在处理长时间运行的对话时,缺乏内置的状态持久性机制。每次对话重启时,都需要从头开始,无法恢复之前的对话状态。
过度封装: AgentExecutor要求被包装的Agent必须符合特定的要求才能使用,例如输入变量固定、输入prompt固定、解析器固定等。这使得二次开发AgentExecutor变得困难。
黑盒不可控: 在构建复杂Agent时,无法修改工具的使用顺序,也无法在执行过程中添加人机交互。
1.3 引入新概念: 图和状态机 面对LCEL和AgentExecutor的局限性,我们需要一个更灵活、更强大的框架来构建复杂的智能体应用。在介绍这个新框架之前,让我们先通过一个简单的概念来帮助大家认识”图”和”状态机”。
想象一下”带娃状态图”这个场景:
状态: 婴儿的行为状态,包括饥饿程度、睡眠状态、体感温度等。
事件: 哄睡、喂奶、换尿布等行为。
节点: 妈妈(决策者)、奶奶/外婆(检查者)、爸爸(执行者)。
我们可以将这个场景构建成一个状态图:
在这个状态图中:
妈妈负责决定需要执行什么事件
老人负责判断妈妈的决定是否合理
爸爸负责执行具体的事件
执行完特定事件后,更新婴儿状态,然后由妈妈再次检测状态继续做决定
这个”带娃状态图”实际上就是一个简化版的图结构和状态机模型。将这个概念应用到LLM/Agent应用开发中,我们可以得到一个更灵活、更强大的框架。
1.4 结语 通过分析LCEL和AgentExecutor的局限性,我们认识到在构建复杂LLM应用时需要一个更灵活的框架。图结构和状态机的概念为我们提供了新的思路。在下一篇文章中,我们将介绍一个基于这些概念的新框架——LangGraph,它如何解决现有技术的问题,以及如何使用它来构建更强大的LLM应用。
2. LangGraph入门:核心概念与基础组件 在上一篇文章中,我们讨论了LCEL和AgentExecutor的局限性。今天,我们将介绍一个强大的解决方案——LangGraph,它是如何通过图和状态机的概念来解决这些问题的。
2.1 LangGraph简介 LangGraph是LangChain生态系统中的一个新成员,它提供了一个基于图(Graph)的框架来构建复杂的LLM应用。通过将应用逻辑组织成有向图的形式,LangGraph使得构建复杂的对话流程变得更加直观和灵活。
2.1.1 主要特性
循环和分支能力
支持条件语句和循环结构
可以根据状态动态决定执行路径
轻松实现复杂的对话流程控制
状态持久化
自动保存和管理状态
支持暂停和恢复执行
便于处理长时间运行的对话
人机交互支持
可以在执行过程中插入人工审核
支持编辑和修改状态
灵活的交互控制机制
流式处理
与LangChain无缝集成
复用现有的LangChain组件
支持LCEL表达式
丰富的工具和模型支持
2.2 核心概念 2.2.1 状态(State) 状态是LangGraph应用的基础,它可以是一个简单的字典或者Pydantic模型。状态包含了应用运行时需要的所有信息:
1 2 3 4 5 6 7 8 from typing import List , Dict from pydantic import BaseModelclass ChatState (BaseModel ): messages: List [Dict [str , str ]] = [] current_input: str = "" tools_output: Dict [str , str ] = {} final_response: str = ""
2.2.2 节点(Node) 节点通常是Python函数,用于处理状态并返回更新后的状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async def process_input (state: ChatState ) -> ChatState: messages = state.messages + [{"role" : "user" , "content" : state.current_input}] return ChatState( messages=messages, current_input=state.current_input, tools_output=state.tools_output ) async def generate_response (state: ChatState ) -> ChatState: response = await llm.ainvoke(state.messages) messages = state.messages + [{"role" : "assistant" , "content" : response}] return ChatState( messages=messages, current_input=state.current_input, tools_output=state.tools_output, final_response=response )
2.2.3 边(Edge) 边定义了节点之间的连接关系和路由逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 from langgraph.graph import StateGraph, ENDworkflow = StateGraph(ChatState) workflow.add_node("process_input" , process_input) workflow.add_node("generate_response" , generate_response) workflow.add_edge("process_input" , "generate_response" ) workflow.add_edge("generate_response" , END)
实战示例:简单聊天机器人 让我们通过一个简单的聊天机器人示例来展示LangGraph的基本用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 from typing import List , Dict , Tuple from pydantic import BaseModelfrom langgraph.graph import StateGraph, ENDfrom langchain_core.language_models import ChatOpenAIclass ChatState (BaseModel ): messages: List [Dict [str , str ]] = [] current_input: str = "" should_continue: bool = True async def process_user_input (state: ChatState ) -> ChatState: """处理用户输入""" messages = state.messages + [{"role" : "user" , "content" : state.current_input}] return ChatState( messages=messages, current_input=state.current_input, should_continue=True ) async def generate_ai_response (state: ChatState ) -> ChatState: """生成AI回复""" llm = ChatOpenAI(temperature=0.7 ) response = await llm.ainvoke(state.messages) messages = state.messages + [{"role" : "assistant" , "content" : response}] return ChatState( messages=messages, current_input=state.current_input, should_continue=True ) def should_continue (state: ChatState ) -> str : """决定是否继续对话""" if "goodbye" in state.current_input.lower(): return "end" return "continue" workflow = StateGraph(ChatState) workflow.add_node("process_input" , process_user_input) workflow.add_node("generate_response" , generate_ai_response) workflow.add_edge("process_input" , "generate_response" ) workflow.add_conditional_edges( "generate_response" , should_continue, { "continue" : "process_input" , "end" : END } ) app = workflow.compile () async def chat (): state = ChatState() while True : user_input = input ("You: " ) state.current_input = user_input state = await app.ainvoke(state) print ("Bot:" , state.messages[-1 ]["content" ]) if not state.should_continue: break import asyncioasyncio.run(chat())
这个示例展示了LangGraph的基本用法:
定义状态模型
创建处理节点
构建图结构
定义路由逻辑
编译和运行
2.3 最佳实践 在使用LangGraph时,有一些最佳实践值得注意:
2.3.1 状态设计
保持状态模型简洁明确
只包含必要的信息
使用类型提示增加代码可读性
2.3.2 节点函数
保持单一职责
处理异常情况
返回新的状态对象而不是修改现有状态
2.3.3 边的设计
使用清晰的条件逻辑
避免复杂的循环依赖
考虑所有可能的路径
2.3.4 错误处理
在关键节点添加错误处理
提供回退机制
记录详细的错误信息
2.4 结语 LangGraph通过提供直观的图形结构和状态管理机制,极大地简化了复杂LLM应用的开发。它不仅解决了LCEL和AgentExecutor的局限性,还提供了更强大的功能和更好的开发体验。
在下一篇文章中,我们将深入探讨LangGraph的高级特性,包括条件边的高级用法和如何实现复杂的工具调用Agent。我们将通过更复杂的实例来展示LangGraph在实际应用中的强大能力。
3. LangGraph进阶:条件边与工具调用Agent实现 在前两篇文章中,我们讨论了LCEL和AgentExecutor的局限性,以及LangGraph的基础概念。今天,我们将深入探讨LangGraph的高级特性,重点关注条件边的使用和如何实现一个完整的工具调用Agent。
3.1 条件边的高级用法 条件边是LangGraph中最强大的特性之一,它允许我们基于状态动态决定执行流程。让我们深入了解一些高级用法。
3.1.1 多条件路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from typing import List , Dict , Literal from pydantic import BaseModelfrom langgraph.graph import StateGraph, ENDclass AgentState (BaseModel ): messages: List [Dict [str , str ]] = [] current_input: str = "" tools_output: Dict [str , str ] = {} status: str = "RUNNING" error_count: int = 0 def route_by_status (state: AgentState ) -> Literal ["process" , "retry" , "error" , "end" ]: """复杂的路由逻辑""" if state.status == "SUCCESS" : return "end" elif state.status == "ERROR" : if state.error_count >= 3 : return "error" return "retry" elif state.status == "NEED_TOOL" : return "process" return "process" workflow = StateGraph(AgentState) workflow.add_conditional_edges( "check_status" , route_by_status, { "process" : "execute_tool" , "retry" : "retry_handler" , "error" : "error_handler" , "end" : END } )
3.2.2 并行执行 LangGraph支持并行执行多个节点,这在处理复杂任务时特别有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 async def parallel_tools_execution (state: AgentState ) -> AgentState: """并行执行多个工具""" tools = identify_required_tools(state.current_input) async def execute_tool (tool ): result = await tool.ainvoke(state.current_input) return {tool.name: result} results = await asyncio.gather(*[execute_tool(tool) for tool in tools]) tools_output = {} for result in results: tools_output.update(result) return AgentState( messages=state.messages, current_input=state.current_input, tools_output=tools_output, status="SUCCESS" )
3.2 实现完整的工具调用Agent 让我们通过实现一个完整的工具调用Agent来展示LangGraph的强大功能。这个Agent能够:
理解用户输入
选择合适的工具
执行工具调用
生成最终响应
3.2.1 定义状态和工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from typing import List, Dict, Optional from pydantic import BaseModel from langchain.tools import BaseTool from langchain.tools.calculator import CalculatorTool from langchain.tools.wikipedia import WikipediaQueryRun from langchain_core.language_models import ChatOpenAI class Tool(BaseModel): name: str description: str func: callable class AgentState(BaseModel): messages: List[Dict[str, str]] = [] current_input: str = "" thought: str = "" selected_tool: Optional[str] = None tool_input: str = "" tool_output: str = "" final_answer: str = "" status: str = "STARTING" # 定义可用工具 tools = [ Tool( name="calculator", description="用于执行数学计算", func=CalculatorTool() ), Tool( name="wikipedia", description="用于查询维基百科信息", func=WikipediaQueryRun() ) ]
3.2.2 实现核心节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 async def think(state: AgentState) -> AgentState: """思考下一步行动""" prompt = f""" 基于用户输入和当前对话历史,思考下一步行动。 用户输入: {state.current_input} 可用工具: {[t.name + ': ' + t.description for t in tools]} 请决定: 1. 是否需要使用工具 2. 如果需要,选择哪个工具 3. 使用什么参数调用工具 以JSON格式返回: {{"thought": "思考过程", "need_tool": true/false, "tool": "工具名", "tool_input": "参数"}} """ llm = ChatOpenAI(temperature=0) response = await llm.ainvoke(prompt) result = json.loads(response) return AgentState( **state.dict(), thought=result["thought"], selected_tool=result.get("tool"), tool_input=result.get("tool_input"), status="NEED_TOOL" if result["need_tool"] else "GENERATE_RESPONSE" ) async def execute_tool(state: AgentState) -> AgentState: """执行工具调用""" tool = next((t for t in tools if t.name == state.selected_tool), None) if not tool: return AgentState( **state.dict(), status="ERROR", thought="Selected tool not found" ) try: result = await tool.func.ainvoke(state.tool_input) return AgentState( **state.dict(), tool_output=str(result), status="GENERATE_RESPONSE" ) except Exception as e: return AgentState( **state.dict(), status="ERROR", thought=f"Tool execution failed: {str(e)}" ) async def generate_response(state: AgentState) -> AgentState: """生成最终响应""" prompt = f""" 基于以下信息生成对用户的回复: 用户输入: {state.current_input} 思考过程: {state.thought} 工具输出: {state.tool_output} 请生成一个清晰、有帮助的回复。 """ llm = ChatOpenAI(temperature=0.7) response = await llm.ainvoke(prompt) return AgentState( **state.dict(), final_answer=response, status="SUCCESS" )
3.2.3 构建完整的工作流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 workflow = StateGraph(AgentState) workflow.add_node("think" , think) workflow.add_node("execute_tool" , execute_tool) workflow.add_node("generate_response" , generate_response) def route_next_step (state: AgentState ) -> str : if state.status == "ERROR" : return "error" elif state.status == "NEED_TOOL" : return "execute_tool" elif state.status == "GENERATE_RESPONSE" : return "generate_response" elif state.status == "SUCCESS" : return "end" return "think" workflow.add_conditional_edges( "think" , route_next_step, { "execute_tool" : "execute_tool" , "generate_response" : "generate_response" , "error" : "error_handler" , "end" : END } ) workflow.add_conditional_edges( "execute_tool" , route_next_step, { "generate_response" : "generate_response" , "error" : "error_handler" , "end" : END } ) workflow.add_edge("generate_response" , END) app = workflow.compile ()
3.2.4 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 async def run_agent (user_input: str ): state = AgentState(current_input=user_input) final_state = await app.ainvoke(state) return final_state.final_answer async def main (): questions = [ "计算23乘以45等于多少?" , "谁发明了相对论?" , "计算圆周率乘以10等于多少?" ] for question in questions: print (f"\n问题: {question} " ) answer = await run_agent(question) print (f"回答: {answer} " ) import asyncioasyncio.run(main())
3.3 高级特性和技巧 3.3.1 状态持久化 LangGraph支持将状态持久化到数据库中:
1 2 3 4 5 6 7 8 9 10 11 12 13 from langgraph.persist import GraphStatePersistclass DBStatePersist (GraphStatePersist ): async def persist (self, state: AgentState ): pass async def load (self ) -> AgentState: pass workflow = StateGraph(AgentState, persist_handler=DBStatePersist())
3.3.2 流式输出 支持实时显示执行过程:
1 2 3 4 5 6 7 8 9 10 11 async def stream_handler (state: AgentState ): """处理流式输出""" yield f"思考中: {state.thought} \n" if state.selected_tool: yield f"使用工具: {state.selected_tool} \n" if state.tool_output: yield f"工具输出: {state.tool_output} \n" yield f"最终答案: {state.final_answer} \n" workflow.set_stream_handler(stream_handler)
3.3.3 错误处理和重试机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 async def error_handler (state: AgentState ) -> AgentState: """处理错误情况""" if state.error_count < 3 : return AgentState( **state.dict (), error_count=state.error_count + 1 , status="RETRY" ) return AgentState( **state.dict (), final_answer="抱歉,我无法完成这个任务。" , status="ERROR" ) workflow.add_node("error_handler" , error_handler)
3.4 结语 通过这个完整的工具调用Agent实现,我们可以看到LangGraph在处理复杂LLM应用时的强大能力:
灵活的流程控制: 通过条件边实现复杂的决策逻辑
状态管理: 清晰的状态定义和转换
错误处理: 完善的错误处理和重试机制
可扩展性: 易于添加新的工具和功能
LangGraph不仅解决了传统方法的局限性,还提供了一个更加强大和灵活的框架来构建复杂的LLM应用。通过合理使用这些高级特性,我们可以构建出更加智能和可靠的AI应用。