7.3 异常恢复与断点续跑(LangGraph)

《LangGraph开发AI Agent实践》1~6章试读_langgraph开发ai agent实践在线阅读-CSDN博客

LangGraph的核心优势在于通过图结构组织多个节点(如LLM调用、工具调用、条件判断等),支持动态流程控制和可扩展的对话逻辑。然而,在长时间运行或生产环境中,LangGraph应用可能面临异常中断(如网络故障、超时、LLM服务异常等),导致状态丢失、流程中断。为此,异常恢复(Exception Recovery)与断点续跑(Checkpointing & Resume)成为其关键的可靠性保障技术。

7.3.1  异常恢复与断点续跑技术详解

1. 常恢复

(1)目标:在图执行过程中发生异常时,捕获错误并采取适当策略(如重试、降级、记录日志、通知等),避免整个流程崩溃。

(2)实现方式:

  • 节点级异常处理:在每个节点(node)函数中使用try-except捕获异常,返回一个代表“错误状态”的图状态,供后续节点决策。
  • 全局异常钩子:通过自定义executor,或在graph.invoke()/graph.stream()调用外层包装异常处理逻辑。
  • 重试机制:结合tenacity等重试库,在节点内部对特定操作(如API调用)进行自动重试。
  • 错误传播控制:设计图状态中包含error字段,使后续节点能根据错误类型决定是否跳过、回退或终止。

2. 点续跑

(1)目标:将图执行过程中的中间状态持久化,以便在进程中断后从最近的检查点恢复执行,避免从头开始。

(2)核心机制:LangGraph内置检查点(Checkpoint)功能,通过Checkpointer接口支持状态持久化。

(3)关键组件:

  • Checkpointer:一个实现了put/get/list等方法的存储后端(如内存、SQLite、Redis、PostgreSQL)。
  • thread_id:每个执行流(thread)通过唯一ID标识,用于关联其检查点。
  • Resume能力:调用graph.invoke(..., thread_id=...)或 graph.stream(..., thread_id=...)时,若存在该thread的检查点,则自动从最新状态继续执行。

(4)支持的存储后端(官方提供):

  • MemorySaver(内存,用于测试)。
  • SQLiteSaver。
  • PostgresSaver。
  • 自定义实现(需符合Checkpointer协议)。

7.3.2  实战案例:带异常恢复的数据分析工作流

【示例7.3】实现一个“数据读取→数据清洗→分析报告生成→可视化”的数据分析工作流,模拟节点异常后,从断点恢复执行(以通义千问Qwen3生成分析报告为例)。

#Data_analysis_workflow_with_exception_recovery.py

# -*- coding: utf-8 -*-

import os

import json

import random

from dotenv import load_dotenv

from langgraph.graph import StateGraph, END

from langgraph.checkpoint.memory import MemorySaver

from langchain_core.prompts import PromptTemplate

from langchain_core.output_parsers import StrOutputParser

from langchain_openai import ChatOpenAI

from typing import TypedDict, Optional

# 加载环境变量

load_dotenv()

# 通义千问 API 配置(兼容 OpenAI 格式)

DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")

DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"

# 1. 定义数据分析状态结构

class DataAnalysisState(TypedDict):

    data: Optional[str]             # 原始数据

    cleaned_data: Optional[str]     # 清洗后数据

    report: Optional[str]           # 分析报告

    error: Optional[str]            # 异常信息

    progress: str           # 进度(readcleanreportvisualizedone

    visualize_desc: Optional[str]  # 可视化描述

# 2. 初始化LLM(使用OpenAI兼容接口访问通义千问)

llm = ChatOpenAI(

    model="qwen-plus",

    api_key=DASHSCOPE_API_KEY,

    base_url=DASHSCOPE_BASE_URL,

    temperature=0.3

)

# 3. 定义节点函数(模拟异常)

def read_data(state: DataAnalysisState) -> dict:

    """节点1:读取数据(模拟偶发异常)"""

    # 模拟1/2概率抛出异常

    if random.random() < 0.5:

        raise RuntimeError("数据读取失败:文件不存在")

   

    # 正常执行逻辑

    data = """用户行为数据:

    日期,访问量,转化率,客单价

    2025-12-01,1000,5.2%,199

    2025-12-02,1200,5.5%,210

    2025-12-03,1100,5.3%,205

    """

    return {"data": data, "progress": "data_read", "error": None}

def clean_data(state: DataAnalysisState) -> dict:

    """节点2:数据清洗"""

    if not state["data"]:

        raise ValueError("无原始数据,清洗失败")

   

    # 模拟数据清洗(去除空行、格式化)

    cleaned_data = "\n".join([line.strip() for line in state["data"].split("\n") if line.strip()])

    return {"cleaned_data": cleaned_data, "progress": "data_cleaned", "error": None}

def generate_report(state: DataAnalysisState) -> dict:

    """节点3:生成分析报告"""

    prompt = PromptTemplate(

        template="""请基于清洗后的用户行为数据生成分析报告:

        数据:{cleaned_data}

        报告要求:包含趋势分析、关键指标解读,字数200字左右。

        报告:""",

        input_variables=["cleaned_data"]

    )

    report_chain = prompt | llm | StrOutputParser()

    report = report_chain.invoke({"cleaned_data": state["cleaned_data"]})

    return {"report": report, "progress": "report_generated", "error": None}

def visualize_data(state: DataAnalysisState) -> dict:

    """节点4:生成可视化描述(模拟)"""

    visualize_desc = f"可视化图表:访问量趋势折线图、转化率柱状图、客单价饼图"

    return {"progress": "done", "error": None, "visualize_desc": visualize_desc}

# 修复后的读取数据函数(独立定义)

def fixed_read_data(state):

    """修复后的读取数据函数(确保100%成功)"""

    data = """用户行为数据:

    日期,访问量,转化率,客单价

    2025-12-01,1000,5.2%,199

    2025-12-02,1200,5.5%,210

    2025-12-03,1100,5.3%,205

    """

    return {"data": data, "progress": "data_read", "error": None}

# 自定义本地持久化工具函数

def save_state_to_file(state: dict, thread_id: str, filepath: str = "./analysis_checkpoints"):

    """将状态保存到本地文件"""

    os.makedirs(filepath, exist_ok=True)

    with open(f"{filepath}/{thread_id}.json", "w", encoding="utf-8") as f:

        json.dump(state, f, ensure_ascii=False, indent=2)

def load_state_from_file(thread_id: str, filepath: str = "./analysis_checkpoints") -> Optional[dict]:

    """从本地文件加载状态"""

    try:

        with open(f"{filepath}/{thread_id}.json", "r", encoding="utf-8") as f:

            return json.load(f)

    except FileNotFoundError:

        return None

# 4. 构建状态机(支持动态替换节点函数)

def build_analysis_graph(checkpointer, read_data_func=read_data):

    """

    构建状态机

    :param checkpointer: 检查点实例

    :param read_data_func: 读取数据的函数(支持动态替换)

    """

    graph_builder = StateGraph(DataAnalysisState)

   

    # 添加节点(使用传入的函数)

    graph_builder.add_node("read_data", read_data_func)

    graph_builder.add_node("clean_data", clean_data)

    graph_builder.add_node("generate_report", generate_report)

    graph_builder.add_node("visualize_data", visualize_data)

   

    # 设置入口和边

    graph_builder.set_entry_point("read_data")

    graph_builder.add_edge("read_data", "clean_data")

    graph_builder.add_edge("clean_data", "generate_report")

    graph_builder.add_edge("generate_report", "visualize_data")

    graph_builder.add_edge("visualize_data", END)

   

    # 编译图

    return graph_builder.compile(checkpointer=checkpointer)

# 5. 异常恢复与断点续跑逻辑

def run_with_recovery():

    # 初始化内存检查点(1.x版本稳定支持)

    checkpointer = MemorySaver()

   

    # 定义唯一任务ID

    thread_id = "analysis_task_001"

    config = {"configurable": {"thread_id": thread_id}}

   

    # 尝试从本地加载历史状态

    saved_state = load_state_from_file(thread_id)

    if saved_state:

        initial_state = saved_state

        print("=== 加载历史状态 ===")

    else:

        # 初始状态

        initial_state = {

            "progress": "init",

            "error": None,

            "data": None,

            "cleaned_data": None,

            "report": None,

            "visualize_desc": None

        }

   

    # ========== 第一次运行(使用原始函数,模拟异常) ==========

    print("=== 第一次运行(模拟异常) ===")

    # 构建包含原始函数的图

    graph = build_analysis_graph(checkpointer, read_data_func=read_data)

    current_state = initial_state

   

    try:

        # 执行图

        current_state = graph.invoke(initial_state, config=config)

        save_state_to_file(current_state, thread_id)

        print("第一次运行成功(未触发异常)")

    except Exception as e:

        print(f"执行异常:{e}")

        # 记录异常状态并保存

        current_state["error"] = str(e)

        current_state["progress"] = "failed"

        save_state_to_file(current_state, thread_id)

   

    # ========== 断点续跑(使用修复后的函数) ==========

    print("\n=== 断点续跑(恢复执行) ===")

    # 构建包含修复后函数的新图(关键:重新编译图)

    recovery_graph = build_analysis_graph(checkpointer, read_data_func=fixed_read_data)

   

    # 从保存的状态恢复执行

    recovery_state = load_state_from_file(thread_id)

    if recovery_state and recovery_state["progress"] == "failed":

        # 重置错误状态,重新执行

        recovery_state["error"] = None

        recovery_state["progress"] = "init"

       

        # 使用修复后的图执行

        final_state = recovery_graph.invoke(recovery_state, config=config)

       

        # 保存最终状态

        save_state_to_file(final_state, thread_id)

    else:

        final_state = recovery_state or current_state

   

    # 输出结果

    print("=== 断点续跑执行结果 ===")

    print(f"进度:{final_state['progress']}")

    print(f"清洗后数据:{final_state['cleaned_data']}")

    print(f"分析报告:{final_state['report']}")

    print(f"可视化描述:{final_state['visualize_desc']}")

# 运行示例

if __name__ == "__main__":

    run_with_recovery()

运行输出:

=== 加载历史状态 ===

=== 第一次运行(模拟异常) ===

执行异常:数据读取失败:文件不存在

=== 断点续跑(恢复执行) ===

=== 断点续跑执行结果 ===

进度:done

清洗后数据:用户行为数据:

日期,访问量,转化率,客单价

2025-12-01,1000,5.2%,199

2025-12-02,1200,5.5%,210

2025-12-03,1100,5.3%,205

分析报告:202512月上旬用户行为数据显示整体呈积极趋势。访问量从1000稳步增长至1200后略有回落至1100,保持高位运行;转化率由5.2%持续提升至5.5%,表明营销策略或页面优化效果显著,用户购买意愿增强;客单价同步上升,从199元增至210元,反映出用户消费能力提升或商品组合优化成功。综合来看,关键指标均呈向好趋势,尤其转化率与客单价双增长,显著提升整体营收潜力。建议继续保持用户体验优化,并针对高价值用户精准投放,以进一步放大增长效应。后续需关注访问量波动原因,确保流量稳定性。

可视化描述:可视化图表:访问量趋势折线图、转化率柱状图、客单价饼图

7.3.3  案例代码解析

该案例代码是一个基于LangGraph实现的、带有异常处理和断点续跑能力的数据分析工作流,核心目标是模拟数据读取偶发异常、捕获异常状态、修复问题后从断点恢复执行,最终完成数据清洗、分析报告生成和可视化描述输出。整体架构分为6个核心模块,以下是分层解析。

1. 环境与依赖配置层

# 核心依赖导入 + 通义千问兼容配置

load_dotenv()  # 加载.env文件中的环境变量

DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")

DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"

llm = ChatOpenAI(

    model="qwen-plus",

    api_key=DASHSCOPE_API_KEY,

    base_url=DASHSCOPE_BASE_URL,

    temperature=0.3

)

  • 核心作用:解决LLM适配问题,因ChatDashScope导入路径变更,改用ChatOpenAI+通义千问兼容接口,复用OpenAI生态代码的同时实现对千问模型的调用。
  • 关键配置:base_url指向阿里云通义千问的OpenAI兼容接口,保证LLM调用逻辑无须大幅修改。

2. 状态结构定义层

class DataAnalysisState(TypedDict):

    data: Optional[str]              # 原始数据

    cleaned_data: Optional[str]     # 清洗后数据

    report: Optional[str]            # 分析报告

    error: Optional[str]             # 异常信息

    progress: str                     # 流程进度标识

    visualize_desc: Optional[str]   # 可视化描述

  • 核心作用:通过TypedDict强类型定义工作流的状态载体,明确所有可能的状态字段,避免运行时因键缺失报错。
  • 设计思路:状态是LangGraph工作流的核心,所有节点的输入/输出都围绕该状态结构。progress字段作为流程节点的“进度标记”,error字段专门存储异常信息,为断点续跑提供判断依据。

3. 核心业务节点函数层

分为“异常模拟函数”和“正常业务函数”两类。

(1)异常模拟函数:read_data(50%概率抛出文件不存在异常)、fixed_read_data(修复后100%成功读取数据)。

(2)核心逻辑:模拟生产环境中“偶发的数据源异常”,fixed_read_data作为修复后的替代函数,保证续跑时能跳过异常。

(3)正常业务函数:

  • clean_data:清洗原始数据(去除空行、格式化)。
  • generate_report:通过PromptTemplate+LLM链生成分析报告,输出解析为字符串。
  • visualize_data:模拟可视化生成,返回图表描述(可扩展为真实绘图逻辑)。

设计思路:每个节点函数接收state状态字典,返回更新后的状态片段(仅修改自身负责的字段),符合LangGraph“节点只处理局部状态,全局状态自动聚合”的设计原则。

4. 本地状态持久化工具层

def save_state_to_file/load_state_from_file:

    # 保存/加载状态到本地JSON文件

  • 核心作用:弥补MemorySaver(内存检查点)重启后状态丢失的问题,实现状态的持久化存储。
  • 关键逻辑:按thread_id(任务唯一标识)分文件存储,保证多任务状态不冲突,同时处理文件不存在的异常(加载时返回None)。

5. 动态图构建层

def build_analysis_graph(checkpointer, read_data_func=read_data):

    graph_builder = StateGraph(DataAnalysisState)

    graph_builder.add_node("read_data", read_data_func) # 动态传入函数

    # 节点关联 + 编译

    return graph_builder.compile(checkpointer=checkpointer)

  • 核心突破:解决LangGraph“编译后函数引用固化”的问题—将read_data_func作为参数传入,支持第一次运行使用异常函数、续跑时使用修复函数。
  • 图结构设计:线性节点关联(read→clean→report→visualize→END),符合数据分析的自然流程,入口节点设为read_data,保证流程从数据读取开始。

6. 点续跑执行层(核心逻辑)

def run_with_recovery():

    # 1. 初始化检查点 + 加载历史状态

    checkpointer = MemorySaver()

    saved_state = load_state_from_file(thread_id)

   

    # 2. 第一次运行(用异常函数)

    graph = build_analysis_graph(checkpointer, read_data)

    try:

        current_state = graph.invoke(initial_state, config)

    except Exception as e:

        current_state["error"] = str(e) # 记录异常

        save_state_to_file(current_state, thread_id) # 保存错误状态

   

    # 3. 断点续跑(用修复函数)

    recovery_graph = build_analysis_graph(checkpointer, fixed_read_data)

    recovery_state["error"] = None # 重置错误状态

    final_state = recovery_graph.invoke(recovery_state, config) # 重新执行

1)核心流程

(1)第一次运行:用带异常的read_data构建图,触发异常后保存错误状态。

(2)续跑准备:用修复后的fixed_read_data重新构建图,此步骤是关键步骤。

(3)续跑执行:重置错误状态,从保存的失败状态开始执行,完成整个流程。

2)关键设计

(1)config中的thread_id是检查点的“任务标识”,保证续跑时能匹配到对应任务的状态。

(2)两次运行分别构建不同的图(异常函数与修复函数),解决LangGraph编译后函数无法修改的问题。

该代码是LangGraph实现“有状态工作流+异常恢复”的典型示例,可直接作为生产级智能体工作流的基础模板,扩展后可适配真实的数据分析、任务调度等场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值