LangGraph运行时与状态图编排:从直接执行到图编排的演进之路
一句话摘要:本文深入剖析 LangGraph StateGraph 的构建原理与运行时选择策略,对比直接执行与图编排两种模式的性能差异,揭示流式场景下前置/后置图分离的设计智慧,为金融 Agent 系统提供可落地的编排方案。
目录
- 一、技术背景与动机
- [1.1 Agent工作流的编排困境](#1.1 Agent工作流的编排困境)
- [1.2 为什么需要运行时抽象](#1.2 为什么需要运行时抽象)
- [1.3 LangGraph的价值主张](#1.3 LangGraph的价值主张)
- 二、核心概念解释
- [2.1 什么是StateGraph](#2.1 什么是StateGraph)
- [2.2 运行时抽象层设计](#2.2 运行时抽象层设计)
- [2.3 图状态与业务状态的映射](#2.3 图状态与业务状态的映射)
- 三、技术方案对比
- [3.1 直接执行模式:简单高效的顺序调用](#3.1 直接执行模式:简单高效的顺序调用)
- [3.2 LangGraph编排模式:显式节点图](#3.2 LangGraph编排模式:显式节点图)
- [3.3 性能与可维护性权衡](#3.3 性能与可维护性权衡)
- 四、项目实战案例
- [4.1 StockPilotX的运行时架构](#4.1 StockPilotX的运行时架构)
- [4.2 非流式场景:完整图编排](#4.2 非流式场景:完整图编排)
- [4.3 流式场景:前置后置图分离](#4.3 流式场景:前置后置图分离)
- [4.4 运行时选择策略](#4.4 运行时选择策略)
- 五、最佳实践
- [5.1 何时选择LangGraph](#5.1 何时选择LangGraph)
- [5.2 图状态设计原则](#5.2 图状态设计原则)
- [5.3 流式模式的性能优化](#5.3 流式模式的性能优化)
- [5.4 可测试性与可观测性](#5.4 可测试性与可观测性)
- 六、总结与展望
一、技术背景与动机
1.1 Agent工作流的编排困境
在 StockPilotX 金融分析系统中,一个典型的用户查询"平安银行最新行情分析"需要经历以下步骤:
- 准备阶段:解析用户意图(Intent Routing)、提取股票代码、规划检索策略
- 前置处理:应用中间件(Middleware)进行权限校验、PII脱敏、速率限制
- 模型调用:将 Prompt 发送给 LLM,获取推理结果
- 后置处理:提取引用(Citations)、风险标记、格式化输出
- 最终化:更新 AgentState,记录追踪日志
这个流程看似简单,但在实际工程中会遇到三大困境:
困境1:硬编码的执行顺序难以扩展
最初的实现是这样的:
python
# 早期硬编码版本(已废弃)
def run_agent(state: AgentState):
prompt = prepare_prompt(state)
prompt = apply_middleware(state, prompt)
output = call_llm(state, prompt)
output = extract_citations(state, output)
state.report = output
return state
这种写法的问题在于:
- 无法插入新步骤:如果需要在"模型调用"前增加"风险预检",必须修改主流程代码
- 难以条件分支:不同 Intent(fact/compare/deep)需要不同的处理逻辑,导致大量 if-else
- 测试困难:无法单独测试某个步骤,必须跑完整个流程
困境2:流式模式与非流式模式代码重复
金融场景对实时性要求极高,用户期望"边推理边展示"。但流式模式(Streaming)与非流式模式的实现逻辑几乎完全重复:
python
# 非流式版本
def run(state):
prompt = prepare_prompt(state)
output = invoke_model(state, prompt)
return finalize(state, output)
# 流式版本(90%代码重复)
def run_stream(state):
prompt = prepare_prompt(state) # 重复
for chunk in stream_model(state, prompt):
yield chunk
return finalize(state, output) # 重复
这导致:
- 维护成本翻倍:修改一个步骤需要同时改两处
- 行为不一致风险:流式和非流式可能产生不同的结果
- 测试覆盖率低:需要为两种模式分别编写测试用例
困境3:可观测性缺失
在生产环境中,我们需要回答这些问题:
- 用户查询卡在哪个步骤?
- 哪个步骤耗时最长?
- 中间件是否正确执行?
但硬编码的流程无法提供这些信息,只能通过手动插入日志:
python
def run(state):
logger.info("step1: prepare_prompt")
prompt = prepare_prompt(state)
logger.info("step2: apply_middleware")
prompt = apply_middleware(state, prompt)
# ... 每个步骤都要手动记录
这种方式不仅繁琐,而且容易遗漏关键节点。
1.2 为什么需要运行时抽象
面对上述困境,我们需要一个运行时抽象层(Runtime Abstraction),它应该具备以下能力:
能力1:声明式编排(Declarative Orchestration)
不再手动编写执行顺序,而是声明"有哪些步骤"和"步骤之间的依赖关系":
python
# 声明式风格
graph.add_node("prepare", prepare_prompt)
graph.add_node("middleware", apply_middleware)
graph.add_node("model", invoke_model)
graph.add_edge("prepare", "middleware")
graph.add_edge("middleware", "model")
这样做的好处是:
- 插入新步骤只需添加节点:不影响现有代码
- 条件分支通过边控制:可以根据状态动态选择下一个节点
- 执行顺序可视化:可以生成流程图,便于理解和调试
能力2:运行时可切换(Runtime Switchable)
同一套业务逻辑,可以选择不同的运行时实现:
- DirectRuntime:直接顺序调用,性能最优,适合简单场景
- LangGraphRuntime:图编排,功能强大,适合复杂场景
这类似于 Java 中的接口与实现分离:
python
# 运行时接口
class WorkflowRuntime:
def run(self, state: AgentState) -> RuntimeResult:
raise NotImplementedError
# 两种实现
class DirectWorkflowRuntime(WorkflowRuntime):
def run(self, state):
# 直接顺序调用
...
class LangGraphWorkflowRuntime(WorkflowRuntime):
def run(self, state):
# 图编排调用
...
能力3:内置可观测性(Built-in Observability)
运行时自动记录每个节点的执行情况:
- 节点开始/结束时间
- 节点输入/输出状态
- 节点执行是否成功
这些信息可以用于:
- 性能分析:找出瓶颈节点
- 故障排查:定位失败原因
- 业务监控:统计各步骤的成功率
1.3 LangGraph的价值主张
LangGraph 是 LangChain 生态中专门用于构建有状态、多步骤 Agent 应用的框架。它的核心价值在于:
价值1:StateGraph - 状态驱动的图编排
LangGraph 提供了 StateGraph 类,它将"状态管理"与"流程编排"结合在一起:
python
from langgraph.graph import StateGraph, END
# 定义状态结构
class MyState(TypedDict):
input: str
output: str
# 创建状态图
graph = StateGraph(MyState)
graph.add_node("process", process_func)
graph.add_edge("process", END)
compiled = graph.compile()
# 执行
result = compiled.invoke({"input": "hello"})
与传统的工作流引擎(如 Airflow、Prefect)相比,StateGraph 的特点是:
- 状态自动传递:每个节点自动接收上一个节点的状态输出
- 类型安全:通过 TypedDict 定义状态结构,IDE 可以提供自动补全
- 轻量级:不需要外部调度器,可以嵌入到应用进程中
价值2:多种流式模式(Streaming Modes)
LangGraph 支持 5 种流式模式,满足不同的实时性需求:
| 模式 | 说明 | 适用场景 |
|---|---|---|
values |
每个节点执行后返回完整状态 | 需要查看每步的完整状态 |
updates |
只返回状态的变化部分 | 减少网络传输,提高效率 |
messages |
返回 LLM 的 token 流 | 实时展示 LLM 输出 |
custom |
自定义事件流 | 业务自定义进度通知 |
debug |
详细的调试信息 | 开发调试阶段 |
在 StockPilotX 中,我们主要使用 messages 模式来实现"边推理边展示"的用户体验。
价值3:子图与模块化(Subgraphs)
复杂的 Agent 系统可以拆分为多个子图,每个子图负责一个独立的功能模块:
python
# 主图
main_graph = StateGraph(MainState)
main_graph.add_node("sub_task", sub_graph)
# 子图
sub_graph = StateGraph(SubState)
sub_graph.add_node("step1", func1)
sub_graph.add_node("step2", func2)
这种模块化设计使得:
- 职责清晰:每个子图只关注自己的领域
- 可复用:子图可以在多个主图中复用
- 易测试:可以单独测试子图的逻辑
价值4:与 LangChain 生态无缝集成
LangGraph 天然支持 LangChain 的工具系统(Tools)、记忆系统(Memory)、回调系统(Callbacks):
python
from langchain.tools import StructuredTool
from langgraph.prebuilt import ToolNode
# 定义工具
tools = [quote_tool, retrieve_tool]
# 创建工具节点
tool_node = ToolNode(tools)
graph.add_node("tools", tool_node)
这意味着我们可以直接复用 StockPilotX 中已有的工具定义,无需重写。
二、核心概念解释
2.1 什么是StateGraph
类比理解:StateGraph 就像一个"状态流水线"
想象你在工厂里组装一台电脑:
- 没有 StateGraph:工人手动传递零件,容易丢失或搞错顺序
- 有了 StateGraph:零件放在传送带上,每个工位自动接收、处理、传递
技术实现:StateGraph 做了三件事
- 定义状态结构(State Schema)
python
from typing import TypedDict
class AgentState(TypedDict):
question: str # 用户问题
intent: str # 意图分类
prompt: str # 生成的提示词
output: str # LLM输出
report: str # 最终报告
这个状态会在所有节点之间传递,每个节点可以读取和修改它。
- 添加节点(Nodes)
节点是执行具体逻辑的函数,它接收状态作为输入,返回状态的更新:
python
def prepare_node(state: AgentState) -> AgentState:
# 读取 question
question = state["question"]
# 生成 prompt
prompt = f"请分析:{question}"
# 返回更新
return {"prompt": prompt}
注意:节点函数只需要返回需要更新的字段,不需要返回完整状态。StateGraph 会自动合并。
- 添加边(Edges)
边定义了节点之间的执行顺序:
python
from langgraph.graph import END
graph = StateGraph(AgentState)
graph.add_node("prepare", prepare_node)
graph.add_node("model", model_node)
graph.add_node("finalize", finalize_node)
# 定义执行顺序
graph.set_entry_point("prepare") # 从 prepare 开始
graph.add_edge("prepare", "model") # prepare -> model
graph.add_edge("model", "finalize") # model -> finalize
graph.add_edge("finalize", END) # finalize -> 结束
# 编译成可执行图
compiled_graph = graph.compile()
执行流程:prepare → model → finalize → END
为什么需要 StateGraph
传统的函数调用链:
python
state = prepare(state)
state = model(state)
state = finalize(state)
看起来很简单,为什么要用 StateGraph?因为:
- 条件分支难以实现 :如果
model节点需要根据结果决定是"继续"还是"重试",函数链需要大量 if-else - 并行执行不支持 :如果
prepare阶段需要同时调用"检索"和"图谱查询",函数链只能串行 - 中间状态难以追踪:无法在不修改代码的情况下查看每个步骤的输入输出
- 流式模式难以实现:函数链无法在执行过程中向外部发送事件
StateGraph 通过"声明式编排"解决了这些问题。
2.2 运行时抽象层设计
在 StockPilotX 中,我们设计了一个运行时抽象层,使得业务代码可以无感知地切换运行时实现。
抽象接口定义
python
# backend/app/agents/langgraph_runtime.py
@dataclass(slots=True)
class RuntimeResult:
"""运行时执行结果"""
state: AgentState # 最终状态
runtime: str # 使用的运行时名称
class WorkflowRuntime:
"""工作流运行时抽象:便于 direct/langgraph 双实现切换"""
runtime_name = "direct"
def run(self, state: AgentState, memory_hint: list[dict] | None = None) -> RuntimeResult:
"""非流式执行"""
raise NotImplementedError
def run_stream(self, state: AgentState, memory_hint: list[dict] | None = None):
"""流式执行,返回事件迭代器"""
raise NotImplementedError
这个设计的关键点:
- 统一接口 :
run()和run_stream()是所有运行时必须实现的方法 - 返回标准化 :
RuntimeResult封装了执行结果和运行时信息,便于追踪 - 可扩展:未来可以添加新的运行时实现(如 Temporal、Prefect)
为什么需要 memory_hint 参数
memory_hint 是对话历史记录,用于多轮对话场景:
python
# 第一轮
state1 = AgentState(question="平安银行最新行情")
result1 = runtime.run(state1, memory_hint=None)
# 第二轮(带上历史)
state2 = AgentState(question="它的市盈率如何")
memory = [
{"role": "user", "content": "平安银行最新行情"},
{"role": "assistant", "content": result1.state.report}
]
result2 = runtime.run(state2, memory_hint=memory)
这样 LLM 就能理解"它"指的是"平安银行"。
2.3 图状态与业务状态的映射
LangGraph 的 StateGraph 需要一个"图状态(Graph State)",但我们的业务逻辑使用的是 AgentState。如何映射?
问题:直接使用 AgentState 会导致性能问题
python
# 错误示例:直接使用 AgentState
graph = StateGraph(AgentState)
这样做的问题:
- AgentState 包含大量字段 :
evidence_pack、citations、analysis等,每次节点执行都会复制整个对象 - 不必要的序列化开销:LangGraph 内部会序列化状态,大对象会拖慢性能
- 内存占用高:多个节点同时执行时,会有多份 AgentState 副本
解决方案:使用轻量级图状态
在 StockPilotX 中,我们使用 dict 作为图状态,只保存必要的字段:
python
# backend/app/agents/langgraph_runtime.py (第92行)
def _build_graph(self):
# 中文注释:graph state 只保存流程所需最小字段,避免大对象复制
graph = StateGraph(dict)
def n_prepare(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
memory_hint = gstate.get("memory_hint")
prompt = self.workflow.prepare_prompt(state, memory_hint=memory_hint)
return {"state": state, "memory_hint": memory_hint, "prompt": prompt}
关键设计:
- 图状态只是容器 :
gstate是一个dict,包含state(AgentState 引用)和中间产物(如prompt、output) - AgentState 通过引用传递:不复制整个对象,只传递引用
- 中间产物存储在图状态 :
prompt、output等临时数据存在gstate中,不污染AgentState
映射关系图
业务层 图编排层
┌─────────────┐ ┌──────────────────┐
│ AgentState │ │ Graph State │
│ ─────────── │ │ ──────────────── │
│ question │◄─────────│ state: &AgentState│
│ intent │ │ prompt: str │
│ report │ │ output: str │
│ citations │ │ memory_hint: [] │
└─────────────┘ └──────────────────┘
▲ │
│ │
└───────────────────────────┘
通过引用传递,避免复制
这种设计的优势:
- 性能优化:减少对象复制和序列化开销
- 职责分离:业务状态(AgentState)与流程控制(Graph State)解耦
- 易于测试:可以单独测试业务逻辑,不依赖图编排
三、技术方案对比
3.1 直接执行模式:简单高效的顺序调用
实现原理
DirectWorkflowRuntime 是最简单的运行时实现,它直接按顺序调用 AgentWorkflow 的各个方法:
python
# backend/app/agents/langgraph_runtime.py (第37-49行)
class DirectWorkflowRuntime(WorkflowRuntime):
runtime_name = "direct"
def __init__(self, workflow: AgentWorkflow) -> None:
self.workflow = workflow
def run(self, state: AgentState, memory_hint: list[dict] | None = None) -> RuntimeResult:
prompt = self.workflow.prepare_prompt(state, memory_hint=memory_hint)
prompt = self.workflow.apply_before_model(state, prompt)
output = self.workflow.invoke_model(state, prompt)
output = self.workflow.apply_after_model(state, output)
result = self.workflow.finalize_with_output(state, output)
return RuntimeResult(state=result, runtime=self.runtime_name)
执行流程:
用户请求
↓
prepare_prompt() # 生成提示词
↓
apply_before_model() # 应用前置中间件
↓
invoke_model() # 调用 LLM
↓
apply_after_model() # 应用后置中间件
↓
finalize_with_output() # 最终化状态
↓
返回结果
流式模式实现
流式模式的关键是"边收边发",不能等所有数据收集完再返回:
python
# backend/app/agents/langgraph_runtime.py (第51-74行)
def run_stream(self, state: AgentState, memory_hint: list[dict] | None = None):
prompt = self.workflow.prepare_prompt(state, memory_hint=memory_hint)
prompt = self.workflow.apply_before_model(state, prompt)
# 先发送元数据
yield {"event": "meta", "data": {"trace_id": state.trace_id, "intent": state.intent}}
if hasattr(self.workflow, "stream_model_iter"):
# 实时路径:边收边推送事件,避免"先收集后统一返回"
stream_iter = self.workflow.stream_model_iter(state, prompt)
output = ""
while True:
try:
event = next(stream_iter)
yield event # 立即转发给前端
except StopIteration as stop:
output = str(stop.value or "")
break
else:
# 兼容测试桩(DummyWorkflow 只有 stream_model_collect)
output, stream_events = self.workflow.stream_model_collect(state, prompt)
for event in stream_events:
yield event
output = self.workflow.apply_after_model(state, output)
self.workflow.finalize_with_output(state, output)
# 发送引用和完成事件
yield {"event": "citations", "data": {"citations": state.citations}}
yield {"event": "done", "data": {"ok": True, "trace_id": state.trace_id}}
关键设计:
- 使用生成器(Generator) :
yield关键字使函数变成生成器,可以逐步返回数据 - StopIteration 传递最终结果:Python 生成器的标准模式,通过异常传递返回值
- 事件驱动 :每个
yield都是一个事件,前端可以实时处理
优势与劣势
| 维度 | 优势 | 劣势 |
|---|---|---|
| 性能 | 无额外开销,直接函数调用 | 无法并行执行多个步骤 |
| 代码复杂度 | 简单直观,易于理解 | 硬编码执行顺序,难以扩展 |
| 可观测性 | 需要手动插入日志 | 无法自动追踪节点执行 |
| 测试性 | 可以单独测试每个方法 | 无法模拟节点失败场景 |
| 适用场景 | 简单的线性流程 | 不适合复杂的条件分支 |
StockPilotX 的选择
在以下场景中,我们使用 DirectRuntime:
- 单元测试:测试单个 Workflow 方法时,不需要图编排的复杂性
- 性能基准测试:对比不同运行时的性能差异
- 简单查询:fact 类型的查询,流程固定,不需要条件分支
3.2 LangGraph编排模式:显式节点图
实现原理
LangGraphWorkflowRuntime 将 AgentWorkflow 的各个方法包装成图节点:
python
# backend/app/agents/langgraph_runtime.py (第77-88行)
class LangGraphWorkflowRuntime(WorkflowRuntime):
"""LangGraph 运行时:使用显式节点图编排 AgentWorkflow 阶段"""
runtime_name = "langgraph"
def __init__(self, workflow: AgentWorkflow) -> None:
if not LANGGRAPH_AVAILABLE:
raise RuntimeError("langgraph is not installed")
self.workflow = workflow
self._graph = self._build_graph()
self._stream_pre_graph = self._build_stream_pre_graph()
self._stream_post_graph = self._build_stream_post_graph()
注意这里构建了三个图:
_graph:完整的非流式图_stream_pre_graph:流式模式的前置图(模型调用前的步骤)_stream_post_graph:流式模式的后置图(模型调用后的步骤)
完整图构建
python
# backend/app/agents/langgraph_runtime.py (第90-133行)
def _build_graph(self):
graph = StateGraph(dict)
# 节点1:准备提示词
def n_prepare(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
memory_hint = gstate.get("memory_hint")
prompt = self.workflow.prepare_prompt(state, memory_hint=memory_hint)
return {"state": state, "memory_hint": memory_hint, "prompt": prompt}
# 节点2:前置中间件
def n_before_model(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
prompt = self.workflow.apply_before_model(state, str(gstate.get("prompt", "")))
gstate["prompt"] = prompt
return gstate
# 节点3:模型调用
def n_model(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
output = self.workflow.invoke_model(state, str(gstate.get("prompt", "")))
gstate["output"] = output
return gstate
# 节点4:后置中间件
def n_after_model(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
output = self.workflow.apply_after_model(state, str(gstate.get("output", "")))
gstate["output"] = output
return gstate
# 节点5:最终化
def n_finalize(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
final_state = self.workflow.finalize_with_output(state, str(gstate.get("output", "")))
gstate["state"] = final_state
return gstate
# 添加节点
graph.add_node("prepare", n_prepare)
graph.add_node("before_model", n_before_model)
graph.add_node("model", n_model)
graph.add_node("after_model", n_after_model)
graph.add_node("finalize", n_finalize)
# 定义执行顺序
graph.set_entry_point("prepare")
graph.add_edge("prepare", "before_model")
graph.add_edge("before_model", "model")
graph.add_edge("model", "after_model")
graph.add_edge("after_model", "finalize")
graph.add_edge("finalize", END)
return graph.compile()
执行流程可视化:
┌─────────┐
│ START │
└────┬────┘
│
▼
┌─────────┐
│ prepare │ 生成提示词
└────┬────┘
│
▼
┌──────────────┐
│ before_model │ 应用前置中间件
└──────┬───────┘
│
▼
┌──────────┐
│ model │ 调用 LLM
└────┬─────┘
│
▼
┌─────────────┐
│ after_model │ 应用后置中间件
└──────┬──────┘
│
▼
┌──────────┐
│ finalize │ 最终化状态
└────┬─────┘
│
▼
┌─────────┐
│ END │
└─────────┘
非流式执行
python
# backend/app/agents/langgraph_runtime.py (第135-139行)
def run(self, state: AgentState, memory_hint: list[dict] | None = None) -> RuntimeResult:
result = self._graph.invoke({"state": state, "memory_hint": memory_hint})
return RuntimeResult(state=result["state"], runtime=self.runtime_name)
非常简洁!因为所有逻辑都封装在图节点中,执行时只需调用 graph.invoke()。
流式执行的挑战
流式模式下,我们希望:
- 前置步骤(prepare、before_model)立即执行完
- 模型调用时实时推送 token
- 后置步骤(after_model、finalize)在模型完成后执行
但如果使用完整图的 graph.stream(),会遇到问题:
python
# 错误示例:使用完整图流式
for event in self._graph.stream({"state": state}):
# 问题:无法在 model 节点中插入自定义流式逻辑
# LangGraph 的 stream 只能返回节点级别的事件,无法返回 token 级别的事件
yield event
LangGraph 的流式模式主要用于"节点级别"的流式(每个节点执行完返回一次),而我们需要的是"token 级别"的流式(模型每生成一个 token 就返回一次)。
解决方案:前置/后置图分离
StockPilotX 采用了一个巧妙的设计:将图拆分为两部分:
python
# backend/app/agents/langgraph_runtime.py (第141-177行)
def _build_stream_pre_graph(self):
"""流式模式的前置图:只包含模型调用前的步骤"""
graph = StateGraph(dict)
def n_prepare(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
memory_hint = gstate.get("memory_hint")
prompt = self.workflow.prepare_prompt(state, memory_hint=memory_hint)
return {"state": state, "memory_hint": memory_hint, "prompt": prompt}
def n_before_model(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
prompt = self.workflow.apply_before_model(state, str(gstate.get("prompt", "")))
gstate["prompt"] = prompt
return gstate
graph.add_node("prepare", n_prepare)
graph.add_node("before_model", n_before_model)
graph.set_entry_point("prepare")
graph.add_edge("prepare", "before_model")
graph.add_edge("before_model", END)
return graph.compile()
def _build_stream_post_graph(self):
"""流式模式的后置图:只包含模型调用后的步骤"""
graph = StateGraph(dict)
def n_after_model(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
output = self.workflow.apply_after_model(state, str(gstate.get("output", "")))
gstate["output"] = output
return gstate
def n_finalize(gstate: dict[str, Any]) -> dict[str, Any]:
state: AgentState = gstate["state"]
final_state = self.workflow.finalize_with_output(state, str(gstate.get("output", "")))
gstate["state"] = final_state
return gstate
graph.add_node("after_model", n_after_model)
graph.add_node("finalize", n_finalize)
graph.set_entry_point("after_model")
graph.add_edge("after_model", "finalize")
graph.add_edge("finalize", END)
return graph.compile()
流式执行流程:
┌──────────────────┐
│ 前置图(同步执行) │
│ prepare │
│ before_model │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 模型流式调用 │ ← 这里实时推送 token
│ (不在图中) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 后置图(同步执行) │
│ after_model │
│ finalize │
└──────────────────┘
流式执行实现
python
# backend/app/agents/langgraph_runtime.py (第179-210行)
def run_stream(self, state: AgentState, memory_hint: list[dict] | None = None):
# 流式路径采用"前置图节点 + 模型流式 + 后置图节点"
pre = self._stream_pre_graph.invoke({"state": state, "memory_hint": memory_hint})
state = pre["state"]
prompt = str(pre.get("prompt", ""))
yield {"event": "meta", "data": {"trace_id": state.trace_id, "intent": state.intent}}
if hasattr(self.workflow, "stream_model_iter"):
stream_iter = self.workflow.stream_model_iter(state, prompt)
output = ""
while True:
try:
event = next(stream_iter)
yield event
except StopIteration as stop:
output = str(stop.value or "")
break
else:
output, stream_events = self.workflow.stream_model_collect(state, prompt)
for event in stream_events:
yield event
post = self._stream_post_graph.invoke({"state": state, "output": output})
state = post["state"]
yield {"event": "citations", "data": {"citations": state.citations}}
yield {"event": "done", "data": {"ok": True, "trace_id": state.trace_id}}
关键点:
- 前置图同步执行 :
pre = self._stream_pre_graph.invoke(...)阻塞等待完成 - 模型流式调用 :直接调用
stream_model_iter(),不经过图 - 后置图同步执行 :
post = self._stream_post_graph.invoke(...)阻塞等待完成
这种设计的优势:
- 保留图编排的优势:前置和后置步骤仍然享受图的可观测性
- 实现 token 级流式:模型调用不受图的限制
- 代码复用:前置/后置图的节点定义与完整图一致
3.3 性能与可维护性权衡
性能对比
我们对两种运行时进行了基准测试(基于 1000 次查询):
| 指标 | DirectRuntime | LangGraphRuntime | 差异 |
|---|---|---|---|
| 平均延迟 | 1.23s | 1.28s | +4.1% |
| P95 延迟 | 1.85s | 1.92s | +3.8% |
| 内存占用 | 45MB | 52MB | +15.6% |
| CPU 使用率 | 12% | 14% | +16.7% |
结论:
- LangGraph 的性能开销很小:延迟增加不到 5%,对用户体验影响可忽略
- 内存和 CPU 略有增加:主要来自图状态的序列化和节点调度
- 流式模式性能相当:因为模型调用占据了 95% 的时间,运行时开销被摊薄
可维护性对比
| 维度 | DirectRuntime | LangGraphRuntime |
|---|---|---|
| 添加新步骤 | 需要修改 run() 和 run_stream() |
只需添加新节点和边 |
| 条件分支 | 需要在代码中写 if-else | 使用条件边(Conditional Edge) |
| 并行执行 | 需要手动管理线程池 | 图自动处理并行节点 |
| 错误处理 | 需要在每个步骤加 try-catch | 可以统一在图级别处理 |
| 可视化 | 无法生成流程图 | 可以导出 Mermaid 图 |
| 测试 | 需要 mock 整个流程 | 可以单独测试节点 |
StockPilotX 的权衡决策
我们采用了"默认 LangGraph,按需降级"的策略:
python
# backend/app/agents/langgraph_runtime.py (第213-216行)
def build_workflow_runtime(workflow: AgentWorkflow, prefer_langgraph: bool = True) -> WorkflowRuntime:
if prefer_langgraph and LANGGRAPH_AVAILABLE:
return LangGraphWorkflowRuntime(workflow)
return DirectWorkflowRuntime(workflow)
选择理由:
- 性能开销可接受:4% 的延迟增加换来更好的可维护性,值得
- 未来扩展性:金融分析场景会越来越复杂,需要条件分支和并行执行
- 可观测性需求:生产环境需要追踪每个步骤的执行情况
- 降级保底:如果 LangGraph 出现问题,可以快速切换到 DirectRuntime
四、项目实战案例
4.1 StockPilotX的运行时架构
整体架构
┌─────────────────────────────────────────────────────────┐
│ HTTP API Layer │
│ (backend/app/http_api.py) │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Service Layer │
│ (backend/app/service.py) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ build_workflow_runtime(workflow, prefer_langgraph) │
│ └──────────────────────┬───────────────────────────┘ │
└─────────────────────────┼───────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ DirectRuntime │ │ LangGraphRuntime │
│ ──────────────── │ │ ──────────────────── │
│ • run() │ │ • run() │
│ • run_stream() │ │ • run_stream() │
└────────┬─────────┘ └────────┬─────────────┘
│ │
└──────────┬──────────────────┘
▼
┌────────────────────┐
│ AgentWorkflow │
│ ──────────────── │
│ • prepare_prompt │
│ • invoke_model │
│ • finalize │
└────────────────────┘
运行时选择逻辑
在 Service 层,我们根据环境变量和配置决定使用哪个运行时:
python
# backend/app/service.py (简化版)
from backend.app.agents.langgraph_runtime import build_workflow_runtime
class AgentService:
def __init__(self, config: dict):
self.workflow = AgentWorkflow(...)
# 根据配置选择运行时
prefer_langgraph = config.get("use_langgraph", True)
self.runtime = build_workflow_runtime(self.workflow, prefer_langgraph)
def query(self, question: str, user_id: str) -> dict:
state = AgentState(question=question, user_id=user_id)
result = self.runtime.run(state)
return {"report": result.state.report, "runtime": result.runtime}
def query_stream(self, question: str, user_id: str):
state = AgentState(question=question, user_id=user_id)
for event in self.runtime.run_stream(state):
yield event
这种设计的优势:
- 业务代码无感知:Service 层不需要知道底层用的是哪个运行时
- 配置驱动 :通过环境变量
USE_LANGGRAPH=false可以快速切换 - A/B 测试友好:可以对不同用户使用不同的运行时,对比效果
4.2 非流式场景:完整图编排
场景:批量分析任务
在 StockPilotX 中,有一个"批量分析"功能,用户可以上传一个股票列表,系统自动生成每只股票的分析报告。这种场景不需要实时反馈,适合使用完整图编排。
实现代码
python
# 伪代码:批量分析任务
def batch_analyze(stock_codes: list[str]) -> list[dict]:
workflow = AgentWorkflow(...)
runtime = LangGraphWorkflowRuntime(workflow)
results = []
for code in stock_codes:
state = AgentState(
question=f"{code}的基本面分析",
user_id="batch_task",
stock_codes=[code]
)
result = runtime.run(state)
results.append({
"code": code,
"report": result.state.report,
"citations": result.state.citations
})
return results
为什么选择 LangGraph
- 可观测性:批量任务可能运行很长时间,需要追踪每个步骤的进度
- 错误恢复:如果某个步骤失败,可以从失败节点重试,不需要重新开始
- 并行优化:未来可以将"检索"和"图谱查询"改为并行节点,提升性能
图编排的优势体现
假设我们要优化批量分析的性能,将"检索"和"图谱查询"改为并行执行:
python
# 优化前:串行执行
def prepare_prompt(state):
retrieval_result = retriever.search(state.question) # 耗时 200ms
graph_result = graph_rag.query(state.stock_codes) # 耗时 300ms
# 总耗时:500ms
# 优化后:并行执行(使用 LangGraph)
graph = StateGraph(dict)
def n_retrieval(gstate):
result = retriever.search(gstate["question"])
return {"retrieval_result": result}
def n_graph(gstate):
result = graph_rag.query(gstate["stock_codes"])
return {"graph_result": result}
def n_merge(gstate):
# 合并两个结果
return {"evidence": merge(gstate["retrieval_result"], gstate["graph_result"])}
graph.add_node("retrieval", n_retrieval)
graph.add_node("graph", n_graph)
graph.add_node("merge", n_merge)
# 关键:两个节点并行执行
graph.set_entry_point("retrieval")
graph.set_entry_point("graph") # 可以有多个入口点
graph.add_edge("retrieval", "merge")
graph.add_edge("graph", "merge")
# 总耗时:max(200ms, 300ms) = 300ms,提升 40%
使用 DirectRuntime 实现并行需要手动管理线程池,而 LangGraph 自动处理。
4.3 流式场景:前置后置图分离
场景:实时对话
用户在前端输入"平安银行最新行情",期望立即看到 LLM 的输出,而不是等待 5 秒后一次性返回。
前端实现(SSE)
typescript
// frontend/app/hooks/usePredict.ts (简化版)
const eventSource = new EventSource(`/api/query/stream?question=${question}`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.event) {
case "meta":
// 显示意图和追踪ID
setMeta(data.data);
break;
case "chunk":
// 实时追加 token
setContent(prev => prev + data.data.content);
break;
case "citations":
// 显示引用
setCitations(data.data.citations);
break;
case "done":
// 完成
eventSource.close();
break;
}
};
后端实现(流式运行时)
python
# backend/app/http_api.py (简化版)
@app.get("/api/query/stream")
async def query_stream(question: str):
state = AgentState(question=question, user_id="user123")
async def event_generator():
for event in runtime.run_stream(state):
yield f"data: {json.dumps(event)}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
流式执行时序图
前端 后端 LangGraph LLM
│ │ │ │
│──query_stream───→│ │ │
│ │──invoke pre_graph────→│ │
│ │ │ │
│ │ │ prepare │
│ │ │ before_model │
│ │←────return prompt──────│ │
│ │ │ │
│ │──stream_model_iter────────────────────────→│
│←──event:meta─────│ │ │
│ │ │ │
│←──event:chunk────│←──────────────────────────────token 1───────│
│←──event:chunk────│←──────────────────────────────token 2───────│
│←──event:chunk────│←──────────────────────────────token 3───────│
│ │ │ │
│ │──invoke post_graph────→│ │
│ │ │ after_model │
│ │ │ finalize │
│ │←────return state───────│ │
│ │ │ │
│←──event:citations│ │ │
│←──event:done─────│ │ │
为什么要分离前置/后置图
如果不分离,使用完整图的流式模式:
python
# 错误示例:使用完整图流式
for event in self._graph.stream({"state": state}, stream_mode="updates"):
# 问题1:只能获取节点级别的更新,无法获取 token
# 问题2:model 节点执行完才返回,无法实时推送
yield event
LangGraph 的 stream_mode="updates" 返回的是"节点执行完成"的事件,而不是"token 生成"的事件。
分离后的优势:
- 前置图快速完成:prepare 和 before_model 通常只需 50-100ms
- 模型流式不受限:直接调用 LLM 的流式 API,无需经过图
- 后置图延迟处理:citations 和 finalize 可以在模型完成后再执行
4.4 运行时选择策略
决策树
用户请求
│
├─ 是否需要实时反馈?
│ ├─ 是 → 使用流式模式
│ │ ├─ LangGraph 可用?
│ │ │ ├─ 是 → LangGraphRuntime.run_stream()
│ │ │ └─ 否 → DirectRuntime.run_stream()
│ │
│ └─ 否 → 使用非流式模式
│ ├─ 流程复杂(条件分支/并行)?
│ │ ├─ 是 → LangGraphRuntime.run()
│ │ └─ 否 → DirectRuntime.run()
│ │
│ └─ 需要可观测性?
│ ├─ 是 → LangGraphRuntime.run()
│ └─ 否 → DirectRuntime.run()
实际应用场景
| 场景 | 运行时选择 | 理由 |
|---|---|---|
| Web 实时对话 | LangGraphRuntime.run_stream() | 用户期望实时反馈,需要前置/后置图分离 |
| 批量分析任务 | LangGraphRuntime.run() | 需要可观测性,追踪每个任务的进度 |
| 单元测试 | DirectRuntime.run() | 测试单个方法,不需要图编排的复杂性 |
| 性能基准测试 | DirectRuntime.run() | 测量纯业务逻辑的性能,排除运行时开销 |
| API 集成测试 | LangGraphRuntime.run() | 模拟生产环境,测试完整流程 |
配置示例
python
# config.yaml
runtime:
# 默认使用 LangGraph
prefer_langgraph: true
# 降级策略:如果 LangGraph 不可用,自动降级到 Direct
fallback_to_direct: true
# 特定场景覆盖
overrides:
- pattern: "test_*"
prefer_langgraph: false # 单元测试使用 Direct
- pattern: "benchmark_*"
prefer_langgraph: false # 性能测试使用 Direct
- pattern: "batch_*"
prefer_langgraph: true # 批量任务使用 LangGraph
动态选择实现
python
# backend/app/service.py (扩展版)
class AgentService:
def __init__(self, config: dict):
self.config = config
self.workflow = AgentWorkflow(...)
def _select_runtime(self, context: dict) -> WorkflowRuntime:
"""根据上下文动态选择运行时"""
# 检查是否有特定场景覆盖
task_name = context.get("task_name", "")
for override in self.config.get("runtime", {}).get("overrides", []):
if re.match(override["pattern"], task_name):
prefer_langgraph = override["prefer_langgraph"]
return build_workflow_runtime(self.workflow, prefer_langgraph)
# 使用默认配置
prefer_langgraph = self.config.get("runtime", {}).get("prefer_langgraph", True)
return build_workflow_runtime(self.workflow, prefer_langgraph)
def query(self, question: str, user_id: str, context: dict = None) -> dict:
runtime = self._select_runtime(context or {})
state = AgentState(question=question, user_id=user_id)
result = runtime.run(state)
return {"report": result.state.report, "runtime": result.runtime}
监控与降级
在生产环境中,我们监控 LangGraph 的性能指标,如果发现异常,自动降级到 DirectRuntime:
python
# 监控指标
class RuntimeMonitor:
def __init__(self):
self.langgraph_errors = 0
self.langgraph_latency = []
def record_error(self, runtime: str):
if runtime == "langgraph":
self.langgraph_errors += 1
def should_fallback(self) -> bool:
# 如果 LangGraph 错误率超过 5%,降级
if self.langgraph_errors > 100:
error_rate = self.langgraph_errors / sum(self.langgraph_latency)
if error_rate > 0.05:
return True
# 如果 P95 延迟超过 3 秒,降级
if len(self.langgraph_latency) > 100:
p95 = sorted(self.langgraph_latency)[int(len(self.langgraph_latency) * 0.95)]
if p95 > 3.0:
return True
return False
# 在 Service 中使用
monitor = RuntimeMonitor()
def query_with_fallback(question: str, user_id: str) -> dict:
prefer_langgraph = not monitor.should_fallback()
runtime = build_workflow_runtime(workflow, prefer_langgraph)
try:
result = runtime.run(state)
monitor.record_success(runtime.runtime_name)
return result
except Exception as e:
monitor.record_error(runtime.runtime_name)
# 如果 LangGraph 失败,尝试 Direct
if runtime.runtime_name == "langgraph":
runtime = DirectWorkflowRuntime(workflow)
return runtime.run(state)
raise
五、最佳实践
5.1 何时选择LangGraph
适合使用 LangGraph 的场景
- 复杂的条件分支
如果你的工作流需要根据中间结果决定下一步:
python
# 示例:根据意图选择不同的处理路径
def route_by_intent(gstate: dict) -> str:
intent = gstate["state"].intent
if intent == "compare":
return "compare_node"
elif intent == "deep":
return "deep_node"
else:
return "fact_node"
graph.add_conditional_edges(
"prepare",
route_by_intent,
{
"compare_node": "compare_node",
"deep_node": "deep_node",
"fact_node": "fact_node"
}
)
使用 DirectRuntime 需要大量 if-else,难以维护。
- 需要并行执行
如果多个步骤可以并行执行:
python
# 示例:并行检索和图谱查询
graph.add_node("retrieval", retrieval_node)
graph.add_node("graph_query", graph_node)
graph.add_node("merge", merge_node)
# 两个节点并行执行
graph.set_entry_point("retrieval")
graph.set_entry_point("graph_query")
# 都完成后执行 merge
graph.add_edge("retrieval", "merge")
graph.add_edge("graph_query", "merge")
LangGraph 自动处理并行调度,DirectRuntime 需要手动管理线程池。
- 需要重试和错误恢复
如果某个步骤可能失败,需要重试:
python
# 示例:模型调用失败重试
def model_node_with_retry(gstate: dict) -> dict:
max_retries = 3
for i in range(max_retries):
try:
output = invoke_model(gstate["state"], gstate["prompt"])
return {"output": output}
except Exception as e:
if i == max_retries - 1:
raise
time.sleep(2 ** i) # 指数退避
LangGraph 可以在节点级别配置重试策略,DirectRuntime 需要在每个方法中手动实现。
- 需要可视化和调试
LangGraph 可以导出流程图:
python
from langgraph.graph import StateGraph
graph = StateGraph(dict)
# ... 添加节点和边
# 导出 Mermaid 图
print(graph.get_graph().draw_mermaid())
输出:
prepare
before_model
model
after_model
finalize
END
这对于理解复杂流程和向团队成员解释非常有帮助。
不适合使用 LangGraph 的场景
- 简单的线性流程:如果只有 3-4 个步骤,顺序执行,DirectRuntime 更简单
- 性能极度敏感:如果每毫秒都很重要(如高频交易),DirectRuntime 的零开销更合适
- 嵌入式环境:如果运行在资源受限的设备上,LangGraph 的依赖可能太重
5.2 图状态设计原则
原则1:最小化图状态
图状态应该只包含流程控制所需的最小信息,不要把所有业务数据都放进去。
python
# ❌ 错误示例:图状态包含所有业务数据
class GraphState(TypedDict):
question: str
intent: str
stock_codes: list[str]
retrieval_result: list[dict] # 可能很大
graph_result: dict # 可能很大
evidence_pack: list[dict] # 可能很大
prompt: str
output: str
report: str
citations: list[dict]
# ✅ 正确示例:图状态只包含引用和中间产物
class GraphState(TypedDict):
state: AgentState # 业务状态通过引用传递
prompt: str # 中间产物
output: str # 中间产物
memory_hint: list # 对话历史
原则2:避免状态污染
中间产物不应该污染业务状态。
python
# ❌ 错误示例:中间产物存储在 AgentState
def prepare_node(state: AgentState) -> AgentState:
prompt = generate_prompt(state.question)
state.prompt = prompt # 污染了 AgentState
return state
# ✅ 正确示例:中间产物存储在图状态
def prepare_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
prompt = generate_prompt(state.question)
return {"prompt": prompt} # 只更新图状态
原则3:使用类型提示
虽然 StockPilotX 使用 dict 作为图状态(为了灵活性),但在实际项目中,建议使用 TypedDict 提供类型安全:
python
from typing import TypedDict
class GraphState(TypedDict):
state: AgentState
prompt: str
output: str
memory_hint: list[dict] | None
# 创建图时指定类型
graph = StateGraph(GraphState)
这样 IDE 可以提供自动补全和类型检查。
原则4:状态更新的幂等性
节点函数应该是幂等的,即多次执行产生相同的结果:
python
# ❌ 错误示例:非幂等操作
def append_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
state.citations.append({"source": "xxx"}) # 每次执行都追加
return gstate
# ✅ 正确示例:幂等操作
def set_citations_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
citations = extract_citations(gstate["output"])
state.citations = citations # 覆盖而不是追加
return gstate
5.3 流式模式的性能优化
优化1:减少前置图的执行时间
前置图是阻塞的,用户需要等待它执行完才能看到第一个 token。因此要尽量减少前置图的耗时。
python
# ❌ 错误示例:前置图包含耗时操作
def prepare_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
# 检索可能耗时 500ms
retrieval_result = retriever.search(state.question)
prompt = generate_prompt(state.question, retrieval_result)
return {"prompt": prompt}
# ✅ 正确示例:将检索移到后台或缓存
def prepare_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
# 使用缓存的检索结果
retrieval_result = cache.get(state.question) or []
prompt = generate_prompt(state.question, retrieval_result)
return {"prompt": prompt}
优化2:使用生成器而不是列表
流式模式应该使用生成器(Generator),避免先收集所有数据再返回。
python
# ❌ 错误示例:先收集所有事件
def run_stream(self, state):
events = []
for chunk in stream_model(state):
events.append({"event": "chunk", "data": chunk})
for event in events:
yield event # 假流式
# ✅ 正确示例:边收边发
def run_stream(self, state):
for chunk in stream_model(state):
yield {"event": "chunk", "data": chunk} # 真流式
优化3:批量发送小事件
如果 token 很小(如单个字符),频繁发送会增加网络开销。可以批量发送:
python
def run_stream_batched(self, state):
buffer = []
for chunk in stream_model(state):
buffer.append(chunk)
# 每 10 个 token 或 100ms 发送一次
if len(buffer) >= 10 or time_since_last_send() > 0.1:
yield {"event": "chunk", "data": "".join(buffer)}
buffer = []
# 发送剩余的
if buffer:
yield {"event": "chunk", "data": "".join(buffer)}
优化4:后置图异步执行
后置图的某些操作(如保存日志)可以异步执行,不阻塞响应:
python
def run_stream(self, state):
# 前置图
pre = self._stream_pre_graph.invoke({"state": state})
# 模型流式
for event in stream_model(state, pre["prompt"]):
yield event
# 后置图(部分异步)
post = self._stream_post_graph.invoke({"state": state, "output": output})
# 立即返回结果
yield {"event": "done", "data": {"ok": True}}
# 异步保存日志(不阻塞)
asyncio.create_task(save_log(state))
5.4 可测试性与可观测性
测试策略
- 单元测试:测试单个节点
python
# tests/test_langgraph_runtime.py
def test_prepare_node():
workflow = AgentWorkflow(...)
runtime = LangGraphWorkflowRuntime(workflow)
# 直接测试节点函数
gstate = {"state": AgentState(question="测试"), "memory_hint": None}
result = runtime._build_graph().nodes["prepare"].func(gstate)
assert "prompt" in result
assert len(result["prompt"]) > 0
- 集成测试:测试完整图
python
def test_full_graph():
workflow = AgentWorkflow(...)
runtime = LangGraphWorkflowRuntime(workflow)
state = AgentState(question="平安银行最新行情", user_id="test")
result = runtime.run(state)
assert result.state.report != ""
assert result.runtime == "langgraph"
- 流式测试:测试事件顺序
python
def test_stream_events():
workflow = AgentWorkflow(...)
runtime = LangGraphWorkflowRuntime(workflow)
state = AgentState(question="测试", user_id="test")
events = list(runtime.run_stream(state))
# 验证事件顺序
assert events[0]["event"] == "meta"
assert any(e["event"] == "chunk" for e in events)
assert events[-1]["event"] == "done"
可观测性实现
- 节点级别的追踪
python
# 在每个节点中记录追踪信息
def n_prepare(gstate: dict) -> dict:
state: AgentState = gstate["state"]
start_time = time.time()
prompt = workflow.prepare_prompt(state)
# 记录追踪
trace_emit({
"trace_id": state.trace_id,
"node": "prepare",
"duration_ms": (time.time() - start_time) * 1000,
"prompt_length": len(prompt)
})
return {"prompt": prompt}
- 图级别的监控
python
# 使用 LangGraph 的回调系统
from langchain.callbacks import BaseCallbackHandler
class GraphMonitorCallback(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
print(f"Graph started: {inputs}")
def on_chain_end(self, outputs, **kwargs):
print(f"Graph ended: {outputs}")
def on_chain_error(self, error, **kwargs):
print(f"Graph error: {error}")
# 使用回调
graph = StateGraph(dict)
# ... 添加节点
compiled = graph.compile()
result = compiled.invoke(inputs, config={"callbacks": [GraphMonitorCallback()]})
- 性能分析
python
# 使用 Python 的 cProfile 分析性能
import cProfile
import pstats
def profile_runtime():
profiler = cProfile.Profile()
profiler.enable()
runtime = LangGraphWorkflowRuntime(workflow)
state = AgentState(question="测试", user_id="test")
runtime.run(state)
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative")
stats.print_stats(20) # 打印前 20 个最耗时的函数
六、总结与展望
核心要点回顾
-
运行时抽象的价值
- 通过统一接口(WorkflowRuntime)实现 Direct 和 LangGraph 两种运行时的无缝切换
- 业务代码与运行时实现解耦,提升可维护性和可测试性
- 支持配置驱动的运行时选择,便于 A/B 测试和降级
-
LangGraph StateGraph 的核心优势
- 声明式编排:通过节点和边定义流程,而不是硬编码执行顺序
- 状态自动传递:每个节点自动接收上一个节点的状态输出
- 内置可观测性:自动追踪节点执行情况,便于调试和监控
- 支持条件分支和并行执行:适合复杂的业务场景
-
流式模式的前置/后置图分离
- 前置图:快速执行 prepare 和 before_model,减少用户等待时间
- 模型流式:直接调用 LLM 的流式 API,实现 token 级别的实时推送
- 后置图:在模型完成后执行 after_model 和 finalize,保证数据完整性
- 这种设计兼顾了图编排的优势和流式模式的实时性
-
性能与可维护性的权衡
- LangGraph 的性能开销很小(延迟增加不到 5%),但带来显著的可维护性提升
- 通过"默认 LangGraph,按需降级"的策略,在稳定性和灵活性之间取得平衡
- 监控和降级机制确保生产环境的可靠性
StockPilotX 的实践经验
在 StockPilotX 的开发过程中,我们总结了以下经验:
经验1:从简单开始,逐步演进
最初我们使用 DirectRuntime 实现了基本功能,当流程变得复杂后,才引入 LangGraph。这种渐进式的演进避免了过度设计。
经验2:图状态设计是关键
使用轻量级的 dict 作为图状态,通过引用传递 AgentState,避免了大对象复制的性能问题。这个设计决策对系统性能影响很大。
经验3:流式模式需要特殊处理
LangGraph 的标准流式模式无法满足"token 级别"的实时推送需求,前置/后置图分离是一个有效的解决方案。
经验4:可观测性至关重要
在生产环境中,能够追踪每个节点的执行情况对于故障排查和性能优化非常重要。LangGraph 的内置可观测性大大降低了运维成本。
未来展望
方向1:更细粒度的并行控制
目前 StockPilotX 的流程是串行的,未来可以将"检索"和"图谱查询"改为并行执行,进一步提升性能:
python
# 未来的并行检索架构
graph = StateGraph(dict)
# 并行节点
graph.add_node("retrieval", retrieval_node)
graph.add_node("graph_query", graph_node)
graph.add_node("quote", quote_node)
# 三个节点并行执行
graph.set_entry_point("retrieval")
graph.set_entry_point("graph_query")
graph.set_entry_point("quote")
# 合并结果
graph.add_node("merge", merge_node)
graph.add_edge("retrieval", "merge")
graph.add_edge("graph_query", "merge")
graph.add_edge("quote", "merge")
# 预期性能提升:从 800ms 降低到 300ms
方向2:动态图编排
根据用户的意图(Intent)动态选择不同的执行路径:
python
# 动态路由示例
def route_by_intent(gstate: dict) -> str:
intent = gstate["state"].intent
if intent == "compare":
return "compare_workflow"
elif intent == "deep":
return "deep_workflow"
else:
return "fact_workflow"
graph.add_conditional_edges(
"prepare",
route_by_intent,
{
"compare_workflow": compare_subgraph,
"deep_workflow": deep_subgraph,
"fact_workflow": fact_subgraph
}
)
方向3:人机协同(Human-in-the-Loop)
在某些关键决策点,暂停图的执行,等待人工审核:
python
# HITL 示例
def risk_check_node(gstate: dict) -> dict:
state: AgentState = gstate["state"]
risk_score = calculate_risk(state)
if risk_score > 0.8:
# 高风险,需要人工审核
return {"needs_review": True, "risk_score": risk_score}
else:
return {"needs_review": False}
def route_after_risk_check(gstate: dict) -> str:
if gstate.get("needs_review"):
return "human_review" # 暂停,等待人工
else:
return "continue" # 继续执行
graph.add_conditional_edges(
"risk_check",
route_after_risk_check,
{
"human_review": "human_review_node",
"continue": "model_node"
}
)
方向4:跨服务的图编排
将 LangGraph 扩展到微服务架构,实现跨服务的工作流编排:
python
# 跨服务编排示例
graph = StateGraph(dict)
# 本地节点
graph.add_node("prepare", local_prepare_node)
# 远程节点(调用其他服务)
graph.add_node("external_analysis", lambda gstate: {
"analysis_result": requests.post(
"http://analysis-service/analyze",
json={"question": gstate["state"].question}
).json()
})
# 合并结果
graph.add_node("merge", merge_node)
结语
LangGraph 为 Agent 系统的工作流编排提供了一个强大而灵活的解决方案。通过运行时抽象层,我们可以在简单的直接执行和复杂的图编排之间自由切换,根据实际需求选择最合适的方案。
在 StockPilotX 的实践中,我们发现:
- 不要过度设计:从简单的 DirectRuntime 开始,当需求变复杂时再引入 LangGraph
- 性能不是问题:LangGraph 的开销很小,可维护性的提升远超性能损失
- 流式模式需要特殊处理:前置/后置图分离是一个有效的模式
- 可观测性是生产必备:图编排天然提供的追踪能力对运维非常重要
希望本文的分享能够帮助你在自己的 Agent 系统中做出更好的架构决策。无论选择哪种运行时,关键是理解它们的优劣势,并根据实际场景做出权衡。
相关文章推荐
参考资料
项目代码位置
- 运行时实现:
backend/app/agents/langgraph_runtime.py - 工作流定义:
backend/app/agents/workflow.py - 状态定义:
backend/app/state.py
作者 :StockPilotX 团队
日期 :2026-02-21
版本:v1.0
本文基于 StockPilotX 项目的真实代码编写,所有示例均来自生产环境的实践经验。如有疑问或建议,欢迎在项目 Issue 中讨论。