把 Agent 跑起来很容易,把它停下来却是一场生产事故。本期对 2026 年主流的六个 Agent 框架——OpenAI Agents SDK、PydanticAI、LangGraph、CrewAI、AutoGen/AG2、LlamaIndex Workflows——做了一次"取消/中断/超时"语义的横向拆解,核心发现只有一句话:绝大多数框架取消了编排循环,却取消不掉已经在途的副作用工具调用。用户按下 Stop 之后,后台的扣款、发邮件、写库还会继续跑完,这就是本文定义的"幽灵执行税"(Ghost Execution Tax)。差异不在模型,而在每个框架把 CancelledError 往下传到第几层就断了。下面给出可直接落地的选型表、代码级证据与幂等护栏方案。
| 框架(2026 版本) | 取消原语 | 流式/LLM 中断 | 工具中断传播 | 幽灵执行风险 | 恢复/幂等支持 | 内建超时 |
|---|---|---|---|---|---|---|
| OpenAI Agents SDK | asyncio Task / Runner 取消 |
中(流式可 .cancel()) |
部分(原生工具被 await) | 低–中 | 弱(无内建幂等) | 无,需自配 |
| PydanticAI | asyncio Task | 强(stream cleanup 已加固) | 未定义(#5837) | 中 | 中(interrupted-history) | 无 |
| LangGraph | interrupt() + Checkpointer |
中 | 节点内自行处理 | 中–高(副作用不回滚) | 强(检查点 + 重放) | 配置式 |
| CrewAI | kickoff_async |
弱 | 弱(同步阻塞工具为主) | 高 | 弱 | 弱 |
| AutoGen / AG2 | asyncio(0.4+ 异步版) | 中 | 部分 | 中(0.2 同步版高) | 弱 | 仅终止条件,非超时 |
| LlamaIndex Workflows | asyncio Task | 中 | 部分 | 中 | 弱 | 无 |
评分依据:每个框架的"取消路径深度",即
CancelledError/取消信号能从编排层传到 LLM 流、再传到正在执行的工具体、最后到副作用善后的层数。层数越浅,幽灵越多。
在拆具体框架前,先把"幽灵执行"分成三类,它们对应三种不同的税:
形态一:在途 LLM 请求不停。 用户点 Stop,前端连接断了,但后端那条 LLM 流式请求还在跑,继续按 token 计费。SynapseAI 的生产复盘记录了最典型的症状:"User hits Stop — agent keeps processing for 30+ more seconds, billing tokens"。这不是 bug,而是框架根本没把"用户取消"翻译成"取消那条 HTTP/SSE 请求"。
形态二:在途工具调用不停。 更危险。Agent 已经决定调用 charge_card,工具的 HTTP 请求发出去了,此时取消只能让你不再等结果,请求本身已经到达对方服务器并会被执行。于是你看到"已取消",用户那边却收到了扣款、收到了验证码邮件、被写进了一条数据库记录。
形态三:恢复时重放。 这是 LangGraph 独有的"幂等债务"——为了能续跑,检查点会重放历史步骤;如果某步是副作用节点且未加幂等键,续跑时它会被再执行一次,于是同一笔订单扣了两次款。这和"回滚即双花"是同一个家族的病,但触发点不是回滚而是取消后续跑。
下面六个深度拆解,逐一说明每框架在哪一层把信号丢了。
架构原理。 SDK 把 Runner.run() 设计成标准 asyncio 协程,Runner.run_streamed() 返回的 RunResultStreaming 带显式 .cancel()。这意味着取消信号在编排循环这一层是干净的——循环会在下一个 await 点抛出 CancelledError 并停止迭代,流式输出也会停。
关键代码:
import asyncio
from agents import Agent, Runner
async def main():
agent = Agent(name="ops", instructions="重启生产库前先确认")
task = asyncio.create_task(Runner.run(agent, "重启 prod-db"))
await asyncio.sleep(0.5) # 让它开始调用工具
task.cancel() # 取消编排循环
try:
await task
except asyncio.CancelledError:
print("run cancelled")
优缺点。 优点是签名诚实——它就是协程,取消语义符合 Python 直觉,迁移成本低,新人 5 分钟能写出可取消的 Agent。缺点是原生工具被 await 但工具体本身并不天然感知取消:如果你的工具函数里是一个不带 cancellation 的 requests.post,编排层虽然不等它了,那条 HTTP 依然会跑到对方服务器执行完。换 httpx.AsyncClient 并在 except CancelledError 里关连接,才能真停。
变现/训练营评估。 极适合做"取消语义入门"那一节课,因为代码最短、心智模型最直白。可作为训练营的骨架框架,但生产章节必须补"工具体取消感知"这一节,否则学员照抄会留坑。
架构原理。 PydanticAI 全异步,agent.run() 是协程,最近还专门加固了"stream cleanup"和"interrupted-history capture"——也就是说流的善后它做得很认真,被取消时不会留下半截脏历史。
关键代码与已知缺口:
import asyncio
from pydantic_ai import Agent
agent = Agent("openai:gpt-4o")
@agent.tool
async def send_email(ctx, to: str, body: str):
# 取消信号能不能传到这里?官方尚未定义
await smtp.send(to, body)
PydanticAI 仓库 issue #5837《Define tool-execution cancellation semantics》 直接点破:工具执行的取消语义目前未定义。流能清,工具不能保证清。
优缺点。 优点是类型安全、流式善后做得好、文档质量高,"取消后历史不脏"这一点比多数框架强。缺点恰恰是它自己承认的那个洞——工具体取消没有契约,副作用工具在取消后是否继续跑是未定义行为,生产里等于不可预测。
变现/训练营评估。 适合"结构化输出 + 流式"主题,但不适合直接拿来做"取消实战" demo,除非你愿意自己给工具包一层 cancellation 门面。它的诚实(公开 issue 承认缺口)反而是教学素材:可以拿 #5837 教学员"如何识别一个框架的语义盲区"。
架构原理。 LangGraph 用检查点(Checkpointer)+ 重放换来可恢复性,interrupt() 是它的人机协作原语。这套设计在"状态一致性"上是六框架里最强的——取消时,未提交的节点写入会被丢弃,状态停在最后一个干净的检查点上。
关键代码与陷阱:
def charge_card(state):
stripe.charge(amount=state["amount"]) # ① 副作用:已扣款
return {"charged": True} # ② 此写入被检查点保存
# 取消后再 resume → 重放历史 → 如果 charge 没有 idempotency_key → ② 被重放 → 二次扣款
优缺点。 优点压倒性:长流程、可恢复、可调试重放,复杂工作流首选。缺点是这套重放模型和副作用天然冲突——检查点只记录图状态,不记录外部世界状态,副作用不回滚,重放时还会再放一遍。于是 LangGraph 的幽灵不在"取消当下",而在"取消后续跑"那一刻集中爆发,且因为是确定性重放,每次续跑都会稳定地多执行一次,比随机 bug 更难排查。
变现/训练营评估。 这是六个里最适合做高阶付费课的框架,因为"可恢复工作流"是真需求。但课程必须把"幂等键 + 副作用隔离节点"作为前置章节,否则学员做出的可恢复 Agent,本质上是一台稳定的双花机器。
架构原理。 CrewAI 是任务编排器,kickoff() 同步阻塞为主,kickoff_async() 提供异步入口。但它的工具大多以同步函数注册,跑在事件循环里等于一个不可中断的阻塞块。
关键代码与症状:
crew = Crew(agents=[a], tasks=[t], process=Process.sequential)
result = crew.kickoff() # 同步阻塞,中途无法干净取消
# kickoff_async 也只是包了一层;工具体同步阻塞 → CancelledError 传不进去
BSWEN 2026-04-17 的生产复盘描述的就是这个画面:"agents started hanging… One agent would timeout, and the entire pipeline would freeze"——CrewAI 在超时/取消上的鲁棒性是六框架里最弱的。
优缺点。 优点是角色化(role + goal + backstory)叙事极其直观,非技术用户上手最快,做 Demo 最漂亮。缺点是在取消/超时维度几乎是"裸奔":同步阻塞工具让取消信号到不了工具体,幽灵执行风险最高;pipeline 一旦卡住,没有内建机制把它救回来。
变现/训练营评估。 最适合做"零基础搭多角色 Agent"的引流课,门槛低、出效果快。但生产章节必须明确标注:CrewAI 不适合不可逆副作用场景,要么所有工具幂等,要么前置一个会签/确认门,否则别上生产。
架构原理。 AutoGen 分两条线:老版 0.2 同步、对话循环为主;新版(AG2 / autogen 0.4+)异步优先,team.run() 是协程,取消靠 asyncio。终止条件(termination)是它的强项——能基于消息数、关键词停,但这不等于超时取消。
关键代码:
# AG2 异步版:取消能传到对话循环
task = asyncio.create_task(team.run(task=user_task))
await asyncio.sleep(2); task.cancel() # 0.4+ 可以较干净地停
# 但 0.2 同步版:run 在线程里,cancel 进不去 → 幽灵
优缺点。 优点是多智能体对话编排成熟,终止条件灵活,异步版取消体验中等偏上。缺点是版本割裂——同一名字两个架构,同步版几乎无法干净取消,新手极易踩到老版而留下幽灵;终止条件解决"该停的时候停",解决不了"用户让它停它就停"。
变现/训练营评估。 多智能体方向绕不开它,但课程必须锁版本(强制 AG2/0.4+),并把"终止条件 ≠ 超时取消 ≠ 用户取消"这三个概念讲清楚,这是 AutoGen 新手最常混淆的点,也是很好的教学抓手。
架构原理。 Workflows 是事件驱动 + 全 asyncio,每个 step 订阅事件、产出事件,事件按顺序在事件循环里流动。取消一个 workflow.run 协程,会在两个 step 之间停下来。
关键代码:
task = asyncio.create_task(workflow.run(**kwargs))
await asyncio.sleep(0.5); task.cancel()
# 取消信号在 step 边界生效;如果一个 step 内部正在 await 一个慢工具,
# 同样要工具体自己感知取消,否则这个 step 会跑完。
优缺点。 优点是事件驱动模型天然适合"可观测、可暂停在边界"的流式管线,RAG 类场景强。缺点和多数框架一样——取消只在 step 边界生效,step 内部的慢工具仍是盲区;且 Workflows 本身没有内建超时,需要外层 asyncio.wait_for 包裹。
变现/训练营评估。 适合"RAG + Agent 工作流"主题,事件驱动可视化效果好。取消章节可作为"通用 asyncio 取消模式"的载体,但要强调:事件驱动的好处是边界清晰,坏处是边界内的盲区更隐蔽。
下面这张图把"取消信号"从用户一直画到副作用工具,标出六框架分别在哪个节点把它丢掉:
flowchart TD
U([用户按 Stop / 超时]) --> ORC[编排循环<br/>所有框架都能停]
ORC -->|CancelledError| LLM[在途 LLM 流式请求]
LLM -->|PydanticAI/LlamaIndex 较干净<br/>CrewAI 弱| TOOL[在途工具 HTTP 调用]
TOOL -->|多数框架在此断链| SE[副作用:扣款/发邮件/写库]
SE --> GHOST((幽灵执行))
GHOST --> RESUME[取消后 resume]
RESUME -->|LangGraph 重放| DOUBLE((二次副作用/双花))
classDef ok fill:#d4edda,stroke:#28a745;
classDef warn fill:#fff3cd,stroke:#ffc107;
classDef bad fill:#f8d7da,stroke:#dc3545;
class ORC ok;
class LLM warn;
class TOOL warn;
class SE bad;
class GHOST bad;
class DOUBLE bad;
读法: 绿色是所有框架都安全的层(编排循环),黄色是表现分化的层(LLM 流、工具调用边界),红色是幽灵真正落地的层(副作用、恢复重放)。一个框架的"取消质量",等于它的绿色能往下延伸到第几层。
不要按"哪个最火"选,按你的工具调用是否可逆选:
纯只读 / 可重试场景(搜索、总结、RAG 问答):六个随便选,幽灵执行无所谓,因为重跑一次也没成本。优先 OpenAI Agents SDK / PydanticAI,代码最短。
有副作用但可幂等(发通知、写日志、调幂等 API):选 OpenAI Agents SDK 或 LlamaIndex Workflows,但强制要求每个副作用工具实现幂等键 + 取消感知(httpx 异步 + except CancelledError 关连接)。这是生产的最低门槛。
不可逆副作用 + 需要长流程可恢复(扣款、下单、改库、运维操作):LangGraph 是唯一合适的骨架,但必须配套三件事——①副作用节点全幂等;②副作用节点用 interrupt() 前置人工确认门;③续跑前用检查点状态做去重。CrewAI 和同步版 AutoGen 在此场景直接排除。
多智能体对话 + 需要能停:选 AG2 / autogen 0.4+ 异步版,锁死版本,并区分"终止条件(自动停)"和"取消(被动停)"。
一条普适护栏,不分框架:永远用 asyncio.wait_for(runner, timeout=T) 给整轮跑加一个硬超时,再用幂等键给每个副作用加一道去重——前者堵住"停不下",后者兜住"停了也跑了"。这两行代码,能挡掉本文里八成的幽灵。
Runner.run / run_streamed 的 asyncio 取消语义与 RunResultStreaming.cancel() 原语。interrupt() 人机协作原语与重放恢复机制。本文由「51学通信」原创出品,站长爱卫生(微信:gprshome201101)持续输出通信与 AI Agent 实战拆解。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
