Appearance
协作工具补遗:如何核验调度边界
团队里有人提了个方案:用 Paseo 把编码任务分给多个执行者并行跑,说“这样肯定更快”。小a心动了,但记得老z教过的——先别急着信结论。他拿着这个方案去找老z。
“并行更快”,这句话听着没毛病,但小a已经学会了追问:什么任务?谁分?冲突了怎么办?失败了算谁的?这些细节没搞清之前,“更快”只是一个乐观的猜测。
老z点头:“你有这个警惕就对了。**并行是优化手段,不是默认状态。**单执行者没有调度问题;一旦分出去,所有权、状态、冲突、失败四个问题同时出现。本附录就是把‘要不要并行’翻译成‘能不能核验’——一个方案能不能上,就看它的这些细节能不能逐条跑出证据。”
老z又补了一句:“还有一个时间点问题——**‘并行更快’通常说的是墙钟时间,但团队真正关心的往往是总成本:调度、合并、排障、重跑加在一起是多少。**墙钟省了,总成本涨了,这笔账谁算?所以要量化的不只是‘快多少’,还有‘贵多少’——两者都拿数字说话,才叫核验。”
本附录服务源码篇和实现篇,当团队考虑把编码任务交给多个执行者时查阅。本仓库快照未包含可验证的 Paseo 实现、配置或一手说明;因此本附录不陈述 Paseo 的具体命令、模型支持或自动化行为——它要讲的是核验协作工具的通用方法,而不是某个产品的功能清单。
“调度边界”这个词,老z给了个通俗解释:**它划在“调度器管得到的事”和“调度器管不到的事”之间。**管得到的是任务队列、执行者分配、状态跟踪;管不到的是任务内容的正确性、共享状态的冲突、副作用的回滚。本附录的核验方法,就围绕这条边界展开——先知道哪些归调度器管,再知道剩下的归谁管。
“分出去就会自动完成”是错觉
小a:“既然叫调度工具,是不是把任务分出去就会自动完成?”
老z:“不能这样推断。**‘调度’只解决‘谁来做’,不解决‘做对了没有’。**任何协作调度都要先回答四个问题:任务所有权归谁、共享状态怎么管、权限范围多大、验收标准由谁定。”
老z把“调度”这个词拆给小a看:它本质是一张任务清单外加一个分发器——把任务 A 给执行者 X,任务 B 给执行者 Y,然后等结果回来。清单和分发器本身不携带“正确性”,它们只负责把工作挪到不同的执行体上。结果对不对、合并不合并、回滚不回滚,全部要调度器之外的东西来回答。
“那任务所有权和验收标准,谁来定?”小a问。
“通常是宿主,也就是你的编排代码。”老z说,“任务 A 归执行者 X,这个‘归’字包含两层:一层是‘X 有权限动它’,一层是‘X 对它负责到验收为止’。很多调度工具只有第一层,没有第二层——它能限制谁写,但‘写成什么样算好’它管不了。验收标准尤其容易悬空:调度器能记录‘任务完成’,但它不会定义‘完成’是指‘命令退出码为 0’还是‘测试真的通过’。这两个定义差着十万八千里,而工具只报前者。”老z让小a想一个最简单的场景:两个执行者同时改同一个文件。
- 如果两个执行者都基于文件的 v1 版本开始改,各自产出 v2-a 和 v2-b,**合并时冲突谁来解决?**调度器不会自动合并语义冲突——文本上能拼回去的改动,语义上可能互相打架(一个加了参数,另一个删了调用它的函数)。
- 如果执行者 A 改了
config.ts,执行者 B 正在读旧版config.ts做决策,B 的决策基于过时信息,这个错误算谁的?A 的,因为 A 没通知?B 的,因为 B 没重新读?调度器不会替你回答。- 如果一个执行者失败了,**它的部分副作用(已写的文件、已发的请求)要不要回滚?**调度器知道它做了什么吗?如果它写了一半就被中止,留下的半截文件比没有更糟。
- 如果两个执行者都向同一个外部 API 发请求,**速率限制算谁的 quota?**调度器通常按执行者记账,不会把它折算成一个整体的预算。
小a把这四个问题抄下来,发现它们其实是同一件事的四个侧面:第一个问所有权,第二个问共享状态,第三个问副作用,第四个问配额。“但它们有个共同点,”小a说,“看起来都不是调度器会替我回答的问题。”
“对。”老z说,“这四问分别对应四份调度器之外的约定:谁有权限动这份文件(所有权)、谁读到的一定是最新版本(状态)、出事的边界画在哪里(副作用)、资源耗尽时算谁的账(配额)。调度器只给四档状态——pending/running/done/failed——它看不到这四档之外的世界。这四份约定没人写,并行就只是把问题放大到更多执行者身上。”
老z又指了一个容易被忽略的点:“调度器还有一层隐形假设——**它假设执行者是同质的。实际上执行者可能连的是不同的模型、不同的 API key、不同的上下文窗口。同一个任务,执行者 A 能跑、执行者 B 跑不动,调度器不会知道,也不会因此调整分配。‘谁来做’在调度器眼里是等价的,在结果上不是。**这层差异,也要在任务设计时想清楚。”
小a盯着第三条:“失败回滚这块,我以为调度器会自动管。”
“不会。”老z说,“**大多数调度器只跟踪任务状态(pending/running/done/failed),不跟踪任务的副作用。**副作用(写了哪些文件、发了哪些网络请求、改了哪些数据库行)在执行者体内,调度器看不到。要回滚,得靠执行者自己写补偿逻辑,或者靠外面的工作区快照——调度器只是触发它,不是实现它。”
小a问:“补偿逻辑长什么样?”
“两个层面。”老z说,“第一层是执行者自己声明‘我会产生这些副作用,失败时这样回滚’——比如先写临时文件,全部成功后再原子改名;或者把‘记日志失败’当成整体失败处理,不让它单独成功。第二层是宿主提供工作区快照和重放能力——出事后把工作区恢复到任务开始前的状态。但这两层都不是默认就有的:第一层要执行者配合写,第二层要宿主配合建。拿到一个新调度工具,别假设‘它一定支持回滚’,要当成‘默认不回滚,除非明确声明’来核验。”
“你看,”老z说,“**并行只在任务真正独立时才‘更快’。**编码任务之间往往有隐含依赖——共享文件、共享配置、共享测试。把有依赖的任务硬并行,得到的不止是冲突,还有更难定位的偶发 bug:串行时第 5 步能稳定复现的错误,并行时只有当第 3 步和第 4 步的时序撞在一起才冒头,而且每次时序都不一样。”
小a:“那怎么判断任务之间有没有隐含依赖?”
“最便宜的办法是看共享资源的交集。”老z说,“两个任务都读写 config.ts,这是文件交集,画一下就能看见;一个任务跑 lint、另一个任务改代码,看似没有交集,但 lint 读到的是改到一半的文件——这是时序交集,比文件交集难发现得多。核验时序交集有一个土办法:**把两个任务的执行顺序对调,各跑一遍,如果结果不同,它们之间就有隐式依赖。**顺序敏感本身就是依赖的证据。”
“还有一类依赖更隐蔽:数据竞争不在文件层,在外部服务层。”老z说,“两个执行者都调同一个外部服务,服务端的状态就是共享的——比如两个任务都往同一个 Git 远程 push,或者都更新同一个数据库行。**文件层交集你画得出,外部服务层的交集画不出来,但它们的冲突后果一样真实。**核验外部服务层依赖,只能靠运行时观察:把两个任务并行跑,盯外部服务的状态变化,看有没有互相覆盖。”
待核验案例的阅读法
老z说:“不是不能并行,而是核验之后再并行。把 Paseo(或任何协作调度工具)当作待核验案例时,按以下五步收集一手资料。”
- 确认来源——版本号、发布日期、官方文档地址。没有一手文档的功能,不要当真。
- 找到真实接口——任务创建、依赖声明、取消、重试、结果汇总的具体 API 或命令。口头描述的“它能做”不算。
- 核验权限边界——每个执行者的工作区、文件所有权、凭据范围。两个执行者能否写到同一路径?
- 构造冲突测试——故意创建两个修改同一文件的任务,观察冲突策略(报错?静默覆盖?锁定?)。不要假定自动合并。
- 量化对比——记录调用次数、耗时、失败率、人工介入次数,与单 Agent 基线比较。更快要拿数字证明,不是凭感觉。
“五步的顺序也别乱,”老z说,“**前两步确定‘它是什么’,中间两步确定‘它实际怎么动’,最后一步确定‘它值不值’。**确认来源和找到接口,是先把它当案例研究;构造冲突和核验权限,是把它放到压力下看行为;量化对比,是回到业务目标问收益。跳过前两步直接做对比,你对比的对象可能根本不是你要用的版本。”
小a:“第二步‘找到真实接口’怎么才算找到?”
“标准是:**你能写出一个完整的调用序列,从任务创建到结果汇总,每一跳都有具体的 API 名或命令。**写不出来的地方,就是还没核验过的地方。”老z说,“很多工具的宣传页会画一张漂亮的架构图,但你把图上的箭头换成实际的调用代码时,会发现有些箭头根本没有对应物——那些箭头就是‘宣传的功能’,不是‘可用的功能’。”
小a:“第五步‘量化对比’里的基线,怎么取?”
“基线就是单 Agent 串行跑同样任务的数字。”老z说,“别拿‘现在团队怎么做的’当基线——那可能本身就很慢。先把同一批任务串行跑一遍,记下耗时和失败率,再并行跑一遍,两个数字摆在一起比。‘更快’的定义就是‘并行耗时 < 串行耗时’;如果并行更快的那点收益,被合并、排障和重跑的时间吃掉了,那‘更快’就是账面上的,不是真实里的。”
小a指着第一步问:“文档写得很漂亮、但没有 API 可核验的功能呢?”
“那是宣传话术,不是功能。”老z说,“一个功能至少要有三样东西才算数:入口(怎么调)、参数(调的时候传什么)、可观察的结果(跑完能看到什么)。三样缺一样,你就没法验证,也没法在出问题时区分‘我用错了’还是‘它坏了’。把‘文档里描述的’和‘能跑出证据的’分开记账,是核验的第一步。”
小a问:“这几步里哪一步最容易翻车?”
老z:“第四步,构造冲突测试。因为工具文档几乎不会主动写‘我处理冲突的方式是静默覆盖’,但很多工具的默认行为恰恰如此。”他让小a在脑子里推演一下三种可能的默认策略:
- 报错并保留两者——调度器把冲突抛出来,让人(或上层 Agent)裁决。最安全,但需要额外的合并流程。
- 排队/锁定——同一资源同时只能被一个执行者持有,另一个等待。安全但会退化成串行,所谓“并行更快”就此打折。
- 后写覆盖——谁最后写谁说了算,中间版本丢失。最快但最危险——它不会报错,只会让改动凭空消失。
“这三种策略,”老z说,“**看文档你可能以为是第一种,跑冲突测试才发现是第三种。**所以冲突测试不是为了找 bug,是为了搞清楚工具到底处在哪一档。”
小a:“除了‘两个都写’,还有没有别的‘读起来安全、跑起来危险’的情形?”
“有,最常见的是读-改-写竞态。”老z说,“两个执行者都先读同一份文件,各自在内存里改,再写回去——两者都不碰同一个字节,但后提交的改动把先提交的整体覆盖了。并发写测试测的是‘两个都写同一路径’,读-改-写要单独测:让两个任务基于同一个 v1 各自修改、各自提交,看 v2-a 和 v2-b 是不是最后只剩一个。‘各自不冲突’和‘合并后不冲突’是两回事,后者才是调度边界要验证的。”小a把这个测试记进了清单。
小a问:“如果文档不全,或者我没法跑冲突测试呢?”
“那就保留‘待核验’,不要补全产品故事。”老z说,“最危险的做法是:文档没写,你就用想象填上,然后基于想象下结论。一手资料和实际运行证据不足时,‘我不知道’比‘我猜它能’更负责任。”
老z又补了一条实操:“如果你能跑测试,优先测三件事:一是同路径并发写,看冲突策略;二是执行者中途被取消,看残留;三是执行者失败后重试,看是否重放副作用(比如重复发邮件、重复扣款)。这三类在文档里往往语焉不详,但跑一次就能看见真实行为。”
小a:“跑完测试,结果怎么留?”
“留一份核验记录。”老z说,“写清楚:核对了哪个版本、核对日期、做了什么测试、观察到什么行为、结论是什么。比如‘截至 Paseo vX.Y / 2026-08 核对,同路径并发写为排队策略’。这条记录就是你下次升级工具时的对照基线——工具一升级,所有结论作废重验;没有记录,你连‘上次是怎么验的’都想不起来。”
多 Agent 并行的真正前提
这与概念篇“多 Agent”那章的结论一致:并行只在三个条件同时满足时才有价值——
- 任务独立——一个任务的输入不依赖另一个的输出,或依赖关系可以被显式建模。
- 交付可合并——各执行者的产出合到一起不冲突,或有明确的冲突解决机制。
- 失败可归属——某个执行者失败了,你能定位是它的责任,而不是整批报废。
老z把这三条解释成“可否拿掉的判据”。任意一条不满足,并行就退化为串行或更糟:
- 任务不独立却硬并行——A 的输出是 B 的输入,但你让它们同时跑,结果 B 用了一个不存在的输入,产出一份看似合理实则错乱的结果。这比串行更慢,因为还要回头查“到底是 A 错了还是 B 错了”。
- 交付不可合并——三个执行者各改一个模块,改完发现互相调用的接口对不上,合并不了,等于三份独立废稿。
- 失败不可归属——某个执行者出错,但你分不清是它的问题还是共享状态被别人改坏了,只能整批重跑。
老z让小a把这三条对应到三个工程动作:“**任务独立 → 做依赖分析,交付可合并 → 定契约与合并流程,失败可归属 → 建隔离与日志。**前两个是任务设计阶段的事,第三个是运行时环境的事。很多人只做第一个——把任务切成小块就并行——结果合并和排障两件苦活全在后面等着。”
小a:“这三条里有没有哪一条是可以先缓一缓的?”
“没有,但有一条最容易‘看起来满足、实际不满足’。”老z说,“交付可合并最容易被低标准骗过去——‘能拼回去’和‘合并后依然正确’是两回事。三个执行者各改一个模块,git 上三个分支合并不报冲突,这是‘能拼回去’;但合并后编译通过、测试通过,这才是‘合并后依然正确’。前者把合并成功的标准定在‘文本没打架’,后者把标准定在‘语义没打架’——并行任务交付时,验收标准要设在语义那一档,不能设在文本那一档。”
“还有一条容易被忽略的边界,”老z说,“**合并本身也是要核验的工作,不是免费的。**并行省下的时间,如果小于‘合并 + 排障 + 重跑’的时间,那这单并行是亏的。所以项目开始前,先估一下合并成本:产出物能机械合并,还是需要人一条条看?机械合并成本低,并行划算;人工合并成本高,并行未必划算。”小a:“能不能把‘任务独立’变成可核对的表格?”
“能,而且很便宜。”老z说,“拿张表,行和列都填任务名,一格一格标三种记号:两个任务共享同一文件,标‘共享’;一个任务的输出是另一个的输入,标‘依赖’;完全不相干,留空。**只要存在一个非空的‘共享’或‘依赖’格子,这两个任务就不能无脑并行。**这张表就是‘并行可行’的原始证据——画完它,你才知道调度器要替你管多少条边界,也才知道哪些边界必须靠你自己的约定来兜。”
小a:“第三条‘失败可归属’听起来最难。”
“对,因为它要求可观察的执行边界。”老z说,“每个执行者要有独立的日志、独立的工作区、可回放的输入。如果执行者之间共享内存、共享文件、共享连接池,失败就会顺着共享状态传染,谁都说‘不是我干的’。这就是为什么隔离(独立工作区、独立凭据)不只是安全要求,也是排障要求。”
小a:“独立日志这条,具体怎么落地?”
“一个执行者一个目录、一套日志、一份可重放的输入副本。”老z说,“代价是磁盘和复杂度,换来的是出事时能指着某一行说‘就是这里,它从这里开始错的’。很多团队为了省这一步,把执行者混进同一份日志,并发事故一出现,谁也说不清先后顺序——而顺序恰恰是并发事故里最重要的信息。日志混用省下的那点空间,会在排障时几十倍地还回去。”
“调度器管理的是队列和分发,”老z说,“**它不会自动消除相关错误、权限扩大或验收缺失。**这三个风险是并行的固有代价,调度工具能减轻,但不能消除。把调度器当成‘我只要把任务丢进去,它就会自己变对’——这是把队列管理器当成了正确性裁判。”
协作工具是运行时边界,不是银弹
小a最后问:“那协作工具到底有没有用?”
“有用,但它是一个可验证的运行时边界,不是银弹。”老z说,“它适合那些你已经证明‘串行太慢、且任务确实独立’的场景——比如对 100 个独立文件各跑一次 lint,或者把 50 个互不相关的 issue 各交给一个执行者去 triage。它不适合那些‘看起来能并行,但隐含共享状态’的场景——比如多人改同一个模块,或者几个执行者都要更新同一份配置。”
老z又给了一个判断标准:“**看任务改的是‘写集合’还是‘跑集合’。**各写各的文件,是写集合独立,适合并行;共享同一个运行环境、同一个测试套件、同一个依赖树,是跑集合共享——这种任务并行时,冲突不在你写的代码里,而在环境的搅动里。适合并行的编码任务,往往是‘各写各的新文件、各自跑各自的验证、最后集中合入’这种形状;不适合的,是‘都动同一片代码、都在同一棵树上验证’这种形状。”
小a:“怎么判断一个任务是‘真独立’还是‘假装独立’?”
老z给了一个土办法:**问三个问题——它读什么?它写什么?它的成功依赖什么状态?**如果两个任务的“写集合”和“读集合”有交集,它们就不真独立;如果其中一个的成功依赖另一个的产出,它们就是有依赖。这个判断不需要调度器,你拿张纸画出来交集就能看出来。
“纸上画完交集还不够,”老z说,“还要把‘成功依赖什么状态’问到底。**任务声称‘成功’和它真的成功是两回事。**任务回报 lint 通过,但它跑在旧版代码上——这是‘假装成功’,不是成功。核验的办法是看它的验收产物:任务完成时留下了什么可观察的结果?这个结果是不是基于最新输入算出来的?并行环境里,没有可观察产物的‘成功’,跟没做没什么两样——因为它既不能被合并,也不能被追溯。”
小a:“那这种‘假装成功’在并行的场景里是不是特别常见?”
“常见,而且比串行更容易藏。”老z说,“串行时你盯着每一步,跑偏了马上看见;并行时你只能看到各执行者的最终汇报,汇报之间到底有没有互相踩脚,要靠合并阶段才发现。所以合并阶段不是‘把结果拼起来’,而是一次二次核验:每个结果都要能对回它的输入、它的工作区、它的日志。对不回来的结果,优先级要排在‘看起来没问题’的后面。”
小a又问:“那 Paseo 这种具体工具,我该怎么引用?”
“外部工具的版本、功能、调度行为会随发布而变化,本附录不替 Paseo 的任何版本背书。”老z说,“凡是引用它的具体调度行为(比如‘它对并发写是排队的’),都要标注核对日期和版本——比如‘截至 vX.Y / 2026-08 核对,默认冲突策略为报错’。一旦跨过核对日期,这条结论就要重新验证。把外部工具的引用当成有时效的证据,而不是常量。”
小a:“既然所有行为都待核验,那核验记录的格式,能不能给个模板?”
“能。”老z说,“五栏就够:对象(工具名加版本)、核对日期、测了什么、观察到什么、结论与有效期。‘观察到什么’那栏别写结论,写事实——比如‘两个任务同时写 config.ts,先后成功,前者的改动被后者的覆盖,无报错’。事实写清楚了,结论(这里是后写覆盖)谁看了都能复现。记录的目的是让别人不重复你的实验,所以别省观察那一栏。”
“还有一条最容易被漏掉的,”老z补充,“**更新工具时,核验记录比文档还重要。**工具的版本号一换,旧记录的‘有效期’就过了,你需要重跑冲突测试和失败归属测试。很多人升级工具只读 release notes——而 release notes 恰恰不会告诉你‘冲突策略改了’这种默认行为的变化。”
核验协作工具的两条底线
- 冲突策略必须可观察——两个任务撞了同一资源时,工具的行为是确定的(报错 / 排队 / 锁定),而不是未定义的。所谓“未定义”往往就是“静默覆盖”,是数据丢失的另一种说法。
- 失败必须可归属——某个执行者失败后,你能从日志追溯到它,而不是面对一堆混在一起的结果无从下手。这要求每个执行者有独立的日志流和可回放的输入。
“这两条底线,”老z说,“**分别是‘不丢数据’和‘能查事故’。**第一条没满足,你的并行在丢东西而不自知;第二条没满足,你的并行一出事就变成悬案。别的都可以先放着,这两条不行——它们是你敢把任务分出去的地基。”
“再回看开头那个‘并行更快’的方案,”老z说,“现在你知道怎么接它了:先让它回答四问(谁拥有、谁共享、谁回滚、谁的配额),再跑冲突测试和失败归属测试,最后拿串行基线对比。四问答不上、两条底线测不出来的方案,它的‘快’就是悬空的。”
回到全书目标:任何协作工具都应被视为一个可验证的运行时边界。在一手资料和实际运行证据不足时,保留“待核验”比补全产品故事更可靠。并行不是默认选项,而是证明独立后的优化手段——而且这个证明,得是你自己跑出来的,不能从工具的营销页抄。
小a把核验记录的五栏模板贴在了工位墙上。下次再有人提“用 Paseo 并行一下”,他知道自己要先问那句已经在嘴边的老问题:“你测过冲突策略吗?”