【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

相关推荐
那我掉的头发算什么1 小时前
【SpringBoot】一篇文章讲清楚拦截器所有知识
java·spring boot·后端·spring
njsgcs1 小时前
ollama 报错dial tcp ipv6:443: connectex: A connection attempt failed because 解决办法
人工智能
ValhallaCoder1 小时前
hot100-堆
数据结构·python·算法·
小小小米粒1 小时前
函数式接口 + Lambda = 方法逻辑的 “插拔式解耦”
开发语言·python·算法
眼镜哥(with glasses)2 小时前
0215笔记-语言模型,提问范式与 Token
人工智能·笔记·语言模型
AIMarketing2 小时前
2026年Q1光引GEO 2.0技术原理解析
人工智能
狮子座明仔3 小时前
体验式强化学习:让模型学会“吃一堑长一智“
人工智能·深度学习·自然语言处理
冬奇Lab3 小时前
一天一个开源项目(第30篇):banana-slides - 基于 nano banana pro 的原生 AI PPT 生成应用
人工智能·开源·aigc
Dr.Kun3 小时前
【鲲码园PsychoPy】延迟折扣任务(DDT)
python·psychopy·心理学编程