跳到正文

TECH NOTE

5801 字 29 分钟 系列 20 / 22

03 Async subagents:后台跑的子智能体(启动、轮询、中途干预)

ON THIS PAGE / 目录34 节

一句话讲透:同步子智能体像打电话——对方说完你才能走开;异步子智能体像派个同事去办事,先给你一张工单号,你该干嘛干嘛,随时能问一句”到哪了”,也能改需求,实在不行还能叫他先停下。

这篇在系列里的位置:04 Subagents 模式原理 的「决策一:同步还是异步」 讲的是”同步 vs 异步”这个概念,以及通用的后台作业三工具模式(start / status / get result);14 底层机制与抓包 把手写 Tool 包装和内置 subagents 的差别拆开了;本目录 01 静态子智能体 讲静态(同步)子智能体的配置面,02 动态子智能体 讲运行时动态生成子智能体。

本篇负责的那一层:如果 04 篇回答的是”为什么要异步”,这篇回答”Deep Agents 里到底怎么异步”——后台作业、长任务、并行工作流、中途干预(steering)与取消、传输方式(transport)与部署拓扑。概念在 04,API 在这儿。

它解决什么问题#

原文的定位很直接:异步子智能体让主管(supervisor)启动一个立刻返回的后台任务,于是主管可以在子智能体并发干活的同时继续和用户对话。主管随时能查进度、补指令、取消任务。

判断标准就一句:主管需要结果才能往下走 → 用同步(01 篇 的静态子智能体、02 篇 的动态子智能体都是同步的);用户不该干等、活儿又长 → 用异步。

情形结论原因
长任务(几分钟到几十分钟的调研、批量生成、长数据拉取)用异步同步会把对话冻住那么久
多个互不依赖的活儿用异步一起甩出去,真正并行
要中途改方向 / 要能取消用异步只有异步有 update / cancel
状态要跨多轮对话活着用异步task ID 存在主管 graph 的专用通道里
一分钟内能跑完的小任务别用启动 + 轮询的额外往返比直接等还慢
下一步动作依赖这个结果别用主管拿不到结果只能空转,异步变成忙等
本地只想 python xx.py 跑通别用调用要打到 Agent Protocol 服务器上,本机不一定有

📌 注意:这张”用 / 别用”表是按原文的对比表反推的判断口径,原文没有单独写”什么时候不要用”。

同步 vs 异步:原文那张对照表#

维度同步子智能体 Sync异步子智能体 Async
执行模型 / 并发性主管阻塞,直到子智能体做完;并行但阻塞立刻返回作业 ID 并继续;并行且非阻塞
任务中更新做不到用 update_async_task 追加指令
取消做不到用 cancel_async_task 取消运行中的任务
有状态性无状态,两次调用之间不留东西有状态,自己一条 thread 跨交互保持
最适合主管必须先等到结果才能继续的任务长跑、复杂、要在聊天里交互式管理的任务

整体机制#

graph TB
U(["用户 User"]) --> S["主管 Supervisor"]
S -->|"start_async_task 启动"| J["作业 Job<br/>task_id 就是 thread_id"]
S -->|"立刻回话,不阻塞"| U
J -->|"Transport 传输方式"| T{"有没有 url?"}
T -->|"没有 url → ASGI 传输"| A["异步子智能体 A: researcher<br/>同进程里的独立 thread"]
T -->|"有 url → HTTP 传输"| B["异步子智能体 B: coder<br/>远端 Agent Protocol 服务器"]
S -->|"check / update / cancel / list"| T
A -.->|"状态与结果"| S
B -.->|"状态与结果"| S

这张图在说什么:

① 主管(Supervisor)是唯一和用户对话的人;异步子智能体(Async Subagent)不直接对用户说话,只对主管负责。

② 主管启动一个作业(Job)时拿到的是一个 task_id 而不是结果——task_id 就是服务器上的 thread_id,“作业”本质是一条独立会话线程。

③ 主管后续所有动作(查状态、补指令、取消、列表)都经过传输方式(Transport)这一层:url 空 → ASGI 传输走进程内调用,url 有值 → HTTP 传输走网络。换传输方式不改业务代码,只改配置。

时间轴:一次真实交互长什么样#

时间往下走(主管视角)
T0 用户 → 主管: "把这份合同审一下,顺便查下对手公司背景"
T1 主管 → start_async_task("审合同", "legal_reviewer")
└─ 立刻返回 task_id: 7f3a9c(= 服务器上的 thread_id)
T2 主管 → 用户: "已开始审查(任务 ID 7f3a9c),你先忙别的" ← 交回控制权
T3 用户 → 主管: "那先聊点别的" ← 同步子智能体永远到不了这一步
T4 用户 → 主管: "审得怎么样了?" → check_async_task("7f3a9c") → running
T5 用户 → 主管: "方向改一下,重点看违约金条款"
主管 → update_async_task("7f3a9c", "重点看违约金") ← 中途干预
(旧 run 被打断,带完整历史 + 新指令重启,task_id 不变)
T6 用户 → 主管: "算了别审了" → cancel_async_task("7f3a9c") → cancelled
T7 用户 → 主管: "刚发的背景调研呢?"
主管 → check_async_task("b21e04") → success + result ← 拿结果
T8 主管 → 用户: 把结果整理成人话汇报

和 04 篇的”同步子智能体”对照着看#

04 篇的同步子智能体本篇的异步子智能体
一次调用的形态主管调 task,等返回值调 start_async_task,拿 task_id 立刻走
用户在等待期间什么都看不到,对话被冻住可以继续聊,也能再派新任务
进度怎么来没有进度,只有最终结果check_async_task 拉当前状态
改需求 / 收手只能撤了重开update_async_task / cancel_async_task
层次通用取舍(概念层)概念在 Deep Agents 的落地 API(实现层)

04 篇那个”三工具模式”(start / status / get result)就是这套东西的极简版;Deep Agents 把它做成了 5 个工具 + 一个中间件 + 一个专用状态通道。

怎么配(Configure async subagents)#

异步子智能体就是一份 AsyncSubAgent 规格列表,每条指向一个 Agent Protocol 服务器:

from deepagents import AsyncSubAgent, create_deep_agent
async_subagents = [
AsyncSubAgent( # 不写 url → ASGI 传输(和主管同一个部署里)
name="researcher",
description="Research agent for information gathering and synthesis",
graph_id="researcher",
),
AsyncSubAgent( # 写了 url → HTTP 传输(远端部署)
name="coder",
description="Coding agent for code generation and review",
graph_id="coder",
# url="https://coder-deployment.langsmith.dev"
),
]
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash", subagents=async_subagents
)

关键点:同一个 subagents= 参数,靠有没有 url 决定这条规格走哪种传输方式,靠对象类型区分同步 / 异步。

官方给这个例子准备了公开 trace:https://smith.langchain.com/public/0727618d-02b5-4e99-b7c7-79d6f37f234d/r

AsyncSubAgent 的字段#

字段类型说明
namestr必填。唯一标识,主管启动任务时用它指名道姓。
descriptionstr必填。这个子智能体干什么,主管靠它决定派谁去。
graph_idstr必填。Agent Protocol 服务器上的 graph ID(或 assistant ID)。LangGraph 部署里必须和 langgraph.json 里注册的名字对上。
urlstr可选。不写 → ASGI 传输(进程内);写了 → HTTP 传输打远端。
headersdict[str, str]可选。给远端服务器带的自定义请求头,自建 Agent Protocol 服务器做认证时用。

同部署时的 langgraph.json#

LangGraph 部署里,被同部署(co-deployed)的图都要注册在同一个 langgraph.json:

{
"graphs": {
"supervisor": "./src/supervisor.py:graph",
"researcher": "./src/researcher.py:graph",
"coder": "./src/coder.py:graph"
}
}

这不是可选项:ASGI 传输要求两边在同一份注册表里,否则主管按 graph_id 找不到图。

和静态 SubAgent 的差异(01 篇 对照)#

维度静态 SubAgent异步 AsyncSubAgent
跑在哪主管进程内(内联子图)独立部署上的 Agent Protocol 服务器(或同进程 ASGI)
主管怎么调task 工具,同步阻塞5 个 *_async_task 工具,非阻塞
必填字段name、description、提示词/工具name、description、graph_id
提示词在哪配规格里直接写 system_prompt规格里没有这个字段,提示词在远端那张图上
状态无状态,一次调用一次结果有状态,自己一条 thread 跨交互保持
中途改 / 取消都不支持update_async_task / cancel_async_task
前置条件无需额外环境需要一个能跑的 agent server / 部署环境(这一篇最容易踩的坑)

主管手里多了哪些工具#

配置了异步子智能体后,Deep Agents stack 会自动包含 AsyncSubAgentMiddleware,主管因此多出 5 个工具:

工具作用返回
start_async_task启动一个新的后台任务任务 ID(立刻返回)
check_async_task取任务的当前状态与结果状态 + 结果(若已完成)
update_async_task给运行中的任务发新指令确认 + 更新后的状态
cancel_async_task停掉运行中的任务确认
list_async_tasks列出所有被跟踪的任务及其实时状态所有任务的摘要

主管的 LLM 像调其它工具一样调它们;建 thread、管 run、持久化状态都由 middleware 自动处理(原文)——你不需要自己写”任务注册表”。

入参长什么样(原文只给了工具名和用途,这里按本机 0.7.5 源码的 schema 补充):

工具入参
start_async_taskdescription(任务描述)、subagent_type(必须是已注册的异步子智能体名)
check_async_tasktask_id(原样传 start_async_task 返回的那个字符串)
update_async_tasktask_id、message(追加的指令或上下文)
cancel_async_tasktask_id
list_async_tasksstatus_filter:running / success / error / cancelled / all(默认 all)

生命周期状态机#

stateDiagram-v2
[*] --> running: start_async_task 新建 thread + run,返回 task_id
running --> running: update_async_task 打断旧 run,带历史 + 新指令重启
running --> success: run 成功,check 时从 thread state 取最终输出
running --> error: run 失败,返回错误信息
running --> cancelled: cancel_async_task 调服务器 runs.cancel()
success --> [*]
error --> [*]
cancelled --> [*]

终态(terminal)就是不会再变的状态。原文明确列了 success、error、cancelled;本机 0.7.5 源码里这个集合是 {cancelled, success, error, timeout, interrupted}——多出来的 timeout / interrupted 属于 SDK 侧状态,原文未提及。

五个动作在服务器上各自做了什么(原文逐个说明)#

  • Launch(启动):在服务器上新建一条 thread,用任务描述作为输入起一个 run,把 thread ID 当 task ID 返回。主管把 ID 报给用户,然后不去轮询等完成。
  • Check(查询):拉当前 run 状态。run 成功了就再取一次 thread state 抽出子智能体的最终输出;还在跑就如实告诉用户还在跑。
  • Update(更新):在同一条 thread 上用 interrupt 多任务策略起一个新 run。旧 run 被打断,子智能体带完整对话历史 + 新指令重启,task ID 不变。
  • Cancel(取消):在服务器上调 runs.cancel(),并把任务标记为 "cancelled"。
  • List(列表):遍历所有被跟踪任务;非终态任务并发去服务器拉实时状态,终态任务直接返回缓存值(省一次网络往返)。

看第 3 条:update 不是”追加一条消息就完了”,而是打断重启。这解释了为什么干预后子智能体还记得前面聊过什么(历史在),也解释了为什么干预本身有成本(一次新 run)。

状态管理(Understand state management)#

任务元数据存在主管 graph 上一个专用状态通道 async_tasks 里,和消息历史分开。

为什么必须分开?因为 deep agents 在上下文窗口塞满时会压缩消息历史(compaction / summarization)。如果 task ID 只存在于某条工具消息里,压缩之后就被摘要掉了,主管从此失忆——用户再问”上次那个调研呢”,主管答不上来,任务也取消不掉。专用通道保证无论压缩多少轮,主管都能通过 list_async_tasks 想起自己派过哪些活。

每个被跟踪的任务记录这些字段(原文):

字段含义
task ID / thread ID任务标识;本机源码里 task ID 就等于 thread ID
agent name / run ID哪个异步子智能体在跑;该 thread 上当前这次执行的 run ID
status当前状态(running / success / error / cancelled 等)
created_at任务创建时间
last_checked_at最后一次通过 SDK 查状态的时间
last_updated_at状态发生变化、或通过 update 发了跟消息的时间

后两个时间戳分工挺讲究:last_checked_at 记录”你查过”,last_updated_at 记录”它变过”。想判断”这个状态是不是老古董”,看这两个字段就够了。

和主管的关系可以概括成三句话:

  1. 状态挂在主管的 graph 上,不在子智能体那边——子智能体只管自己的 thread 和 run。
  2. 主管是唯一的读写方:启动、查询、更新、取消都改这条通道,子智能体压根不知道有这张表。
  3. 状态随主管会话持久化(配了 checkpointer 就落库),所以主管重启或换轮次后依然能接管之前派的活。

选传输方式(Transport)#

ASGI 传输(同进程部署)#

规格里不写 url 就用 ASGI 传输:LangGraph SDK 的调用被路由成进程内函数调用,不走 HTTP。LangGraph 部署下要求两张图注册在同一个 langgraph.json。

原文给的结论:省掉网络延迟;不需要额外认证配置;子智能体仍是独立的一条 thread,有自己的状态(不是”变成一次函数调用就没状态了”);这是推荐的默认选择。

HTTP 传输(远端)#

加个 url 字段就切到 HTTP 传输,SDK 调用走网络打到远端 Agent Protocol 服务器:

from deepagents import AsyncSubAgent
AsyncSubAgent(
name="researcher",
description="Research agent",
graph_id="researcher",
url="https://my-research-deployment.langsmith.dev",
)

认证方面(原文):LangGraph 部署由 SDK 用环境变量 LANGSMITH_API_KEY(或 LANGGRAPH_API_KEY)自动处理;自建的 Agent Protocol 服务器可能用别的认证机制。

什么时候值得付这份网络和运维成本:子智能体要独立扩缩容、资源画像不同(比如要 GPU)、或者干脆是别的团队维护的。

两种传输方式对照#

ASGI 传输HTTP 传输
触发条件 / 调用路径不写 url;进程内函数调用写了 url;网络请求
延迟无网络开销有一次网络往返
认证不需要额外配SDK 读 LANGSMITH_API_KEY / LANGGRAPH_API_KEY,或自建认证
前置要求同部署 + 同一份 langgraph.json远端有可用的 Agent Protocol 服务器
适用默认起步、本地和单部署独立扩缩容、异构资源、跨团队

⚠️ 警告(本机 0.7.5 源码观察,原文未说明):ASGI 传输(url=None)只在异步调用路径上可用。本机源码里,同步客户端拿到没有 url 的规格会直接报错:ASGI transport (url=None) requires async invocation。所以纯异步子智能体 + ASGI 的组合,要用 ainvoke / astream 这类异步入口跑主管,别用同步 invoke。

💡 提示(本机 0.7.5 源码观察,原文未说明):HTTP 传输的请求头默认会补上 x-auth-scheme: langsmith(除非你在 headers 里自己给了)。自建服务器一般忽略它,真要覆盖就用 headers。

选部署拓扑(Deployment topology)#

单部署(Single deployment)#

所有 agent 同部署、同服务器,用 ASGI 传输;LangGraph 部署下所有图注册进一份 langgraph.json。

单部署(推荐起步)
一个服务器 / 一份 langgraph.json
主管 supervisor
├─ ASGI 传输 ─▶ researcher (同进程,独立 thread + 独立状态)
└─ ASGI 传输 ─▶ coder (同进程,独立 thread + 独立状态)

一个服务器管到底,零网络延迟;缺点是大家抢同一份资源,谁把 worker 占满大家一起排队。

拆分部署(Split deployment)#

主管一台服务器,子智能体另一台,用 HTTP 传输。

拆分部署
服务器 A:主管 ──HTTP + 认证──▶ 服务器 B:researcher / coder

适合:子智能体需要不同的计算画像(CPU 密集 / GPU / 内存大户),或者要独立扩缩容。

混合(Hybrid)#

一部分子智能体同部署走 ASGI,另一部分远端走 HTTP:

from deepagents import AsyncSubAgent
async_subagents = [
AsyncSubAgent( # 不写 url → ASGI(同部署)
name="researcher", description="Research agent",
graph_id="researcher",
),
AsyncSubAgent( # 写了 url → HTTP(远端)
name="coder", description="Coding agent",
graph_id="coder", url="https://coder-deployment.langsmith.dev",
),
]
混合部署
用户 ─▶ 主管(服务器 A)
├─ ASGI ─▶ researcher 同进程:轻量、频繁调用
└─ HTTP ─▶ coder 远端:重资源、独立扩缩容

官方混合部署示例的公开 trace:https://smith.langchain.com/public/dddcb1d1-92ac-468b-aa34-5d9070661aa1/r

原文的推荐路线很清楚:从单部署开始(一个服务器管到底、零网络延迟),等真出现资源或组织边界了再拆:拆分部署适合子智能体资源画像差异大、要独立扩缩容的场景(代价是多一套部署 + 认证 + 网络延迟);混合适合大部分子智能体轻量、个别重的场景(代价是配置更碎)。

本机可用性实测(deepagents 0.7.5)#

结论先给:这一篇的能力在本机可用,API 已经导出——但能不能真跑起来取决于有没有能跑的 agent server / 部署环境。

已核实的事实(本机 deepagents 0.7.5):

  • from deepagents import AsyncSubAgent, AsyncSubAgentMiddleware 可用,两个符号都已导出。
  • AsyncSubAgent 的字段是 name / description / graph_id / url / headers,和原文表格一致。
  • create_deep_agent(model=..., subagents=[AsyncSubAgent(...)]) 能构造出 agent;工具里确实有 start_async_task、check_async_task、update_async_task、cancel_async_task、list_async_tasks 五个;状态通道名 async_tasks 与中间件 AsyncSubAgentMiddleware 都在本机包里。
from deepagents import AsyncSubAgent
spec = AsyncSubAgent(name="researcher", description="Research agent",
graph_id="researcher") # 不写 url → ASGI 传输
print(spec)

但请注意边界:构造 agent 不等于跑得通任务。任务真正执行要靠 Agent Protocol 服务器——同进程 ASGI 传输需要同一个部署(同一份 langgraph.json,且走异步调用入口),远端 HTTP 传输需要一台真实的远端服务。本地单纯 python xx.py 不一定具备这个环境;原文整篇也是围绕部署环境展开的(langgraph.json、langgraph dev、LangSmith Deployments)。

📌 注意(原文未说明):本机实测里只传一个 AsyncSubAgent 时,构造出的 agent 里同时还有同步的 task 工具——默认的 general-purpose 同步子智能体仍会被挂上。两者并存不冲突,但排查”主管为什么走了同步路径”时要记得这一点。

最佳实践#

本地开发时给工作进程池(worker pool)定容量#

用 langgraph dev 在本地跑时,要把工作进程池调大以容纳并发的子智能体 run。每个活跃 run 占一个 worker 槽位,算账很简单:1 个主管 + 3 个并发子智能体任务 = 需要 4 个槽位;槽位给少了不会报错,而是排队(launch 一直等着),这是最容易被误判成”卡死”的现象。

Terminal window
langgraph dev --n-jobs-per-worker 10

子智能体的 description 怎么写#

主管就是靠 description 决定派谁去的,所以要具体、以动作开头。原文给了一好一坏两个例子:

AsyncSubAgent( # 好例子:说清手段 + 什么场景该找我
name="researcher",
description="Conducts in-depth research using web search. Use for questions requiring multiple searches and synthesis.",
graph_id="researcher",
)

反面例子:AsyncSubAgent(name="helper", description="helps with stuff", graph_id="helper")——等于没说,主管只能瞎猜,然后派错人。

用会话线程 ID(thread ID)做追踪#

LangGraph 部署下,每个异步子智能体 run 都是标准的 LangGraph run,在 LangSmith 里完整可见。主管的 trace 里能看到 launch / check / update / cancel / list 这些工具调用,每个子智能体 run 是另一条独立 trace,两边靠 thread ID 串起来。

所以排障姿势是:用 thread ID(= task ID)把”主管的编排 trace”和”子智能体的执行 trace”对上——先看主管什么时候派的活,再看子智能体那边跑到哪一步。

排障清单#

原文的 Troubleshooting 给了四个问题,每个都是”模型行为”层面的坑。

主管启动后立刻轮询#

  • 症状:主管刚 launch 完就在循环里调 check,把异步硬生生用成了阻塞。
  • 原因:模型的本能是”我派了活就该盯着”,于是自己把非阻塞变成了忙等。
  • 处理:middleware 会注入系统提示规则来阻止这种行为;如果还在轮询,就在主管的 system_prompt 里再加一条硬约束:
from deepagents import create_deep_agent
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
system_prompt="""...your instructions...
After launching an async subagent, ALWAYS return control to the user.
Never call check_async_task immediately after launch.""",
subagents=async_subagents,
)

官方这个问题的公开 trace:https://smith.langchain.com/public/181f8fc2-b3aa-4c4b-b23d-d4204db98849/r

主管报的是旧状态#

  • 症状:主管直接引用对话历史里早先那次的状态,而不去重新 check。
  • 原因:对话历史里的状态天然过期,模型却当成了当前值。
  • 处理:middleware 提示里已写明”对话历史里的任务状态永远是旧的”;如果还犯,就在系统提示里明确要求”报状态前必须先调 check 或 list”。

任务 ID 查不到#

  • 症状:check 或 cancel 失败,因为主管把 task ID 截断或改写格式了。
  • 原因:模型倾向于”省略中间那段”或”美化一下”,而 task ID 必须逐字精确。
  • 处理:middleware 提示要求始终使用完整 task ID;如果还截断,原文说这通常是模型相关的毛病——换个模型,或在系统提示里加”永远显示完整 task_id,绝不截断或缩写”。

子智能体只排队、不执行#

  • 症状:启动一个子智能体后长时间挂着,或者很久才开始跑。
  • 原因:工作进程池被占满了,新 run 没有槽位。
  • 处理:用 --n-jobs-per-worker 把池子调大,参考工作进程池容量算法。

四个问题排一下优先级:先怀疑模型行为(前三个),再怀疑资源(第四个)。前三个的修法都在提示词层,第四个在启动参数层。

参考实现#

  • async-deep-agents 仓库:https://github.com/langchain-ai/async-deep-agents Python 和 TypeScript 两种语言的可运行示例,都能部署到 LangSmith Deployments;里面演示了一个主管带 researcher 和 coder 两个子智能体、以后台任务方式并跑。想抄作业就从这里开始,它把”主管 + 两个后台子智能体”的最小闭环跑通了。

小结#

  1. 异步子智能体 = 后台作业 + 主管不阻塞。启动立刻拿 task ID,用户该聊天聊天,随时能查进度、改需求、取消——这正是 04 篇 讲的”异步”概念在 Deep Agents 里的落地 API。
  2. 配置只差一个 url:AsyncSubAgent 必填 name / description / graph_id;不写 url 走 ASGI 传输(同部署、推荐默认),写了走 HTTP 传输(远端、要认证)。
  3. 主管多 5 个工具:start_async_task / check_async_task / update_async_task / cancel_async_task / list_async_tasks。update 是”打断重启、task ID 不变”,cancel 走服务器的 runs.cancel()。
  4. 状态存在主管 graph 的 async_tasks 通道里,刻意和消息历史分开——因为消息会被压缩,task ID 一旦进了被摘要掉的工具消息就找不回来了。
  5. 部署拓扑从单部署起步:不够用了再拆分或混合,别一上来就上多套部署 + 认证 + 网络延迟。
  6. 本机能导入、能构造,但要有 server 才跑得通:本机 deepagents 0.7.5 已导出 AsyncSubAgent 与 AsyncSubAgentMiddleware;端到端跑通还需要能跑的 agent server / 部署环境。

一句话选型#

用户不该干等、活儿又长又可能要改方向 → 用异步子智能体(先单部署 + ASGI 起步);主管必须拿到结果才能往下走 → 老老实实用 01 篇 / 02 篇 的同步子智能体,别为了”显得高级”把对话冻住。

03 Async subagents:后台跑的子智能体(启动、轮询、中途干预)
https://aicooleo.cn/blog/posts/deepagents-03-async/
作者
aicooleo
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0
多 Agent 精讲 · 第 20 / 22 篇 返回合集