【AI应用开发实战】 03_LangGraph运行时与状态图编排:从直接执行到图编排的演进之路

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 金融分析系统中,一个典型的用户查询"平安银行最新行情分析"需要经历以下步骤:

  1. 准备阶段:解析用户意图(Intent Routing)、提取股票代码、规划检索策略
  2. 前置处理:应用中间件(Middleware)进行权限校验、PII脱敏、速率限制
  3. 模型调用:将 Prompt 发送给 LLM,获取推理结果
  4. 后置处理:提取引用(Citations)、风险标记、格式化输出
  5. 最终化:更新 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 做了三件事

  1. 定义状态结构(State Schema)
python 复制代码
from typing import TypedDict

class AgentState(TypedDict):
    question: str      # 用户问题
    intent: str        # 意图分类
    prompt: str        # 生成的提示词
    output: str        # LLM输出
    report: str        # 最终报告

这个状态会在所有节点之间传递,每个节点可以读取和修改它。

  1. 添加节点(Nodes)

节点是执行具体逻辑的函数,它接收状态作为输入,返回状态的更新:

python 复制代码
def prepare_node(state: AgentState) -> AgentState:
    # 读取 question
    question = state["question"]
    # 生成 prompt
    prompt = f"请分析:{question}"
    # 返回更新
    return {"prompt": prompt}

注意:节点函数只需要返回需要更新的字段,不需要返回完整状态。StateGraph 会自动合并。

  1. 添加边(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()

执行流程:preparemodelfinalizeEND

为什么需要 StateGraph

传统的函数调用链:

python 复制代码
state = prepare(state)
state = model(state)
state = finalize(state)

看起来很简单,为什么要用 StateGraph?因为:

  1. 条件分支难以实现 :如果 model 节点需要根据结果决定是"继续"还是"重试",函数链需要大量 if-else
  2. 并行执行不支持 :如果 prepare 阶段需要同时调用"检索"和"图谱查询",函数链只能串行
  3. 中间状态难以追踪:无法在不修改代码的情况下查看每个步骤的输入输出
  4. 流式模式难以实现:函数链无法在执行过程中向外部发送事件

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

这个设计的关键点:

  1. 统一接口run()run_stream() 是所有运行时必须实现的方法
  2. 返回标准化RuntimeResult 封装了执行结果和运行时信息,便于追踪
  3. 可扩展:未来可以添加新的运行时实现(如 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_packcitationsanalysis 等,每次节点执行都会复制整个对象
  • 不必要的序列化开销: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}

关键设计:

  1. 图状态只是容器gstate 是一个 dict,包含 state(AgentState 引用)和中间产物(如 promptoutput
  2. AgentState 通过引用传递:不复制整个对象,只传递引用
  3. 中间产物存储在图状态promptoutput 等临时数据存在 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}}

关键设计:

  1. 使用生成器(Generator)yield 关键字使函数变成生成器,可以逐步返回数据
  2. StopIteration 传递最终结果:Python 生成器的标准模式,通过异常传递返回值
  3. 事件驱动 :每个 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()

注意这里构建了三个图

  1. _graph:完整的非流式图
  2. _stream_pre_graph:流式模式的前置图(模型调用前的步骤)
  3. _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()

流式执行的挑战

流式模式下,我们希望:

  1. 前置步骤(prepare、before_model)立即执行完
  2. 模型调用时实时推送 token
  3. 后置步骤(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}}

关键点:

  1. 前置图同步执行pre = self._stream_pre_graph.invoke(...) 阻塞等待完成
  2. 模型流式调用 :直接调用 stream_model_iter(),不经过图
  3. 后置图同步执行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)

选择理由:

  1. 性能开销可接受:4% 的延迟增加换来更好的可维护性,值得
  2. 未来扩展性:金融分析场景会越来越复杂,需要条件分支和并行执行
  3. 可观测性需求:生产环境需要追踪每个步骤的执行情况
  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

  1. 可观测性:批量任务可能运行很长时间,需要追踪每个步骤的进度
  2. 错误恢复:如果某个步骤失败,可以从失败节点重试,不需要重新开始
  3. 并行优化:未来可以将"检索"和"图谱查询"改为并行节点,提升性能

图编排的优势体现

假设我们要优化批量分析的性能,将"检索"和"图谱查询"改为并行执行:

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 的场景

  1. 复杂的条件分支

如果你的工作流需要根据中间结果决定下一步:

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,难以维护。

  1. 需要并行执行

如果多个步骤可以并行执行:

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 需要手动管理线程池。

  1. 需要重试和错误恢复

如果某个步骤可能失败,需要重试:

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 需要在每个方法中手动实现。

  1. 需要可视化和调试

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 的场景

  1. 简单的线性流程:如果只有 3-4 个步骤,顺序执行,DirectRuntime 更简单
  2. 性能极度敏感:如果每毫秒都很重要(如高频交易),DirectRuntime 的零开销更合适
  3. 嵌入式环境:如果运行在资源受限的设备上,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 可测试性与可观测性

测试策略

  1. 单元测试:测试单个节点
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
  1. 集成测试:测试完整图
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"
  1. 流式测试:测试事件顺序
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"

可观测性实现

  1. 节点级别的追踪
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}
  1. 图级别的监控
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()]})
  1. 性能分析
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 个最耗时的函数

六、总结与展望

核心要点回顾

  1. 运行时抽象的价值

    • 通过统一接口(WorkflowRuntime)实现 Direct 和 LangGraph 两种运行时的无缝切换
    • 业务代码与运行时实现解耦,提升可维护性和可测试性
    • 支持配置驱动的运行时选择,便于 A/B 测试和降级
  2. LangGraph StateGraph 的核心优势

    • 声明式编排:通过节点和边定义流程,而不是硬编码执行顺序
    • 状态自动传递:每个节点自动接收上一个节点的状态输出
    • 内置可观测性:自动追踪节点执行情况,便于调试和监控
    • 支持条件分支和并行执行:适合复杂的业务场景
  3. 流式模式的前置/后置图分离

    • 前置图:快速执行 prepare 和 before_model,减少用户等待时间
    • 模型流式:直接调用 LLM 的流式 API,实现 token 级别的实时推送
    • 后置图:在模型完成后执行 after_model 和 finalize,保证数据完整性
    • 这种设计兼顾了图编排的优势和流式模式的实时性
  4. 性能与可维护性的权衡

    • 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 中讨论。


项目地址https://github.com/luguochang/StockPilotX

相关推荐
湘美书院--湘美谈教育4 小时前
湘美书院主理人:AI时代的文雅智能,赏花赏月赏秋香
人工智能·深度学习·神经网络·机器学习·ai写作
aiAIman4 小时前
OpenClaw Web Search 完全指南(2026年3月最新)
人工智能·开源·aigc
清水白石0084 小时前
Python 内存陷阱深度解析——浅拷贝、深拷贝与对象复制的正确姿势
开发语言·python
岛雨QA4 小时前
【基础知识】人工智能大模型常见术语(1)
人工智能·aigc·openai
国家二级编程爱好者4 小时前
删除typora文档没有引用的资源文件
git·python
进击的雷神4 小时前
邮箱编码解码、国际电话验证、主办方过滤、多页面深度爬取——柬埔寨塑料展爬虫四大技术难关攻克纪实
爬虫·python
互联网志5 小时前
为数字经济与人工智能深度融合筑牢坚实底座
人工智能
l112206035 小时前
网站SEO定时自动发布工具优质厂家推荐
人工智能·推荐·seo优化·网站seo·定时发布工具·优质厂家
ASD123asfadxv5 小时前
RepVGG重参数化架构改进YOLOv26训练推理双模式与多分支融合协同突破
人工智能·yolo·目标跟踪
九硕智慧建筑一体化厂家5 小时前
一站式交付!绿色工厂节能降碳+照明+PLC集成解决方案直供
人工智能