
运行时上下文(Runtime context)
什么是运行上下文?
接下来我们继续讲解 LangGraph 的其他核心能力,首先重点介绍运行时上下文。
首先我们来理解上下文的基本定义。上下文,指的是程序运行过程中能够被访问到的数据与环境相关信息。我们此前学习的 state,同样是贯穿程序运行全程、可随时读取的数据,因此 state 也属于上下文的范畴。在 LangGraph 中,上下文主要用来传递各类关键信息,比如用户身份、系统配置参数;后续学习数据库调用时,数据库连接信息、各类密钥,也都可以通过上下文进行传递和读取。除此之外,对话状态、聊天历史记录这类数据,同样能够存放在上下文当中。简单来说,之前所学的 state,就是程序运行阶段可正常访问的一类上下文。
上下文分类
针对上下文,我们可以依据两个维度 进行分类。第一个维度是可变性 ,也就是判断数据在程序运行过程中是否会发生改变。运行过程中内容保持不变的数据,我们称之为静态上下文 ;运行过程中内容会发生变动的数据,则称之为动态上下文 。
第二个维度是生命周期 。如果上下文仅在单次会话、单次程序运行中生效,就叫做运行时上下文 ;如果数据可以跨多次会话持久保存,无论重复运行多少次程序、开启多少轮对话都能保留,这类上下文就是跨会话上下文 。
上下文可按两个维度分类:
| 维度 | 类型 | 描述 | 示例 |
|---|---|---|---|
| 可变性 | 静态上下文 | 运行中不变的数据 | 用户 ID、数据库连接 |
| 动态上下文 | 运行中会变化的数据 | 对话记录、中间结果 | |
| 生命周期 | 运行时上下文 | 单次运行 / 线程有效 | 当前请求的临时数据 |
| 跨会话上下文 | 多次会话持久化 | 用户偏好、历史记录 |
结合这两个分类维度,我们结合实际场景进一步区分。像用户 ID、数据库连接信息,在单次程序运行中不会发生变化,属于静态上下文;而聊天记录、程序运行产生的中间结果与状态,存储在 state 当中,内容会不断变化,属于动态上下文。
从生命周期角度来看,当前请求产生的临时数据,仅在单次运行内有效,结束后数据便会销毁,这就是典型的运行时上下文,我们日常使用的 state 大多属于这类。如果开启多线程并重复执行工作流,也可以通过线程级别的状态还原,重新加载这类数据。而用户偏好设置、长期对话历史这类数据,会被持久化保存,不受单次会话限制,就属于跨会话上下文。
将可变性 与生命周期 两个维度相互组合,LangGraph 中就形成了三类上下文,其中两类是我们此前已经接触过的内容。 第一类是动态运行时上下文 :既具备动态可变的特性,生命周期又仅限单次运行。我们之前讲到的图状态快照、state 都属于这一类。状态快照中存放着 state 数据,内容可以动态修改,且只在单次运行、单一线程内生效,完全符合动态运行时上下文的特征。
第二类是动态跨会话上下文 :数据支持修改、具备动态属性,同时生命周期可以跨越多次会话。我们之前学习的 store 存储模块就对应这类上下文,我们可以对其中的数据执行查询、新增、修改等操作,并且数据能够长期保存,实现跨会话使用。
以上两类内容大家已经有所了解,而我们本篇重点学习的是第三类 ------静态运行时上下文 。这类上下文有两个核心特点:一是数据为静态,程序运行全程不允许修改;二是生命周期仅限单次运行、单一线程。适合存放在这里的数据,都是单线程运行中固定不变的内容,例如用户 ID、系统配置、接口密钥、数据库连接地址(URI)等。
因此,在 LangGraph 中,包含三种上下文:
| 类型 | 可变性 | 生命周期 | 访问方式 |
|---|---|---|---|
| 静态运行时上下文 | 静态 | 单次运行 / 线程 | context 参数传入 |
| 动态运行时上下文 | 动态 | 单次运行 | 图状态对象 |
| 动态跨会话上下文 | 动态 | 跨会话 | 存储(Store) |
这里我们结合过往知识点做回顾。此前使用跨会话持久化功能时,我们需要依靠用户 ID 构建命名空间,以此在 store 中完成数据的查询和存储。当时我们将用户 ID 放置在配置项里,结合现在上下文的知识来看,用户 ID 在单次调用中不会改变,完全适合存入静态运行时上下文。
总结来说,我们之前学过的 checkpoints(检查点)对应动态运行时上下文,store 存储对应动态跨会话上下文,而今天全新学习、需要掌握用法的,就是静态运行时上下文。
场景练习
理解完基础概念后,我们通过一个实战场景练习,进一步区分三类上下文的使用场景。假设我们要开发一款智能旅行规划助手,该助手有五项核心需求:根据用户母语给出对应语言的回答、依据会员等级提供差异化服务、连接旅游数据库查询数据、根据当下季节推荐游玩项目、记忆用户历史查询记录并给出精准建议。
| 数据类型 | 上下文类型 | 为什么 |
|---|---|---|
| 数据库连接 | 静态运行时上下文 | 每次查询都需要,单次运行中不变 |
| 用户语言偏好 | 静态运行时上下文 | 个性化回答,单次运行中不变 |
| 用户会员等级 | 静态运行时上下文 | 服务分级,单次运行中不变 |
| 当前季节 | 静态运行时上下文 | 推荐季节性活动,单次运行中不变 |
| 对话历史 | 动态运行时上下文 | 了解上下文,单次运行中变化 |
| 用户旅行偏好 | 跨会话上下文 | 长期记忆,跨会话 |
对应场景里的六类数据,我们逐一分析归类:
- 数据库连接信息 :每次查询都需要使用,且单次运行中不会改变,归类为静态运行时上下文。
- 用户语言偏好 :单次工作流启动后,用户使用的语言就已确定,运行中不会变更,归类为静态运行时上下文。
- 用户会员等级 :单次调用过程中用户身份固定,等级信息不会改变,归类为静态运行时上下文。
- 当前季节 :单次推荐流程里季节是固定值,不会中途更改,归类为静态运行时上下文。
- 历史对话信息 :对话过程中内容会持续新增、不断变化,且仅在单次会话内生效,归类为动态运行时上下文。
- 用户旅行偏好 :需要长期记忆、跨多次会话使用,归类为动态跨会话上下文。
通过这个案例,我们就能清晰区分静态与动态、运行时与跨会话这四类上下文的差异。掌握概念之后,接下来我们讲解代码层面如何配置和使用静态运行时上下文。
记住:良好的上下文管理是构建复杂、可维护 AI 应用的关键!正确的上下文设计能让我们的 AI 应用:
- ✅ 更高效:避免重复传递不变数据
- ✅ 更智能:基于上下文提供个性化服务
- ✅ 更可维护:清晰的数据边界和职责分离
- ✅ 更易扩展:支持多用户、多场景、长期记忆
配置运行时上下文
定义上下文模式
首先需要定义上下文的数据结构,Python 中主要有两种常用方式:
-
一种是使用
dataclass装饰器,该特性在 Python3.7 及以上版本可用,它能自动为类生成初始化、判等等基础方法,简化类的编写; -
另一种是定义
TypedDict,和我们平时定义state的方式保持一致。
两种方式都可以明确定义上下文包含的字段与数据类型,也是创建静态运行时上下文的标准写法。
接下来结合代码示例,演示在 LangGraph 图结构中使用静态运行时上下文。 我们知道,常规的节点函数会接收 state 参数,state 对应动态运行时上下文,内容支持修改。而想要使用静态运行时上下文,需要给节点函数新增一个 runtime 参数,该参数的类型为 LangGraph 提供的 Runtime,并关联我们自定义的上下文结构。
实操步骤如下:第一步,通过 TypedDict 定义 state,里面存放允许动态修改的数据,比如对话消息列表、用户名;第二步,通过 dataclass 定义静态上下文结构,存放运行中固定不变的数据,例如用户 ID、使用语言,并可以为字段设置默认值。
python
from typing import TypedDict, List, Optional
from dataclasses import dataclass
from langgraph.graph import StateGraph, add_messages
from langgraph.runtime import Runtime # 注意:实际导入路径可能根据版本不同
# 第一步:定义动态运行时上下文(State)- 支持修改
class MyState(TypedDict):
messages: List[str] # 对话消息列表,使用 add_messages 可以实现累加
username: str # 用户名,可在运行中修改
# 第二步:定义静态运行时上下文 - 运行中固定不变
@dataclass
class MyStaticContext:
user_id: str # 用户ID,固定不变
language: str = "zh" # 使用语言,默认中文
# 可以添加更多固定配置
# region: str = "CN"
# subscription_level: str = "free"
在节点中访问上下文
在节点函数中,我们可以同时读取两类上下文:通过 runtime.context 获取静态运行时上下文的数据,比如读取用户使用的语言,根据语言字段判断返回中文问候 "你好" 还是英文问候 "Hello";通过 state 获取动态运行时上下文的数据,比如读取用户名,若未设置则默认显示 "访客"。由于 state 支持修改,我们最终返回新的数据,就能更新状态内容,这也印证了它动态可变的特性。
这种分类存储的方式也更符合开发规范:固定不变的配置、身份信息统一放入静态运行时上下文,可变更的业务数据存入 state,代码逻辑会更加清晰,可读性和可维护性也会大幅提升。
节点函数可通过 runtime 参数访问上下文。
python
# 节点函数:同时接收 state(动态)和 runtime(静态上下文)
def greeting_node(state: MyState, runtime: Runtime[MyStaticContext]) -> dict:
"""
问候节点:根据静态上下文的语言配置和动态上下文的用户名返回问候语
"""
# 从 runtime 获取静态上下文数据
static_context = runtime.context
language = static_context.language
user_id = static_context.user_id
# 从 state 获取动态数据
username = state.get("username", "访客")
# 根据语言选择问候语
if language == "zh":
greeting = f"你好,{username}!用户ID:{user_id}"
elif language == "en":
greeting = f"Hello, {username}! User ID: {user_id}"
elif language == "ja":
greeting = f"こんにちは、{username}さん!ユーザーID:{user_id}"
else:
greeting = f"Hello, {username}! User ID: {user_id}"
# 返回新数据来更新 state(动态运行时上下文支持修改)
return {
"messages": [greeting], # 添加问候消息
"username": username # 可以保持不变,也可以修改
}
def process_node(state: MyState, runtime: Runtime[MyStaticContext]) -> dict:
"""
处理节点:根据静态配置和动态数据执行不同逻辑
"""
static_context = runtime.context
# 可以根据静态配置决定行为
if static_context.language == "zh":
# 中文用户走特定逻辑
return {"messages": ["处理中文用户请求"]}
else:
# 其他语言用户走通用逻辑
return {"messages": ["Processing user request"]}
在图中使用上下文模式
完成节点编写后,下一步是构建图结构。在实例化 StateGraph 构建图时,需要新增 context_schema 参数,传入我们自定义的上下文类,告诉当前图需要依赖该套静态运行时上下文。这里要区分两个配置阶段:数据的静态 / 动态属性 ,是在构建图(StateGraph)阶段定义的;而生命周期(单次运行 / 跨会话) ,则是在图编译(compile)阶段,通过 checkpoint 等配置来指定,两种配置可以自由组合使用。
创建图时传入 context_schema 参数。
python
# 实例化 StateGraph,传入 state 类型和静态上下文类型
graph_builder = StateGraph(
MyState,
context_schema=MyStaticContext # 新增参数,告诉图需要依赖静态运行时上下文
)
# 添加节点
graph_builder.add_node("greeting", greeting_node)
graph_builder.add_node("process", process_node)
# 添加边(示例:顺序执行)
graph_builder.set_entry_point("greeting")
graph_builder.add_edge("greeting", "process")
graph_builder.set_finish_point("process")
# 编译图(这里不启用 checkpoint,所以是单次运行)
# 注意:生命周期(单次运行/跨会话)通过 compile 的参数配置
graph = graph_builder.compile() # 默认单次运行,不持久化状态
运行图时传入上下文
图的节点、边全部配置完成后,调用 compile 方法完成编译,之后就可以执行图逻辑。调用 graph.invoke() 方法时,需要传入两部分内容:第一部分是 state 的初始数据,也就是动态状态的初始化内容;第二部分是 context 字典,传入静态运行时上下文的对应字段,比如用户 ID、使用语言。
python
# 调用 invoke 方法,需要传入两部分内容
# 第一部分:state 的初始数据(动态状态初始化)
initial_state = {
"messages": [], # 初始为空列表
"username": "张三" # 用户名
}
# 第二部分:context 字典(静态运行时上下文)
static_context_data = {
"user_id": "USER_12345",
"language": "zh" # 中文问候
}
# 执行图
result = graph.invoke(
initial_state,
context=static_context_data # 传入静态上下文
)
# 查看结果
print("执行结果:")
print(f"消息列表:{result['messages']}")
print(f"用户名:{result['username']}")
# 示例输出:
# 执行结果:
# 消息列表:['你好,张三!用户ID:USER_12345', '处理中文用户请求']
# 用户名:张三
运行代码后可以看到,节点能够正常读取 context 中的语言配置,自动匹配对应的问候语,这就证明静态运行时上下文可以正常读取和使用。
不同场景的调用示例
python
# 场景1:英文用户,不传初始 state(使用默认空值)
en_result = graph.invoke(
{}, # state 初始数据为空
context={"user_id": "USER_67890", "language": "en"}
)
print(f"英文用户:{en_result['messages']}")
# 输出:英文用户:['Hello, 访客! User ID: USER_67890', 'Processing user request']
# 场景2:日文用户
ja_result = graph.invoke(
{"username": "田中", "messages": []},
context={"user_id": "USER_111", "language": "ja"}
)
print(f"日文用户:{ja_result['messages']}")
# 输出:日文用户:['こんにちは、田中さん!ユーザーID:USER_111', 'Processing user request']
# 场景3:同一个静态上下文可以用于多个不同 state
common_context = {"user_id": "USER_999", "language": "zh"}
user1_result = graph.invoke({"username": "李四", "messages": []}, context=common_context)
user2_result = graph.invoke({"username": "王五", "messages": []}, context=common_context)
带 Checkpoint 的跨会话场景
python
from langgraph.checkpoint import MemorySaver
# 编译时启用 checkpoint,实现跨会话持久化
checkpointer = MemorySaver()
graph_with_persistence = graph_builder.compile(checkpointer=checkpointer)
# 第一次调用,传入配置ID,状态会被保存
config = {"configurable": {"thread_id": "user_session_001"}}
result1 = graph_with_persistence.invoke(
{"username": "赵六", "messages": []},
context={"user_id": "USER_001", "language": "zh"},
config=config
)
# 第二次调用,使用相同的 thread_id,可以获取之前的状态
result2 = graph_with_persistence.invoke(
{}, # 可以不传初始 state,会从 checkpoint 恢复
context={"user_id": "USER_001", "language": "zh"},
config=config
)
# 注意:静态上下文每次都需要重新传入,因为它不会保存在 checkpoint 中
最后补充两个使用要点:第一,调用时 state 的初始数据可以不传,不传则默认全部为空,后续运行中可自由修改;第二,静态运行时上下文必须每次调用都主动传入,因为它不支持运行中修改,系统无法自动维护,只有每次调用时传递完整数据,节点才能正常读取使用。
【完整示例】
python
import operator
from typing import TypedDict, List, Optional, Annotated
from dataclasses import dataclass
from langgraph.graph import StateGraph, add_messages
from langgraph.runtime import Runtime # 注意:实际导入路径可能根据版本不同
# 第一步:定义动态运行时上下文(State)- 支持修改
class MyState(TypedDict):
messages: Annotated[List[str], operator.add]
username: str # 用户名,可在运行中修改
# 第二步:定义静态运行时上下文 - 运行中固定不变
@dataclass
class MyStaticContext:
user_id: str # 用户ID,固定不变
language: str = "zh" # 使用语言,默认中文
# 可以添加更多固定配置
# region: str = "CN"
# subscription_level: str = "free"
# 节点函数:同时接收 state(动态)和 runtime(静态上下文)
def greeting_node(state: MyState, runtime: Runtime[MyStaticContext]) -> dict:
"""
问候节点:根据静态上下文的语言配置和动态上下文的用户名返回问候语
"""
# 从 runtime 获取静态上下文数据
static_context = runtime.context
language = static_context.language
user_id = static_context.user_id
# 从 state 获取动态数据
username = state.get("username", "访客")
# 根据语言选择问候语
if language == "zh":
greeting = f"你好,{username}!用户ID:{user_id}"
elif language == "en":
greeting = f"Hello, {username}! User ID: {user_id}"
elif language == "ja":
greeting = f"こんにちは、{username}さん!ユーザーID:{user_id}"
else:
greeting = f"Hello, {username}! User ID: {user_id}"
# 返回新数据来更新 state(动态运行时上下文支持修改)
return {
"messages": [greeting], # 添加问候消息
"username": username # 可以保持不变,也可以修改
}
def process_node(state: MyState, runtime: Runtime[MyStaticContext]) -> dict:
"""
处理节点:根据静态配置和动态数据执行不同逻辑
"""
static_context = runtime.context
# 可以根据静态配置决定行为
if static_context.language == "zh":
# 中文用户走特定逻辑
return {"messages": ["处理中文用户请求"]}
else:
# 其他语言用户走通用逻辑
return {"messages": ["Processing user request"]}
# 实例化 StateGraph,传入 state 类型和静态上下文类型
graph_builder = StateGraph(
MyState,
context_schema=MyStaticContext # 新增参数,告诉图需要依赖静态运行时上下文
)
# 添加节点
graph_builder.add_node("greeting", greeting_node)
graph_builder.add_node("process", process_node)
# 添加边(示例:顺序执行)
graph_builder.set_entry_point("greeting")
graph_builder.add_edge("greeting", "process")
graph_builder.set_finish_point("process")
# 编译图(这里不启用 checkpoint,所以是单次运行)
# 注意:生命周期(单次运行/跨会话)通过 compile 的参数配置
graph = graph_builder.compile() # 默认单次运行,不持久化状态
# 调用 invoke 方法,需要传入两部分内容
# # 第一部分:state 的初始数据(动态状态初始化)
# initial_state = {
# "messages": [], # 初始为空列表
# "username": "张三" # 用户名
# }
#
# # 第二部分:context 字典(静态运行时上下文)
# static_context_data = {
# "user_id": "USER_12345",
# "language": "ja" # 中文问候
# }
#
# # 执行图
# result = graph.invoke(
# initial_state,
# context=static_context_data # 传入静态上下文
# )
#
# # 查看结果
# print("执行结果:")
# print(f"消息列表:{result['messages']}")
# print(f"用户名:{result['username']}")
# 场景1:英文用户,不传初始 state(使用默认空值)
en_result = graph.invoke(
{}, # state 初始数据为空
context={"user_id": "USER_67890", "language": "en"}
)
print(f"英文用户:{en_result['messages']}")
# 输出:英文用户:['Hello, 访客! User ID: USER_67890', 'Processing user request']
# 场景2:日文用户
ja_result = graph.invoke(
{"username": "田中", "messages": []},
context={"user_id": "USER_111", "language": "ja"}
)
print(f"日文用户:{ja_result['messages']}")
# 输出:日文用户:['こんにちは、田中さん!ユーザーID:USER_111', 'Processing user request']
# 场景3:同一个静态上下文可以用于多个不同 state
common_context = {"user_id": "USER_999", "language": "zh"}
user1_result = graph.invoke({"username": "李四", "messages": []}, context=common_context)
user2_result = graph.invoke({"username": "王五", "messages": []}, context=common_context)
以上就是 LangGraph 中运行时上下文的完整概念、分类、场景应用,以及静态运行时上下文的代码配置、节点访问、调用执行的全部用法。
在工具中访问上下文
除了在节点中使用上下文以外,工具同样也支持使用上下文,不过工具里的使用方式和节点存在区别,下面我们详细介绍具体用法。
先做一个名称约定,方便后续讲解和理解:后续,我们将静态运行时上下文 统一称作上下文(context) ,把动态运行时上下文 依旧沿用之前的叫法,称作状态(state)。大家需要分清两个概念的底层含义,这能帮助我们更好地区分和使用。
工具是调用外部系统、API、数据库交互或执行计算的功能。因此,对于用户身份、配置参数、数据库连接、API 密钥等这类调用 API 的参数信息和配置信息,则需要传递给工具。
上下文对工具的重要性:
- 个性化响应:根据用户上下文提供定制化回答
- 权限控制:基于用户身份限制工具访问
- 状态感知:工具可以根据当前状态决定行为
- 依赖注入:避免硬编码配置,提高可测试性
基本用法
现在我们新建案例来演示工具中上下文的使用。首先完成基础结构的定义,第一步先定义状态。我们创建消息状态结构,内部设置消息列表字段,同时新增user_name字段,该字段内容在运行过程中允许发生变动,默认设置为空字符串。
python
from typing import List, Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
# 创建消息状态结构
class MyState(TypedDict):
messages: Annotated[List, add_messages] # 消息列表字段
user_name: str # 用户名,运行过程中允许变动,默认设置为空字符串
接着定义上下文结构,这里使用数据类来实现,上下文当中仅保留user_id这一个字段即可,用来存放单次运行中固定不变的用户标识信息。
python
from dataclasses import dataclass
# 使用数据类实现上下文结构
@dataclass
class MyContext:
user_id: str # 单次运行中固定不变的用户标识信息
基础结构定义完成后,开始编写业务逻辑。首先创建大模型相关节点,这个节点的作用是让大语言模型绑定工具,并判断是否需要调用工具。该节点仅接收state状态参数,不在节点内部读取上下文,我们把读取上下文和状态的逻辑全部放在工具中实现。
在编写节点之前,需要先初始化大模型并绑定工具。我们选用对应的模型,同时提前定义好待绑定的工具。这里我们设计一个search搜索工具,模拟天气查询的业务场景。先为工具添加标准注解,初步搭建工具框架。这个工具无需额外入参,核心逻辑是模拟接口调用,直接返回模拟结果,例如查询到天气为晴天,气温 15 至 20 度。
定义一个带有运行时信息的工具如下所示:
python
from langchain.tools import tool, ToolRuntime
# 设计一个search搜索工具,模拟天气查询业务场景
@tool
def search_weather(runtime: ToolRuntime[MyContext, MyState]) -> str:
"""
查询天气信息。
当用户询问某个地方的天气时,调用此工具获取天气数据。
返回内容包括天气状况和温度范围。
"""
# 核心逻辑:模拟接口调用,直接返回模拟结果
return "查询到天气:晴天,气温 15 至 20 度"
这里重点来看工具如何读取上下文与状态。想要在工具中使用相关数据,需要新增ToolRuntime类型的参数,这也是工具和节点的核心区别 :节点使用Runtime,工具则使用ToolRuntime。借助这个参数,我们既可以读取静态上下文,也能读取动态状态。
| 函数类型 | 签名 | 为什么这样设计 |
|---|---|---|
| 节点 | fn(state, runtime) |
state 是节点的核心输入输出,设计为显式参数更清晰 |
| 工具 | fn(args, runtime) |
工具的参数由 LLM 决定(如 city: str),state 不是工具的"自然"输入,所以放到 runtime.state 里作为"额外上下文" |
工具可以通过 ToolRuntime 参数访问运行时信息。这个参数,为工具提供包括:
State:图状态数据Context:静态上下文Store:持久化存储等
使用 ToolRuntime 时,只需在工具签名中添加 runtime: ToolRuntime,它会自动注入。调用时,无需手动传输。
通过runtime.context.user_id就能获取上下文里的用户 ID;通过runtime.state可以拿到全局状态,进而读取到状态中的user_name。本质上来说,context和state都属于广义的上下文范畴,前者对应静态数据,后者对应动态数据,只是在 LangGraph 中我们习惯将动态数据单独称作状态。
为了直观验证取值效果,我们在工具内添加日志打印语句,记录当前发起查询的用户 ID 和用户名,模拟线上环境的日志记录功能。至此,工具中读取上下文、读取状态的代码就编写完成了。
python
from langchain.tools import tool, ToolRuntime
@tool
def search_weather(runtime: ToolRuntime[MyContext, MyState]) -> str:
"""
查询天气信息。
当用户询问某个地方的天气时,调用此工具获取天气数据。
返回内容包括天气状况和温度范围。
"""
# 通过 runtime.context.user_id 获取上下文里的用户 ID
user_id = runtime.context.user_id
# 通过 runtime.state 拿到全局状态,读取 user_name
user_name = runtime.state.get("user_name", "匿名用户")
# 添加日志打印,验证取值效果
print(f"【工具日志】用户ID: {user_id}, 用户名: {user_name}")
return "查询到天气:晴天,气温 15 至 20 度"
完成工具开发后,继续编写大模型节点的逻辑。该节点会调用初始化好的大模型,先设置系统提示词,告知模型支持调用工具查询天气,再拼接历史消息一并传入模型。模型执行后会返回 AI 消息,我们将这条消息追加到消息列表中,完成节点逻辑编写。
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
def llm_node(state: MyState) -> dict:
"""大模型节点:判断是否需要调用工具"""
# 初始化大模型并绑定工具
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
model_with_tools = model.bind_tools([search_weather])
# 设置系统提示词
system_prompt = SystemMessage(
content="你是一个助手,支持调用工具查询天气。"
)
# 拼接历史消息
messages = [system_prompt] + state.get("messages", [])
# 调用大模型
response = model_with_tools.invoke(messages)
# 将AI消息追加到消息列表中
return {"messages": [response]}
结构、工具、节点全部就绪后,开始构建并编译图。实例化StateGraph时,除了传入定义好的状态,还必须指定context_schema绑定自定义上下文结构,这是使用静态上下文的必要配置。
随后依次向图中添加两个节点:第一个是刚刚编写的大模型节点,用于判断是否调用工具;第二个是工具节点,需要使用框架提供的ToolNode来封装我们定义的search工具,这个节点的作用是真正执行工具逻辑。两个节点分工明确:大模型节点负责决策,工具节点负责执行。
接下来配置节点之间的连线。首先设置起始节点指向大模型节点。由于流程存在分支,这里需要配置条件边 :模型返回的消息中如果包含工具调用指令,就跳转到工具节点执行查询;如果没有工具调用指令,就直接结束流程。我们可以直接使用 LangGraph 内置的tools_condition方法实现该判断逻辑,这个方法会自动检测消息内的工具调用标识,并根据结果分发流程。我们做好路径映射,判定需要调用工具时,流程走向工具节点;无需调用工具则直接终止。
工具执行完成后,还需要配置一条普通连线,让工具节点重新回到大模型节点。原因在于:工具执行后会产生新的工具消息,此时需要把历史所有消息(用户提问、模型调用指令、工具返回结果)再次交给大模型,由模型整合全部信息,生成最终回复给用户的内容。
连线配置完毕,直接编译图,本次编译暂不配置检查点相关功能。
python
from langgraph.graph import StateGraph, END, START
from langgraph.prebuilt import ToolNode, tools_condition
# 实例化StateGraph,传入状态,并指定context_schema绑定上下文结构
graph_builder = StateGraph(
MyState,
context_schema=MyContext # 必须指定,这是使用静态上下文的必要配置
)
# 添加两个节点
graph_builder.add_node("llm", llm_node) # 大模型节点(决策)
graph_builder.add_node("tools", ToolNode([search_weather])) # 工具节点(执行)
# 设置起始节点指向大模型节点
graph_builder.add_edge(START, "llm")
# 配置条件边:使用内置tools_condition判断是否需要调用工具
graph_builder.add_conditional_edges(
"llm",
tools_condition,
{
"tools": "tools", # 需要调用工具 → 跳转到工具节点
END: END # 无需调用工具 → 直接结束
}
)
# 配置普通连线:工具执行完成后,回到大模型节点
graph_builder.add_edge("tools", "llm")
# 编译图(暂不配置检查点)
graph = graph_builder.compile()
图编译完成后,采用流式调用的方式执行图逻辑。
python
from langchain_core.messages import HumanMessage
# 准备输入参数
# 第一组:状态的初始化数据
initial_state = {
"messages": [HumanMessage(content="今天西安天气如何?")],
"user_name": "小明"
}
# 第二组:上下文数据
static_context = {
"user_id": "USER_12345"
}
# 流式调用执行
for event in graph.stream(initial_state, context=static_context):
for node_name, state_update in event.items():
print(f"节点: {node_name}")
if "messages" in state_update:
print(f"消息: {state_update['messages'][-1].content}")
python
节点: llm
消息: 好的,我来查询一下西安的天气信息。
【工具日志】用户ID: USER_12345, 用户名: 小明
节点: tools
消息: 查询到天气:晴天,气温 15 至 20 度
节点: llm
消息: 今天西安的天气情况如下:
- **天气状况**:☀️ 晴天
- **温度范围**:15°C ~ 20°C
天气不错,适合外出活动,不过早晚温差较大,建议带件外套哦!
进程已结束,退出代码为 0
流式调用会逐个输出每个节点的执行结果,能清晰看到整个流程的运转过程。调用时需要传入两组参数:第一组是状态的初始化数据,在消息列表中传入用户提问 "今天西安天气如何?",同时设置user_name为 "小明";第二组是上下文数据,以字典形式传入user_id。
在结果接收环节,遍历流式返回的数据。流式数据会以节点名称、状态更新内容的元组形式返回,我们依次打印节点名称和最新的消息内容,查看每一步的执行结果。
运行代码后可以清晰看到完整的执行流程:首先执行大模型节点,模型生成带有工具调用指令的消息,触发工具调用;接着执行工具节点,日志成功打印出我们传入的user_id和user_name,证明工具正常读取到了上下文与状态,同时返回模拟的天气查询结果;最后流程回到大模型节点,模型整合所有信息,输出最终的自然语言回答。
通过这个案例就能明确:在工具中借助ToolRuntime参数,既可以读取静态的上下文数据,也能读取动态的状态数据。
【完整示例】
构建一个支持搜索的 AI 系统,假设调用搜索 API 需要用户数据作为参数,则需要向工具中传入相关信息。关键步骤如下:
- 定义状态、上下文结构
- 定义工具节点(
ToolNode)、定义 LLM 节点 - 构建并编译图,需加入状态和上下文参数
- 执行并验证结果
python
from typing import List, Annotated
from langchain_core.messages import SystemMessage, HumanMessage
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
from langgraph.graph import StateGraph, END, START
from langgraph.prebuilt import ToolNode, tools_condition
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
# 设置 DeepSeek API Key
os.environ["DEEPSEEK_API_KEY"] = "sk-............"
# 创建消息状态结构
class MyState(TypedDict):
messages: Annotated[List, add_messages] # 消息列表字段
user_name: str # 用户名,运行过程中允许变动,默认设置为空字符串
# 使用数据类实现上下文结构
@dataclass
class MyContext:
user_id: str # 单次运行中固定不变的用户标识信息
@tool
def search_weather(runtime: ToolRuntime[MyContext, MyState]) -> str:
"""
查询天气信息。
当用户询问某个地方的天气时,调用此工具获取天气数据。
返回内容包括天气状况和温度范围。
"""
# 通过 runtime.context.user_id 获取上下文里的用户 ID
user_id = runtime.context.user_id
# 通过 runtime.state 拿到全局状态,读取 user_name
user_name = runtime.state.get("user_name", "匿名用户")
# 添加日志打印,验证取值效果
print(f"【工具日志】用户ID: {user_id}, 用户名: {user_name}")
return "查询到天气:晴天,气温 15 至 20 度"
def llm_node(state: MyState) -> dict:
"""大模型节点:判断是否需要调用工具"""
# 初始化大模型并绑定工具
# 使用 OpenAI 兼容方式调用 DeepSeek
model = ChatOpenAI(
model="deepseek-chat", # 或 deepseek-reasoner
temperature=0,
openai_api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/v1"
)
model_with_tools = model.bind_tools([search_weather])
# 设置系统提示词
system_prompt = SystemMessage(
content="你是一个助手,支持调用工具查询天气。"
)
# 拼接历史消息
messages = [system_prompt] + state.get("messages", [])
# 调用大模型
response = model_with_tools.invoke(messages)
# 将AI消息追加到消息列表中
return {"messages": [response]}
# 实例化StateGraph,传入状态,并指定context_schema绑定上下文结构
graph_builder = StateGraph(
MyState,
context_schema=MyContext # 必须指定,这是使用静态上下文的必要配置
)
# 添加两个节点
graph_builder.add_node("llm", llm_node) # 大模型节点(决策)
graph_builder.add_node("tools", ToolNode([search_weather])) # 工具节点(执行)
# 设置起始节点指向大模型节点
graph_builder.add_edge(START, "llm")
# 配置条件边:使用内置tools_condition判断是否需要调用工具
graph_builder.add_conditional_edges(
"llm",
tools_condition,
{
"tools": "tools", # 需要调用工具 → 跳转到工具节点
END: END # 无需调用工具 → 直接结束
}
)
# 配置普通连线:工具执行完成后,回到大模型节点
graph_builder.add_edge("tools", "llm")
# 编译图(暂不配置检查点)
graph = graph_builder.compile()
# 准备输入参数
# 第一组:状态的初始化数据
initial_state = {
"messages": [HumanMessage(content="今天西安天气如何?")],
"user_name": "小明"
}
# 第二组:上下文数据
static_context = {
"user_id": "USER_12345"
}
# 流式调用执行
for event in graph.stream(initial_state, context=static_context):
for node_name, state_update in event.items():
print(f"节点: {node_name}")
if "messages" in state_update:
print(f"消息: {state_update['messages'][-1].content}")
总结一下上下文的整体使用方式:我们可以单独定义类来存放静态上下文数据,这类数据会在整个工作流的运行过程中全局可访问。不管是图中的普通节点,还是自定义工具,都能够按照对应语法读取上下文和状态。以上就是 LangGraph 中上下文完整的使用方法。