【LangGraph实战】《LangGraph实战》_165.[第8章 LangGraph平台] Agent Protocol核心组件:Runs、Threads、Assistants

从“一脸懵”到“玩转Agent Protocol”:LangGraph平台三大核心组件全拆解,这才是构建生产级AI Agent的底层密码!读完全文,你会彻底搞懂Runs、Threads、Assistants到底怎么配合,不再被官方文档绕晕。
文字目录:
- Assistants:Agent的“灵魂蓝图”
- Threads:对话的“记忆容器”
- Runs:单次执行的“动作实例”
- 协作关系:从调用到完成的闭环
- 状态管理与Checkpoint持久化
- 生产环境最佳实践
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》
俗话说“饭要一口一口吃,代码要一行一行敲,但Agent Protocol要是啃不透,生产环境能让你敲到地老天荒”。你是不是也这样?照着LangGraph官方示例跑通了Demo,心里正美滋滋,结果一打开LangGraph Platform的文档,看到Runs、Threads、Assistants这三个名词,瞬间脑子嗡的一声——这三个家伙到底啥关系?谁先谁后?我该怎么调接口?别慌,这种“一看就会,一写就废”的无力感,我当年踩坑的时候全经历过。今天咱们就把这三个核心组件掰开了、揉碎了,聊明白。
1. Assistants:Agent的“灵魂蓝图”
在LangGraph Platform的语境里,Assistants可不是OpenAI那个Assistant API的翻版。它更像是一份“施工蓝图”,或者说是一个“模具”。你把LangGraph的图注册到平台后,Assistants负责把这个图封装成一个可远程调度、可配置、可追踪的实体。它身上通常带着三样东西:graph(定义了节点和边)、config(运行时的默认参数,比如用哪个模型、温度值多少)、metadata(版本号、负责人、业务标签等)。
说白了,Assistants回答的是“我是谁、我能干什么、我的默认配置是什么”这三个哲学问题。一个设计良好的Assistants,应该是稳定、可复用、版本化的,而不是每次都现场捏一个。
新手最容易犯的错,就是把Assistants当成“临时变量”或者“Prompt存储桶”。我见过太多同学,每来一个新请求,就现场创建一个Assistants,或者把几百字的系统Prompt直接硬编码在Assistants的config里。更绝的是,有人觉得Assistants里应该塞业务逻辑代码,比如“如果用户问订单,就走A分支”——打住!那是Graph该干的事,不是Assistants的职责。
来看看典型的反模式:
# 这是一个典型的反模式
def handle_request(user_input):
# 每次请求都新建一个Assistant,这谁扛得住?
assistant = client.assistants.create(
graph="support_bot",
config={
"configurable": {
# 把业务规则和超长Prompt全塞进去
"system_prompt": "你要先问候用户,然后询问订单号,然后查询物流,然后...(此处省略500字)"
}
}
)
# 用完即抛,完全没有复用
run = client.runs.create(assistant_id=assistant.id, ...)
这样做的问题太大了。第一,创建Assistants是有开销的,你把它当成一次性筷子,接口延迟直接起飞。第二,Prompt和规则写死在配置里,产品运营想改个欢迎语,都得找研发发版,灵活性为零。第三,你根本没有版本管理意识,线上出问题都不知道是哪个“临时Assistant”干的。
正确的姿势是把Assistants当作“长期资产”来维护。基础能力(比如图结构、默认模型)在创建时定好,动态变量(比如本次会话的业务上下文、温度值)放到Run的输入或config里传进去。metadata一定要打标签,做版本控制。
# 1. 预定义,一次创建,长期复用
assistant = client.assistants.create(
graph="customer_support_graph",
config={
"configurable": {
"model": "gpt-4o", # 基础能力配置
"temperature": 0.3
}
},
metadata={
"name": "客服助手V2",
"version": "2.1.0",
"team": "ai-platform",
"env": "production"
}
)
# 记下这个 assistant.id,后面反复用
# 2. 真正运行时,动态传业务上下文
run = client.runs.create(
assistant_id=assistant.id,
thread_id=thread.id,
input={"messages": [("human", "帮我查订单")]},
config={
"configurable": {
"biz_context": "双11大促", # 动态变量
"user_tier": "VIP"
}
}
)
这样做的好处一目了然。Assistants保持稳定,你可以围绕它做灰度发布、A/B测试;Run保持灵活,每次请求都能注入新鲜上下文。而且metadata里的版本信息,让你在排查线上问题时,一眼就能定位到是哪个版本的逻辑在跑。
小结:Assistants是Agent的“灵魂蓝图”,先画好图、定好规,再谈盖房子。别把它当成一次性草稿纸。
2. Threads:对话的“记忆容器”
Thread这个单词,在程序员的第一反应里往往是“线程”。但在LangGraph Platform里,它跟操作系统线程半毛钱关系都没有,它是一个“状态容器”、一个“对话的记事本”、一个“记忆的抽屉”。每一个Thread内部都维护着一套状态(State),包括消息历史(messages)、图的当前值(values)、以及一些元数据(metadata)。最关键的是,LangGraph通过Thread实现了跨Run的状态持久化。你第一次Run走到一半,用户去喝了杯咖啡,回来接着聊,Thread能帮你从断点无缝续上。
新手对Thread最大的误解,就是把它等同于一个简单的List[Message]。我见过不少同学,自己吭哧吭哧在Redis里存聊天记录,然后每次调用LangGraph时把这段纯文本历史塞进去,觉得自己“管理了上下文”。殊不知,LangGraph的Thread里存的可不只是聊天记录,还有图执行的中间状态、下一个该执行哪个节点的路由信息、以及Checkpoint检查点。你自己存的那点纯文本,根本不足以让图恢复到上次执行到一半的现场。
看看这个误区:
# 误区:自己用Redis管理历史,断了LangGraph的上下文
history = redis.get("chat_history:user_123") # 只存了文本字符串
# 直接传给图,以为这样就接续了上下文
result = graph.invoke({
"messages": [HumanMessage(content=msg) for msg in history.split("\n")]
})
# 如果图里有个“审批流”,第一步让用户填金额,第二步让用户确认,
# 现在用户回复“确认”,图根本不知道自己处在第二步,因为状态全丢了!
这种情况特别常见。比如在一个人机协作的审批流里,第一个Run已经问了用户“请输入金额”,用户回复了“5000元”。第二个Run开始,如果没有Thread的原生状态,LangGraph根本不知道“5000元”已经填完了,现在该进入“确认”节点。它可能还会从头执行,再问一遍“请输入金额”。你说用户气不气?
相信原生的Thread,让LangGraph自己管理状态。你要做的,只是为每个用户或每个会话创建一个Thread,然后在后续的所有Run里复用同一个thread_id。LangGraph的Checkpoint机制会自动在后台帮你落盘、恢复。
# 1. 给每个用户创建一个独立的Thread
thread = client.threads.create(
metadata={
"user_id": "user_123",
"session_id": "sess_456",
"biz_type": "refund"
}
)
# 2. 第一轮对话:用户提交申请
run1 = client.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
input={"messages": [("human", "我要申请退款,订单号是ABC123")]}
)
# 3. 第二轮对话:用户回复确认(复用同一个Thread!)
run2 = client.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
input={"messages": [("human", "确认退款")]} # 同一个thread_id
)
# LangGraph会自动从Thread的最后一个Checkpoint恢复,
# 知道“确认退款”对应的是审批流的第二个节点,直接推进
Thread还有一个隐藏福利:你可以随时查询它的完整状态历史,甚至“时间旅行”回到之前的某个状态重新分叉。这种能力在调试复杂工作流时,简直是救命稻草。
小结:Thread是Agent的“记事本”,一页只记一个客户的事。别自己造轮子存聊天记录,让LangGraph替你操心。
3. Runs:单次执行的“动作实例”
如果Assistants是蓝图,Thread是场地,那Run就是真正开干的那支“施工队”。Run代表的是对Assistant的一次具体调用,它会经历完整的生命周期:从排队(pending/queued),到执行(running),再到完结(complete/error/interrupted)。每一次Run都会产生一个新的状态快照,并写回Thread。所以Run既是动作的发起者,也是状态的制造者。
LangGraph Platform的Run设计是异步的,这意味着你提交一个Run之后,它不会立刻给你最终结果,而是给你一个Run ID,让你去轮询、去监听流式事件、或者配置Webhook接收回调。这跟咱们平常写同步函数result = func()的思维习惯完全不同。
很多新手拿到SDK的第一反应,就是把Run当成普通函数调用。“我创建一下,然后直接拿结果”。结果要么是拿到一个None,要么是在running状态就急吼吼去取结果,程序直接报错。还有一种更隐蔽的坑:LangGraph图里设计了人机交互节点(比如interrupt),Run的状态会变成interrupted,这时候如果你不处理,傻等着它变成complete,那能等到天荒地老。
看看这个反模式:
# 反模式:把Run当同步函数
run = client.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
input={"messages": [("human", "查一下库存")]}
)
# 刚创建完,Run可能还在pending或者queued
print(run.status) # 输出可能是 "pending"
print(run.result) # 大概率是None,或者抛异常
# 或者这样轮询,但完全没处理中断和异常
while run.status != "complete":
time.sleep(0.5)
run = client.runs.get(run.id)
# 如果Run状态变成interrupted,这个循环就死循环了!
第一,接受异步思维。第二,善用Stream模式,实时感知Run的执行过程。第三,对每种终态(complete/error/interrupted/expired)都做显式处理。
# 方案A:Stream模式,实时监听事件(推荐)
for event in client.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
input={"messages": [("human", "帮我生成周报")]},
):
if event.event == "on_chain_start":
print(f"节点 {event['name']} 开始执行...")
elif event.event == "on_chain_stream":
print(f"中间状态: {event.data}")
elif event.event == "on_chat_model_stream":
print(f"模型输出: {event.data}", end="")
# 方案B:后台轮询 + 完备状态处理
run = client.runs.create(...)
terminal_states = {"complete", "error", "expired", "cancelled"}
while run.status not in terminal_states:
time.sleep(1)
run = client.runs.get(run.id)
if run.status == "interrupted":
# 人机交互节点,需要前端介入
user_input = get_user_input_from_frontend(run.thread_id)
# 重新注入输入,继续执行
run = client.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
input={"messages": [("human", user_input)]}
)
if run.status == "error":
logger.error(f"Run failed: {run.error}")
elif run.status == "complete":
print("执行成功:", run.result)
Stream模式特别适合前端打字机效果展示;而轮询模式更适合后台任务。无论哪种,你都得把Run的生命周期纳入掌控,不能让它裸奔。
小结:Run是Agent的“一次心跳”,既要发得出,也要等得回。学会异步思维,你就从“写脚本”进化到“做系统”了。
4. 协作关系:从调用到完成的闭环
单独看懂三个组件不算本事,能把它们串成一条流畅的流水线,才是架构能力的体现。正确的调用链条是:客户端发起请求 → 定位到具体的Assistants(能力定义) → 准备或复用Thread(状态容器) → 启动Run(执行实例) → 图节点开始执行 → 中间状态写入Thread(Checkpoint) → Run到达终态 → 返回结果 → 下一次对话复用同一个Thread。
这个链条里,Assistants提供“能力”,Thread提供“记忆”,Run提供“动力”。三者缺一不可,而且顺序不能乱。
顺序搞反、资源错配,是这一趴最常见的两副毒药。我见过有人先create一个Run,然后发现Run没地方写状态,再回头补建Thread;还有人为了图省事,把多个不同业务的Assistant往同一个Thread里塞。比如A业务的Assistant是“客服退款图”,B业务的Assistant是“营销推荐图”,两个Run在同一个Thread上执行,状态直接互相覆盖,LangGraph恢复Checkpoint时直接懵圈。
看看这个错误示范:
# 错误示范:跨Assistant混用Thread
thread = client.threads.create(metadata={"biz": "refund"})
# 第一次用退款图
run1 = client.runs.create(
thread_id=thread.id,
assistant_id="asst_refund_graph", # 退款图
input={"messages": [("human", "我要退款")]}
)
# 第二次居然用推荐图,但Thread里存的是退款图的Checkpoint!
run2 = client.runs.create(
thread_id=thread.id, # 同一个Thread!
assistant_id="asst_recommend_graph", # 推荐图
input={"messages": [("human", "给我推荐商品")]}
)
# 推荐图读到退款图的状态,节点对不上,直接报错或者行为异常
严格遵循“一个业务流对应一个Assistant,一个用户会话对应一个Thread”的原则。Run只是无状态的执行动作,它不拥有任何状态,所有状态都归Thread管。在Thread的metadata里打上业务标签,调用前做一致性校验。
# 标准三板斧
ASSISTANT_ID = "asst_customer_support_v2" # 1. 确定能力定义
def handle_user(user_id, message):
# 2. 复用该用户的会话容器
threads = client.threads.search(
metadata={"user_id": user_id, "biz": "support"}
)
if threads:
thread = threads[0]
else:
thread = client.threads.create(
metadata={"user_id": user_id, "biz": "support"}
)
# 3. 触发执行(Run只负责动,不负责存)
run = client.runs.create(
assistant_id=ASSISTANT_ID,
thread_id=thread.id,
input={"messages": [("human", message)]}
)
return run.id
这个流程的好处是权责清晰。出了bug,你先查Assistants版本对不对,再查Thread里的状态历史,最后看Run的执行日志。三个维度一交叉,问题定位快得很。
小结:Assistants出图纸,Thread当场地,Run是施工队。三者各安其位,Agent系统才能稳如老狗。
5. 状态管理与Checkpoint持久化
LangGraph最迷人的地方,不是它能画图,而是它能“断点续传”。这背后的功臣就是Checkpoint机制。每次Run执行节点时,LangGraph会根据策略把当前状态快照(Checkpoint)落盘到持久化存储(比如PostgreSQL、Redis)中,并与Thread绑定。这意味着你的Agent不是“无状态函数”,而是一个可以暂停、可以恢复、可以重试的“有状态服务”。
对于LangGraph Platform来说,状态管理基本是透明的。但如果你是自托管,或者想深入理解原理,就必须搞清楚Checkpoint是怎么写入Thread的,以及如何利用它实现时间旅行(Time Travel)和错误重试。
最容易踩的坑,就是以为LangGraph的图是“纯函数”,每次invoke都从零开始。于是很多新手在编译图的时候不配置checkpointer,然后在图外面自己包一层Redis,手动存个最终结果。这简直是买椟还珠——你丢了中间状态,一旦某个节点失败,只能从头重跑,而且根本无法利用LangGraph原生的人机协作能力。
from langgraph.graph import StateGraph
builder = StateGraph(State)
# ... 添加各种节点 ...
graph = builder.compile() # 注意:没有配置 checkpointer!
# 调用方自己存结果
result = graph.invoke({"messages": [("human", "很复杂的长任务")]})
# 只存了最终结果,中间状态全丢了
redis.set("final_result", str(result))
# 如果任务有5步,第3步因为网络抖动失败了,
# 你想从第3步重试?没门,只能全部重跑。
在编译图时注入Checkpointer,生产环境用PostgresSaver,开发环境用MemorySaver。让LangGraph平台层或自托管的API来帮你管理持久化。同时,学会利用Thread的state查询和时间旅行接口。
# 自托管时正确配置持久化
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg import Connection
conn = Connection.connect("postgresql://user:pass@localhost/db")
checkpointer = PostgresSaver(conn)
checkpointer.setup() # 初始化checkpoint表
# 编译图时注入
graph = builder.compile(checkpointer=checkpointer)
# 通过SDK查询Thread状态(平台层)
states = client.threads.get_history(thread_id=thread.id)
# 可以看到完整的Checkpoint链条,每个节点执行后的状态
# 时间旅行:回到某个历史状态重新执行
config = {
"configurable": {
"thread_id": thread.id,
"checkpoint_id": states[-2].checkpoint_id
}
}
# 从倒数第二个状态重新分叉
使用原生Checkpoint的另一个好处是“人机协作”(Human-in-the-loop)。当Run遇到interrupt节点暂停时,状态已经被安全地保存在Thread里。你可以放心地让前端弹窗让用户确认,确认后同一个Thread继续跑,LangGraph从断点无缝恢复。
小结:Checkpoint是Agent的“存档点”。学会用存档,就不怕死机重来,也不怕复杂流程半途而废。
6. 生产环境最佳实践
前面的五个章节,咱们聊的是“是什么”和“为什么”。现在来到最关键的“怎么做”——怎么把这些知识打包成一套能扛住真实流量的生产系统。这里的坑包括但不限于:并发冲突、Run超时、状态僵尸、Assistant版本混乱、监控缺失。咱们一个个来排雷。
用Demo代码直接上线,是新手崩溃最快的方式。我见过一个典型的“裸奔”代码:接口收到请求后,现场创建Thread、创建Run,然后死循环轮询直到complete,期间没有任何超时、没有异常处理、没有用户隔离。上线第一天,流量一并发,数据库连接池打满,Redis内存爆炸,接口响应时间直接飙到30秒。
看看这个反面教材:
# 裸奔代码,千万别学
@app.post("/chat")
async def chat(message: str):
# 每次请求都新建Thread,没有查重
thread = client.threads.create()
# 同步阻塞,死等Run完成
run = client.runs.create(
thread_id=thread.id,
assistant_id=ASSISTANT_ID,
input={"messages": [("human", message)]}
)
# 死循环轮询,没有超时,没有退避策略
while run.status != "complete":
await asyncio.sleep(0.1)
run = client.runs.get(run.id)
return run.result # 如果Run是error,这里直接抛异常给用户
生产环境必须穿上“铠甲”。我给你列一个六层防护:
- 用户隔离:收到请求后,先搜索该用户是否已有活跃Thread,有则复用,无则创建。避免一个人开十个Thread。
- 异步化:把Run提交到后台任务队列(Celery、RQ、Arq),接口立即返回
task_id。前端用SSE或WebSocket推送结果。 - 并发控制:对同一个Thread,使用
multitask_strategy="enqueue",让多个Run排队执行,避免状态冲突。 - 超时与重试:给Run设置合理的
timeout,对error状态做指数退避重试,对expired做清理。 - 生命周期管理:定时归档已完成的Thread,清理长期
interrupted的僵尸Run。 - 版本灰度:Assistant更新时,通过metadata区分版本,逐步切流,不要一刀切。
@app.post("/chat")
async def chat(req: ChatRequest):
# 1. 用户隔离:查复用
threads = client.threads.search(
metadata={"user_id": req.user_id, "status": "active"}
)
thread = threads[0] if threads else client.threads.create(
metadata={"user_id": req.user_id, "status": "active"}
)
# 2. 提交Run(带排队策略)
run = client.runs.create(
thread_id=thread.id,
assistant_id=ASSISTANT_ID,
input={"messages": [("human", req.message)]},
multitask_strategy="enqueue", # 同Thread并发时自动排队
timeout=300 # 5分钟超时
)
# 3. 立刻返回任务ID,不阻塞
return {"task_id": run.id, "status": run.status}
# 后台Worker处理(伪代码)
def process_run_events(run_id: str):
run = client.runs.get(run_id)
if run.status == "interrupted":
notify_frontend(run.thread_id, "需要人工确认")
elif run.status == "error":
logger.error(f"Run {run_id} failed", extra={"error": run.error})
# 根据策略重试或告警
elif run.status == "complete":
push_to_frontend(run.thread_id, run.result)
配合监控大盘,统计每分钟Run的complete/error/interrupted分布,配上P95延迟,你的系统才算真正长出了眼睛。
小结:上线不是Demo的复制粘贴,多加几层保护伞,才能经得起用户折腾。生产级Agent,拼的是细节。
写在最后
不知不觉,咱们已经把Runs、Threads、Assistants这三个核心组件,从里到外扒了个干净。回顾一下:Assistants是Agent的“灵魂蓝图”,定义了你能做什么;Threads是“记忆容器”,承载了每一次对话的上下文和状态;Runs是“动作实例”,负责把蓝图和记忆变成现实。Checkpoint像存档点,让整个过程可以暂停、可以重试、可以回溯。
它们不是三个孤立的API,而是“行为定义、状态容器、执行实例”的铁三角。你只有把这套协议吃透,才算真正从“调包侠”进化成了“架构师”。
学编程这条路,就像跑马拉松,不是谁起步快,而是谁节奏稳。Agent开发是未来十年的大方向,你现在觉得晦涩的每一个概念,都会变成日后面试和项目里的闪光点。别怕这些名词多,当年我啃微服务的服务发现、配置中心、熔断降级,也是一口老血。但只要你耐着性子,画个图、写两行代码、跑通一个流程,那种“哦!原来如此”的通透感,真的会上瘾。
保持好奇,保持耐心,持续学习。你流的每一滴汗,代码都记得。咱们下一篇,继续肝!
关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》
更多推荐


所有评论(0)