【第3篇】使用LangGraph构建工作流

LangGraph

工作流引擎
核心架构
StateGraph

状态机
State

类型安全状态
Node

纯函数节点
Edge

条件边/普通边
状态管理
Reducer

状态合并策略
配置系统

多租户支持
结构化输出

Pydantic验证
提示工程
动态模板

上下文感知
Few-shot

语义示例选择
CoT推理

显式思维链
自洽性

多路径验证
记忆机制
滑动窗口

Token管理
摘要压缩

层次化记忆
持久化

PostgreSQL/SQLite
检查点

容错恢复
生产特性
人工介入

Human-in-the-loop
时间旅行

调试与回滚
并发控制

版本向量

3.1 LangGraph基础:状态机驱动的业务流程自动化

LangGraph并非简单的"LangChain扩展",而是基于状态机(State Machine)理论的持久化工作流引擎。它解决了传统Chain无法处理的循环、分支和状态持久化问题。

3.1.1 状态管理:单一事实源架构

核心架构对比
传统Chain(无状态)
输入
步骤1
步骤2
步骤3
输出
LangGraph(有状态)
条件A
条件B
继续
结束
输入
节点1
更新状态

State
节点2
更新状态

State
条件分支

Conditional Edge
节点3A
节点3B
更新状态
循环或结束
输出

状态(State)的技术定义

State是不可变数据结构的累积器,每个节点接收当前状态,返回更新片段,LangGraph自动合并:

python 复制代码
from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, END
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, AIMessage
import operator
from datetime import datetime

# 图书馆业务状态定义(类型安全)
class LibraryState(TypedDict):
    # 消息历史:使用operator.add实现追加语义
    messages: Annotated[List[AnyMessage], operator.add]
    
    # 当前业务实体
    current_book: Optional[str]
    borrower_id: Optional[str]
    
    # 检索结果缓存(自定义reducer:替换而非追加)
    search_results: Annotated[List[dict], lambda x, y: y if y else x]
    
    # 业务标志位
    intent: Optional[str]              # 识别到的意图
    eligibility: Optional[bool]        # 借阅资格
    confirmation: Optional[bool]       # 用户确认
    
    # 元数据(用于限流和调试)
    step_count: Annotated[int, operator.add]  # 步骤计数
    start_time: str
    library_code: str                  # 分馆代码

# 初始化状态
initial_state = {
    "messages": [SystemMessage(content="图书馆智能服务系统启动")],
    "current_book": None,
    "borrower_id": None,
    "search_results": [],
    "intent": None,
    "eligibility": None,
    "confirmation": None,
    "step_count": 0,
    "start_time": datetime.now().isoformat(),
    "library_code": "MAIN"
}

# 构建状态图
builder = StateGraph(LibraryState)

节点(Node)的函数式语义

节点是纯函数 f(state) -> update_dict,遵循不可变性原则:

python 复制代码
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

llm = ChatOpenAI(model="gpt-4", temperature=0)

# 节点1:意图识别(分类任务)
class IntentClassification(BaseModel):
    intent: str = Field(description="意图类型:search/loan/return/renew/help")
    confidence: float = Field(description="置信度0-1")
    entities: dict = Field(description="提取的实体(书名、作者等)")

intent_parser = PydanticOutputParser(pydantic_object=IntentClassification)

def understand_request(state: LibraryState) -> dict:
    """
    分析用户最后一条消息,识别意图和提取实体
    返回:状态更新字典(仅包含变更的字段)
    """
    last_message = state["messages"][-1]
    
    prompt = f"""分析以下读者咨询,提取意图和关键信息:
    咨询内容:{last_message.content}
    格式要求:{intent_parser.get_format_instructions()}"""
    
    response = llm.invoke(prompt)
    parsed = intent_parser.parse(response.content)
    
    # 仅返回需要更新的字段
    return {
        "intent": parsed.intent,
        "step_count": 1,  # 累加到现有值
        "messages": [AIMessage(content=f"系统理解:意图={parsed.intent}")]  # 追加
    }

# 节点2:图书检索(条件执行)
def search_books(state: LibraryState) -> dict:
    """仅在intent为search时执行"""
    if state["intent"] != "search":
        return {"step_count": 0}  # 空操作仍需返回dict
    
    # 从state中提取查询参数
    query = state.get("current_book", "")
    
    # 模拟向量检索(实际应连接真实数据库)
    results = [
        {"title": "Python编程:从入门到实践", "call_number": "TP311.56/12", "available": True},
        {"title": "流畅的Python", "call_number": "TP311.56/34", "available": False}
    ]
    
    return {
        "search_results": results,
        "step_count": 1,
        "messages": [AIMessage(content=f"检索到{len(results)}本相关图书")]
    }

# 节点3:资格检查(外部API调用)
def check_eligibility(state: LibraryState) -> dict:
    """检查读者借阅资格"""
    borrower_id = state.get("borrower_id")
    
    # 模拟外部系统查询
    # 实际:调用图书馆管理系统API
    is_eligible = borrower_id and borrower_id.startswith("LIB")
    
    return {
        "eligibility": is_eligible,
        "step_count": 1,
        "messages": [AIMessage(content="资格检查完成" if is_eligible else "无有效借书证")]
    }

⚠️ 注意 :看到上述节点函数,可能误以为返回{"intent": intent},实际上LangGraph要求返回完整的更新字典,且类型必须与State定义匹配。缺失的字段不会被修改,但返回的类型必须正确。

3.1.2 Reducer:状态合并的代数结构

Reducer定义了状态更新的半群(Semigroup)操作,这是LangGraph的核心数学抽象:
Reducer代数
旧状态

old_state
合并操作

Reducer
新片段

update_dict
新状态

new_state
operator.add

列表追加
累加语义

messages
lambda x,y: y

替换
覆盖语义

search_results
max

取最大
极值语义

优先级

Reducer类型详解

Reducer 数学操作 适用场景 示例
operator.add 幺半群(Monoid)追加 消息历史、日志 messages
lambda x, y: y 替换(Replace) 缓存结果、当前实体 search_results
operator.max 取最大值 优先级、分数 confidence_score
lambda x, y: x + y 数值累加 计数器、积分 step_count
自定义函数 复杂合并 去重、加权平均 unique_tags

Reducer设计

python 复制代码
from typing import List, Dict, Any

def merge_messages(old: List[AnyMessage], new: List[AnyMessage]) -> List[AnyMessage]:
    """
    智能消息合并:去重 + 长度限制
    防止上下文窗口溢出(Token爆炸)
    """
    combined = old + new
    
    # 去重:基于消息内容hash
    seen = set()
    unique = []
    for msg in combined:
        msg_hash = hash((msg.type, msg.content))
        if msg_hash not in seen:
            seen.add(msg_hash)
            unique.append(msg)
    
    # 保留最近20条(可配置)
    max_history = 20
    if len(unique) > max_history:
        # 始终保留第一条system消息
        system_msgs = [m for m in unique if isinstance(m, SystemMessage)]
        other_msgs = [m for m in unique if not isinstance(m, SystemMessage)]
        unique = system_msgs + other_msgs[-(max_history-len(system_msgs)):]
    
    return unique

def merge_search_results(old: List[dict], new: List[dict]) -> List[dict]:
    """
    检索结果合并:加权去重(基于相关性分数)
    """
    if not new:
        return old
    
    # 合并并去重(基于title)
    combined = {item["title"]: item for item in old}
    for item in new:
        title = item["title"]
        if title in combined:
            # 保留分数更高的
            if item.get("score", 0) > combined[title].get("score", 0):
                combined[title] = item
        else:
            combined[title] = item
    
    # 按相关性排序
    return sorted(combined.values(), key=lambda x: x.get("score", 0), reverse=True)[:10]  # Top 10

# 应用自定义reducer
class LibraryState(TypedDict):
    messages: Annotated[List[AnyMessage], merge_messages]
    search_results: Annotated[List[dict], merge_search_results]
    # ... 其他字段

状态膨胀防护

"设置上限"具体机制,实际上需要双重防护

python 复制代码
def check_token_overflow(state: LibraryState) -> bool:
    """检查上下文是否即将溢出"""
    from langchain_core.messages import convert_to_openai_messages
    
    # 估算token数(1 token ≈ 4字符中文,3字符英文)
    total_chars = sum(len(m.content) for m in state["messages"])
    estimated_tokens = total_chars // 3
    
    # GPT-4上下文限制8K,预留1K给输出
    return estimated_tokens > 7000

def summarize_if_needed(state: LibraryState) -> LibraryState:
    """如果上下文过长,触发摘要"""
    if check_token_overflow(state):
        # 保留最近2轮,摘要更早的历史
        recent = state["messages"][-2:]
        older = state["messages"][:-2]
        
        summary_prompt = f"总结以下对话的关键信息(100字内):{older}"
        summary = llm.invoke(summary_prompt).content
        
        new_messages = [
            SystemMessage(content=f"历史摘要:{summary}"),
            *recent
        ]
        return {**state, "messages": new_messages}
    return state

3.1.3 配置系统:多租户架构

ConfigSchema实现环境适配

python 复制代码
from langgraph.graph import StateGraph
from langchain_core.runnables import RunnableConfig
from typing import TypedDict

class LibraryConfig(TypedDict):
    """运行时配置模式"""
    library_code: str           # 图书馆代码(分馆标识)
    max_loan_days: int         # 最大借阅天数(分馆策略)
    max_books_per_user: int    # 借阅上限
    model_name: str            # LLM模型选择
    enable_ai_recommendation: bool  # 功能开关
    language: str              # 服务语言

# 默认配置
DEFAULT_CONFIG: LibraryConfig = {
    "library_code": "MAIN",
    "max_loan_days": 30,
    "max_books_per_user": 10,
    "model_name": "gpt-4",
    "enable_ai_recommendation": True,
    "language": "zh-CN"
}

def search_node(state: LibraryState, config: RunnableConfig) -> dict:
    """
    节点接收config参数,实现多租户逻辑
    """
    # 提取配置(带默认值)
    library_config = config.get("configurable", {})
    library_code = library_config.get("library_code", "MAIN")
    lang = library_config.get("language", "zh-CN")
    
    # 根据分馆选择数据库
    db = get_library_db(library_code)
    
    # 根据语言选择提示词
    prompt_template = get_multilingual_prompt("search", lang)
    
    # 执行检索
    results = db.query(
        state["query"],
        limit=library_config.get("max_results", 5)
    )
    
    return {"search_results": results}

# 运行时动态配置
result = graph.invoke(
    {"query": "Python编程"},
    config={
        "configurable": {
            "library_code": "BRANCH_EAST",  # 东区分馆
            "max_loan_days": 15,            # 分馆策略:更短借阅期
            "language": "en-US"             # 服务外籍读者
        }
    }
)

配置继承与覆盖机制
⚙️ 配置加载优先级层级
📦 系统默认配置

DEFAULT_CONFIG
🌍 环境变量

ENV VARIABLES
🚀 运行时注入

invoke(config=...)
🎯 节点级覆盖

NODE SPECIFIC
✨ 最终生效配置

MERGED CONFIG

3.1.4 结构化输出:类型安全的业务数据

Pydantic与LangGraph的深度集成

python 复制代码
from pydantic import BaseModel, Field, validator
from typing import Literal
from langchain.output_parsers import PydanticOutputParser

class BookRecommendation(BaseModel):
    """图书推荐结构化输出"""
    title: str = Field(description="书名(精确匹配馆藏)")
    author: str = Field(description="作者(姓在前,名在后)")
    call_number: str = Field(
        description="索书号(遵循《中国图书馆分类法》)",
        pattern=r"^[A-Z][0-9]+(\.[0-9]+)?/.+$"  # 正则验证
    )
    isbn: str = Field(description="ISBN-13", pattern=r"^\d{13}$")
    reason: str = Field(description="推荐理由(50-200字)")
    availability: Literal["available", "loaned", "reserved", "unknown"] = Field(
        description="在架状态"
    )
    shelf_location: str = Field(description="馆藏位置(楼层+区域)")
    
    @validator('reason')
    def validate_reason_length(cls, v):
        if not 50 <= len(v) <= 200:
            raise ValueError('推荐理由长度必须在50-200字之间')
        return v

# 创建带验证的解析器
parser = PydanticOutputParser(pydantic_object=BookRecommendation)

def generate_recommendation(state: LibraryState, config: RunnableConfig) -> dict:
    """
    生成结构化推荐,带错误恢复
    """
    # 构建格式化的提示
    format_instructions = parser.get_format_instructions()
    
    prompt = f"""基于以下检索结果,为读者推荐一本最合适的图书。
    检索结果:{state['search_results']}
    读者偏好:{state.get('preferences', '无特殊偏好')}
    
    必须严格按以下JSON格式返回:
    {format_instructions}
    
    注意:所有字段必须填写,不能为null"""
    
    # 带重试的生成
    max_retries = 3
    for attempt in range(max_retries):
        try:
            response = llm.invoke(prompt)
            structured = parser.parse(response.content)
            
            # 验证通过
            return {
                "recommendation": structured.model_dump(),
                "messages": [AIMessage(content=f"推荐图书:《{structured.title}》")]
            }
            
        except Exception as e:
            if attempt == max_retries - 1:
                # 最终失败,返回错误状态
                return {
                    "error": f"结构化生成失败:{str(e)}",
                    "messages": [AIMessage(content="抱歉,推荐系统暂时故障,请稍后重试")]
                }
            # 修正提示词,要求更严格的格式
            prompt += f"\n\n上次的输出格式有误:{str(e)},请修正后重新输出。"

错误处理节点模式
成功
解析失败
重试<3次
重试耗尽
生成节点
验证通过
错误处理节点
降级输出

纯文本
继续流程


3.2 提示工程:咨询质量的系统性优化

3.2.1 动态模板系统:上下文感知的提示组装

分层模板架构

python 复制代码
from langchain_core.prompts import (
    ChatPromptTemplate, 
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate
)
from datetime import datetime

def create_dynamic_template(state: LibraryState, config: LibraryConfig) -> ChatPromptTemplate:
    """
    根据当前状态和配置动态构建提示模板
    """
    # 基础系统消息(所有场景)
    base_system = SystemMessagePromptTemplate.from_template("""你是{library_name}的智能馆员。
    当前时间:{current_time}
    服务语言:{language}
    读者ID:{borrower_id}
    """)
    
    # 动态添加角色专长(基于意图)
    intent = state.get("intent")
    expertise_map = {
        "search": "精通图书检索和分类法",
        "loan": "熟悉借阅规则和流程",
        "reference": "擅长深度咨询和文献传递",
        "technical": "了解图书馆IT系统和数据库"
    }
    
    role_expertise = expertise_map.get(intent, "通用咨询服务")
    
    # 动态添加业务规则(基于分馆配置)
    rules = []
    if config.get("max_loan_days"):
        rules.append(f"本馆借阅期限为{config['max_loan_days']}天")
    if config.get("enable_ai_recommendation"):
        rules.append("可使用AI推荐功能")
    
    # 组装完整模板
    template = ChatPromptTemplate.from_messages([
        base_system,
        ("system", f"你的专长:{role_expertise}"),
        ("system", "业务规则:" + "\n".join(rules)) if rules else ("system", ""),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}")
    ])
    
    return template.partial(
        library_name=config.get("library_code", "图书馆"),
        current_time=datetime.now().strftime("%Y-%m-%d %H:%M"),
        language=config.get("language", "zh-CN"),
        borrower_id=state.get("borrower_id", "访客")
    )

# 使用示例
template = create_dynamic_template(current_state, current_config)
prompt = template.format_messages(
    chat_history=state["messages"][:-1],
    input=state["messages"][-1].content
)

3.2.2 少样本学习的工程化实现

动态示例选择(基于语义相似度)

python 复制代码
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# 构建示例库(覆盖主要业务场景)
example_library = [
    {
        "query": "我想借《三体》",
        "thought": "读者要借阅具体图书,需检查:1)馆藏是否存在 2)在架状态 3)读者资格",
        "action": "check_availability",
        "action_input": "三体"
    },
    {
        "query": "推荐几本Python入门书",
        "thought": "读者需要推荐服务,应使用AI推荐功能,考虑:1)初学者水平 2)实践导向 3)馆藏可得性",
        "action": "ai_recommend",
        "action_input": "Python programming, beginner, practical"
    },
    {
        "query": "怎么续借?",
        "thought": "读者咨询流程性问题,直接提供续借操作指引,无需检索",
        "action": "provide_guidance",
        "action_input": "renewal_process"
    },
    {
        "query": "这个月的科技新书有哪些?",
        "thought": "读者需要新书通报,查询本月到馆的新书,按学科分类展示",
        "action": "new_arrivals",
        "action_input": "current_month, technology"
    }
]

# 创建语义选择器
example_selector = SemanticSimilarityExampleSelector.from_examples(
    example_library,
    OpenAIEmbeddings(),
    FAISS,
    k=2  # 只选最相关的2个示例
)

def create_few_shot_prompt(user_query: str) -> str:
    """动态选择示例并组装提示"""
    selected = example_selector.select_examples({"query": user_query})
    
    examples_text = "\n\n".join([
        f"咨询:{ex['query']}\n"
        f"思考:{ex['thought']}\n"
        f"行动:{ex['action']}\n"
        f"输入:{ex['action_input']}"
        for ex in selected
    ])
    
    return f"""以下是处理读者咨询的示例:

{examples_text}

现在处理新的咨询:
咨询:{user_query}
思考:"""

⚠️ 注意 :并不是建议"示例不超过5个",实际上应使用动态选择而非固定数量。通过语义相似度选择最相关的2-3个示例,比固定5个无关示例效果更好。

3.2.3 CoT与自洽性:推理质量的双重保障

思维链(Chain-of-Thought)的形式化

CoT的本质是显式化隐式推理路径,通过增加token计算提升准确性:

python 复制代码
cot_template = """你是一位经验丰富的参考咨询馆员。处理读者咨询时,请严格遵循以下推理框架:

【需求解析阶段】
1. 识别信息需求类型:事实型/主题型/文献型/导向型
2. 确定学科领域和深度要求
3. 识别任何隐含约束(时间、语言、可获取性)

【策略规划阶段】
4. 选择检索工具:馆藏目录/数据库/网络资源/专家咨询
5. 构建检索式:确定关键词、同义词、分类号
6. 设定评估标准:相关性、权威性、时效性

【执行与评估阶段】
7. 执行检索并评估结果质量
8. 若结果不足,调整策略(扩展/缩小/转换检索词)
9. 整理想法,准备回答

【回答组织阶段】
10. 结构化呈现:直接答案→详细解释→相关资源
11. 验证信息准确性(交叉验证)
12. 提供后续咨询路径

当前咨询:{question}

请展示完整思考过程:"""

# 在LangGraph节点中使用
def cot_reasoning_node(state: LibraryState) -> dict:
    prompt = cot_template.format(question=state["messages"][-1].content)
    response = llm.invoke(prompt)
    
    # 提取最终答案(通常在最后部分)
    thinking_process = response.content
    
    return {
        "cot_thinking": thinking_process,
        "messages": [AIMessage(content=thinking_process)]
    }

自洽性验证(Self-Consistency)的并行实现

python 复制代码
from concurrent.futures import ThreadPoolExecutor, as_completed

def self_consistency_node(state: LibraryState) -> dict:
    """
    并行生成多个推理路径,通过投票选择最可靠的答案
    """
    question = state["messages"][-1].content
    num_samples = 3  # 采样次数
    
    # 使用稍高的temperature增加多样性
    diverse_llm = ChatOpenAI(model="gpt-4", temperature=0.7)
    
    def single_reasoning(_):
        """单次推理"""
        prompt = f"""回答读者咨询,先思考后回答:
        咨询:{question}
        思考过程:"""
        return diverse_llm.invoke(prompt).content
    
    # 并行执行(I/O密集型,适合多线程)
    responses = []
    with ThreadPoolExecutor(max_workers=num_samples) as executor:
        futures = [executor.submit(single_reasoning, i) for i in range(num_samples)]
        for future in as_completed(futures):
            try:
                responses.append(future.result())
            except Exception as e:
                print(f"推理路径失败:{e}")
    
    if len(responses) < 2:
        return {"final_answer": responses[0] if responses else "推理失败"}
    
    # 答案聚类(基于嵌入相似度)
    embeddings = OpenAIEmbeddings().embed_documents(responses)
    
    # 简单投票:选择与其他答案平均相似度最高的
    import numpy as np
    similarity_matrix = np.inner(embeddings, embeddings)
    avg_similarities = similarity_matrix.mean(axis=1)
    best_idx = avg_similarities.argmax()
    
    # 计算置信度(一致性比例)
    confidence = avg_similarities[best_idx]
    
    return {
        "final_answer": responses[best_idx],
        "confidence": float(confidence),
        "all_responses": responses,
        "messages": [AIMessage(content=f"[置信度{confidence:.2f}] {responses[best_idx]}")]
    }

架构整合
置信度<0.8
置信度≥0.8
用户咨询
CoT推理节点

详细思考
置信度检查
自洽性验证

多路径采样
直接输出
答案聚类

相似度计算
选择一致性最高答案
最终回答
用户反馈收集
优化示例库


3.3 上下文窗口管理:记忆压缩与检索

3.3.1 滑动窗口与摘要机制

Token预算管理

python 复制代码
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
import tiktoken

def count_tokens(messages: list, model: str = "gpt-4") -> int:
    """精确计算token数"""
    encoding = tiktoken.encoding_for_model(model)
    total = 0
    for msg in messages:
        total += len(encoding.encode(msg.content))
        # 消息格式开销(角色标签等)
        total += 4  # 近似开销
    return total

def smart_context_window(state: LibraryState, max_tokens: int = 6000) -> dict:
    """
    智能上下文管理:滑动窗口 + 摘要 + RAG检索
    """
    messages = state["messages"]
    current_tokens = count_tokens(messages)
    
    if current_tokens <= max_tokens:
        return {"messages": messages}  # 无需处理
    
    # 策略1:保留System消息和最近N轮
    system_msgs = [m for m in messages if isinstance(m, SystemMessage)]
    conversation = [m for m in messages if not isinstance(m, SystemMessage)]
    
    # 保留最近4轮(8条消息)
    recent = conversation[-8:] if len(conversation) > 8 else conversation
    older = conversation[:-8] if len(conversation) > 8 else []
    
    if not older:
        # 仅保留system + recent
        return {"messages": system_msgs + recent}
    
    # 策略2:对历史生成摘要
    summary_prompt = f"""将以下对话历史总结为关键信息(保留实体、意图、已确认的事实):
    {older}
    
    摘要(200字内):"""
    
    summary = llm.invoke(summary_prompt).content
    summary_msg = SystemMessage(content=f"[历史摘要] {summary}")
    
    # 策略3:对当前查询相关的历史进行RAG检索(可选增强)
    if state.get("enable_rag_memory"):
        relevant = retrieve_relevant_history(older, recent[-1].content)
        context_msgs = [SystemMessage(content=f"[相关历史] {r}") for r in relevant]
    else:
        context_msgs = []
    
    new_messages = system_msgs + [summary_msg] + context_msgs + recent
    
    # 验证最终token数
    final_tokens = count_tokens(new_messages)
    print(f"上下文压缩:{current_tokens} -> {final_tokens} tokens")
    
    return {"messages": new_messages}

def retrieve_relevant_history(history: list, current_query: str, k: int = 2) -> list:
    """从长历史中检索与当前查询相关的片段"""
    from langchain_community.vectorstores import Chroma
    
    # 临时构建向量库(实际应持久化)
    texts = [m.content for m in history]
    metadatas = [{"index": i} for i in range(len(history))]
    
    db = Chroma.from_texts(texts, OpenAIEmbeddings(), metadatas=metadatas)
    results = db.similarity_search(current_query, k=k)
    
    return [r.page_content for r in results]

3.3.2 分层记忆架构

记忆层级
摘要/溢出
持久化
检索
工作记忆

Working Memory

当前对话

~4K tokens
短期记忆

Short-term

会话历史

~100K tokens
长期记忆

Long-term

用户画像

无限
外部记忆

External

知识库/文档

RAG检索


3.4 持久化与检查点:可靠性工程

3.4.1 记忆持久化架构

PostgreSQL生产级配置

python 复制代码
from langgraph.checkpoint.postgres import PostgresSaver
from contextlib import contextmanager

@contextmanager
def get_checkpointer():
    """数据库连接池管理"""
    connection_kwargs = {
        "autocommit": True,
        "prepare_threshold": 0,  # 禁用预处理语句缓存(兼容PgBouncer)
    }
    
    checkpointer = PostgresSaver(
        conninfo="postgresql://user:pass@localhost:5432/library_db",
        connection_kwargs=connection_kwargs
    )
    
    # 初始化表结构(首次运行)
    checkpointer.setup()
    
    try:
        yield checkpointer
    finally:
        checkpointer.conn.close()

# 使用
with get_checkpointer() as checkpointer:
    graph = builder.compile(checkpointer=checkpointer)
    result = graph.invoke(initial_state, config={"configurable": {"thread_id": "user_123"}})

SQLite轻量级方案

python 复制代码
from langgraph.checkpoint.sqlite import SqliteSaver

# 内存模式(测试)
memory_saver = SqliteSaver.from_conn_string(":memory:")

# 文件模式(小型生产)
disk_saver = SqliteSaver.from_conn_string("/data/library_checkpoints.db")

graph = builder.compile(checkpointer=disk_saver)

3.4.2 检查点机制:容错与恢复

检查点的数据结构

python 复制代码
# LangGraph自动保存的检查点包含:
checkpoint = {
    "v": 1,  # 版本
    "ts": "2024-01-15T10:30:00Z",  # 时间戳
    "id": "checkpoint_id",  # 唯一ID
    "channel_values": {  # 状态通道值
        "messages": [...],
        "search_results": [...],
        # ... 所有state字段
    },
    "channel_versions": {  # 版本向量(用于并发控制)
        "messages": 5,
        "search_results": 3
    },
    "versions_seen": {},  # 已见的父版本
    "pending_sends": []   # 待处理的异步消息
}

断点续传与人工介入

python 复制代码
# 配置检查点命名空间
config = {
    "configurable": {
        "thread_id": "loan_application_001",  # 业务ID
        "checkpoint_ns": "circulation",        # 命名空间
        "checkpoint_id": None                  # 首次运行为None
    }
}

# 场景1:流程中断恢复
def resume_workflow(thread_id: str):
    """从最后检查点恢复流程"""
    # 获取最新检查点
    checkpoint = checkpointer.get({
        "configurable": {"thread_id": thread_id}
    })
    
    if not checkpoint:
        return "无历史记录"
    
    # 继续执行
    for event in graph.stream(None, config, stream_mode="values"):
        print(event)
    
    return "流程已恢复"

# 场景2:人工审核节点(Human-in-the-loop)
def human_approval_node(state: LibraryState) -> dict:
    """暂停等待人工确认"""
    # 实际实现:抛出特殊异常或设置等待标志
    # LangGraph会在检查点保存状态,等待外部触发
    raise NodeInterrupt("等待管理员确认借阅申请")

# 编译时配置中断点
graph = builder.compile(
    checkpointer=checkpointer,
    interrupt_before=["human_approval"]  # 在这些节点前暂停
)

# 外部恢复(管理员审核后)
def approve_and_continue(thread_id: str, approval: bool):
    """管理员审核后继续"""
    config = {"configurable": {"thread_id": thread_id}}
    
    # 注入人工决策
    human_response = "approved" if approval else "rejected"
    
    # 恢复执行
    for event in graph.stream(
        Command(resume=human_response),  # 新版LangGraph语法
        config
    ):
        print(event)

时间旅行调试(Time Travel)

python 复制代码
# 查看历史检查点
history = list(graph.get_state_history(config))
for checkpoint in history:
    print(f"时间:{checkpoint.ts}, 节点:{checkpoint.metadata['step']}")

# 回滚到特定检查点
target_checkpoint = history[3]  # 回到第3步
graph.update_state(
    config,
    target_checkpoint.config,  # 使用历史配置
    as_node="manual_rollback"
)

# 从该点重新执行(不同分支)
for event in graph.stream(None, config):
    print(event)

3.5 本章小结

关键工程原则

  1. 状态是单一事实源:所有业务逻辑通过状态转换实现,避免副作用
  2. Reducer定义业务语义:选择合适的合并策略(追加/替换/聚合)
  3. 检查点即服务:将持久化视为一等公民,确保流程可恢复
  4. 配置驱动多租户:同一图实例服务不同分馆,通过config隔离
  5. 防御性提示工程:结构化输出+验证+重试,确保可靠性
相关推荐
Jial-(^V^)2 小时前
使用强化学习微调大模型
人工智能·llm
李少兄2 小时前
Windows系统JDK安装与环境配置指南(2026年版)
java·开发语言·windows
风雨中的小七2 小时前
和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录
人工智能
一个帅气昵称啊2 小时前
.NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块
人工智能·ai·.net
Rubin智造社2 小时前
04月13日AI每日参考:Anthropic高危模型限流,中国每日处理140万亿Token
人工智能·anthropic·claude mythos·ai每日参考·apple智能眼镜·华为昇腾·aigc监管
东坡肘子2 小时前
被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131
人工智能·swiftui·swift
AI袋鼠帝2 小时前
我跑通了辅助起号Skil,新手也能直接抄~
人工智能
Wild API2 小时前
Claude、GPT、Gemini 场景对比表
人工智能·gpt·深度学习
星纬智联技术2 小时前
AI代码审查工具集成趋势:从“降本”到“提质”的流程重构
人工智能·aigc