目录
[5.Threads 会话id](#5.Threads 会话id)
一、基础概念
1.状态:定义的变量类
2.节点:函数
3.边:连接函数
(1)正常边
(2)条件边:对应条件函数
# 事件:节点
graph_builder.add_conditional_edges('processor', route_tools, {"tj_1": "node_b", "tj_2": "node_a"})
# 这里的条件边,如果函数返回的就是可用节点名,则不需要字典说明,如果函数返回的是自定义的字典,则需要字典说明
builder.add_conditional_edges(START, continue_to_jokes)
(3)入口点
(4)条件入口点
二、LangGraph的高级特性
1.Reducer消息添加
-
把每次节点return的消息,都追加到消息列表
-
消息存储只在当前的图中有效
-
对当前新的值进行返回,并合并之后的值,类似消息记录
-
自定义规约器
class ChatState(TypedDict):
messages: Annotated[Sequence[AnyMessage], operator.add]
2.Send机制:[send(节点,参数)]
-
简单的send类似动态条件边
-
随时给多个节点发任务
-
支持并行
-
动态创建节点
定义一个函数continue_to_jokes,接受一个OverallState类型的参数state
def continue_to_jokes(state: OverallState):
# 返回一个Send对象的列表,每个对象包含一个"generate_joke"的node和传递给node的状态
# subjects中有几个元素,就会有几个Send对象
return [Send("generate_joke", {"subject": s}) for s in state['subjects']]创建一个StateGraph对象builder,传入OverallState类型
builder = StateGraph(OverallState)
LangGraph 把 Send 的第二个参数原封不动地当作节点函数的入参,因此:
Send("generate_joke", {"subject": "cats"})
会让 "generate_joke" 节点收到的 state 就是:
{"subject": "cats"} # 只有这一个键
添加一个名为"generate_joke"的节点,节点执行一个lambda函数,生成一个关于主题的笑话
匿名函数
builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
3.Command节点跳转
-
把图暂停、改状态、跳到别的节点、甚至把子图重新派发到别的 worker
-
加强版条件边
-
可以结合人类的干预
graph=Command.PARENT, # 指定跳转的父图
goto=goto, # 跳转到子图相应节点
resume= # 给被暂停的interrupt节点传递数据,
update={"foo": value}, # 更新值的内容,value是占位符
4.checkpointer持久化
- 保存图中state的状态,state更新一次,保存一次
- 可以跨多次invoke调用拿到消息
- 必须结合 config使用
- 父图设置了检查点,子图一样具有检查点
- 参数:
config:配置
metadata:与此检查点相关的元数据
values:此时状态的通道
next:图中下一个要执行的节点名称元组
tasks:包含有关要执行的下个任务的RregelTask对象的元组
5.Threads 会话id
- 保证一轮连续对话
- 一次从起始节点出发、沿着图里的边一步一步往下走的完整"流程实例"。
6.Configuration节点内部配置
- 比如切换某个大模型
7.interupt中断
- 需要结合checkpointer,保证每次图状态的记忆
- 结合config,确定不同用户的状态
- 结合command,给中断点发送消息,让图继续运行
- 写了interupt,就得写两次调用,第一次运行到节点等待,第二次传递值得到数据
- 人工输入,与模型流程无关
- 在图中的某个节点,使用interup运行到该节点,操作暂停,调用这个图的时候,需要通过command的resume传递一个消息给暂停的节点,让图继续运行
8.Subgraph子图
- 父图和子图有共享字段,父图中可添加子图节点
- 如果没有共享字段,必须添加一个节点函数,父图调用子图
- 子图作为父图的一个节点,子图到父图可以通过command跳跃
9.Graphviz图可视化
from IPython.display import Image, display
"""可视化图"""
#方式一:需要单独安装graphviz
# 方法:从官网下载
# 访问 Graphviz官网 https://graphviz.org/download/
# 下载Windows版本的安装包
# 运行安装程序并按照提示完成安装
# 将Graphviz的bin目录添加到系统PATH环境变量中(通常是C:\Program Files\Graphviz\bin)
# 并且请用try块包裹下面的语句,因为执行会报错,但是不影响图片的生成
try:
display(Image(app.get_graph().draw_png(output_file_path='./可视化图1.png')))
except:
pass
#方式二:不需要额外安装软件,但是访问网址mermaid.ink非常容易失败,开启科学上网比较容易成功
display(Image(app.get_graph().draw_mermaid_png(output_file_path='./可视化图2.png')))
10.ToolNode工具节点
-
工具节点调用工具,实现循环调用
tools:有哪些工具
name:工具箱节点名字
tags:给工具箱贴标签
handle_tool_errors:工具抛错处理
messages_key:消息存在哪个字段
wrap_tool_call:执行前拦截/包装(同步)
awrap_tool_call:执行前拦截/包装(异步)
三、对图的细粒度控制
1.人机交互
2.retry=RetryPolicy节点重试策略
builder.add_node("query_database",query_database,retry=RetryPolicy(retry_on=sqlite3.OperationalError))
builder.add_node("model", call_model, retry=RetryPolicy(max_attempts=5))
|----------------------|------------------------------------------|-----------------------------------|
| initial_interval | 第一次失败后"等几秒"再试。 | 0.5 → 等 0.5 秒。 |
| backoff_factor | 每多失败一次,等待时间乘以这个数。 | 2.0 → 第二次 1 s,第三次 2 s,第四次 4 s ... |
| max_interval | "最长能等多久"的上限,防止无限翻倍。 | 128 → 不管失败多少次,最多等 128 s。 |
| max_attempts | 总共尝试次数(含第一次)。 | 3 → 第一次 + 最多再重试 2 次。 |
| jitter | 是否加随机扰动,避免所有节点同时重试造成" thundering herd "。 | True → 在计算出的等待时间上随机 ± 一点。 |
| retry_on | 只有这些异常才重试;其余异常直接抛给用户。 | 默认只认 Exception 的子集(网络超时、5xx 等); |
def query_database(state):
try:
return real_work(state)
except Exception as e:
print('>>> 捕获到异常', type(e).__module__+'.'+type(e).__qualname__, e)
raise # 重新抛出,方便后面细化
# 临时全捕获,观察
retry=RetryPolicy(retry_on=lambda e: True) # 啥都重试
3.同一用户所有消息的持久化存储
- 利用user_id 和thread_id
- 除了线程id,还需要引入外部存储
| ID | 谁来生成 | 谁来传递 | 常见做法 |
|---|---|---|---|
| user_id | ① 登录成功后后端把用户主键(UUID)写进 JWT / Session | ① 客户端:每次请求在 Header/Body 里带上 ② 后端网关:JWT 解析后把 sub 字段注入 RunnableConfig |
"只要用户不换账号就永远不变" |
| thread_id | ① 前端"新建对话"按钮点击时 uuidv4() 生成一个 ② 或者后端在创建会话记录时返回 |
① 前端:放在每次对话请求的 JSON 里 ② 后端:建立 WebSocket 时把 thread_id 写进 URL Path 参数 |
"每次新建聊天就换" |
-
checkpoint + thread_id = 同一次聊天的"存档槽"
-
向量库 + user_id = 用户终身"知识库"
-
和RunnableWithMessageHistory的区别,这个是把所有的对话全部传递进去
prefix,key的形式存储,实现用户消息隔离
后续查找搜索,使用相似度搜索namespace = ("memories", user_id)
搜索
memories = store.search(namespace, query=str(state["messages"][-1].content))
存储
store.put(namespace, str(uuid.uuid4()), {"data": memory})
-
关于user_id和jwt权限
- 客户端 → 提交登录凭证(手机+密码)
请求头:Content-Type: application/json
请求体:{"phone":"13812345678","password":"123456"}
- 后端 → 查库拿到主键
SELECT id FROM users WHERE phone='13812345678' AND pwd=...;
假设 id = 42
- 后端 → 生成 JWT
payload = {"sub":42,"exp":1234567890}
JWT = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQyLCJleHAiOjEyMzQ1Njc4OTB9.xxxxx
- 后端 → 返回 JWT
响应体:{"token":"eyJ..."}
- 客户端 → 后续请求带 JWT
请求头:Authorization: Bearer eyJ...
- 登录流程
-
登录前
请求头里什么都不用带(或只带一次性的登录凭证:手机+密码、验证码、OAuth code)。
-
登录后
后端把 42 写进 JWT 的
sub字段,把完整的 JWT 字符串 返回给客户端。客户端后续请求只需在 Header 写:
Authorization: Bearer <JWT字符串> -
后端接到请求
验签并解析 JWT → 拿到
sub=42→ 这就是user_id,再注入RunnableConfig即可。
4.checkpoint持久化存储到数据库
-
可以使用自带插件
-
任何中断、重启、新开 Python 进程,只要
thread_id相同,对话断点能续上。 -
取出消息由invoke的时候自动去拿
5.消息的摘要和删除RemoveMessage处理
-
摘要发生在**"把消息送进 LLM 之前"**这一步。
-
删除消息是根据删除消息列表里面对应id实现的
| 维度 | RunnableWithMessageHistory | LangGraph 摘要模式 |
|---|---|---|
| 存储内容 | 完整消息列表(逐条 Human/AI) | 1 段摘要 + 最近 2 条消息 |
| Token 增长 | 线性增加 → 易爆表 | 几乎恒定(只留最新句) |
| 跨会话 | 同 session_id 可续聊 |
同 thread_id 可续聊 |
| 信息丢失 | 无 | 仅保留摘要,细节被丢弃 |
| 实现方式 | 链外层自动帮你拼消息 | 图节点里手动总结 + RemoveMessage |
| 适用场景 | 短对话、快速接入 | 长对话、Token 敏感、需断点续跑 |
图的方法调用
|---------------------------------------------------------|-----------------|-------------------|
| app.get_state(config) | 读取最新快照 | 调试、手动改状态前 |
| app.update_state(config, values, *, as_node=None) | 手动写入/补丁状态 | 删消息、注入摘要、人工干预 |
| app.stream(..., config) | 从快照断点继续跑 | 断点续聊、交互式对话 |
| app.invoke(input, config) | 一次性跑完并返回终态 | 脚本批量测试 |
| app.get_state_history(config) ⭐ | 遍历该线程所有历史快照 | 回溯、审计、可视化 |
| app.wakeup(config) ⭐ | 唤醒被中断的图 | 人机审批、异步回调 |
| app.search(*, thread_id, ...) ⭐ | 跨线程搜索状态 | 后台运营查询 |
6.toolNode工具调用失败处理
-
通过handle_tool_errors= True实现
def get_weather(location: str):
"""获取当前天气."""
print('location:', location)
if location == "SH":
raise ValueError("输入查询必须是专有名词")
elif location == "上海":
return "气温23度,有雾."
else:
raise ValueError("无效输入.")tool_node = ToolNode([get_weather],handle_tool_errors= True)
7.toolNode工具调用失败,转到新的工具节点调用
- 判断返回的消息中是否有error消息,如果第一次工具调用失败,重新跳转到节点,使用新的工具,并删除最后一次的ai消息。
7.将图state状态注入给工具函数
-
注入状态:InjectedState,
-
注入后工具函数就能正常的取值使用
-
Annotated 是 Python 3.9+ 自带的类型注解增强器
-
Annotated[真正类型, 元数据1, 元数据2, ...]
-
InjectedState 是 LangGraph 提供的一个标记常量,含义: "这个参数不需要 LLM 填,由框架把当前图的 state 整字典注入进来。
存储方式
class State(AgentState):
docs: List[str]@tool
def get_context(question: str, state: Annotated[dict, InjectedState]):
"""获取回答问题的相关背景."""
return "\n\n".join(doc for doc in state["docs"])
8.将图第三方存储和配置注入给工具函数
-
InjectedStore()+RunnableConfig -
每次
app.stream/app.invoke时传的config字典:在图内部会被自动封装成 RunnableConfig 实例数据库方式
def get_context(
question: str,
config: RunnableConfig, # 参数1:注入运行时配置
store: Annotated[BaseStore, InjectedStore()], # 参数2:注入文档存储
) -> Tuple[str, List[Document]]:
"""获取回答问题的相关背景."""
# 从运行时配置中获取 user_id
user_id = config.get("configurable", {}).get("user_id")
# 从注入的 store 中根据 user_id 搜索文档
docs = [item.value["doc"] for item in store.search(("documents", user_id))]
return "\n\n".join(doc for doc in docs) # 返回拼接后的文档字符串doc_store = InMemoryStore()
graph = create_agent(model, tools, checkpointer=checkpointer, store=doc_store)
9.工具函数中更新state
工具函数不是节点,不能直接更新state
通过工具id和return command实现
# 自动注入tool_call的id
tool_call_id: Annotated[str, InjectedToolCallId],
return Command(
update={
"user_info": user_info,
"messages": [
ToolMessage(
"成功查询用户信息", tool_call_id=tool_call_id
)
],
}
)
stream_mode的参数
| 模式 | 每帧推送的内容 | 典型用途 |
|---|---|---|
| "values" | 整个状态对象的快照(messages、user_info...全量) | 调试、前端需要随时拿到完整上下文 |
| "updates" | 仅本次被改动的字段(增量) | 节省流量,只拿变化 |
| "messages" | 仅消息数组里新增的那一条 | 聊天 UI 逐字逐句渲染 |
| "debug" | 调试信息(节点进出、异常等) | 排查流程 |
10.如何处理大量的工具调用