我给 AI Agent 一个稍微复杂点的任务、让它自己拆成几步去做,它确实拆得有模有样,可执行起来却经常翻车——后面那步明明要用到前面那步产出的结果,它却抢在前面没跑完就先干了、拿着一个空值或占位符往下走,最后整条链全错,排查很久才搞懂它把本来有先后依赖的几步,当成了互不相干、谁先谁后都行的一张平铺清单的深度复盘
这次踩的坑很有意思:Agent 看起来"很聪明"——它会分解任务、会调工具、单看每一步也都做对了,可结果就是不对。问题不在任何单步,而在它对这几步之间关系的理解。
故障现场:每一步看着都对,合起来却全错
我做了个能调多个工具的 Agent,给它一个典型的多步任务,比如:"给新用户开通服务"——这需要先创建用户账号拿到 user_id,再用这个 user_id 去创建对应的订阅,最后用订阅信息发一封欢迎邮件。我把工具都给齐了,让 Agent 自己规划执行。结果:
- 分解得很漂亮:Agent 确实把任务拆成了"创建账号 / 创建订阅 / 发欢迎邮件"几步,列得清清楚楚,看着挺专业。
- 但执行顺序常常乱:它有时先去创建订阅,可这时账号还没创建、根本没有 user_id,它就传了个空的或者瞎编的 user_id 进去。
- 用空值/占位符往下走:更糟的是它不报错也不停下,而是拿着那个 null 或占位符继续往下做,后面发邮件又引用了这个错误的订阅,整条链就这么错到底。
- 时灵时不灵:同样的任务,有时它恰好按正确顺序做了就成功,有时顺序一乱就失败,不稳定、不可复现,这让我一开始以为是偶发。
"每一步单独看都对、顺序一乱就全错、还时灵时不灵"——这几个特征合起来,把矛头指向了一个我没重视的地方:问题不在 Agent 会不会做某一步,而在它有没有意识到这几步之间存在先后依赖——后面的步骤需要前面步骤的产出作为输入。它显然没把这层依赖当回事,而是当成了一张"随便什么顺序做都行"的清单。我得去想清楚,Agent 规划任务时,到底是怎么对待步骤之间的关系的。
第一件事:搞懂任务分解出的子步骤,往往不是平级清单而是有依赖的图
顺着"它忽略了步骤间的依赖"这条线,我重新审视了"让 Agent 自己分解任务"这件事,才意识到一个被我和 Agent 都低估的事实——把一个复杂任务分解出来的若干子步骤,它们之间的关系绝大多数不是"平级、独立、任意顺序"的,而是带着先后依赖的:某些步骤必须等另一些步骤完成并产出结果之后才能开始,因为它要用到那个结果。
以我这个任务为例,三步之间的真实关系是一条依赖链:
- "创建订阅"依赖"创建账号"的产出——它需要账号返回的
user_id; - "发欢迎邮件"依赖前两步的产出——它需要用户信息和订阅详情。
也就是说,这三步必须按"创建账号 → 创建订阅 → 发邮件"的顺序来,因为后一步的输入,正是前一步的输出。它们构成的是一个有方向的依赖图(DAG),而不是一张可以打乱顺序的待办清单。
可问题在于:大模型在做任务分解时,天然倾向于产出一个"平铺的步骤列表",它很会"列出要做哪些事",却不会自动、可靠地建立并尊重这些事之间的依赖关系。它可能在脑子里隐约知道"得先有账号",但在实际一步步执行、尤其是被允许"并行"或自由选择下一步时,这个隐含的依赖很容易被丢掉——于是它就抢跑了:在 user_id 还不存在时就去创建订阅。而 LLM 的非确定性更是雪上加霜:它这次可能恰好顺序对了,下次就乱了,所以才"时灵时不灵"。我把这个"平铺清单 vs 依赖图"的差别对照出来:
# Agent 脑子里(错):一张平铺清单, 谁先谁后都行
[ ] 创建账号
[ ] 创建订阅 <- 被它当成可以独立先做
[ ] 发欢迎邮件
# 实际(对):一个有方向的依赖图, 后者要用前者的产出
创建账号 ──产出 user_id──> 创建订阅 ──产出 sub_id──> 发欢迎邮件
(必须先完成并拿到真实结果, 后一步才能开始)
# 抢跑的后果:user_id 还没有 -> 创建订阅传了 null/占位符 -> 错到底
真相大白:不是 Agent 不会做这些步骤,而是它把一组有内在先后依赖的工作,误当成了一张可以任意排序的平铺清单,于是在依赖还没满足(前一步结果还没产出)时就抢着执行了后一步,拿着空值一路错下去。错的根源,是步骤之间的依赖关系这个关键信息,在"分解—执行"的过程中丢失了、没有被显式表达和强制遵守。
第二件事:正解——让依赖显式化,按依赖驱动执行、用真实产出喂下一步
根因是"步骤间的依赖没被显式表达和强制遵守",那正解的核心就一句话:别让 Agent 凭感觉自由排序,而要把子步骤之间的依赖显式地表达出来,并按依赖关系驱动执行——依赖没满足就不准开始,且后一步的输入必须是前一步真实产出的结果。我做了这么几件事:
// 正解 1:让 Agent 规划时输出"带依赖声明"的计划, 而不是平铺清单
{
"steps": [
{ "id": "s1", "tool": "create_account", "args": {"name": "Alice"},
"depends_on": [] },
{ "id": "s2", "tool": "create_subscription",
"args": {"user_id": "${s1.user_id}"}, // 显式引用 s1 的产出
"depends_on": ["s1"] }, // 声明:必须等 s1 完成
{ "id": "s3", "tool": "send_welcome_email",
"args": {"user_id": "${s1.user_id}", "sub_id": "${s2.sub_id}"},
"depends_on": ["s1", "s2"] }
]
}
// 执行器按 depends_on 做拓扑排序: 依赖未完成的步骤绝不提前执行;
// ${s1.user_id} 这类引用, 在 s1 真正跑完后用它的真实返回值填充
这套做法有几个关键点:第一,依赖显式化——让 Agent 在计划里用 depends_on 明确声明每一步依赖哪些前置步骤,把"隐含在脑子里的顺序"变成"写在结构里的硬约束"。第二,按依赖驱动执行——执行器拿到计划后做拓扑排序,一个步骤只有当它依赖的所有步骤都成功完成,才被允许执行;依赖没满足,它就得等着,绝不抢跑。第三,用真实产出做输入——后一步参数里的 ${s1.user_id} 这种引用,要在 s1 真正执行完之后,用它返回的真实结果去填充,而不是让 LLM 瞎编一个或填空值。
这样一来,无依赖的步骤(如果有)可以放心并行,有依赖的步骤被严格串起来,既正确又尽量快。核心就一条:有依赖关系的多步任务,要把依赖显式建模成一张图、按图调度,而不是丢给 Agent 一张清单让它自己"感觉"该先做哪个。对关键流程,还可以让 Agent 先把这张依赖计划交给人审一眼再执行。
第三件事:同一类"有依赖关系却被当成无序集合"的坑,我后来又撞见好几个
这次踩坑让我看清了一个更普遍的模式:很多工作内部存在依赖关系(B 要用 A 的结果),可一旦我们把它表达成一个"清单/集合",这层依赖就不见了,执行者就会按错误的顺序去做。这种坑远不止 Agent:
- 构建系统/任务编排不声明依赖:Makefile、CI 流水线里任务不声明前后依赖,并行跑时后置任务在前置产物还没生成时就开跑、失败。
- 数据库迁移脚本顺序错乱:多个 migration 之间有依赖(先建表再加外键),按文件名乱序执行就会引用还不存在的表。
- 微服务启动顺序:服务 B 启动依赖服务 A 就绪,编排时不声明依赖,B 先起来连不上 A 就崩。
- 异步任务/消息乱序:有因果关系的事件(先下单后支付)被当成可乱序处理的独立消息,乱序到达就处理出错。
- 前端数据加载竞态:几个请求有数据依赖(拿到 id 再查详情),不串起来而是一起发,详情请求拿着 undefined 的 id 就发了。
它们的内核是同一个:一组工作之间真实存在的依赖关系(谁的输出是谁的输入、谁必须在谁之前),是一种关键的结构信息;如果你只把这些工作罗列成一个"集合/清单",这个结构信息就被抹掉了,而任何不知道依赖的执行者(无论是 Agent、调度器还是并发框架)就会假设它们相互独立、可任意排序,从而在依赖未满足时抢跑出错。所以,处理任何有内在依赖的工作,都必须把依赖显式表达出来(建成依赖图)、并让执行严格遵循它(按拓扑序、依赖驱动),而不能指望执行者自己"悟"出正确顺序。我把这套判断画成了一张图(见后文)。
| 场景 | 把有依赖的工作当无序集合的后果 | 正确做法 |
|---|---|---|
| Agent 多步任务 | 抢跑、用空值往下走、链式错 | depends_on 声明 + 拓扑执行 |
| CI/构建任务 | 后置任务在产物未就绪时开跑 | 显式声明任务依赖 |
| DB 迁移脚本 | 引用还不存在的表/列 | 有序版本号 + 依赖顺序 |
| 微服务启动 | 依赖方还没就绪就连接失败 | 声明依赖 + 就绪探针 |
| 前端并发请求 | 拿着 undefined 的 id 发请求 | 按数据依赖串行/await |
第四件事:平铺清单 vs 依赖图——一张对照表
这次事故逼我把"把多步任务当清单"和"当依赖图"的区别摆成一张表,设计 Agent 编排前先对照:
| 维度 | 平铺清单(错) | 依赖图 DAG(对) |
|---|---|---|
| 步骤间关系 | 视为独立、任意顺序 | 显式声明 depends_on |
| 执行顺序 | Agent 凭感觉、非确定 | 拓扑排序、依赖驱动 |
| 后一步的输入 | 可能空值/占位符/瞎编 | 前一步真实产出填充 |
| 依赖没满足时 | 照样抢跑、错到底 | 阻塞等待、绝不提前 |
| 能否并行 | 乱并行导致竞态 | 无依赖的才安全并行 |
| 稳定性 | 时灵时不灵、不可复现 | 顺序确定、可复现 |
看清这张表,编排的思路就该变:不能把多步任务当成"列出来让 Agent 挨个做"的清单,而要当成"带依赖边的图",由执行器按拓扑序调度、用真实产出串接。清单丢掉了依赖这层结构,图把它显式保留了下来。
第五件事:我曾经对"让 Agent 自己分解执行"想当然的几个误区
这场"抢跑全错"的事故,把我对 Agent 任务编排的一堆想当然照得清清楚楚:
| 我以为 | 实际上 |
|---|---|
| Agent 会分解任务就会安排好顺序 | 它会列步骤、却不可靠地建立和遵守依赖 |
| 步骤列出来了顺序它自然懂 | 隐含依赖执行时容易丢、会抢跑 |
| 后一步会自动等前一步的结果 | 不串依赖它就拿空值/占位符往下走 |
| 偶尔失败是模型不稳定的偶发 | 是没强制依赖、靠运气撞对顺序 |
| 把工具都给齐它就能编排好 | 给齐工具≠表达清楚步骤间依赖 |
| 多步任务就是一串待办清单 | 多是有方向依赖的图、不能任意排序 |
这些误区的根子是同一个:我默认"会做每一步"就等于"会按正确顺序把这些步骤串成一件事",把"执行单步的能力"和"编排多步的能力"混为一谈了。正因为我以为 Agent 既然能分解、能调工具,就自然会处理好步骤间的先后,我才没有把依赖关系这层最关键的结构显式地交给它、并强制它遵守,而是放心地让它"自己看着办"。把"完成各个局部"的能力,等同于"正确编排局部之间关系"的能力,忽视依赖这层结构需要被显式表达和强制,是这类编排型错误的共同根源。
第六件事:编排 Agent 多步任务、排查"抢跑/用空值"时,我现在的自检习惯
现在每当我设计 Agent 的多步编排、或排查"后一步拿着空值往下走",我都会先把步骤间的依赖关系摆到台面上。先看清依赖驱动执行该是什么样:
然后用这张自检图判断一个多步任务该怎么编排:
配套地,我把"按依赖拓扑执行、用真实产出填参数"固化成了一个简单的执行器骨架:
# 按 depends_on 拓扑执行: 依赖未完成的步骤绝不提前, 用真实产出填占位符
import re
def run_plan(steps):
results, done = {}, set()
remaining = {s["id"]: s for s in steps}
while remaining:
# 只挑"依赖全部已完成"的步骤来执行(依赖驱动)
ready = [s for s in remaining.values()
if all(dep in done for dep in s["depends_on"])]
if not ready:
raise RuntimeError("存在循环依赖或依赖缺失, 无法继续") # 防死锁
for s in ready:
args = resolve(s["args"], results) # 把 ${s1.user_id} 用真实结果填上
results[s["id"]] = call_tool(s["tool"], args)
done.add(s["id"]); del remaining[s["id"]]
def resolve(args, results):
def sub(v):
m = re.fullmatch(r"\$\{(\w+)\.(\w+)\}", v) if isinstance(v, str) else None
return results[m.group(1)][m.group(2)] if m else v # 引用前置真实产出
return {k: sub(v) for k, v in args.items()}
而排查一个"时灵时不灵"的多步任务时,我固定先确认是不是依赖被无视了:
# 排查清单:多步任务时对时错, 先怀疑依赖没被强制
1. 把 Agent 这次的执行顺序打出来, 和正确的依赖顺序对比
2. 看失败那次, 是不是某步在它依赖的步骤还没产出结果时就跑了
3. 看传入参数里有没有 null / 占位符 / 明显瞎编的 id
4. 若是 -> 不是模型偶发, 是没显式声明并强制 depends_on
5. 整改: 计划里补 depends_on + 引用前置产出 + 执行器按拓扑序调度
这套习惯的精髓,是"多步任务先问步骤间有没有依赖、有就显式声明 depends_on、按拓扑序执行、用前一步真实产出填后一步、时灵时不灵先怀疑依赖没强制"。它让我从"列出步骤丢给 Agent 自己排",变成了"把依赖建成图、按图调度"——核心始终是:把一个复杂任务分解出来的若干子步骤,它们之间的关系绝大多数不是平级独立可任意顺序的、而是带有先后依赖的(某些步骤必须等另一些步骤完成并产出结果之后才能开始因为它要用到那个结果),这些步骤构成的是一个有方向的依赖图 DAG 而不是一张可以打乱顺序的待办清单;而大语言模型在做任务分解时天然倾向于产出一个平铺的步骤列表、它很会列出要做哪些事却不会自动可靠地建立并尊重这些事之间的依赖关系,加上 LLM 执行的非确定性,就会在前一步结果还没产出时抢跑后一步、拿着空值或占位符或瞎编的值继续往下走且不报错、导致整条链错到底而且时灵时不灵不可复现;所以正解是不要把有依赖的多步任务当成让 Agent 凭感觉自由排序的清单,而要让 Agent 在规划时用 depends_on 显式声明每一步依赖哪些前置步骤把隐含的顺序变成写在结构里的硬约束、由执行器对这些步骤做拓扑排序使一个步骤只有当它依赖的所有步骤都成功完成才被允许执行依赖没满足就阻塞等待绝不抢跑、并且后一步参数里对前置产出的引用(如 ${s1.user_id})要在前一步真正执行完后用它返回的真实结果去填充而不是让模型瞎编或填空值,这样无依赖的步骤可以安全并行有依赖的被严格串起来既正确又尽量快,关键流程还可让依赖计划先过人审;更一般地,一组工作之间真实存在的依赖关系(谁的输出是谁的输入谁必须在谁之前)是一种关键的结构信息,一旦你只把这些工作罗列成一个无序的集合或清单这层结构信息就被抹掉了,任何不知道依赖的执行者(Agent、构建系统、调度器、并发框架、消息处理)都会假设它们相互独立可任意排序从而在依赖未满足时抢跑出错,所以处理任何有内在依赖的工作都必须把依赖显式表达出来建成依赖图并让执行严格按拓扑序依赖驱动地遵循它而不能指望执行者自己悟出正确顺序、更不能把会完成各个局部的能力等同于会正确编排局部之间关系的能力。
我立下的几条规矩
这场"抢跑全错"的事故,换来了我编排 Agent 多步任务时,刻进骨子里的几条铁律:
- 分解出的子步骤多是有依赖的图,不是可任意排序的清单。
- LLM 会列步骤、却不可靠地建立和遵守步骤间依赖。
- 让 Agent 显式声明 depends_on,把隐含顺序变成硬约束。
- 执行器按拓扑序调度,依赖没满足的步骤绝不提前执行。
- 后一步的输入用前一步真实产出填充,别瞎编/别填空值。
- 无依赖才并行,关键流程的依赖计划先过人审。
- 多步任务时灵时不灵,先怀疑依赖没被显式声明和强制。
附:一段让 Agent 输出"带依赖计划"的提示与校验骨架
最后留一段我自己让 Agent 产出可执行依赖计划、并在执行前校验的提示与代码骨架:
# 1) 提示 Agent: 不要给平铺清单, 要给带 depends_on 和产出引用的计划
PLAN_PROMPT = """
把任务分解为步骤, 以 JSON 输出。每个步骤必须包含:
- id: 唯一标识
- tool / args: 调用的工具和参数
- depends_on: 本步依赖的前置步骤 id 列表(没有则空数组)
要求: 若某步要用到前置步骤的产出, 必须在 args 里用 "${前置id.字段}" 引用,
并把该前置 id 写进 depends_on。不要把有先后依赖的步骤列成无依赖的平铺清单。
"""
# 2) 执行前先校验这张计划是不是合法的 DAG(无环、引用的依赖都存在)
def validate_plan(steps):
ids = {s["id"] for s in steps}
for s in steps:
for dep in s["depends_on"]:
assert dep in ids, f"{s['id']} 依赖了不存在的步骤 {dep}"
# 拓扑排序检测有没有环
indeg = {s["id"]: len(s["depends_on"]) for s in steps}
q = [i for i, d in indeg.items() if d == 0]
seen = 0
while q:
cur = q.pop(); seen += 1
for s in steps:
if cur in s["depends_on"]:
indeg[s["id"]] -= 1
if indeg[s["id"]] == 0: q.append(s["id"])
assert seen == len(steps), "计划里存在循环依赖!" # 有环就拒绝执行
# 校验每个 ${x.y} 引用的 x 确实在自己的 depends_on 里, 防"用了却没声明依赖"
return True
这段骨架的核心就一句:先逼 Agent 把依赖显式写进计划(depends_on + 产出引用),再在执行前校验它是一张合法无环的依赖图、且"用了某步产出就必须声明依赖它"——把"顺序对不对"从"靠模型每次发挥"变成"结构上就保证"。依赖一旦上了图、过了校验,抢跑和空值就再没有机会发生了。
写在最后
回头看,这场由"Agent 忽略步骤依赖、抢跑出错"引发的事故,真正教给我的,远不止"用 depends_on 声明依赖"这一个技巧。它让我对"能把一件复杂的事拆成一个个能做的小步,和能把这些小步按正确的关系组装回一件完整的事,是两种不同的能力;我们常常因为看到了前者,就想当然地以为后者也具备了",有了一次刻骨的体会。我栽跟头,是因为我看到 Agent 能漂亮地把任务分解成几步、又能熟练地执行每一步,就理所当然地认定它也能把这几步按正确的顺序串起来;我没意识到,"分解"产出的只是一堆零件,而把零件组装成能运转的整体,靠的是零件之间那些看不见的依赖关系——谁要插在谁后面、谁的输出是谁的输入;这层关系,Agent 在分解时并没有可靠地把它表达和保留下来,它给我的是一张抹掉了依赖的平铺清单,而我却拿着这张清单,以为它已经包含了正确的顺序。这让我领悟到一个关于"局部与结构"的深刻认知:完成一件由多个部分组成的复杂工作,需要两样东西:一是每个局部都被正确地完成,二是这些局部之间的关系(依赖、顺序、数据流)被正确地组织;我们的注意力天然容易被前者吸引——因为局部是看得见、摸得着、能逐个验证的,而"关系"是抽象的、隐含的、不直接显形的;可恰恰是这层"关系",决定了一堆正确的局部能不能真正合成一个正确的整体:局部全对、关系错了,整体照样是错的(就像零件全是好的、装配顺序错了,机器照样不转);所以,面对任何"把多个部分组合成一个整体"的工作,我都不能只盯着"每个部分做对没有",而必须同等地、甚至更警惕地去对待"部分之间的关系有没有被显式地表达出来、并被正确地遵守"——因为关系不会自动浮现,它需要被主动建模、显式声明、强制执行。这给了我一种面对"一切'把零散的部分组织成有序的整体'之事"时的审慎:每当我要把一件事拆成多步、交给某个执行者(Agent、流水线、并发任务)去做,我都追问"这些步骤之间有没有依赖?我有没有把这些依赖显式地表达出来、并让执行强制遵循?还是我只给了一张抹掉了依赖的清单、指望它自己排对"——把依赖当成一等公民显式建模和强制,而不是寄望于执行者的悟性;"分解之外更要组织、把局部间的依赖显式建图并强制遵循",是编排好 Agent、也是组织好一切多部件复杂工作的关键。认清子步骤是有依赖的图、LLM 不会自动守依赖、要显式声明并按拓扑序强制执行——这,是我用一次"Agent 抢跑、拿着空值把整条链做错"的事故,换来的、关于 AI Agent、也关于如何把零散步骤组织成正确整体的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次让 Agent 跑多步任务时,先停一下问"这几步之间谁依赖谁?我把依赖说清楚、让它按序执行了吗",那我对着那一串"时对时错"的执行日志挠的那阵头,就值了。
—— 别看了 · 2026