LangGraph 入门到精通0x02:基础 API (二)

前言

今天继续学习一些 LangGraph 常用到的基础 API。废话少说上干货。

.点语法

Graph 的创建可以使用看着更简洁的 .语法。

Python 复制代码
graph = (
    StateGraph(State)
    .add_node(a)
    .add_node(b)

    .add_edge(START, "a")
    .add_edge("a", "b")
    .add_edge("b", END)

    .compile()
)

步骤序列

Python 复制代码
StateGraph(State).add_sequence([step_1, step_2, step_3])

并行运行

Python 复制代码
graph = (
    StateGraph(State)
    .add_node(a)
    .add_node(b)
    .add_node(c)
    .add_node(d)

    .add_edge(START, "a")
    .add_edge("a", "b")
    .add_edge("a", "c")
    .add_edge("b", "d")
    .add_edge("c", "d")
    .add_edge("d", END)

    .compile()
)

上面的 Graph就是支持并行的,b, c 两个节点就是并行运行的两个节点。这个时候图的结构为:

MapReduce

MapReduce 模式,是一套用于构建动态、可扩展的智能体(Agent)或数据处理流水线的核心架构模式。

  • 本质:将经典的"分而治之"并行计算

  • Map:"分"与"散"

    • 输入:一个包含多个待处理项(如文档列表、查询列表、任务对象)的总体状态。

    • 过程 :通过一个条件边(Conditional Edge ​ 中的路由函数,利用 Send API 将输入列表"映射"为多个独立的子任务。每个 Send对象指定一个目标处理节点和一份独立的、仅包含该子任务数据的私有状态。

    • 关键 :这些 Send出的子任务会并行执行,实现高效扇出(Fan-out)。

  • Reduce:"合"与"聚"

    • 输入:所有并行 Map 节点处理完成后产生的多个中间结果状态。

    • 过程 :通过预定义的 Reducer (如 operator.add用于列表合并)自动将所有子任务的结果状态"归约"合并,更新回主工作流状态。

    • 关键:自动声明式,无需编写结果收集与合并代码,实现扇入(Fan-in)

图状态:

Python 复制代码
class OverallState(TypedDict):
    """全局状态:贯穿整个工作流"""
    topic: str                  # 输入的大主题(如 animals)
    subjects: list              # 生成的子主题列表
    jokes: Annotated[list, operator.add]  # 自动合并多条并行生成的笑话
    best_selected_joke: str     # 最终选中的最佳笑话

class JokeState(TypedDict):
    """子任务状态:仅给 generate_joke 使用"""
    subject: str

核心:

Python 复制代码
def continue_to_jokes(state: OverallState):
    """
    核心技术:Map-Reduce 并行执行
    给每个子主题分发一个独立的 generate_joke 任务
    """
    return [Send("generate_joke", {"subject": sub}) for sub in state["subjects"]]

recursion_limit

  • recursion_limit:LangGraph 强制安全上限 ,防止死循环,我们可以控制的最大步数
    • 步数:每个 Node 的执行为一步

以下代码表示:程序最多执行 100步

Pyth 复制代码
result = graph.invoke({"aggregate": []}, {"recursion_limit": 100})

RemainingSteps

  • RemainingSteps:LangGraph 的一个内置托管值,表示程序还剩多少步
    • ReadOnly:完全由框架自动维护
    • RemainingSteps = recursion_limit - 已走步数(框架自动算)

RetryPolicy

RetryPolicy 是一个声明式的重试策略配置对象。你通过参数告诉 LangGraph:"当这个节点执行失败时,请按我定的规矩再试几次,而不是立刻报错"。核心参数:

  • max_attempts: 最大重试次数(含首次)。

  • initial_interval: 首次重试前的等待时间(s)。

  • backoff_factor: 退避因子。每次重试间隔会乘以这个因子,用于实现"**指数退避

  • jitter: 是否在重试间隔中加入随机抖动。用于"重试风暴"。

  • retry_on: 一个异常类型或函数,用于判断何种失败才需要重试。

赶脚是不是和 http 请求的重试差不多,我们也模拟一个 http 节点 的重试case。这里实现重试策略有两种方式:

  1. 内置的 RetryPolicy 方法
Python 复制代码
# 2. 定义针对此节点的"重试作战计划"
api_retry_policy = RetryPolicy(
    max_attempts=3,           # 最多尝试3次(首次+2次重试)
    initial_interval=1.0,     # 第一次重试等1秒
    backoff_factor=2.0,       # 指数退避:第二次等2秒,第三次等4秒
    jitter=True,              # 增加随机抖动,避免重试风暴
    retry_on=(requests.exceptions.Timeout,
              requests.exceptions.ConnectionError,
              requests.exceptions.HTTPError)  # 只针对网络和5xx错误重试
)
  1. 自定义的 retry 方法
Python 复制代码
# 方法2:自定义函数匹配 - 更精细地控制,例如只重试特定HTTP状态码
def retry_on_policy(exception: Exception) -> bool:
    """
    自定义重试判断函数:
    1. 对所有网络连接错误(ConnectionError)和超时(Timeout)进行重试。
    2. 仅对特定HTTP状态码(429, 503)进行重试。
    """
    # 规则1: 如果是连接错误或超时,立即决定重试
    if isinstance(exception, (requests.exceptions.ConnectionError,
                              requests.exceptions.Timeout)):
        return True

    # 规则2: 如果是HTTP错误,则检查状态码
    if isinstance(exception, requests.exceptions.HTTPError):
        # 注意:exception.response 可能为 None(例如在请求未发出时)
        if exception.response is not None:
            if exception.response.status_code in (429, 503):
                return True
    # 其他所有情况,不重试
    return False

重试方法和节点绑定:

Python 复制代码
builder.add_node("call_api", call_external_api, retry=api_retry_policy)

长时记忆(向量检索)

前面我们知道 checkpointer 用于 LangGraph 中工作流的快照,但是他只能保存基于上下文的短期记忆。那么如果我们要做长时记忆该怎么做呢?

InMemoryStore

InMemoryStore 是 LangGraph 中基于 Python 字典实现的内存键值存储库。其核心主要在:

  • 存储 (put)

    • namespace:命名空间,是一个元组,用于逻辑隔离,如 ("users", "123", "preferences")
    • 键 (key):字典 key
    • 值 (value):字典 value
  • 读取 (get) :通过命名空间和键精确获取数据。

  • 检索 (search) :可检索某个命名空间下的所有条目,如果配置了嵌入模型,还能支持基于语义相似度的查询。

和用于 checkpointerInMemorySaver 不同,InMemorySaver用于保存单个对话线程(Thread)的完整状态,数据与 thread_id 强绑定。而 InMemoryStore 更像一个共享的白板或缓存 ,用结构化的方式(通过命名空间)存储和检索任意数据,这些数据可以被多个不同的线程或会话访问和复用。一个管"对话流程状态",一个管"共享知识记忆"。

InMemoryStore 构造代码:

  • embed:文本嵌入模型,用于文本向量化
  • dims:嵌入模型维度。各个模型可能不同!!!
Python 复制代码
in_memory_store = InMemoryStore(
    index={
        "embed": get_ali_embeddings(),  # 文本向量化
        "dims": 1024,  # 嵌入模型固定输出维度。各个模型可能不同!!!
    }
)

咿?存储就存储呗,怎么参数还需要文本嵌入,这是为什么呢?我们上面也看到了 InMemoryStore检索 功能。长期记忆若只支持精确键值检索,则效用有限。向量LLM 的引入,将其升级为一个支持模糊联想、概念匹配 的智能检索系统。这也正是轻量化的 RAG 在记忆系统中的实践。

记忆 + 对话

记忆和对话是长时记忆的核心与灵魂。

Python 复制代码
def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
    # 获取用户ID → 每个用户独立记忆空间
    user_id = config["configurable"]["user_id"]
    namespace = ("memories", user_id)

    # 向量检索:从长时记忆中匹配当前对话相关信息
    user_query = state["messages"][-1].content
    memories = store.search(namespace, query=user_query)
    user_info = "\n".join([data.value["data"] for data in memories])

    # 构建系统提示:注入用户记忆
    system_prompt = f"""
    你是乐于助人的助手。
    用户信息(请记住并使用):{user_info}
    """

    # 判断:用户提到 remember → 自动存储记忆
    last_msg = user_query.lower()
    if "请记住" in last_msg:
        # 提取用户真实输入的内容
        new_memory = user_query.strip()
        # 存储:命名空间 + 唯一ID + 记忆内容
        store.put(namespace, str(uuid.uuid4()), {"data": new_memory})

    # 调用AI生成回复(系统提示 + 对话历史)
    response = model.invoke(
        [{"role": "system", "content": system_prompt}] + state["messages"]
    )
    return {"messages": response}

Graph 构造

Python 复制代码
workflow = (
    StateGraph(MessagesState)
    .add_node("dialogue_with_memory", call_model)
    .add_edge(START, "dialogue_with_memory")
)

工作流编译

双记忆配置的 Graph:

  • checkpointer:短时对话记忆(聊天上下文)
  • store:长时永久记忆(用户信息)
Python 复制代码
agent = workflow.compile(
    checkpointer=MemorySaver(),
    store=in_memory_store
)

示例代码

Python 复制代码
config1 = {"configurable": {"thread_id": "1", "user_id": "1"}}
logger.info("【第一轮对话:存储记忆】")
for chunk in agent.stream(
        {"messages": [{"role": "user", "content": "你好! 请记住: 我就叫 Monkey Bro"}]},
        config1,
        stream_mode="values"
):
    chunk["messages"][-1].pretty_print()

# 查看存储的记忆
logger.info("\n【已存储的长时记忆】")
for memory in in_memory_store.search(("memories", "1")):
    logger.info(memory.value)

# --------------------------
# 测试2:跨线程读取记忆
# 换thread_id,依然能读取同一user_id的记忆
# --------------------------
config2 = {"configurable": {"thread_id": "3", "user_id": "1"}}
logger.info("\n【第二轮对话:读取记忆】")
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "我的名字叫啥?"}]},
    config2, stream_mode="values"
):
    chunk["messages"][-1].pretty_print()

Running

源码

github

相关推荐
杨艺韬6 小时前
Vite内核解析-第17章 Web Worker 与特殊资源
agent
杨艺韬6 小时前
Vite内核解析-第16章 Environment API
agent
杨艺韬6 小时前
Vite内核解析-第12章 静态资源处理
agent
杨艺韬6 小时前
Vite内核解析-第15章 SSR 与模块运行器
agent
杨艺韬6 小时前
Vite内核解析-第13章 Rolldown 构建引擎
agent
杨艺韬6 小时前
Vite内核解析-第18章 设计模式与架构决策
agent
杨艺韬6 小时前
Vite内核解析-第4章 插件系统与 Hook 机制
agent
杨艺韬6 小时前
Vite内核解析-第6章 模块图与依赖追踪
agent
杨艺韬6 小时前
Vite内核解析-第11章 HTML 转换与入口解析
agent