1. LCEL与AgentExecutor的局限性分析

在大语言模型(LLM)应用开发领域,LangChain表达式语言(LCEL)和AgentExecutor一直是开发者的得力助手。然而,随着应用场景的复杂化,这些工具的局限性也日益凸显。本文将深入探讨LCEL和AgentExecutor的不足,并引入一个新的解决方案。

1.1 LCEL链表达式的局限性

LangChain表达式语言(LCEL)提供了一种便捷的方式来创建链式应用,将知识库、LLM、prompt、工具调用和输出解析器等组件串联成一个有向无环图。尽管LCEL极大地降低了LLM应用的开发难度,但在处理复杂、动态的对话流程时,它仍然存在一些明显的局限性:

线性流程: LCEL链通常是线性的,只能按照预定义的顺序执行步骤。这种线性结构限制了在对话中进行动态路由和条件分支的能力,使得处理复杂对话场景变得困难。

状态管理复杂: 在处理多轮对话时,LCEL的状态管理变得非常复杂。每次调用链时,都需要手动传递和更新状态,这不仅增加了代码的复杂性,也提高了出错的可能性。

工具集成不直观: 虽然LCEL链可以调用外部工具,但在链的内部结构中集成和协调多个工具的使用并不直观,尤其是在需要根据对话上下文动态选择工具时。

举个例子,在构建一个问题分解策略的并行子问题优化链时,LCEL表达式的复杂性就会显现出来:

# 分解问题链
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的局限性,我们需要一个更灵活、更强大的框架来构建复杂的智能体应用。在介绍这个新框架之前,让我们先通过一个简单的概念来帮助大家认识”图”和”状态机”。

想象一下”带娃状态图”这个场景:

  • 状态: 婴儿的行为状态,包括饥饿程度、睡眠状态、体感温度等。
  • 事件: 哄睡、喂奶、换尿布等行为。
  • 节点: 妈妈(决策者)、奶奶/外婆(检查者)、爸爸(执行者)。

我们可以将这个场景构建成一个状态图:

img

在这个状态图中:

  • 妈妈负责决定需要执行什么事件
  • 老人负责判断妈妈的决定是否合理
  • 爸爸负责执行具体的事件
  • 执行完特定事件后,更新婴儿状态,然后由妈妈再次检测状态继续做决定

这个”带娃状态图”实际上就是一个简化版的图结构和状态机模型。将这个概念应用到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 主要特性

  1. 循环和分支能力
  • 支持条件语句和循环结构
  • 可以根据状态动态决定执行路径
  • 轻松实现复杂的对话流程控制
  1. 状态持久化
  • 自动保存和管理状态
  • 支持暂停和恢复执行
  • 便于处理长时间运行的对话
  1. 人机交互支持
  • 可以在执行过程中插入人工审核
  • 支持编辑和修改状态
  • 灵活的交互控制机制
  1. 流式处理
  • 支持流式输出
  • 实时反馈执行状态
  • 提升用户体验
  1. 与LangChain无缝集成
  • 复用现有的LangChain组件
  • 支持LCEL表达式
  • 丰富的工具和模型支持

2.2 核心概念

2.2.1 状态(State)

状态是LangGraph应用的基础,它可以是一个简单的字典或者Pydantic模型。状态包含了应用运行时需要的所有信息:

from typing import List, Dict
from pydantic import BaseModel

class ChatState(BaseModel):
messages: List[Dict[str, str]] = []
current_input: str = ""
tools_output: Dict[str, str] = {}
final_response: str = ""

2.2.2 节点(Node)

节点通常是Python函数,用于处理状态并返回更新后的状态:

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:
# 使用LLM生成回复
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)

边定义了节点之间的连接关系和路由逻辑:

from langgraph.graph import StateGraph, END

# 创建图结构
workflow = 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的基本用法:

from typing import List, Dict, Tuple
from pydantic import BaseModel
from langgraph.graph import StateGraph, END
from langchain_core.language_models import ChatOpenAI

# 1. 定义状态
class ChatState(BaseModel):
messages: List[Dict[str, str]] = []
current_input: str = ""
should_continue: bool = True

# 2. 定义节点函数
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"

# 3. 构建图
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
}
)

# 4. 编译图
app = workflow.compile()

# 5. 运行对话
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 asyncio
asyncio.run(chat())

这个示例展示了LangGraph的基本用法:

  1. 定义状态模型
  2. 创建处理节点
  3. 构建图结构
  4. 定义路由逻辑
  5. 编译和运行

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 多条件路由

from typing import List, Dict, Literal
from pydantic import BaseModel
from langgraph.graph import StateGraph, END

class 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支持并行执行多个节点,这在处理复杂任务时特别有用:

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能够:

  1. 理解用户输入
  2. 选择合适的工具
  3. 执行工具调用
  4. 生成最终响应

img

3.2.1 定义状态和工具

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 实现核心节点

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 构建完整的工作流

# 创建图结构
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 使用示例

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 asyncio
asyncio.run(main())

3.3 高级特性和技巧

3.3.1 状态持久化

LangGraph支持将状态持久化到数据库中:

from langgraph.persist import GraphStatePersist

class DBStatePersist(GraphStatePersist):
async def persist(self, state: AgentState):
# 实现状态持久化逻辑
pass

async def load(self) -> AgentState:
# 实现状态加载逻辑
pass

# 使用持久化
workflow = StateGraph(AgentState, persist_handler=DBStatePersist())

3.3.2 流式输出

支持实时显示执行过程:

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 错误处理和重试机制

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应用时的强大能力:

  1. 灵活的流程控制: 通过条件边实现复杂的决策逻辑
  2. 状态管理: 清晰的状态定义和转换
  3. 错误处理: 完善的错误处理和重试机制
  4. 可扩展性: 易于添加新的工具和功能

LangGraph不仅解决了传统方法的局限性,还提供了一个更加强大和灵活的框架来构建复杂的LLM应用。通过合理使用这些高级特性,我们可以构建出更加智能和可靠的AI应用。


本站由 卡卡龙 使用 Stellar 1.33.1主题创建

本站访问量 次. 本文阅读量 次.