1. 不是所有任务都适合扔给 AI:先理解"顺序"为什么成了瓶颈
先讲一个我最近的真实经历。团队里有个小伙伴用 Claude Code 处理一批文本清洗任务,需求很简单:先按规则拆分段落,再对每个段落做实体识别,最后把结果汇总成结构化 JSON。他当时为了省事,把整个流程写进一个巨大的 prompt,让模型"自己看着办"。结果模型确实照办了,但顺序完全乱了——先做了实体识别,再回头拆分段落,导致后面所有实体都挂在错误的上下文上,输出质量一塌糊涂。
这个问题不是偶发,而是当前大模型工作方式的必然结果。无论是 Claude、GPT 还是其他主流模型,本质上都是"下一步 Token 预测器",它们擅长的是"根据上下文生成合理的下一步",但天生不擅长"严格保证用户指令中被隐含的先后依赖关系"。你告诉它"先 A 再 B 再 C",它大概率会照做;可一旦 A、B、C 之间还存在数据依赖、输出格式依赖、需要中间校验的环节,模型的短期注意力很容易丢失这种约束。尤其是任务一旦超过三五个步骤,或者每一步的输入都是上一步的产物时,纯靠 prompt 去约束执行顺序,基本等于赌运气。
很多人第一反应是:那我给 AI 写个 ToDo 清单总行了吧?比如:
- 读取输入文件
- 拆分段落
- 提取实体
- 汇总结果
看起来逻辑清晰,但问题在于:ToDo 清单只是向模型描述了一个愿望,模型并没有被"锁死"在每一步的边界里。 它可能在执行第 2 步时顺便做了第 3 步的判断,也可能在第 4 步突然觉得第 1 步的数据格式不对,擅自回头调整。模型的工具调用(tool use)本身也是流式的,Claude Code 这类 Agent 产品会自主决定何时调用工具、调用哪个工具、以什么顺序调用。一旦任务复杂,这种自主性就变成了失控源。
那真正靠谱的做法是什么?答案是:把任务的"顺序约束"从模型的大脑中剥离出来,交给一个它无法随意篡改的调度结构。这就是 DAG——有向无环图(Directed Acyclic Graph)。
我第一次把 DAG 用在 Claude Code 工作流里时,团队里有人质疑:这不就是把 ToDo 换个名字吗?实际跑完两轮后所有人都闭嘴了。区别在于:ToDo 是写给模型看的"建议",DAG 是交给执行框架的"法律"。DAG 里的每个节点是一个明确且独立的执行单元,节点之间的边是强制依赖关系。模型可以在每个节点内部自由发挥,但它无法跳过某个节点,无法逆转节点顺序,也无法在上一个节点没有成功产出的情况下贸然进入下一个节点。
这篇文章我写给谁?如果你正在用 Claude Code、Cursor 这类 AI 编程/Agent 工具处理多步骤任务;如果你发现自己每次给 AI 布置复杂任务都要反复强调先后顺序,结果还是经常出错;如果你已经受够了"prompt 越写越长,模型却越来越糊涂"——那这篇文章就是写给你的。我会从 DAG 的核心原理讲起,然后给出可以在 Claude Code 里直接落地的方案,最后聊聊落地时的坑和排查思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么 Claude Code 这类 Agent 特别需要外部顺序约束
2.1 Agent 的自主性与任务稳定性之间的天然矛盾
Claude Code 本质上是"能调用工具的 Agent"。它有自己的"想法"能力——模型在生成最终回复之前,会先经过一段内部推理过程,决定下一步做什么。这个设计带来了极大的灵活性,但也带来了一个副作用:模型会依据自己每一步的中间状态决定下一步动作,而这个决策过程对用户来说往往是不可控的。
用一个不太恰当但很形象的类比:你雇了一个能力很强的实习生,你跟他说"今天把这份报告整理好"。实习生很卖力,但他可能会因为觉得某个数据不太对,就先去找其他数据源核实;又可能觉得某个段落结构不顺眼,顺便帮你重写了。结果到下班时,报告确实出来了,但结构跟你要的完全不一样。ToDo 清单就像你贴在实习生桌上的便利贴,他忙起来根本顾不上看;而 DAG 则像一份带强制节点的项目计划,每一项交付物必须经过检查点确认,才能进入下一阶段。
在 Claude Code 中,这种"自主决策"的能力体现在多个层面:它可以选择调用工具、读取文件、执行 bash 命令、甚至修改自己的记忆文件。当我给 Claude Code 布置一个包含数据抓取、清洗、分析、出图四个阶段的任务时,模型很可能会自作主张地在"清洗"阶段就开始做部分"分析"——因为分析逻辑恰好被它联想到了。这种提前执行在单步任务中或许无害,但在多依赖链路上,一个节点提前执行往往意味着它使用的数据还没准备好,最后结果自然崩。
2.2 模型的 Token 窗口与"长任务失忆"
做过复杂任务的朋友一定有过这种感觉:Claude Code 跑一个长链路任务时,前面的上下文越堆越长,到了后半程,模型对早期指令的遵循度明显下降。这不是错觉,而是 Transformer 架构的一个天然短板:当输入 Token 数量逼近窗口上限时,模型对早期 Token 的注意力权重会被大量后续 Token 稀释。
比如你给 Claude Code 的初始指令是"严格按照这个流程执行:第一步做数据清洗,第二步做特征工程",然后任务跑到中段时,上下文中已经积累了五十多轮工具调用记录和日志,每轮都有几千 Token。此时模型再读到"特征工程"阶段的任务描述,它对"数据清洗已完成"这个状态记忆已经变得模糊了。它可能觉得某些字段需要回炉重造,于是调用了清洗脚本,覆盖了原本已经干净的数据。
ToDo 清单在这种场景下几乎是无效的。因为 ToDo 本身也在上下文里,同样会被稀释;而且 ToDo 项之间没有"完成/未完成"的强语义绑定,模型不会主动去勾选。DAG 的解法是把任务状态外置——节点是否执行完毕,由外部调度器记录,不依赖模型记忆。模型每次进入新节点时,只需要面对这一个节点的输入和指令,不需要时刻背着整个流程。这就好比你给实习生安排了一天的工作,不是让他背下所有任务,而是每做完一步就回来找你领下一步的任务单,你手里有个总进度表,他完成到哪一步由你说了算。
2.3 工具调用的不确定性依赖
在 Claude Code 这类环境中,真正的风险还不只是模型乱序,而是工具调用的结果不可预测。一个写文件的工具,可能会因为磁盘权限失败;一个执行 python 脚本的工具,可能会因为依赖缺失而报错;一个读取远程 API 的工具,可能会因为网络超时而返回空结果。这些不确定性意味着:每个节点执行后,都必须有明确的成功/失败校验,否则后续节点基于错误的数据继续执行,会一路错到底。
我自己踩过一个非常深刻的坑:当时让 Claude Code 批量抓取网页摘要并存入本地文件。原始流程写在一个大 prompt 里,没有对"写入文件"做校验。结果某次运行时,网页抓取部分因为目标站点改版返回了 403,Claude Code 在日志中看到 403 后,居然"聪明地"抓取了缓存页面并存入了文件。下游的摘要节点不知道这些缓存页面内容已经过时,照样生成了一批充满错误信息的摘要。问题根子在于:上游执行失败后,系统没有在节点边界上中止链路,模型被自己的"灵活性"带偏了。
DAG 在这里的意义就在于:每一条边都代表一个条件,只有上游节点以成功的状态结束,下游节点才被允许开始。状态判断不是模型做的,而是执行框架做的。框架不会因为模型觉得"缓存页面也能用"就放行。
3. 从 ToDo 到 DAG:两种工作流描述方式的本质差异
3.1 ToDo 是"意图描述",DAG 是"执行契约"
先看一个最直观的对比。假设我们要完成一个典型的研发辅助任务:分析一个开源项目代码结构,然后根据结构生成架构文档,再根据架构文档和代码生成一份测试计划。
用 ToDo 方式喂给 Claude Code,通常长这样:
text复制请完成以下任务:
1. 克隆项目并分析代码结构
2. 根据代码结构生成架构文档
3. 根据架构文档和代码生成测试计划
请务必按顺序执行。
这段指令的问题在哪?它没有定义每个任务的输出位置、成功标准、输入来源。步骤 2 用什么作为输入?是步骤 1 生成的中间文件,还是模型对步骤 1 的记忆?步骤 1 结束后,系统如何知道它"完成"了?靠模型自己说"我完成了"吗?这些都是模糊地带。
用 DAG 方式,我们会把任务拆成三个独立节点,每个节点有明确的输入、执行器、输出校验:
| 节点 | 职责 | 上游依赖 | 输入来源 | 输出校验 |
|---|---|---|---|---|
| clone_and_analyze | 克隆仓库并统计代码结构 | 无 | 仓库 URL | 生成 structure.json,且文件非空 |
| generate_arch_doc | 生成架构文档 | clone_and_analyze | structure.json | 生成 arch.md,且包含指定章节 |
| generate_test_plan | 生成测试计划 | generate_arch_doc | arch.md + structure.json | 生成 test_plan.md,包含测试用例列表 |
在这个 DAG 里,模型每个节点只负责生成一个产物,而产物的正确性不是靠模型自觉,而是靠框架脚本去检查。如果 structure.json 内容为空,第三个节点永远不会被启动。
3.2 DAG 的每个节点天然自带"最少上下文"优势
如果所有步骤塞进同一个 prompt,那么模型每一步都要面对全部上下文。而 DAG 调度器每次只把当前节点的输入传给模型,这有一个巨大的好处:模型在单个节点内的注意力可以全部集中在该节点的任务上,不被其他阶段的信息干扰。
还是拿实习生类比。你把整个项目计划一次全塞给他,跟每完成一个阶段再给下一个阶段的详细任务书,哪个效率高?显然是后者。前者他脑子里存着太多信息,容易瞻前顾后;后者他可以心无旁骛地处理眼下这一件事。
Claude Code 这种工具本来就有"会话中逐步引导"的能力。你可以用它的 subagent 机制,在同一个会话里多次调用,每次都只发送当前节点的任务。但问题是,默认情况下 Claude Code 的上下文是连续累积的,之前节点的输入输出仍然会留在上下文中。所以光靠"分多次发送任务"还不够,还需要在外层用一个 DAG 调度脚本来控制数据流,并在每个节点运行前清理不必要的上下文。
3.3 错误隔离:失败不会波及其他节点
有一个容易被忽视的点:ToDo 模式中,任何一个步骤的失败都可能导致整个上下文被污染。假设一个五步的 prompt 流程,第三步因为某个外部 API 报错失败了。模型为了"修复"问题,可能会尝试修改第一步的输出数据,这使得上下文变得混乱。到了第四步,模型面对的错误信息甚至可能与最初的任务毫无关系。
DAG 模式天然容错。每个节点是独立的进程或独立的工具调用,节点失败后,你可以单独重启该节点,而不需要重跑所有上游节点。如果该节点的输入数据没问题,只是执行过程中出现了瞬时错误,那么重跑这一个节点的成本远小于重跑整条链路。更妙的是,DAG 允许你在节点间插入验证任务,只有验证通过了,下游节点才会收到下一步的信号。
我在实际项目中遇到过一次 Node.js 依赖安装失败,如果放在 ToDo 流程里,很可能要整个重来;但 DAG 模式下,我只需要修复依赖问题,然后重跑那个失败的节点,上游和下游都无需变动。
4. 用 DAG 调度 Claude Code 的实战方案
4.1 最基础的做法:用脚本做顺序编排
如果不想引入太重的框架,最简单的方式是用 shell 脚本或者 Python 脚本,把 Claude Code 的 CLI 调用编排成有向无环图。Claude Code 提供了命令行调用界面,允许你以非交互方式传递 prompt 并执行任务,这样我们就可以在外部脚本里控制执行顺序。
我常用的一个朴素方案:每个节点对应一个 markdown 文件或者一个 Python 函数,里面保存待执行的 prompt 模板,以及输入文件/输出文件路径。调度脚本负责按依赖顺序执行每个节点,并在节点结束后检查必须的输出文件是否存在且满足基本条件。
举个具体例子。我要做这样一个链路:
- 节点 A:读取一份原始产品需求文档(PRD.md),抽取其中的功能列表,存为 features.json。
- 节点 B:读取 features.json,为每个功能生成用户故事,存为 stories.md。
- 节点 C:读取 stories.md,生成技术任务拆解,存为 tasks.md。
对应 Python 调度脚本的骨架如下(示意,不依赖特定 SDK):
python复制import subprocess
import os
import json
NODES = {
"A": {
"cmd": "claude -p '分析 PRD.md,抽取所有功能点,输出 JSON 到 features.json'",
"output": "features.json",
"deps": []
},
"B": {
"cmd": "claude -p '读取 features.json,为每个功能编写用户故事,输出到 stories.md'",
"output": "stories.md",
"deps": ["A"]
},
"C": {
"cmd": "claude -p '读取 stories.md,逐条拆解为技术任务,输出到 tasks.md'",
"output": "tasks.md",
"deps": ["B"]
}
}
def validate_output(path):
if not os.path.exists(path):
return False
if os.path.getsize(path) < 10:
return False
return True
def run_node(name):
print(f"Running node {name}")
result = subprocess.run(NODES[name]["cmd"], shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Node {name} failed: {result.stderr[:500]}")
if not validate_output(NODES[name]["output"]):
raise RuntimeError(f"Node {name} output invalid")
def main():
completed = set()
# 简化版拓扑调度:每次挑出所有依赖已完成的节点来执行
while len(completed) < len(NODES):
progressed = False
for name, cfg in NODES.items():
if name in completed:
continue
if all(dep in completed for dep in cfg["deps"]):
run_node(name)
completed.add(name)
progressed = True
if not progressed:
raise RuntimeError("Dependency cycle or unsatisfiable graph")
if __name__ == "__main__":
main()
这个脚本虽然简陋,但已经具备 DAG 的核心三要素:节点定义、依赖关系、输出校验。你可以非常容易地扩展它,比如在节点失败时发送通知、把每个节点的执行日志单独存档、设置全局超时时间等。
4.2 借助 Claude Code 的钩子系统实现节点级门禁
Claude Code 本身也提供了一些机制来约束模型行为,其中之一是 hook(钩子)。通过 hook,你可以在某些关键事件发生时,让外部脚本介入,阻止模型继续执行或者修改输入。对于 DAG 调度来说,hook 的价值在于:它能让你在模型"即将"执行一个工具调用之前,检查当前节点是否允许调用该工具。
举个例子。假设你在节点 B 中,只希望模型读取 features.json 和调用文件写入工具,不希望它去执行任意 bash 命令。传统做法是你在 prompt 里反复强调"不要执行 bash 命令"——但我们都懂,这种强调在高复杂度任务中经常会失效。用 hook 就能硬拦住:当 Claude Code 要做 PreToolUse 事件时,如果即将执行的工具是 Bash,且当前处于节点 B 的上下文中,就返回一个阻止执行的结果。
这种做法的好处是:顺序约束不仅体现在外部调度层,还能体现在模型自主行为层。模型想越权时会被强制纠偏。不过 hook 也带来两个额外复杂点:第一,你需要维护"当前节点"的状态,让 hook 知道模型现在属于哪个节点;第二,hook 的返回值格式如果不正确,会直接导致 Claude Code 报错。所以不建议初学者一上来就做精细化 hook,先把外部的 DAG 调度跑通,再用 hook 做增强。
4.3 让每个节点单独使用一个"最小化上下文"的会话
Claude Code 支持在命令行里通过 --resume 或者 --continue 来恢复会话,也支持 --session-id 指定会话。做 DAG 调度时,尽量不要在一个超长会话里连续跑多个节点。原因是:即使外部脚本已经将节点划分清楚,模型的上下文仍然是连续累积的。上一个节点产生的大量中间分析,会污染下一个节点。
我推荐的模式是"一个节点一个会话"。调度脚本为每个节点创建一个新的 Claude Code 会话,只把该节点所需的最小上下文通过 prompt 传入。节点完成后,脚本把产物保存成文件,下一个节点再通过文件读取输入。节点与节点之间唯一的数据通道是文件系统,不再依赖模型对上一轮的"记忆"。
实际操作中我会写一个辅助函数,用来生成节点专用的 prompt。例如:
python复制def make_prompt(node_prompt_path, input_files, output_file):
with open(node_prompt_path, "r", encoding="utf-8") as f:
template = f.read()
return template.format(
input_files=", ".join(input_files),
output_file=output_file
)
每个节点对应一个 node_prompt.md,里面会有类似这样的描述:
markdown复制你是软件架构分析助手。
请读取文件:{input_files}。
提取所有功能点并转换为 JSON 数组,每个功能点包含 id、name、description 三个字段。
将结果保存到 {output_file}。
输出必须是合法 JSON,不要附加任何解释文字。
通过这种模式,每个节点里 Claude Code 只关注当前目标,不会想着去"完成整个项目"。整体的项目状态管理由调度脚本负责,模型的角色被降级为"执行单元",这恰恰是稳定性的来源。
4.4 一个稍完整的多阶段示例:代码库体检与文档生成
为了让你更好理解整个方案,我完整描述一个我自己的真实示例。假设要对一个 Python 项目做技术债体检,输出报告,再根据报告生成优化建议,然后执行建议中的一部分自动修复。
DAG 分三层:
第一层:scan_repo。用 Claude Code 的 Bash 工具运行 python -m ruff check . --output-format=json,再运行 python -m pytest --collect-only -q,把结果存到 repo_health.json。这个节点不涉及太多模型智能,更多是执行指令框架,但用 Claude Code 来跑的好处是它可以解析输出中的错误提示并做初步归类。
第二层:generate_report。读取 repo_health.json,调用 Claude 模型生成 Markdown 格式的健康报告,包含问题分类、影响范围、修复优先级。输出到 health_report.md。
第三层:suggest_fixes。读取 health_report.md,针对 P0 和 P1 级别的问题,生成具体的修复步骤,输出到 fix_suggestions.md。这个节点不自动改代码,只是产出建议,避免模型误操作破坏代码。
如果我想把自动修复纳入流程,就加入第四层节点:apply_fixes,但必须有前置校验节点来确认修复目标是明确的。比如只有 fix_suggestions.md 中包含明确文件路径,才允许进入修复节点。DAG 的价值恰恰在这里:系统可以设计成"故障敏感行为必须经过人工或规则闸门"。
5. 逃不过的细节:节点内指令设计、状态外置、幂等性
5.1 每个节点内的 prompt 必须高度聚焦且自带上下文
有人会问:既然我们做了 DAG,是不是每个节点里的 prompt 随便写写就行了?恰恰相反,节点内的 prompt 写作比单一大 prompt 更难,因为它要求你在短时间内把核心目标和限制条件说清楚,同时不能引入多余信息。
我的经验是,节点 prompt 的结构应固定为四段式:角色定义、读取内容、执行动作、输出要求。每段之间用空行隔开,避免模型混淆。
例如:
text复制角色:你是资深前端重构工程师。
读取内容:只读取 components/ 目录下的 Button.tsx。
执行动作:分析该组件的全部 props,列出可复用性问题和重构建议。
输出要求:以要点列表输出到 button_analysis.md,每项包含文件位置、问题类型、重构图示。
为什么要"只读取"?因为模型如果感知到仓库里有大量其他文件,它可能自作主张扩大分析范围,增加不必要的延迟和错误。所以我在 prompt 里强调了读取范围,最好在调度层面就限制工作目录。
5.2 状态外置:进度记录文件比模型自述可靠得多
在 ToDo 模式中,我们依赖模型自己报告"步骤 1 已完成"。模型当然可能撒谎——不是它想骗你,而是它会把"我认为这个步骤已经完成"当作"这个步骤确实已经完成"。例如在"分析代码依赖"这个节点中,模型可能只是读了一部分文件,就认为结构已经清楚了,然后告诉你分析完成。
DAG 模式要求状态外置,也就是把"任务是否完成"的判定交给客观条件:输出文件是否存在、文件大小是否合理、JSON 能否被解析、特定标记是否出现在输出内容中。调度器在节点执行后做这些检查,确认通过后才把该节点标记为 complete。
我建议在所有节点的输出校验中至少包含这三类检查:
- 文件存在性检查:输出文件路径是否存在。
- 内容有效性检查:如果输出应该是 JSON,用 json.loads 是否能正常解析;如果是 Markdown,是否包含标题结构。
- 业务规则检查:比如报告必须包含"风险等级"字段、代码清单必须包含文件路径。这种校验可以非常便宜地执行,但能把大量脏数据挡在门外。
5.3 节点要设计成可重跑的:幂等性
DAG 一定会在实践中遇到节点失败重跑的情况。如果节点内的操作不是幂等的,重跑就可能产生副作用。Claude Code 在执行 Bash 工具时,如果让它修改某个文件,重复执行同样的节点可能把文件追加两次。为了避免这种问题,我的设计原则是:每个节点的输出文件名固定,节点启动时先删除旧的输出文件,再重新生成。
例如,在 Python 调度脚本里,每个节点执行前可以调用 os.remove(output_file)(如果存在),或者为输出文件使用 fixed_name 配合 temp 文件的原子写入策略。这样即使节点重跑,也不会因为旧文件残留造成新数据拼接到旧数据后面的情况。
另外,节点内尽量不修改仓库中的源文件,除非该节点确实就是"自动修复"节点。设计阶段就把"读"型节点和"写"型节点分开,能极大降低调试难度。
5.4 日志和审计:DAG 调度器最重要的隐性价值
跑复杂任务时,如果用户的 AI Agent 出了错,很多人的第一反应是去翻聊天记录。但在 Claude Code 这种服务端日志不一定详细可见的情况下,外部调度器记录的日志反而更有价值。我在每个节点执行时,会把以下内容写入结构化日志:
- 节点名称、开始时间、结束时间、耗时
- 节点的输入摘要(比如输入文件的 hash)
- 执行后的输出产物校验结果
- Claude Code 进程的退出码和最后几行 stderr
这样当最终结果不对时,我可以回溯到底是哪个节点产出了脏数据,而不是对着黑盒发呆。这个习惯救过我很多次。在我看来,DAG 调度器的这些"附加价值",有时候比"顺序执行"本身还重要。
6. 踩坑实录:我在 DAG + Claude Code 工作流中遇到的两类典型故障
6.1 故障一:模型在节点内部"偷偷"跳过了必要步骤
我的第一个 DAG 版本里,节点 2 的任务是"读取 features.json,为每个功能生成验收标准,保存到 acceptance.md"。prompt 写得很明确。结果运行时,我发现 acceptance.md 里只包含前两个功能的验收标准,后五个功能全被跳过了。回溯日志发现,模型在输出完两个功能后,就直接结束了节点,因为上下文中的 features.json 只被读取了一部分——它读完前两个功能后就没有继续读后续内容,而工具调用中读取文件的长度限制或者它的注意力截断,让它误以为"任务已完成"。
这个问题的解决方式不是加大 prompt 的威胁力度,而是用校验规则兜底。我在调度器里加了业务规则检查:解析 acceptance.md,统计其中的功能 ID 数量,必须与 features.json 中的功能 ID 数量完全一致;如果不一致,节点直接标记为失败并重跑。第二次运行时模型仍然只生成了部分内容,但校验失败后重跑,它似乎在后续尝试中完整读取了输入。后来我又在节点 prompt 里加了一句"在开始前,先读取整个 features.json,并使用 Python 脚本统计功能数量,将数量打印出来",模型的完成率明显提升。
这里有个深层次原因值得一说:模型在长输出过程中,"输出已经足够长"可能会触发它的"结束信号"——它会认为任务已经完成,从而提前终止。外部校验之所以必要,是因为模型自身的完成判断并不可靠。你可以把节点输出想象成一份考卷,模型自己觉得"写满了"就能交卷,但字数不等于得分,外部阅卷人(校验脚本)必须逐项核对。
6.2 故障二:上游节点成功了,但产物格式与下游预期完全不一致
还有一次,节点 A 成功生成了 code_map.json,校验文件存在且可解析为 JSON,节点顺利通过。节点 B 启动后,Claude Code 读取 code_map.json,却有相当大概率理解错了结构。我一看 code_map.json,发现它是个深度嵌套的结构:最外层是模块名,里面是类名,再里面是函数名。而节点 B 的 prompt 里,我描述的是"读取 code_map.json 中的文件列表",但实际上 code_map.json 里根本没有文件列表,只有模块类函数树。这种语义错位,靠文件存在校验无法发现。
解决方向是:在不同节点之间,必须保持一种稳定的、符合模型惯性的中间数据格式。 我发现 Claude Code 对"扁平的行式数据"理解远比"深层嵌套的树形结构"稳定。所以我要求节点 A 在输出 code_map.json 时,额外生成一份 flat_symbols.csv,每一行表示一个符号,包含 file_path、symbol_type、symbol_name。节点 B 只读取这个扁平 CSV,不再读取原来的嵌套 JSON。这样下游节点要理解的数据格式变得极其简单,模型的准确率立刻上来了。
这个教训很通用:DAG 里的中间产物不仅要"机器可读",还要"模型可懂"。而模型可懂的数据格式往往不是 API 级别的精细结构,而是接近人眼扫一眼就能明白的表格/列表/纯文本。宁可牺牲一部分机器结构化效率,也要保证每个节点里模型理解正确,因为模型理解错误时返工的代价远大于数据解析的效率损失。
6.3 故障三:工具并行执行时的资源竞争
如果节点允许并行执行(DAG 也有并行分支),例如节点 B 和 C 都依赖 A,可以同时跑,要小心它们是否同时操作同一个文件。Claude Code 会启动多个进程,如果两个进程同时写同一个临时文件,就会互相覆盖。我第一次设计时就犯了这个错误:节点 B 和 C 都生成了 intermediate_cache.json 作为自己的工作文件,结果调度器发现其中一个文件反复损坏。
之后我强制要求每个节点使用独立的临时目录,并设定环境变量传入节点工作目录。节点内部所有临时文件一律放在该目录下,外部调度器只识别该节点指定前缀的输出文件。这个细节不处理干净,DAG 在单流程没问题,一旦并行分支增多就立刻暴露问题。
7. 进阶思路:从"DAG 调度 AI"到"AI 辅助构建 DAG"
刚开始时,DAG 需要人工来设计,我也一直如此。后来用了很多轮后,我发现 Claude Code 本身其实也能辅助我们设计 DAG——只不过需要人类的经验和最终拍板。
我的做法是这样:在启动一个复杂项目之前,先用自然语言和 Claude Code 聊天,描述我的目标,然后让它生成一份候选的任务拆解清单,每个任务标注输入输出和依赖关系。它返回的清单通常包含 8-12 个候选节点,覆盖我没想到的边界情况。这时候我不会直接让它执行,而是把这个清单拿过来自己审一遍,删掉多余节点,合并相同粒度的节点,调整依赖关系,最终形成可执行的 DAG。
Claude Code 在这个过程中更像是一个"助手"而不是"执行者"——这听起来有点违背"让 AI 自动化执行"的初衷,但事实上,在需要稳定可靠交付的场景中,人类的流程设计过滤是必要的。 一旦 DAG 设计好了,节点内的具体工作就可以完全交给 AI 自动执行。这种"人管结构,AI管内容"的分工方式,是我目前认为最稳的组合。
实际上,这也暗示了未来 AI 工程化的一种形态:无论底层模型多强大,上层都需要一层确定性的流程控制逻辑。DAG 正是这一层控制逻辑的骨架。你不需要让 Claude 理解完整的 DAG,只需要让它在每一个受控的节点内充分发挥模型能力。
8. 可用工具和周边生态:不要把 DAG 想得太复杂
有人一听到 DAG,就联想到复杂的 Airflow、Prefect、Temporal 这些重量级工作流引擎。它们当然可以,但对大多数 Claude Code 用户来说,完全是大炮打蚊子。我更推荐从轻量方案开始,等任务规模和协作人数上来后再考虑专有系统。
如果只是个人使用,前面我给出的 Python 脚本编排模式已经足够。把节点定义和校验规则写在 Python 字典或 YAML 文件里,维护成本极低。Python 的 subprocess 调 Claude Code 的 CLI,只要没超出系统进程数上限,就能满足大多数场景。
如果项目需要和团队共享,可以考虑引入 n8n 或 Dify 这类低代码工作流平台,它们内置了 DAG 节点编排和大量的工具节点,支持调用 HTTP API、CLI 等。Claude Code 提供了 headless 模式,可以包装成一个工具节点,由 n8n 统一调度。这样团队里不懂 Python 的人也能通过拖拽配置流程。
还有一类更接近"AI Native"的工具,如 LangGraph、CrewAI 的 flow 机制,它们把 Agent 作为节点,图作为协调器。这类工具的优点是每个节点的状态管理、持久化、重试机制都比自研脚本成熟,缺点是你需要学习框架的抽象模型,并且框架本身的版本更新也可能会调整 API。如果你的核心诉求是"严格约束 Claude Code 的执行顺序",引入这些框架前一定先想清楚值不值。
我在实际工作中,极少一上手就引入重量级框架。通常先用简单的 Python 脚本验证流程的收益,确认"顺序约束确实解决了我的痛点"后,再逐步演进。这个过程让我少走了很多弯路。
9. 我最后想分享的三个判断标杆
判断是否应该把一个任务改造成 DAG,不需要看它多复杂,只需要问自己三个问题:第一,这个任务如果顺序错了,后果是否严重?是浪费几分钟重跑,还是产生脏数据影响下游几天?第二,模型在当前流程中是否经常表现出"乱序"或"跳步"?如果你发现同一个 prompt 连续跑两次结果差异很大,那大概率需要引入顺序约束。第三,任务是否可以拆成边界清晰的中间产物?如果每个步骤的产物能被文件系统物化,就非常适合 DAG;反之,如果任务极其开放、需要模型大量自由探索,强行套 DAG 反而会弄巧成拙。
以我自己的经验来收尾。最开始时,我也跟大多数同行一样,天真地以为"prompt 写得够细,AI 就不会乱来"。在交了几次学费后,现在我的默认工作方式已经完全翻转:凡是会用在生产链路上的多步骤 AI 任务,一律默认编排成 DAG——哪怕初始版本的节点划分很粗糙,也先把结构立起来,再逐步细化节点内部的指令。把顺序的控制权从模型手里拿回来,放到代码和数据流里,这可能是让 Claude 这样的模型在复杂任务中真正变得"靠谱"的最关键一步。
最后分享一个提效小技巧:在你写第一个 DAG 节点 prompt 时,尽可能像在给一个完全不了解项目背景的同事写交接文档,把每条输入文件的路径、格式、示例片段都贴进去。你会发现,前期多花这五分钟,能省下下游大量的 debug 时间。
