AI Agent 流水线的编排协议设计
把流水线当分布式系统设计
八个 skill 各自是一个独立指令包,谁也不 import 谁,谁也不知道别人的存在。功能点提取不知道链路图生成会读它的 JSON,业务需求生成不知道模块归类在等它的产物。要让这条链自动跑起来,本质上是在设计一套进程间通信:生产者怎么交付,消费者怎么确认,调度方怎么判断某一步「完成了」。
分布式系统里的经典方案在这场景下都不合适:消息队列太重,共享内存不存在(每次会话都是全新的大模型),RPC 需要常驻服务。流水线最后采用的方案朴素到几乎简陋:生产者写文件,调度器看一行控制台输出。但协议的每个细节都经过推敲,简陋和健壮之间的差距全在这些细节里。
文件即接口
第一步到第五步的产物全部落在约定目录,目录本身就是接口定义:
| 目录 | 生产者 | 内容 |
|---|---|---|
doc/function-list/ | skill1 功能点提取 | 每页面一份 function-list JSON |
doc/link_diagram/ | skill2 链路图生成 | 每页面一份链路图文档 |
doc/business/ | skill3 业务需求生成 | 每页面一份业务需求文档 |
doc/business/ + business.json | skill6 模块归类 | 归类标记与模块配置 |
doc/merged/ | skill7 模块合并 | 每模块一份功能资产总清单 |
doc/merged/00_索引.md | skill4 模块索引 | 全局索引与对账 |
doc/sql/ | skill5 数据字典 | 表清单、数据字典、注释脚本 |
命名也定了契约:产物文件名带语义后缀,台账列表-链路图.md 进了 doc/business/ 就变成 台账列表-业务需求.md,后缀替换、词干不动。消费者靠目录加后缀定位输入,不需要任何注册中心。
选文件而不是内存传递,换来三个性质。中间产物人人可读:跑到一半出问题,打开文件就能诊断,不用给模型加日志。断点可续跑:文件在,成果就在,从任何一步重启都行。执行可审计:哪个文件什么时候生成、内容是什么,全有据可查。代价是磁盘上多几个文件——在工程师手里,这几乎不构成代价。
一行输出就是协议
每步完成时,skill 被要求在控制台打印一行固定格式的输出:
输出文件:pbk-pageList-台账列表-业务需求.md
这就是完成标志。调度器捕获这行输出,才认定该步骤成功、放行下一步。格式规定得极细:中文全角冒号,完整文件名,不含路径。
这些细节各有原因。不含路径,因为调度器只关心文件名,路径由目录契约决定,写进标志反而引入跨平台分隔符问题。全角冒号加中文名,是为了让标志行在混合中英文的执行日志里有足够的辨识度,误匹配概率趋近于零。完整文件名而非「等文件」,则是把模糊性掐死在协议层:下一棒的输入是什么,一行之内说得清清楚楚。
没有完成标志,调度器只能靠猜:模型说「我完成了」算完成吗?工具没报错算完成吗?一行明确格式的输出,把「完成」从语义判断降维成了字符串匹配。
开工先查前置条件
每个 skill 的指令开头都有一段几乎相同的话:执行第一步前,先检查依赖产物是否存在,缺失则报告错误并停止,不许静默跳过。
模块合并的检查最典型:先看 business.json 在不在,再核对 doc/business/ 里的文档清单和模块配置对不对得上,最后确认 .模块.txt 归类文件齐全。任何一项缺失,输出明确的错误信息——缺什么、应该在哪、由哪一步生成——然后退出。模块归类还有一层显式降级:模块配置文件不存在时,不假装配置齐全硬跑,而是转入引导创建流程,让用户决定模块结构。
前置检查防的是流水线最阴险的故障:静默空转。上一步实际没产出,本步不检查就开跑,拿着空输入生成一份「看起来完整」的空文档,错误向下传播两三步后才暴露,排查时要倒着回溯好几层。开工先查依赖,等于把故障拦截在传播之前,错误信息里写明缺口位置,修复成本回到最低。
工程细节清单
协议层还有一批小到不起眼、漏掉就随机出事的工程约定:
UTF-8 编码强制。所有产物统一 UTF-8,Python 脚本写文件显式指定编码,否则 Windows 下默认 GBK,中文文件名和内容会在不同 skill 之间乱码。ensure_ascii=False。JSON 序列化必须带这个参数,否则中文字段全变成 \uXXXX 转义,下游模型读到的功能点名是一串转义码。路径统一正斜杠。Windows 反斜杠在不同工具里转义行为不一,正斜杠全平台兼容。文件名禁特殊字符。中文可用,空格、冒号、斜杠禁用,保证文件名在任何操作系统和版本控制系统里都安全。
这类细节的共同特点是:写进指令就永远不出事,不写就随机出事,而且出错表现千奇百怪,极难归因。把它们固化成协议条款,是省下将来成小时的调试时间。
状态账本与断点续跑
business.json 的状态字段(每个模块记录文档总数、已处理数、处理状态,见文档资产组织三部曲),对编排的意义值得单独看:流水线是天然的长任务,几十个页面走完七步要跑很久,中途断电、超时、人为中止都是常态。有了状态账本,重启不是从头再来:模块归类读 business.json 就知道哪些模块已合并,合并 skill 跳过已完成模块继续干活;链路图、业务需求各目录里的文件本身就是处理记录,目录里没有的文件名就是没跑过的页面。
sequenceDiagram
participant S1 as 调度器
participant S2 as skill3 业务需求
participant FS as 文件系统
S1->>S2: 分配 pbk-pageList 页面
S2->>FS: 写入 台账列表-业务需求.md
S2-->>S1: 输出文件:台账列表-业务需求.md
Note over S1: 字符串匹配确认成功
S1->>S2: 分配下一页面
Note over S1,FS: 中断后重启
S1->>FS: 扫描 doc/business/ 已有文件
S1->>S1: 跳过已完成 续跑缺失
断点续跑的关键在状态的可推断性:不需要专门的数据库记录进度,文件系统本身就是进度表。产物存在即完成,缺失即未跑,账本和存储合一,不存在状态与实际两张皮的风险。
依赖图与并行位
七个逆向 skill 的依赖关系是一张简单的有向无环图:
flowchart LR
A[skill1 功能点提取] --> B[skill2 链路图]
B --> C[skill3 业务需求]
C --> D[skill6 模块归类]
D --> E[skill7 模块合并]
E --> F[skill4 模块索引]
G[skill5 数据字典]
G -.->|业务语境反哺注释| C
唯一的并行位是 skill5:它不依赖任何前序产物,随时可以和其他步骤同时开跑;反过来它的注释精修又要消费业务产物,所以实践中让它先出表清单和初稿,精修阶段再等业务语境就绪。依赖图简单到一张图讲完,这正是分步设计的红利——每步职责单一,依赖自然清爽,不需要复杂的调度引擎。
协议的可替换性
这套协议最深远的好处是解耦。任何一步的 skill 都可以整体重写、换模型、换实现,只要它还遵守三条:读约定目录的输入,写约定目录的输出,完成时打印那行标志。调度器对 skill 的内部实现一无所知,也毫不在意。
换句话说,流水线的稳定性锚在协议上而不是实现上。大模型几个月一换代,实现层面的最优解不断变化,但「文件即接口、标志即确认、开工查依赖」这套协议没有过时问题。把多变的实现和稳定的协议分层,是软件工程的老智慧,在 Agent 流水线里同样成立。
协议保证了步骤之间接得上,下一步的问题是每一步的产物质量怎么保证:LLM 输出质量控制的三层防线。