用户在上一条消息处理完成前发送新消息——深度讲解生产环境中并发消息的四种处理策略:Reject、Enqueue、Interrupt 与 Rollback。
Double Texting(双重发送)指用户在当前 run 尚未完成时,向同一线程发送了新的消息。在真实的聊天应用中,这种情况极为常见:
LangGraph Server 通过 multitask_strategy 参数提供四种策略,让开发者根据业务场景选择最合适的并发处理方式。
同一个线程上,Run 1 还在执行,Run 2 到来时应该怎么办?这四种策略代表了四种不同的权衡取舍。
Run 1 进行中时,直接拒绝 Run 2,返回 409 错误。Run 1 正常完成。
Run 2 进入队列等待。Run 1 完成后,Run 2 自动开始执行。两个 run 都会完成。
Run 1 在当前节点结束后停止(保留已完成的工作),Run 2 立即开始。Run 1 状态为 interrupted。
Run 1 被立即停止并删除(撤销所有状态),Run 2 从线程的上一个干净状态开始。
最简单的策略:Run 1 运行时拒绝所有新请求,返回 HTTP 409 Conflict。
import httpx
thread = await client.threads.create()
config = {"configurable": {"user_id": "Test"}}
# 启动 Run 1
run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to follow-up with DI Repairs.")]},
config=config,
)
# Run 2 使用 reject 策略 → 触发 HTTPStatusError 409
try:
await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to mount dresser to the wall.")]},
config=config,
multitask_strategy="reject",
)
except httpx.HTTPStatusError as e:
print("Failed to start concurrent run", e)
Run 1 正常完成后,线程状态中只包含第一条 ToDo(mount dresser 未被添加)。
适合不允许消息丢失且可以告知用户"请稍候"的场景。例如:金融交易处理、表单提交。客户端需要捕获 409 并提示用户等待后重试。
Run 2 不会被拒绝,而是进入等待队列。Run 1 完成后,服务器自动开始执行 Run 2。两个 run 都会执行,消息不丢失。
thread = await client.threads.create()
# Run 1:立即开始
first_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Send Erik his t-shirt gift this weekend.")]},
config=config,
)
# Run 2:进入队列,等待 Run 1 完成
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Get cash and pay nanny for 2 weeks. Do this by Friday.")]},
config=config,
multitask_strategy="enqueue",
)
# 等待 Run 2 完成(Run 1 → Run 2 串行执行)
await client.runs.join(thread["thread_id"], second_run["run_id"])
适合每条消息都必须被处理的场景,且消息之间有逻辑顺序(Run 2 需要看到 Run 1 的结果)。例如:多步骤任务录入、聊天记录保存。注意:如果用户发送的是"补充说明",排队可确保 AI 先处理原始消息再处理补充。
Run 2 到来时,Run 1 在当前节点执行完毕后暂停(不会立即杀死)。Run 1 已完成的工作被保留到 PostgreSQL,状态变为 interrupted;Run 2 立即开始执行。
import asyncio
thread = await client.threads.create()
# Run 1 开始(查询 ToDo 摘要)
interrupted_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Give me a summary of my ToDos due tomorrow.")]},
config=config,
)
# 等 1 秒让 Run 1 执行一部分节点
await asyncio.sleep(1)
# Run 2 使用 interrupt 策略 → Run 1 在当前节点结束后停止
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Never mind, create a ToDo to Order Ham for Thanksgiving by next Friday.")]},
config=config,
multitask_strategy="interrupt",
)
await client.runs.join(thread["thread_id"], second_run["run_id"])
# 确认 Run 1 状态
status = (await client.runs.get(thread["thread_id"], interrupted_run["run_id"]))["status"]
print(status) # "interrupted"
Interrupt 保留 Run 1 已经完成的节点的结果(如果 Run 1 已经写入了 Store,那些写入会保留)。Rollback 则会撤销 Run 1 的一切影响。如果 Run 1 的副作用(写入 DB、发送通知)不可接受,用 Rollback;如果已完成的部分工作有价值,用 Interrupt。
Run 2 到来时,Run 1 被立即停止并从数据库中删除(包括其所有状态变更)。Run 2 从线程的上一个干净 checkpoint 开始执行,就好像 Run 1 从未发生过一样。
thread = await client.threads.create()
# Run 1:用户的原始意图
rolled_back_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to call to make appointment at Yoga.")]},
config=config,
)
# Run 2:用户改变了主意
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Actually, add a ToDo to drop by Yoga in person on Sunday.")]},
config=config,
multitask_strategy="rollback",
)
await client.runs.join(thread["thread_id"], second_run["run_id"])
# 验证 Run 1 已被删除
try:
await client.runs.get(thread["thread_id"], rolled_back_run["run_id"])
except httpx.HTTPStatusError:
print("Original run was correctly deleted") # ✓
Rollback 会物理删除 Run 1 的记录。如果 Run 1 已经触发了不可逆的外部副作用(如发送邮件、调用外部 API、扣款),这些副作用不会被撤销。Rollback 仅撤销 LangGraph 内部的状态(checkpoints、Store 中的写入)。
| 策略 | Run 1 结果 | Run 2 结果 | 线程历史 | 适用场景 |
|---|---|---|---|---|
| Reject | 正常完成 | ❌ 被拒绝(409) | 仅 Run 1 | 不允许并发,客户端能处理重试 |
| Enqueue | 正常完成 | ✅ 排队后完成 | Run 1 → Run 2(串行) | 每条消息都重要,有顺序依赖 |
| Interrupt | ⏸ 中断(保留已完成节点) | ✅ 立即开始 | Run 1 部分 + Run 2 | 用户想改变方向,但不需要撤销已完成的工作 |
| Rollback | 🗑 删除(彻底撤销) | ✅ 从干净状态开始 | 仅 Run 2 | "我改主意了,之前说的不算" — 需要干净重来 |
通用聊天应用:Interrupt 或 Rollback(用户体验更流畅)。任务管理/工作流:Enqueue(每条指令都需要执行)。金融/表单:Reject(防止重复提交)。大多数对话助手场景下,Rollback 最符合用户直觉——"我已经改主意了,上一条当没说"。