Langgraph 源码解析 - 状态&&DI
AgentState | configurable | ToolNode | Dependency Injection
Part 1: 核心概念 - 工作台与任务指令
在 LangGraph 中,数据主要通过两种截然不同的方式在系统中流动:AgentState 和 configurable。理解它们的区别是掌握 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)。
- 节点是工人: 每个节点就是一个执行具体任务的函数。
AgentState是输入: 节点接收当前的AgentState作为输入。- 返回字典是更新指令: 节点完成工作后,返回一个字典。这个字典就是对
AgentState的更新指令。 - 框架执行更新: LangGraph 框架接收这个返回的字典,并将其应用到当前的
AgentState上。
所谓的“命令”(比如一个工具调用的 JSON),它本身只是存放在 AgentState 中的数据,路由器(Conditional Edge)可以读取这些数据来决定下一步的走向,但更新 AgentState 这一动作,始终是由节点的返回值触发的。
Part 3: 依赖注入 (DI) - 更优雅的数据传递方式
当我们发现,某个节点需要一个非常 spezifisch 的、与当前上下文相关的参数(比如 tool_call_id)时,传统的 AgentState 和 configurable 方案就显得有些笨拙。这时,依赖注入(DI)就登场了。
普遍的 DI 思想:控制反转 (IoC)
依赖注入是一种设计模式,其核心是“控制反转”。一个对象不应该自己去创建或寻找它所**依赖(Dependency)的东西,而应由外部框架来注入(Injection)**给它。这就像大厨只需要在菜单上写明需要什么食材,助手就会自动把食材递给他,而不需要大厨亲自去仓库翻找。
LangGraph 中的 DI 实现:魔法在于调用者
一个核心的困惑点是:“是不是我的函数被重新定义或改造了才能使用 DI?”
答案:不是。 函数本身是 100%普通的 Python 函数。魔法在于调用它的框架。
这个机制依赖于 Python 的两大特性:
typing.Annotated: Python 内置的类型提示工具,允许我们为类型“贴上”额外的元数据标签。它是 DI 的“声明舞台”。inspect模块: Python 的内省工具,允许框架在运行时检查函数的签名,读取这些Annotated标签。
精确到源码:ToolNode 的 DI 工作流
tool_node.py 1000 行左右。
ToolNode 是 LangGraph 中实现 DI 的一个完美范例。它作为一个“聪明的调用者”或“导演”,在调用你的工具函数(演员)之前,完成了一系列精密的准备工作。
1. 初始化 (ToolNode.__init__) - 准备阶段
当你创建一个 ToolNode 实例并传入工具列表时,它会立刻遍历每个工具函数。
- 动作: 使用内部的辅助函数(如
_get_state_args),通过inspect模块扫描每个函数的签名。 - 目的: 查找所有被特殊“标记类”(如
InjectedToolCallId,InjectedState等InjectedToolArg的子类)注解的参数。 - 结果: 它将这些“注入需求”记录并缓存在
ToolNode实例内部,就像导演提前阅读了所有演员的合同附录一样。
2. 输入解析 (ToolNode._parse_input) - 触发阶段
当图运行到 ToolNode 并接收到一个包含 tool_calls 的 AgentState 时:
- 动作: 此方法被触发,它从状态中提取出最新的
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等上下文信息。你的函数可以在完全不知情的情况下,优雅地接收到所有需要的数据。