各位同仁,下午好!
今天我们齐聚一堂,探讨在构建复杂、动态的AI代理系统时,两种核心的流程管理范式:Flow Orchestration (流编排) 和 Choreography (流编舞)。特别地,我们将深入研究它们在 LangGraph 框架中的应用,并分析哪种模式更适合处理 LangGraph 所擅长的动态任务。
在人工智能领域,尤其是大语言模型(LLM)驱动的代理系统中,任务往往不是线性的。它们可能涉及条件判断、工具调用、多轮对话、错误恢复甚至人机协作,这些都属于“动态任务”。有效的管理这些任务的执行流程,是构建健壮、智能代理的关键。
LangGraph 作为 LangChain 的一个扩展,提供了一种强大的方式来构建有状态、多步骤的代理。它将代理的决策逻辑和执行路径建模为有向图,从而能够更清晰地管理复杂流程。那么,在这片图结构的世界里,我们该如何选择编排与编舞这两种不同的策略呢?
一、理解 Flow Orchestration(流编排)
1.1 定义与核心原则
流编排是一种集中式的流程管理模式。它假定存在一个中心化的协调者(orchestrator),负责定义、控制和管理整个业务流程的执行顺序和步骤。这个协调者知道所有参与者(或服务)的角色、它们之间的依赖关系以及每个步骤的预期结果。它就像一个乐队指挥,精确地指导每个乐器何时演奏、演奏什么,确保整体和谐。
在编排模式下,流程逻辑通常是明确定义的,并且集中在一个地方。协调者会依次调用各个服务,传递必要的数据,并等待每个服务的响应,然后根据响应决定下一步行动。
核心原则:
- 集中控制: 有一个明确的中心实体来控制整个流程。
- 显式调用: 协调者显式地调用每个服务或组件。
- 强耦合: 协调者对参与者有较强的依赖,了解它们的接口和行为。
- 状态管理: 协调者通常负责维护整个流程的状态。
1.2 何时使用编排
编排模式在以下场景中表现出色:
- 复杂、有状态的业务流程: 当流程涉及多个步骤,且步骤之间存在复杂的条件判断、数据依赖或状态转换时。
- 需要明确的执行顺序和决策逻辑: 当业务流程有清晰的开始、结束,并且中间步骤的流转逻辑是可预测和可控的。
- 错误处理和补偿逻辑: 集中式控制器更容易实现全局的错误处理和回滚机制。
- 需要审计和监控: 所有的流程路径都经过中心协调者,便于追踪和日志记录。
- LangGraph 的天然契合: LangGraph 的图结构本身就是一种强大的编排工具。
1.3 优点与缺点
优点:
- 清晰的流程视图: 整个业务流程一目了然,易于理解和维护。
- 强大的控制力: 协调者可以精确控制每个步骤的执行,进行条件分支、并行执行等。
- 易于调试和错误处理: 所有逻辑集中,便于定位问题和实现统一的错误处理策略。
- 适合复杂依赖: 能够很好地管理服务之间的复杂依赖关系。
缺点:
- 单点故障风险: 协调者是核心,如果它出现问题,整个流程都会中断。
- 可伸缩性挑战: 随着业务逻辑的增长,协调者的复杂性会急剧增加,可能成为瓶颈。
- 耦合度高: 协调者需要了解所有参与者的接口和行为,导致系统各部分之间耦合度较高。
- 灵活性受限: 改变流程或引入新服务可能需要修改协调者逻辑。
1.4 LangGraph 中的编排实现
LangGraph 的核心设计理念与编排模式高度契合。它通过将代理逻辑建模为有向图,天然地提供了一个强大的“协调者”。图中的每个节点可以是一个LLM调用、一个工具执行、一个数据处理函数,而边则定义了数据流和控制流。整个图的执行过程,就是由 LangGraph 运行时所编排的。
让我们通过一个具体的 LangGraph 例子来深入理解编排。假设我们要构建一个AI代理,其任务是:
- 规划 (Plan): 根据用户请求生成一个执行计划。
- 执行 (Execute): 根据计划执行相应的工具或操作。
- 审查 (Review): 审查执行结果,判断是否满足用户请求。
- 修正/完成 (Revise/Finish): 如果不满意则修正计划并重新执行,否则完成任务。
这是一个典型的迭代式、有条件的动态任务,非常适合用 LangGraph 的编排模式来实现。
代码示例:LangGraph 编排一个多步骤代理
首先,我们需要安装 LangGraph 和 LangChain:
pip install -U langgraph langchain_openai
接下来,我们定义一些工具和LLM,以及我们的图节点和边。
import operator
from typing import Annotated, Sequence, TypedDict, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, START
# 确保设置你的OpenAI API Key
import os
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
# --- 1. 定义工具 ---
@tool
def search_web(query: str) -> str:
"""在互联网上搜索信息。"""
print(f"Executing tool: search_web with query: {query}")
# 模拟网络搜索结果
if "Python 3.10 新特性" in query:
return "Python 3.10 引入了结构化模式匹配、`with` 语句中的括号上下文管理器、更精确的行号报告等新特性。"
elif "LangGraph" in query:
return "LangGraph 是 LangChain 的一个库,用于构建有状态、多步骤的代理。它基于图结构,允许定义节点和边来控制代理的执行流。"
else:
return f"搜索 '{query}' 没有找到特定结果,但可以假设有一些通用信息。"
@tool
def calculate(expression: str) -> str:
"""执行一个数学表达式的计算。"""
print(f"Executing tool: calculate with expression: {expression}")
try:
return str(eval(expression))
except Exception as e:
return f"计算失败: {e}"
tools = [search_web, calculate]
# --- 2. 定义代理状态 ---
# 代理状态是 LangGraph 中在节点之间传递和更新的数据。
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
plan: str # 代理的当前计划
iterations: int # 记录迭代次数,防止无限循环
# --- 3. 定义图节点 ---
# LLM 代理
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 绑定工具到LLM,使其能够使用工具
llm_with_tools = llm.bind_tools(tools)
def call_model(state: AgentState):
"""调用LLM生成回复或工具调用。"""
messages = state["messages"]
print(f"n--- Calling LLM with messages ---n{messages}")
response = llm_with_tools.invoke(messages)
print(f"LLM Response: {response}")
return {"messages": [response]}
def call_tool(state: AgentState):
"""执行LLM建议的工具调用。"""
last_message = state["messages"][-1]
tool_calls = last_message.tool_calls
if not tool_calls:
raise ValueError("No tool calls found in the last message.")
tool_outputs = []
for tool_call in tool_calls:
print(f"n--- Executing tool: {tool_call.name} with args: {tool_call.args} ---")
tool_name = tool_call.name
tool_args = tool_call.args
# 查找并执行工具
found_tool = next((t for t in tools if t.name == tool_name), None)
if found_tool:
output = found_tool.invoke(tool_args)
tool_outputs.append(AIMessage(content=output, name=tool_name))
else:
tool_outputs.append(AIMessage(content=f"Tool {tool_name} not found.", name=tool_name))
print(f"Tool Outputs: {tool_outputs}")
return {"messages": tool_outputs}
def plan_and_decide(state: AgentState):
"""
代理根据当前对话和历史来规划下一步,并决定是使用工具、回复用户还是结束。
这个函数是编排的核心决策点。
"""
messages = state["messages"]
iterations = state.get("iterations", 0) + 1
if iterations > 5: # 简单防止无限循环
print("n--- Max iterations reached. Forcing finish. ---")
return "end"
prompt = HumanMessage(
content=f"""
你是一名智能助手,你已经完成了 {iterations-1} 轮思考。
当前的对话历史如下:
{messages}
你的任务是根据用户的需求,使用可用工具(search_web, calculate)来获取信息或进行计算,然后提供一个最终的答案。
如果你认为已经获得了足够的信息,并且可以直接回答用户的问题,或者已经完成任务,请直接回复。
如果你需要使用工具,请以 JSON 格式调用工具。
如果你需要进一步思考,但还没有决定工具调用或最终答案,请思考。
请决定下一步:
1. "call_tool": 如果需要调用工具。
2. "finish": 如果可以给出最终答案或任务已完成。
3. "continue_plan": 如果需要进一步思考(这会再次调用LLM)。
"""
)
response = llm.invoke(messages + [prompt])
print(f"n--- Agent Decision Response ---n{response.content}")
if response.tool_calls:
print("Decision: call_tool")
return "call_tool"
elif "finish" in response.content.lower() or "最终答案" in response.content:
print("Decision: finish")
return "finish"
else:
# 如果LLM没有明确指示工具调用或完成,我们默认它需要进一步思考
print("Decision: continue_plan")
return "continue_plan"
# --- 4. 构建 LangGraph ---
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("planner", call_model) # 初始规划
workflow.add_node("agent", call_model) # 代理LLM思考和决策
workflow.add_node("tools", call_tool) # 工具执行
# 设置入口点
workflow.set_entry_point("planner")
# 定义边和条件逻辑 (这是编排的关键)
# 从 planner 节点,总是到 agent 节点进行决策
workflow.add_edge("planner", "agent")
# 从 agent 节点,根据 plan_and_decide 函数的输出进行条件路由
workflow.add_conditional_edges(
"agent",
plan_and_decide, # 这个函数决定了下一跳
{
"call_tool": "tools",
"finish": END,
"continue_plan": "agent" # 如果需要进一步思考,则再次回到 agent 节点
}
)
# 从 tools 节点,工具执行完毕后,总是回到 agent 节点进行下一步决策
workflow.add_edge("tools", "agent")
# 编译图
app = workflow.compile()
# --- 5. 运行代理 ---
print("--- Starting Agent Run ---")
initial_state = {"messages": [HumanMessage(content="帮我查一下 Python 3.10 有哪些新特性,然后计算 123 * 45 的结果。")]}
# 运行 LangGraph
final_state = app.invoke(initial_state)
print("n--- Agent Run Finished ---")
print("Final Messages:")
for msg in final_state["messages"]:
print(f"- {type(msg).__name__}: {msg.content}")
# 另一个例子
print("n--- Starting Another Agent Run ---")
initial_state_2 = {"messages": [HumanMessage(content="帮我搜索一下 LangGraph 是什么,并总结其主要功能。")]}
final_state_2 = app.invoke(initial_state_2)
print("n--- Agent Run Finished ---")
print("Final Messages:")
for msg in final_state_2["messages"]:
print(f"- {type(msg).__name__}: {msg.content}")
代码解析:
在这个例子中,StateGraph 本身就是我们的协调者。
AgentState定义了在整个流程中传递的共享状态。call_model节点负责调用 LLM。call_tool节点负责执行工具。plan_and_decide函数是编排逻辑的核心。它根据当前状态(messages)决定下一步的走向:是调用工具 ("call_tool")、给出最终答案 ("finish") 还是继续思考 ("continue_plan")。这个函数是 LangGraphadd_conditional_edges方法的关键参数,它实现了基于运行时条件的动态路由。add_edge定义了无条件转移。add_conditional_edges定义了条件转移,即流程根据plan_and_decide函数的输出来选择下一跳。
整个流程的执行路径完全由 LangGraph 的图结构和 plan_and_decide 函数集中控制和编排。这就是典型的流编排模式。代理的决策逻辑集中在 plan_and_decide 函数中,它检查当前状态并显式地指导流程走向。
二、理解 Choreography(流编舞)
2.1 定义与核心原则
流编舞是一种去中心化的流程管理模式。它没有一个中心化的协调者,而是通过让各个参与者(服务或组件)独立地响应事件来驱动整个流程。每个参与者只关心自己的职责,并在完成任务后发布一个事件,其他对该事件感兴趣的参与者会监听并作出响应,从而推动流程向前。这就像一群舞者,没有指挥,但每个人都通过观察其他舞者的动作和音乐节奏来决定自己的下一步动作,最终形成和谐的整体。
在编舞模式下,流程逻辑是隐式的,分散在各个参与者中。它们通过事件异步通信,高度解耦。
核心原则:
- 去中心化: 没有中心实体控制整个流程,流程由事件驱动。
- 事件驱动: 参与者通过发布和订阅事件进行通信。
- 松耦合: 参与者之间不知道彼此的具体实现,只知道事件的语义。
- 自治性: 每个参与者都是自治的,独立完成其职责。
2.2 何时使用编舞
编舞模式在以下场景中表现出色:
- 需要高可伸缩性和弹性: 去中心化结构使得系统更容易扩展,单个服务故障不会影响整个系统。
- 服务高度独立且自治: 当各个服务可以独立部署、升级和扩展时。
- 流程不是严格线性的,或有多种变体: 事件驱动的特性使得流程更具灵活性和适应性。
- 微服务架构: 编舞是微服务架构中常见的集成模式。
- 系统组件众多,且可能动态增减: 新的组件可以很容易地加入事件总线并开始响应事件。
2.3 优点与缺点
优点:
- 高可伸缩性与弹性: 去中心化减少了瓶颈,提高了容错性。
- 松耦合: 服务之间高度解耦,易于独立开发、部署和维护。
- 高灵活性: 流程可以更容易地适应变化,引入新服务或修改现有服务对整体影响较小。
- 更高的自治性: 每个服务可以独立决策。
缺点:
- 流程视图不清晰: 整个业务流程的端到端视图难以获取,调试和追踪问题可能更困难。
- 错误处理复杂: 缺乏中心协调者,实现全局事务和补偿逻辑更具挑战性。
- 开发复杂性高: 需要更成熟的事件管理、消息队列和分布式事务机制。
- 状态管理分散: 整个流程的状态可能分散在各个参与者中,需要额外的机制来聚合。
2.4 LangGraph 中的编舞实现
LangGraph 本身是为编排而设计的,其核心是构建一个有向图来显式地控制流程。因此,直接在 LangGraph 内部实现纯粹的“编舞”模式并不自然。然而,我们可以通过一些设计模式来模拟或适配编舞的理念,特别是在构建由多个 LangGraph 代理组成的宏观系统时。
一种常见的做法是让多个 LangGraph 代理(或其内部节点)通过一个共享的“事件总线”或“消息队列”进行通信。每个 LangGraph 代理充当一个独立的“服务”,监听特定事件,执行其内部的编排逻辑,然后发布新的事件。
代码示例:模拟 LangGraph 间的编舞
在这个例子中,我们将创建两个独立的 LangGraph 代理,它们不直接调用彼此,而是通过一个共享的简单消息队列进行通信,模拟事件驱动的编舞。
Agent A 负责接收初始请求,执行搜索,并将搜索结果发布到队列。
Agent B 监听队列中的特定事件,获取搜索结果,然后进行计算,并将计算结果发布到队列。
一个“外部协调器”(在这里只是一个简单的循环)会读取队列并打印结果。
import operator
import time
from typing import Annotated, Sequence, TypedDict, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, START
from collections import deque # 用作简单的消息队列
# 确保设置你的OpenAI API Key
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
# --- 共享消息队列 ---
# 模拟一个全局的事件总线或消息队列
class MessageBus:
def __init__(self):
self.queue = deque()
def publish(self, event_type: str, payload: dict):
event = {"type": event_type, "payload": payload}
self.queue.append(event)
print(f"[MessageBus] Published event: {event_type} with payload: {payload}")
def subscribe(self, event_type: str):
# 这是一个简化的订阅,实际中可能需要更复杂的筛选和持久化
# 这里只是从队列中取出匹配的事件
matching_events = []
temp_queue = deque()
while self.queue:
event = self.queue.popleft()
if event["type"] == event_type:
matching_events.append(event)
else:
temp_queue.append(event)
self.queue.extend(temp_queue) # 将未匹配的事件放回
return matching_events
def get_all_events(self):
return list(self.queue)
def clear(self):
self.queue.clear()
global_message_bus = MessageBus()
# --- Agent A: 搜索代理 ---
@tool
def search_web_agent_a(query: str) -> str:
"""Agent A 专用的网络搜索工具。"""
print(f"[Agent A] Executing tool: search_web_agent_a with query: {query}")
if "Python 3.10 新特性" in query:
return "Python 3.10 引入了结构化模式匹配、`with` 语句中的括号上下文管理器等。"
elif "LangGraph" in query:
return "LangGraph 是 LangChain 的一个库,用于构建有状态、多步骤的代理。"
else:
return f"Agent A 搜索 '{query}' 的结果。"
tools_agent_a = [search_web_agent_a]
llm_agent_a = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools_agent_a)
class AgentAState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
search_query: str # Agent A 自己的状态,记录搜索查询
search_result: str # Agent A 自己的状态,记录搜索结果
def call_llm_agent_a(state: AgentAState):
"""Agent A 调用LLM生成搜索请求。"""
messages = state["messages"]
print(f"n[Agent A] Calling LLM with messages: {messages}")
response = llm_agent_a.invoke(messages)
return {"messages": [response]}
def execute_search_and_publish(state: AgentAState):
"""Agent A 执行搜索工具并发布事件。"""
last_message = state["messages"][-1]
tool_calls = last_message.tool_calls
if not tool_calls:
print("[Agent A] No tool calls, assuming direct answer or error.")
return {"messages": [AIMessage(content="Agent A 无法执行搜索。")]}
tool_output = ""
for tool_call in tool_calls:
if tool_call.name == "search_web_agent_a":
query = tool_call.args["query"]
output = search_web_agent_a.invoke({"query": query})
tool_output = output
# 将搜索结果存储在 Agent A 自己的状态中
state["search_query"] = query
state["search_result"] = output
# 发布事件,通知其他代理搜索已完成
global_message_bus.publish("SEARCH_COMPLETED",
{"search_query": query, "search_result": output})
return {"messages": [AIMessage(content=f"搜索工具已执行,结果已发布。原始结果:{output}", name="search_web_agent_a")]}
return {"messages": [AIMessage(content="Agent A 未知工具调用。", name="unknown_tool")]}
# 构建 Agent A 的 LangGraph
workflow_a = StateGraph(AgentAState)
workflow_a.add_node("llm", call_llm_agent_a)
workflow_a.add_node("tool_exec", execute_search_and_publish)
workflow_a.set_entry_point("llm")
workflow_a.add_edge("llm", "tool_exec")
workflow_a.add_edge("tool_exec", END) # Agent A 完成其任务后即结束
app_a = workflow_a.compile()
# --- Agent B: 计算代理 ---
@tool
def calculate_agent_b(expression: str) -> str:
"""Agent B 专用的计算工具。"""
print(f"[Agent B] Executing tool: calculate_agent_b with expression: {expression}")
try:
return str(eval(expression))
except Exception as e:
return f"Agent B 计算失败: {e}"
tools_agent_b = [calculate_agent_b]
llm_agent_b = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools_agent_b)
class AgentBState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
received_search_result: str # Agent B 自己的状态,记录收到的搜索结果
calculation_expression: str # Agent B 自己的状态,记录计算表达式
calculation_result: str # Agent B 自己的状态,记录计算结果
def listen_for_search_result(state: AgentBState):
"""Agent B 监听搜索结果事件,并更新自己的状态。"""
events = global_message_bus.subscribe("SEARCH_COMPLETED")
if events:
last_event = events[-1] # 取最新事件
search_result = last_event["payload"]["search_result"]
print(f"n[Agent B] Received SEARCH_COMPLETED event. Search Result: {search_result}")
state["received_search_result"] = search_result
return {"messages": [HumanMessage(content=f"我收到了搜索结果:{search_result}。现在需要根据这个结果进行计算。")]}
else:
print("[Agent B] No SEARCH_COMPLETED event received yet. Waiting...")
# 实际的异步系统会在这里等待或返回一个表示未就绪的状态
return {"messages": [HumanMessage(content="等待搜索结果...")]}
def call_llm_agent_b(state: AgentBState):
"""Agent B 调用LLM生成计算请求。"""
messages = state["messages"]
print(f"n[Agent B] Calling LLM with messages: {messages}")
response = llm_agent_b.invoke(messages)
return {"messages": [response]}
def execute_calculation_and_publish(state: AgentBState):
"""Agent B 执行计算工具并发布事件。"""
last_message = state["messages"][-1]
tool_calls = last_message.tool_calls
if not tool_calls:
print("[Agent B] No tool calls, assuming direct answer or error.")
return {"messages": [AIMessage(content="Agent B 无法执行计算。")]}
tool_output = ""
for tool_call in tool_calls:
if tool_call.name == "calculate_agent_b":
expression = tool_call.args["expression"]
output = calculate_agent_b.invoke({"expression": expression})
tool_output = output
state["calculation_expression"] = expression
state["calculation_result"] = output
# 发布事件,通知计算已完成
global_message_bus.publish("CALCULATION_COMPLETED",
{"expression": expression, "result": output})
return {"messages": [AIMessage(content=f"计算工具已执行,结果已发布。原始结果:{output}", name="calculate_agent_b")]}
return {"messages": [AIMessage(content="Agent B 未知工具调用。", name="unknown_tool")]}
# 构建 Agent B 的 LangGraph
workflow_b = StateGraph(AgentBState)
workflow_b.add_node("listener", listen_for_search_result) # 监听节点
workflow_b.add_node("llm", call_llm_agent_b)
workflow_b.add_node("tool_exec", execute_calculation_and_publish)
workflow_b.set_entry_point("listener")
workflow_b.add_edge("listener", "llm")
workflow_b.add_edge("llm", "tool_exec")
workflow_b.add_edge("tool_exec", END) # Agent B 完成其任务后即结束
app_b = workflow_b.compile()
# --- 外部驱动器/模拟主程序 ---
def run_choreographed_agents(initial_prompt: str):
global_message_bus.clear() # 清空消息总线
print("n--- Choreography Simulation Start ---")
# 1. 驱动 Agent A
print("n--- Driving Agent A ---")
initial_state_a = {"messages": [HumanMessage(content=initial_prompt)]}
final_state_a = app_a.invoke(initial_state_a)
print(f"n[Agent A] Final State: {final_state_a}")
# 2. 驱动 Agent B (需要等待 Agent A 发布事件)
print("n--- Driving Agent B ---")
# 为了模拟异步,我们可能需要多次调用 Agent B 直到它收到事件
max_attempts = 5
attempts = 0
final_state_b = None
while attempts < max_attempts:
initial_state_b = {"messages": [HumanMessage(content="Agent B 开始工作,等待搜索结果。")]}
# 这里 Agent B 的 `listener` 节点会尝试从消息总线获取事件
current_state_b = app_b.invoke(initial_state_b)
if current_state_b.get("received_search_result"):
print(f"[Agent B] Successfully received search result. Proceeding with calculation.")
# 此时 Agent B 已经收到了搜索结果,可以继续其计算流程
# 我们需要重新创建一个 Agent B 的执行流程,但这次它已经有了搜索结果作为起点
# 这是一个简化的模拟,实际中 Agent B 可能会将收到的事件作为其新一轮执行的输入
# 或者像这里一样,它在 `listener` 节点就直接触发了后续的 LLM 和工具调用
final_state_b = current_state_b
break
else:
print(f"[Agent B] No search result yet. Attempt {attempts+1}/{max_attempts}. Waiting 1 second...")
time.sleep(1)
attempts += 1
if not final_state_b:
print("[Agent B] Failed to receive search result after multiple attempts.")
return
print(f"n[Agent B] Final State: {final_state_b}")
print("n--- Choreography Simulation End ---")
print("n--- Final Results from Message Bus ---")
for event in global_message_bus.get_all_events():
print(f"- {event['type']}: {event['payload']}")
# 运行模拟
run_choreographed_agents("帮我搜索一下 LangGraph 是什么,然后基于搜索结果,计算 100 + 50 的结果。")
代码解析:
MessageBus: 这是一个简单的内存队列,模拟了事件总线。publish方法将事件放入队列,subscribe方法则检查队列中是否有特定类型的事件。AgentA(搜索代理):- 它的 LangGraph 负责接收用户请求。
execute_search_and_publish节点在执行完search_web_agent_a工具后,会调用global_message_bus.publish("SEARCH_COMPLETED", ...)将搜索结果作为一个事件发布出去。AgentA完成其搜索任务后,其 LangGraph 实例就结束了。它不关心谁会使用这个搜索结果。
AgentB(计算代理):- 它的 LangGraph 的入口点是
listen_for_search_result节点。 - 这个节点会调用
global_message_bus.subscribe("SEARCH_COMPLETED")来检查是否有SEARCH_COMPLETED事件。 - 一旦收到事件,它就将搜索结果更新到自己的状态
received_search_result中,然后流程继续到call_llm_agent_b进行计算规划,最后到execute_calculation_and_publish执行计算并发布CALCULATION_COMPLETED事件。
- 它的 LangGraph 的入口点是
run_choreographed_agents: 这个函数充当了外部的驱动程序。它首先启动AgentA的流程,然后在一个循环中反复尝试启动AgentB,直到AgentB成功从消息总线中获取到SEARCH_COMPLETED事件。这模拟了异步和事件驱动的特性。
这个例子虽然在同一个进程中运行,但它演示了多个独立的 LangGraph 代理如何通过事件总线进行去中心化的协作。每个代理只关心自己要发布的事件和订阅的事件,它们之间没有直接的调用关系,符合编舞的核心原则。
三、编排与编舞的比较分析
为了更清晰地理解两种模式的异同,我们通过一个表格进行对比:
| 特性 | 流编排 (Flow Orchestration) | 流编舞 (Flow Choreography) |
|---|---|---|
| 控制中心 | 集中式:一个明确的协调者(orchestrator) | 去中心化:无中心协调者,由参与者自治驱动 |
| 通信方式 | 显式调用:协调者直接调用服务并等待响应 | 事件驱动:通过发布/订阅事件进行异步通信 |
| 耦合度 | 强耦合:协调者需要了解所有参与者的接口和行为 | 松耦合:参与者只关心事件,不了解彼此的具体实现 |
| 流程可见性 | 高:整个业务流程的端到端视图清晰,易于理解和追踪 | 低:流程逻辑分散在各个参与者中,端到端视图难以获取 |
| 复杂性 | 协调者逻辑可能复杂,但流程清晰 | 参与者逻辑相对简单,但系统整体的事件流和依赖更难追踪 |
| 可伸缩性 | 协调者可能成为瓶颈,横向扩展可能受限 | 高:参与者独立伸缩,易于应对高并发和分布式环境 |
| 容错性 | 协调者是单点故障风险,但易于实现全局错误处理和补偿 | 高:单个服务故障不影响整体,但全局错误处理更复杂 |
| 适用场景 | 复杂、有状态、严格流程、强依赖、需要集中控制的业务逻辑 | 大规模、分布式、高并发、松耦合、事件驱动的微服务架构 |
| LangGraph 契合度 | 天然契合: LangGraph 的图结构本身就是强大的编排工具 | 需要适配: LangGraph 内部是编排,但多个 LangGraph 实例可通过外部机制模拟编舞 |
3.1 LangGraph 对动态任务的适用性
LangGraph 的设计理念决定了它天生更适合编排模式来处理动态任务。
- 图结构即编排: LangGraph 的核心是定义一个状态机图,其中节点是代理的步骤(LLM调用、工具执行、自定义函数),边定义了这些步骤的执行顺序和条件跳转。这个图本身就是一个强大的、可视化的协调者。
- 有状态管理: LangGraph 的
StateGraph允许在整个图的执行过程中维护和更新一个共享状态。这使得协调者能够根据历史信息和当前上下文做出决策,非常适合处理复杂的、有记忆的动态任务。 - 条件路由:
add_conditional_edges方法是 LangGraph 实现动态任务编排的关键。它允许代理根据运行时状态(例如 LLM 的输出、工具执行结果)动态选择下一跳,从而实现复杂的条件分支、循环和决策逻辑。 - 清晰的执行路径: 对于动态任务,理解代理的决策过程和执行路径至关重要。编排模式提供了这种清晰度,使得调试、优化和解释代理行为变得更容易。
虽然可以通过外部消息队列等方式让多个 LangGraph 实例模拟编舞,但这通常意味着你正在构建一个更宏大的、分布式系统,而 LangGraph 只是其中一个执行单元。在单个 LangGraph 代理的范畴内,它更倾向于作为任务的“编排者”。
3.2 混合方法:结合两者的优势
在实际应用中,纯粹的编排或编舞模式往往不足以应对所有挑战。一种常见的、更为强大的方法是采用混合模式:
- 宏观编舞,微观编排: 整个系统可能由多个独立的微服务(每个服务可能是一个 LangGraph 代理)组成,它们通过事件总线进行编舞式通信。例如,一个“订单处理服务”发布“订单已创建”事件,一个“库存服务”监听该事件并执行库存扣减,一个“支付服务”也监听该事件并处理支付。
- LangGraph 内部的编排: 每个微服务内部,如果其逻辑复杂,可以利用 LangGraph 构建一个编排图来管理其内部的动态任务流程。例如,“库存服务”内部可能有一个 LangGraph 来编排“检查库存 -> 预留库存 -> 更新库存”等步骤。
这种混合方法能够利用编舞的松耦合和可伸缩性,同时利用编排的强大控制力和流程可见性来管理每个服务内部的复杂逻辑。
四、LangGraph 中的实践考量与最佳实践
4.1 状态管理策略
- 精简状态:
AgentState应该只包含在节点之间传递和决策所需的最少信息。过大的状态会增加序列化/反序列化开销,并可能导致内存问题。 - 不可变性 vs. 可变性: LangGraph 默认通过
operator.add等方式合并状态,这鼓励了不可变性的思想。对于复杂对象,考虑使用深拷贝或明确的更新逻辑。 - 持久化: 对于长时间运行的代理或需要恢复的场景,考虑将 LangGraph 的状态持久化到数据库或消息队列中。LangGraph 提供了
Checkpoint接口来支持这一功能。
4.2 错误处理
- 节点级错误处理: 每个节点内部应包含健壮的
try-except块,捕获工具调用或LLM交互中的特定错误。 - 条件边缘的错误分支: 可以在
add_conditional_edges中定义一个专门的“错误”分支,当某个节点执行失败时,路由到错误处理节点(例如,发送通知、记录日志、尝试重试、请求人工干预)。 - 全局错误处理: 对于未捕获的异常,确保 LangGraph 应用程序本身有适当的顶级错误处理机制,以防止整个进程崩溃。
4.3 可观测性与调试
- 日志记录: 在每个节点和关键决策点添加详细的日志输出,记录输入、输出、决策路径和时间戳。
- 跟踪: 利用 LangChain Hub 或类似的跟踪工具来可视化 LangGraph 的执行路径。这对于理解复杂图的运行时行为至关重要。
- 状态检查: 在调试时,打印或检查
AgentState的内容,以确保数据在节点之间正确传递和更新。 - 可视化工具: LangGraph 可以生成其图的DOT表示,可以转换为图片,帮助理解图的结构(虽然文章中不包含图片,但在实际开发中非常有益)。
4.4 扩展性与性能
- 异步执行: LangGraph 支持异步函数,可以利用
async/await来提高并发性,尤其是在进行多个外部I/O操作(如LLM调用、工具API调用)时。 - 并行节点: 对于没有数据依赖关系的节点,可以考虑并行执行。LangGraph 的图结构允许定义并行路径。
- 缓存: 对于重复的LLM调用或工具执行,考虑使用缓存来减少延迟和成本。
- 批量处理: 如果可能,将多个小的任务合并成一个批次处理,以减少LLM的API调用次数。
4.5 何时选择哪种模式
- 默认选择编排: 对于大多数 LangGraph 代理,特别是处理单个复杂用户请求、需要多步骤决策和状态管理的场景,编排是天然且更优的选择。LangGraph 的设计就是为了让编排变得简单而强大。
- 考虑编舞(宏观层面): 当你的系统需要集成多个独立的服务,这些服务由不同的团队开发,需要高度解耦,并且可能涉及大规模分布式部署时,才应该在系统层面考虑编舞。此时,每个 LangGraph 代理可能只是一个更大的编舞系统中的一个“舞者”。
- 混合模式的强大: 对于企业级应用,通常会采用混合模式。使用编舞来管理服务间的宏观协作,而每个服务内部(如果其复杂性允许)则使用 LangGraph 进行微观编排。
五、高级主题探讨
5.1 子图和嵌套编排
LangGraph 支持将一个 StateGraph 作为另一个 StateGraph 的一个节点。这意味着你可以构建模块化的代理,将复杂的子任务封装在一个独立的子图(子代理)中。这是一种强大的编排技术,可以实现层次化的流程管理,大大提高了复杂系统的可维护性和可重用性。
例如,一个主代理负责“用户意图识别”,一旦识别出“数据分析”意图,就可以将控制权传递给一个专门的“数据分析子代理”(它本身也是一个 LangGraph),该子代理内部编排了数据加载、清洗、模型训练和报告生成等步骤。
5.2 工具集成与动态工具选择
LangGraph 与 LangChain 的工具生态系统紧密集成。代理可以动态地根据当前任务和上下文选择并调用合适的工具。在编排模式下,LLM 节点可以输出工具调用请求,然后通过条件路由将控制权转移到工具执行节点。动态工具选择使得代理能够应对更广泛、更灵活的任务。
5.3 人机协作(Human-in-the-Loop)
在某些动态任务中,可能需要人类的干预或审批。LangGraph 可以很容易地集成人机协作。例如,在某个关键决策点,可以路由到一个“人工审批”节点,该节点将任务挂起,等待外部系统或人类用户的输入,然后根据人工输入继续流程。这种模式对于高风险决策或需要创造性输入的任务非常有用。
六、核心要点与展望
在 LangGraph 中处理动态任务时,流编排模式是其核心优势所在。LangGraph 的图结构、状态管理和条件路由机制,天然地为集中式、有状态的复杂流程管理提供了强大而直观的框架。它使得代理的决策逻辑和执行路径清晰可见、易于控制和调试。
尽管纯粹的流编舞在 LangGraph 内部不那么自然,但我们可以在构建由多个 LangGraph 代理组成的分布式系统时,通过事件总线等机制,在宏观层面实现编舞。最佳实践往往是采用编舞和编排的混合模式:利用编舞的松耦合和可伸缩性来协调高级别服务间的交互,同时在每个服务内部运用 LangGraph 的编排能力来管理其复杂的子任务。
随着 AI 代理能力的不断提升,以及 LangGraph 等框架的持续演进,我们有理由相信,无论是集中式的精细编排,还是去中心化的灵活编舞,都将在构建更智能、更健壮的 AI 驱动应用中发挥越来越重要的作用。关键在于理解它们的适用场景,并根据具体的业务需求和系统架构,做出明智的设计选择。
感谢各位!