一句话总结:好的 Agent 开发流程,核心是"模型是智能,代码是手脚 "。但大多数系统只做对了一半------它们把关键契约写成文档里的大写字母
MUST,求模型记得遵守。真正成熟的做法,是把这些HARD RULE变成做不出错的闸门。
最近我用一队 AI agent 去"审"了一个开源项目:OpenMontage------一个完全由指令(Markdown + YAML)驱动的 AI 视频生产系统。它的架构相当成熟,但在审查过程中,我亲手验证了三个真实的翻车现场,而它们的形状惊人地一致。
这三个 bug 不是这篇文章的目的,而是它的证据。它们恰好印证了一套关于 "Agent 流程到底该怎么设计" 的框架------这套框架,才是本文要讲清楚的东西。
一、Agent 流程的第一性原理
传统软件:逻辑在代码里,人写死每一步。
Agent 系统反过来:逻辑在模型的判断里,代码只提供能力。一个成熟的 Agentic 工作流,骨架通常长这样:
Agent 读流程定义(YAML)→ 读当前阶段的指令(MD)
→ 调用工具(代码)→ 自我审查 → 存档 → 交人类确认
OpenMontage 把这套思想写进了它的"宪法",原话是:
Python = 工具 + 持久化。不在代码里写编排逻辑、创意决策、审查逻辑。 这些由 Agent 读指令现场决定。
这句话是整个框架的地基。但只有地基不够。一个 Agent 流程从设计到上线,要连过三关:切分得对不对 (系统该怎么拆)、执行得住不住 (拆完的契约靠什么保证)、会不会复发 (同一类坑怎么根治)。这三关,正好对应本文的三层------原则层 定方向、机制层 保执行、反模式层防翻车。下面逐层展开。
二、原则层:四条定方向的思想
这一层回答的是"系统应该怎么切分"。它决定了你的 Agent 是灵活还是僵硬、是可维护还是一改就崩。
原则一:模型管判断,代码管能力
把系统拆成两类东西:
- 确定性能力(生成图片、转码、写文件)→ 留在代码里,做成工具;
- 判断与编排(选哪个方案、何时该停、质量够不够)→ 留在指令里,交给模型。
好处:改流程不用改代码,改能力不用动流程。产品想换个创作策略,改一个 Markdown 就行,不用发版。
原则二:能力靠"发现",不靠"枚举"
新手最容易犯的错,是在 prompt 或代码里硬编码一份工具清单。能力一多,清单就到处漂。
成熟做法是运行时发现:所有工具注册到一个 registry,模型查 registry 拿到"现在到底有哪些能力、哪些配好了"。
ini
# 反面:硬编码,新增 provider 要改 N 处
TTS_PROVIDERS = ["elevenlabs", "openai"] # 漏了新加的、还会过期
# 正面:运行时发现,加一个工具,选择器零改动
registry.get_by_capability("tts")
OpenMontage 的选择器(selector)就是这么做的:加一个新 provider 工具,选择器代码一行都不用改,它自己会发现。这就是"发现优于枚举"。
原则三:上下文是预算,默认渐进披露
模型的上下文窗口是稀缺资源,不是垃圾桶。把所有信息一股脑塞进去,既贵又稀释注意力。
OpenMontage 在指南里专门写了:先读 provider_menu_summary()(人类可读的摘要),别读 support_envelope()(几 MB 的原始 JSON firehose) 。深入信息按需再取。
给模型设计文档,要像设计 API 一样分层------便宜的摘要在前,昂贵的细节按需加载。
原则四:长流程要可恢复、可审计
一个跑十几步、要花钱、可能中途挂掉的流程,必须解决两个问题:断了能不能续?事后能不能查?
OpenMontage 的答案是一套状态机:
research → proposal → script → scene_plan → assets → edit → compose
每个阶段产出一个 规范工件(canonical artifact),用 JSON Schema 校验;checkpoint 记录走到哪、能从哪续。
stage 之间用"带类型的工件"做接口,在边界处校验,用 checkpoint 支持断点续跑。 这是把"会即兴发挥的模型"变成"可审计流水线"的关键一招。
原则层做对了,你的 Agent 就有了一副好骨架。但骨架不保证不出错------出错往往出在第二层。
三、机制层:原则如何真正落地
这一层回答的是"原则靠什么保证执行"。这也是最容易被跳过、却最致命的一层。
核心命题只有一句:
真正的 harness(承载 Agent 的框架)会让错的事"做不出来",而不是"被禁止"。
为什么这条这么重要?因为 Agent 系统里的契约有两种落实方式,它们的可靠性天差地别:
| 落实方式 | 本质 | 可靠性 |
|---|---|---|
| 机制:代码层面让非法状态无法产生 | 闸门 | 确定性,每次都拦得住 |
喊话 :文档里写 MUST / HARD RULE |
祈使句 | 概率性,靠模型自觉兜底 |
模型的"自觉"是概率性的------今天遵守,明天就可能忘。所以任何关键契约,只要还停留在文档的大写字母里,它就是一颗定时炸弹。
那"闸门"到底是什么?
这个词我用了一路,得先把它讲清楚------否则"做成闸门"就成了又一句喊话。
闸门不是"加一行校验"那么简单,它和你熟悉的两样东西有本质区别:
- 它不是事后 review。 review 是事情做完了再回头挑错,错误已经产生、可能已经流到下游;闸门是在错误状态被写下来的那一刻就拒绝它,错误根本没机会存在。
- 它不是普通断言。 一句
assert x > 0拦的是"程序员写错了";闸门拦的是"模型的判断没有被记录、或记录得不合法"。它守的是契约,不是变量。
一道合格的闸门,最小构成是三样东西:
- 一个绕不过去的卡点------通常是状态写入的唯一出口(存档、阶段推进、工件落盘),所有路径都得从这过;
- 一条可机器校验的条件------把契约翻译成"什么样的状态算合法"(不是"必须呈现两个引擎"这种人话,而是"决策记录里存在一条 engine_choice,且 candidates 字段长度 ≥ 2");
- 不合法就硬失败 ------
raise/fail,不是 warning,不是日志,是直接让流程停下。
一句话:闸门 = 唯一出口 + 可校验条件 + 硬失败。 三样缺一,它就退化回喊话。比如只写了校验条件、却允许模型走别的路径绕开它,那等于没有;又比如条件不合法只打一行 warning,模型照样往下跑,那也等于没有。
记住这个"三件套",下面看 OpenMontage 哪些契约配齐了、哪些没配齐,就一目了然。
把 OpenMontage 拆开看,它其实一半是机制,一半是喊话------而它的可靠性,精确地沿着这条线分裂:
| 契约 | 它怎么落实的 | 靠谱吗 |
|---|---|---|
| 工件必须合法 | JSON Schema 校验,非法直接 fail | ✅ 机制 |
| 流程可恢复 | checkpoint 记录状态 | ✅ 机制 |
| 能力可发现 | registry 运行时查询 | ✅ 机制 |
| 必须呈现两个引擎 | 文档里写 HARD RULE |
❌ 喊话 |
| 执行前必须声明模型 | 文档里写 MUST announce |
❌ 喊话 |
| 每个阶段先读对应 skill | 文档里写 Do NOT skip |
❌ 喊话 |
凡是它造了机制的地方,契约就硬;凡是它只写了文档的地方,契约就软,然后翻车。
这不是这个项目的毛病,是所有指令驱动 Agent 系统的通病。下面三个翻车现场,全都掉在表格的下半区。
四、反模式层:三个翻车现场,一种形状
上一节说它们"全都掉在表格的下半区",这里把现场摊开看。三个 bug 的根因是同一个:契约存在于文档,但不存在于机制。
翻车一:HARD RULE 被违反 7 周 指南规定"必须同时呈现两个渲染引擎给用户,禁止默认选一个"。但有个 skill 文件里直接写着 Remotion is the default ... Never default to FFmpeg------字面违反那条 HARD RULE,issue 挂了 7 周没人动。契约只写在文档里,没人(也没有机制)去对照执行。
翻车二:工具谎报自己可用 治理契约三令五申"绝不谎报能力可用"。但底层依赖检查的代码漏了一种依赖前缀:
python
def check_dependencies(self):
for dep in self.dependencies:
if dep.startswith("cmd:"): ...
elif dep.startswith("env:"): ...
elif dep.startswith("python:"): ...
# 漏了 "binary:" ------ 于是声明 binary:ffmpeg 的工具,
# ffmpeg 不存在时也照样报告"我可用"
契约写在文档里,机制没堵住,工具照样撒谎。
翻车三:能给不存在的阶段存档 checkpoint 的阶段表漏了两个真实存在的阶段,于是相关流程一存档就 KeyError 崩溃;而另一处过宽的 except 又会在加载失败时返回一份错误但看起来合理的阶段列表,让模型去跑一个根本不存在的阶段。
三个 bug,一个共同的根因:
当你发现自己在用大写字母恳求模型按顺序做事,那段顺序本该是确定性机制,而不是文档里的祈使句。
五、怎么改:把祈使句翻译成机制
方法只有一句话:把 HARD RULE 翻译成闸门。 但翻译是有成本的,所以先要会判断------
哪些契约值得机制化?
不是每条 MUST 都该立刻做成闸门。一个实用的判断标准:
- 违反的代价 × 违反的概率高 → 优先机制化(比如"用错模型"会带来 6 倍成本差,必须堵);
- 契约有明确可校验的状态 → 适合机制化("决策记录里有没有合法的引擎选择"是可校验的);
- 契约本质是开放判断 ("文案够不够吸引人")→ 留给模型,但可以在边界处加一道"是否已记录判断结果"的轻量闸。
换句话说:机制化不是消灭判断,而是消灭"判断结果可以静默丢失"的可能。
三种典型翻译
1)把"必须呈现两个引擎"做成校验闸 不要靠 reviewer 事后挑刺。套上三件套:唯一出口 是存档------所有流程都得经过它;可校验条件 是"决策记录里存在一条合法的 engine_choice,且候选项 ≥ 2";硬失败是不满足就让 checkpoint 直接 fail。模型想跳过都跳不过去。
2)把阶段顺序交给确定性驱动器 与其让模型自己"记得"调 get_next_stage() 别跳步,不如用一段确定性代码推进状态机,模型只在每个节点填判断。控制流是控制流,判断是判断,别混。
3)消灭"同一事实的多个副本" 这是最微观、也最普适的一招。我在这个项目里修的一个真实 bug 就是典型:某个工具的"默认模型"在 schema 里写了一遍、在三个方法里又各写了一遍------四份副本,然后漂移了。结果用户看到的报价是 A 模型的价,实际生成的是 B 模型(成本差 6 倍,还偷偷换了模型)。
修法不是"把四个值改对",而是收口成一份真相:
ini
# 之前:默认值散落在 schema + 3 个方法里,迟早漂移
"default": "seedance_2.0", # schema
model = inputs.get("model", "gen4_turbo") # 方法里却是另一个值
# 之后:单一来源,schema 和所有代码都引用它,想不一致都不行
_DEFAULT_MODEL = "seedance_2.0"
"default": _DEFAULT_MODEL,
model = inputs.get("model", _DEFAULT_MODEL)
在"模型会同时读 schema 又读代码"的时代,single source of truth 比以往更要命------因为模型会把两个互相矛盾的"真相"都当真。
六、Agent 流程心法:一张贴在工位上的清单
把全文浓缩成三层、七条:
原则层(定方向)
- 模型管判断,代码管能力。 编排和创意别写进代码,确定性能力别交给模型即兴发挥。
- 能力靠发现,不靠枚举。 别在 prompt/代码里硬编码工具清单。
- 上下文是预算。 文档分层,摘要在前,细节按需。
- 长流程要有工件契约 + checkpoint。 边界处校验,支持断点续跑。
机制层(保执行) 5. 数一数你文档里的 MUST。 数量就是你的技术债------每一条都是一个"本该是机制、却写成了喊话"的地方。 6. 高代价契约做成闸门,不做成祈使句。 让错的操作做不出来,而不是被禁止。
反模式层(防翻车) 7. 同一个事实只留一份。 模型会把所有副本都当真,矛盾即 bug。
结语
Agent 开发最反直觉的一点是:它信对了"模型是智能",却太容易高估"指令能当约束"。
模型的判断力可以外包给 prompt,但契约的执行力不能。文档里的大写字母,模型今天遵守、明天就可能忘;只有闸门,每次都拦得住。
原则层让你的 Agent 有骨架,机制层让它不散架,反模式层让它不复发。三层都补齐,你才算从"半个 Agent 框架"走到了"完整 Agent 框架"。
把
HARD RULE从大写字母,变成一道做不出错的闸门------这就是那最后、也最关键的一步。
这篇文章的素材,来自我用一队 agent 去审 OpenMontage 的过程,顺手给它提了个修复 single-source-of-truth 漂移的 PR。一手翻车现场,比任何理论都有说服力。