<!-- title: 取消不停：六框架幽灵执行实测 -->

# 取消不停：六大Agent框架"幽灵执行"实测

## 概述

把 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 流、再传到正在执行的工具体、最后到副作用善后的层数。层数越浅，幽灵越多。

## 一、幽灵执行的三种形态：你按下的 Stop 其实没停

在拆具体框架前，先把"幽灵执行"分成三类，它们对应三种不同的税：

**形态一：在途 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 独有的"幂等债务"——为了能续跑，检查点会重放历史步骤；如果某步是副作用节点且未加幂等键，续跑时它会被**再执行一次**，于是同一笔订单扣了两次款。这和"回滚即双花"是同一个家族的病，但触发点不是回滚而是取消后续跑。

下面六个深度拆解，逐一说明每框架在哪一层把信号丢了。

## 二、OpenAI Agents SDK：编排层最干净，工具体仍是盲区

**架构原理。** SDK 把 `Runner.run()` 设计成标准 asyncio 协程，`Runner.run_streamed()` 返回的 `RunResultStreaming` 带显式 `.cancel()`。这意味着取消信号在**编排循环这一层是干净的**——循环会在下一个 await 点抛出 `CancelledError` 并停止迭代，流式输出也会停。

**关键代码：**
```python
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：异步原生却卡在工具执行语义

**架构原理。** PydanticAI 全异步，`agent.run()` 是协程，最近还专门加固了"stream cleanup"和"interrupted-history capture"——也就是说**流的善后它做得很认真**，被取消时不会留下半截脏历史。

**关键代码与已知缺口：**
```python
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：检查点保住了状态，却保不住副作用

**架构原理。** LangGraph 用**检查点（Checkpointer）+ 重放**换来可恢复性，`interrupt()` 是它的人机协作原语。这套设计在"状态一致性"上是六框架里最强的——取消时，未提交的节点写入会被丢弃，状态停在最后一个干净的检查点上。

**关键代码与陷阱：**
```python
def charge_card(state):
    stripe.charge(amount=state["amount"])   # ① 副作用：已扣款
    return {"charged": True}                 # ② 此写入被检查点保存
# 取消后再 resume → 重放历史 → 如果 charge 没有 idempotency_key → ② 被重放 → 二次扣款
```

**优缺点。** 优点压倒性：长流程、可恢复、可调试重放，复杂工作流首选。缺点是这套重放模型和副作用天然冲突——**检查点只记录图状态，不记录外部世界状态**，副作用不回滚，重放时还会再放一遍。于是 LangGraph 的幽灵不在"取消当下"，而在"取消后续跑"那一刻集中爆发，且因为是确定性重放，**每次续跑都会稳定地多执行一次**，比随机 bug 更难排查。

**变现/训练营评估。** 这是六个里**最适合做高阶付费课**的框架，因为"可恢复工作流"是真需求。但课程必须把"幂等键 + 副作用隔离节点"作为前置章节，否则学员做出的可恢复 Agent，本质上是一台稳定的双花机器。

## 五、CrewAI：幽灵风险最高，同步阻塞工具是元凶

**架构原理。** CrewAI 是任务编排器，`kickoff()` 同步阻塞为主，`kickoff_async()` 提供异步入口。但它的工具大多以**同步函数**注册，跑在事件循环里等于一个不可中断的阻塞块。

**关键代码与症状：**
```python
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 / AG2：异步版可救，同步版别用

**架构原理。** AutoGen 分两条线：老版 0.2 同步、对话循环为主；新版（AG2 / autogen 0.4+）异步优先，`team.run()` 是协程，取消靠 asyncio。终止条件（`termination`）是它的强项——能基于消息数、关键词停，但这**不等于超时取消**。

**关键代码：**
```python
# 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 新手最常混淆的点，也是很好的教学抓手。

## 七、LlamaIndex Workflows：事件驱动，取消停在边界

**架构原理。** Workflows 是事件驱动 + 全 asyncio，每个 step 订阅事件、产出事件，事件按顺序在事件循环里流动。取消一个 workflow.run 协程，会在两个 step 之间停下来。

**关键代码：**
```python
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 取消模式"的载体，但要强调：**事件驱动的好处是边界清晰，坏处是边界内的盲区更隐蔽**。

## 八、Mermaid：取消信号在哪一层断掉

下面这张图把"取消信号"从用户一直画到副作用工具，标出六框架分别在哪个节点把它丢掉：

```mermaid
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)` 给整轮跑加一个硬超时，再用幂等键给每个副作用加一道去重——前者堵住"停不下"，后者兜住"停了也跑了"。这两行代码，能挡掉本文里八成的幽灵。

---

## 参考来源

1. [PydanticAI Issue #5837 — Define tool-execution cancellation semantics](https://github.com/pydantic/pydantic-ai/issues/5837) — PydanticAI 官方承认工具执行取消语义尚未定义，是本文"工具中断传播"评级的核心依据。
2. [SynapseAI — Agent Doesn't Cancel In-Flight Requests When User Aborts](https://ddaekeu3-cyber.github.io/synapse-ai/solutions/concurrency/agent-doesnt-cancel-in-flight-requests-on-user-abort) — 生产复盘："按 Stop 后继续跑 30+ 秒并计费""导航离开后后台仍在写库"，幽灵执行症状的直接证据。
3. [BSWEN — How to Handle Agent Timeout and Failure Recovery in Multi-Agent Systems (CrewAI vs LangGraph)](https://docs.bswen.com/blog/2026-04-17-agent-timeout-failure-recovery/) — 2026-04 实战复盘，记录 CrewAI agent 挂起、整条 pipeline 冻结的超时失败现场。
4. [OpenAI Agents SDK 官方文档](https://openai.github.io/openai-agents-python/) — `Runner.run` / `run_streamed` 的 asyncio 取消语义与 `RunResultStreaming.cancel()` 原语。
5. [LangGraph 官方文档](https://langchain-ai.github.io/langgraph/) — Checkpointer 检查点、`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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
