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

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

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

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

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模型。状态包含了应用运行时需要的所有信息:

1
2
3
4
5
6
7
8
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函数,用于处理状态并返回更新后的状态:

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:
# 使用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)

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

1
2
3
4
5
6
7
8
9
10
11
12
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的基本用法:

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

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

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

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

img

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 asyncio
asyncio.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 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 流式输出

支持实时显示执行过程:

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

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

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


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

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