从 ADE 到 orchestrator:PABLO 的诞生
我的 agent 变快了,我的脑子却没有。
我们走到哪一步了
上一篇文章里,我介绍了自己的工作环境:一个 Agent 开发环境(ADE),它不是给 IDE 外挂一个聊天面板,而是围绕任务、worktree 和 agent 打造的工具。我提一个 issue,ADE 就开一个隔离的 worktree,里面已经有个 agent 在跑,我做的是指挥这个 agent,而不是亲手敲代码。在此之上,是我打磨了好几个月的 agent,它们从设计上都是只读的;只有 build 这个 agent 有权改代码。
这些 agent 带来的收益集中在一件事上:理解。在公司里我们没有专门的团队划分,所以谁都可能接到任何任务:早上是库存同步的 bug,下午是我从没打开过的那块代码里的新功能,再下一个是零售应用里的工单。每个任务都可能是完全不同的领域,有自己的业务规则,而我一天要在它们之间切换三四次。task-analyst 这个 agent 会读工单、读它的父级工单、读相关代码,然后一次性把这些都摆到我面前。
读懂一个工单是一回事,可功能并不是 push 完分支就算完。代码和生产之间还隔着另一套活儿,全是等待和核对:CI 状态、review、QA。这些都在 GitHub 和 Jira 里,在我的 worktree 之外。我把「思考」自动化了,却把「等待」留给了自己。这篇文章讲的就是我为了填上这道沟而做的工具:PABLO,一个 Symfony 应用。它的核心是一台状态机——PHP 开发者十年来一直在交付的东西,只是现在 AI 圈子给它起了个更时髦的名字。
没人自动化过的那些琐事
我早上真实的模样是这样的:在写下任何一行代码之前,我得先在脑子里过一遍清单,上面有 5、6 个开着的 pull request 和两三个项目。
- CI 通过了吗?ADE 只显示当前工作树(worktree)中 PR 的状态,其他 PR 都散落在各个浏览器标签页里。
- 有人审查过它们吗?现在是不是该催一催了,又不会变成那个每 90 分钟就催一次的人?
- QA 已经排期了吗,还是被搞坏了?
- 工单上有新反馈吗?要找到它,还得为一个上周已删除的分支重新创建工作树。
- CI 挂了?那在我自己重新推动一切之前,智能体为我做的一切都是白费:发现失败、创建工作树、选择智能体、启动它、等待。
这些事都不难,而问题恰恰就在这里:每一点都会带来一次上下文切换,而我花了一年时间才消除掉上下文切换。真正让人难受的从来不是那几分钟,而是那种交织感:检查 CI,回到某个任务,想起没人审查过,再回来。我的智能体消除了噪音,让工作变得容易理解。但跟踪所有这些任务的成本仍然落在我身上。三个工具里十几个动态部件,而在这样的系统里,总得有什么东西去轮询。那个东西就是我。
PABLO
所以我做了 PABLO,全称是 Personal Assistant for Boring Logic & Operations(无聊逻辑与运营的个人助理)。这个缩写是名字取好之后才凑出来的,但“boring”(无聊)是里面最诚实的词。PABLO 做的事一点都不聪明:它查看拉取请求,读取日期,和 10 分钟前的状态比较,然后判断某个东西变了(或者没变)。ADE 仍然是我工作的地方,PABLO 则待在上一层,循环回答一个问题:考虑到这个拉取请求的状态,有没有什么智能体该做的事,如果有,是哪个?当答案是“有”时,PABLO 就通过 ADE 的二进制文件自动启动那个智能体,在正确的工作树里,完全就像我会做的那样。
底层是一个 Symfony 控制台应用,有三个入口:
- 一个 pablo 二进制文件;
- Symfony 控制台;
- 一个小型仪表盘。
这张表里有个例外:/pablo-commit-and-pr 是一条 OpenCode 命令,而不是 PABLO 的命令。这个 agent 负责写提交信息和 pull request 描述(整个流程里唯一一处 agent 确实比几行代码更靠谱的环节),然后把任务交回 PABLO;PABLO 把任务切到草稿(draft)状态,接手后续。最后,一个后台调度器(Linux 上是 systemd,macOS 上是 launchd)每五分钟刷新一次 pull request 的信息。
这种拆分比看上去更重要。PABLO 对 ADE 的全部要求就两件事:在某个 worktree 里启动一个 agent,跑完之后通知它。所以 PABLO 对 ADE 的了解全被收在一个接口后面:AgentLauncherInterface。有了这层接口,ADE 就可以替换——如果下个月出现更好的,我只要重写一个类。这种替换已经发生过一次:之前我用 Orca,现在用 OpenChamber。还有一处耦合更深:当 ADE 拒绝某个 worktree 时,兜底方案是用 opencode run 走无头模式。PABLO 真正依赖的只有 OpenCode。
2026 年了,后台守护进程为什么用 PHP?
因为这是我想得最快的语言,而唯一的用户就是我自己。
最后一个决定:PABLO 不存任何 token,也不直接调用任何 API。一切都交给本机已经认证过的命令行工具(gh、acli、linear、orca、openchamber),所以既没有需要常驻运行的东西,也没有会泄露的东西。缺了什么,pablo system:doctor 会直接指出来,而不是让某个 cron 任务在凌晨三点悄悄失败。
智能一直都在 agent 那边。缺的是一个笨到能每五分钟跑一次、永远跑下去也不会腻的东西。
Graph engineering:不过是一个换了名字的状态机
如果你最近读过关于 AI agent 的文章,应该见过“graph engineering”这个词:把工作建模成节点和边,而不是一个巨大的 prompt。去掉这个时髦词汇,它不过就是个状态机。因为软件开发本身就是混乱的,PABLO 的状态机必须足够灵活。多个状态都应该能轻松回落到 draft,而且手动命令可以随时把任务从某个状态中拉出来。重型状态机框架要求你提前声明每一条允许的转移,并封锁其他所有路径。这对我的工作流来说太死板了,所以我干脆跳过框架,自己写了一个简单的引擎:状态、转移,再加上一条关于“从哪儿能到哪儿”的宽松规则。在大多数 agent 框架里,节点是推理步骤(计划、搜索、总结、评审),边决定下一步去哪个节点。而在我这里,节点不是思考的时刻:它们是 pull request 的具体状态。draft、in-review、ci-red、approved、in-qa、merged。边也不是模型的决策,而是规则:如果 CI 从红变绿,就送回 in-review;如果 review 通过了,就进入 in-qa;如果 QA 返回了反馈,就退回 draft。这一切都不需要 LLM:只需要比较时间戳和一张转移表。
每个状态都知道两件事:pull request 进入这个状态时是什么样子,以及该启动哪个 agent(或者不启动)。正是这个特性让工具变得可控:PABLO 不需要“理解”PR 才能决定做什么,它只需要识别 PR 当前处于哪个状态。智能留在 agent 里;PABLO 只负责把任务路由过去。
这些环节串起来,就形成一条我不用再盯的流程:CI 半夜挂了,PABLO 打开 worktree,跑 build,重新触发 CI,第二天早上 dashboard 里的状态已经更新好了。有 review 进来,它把任务切回 review 状态,触发 task-analyst,再把摘要发给我。QA 在 Jira 里传了张截图,它就基于那个分支重新开一个 worktree,跑对应的 agent。我的角色变了:不再轮询,而是做决定。我看 agent 理解了什么,然后决定下一步往哪走。
2026 年用状态机加 cron 来写编排器,多少有点讽刺。整个圈子都在聊图、记忆、多步推理;而真正帮了我大忙的,是一个按状态走的 switch 和一个 systemd timer。但这恰恰是重点:花了一年把智能做出来之后,最后一块拼图不需要智能。它只需要每五分钟准时出现,不出错、不厌烦,而且永远不占用我的注意力。
PABLO 不解决那个难的问题。它拿走的是那个无聊的问题——而这正是我对它的全部要求。
而图活在任务内部。在 PABLO 里,节点就是真实团队中一个 pull request 会经过的状态:drafted、ci-red、等 review、changes requested、等 QA。图不是 agent,它是我本来就在做的那套流程。这也是为什么下次模型更新后图还能保持稳定:prompt 和模型都活在节点内部。所以状态机其实是一张表。真的就是类里的一张表:
State::Draft->value => [
'emoji' => '📝',
'label' => 'draft',
'onEnter' => [self::class, 'enterDraft'],
'polled' => true,
],
State::CiRed->value => [
'emoji' => '🔴',
'label' => 'ci-red',
'onEnter' => [self::class, 'enterCiRed'],
'polled' => true,
],
表里每一行声明这个状态怎么显示、进入时执行什么、poll 器要不要盯着它。在第一张表旁边还有第二张,说明每个状态能验证什么:
public const POLL_CHECKS = [
State::Draft->value => ['checkCiRed', 'checkCiGreen'],
State::CiRed->value => ['checkCiGreen'],
State::ReadyToReview->value => ['checkCiRed', 'checkReviews'],
State::WaitingReview->value => ['checkCiRed', 'checkReviews'],
State::NeedsTesting->value => ['checkFailureSignal'],
];
整个引擎就这么多。新增一个阶段,只需加一行配置、写一个方法,不必重构。轮询器、命令和人工覆盖共用同一个 on-enter 处理器,因此状态迁移永远不会出现分叉。tracker 的设计思路也一样:只定义一个 provider 接口,状态机根本不知道自己对接的是哪个 tracker。
状态,以及唤醒它们的东西
一共九个状态。刻意没有 todo,也没有 init:我还没开始的 issue 本来就没有对应任务,它只会出现在分配给我的列表里。进入 in-progress 时,会创建任务、分支、worktree,并跑第一轮 agent;一旦合并,不管从哪个状态来,任务都会被关闭,worktree 也会被删掉。
| 状态 | 触发方式 | 进入该状态时发生什么 |
|---|---|---|
🔨 in-progress |
pablo task:start <issue-url> |
创建分支和 worktree,运行一次 task-analyst,如果项目有启动脚本也一并执行 |
🥱 waiting |
pablo task:waiting |
什么都不做,轮询停止。回退时会恢复之前的状态 |
📝 draft |
/pablo-commit-and-pr |
提交、推送,打开 draft pull request |
🔴 ci-red |
轮询器:CI 失败 | 针对失败的检查和日志运行 ci-analyst |
👀 ready-to-review |
轮询器:CI 通过 | 在 GitHub 上把 pull request 标记为可审查,然后立即切到下一个状态 |
👀 waiting-review |
从上一状态进入 | 什么都不做,任务在这里等待人类 |
🧪 needs-testing |
轮询器:收到一次批准的 review | 记录时间戳,作为 QA 的基线 |
🔁 request-changes |
轮询器:changes requested | 把 pull request 退回 draft,运行 pr-feedback |
🚨 testing-failed |
轮询器:QA 失败信号触发 | 把 pull request 退回 draft,运行 task-feedback |
整个东西的形态,用图表示:
看箭头,不要看方框:每一条指向 draft 的箭头,都是我敲下的命令;其余箭头都是轮询器。我决定工作什么时候离开我的手,机器负责剩下的部分。
有三个细节花的时间远超预期。
第一个:什么才算一次 review。 我自己的 review 要排除掉:在我自己的 pull request 下回复评论,会生成一条以我名义的 review,否则光是回答一个问题就会把自己打回 draft。机器人 review 除非加入白名单,否则忽略。只有发生在 GitHub 最近一次 ready_for_review 时间线事件之后的 review 才算数(那是真正的时间戳,不同于 summary 里的结论),所以过期的批准不会混进这一轮。每个 reviewer 的最后一次 review 生效,而任何非批准的内容都压过所有批准:如果有人专门花时间写了东西,我会在 QA 之前先读反馈。
第二点:回到 draft 的路径有且只有一条,而且必须通过命令触发。 不做 push 检测:单纯 git push 不会改变状态。这听起来限制很死,但考虑到我一天要 push 几十次,很多时候只是为了触发 CI,就能理解了。显式命令是信号,push 是噪音。
第三点:哪些东西是我故意没当成事件的。 CI 还在跑,不触发任何动作。needs-testing 里 CI 红了,也不触发:构建挂了在合并时自然会暴露,中途打断 QA 对谁都没好处。设计状态机有一半工作是在决定什么不算事件,而这一半几乎没人关注。
「绿色」到底意味着什么
其中两个状态转换都取决于一个问题:CI 是不是绿的?这听起来像是个查一下就有答案的事实。其实不是。
我最初的版本:失败就是红,全部通过就是绿,其他情况算等待。这套规则在我最需要 PABLO 的那个项目上翻了车。那个项目跑在 CircleCI 上,CircleCI 会把部署门禁(deployment gates)以状态上下文(status contexts)的形式发布到 GitHub:deploy-code-approval-ppr、deploy-code-approval-qlf。这些是每条流水线上的手动门禁,包括 feature 分支,而没人会去点它们(我们不会每次提交都部署到预发环境)。它们就一直停在 PENDING,或者变成 ACTION_REQUIRED,而我的规则把后者算作失败。于是「CI 绿了吗?」的答案永远是「否」。
修复方法是在 PABLO 里加一行配置:
ci:
ignore_checks: ["approval"]
只要某个 check 的名称、workflow 或状态上下文里包含这个子串,它就会被整个排除在判断之外:既不会让结论变红,也不会让它永远卡在等待状态。两个门禁,一个词搞定。
这个配置项让我意识到一件事:CI 显示绿色是一条规则,而不是事实。GitHub 所谓的"CI check",其实是一堆嘈杂的单元测试、手动部署门禁和话痨机器人。既然没有工具能神奇地猜出哪些检查对某个分支真正重要,PABLO 就把这个决定权交给项目的 YAML 文件。为了让引擎保持简单,我又加了两条粗暴的规则:一个仓库如果零检查,就视为绿色;子串匹配则故意保持"笨"的方式。
我自己信条的唯一例外
在上一篇文章里,我总结过:我的 agent 都是只读的——不是靠指令约束,而是靠构造保证:frontmatter(我在那篇文章里详细解释过这是什么)直接拒绝 edit 和 write。PABLO 的四个分析师都遵守同一规则:用便宜快速的模型,禁止写入。
但一共有五个 agent。第五个是……
ent 就是 rebase-conflict-resolver,也是我唯一故意打破自己规则的地方。每十二小时,一个同步任务会把我的每个任务 worktree 都 rebase 到主分支上。一旦发生冲突,git 就放弃并重置 worktree,然后启动一个 agent 重新做 rebase,逐个解决冲突。
它的 frontmatter 暴露了代价:edit: allow、write: allow、git push*: allow。为什么要放它进来?因为同步默认是 dry run。有一个配置项能把报告变成实际行动:auto_apply: true,只在我确信风险最低的地方才会设置。
它的活动范围也像其他所有 agent 一样受到限制。它可以写代码、可以推送,但任何针对流程而非代码的命令(合并、批准、评论)都会被拒绝。它可以重写我的分支,但不能批准它、不能合并它,也不能以我的身份发言。真正危险的权限从来不是 edit;而是在别人面前以我的身份行事。
还有两道防线:工作区不干净(残留未提交文件)的分支永远不做 rebase;当 agent 接手一次 rebase 时,PABLO 会给出一条清晰、可直接复制的命令,用来在 OpenCode 里查看这个 agent 的会话。
剩下的安全兜底交给 Git。推送永远用 --force-with-lease,绝不用裸的 --force。如果我在 cron 运行期间自己推了代码,lease 校验就会失败,worktree 回退,这个任务十二小时后再重试。我的推送永远赢。当冲突确实需要人来处理时,agent 会输出 PABLO_CONFLICT_UNRESOLVABLE,什么也不推。
说句实在话:和另外四个 agent 不同,这个 agent 是真的可能搞出乱子的。我接受这个风险,因为它写出的所有东西最终都落在一个 pull request 里,合并之前我会亲自 review。那是我自己的代码。作为交换,我已经有一阵子没碰到过有冲突的 pull request 了。
不是所有事都值得动用 LLM
在上一篇文章里,我介绍了自己最喜欢的两个 agent 命令:每天跑的提示词,调用 gh 命令行工具,读取我打开的 pull request,再用 LLM 写一条 Slack 消息,请团队来做 review 和 QA。当时看着像魔法。如今两个都没活下来。它们现在就是 pablo show:prs:一个 PHP 脚本,从轮询器的缓存里读数据,输出的 markdown 和当年一模一样、可以直接贴进 Slack——先是待 review 队列,一条分隔线,然后是待 QA 队列。全程没有 LLM。
同样的直觉也塑造了那个 Web 面板——它同样从轮询缓存渲染,而这引出了一个我没预料到的结论:一个用来编排 AI agent 的工具,本质上不过是时间戳、文件锁、解析出来的命令行输出,再加一张允许的状态转移表。智能在那五个 markdown 文件里;其余全是毫不起眼的管道,只负责决定什么时候拧开水龙头。
真正改变的地方
最明显的变化:我并没有少检查,而是不再自己去检查了。不用再为了红色流水线挨个翻我的 pull request,也不用再追着找 review。一个列表、pablo tasks 或者 web dashboard,会告诉我接下来该做什么;剩下的都是别人的回合。
仅 CI 这一个场景,就足以证明值得构建 PABLO。CI 在晚上十一点变红,poller 不到十分钟就注意到,并在正确的 worktree 里启动 ci-analyst;到早上,诊断结果已经等在那里了。分析早就由我的 agents 自动化了;这次变的是,「注意到」和「等待」也自动化了。
现在一个完整任务是这样走的:对 Jira 链接执行 pablo task:start,和 agent 一起干活,/pablo-commit-and-pr,然后就不再去想它。
pablo task:start https://acme.atlassian.net/browse/XXX-123
大脑时间才是真正的资源
现代开发有一项隐藏成本,不是检查 CI 流水线花的那三分钟;而是那个标签页一直挂在脑子里所交的认知税。深度工作需要长时间不被打断的注意力。如果你用一堆行政式的来回把它打碎:刷 GitHub、追着要审批、琢磨 QA 有没有接你的 ticket。你损失的不只是时间,而是解决难题所需的那部分脑力。
我构建 PABLO,不只是为了少敲几行 bash,也不是为了自动化而自动化。我构建它,是让它当一面认知盾牌。把上线的官僚流程交给一个傻乎乎、不知疲倦的 PHP 循环,我就不再是自己任务的「人类路由器」。我又能单纯地做个工程师了。
PABLO 已公开
PABLO 已经公开:github.com/korbeil/pablo。它只为一个用户而建——我团队的工作流、我常用的命令行工具、我对等待的忍耐度;这些没有一样是通用的。把它公开,就像递给别人一份整理得很整齐的配置文件:可以读、可以借、也可以弄坏。状态机之所以做成一张表,正是为了让你的那张表能替换我的,而不用打架。
想试试的话,最短路径是这样:
git clone https://github.com/korbeil/pablo && cd pablo
./bin/install.sh # deps, liens agents/commandes, démarre le scheduler,
# finit avec pablo system:doctor
pablo system:doctor # quel CLI manque ou est déconnecté
pablo project:new # ajouter un projet ; ou déposer un YAML dans ~/.pablo/projects/
你需要 PHP 8.4+,以及几个已经各自登录好的 CLI(gh、acli、linear、opencode)。然后挑一件小事,跑 pablo task:start <issue-url>,再执行 /pablo-commit-and-pr,之后就不用再惦记它了。如果你更喜欢网页面板而不是命令行,pablo web 会在 8321 端口把它跑起来。