Langgraph 源码解析 - 状态&&DI

AgentState | configurable | ToolNode | Dependency Injection

Part 1: 核心概念 - 工作台与任务指令

在 LangGraph 中,数据主要通过两种截然不同的方式在系统中流动:AgentStateconfigurable。理解它们的区别是掌握 LangGraph 的第一步。

AgentState: 智能体的动态工作台

AgentState 是图(Graph)的“共享内存”或“动态工作台”。

  • 目的: 管理图在执行过程中动态、可变的数据。它是智能体思考、记录和迭代的地方。
  • 生命周期: 在图的整个执行过程中持续存在,并被各个节点(Node)读取和修改
  • 类比: 一个智能体用来完成任务的白板或草稿纸
  • 典型内容:
    • messages: 持续追加的对话历史。
    • intermediate_steps: 工具调用的中间结果。
    • 任何自定义的、需要在节点间传递和更新的工作数据。
  • 更新机制: 节点函数返回一个字典,LangGraph 框架使用这个字典来更新(update)当前的状态。对于配置了特殊 Reducer(如 add_messages)的字段,更新行为是追加而非覆盖。

configurable: 不可变的运行时指令

configurable 是在调用图时,从外部传入的一组静态、不可变的参数。

  • 目的: 在任务开始时,为这一次特定的运行提供配置和环境变量。
  • 生命周期: 在单次 .invoke().stream() 调用期间保持不变,是只读的。
  • 类比: 智能体开始工作前收到的一次性任务指令单或工具箱
  • 典型内容:
    • user_id, thread_id: 用于多租户和会话管理。
    • model_name: 动态选择要使用的大语言模型。
    • system_prompt: 动态定制智能体的角色或指令。
  • 访问方式: 节点函数通过在签名中声明 config: RunnableConfig 参数来接收,并通过 config['configurable'] 来访问。

核心困惑点:State vs. configurable

特性AgentState (工作台)configurable (任务指令单)
读写性可读可写 (Mutable)只读 (Immutable)
来源内部生成和流转外部传入
目的记录过程结果定义前提环境
数据对话历史、中间步骤、动态数据用户 ID、会话 ID、模型名称、API 密钥

Part 2: 动作的执行者 - 谁在更新工作台?

一个常见的误解是,是否存在一个特定的“Command”对象来更新 AgentState

答案是:没有。

真正的更新机制是:节点的返回值(return

  1. 节点是工人: 每个节点就是一个执行具体任务的函数。
  2. AgentState 是输入: 节点接收当前的 AgentState 作为输入。
  3. 返回字典是更新指令: 节点完成工作后,返回一个字典。这个字典就是对 AgentState 的更新指令。
  4. 框架执行更新: LangGraph 框架接收这个返回的字典,并将其应用到当前的 AgentState 上。

所谓的“命令”(比如一个工具调用的 JSON),它本身只是存放在 AgentState 中的数据,路由器(Conditional Edge)可以读取这些数据来决定下一步的走向,但更新 AgentState 这一动作,始终是由节点的返回值触发的。


Part 3: 依赖注入 (DI) - 更优雅的数据传递方式

当我们发现,某个节点需要一个非常 spezifisch 的、与当前上下文相关的参数(比如 tool_call_id)时,传统的 AgentStateconfigurable 方案就显得有些笨拙。这时,依赖注入(DI)就登场了。

普遍的 DI 思想:控制反转 (IoC)

依赖注入是一种设计模式,其核心是“控制反转”。一个对象不应该自己去创建或寻找它所**依赖(Dependency)的东西,而应由外部框架来注入(Injection)**给它。这就像大厨只需要在菜单上写明需要什么食材,助手就会自动把食材递给他,而不需要大厨亲自去仓库翻找。

LangGraph 中的 DI 实现:魔法在于调用者

一个核心的困惑点是:“是不是我的函数被重新定义或改造了才能使用 DI?”

答案:不是。 函数本身是 100%普通的 Python 函数。魔法在于调用它的框架

这个机制依赖于 Python 的两大特性:

  1. typing.Annotated: Python 内置的类型提示工具,允许我们为类型“贴上”额外的元数据标签。它是 DI 的“声明舞台”。
  2. inspect 模块: Python 的内省工具,允许框架在运行时检查函数的签名,读取这些 Annotated 标签。

精确到源码:ToolNode 的 DI 工作流

tool_node.py 1000 行左右。

ToolNode 是 LangGraph 中实现 DI 的一个完美范例。它作为一个“聪明的调用者”或“导演”,在调用你的工具函数(演员)之前,完成了一系列精密的准备工作。

1. 初始化 (ToolNode.__init__) - 准备阶段

当你创建一个 ToolNode 实例并传入工具列表时,它会立刻遍历每个工具函数。

  • 动作: 使用内部的辅助函数(如 _get_state_args),通过 inspect 模块扫描每个函数的签名。
  • 目的: 查找所有被特殊“标记类”(如 InjectedToolCallIdInjectedStateInjectedToolArg 的子类)注解的参数。
  • 结果: 它将这些“注入需求”记录并缓存在 ToolNode 实例内部,就像导演提前阅读了所有演员的合同附录一样。

2. 输入解析 (ToolNode._parse_input) - 触发阶段

当图运行到 ToolNode 并接收到一个包含 tool_callsAgentState 时:

  • 动作: 此方法被触发,它从状态中提取出最新的 tool_calls 列表。
  • 关键: 对于列表中的每一个 tool_call,它都会调用 self.inject_tool_args 方法,准备开始注入。

3. 参数注入 (ToolNode.inject_tool_args & BaseTool logic) - 魔法核心

这是整个 DI 机制的心脏。

  • 输入: 它接收一个 tool_call 字典,其中包含了 name, args, 和最重要的 id
  • 逻辑:
    • ToolNode 会调用 langchain-core 中 BaseTool 的注入逻辑。
    • 这个底层实现知道,需要去匹配在阶段一中记录下来的“注入需求”。
    • 当它发现一个参数被 Annotated[..., InjectedToolCallId] 标记时,它就会从当前的 tool_call 字典中提取出 id 的值
    • 然后,它将这个 id 值准备好,用于填充那个被标记的参数。
    • 类似地,_inject_state_inject_store 方法也会被调用,为其他类型的注入标记(InjectedState, InjectedStore)提供相应的值。

4. 工具执行 (ToolNode._run_one) - 表演阶段

  • 动作: ToolNode 调用工具的 .invoke() 方法。
  • 结果: 此时,传递给你的工具函数的参数已经是一个“全家福”了:既包含了来自 LLM 的 args,也包含了由框架在阶段三中精准注入的 tool_call_id 等上下文信息。你的函数可以在完全不知情的情况下,优雅地接收到所有需要的数据。