乐于分享
好东西不私藏

ai 编程的自我进化机制:自主编码循环的分层准入控制

ai 编程的自我进化机制:自主编码循环的分层准入控制

Closing the Loop Without Self-Approval: Tiered Admission Control for Autonomous Coding Loops

封面

系列说明 本文是“7×24 自主 AI 开发团队”系列的准入层深潜。系列另含:总纲《如何组建一支 7×24 自主 AI 开发团队:ppt-bot-v2 的三层架构实践》(building-a-7x24-autonomous-ai-team.md)、物理层深潜《先把灯点亮:7×24 自主 AI 开发团队的物理地基》(keeping-the-lights-on-7x24.md)、流程层深潜《不信代理,信门禁:基于角色分离与机械验证的多 AI 协作开发流程》(trust-the-gate-not-the-agent.md),以及姊妹篇《把软件公司装进生产服务器:三角色 × AI Company 无人值守自动开发工作流》(ai-company-workflow.md)。《不信代理,信门禁》回答了“为什么门禁不能信任代理的声明”,本篇回答它的下一问:当门禁的白名单本身需要人来维护时,如何让闭环在人不离场的纪律下自动闭合


摘要

自主编码循环会给自己提工单。若循环也能决定自己做什么,就同时掌握了“生产者”与“批准者”两种权力——成本失控与自我放大随之而来。ppt-bot-v2 项目用“创建者身份闸门”解决了这个问题:循环账号所提工单一律门禁拦截,除非进入操作者维护的准入白名单。但白名单的人工编辑随即成为闭环上的最后一个人工瓶颈:循环停在“无可执行工单”状态,等待人类往一个文本文件里追加行。本文总结该项目把这一人工步骤自动化而不破坏职责分离的分层准入设计:写者身份白名单(循环账号永不在内,机械强制执行)、策略即数据(操作者编辑的 markdown 策略块)、机械过滤先于 LLM 判断(LLM 只能收窄、不能放宽候选集)、失败大声的机械追加器(幽灵 ID 拒绝、每日上限、出处行与状态同址)、以及否决与熔断两个逃生口(删行否决、两次失误挂起标记)。该设计在 23 个 pin 测试固定不变量后上线,首轮生产真实运行即捕获一个真实缺陷(LLM 批准数超过每日上限时追加器整批拒绝,导致零准入),修复后闭环生效:闸门从“无可执行工单”翻转为可执行,循环在零人工编辑下自动恢复点火。实践表明:闭环的关键不是让 AI 更聪明,而是让“批准权”永远留在“生产权”之外,并用机械层把 LLM 的判断关进笼子。

关键词: 自主编码循环;职责分离;准入控制;LLM 护栏;策略即数据;机械化验证


1 问题:闭环上的最后一个人工瓶颈

1.1 自提工单与自批是同一个问题

一个 7×24 运转的自主编码循环,其问题来源之一是它自己:运行时错误、UI 冒烟失败、代码审查发现,都会由循环账号自动提单。这本身是好事——系统能给自己记账。但它带来一个权力结构问题:如果循环总能给自己派活,它就同时是生产者与批准者。后果有二:其一,成本失去外部约束,一个误判的循环可以无限给自己制造工作;其二,错误会自我放大——循环的幻觉变成工单,工单再变成代码。

ppt-bot-v2 的解法是把闸门键控在创建者身份上,而不是工单内容上。scripts/check-open-backlog.py 中的纯过滤器如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14


def split_actionable(issues, scope_ok, team_key="TRA", auto_user=None):
    normalized = {k.upper() for k in scope_ok}
    actionable, gated = [], []
    for issue in issues:
        key = f"{team_key}-{issue.get('number')}".upper()
        opted_in = key in normalized
        is_auto_creator = bool(auto_user) and \
            (issue.get("creator_username") or "") == auto_user
        ui_smoke = UI_SMOKE_MARKER in (issue.get("description") or "")
        if is_auto_creator and not ui_smoke and not opted_in:
            gated.append(issue)
        else:
            actionable.append(issue)
    return actionable, gated

规则一句话:循环账号创建的工单一律 GATED,除非它出现在准入白名单(scope-ok 文件)里;其他任何创建者的工单一律 ACTIONABLE。键控身份而非内容,是因为身份不可伪造、内容判断可被措辞绕过。闸门对调度器只暴露一个信号:有可执行工单时退出码 0,没有时退出码 3——2 小时一次的看门狗据此决定是否点火编码链,空转的 tick 成本为零。

1.2 人工白名单:闭环在最后一米停下

闸门解决了自批,却把批准权交给了一个最朴素的结构:一个纯文本白名单文件,唯一受权的写者是人类操作者。于是闭环上出现了最后一米的人工瓶颈:

  • 循环账号陆续提了 10 个真实问题(i18n、a11y、UX、内容质量),全部被正确门禁;
  • 闸门每次 tick 都返回“无可执行工单”,循环安静地停摆;
  • 恢复循环的唯一办法,是人类登录服务器,往文件里手工追加几行。

这不是一个可以“提醒一下就好”的瓶颈:它把自主系统的吞吐绑定在人类的注意力上,而人类注意力恰恰是这套体系想要释放的东西。需求因此非常明确:把人工准入自动化,但不把批准权还给循环。任何让循环“自己审自己提的工单”的方案都直接违反职责分离;任何让 LLM 无约束地写白名单的方案,则只是把自批从账号层面搬到了模型层面。


2 方法:职责分离的分层准入

2.1 架构:生产者与批准者永远不同名

总体结构如图 1。编码循环自提工单;闸门按创建者身份分流;被门禁的工单进入每日准入评审作业;评审以独立的复核者身份向白名单追加;白名单回流到闸门;可执行工单触发看门狗点火循环。闭环闭合了,但闭合路径上没有一处是“循环批准循环”。

图 1:闭环准入架构。循环自提工单,闸门按创建者身份分流,每日评审作业以独立身份写白名单,白名单回流闸门

身份矩阵是整个设计的承重墙:

身份
权限来源
每日上限
典型用途
循环账号
无(永不在写者白名单内)
只提单,不准入
scope-reviewer
机械追加器的身份白名单
3(策略给定)
每日评审作业的自动准入
operator
人类操作者
豁免
人工覆盖、误判纠正

关键点:白名单用环境变量定义(默认 operator,scope-reviewer),由追加器在写入前机械校验。职责分离不靠提示词、不靠自觉,靠的是追加器里的一行成员检查——行为可以被提示词骗,成员检查不能。

2.2 机械过滤先于 LLM 判断

准入评审作业是一个五层管道(图 2)。第一层是策略即数据:操作者的长期意志写在 markdown 的显式标记块里,解析器只读标记之间的行:


1
2
3
4
5
6


<!-- policy-begin -->
auto_categories: UX, a11y, i18n
max_priority: 3
daily_cap: 3
llm_review: true
<!-- policy-end -->

类别白名单 + 优先级上限构成机械过滤器,产出“候选集”。第二层才是 LLM 复核,而 LLM 的权力被严格限定为在候选集内做减法


1
2
3
4
5
6
7
8


valid = {f"TRA-{i.get('number')}".upper() for i in candidates}
approved = []
for entry in data.get("approved", []) if isinstance(data, dict) else []:
    iid = str(entry.get("id", "")).upper()
    if iid in valid:
        approved.append({"id": iid,
                         "reason": str(entry.get("reason", ""))[:120]})
return approved, None

LLM 返回的任何不在候选集中的 ID 都被静默丢弃——模型可以拒绝一个候选,但永远不能批准一个机械层没放行的工单。判断可以交给模型,边界必须留在代码里。

图 2:分层准入管道。机械过滤产出候选,LLM 只能收窄,上限裁剪后由机械追加器落盘并通知

2.3 机械追加器:失败大声,出处同址

第三层是追加器 scripts/scope-optin.py,它把三类失败严格区分,并用不同退出码表达:身份不在白名单、超过每日上限属于策略拒绝(退出码 2);幽灵 ID、已关闭 ID、tracker 不可达属于操作失败(退出码 1,拒绝盲追加)。追加前对 tracker 做一次“工单确实开放”的校验,是这个组件最重要的不变量——白名单里混入一个不存在的 ID,比少准入一个工单危害大得多。

写入格式同样是一个设计决策:出处信息以独立注释行与状态同址存放,


1


fh.write(f"# auto-optin {identity} {today} reason={reason}\n{i}\n")

而闸门的读取端跳过 # 行、整行取键:


1
2
3
4
5


for line in fh:
    line = line.strip()
    if not line or line.startswith("#"):
        continue
    keys.add(line.upper())

审计信息住在它所解释的状态旁边,且不污染状态解析——状态文件本身就是审计日志,操作者打开文件就能看到每一行是谁、何时、以什么理由加进去的。

2.4 降级、否决与熔断

三个逃生口保证系统在每一层失败时都有明确姿态:

  • LLM 降级: 复核命令超时、输出非 JSON、进程缺失,一律回落到“仅机械策略”,并在通知中注明降级发生。准入层允许降级,因为机械层本身就是安全底线;
  • 操作者否决: 删除白名单中的行即否决,下一次闸门读取立即生效。否决成本必须低于准入成本,否则监督就是空话;
  • 两次失误熔断: --suspend "<reason>" 写一个标记文件使作业跳过,--resume 移除。熔断由人或审查角色在两次坏准入后触发——v1 不做自动打击检测,因为“坏准入”的判定需要结果追踪,而结果追踪本身又需要门禁,先承认边界比假装完备更诚实。

3 工程验证

3.1 pin 测试:23 个测试固定不变量

两个测试文件共 23 个用例(追加器 13、评审作业 10),固定的不是“代码能跑”,而是设计不变量:身份白名单拒绝循环账号、每日上限计数且 operator 豁免、跨日不计数、幽灵/已关闭 ID 拒绝、tracker 失败时失败大声而非盲追加、重复 ID 去重、LLM 批准集必须是候选子集、策略块外的同名字段不生效、挂起/恢复标记语义、上限裁剪按紧急度排序。其中最后一组 3 个用例来自 3.2 的生产缺陷修复——每个生产缺陷都以一个 pin 测试收尾,这是该项目“被拦截的事件变成门禁”惯例在准入层的延伸。

3.2 首轮生产运行捕获真实缺陷

dry-run 一切正常,但首轮真实运行暴露了一个 dry-run 看不到的缺陷:LLM 批准了 4 条工单,而策略上限是 3,追加器对超上限批次整批拒绝——结果是零准入,且通知里看不出异常。修复是把上限裁剪前移到评审作业:批准数超过上限时按紧急度保留前 N 条,其余顺延到后续运行:


1
2
3
4
5
6
7
8
9
10
11
12
13
14


def apply_daily_cap(approved, candidates, cap):
    """Trim approvals to the daily cap, most urgent first."""
    if cap <= 0 or len(approved) <= cap:
        return approved, []
    rank = {}
    for i in candidates:
        iid = f"TRA-{i.get('number')}".upper()
        try:
            pri = int(i.get("priority"))
        except (TypeError, ValueError):
            pri = 99
        rank[iid] = (pri, int(i.get("number") or 0))
    ordered = sorted(approved, key=lambda a: rank.get(a["id"], (99, 10**9)))
    return ordered[:cap], ordered[cap:]

教训有两条:其一,强制执行层的行为必须在强制执行层测试,dry-run 打印的是“打算做什么”,不是“执行层会接受什么”;其二,全有或全无不总是美德——追加器的整批拒绝在身份/幽灵校验上是正确的(宁可不做),在容量约束上是错误的(应该截断),同一个“拒绝”语义需要按失败类别区分。

3.3 闭环生效与投入产出

修复上线后的真实运行:10 条 GATED → 机械候选 4 条 → LLM 逐条复核给出具体理由 → 上限裁剪保留 3 条、顺延 1 条 → 以复核者身份追加并逐条通知。随后闸门退出码由 3 翻转为 0,可执行清单按优先级排序,2 小时看门狗在下一个 tick 自动点火编码链——人类没有编辑过白名单的任何一行

项目
成本
收益
计算
每日一次受限 LLM 调用(4 条工单、约 400 字摘要/条)
闭环不再绑定人类注意力
代码
两个 stdlib 脚本(约 440 行)+ 23 个测试
准入纪律机械化、可审计
风险面
LLM 判断进入准入路径
机械层限定其只能做减法,降级路径明确
监督
操作者阅读通知的成本
否决成本(删一行)低于准入成本

4 可迁移的设计思路

  • 分离的必须是身份,不是提示词。 “让另一个 AI 来审”只有在审批者的写权限被机械地限定在另一个身份下时才成立。把职责分离落实为成员检查、文件权限或令牌范围,而不是系统提示里的角色名。
  • LLM 做减法,机械做边界。 先由确定性规则产出候选集,再让模型在集合内否决;模型的输出永远不能越过机械层的边界。这个“子集约束”模式适用于一切“AI 审批 AI”的场景。
  • 策略即数据,且带显式标记。 把操作者的长期意志从代码里搬进带标记的配置块,改策略不需要改代码、不需要重新部署,也不需要读懂代码——监督者的工具必须比被监督的系统更简单。
  • 出处与状态同址。 审计信息写在它所解释的状态旁边(且不影响状态解析),监督者打开状态文件即见全貌。分离的审计系统会在第一次事故时暴露同步问题,同址的不会。
  • 按失败类别选择失败姿态。 安全类失败(身份越权、幽灵 ID)要全有或全无地拒绝;容量类失败(超上限)要截断而非拒绝;建议类失败(LLM 不可用)要降级并注明。用一个退出码或一个异常类型掩盖三类失败,是这类系统最常见的事故源。
  • 首轮真实运行是测试的一部分。 有界真实运行(小上限、可否决、可熔断)应被视为验收步骤而非上线后的运气。本设计的第一个真实缺陷正是首轮运行捕获的。

5 在你的项目中落地

5.1 适用性自查

满足以下三条中的两条,本模式大概率适用:(1) 存在会自动产生工作项的代理/流水线;(2) 存在一个“是否执行”的闸门,且闸门当前由人工维护白名单或人工审批;(3) 你希望系统在无人值守时继续运转,但又不信任代理自我派单。若你的系统没有闸门(代理想做什么就做什么),先补闸门,再谈自动化准入——顺序不能反。

5.2 实施顺序与最小可行子集

步骤
内容
是否最小可行子集
1
闸门键控创建者身份(纯函数 + 退出码信号)
2
机械追加器:身份白名单 + 幽灵 ID 校验 + 出处行
3
策略文件(类别/优先级/上限)+ 机械过滤器
4
定时调度(cron/launchd 均可)
5
LLM 复核层(子集约束 + 降级注明)
否,可后加
6
通知 + 否决通道 + 熔断标记
否,但强烈建议

步骤 1–4 约 200 行 stdlib 代码即可构成最小闭环;LLM 层是锦上添花,且只有在子集约束写好之后才是安全的。

5.3 参数选择

参数
本项目默认
含义
调参方向
auto_categories
UX, a11y, i18n
允许自动准入的类别
主观性强的类别(内容润色、重构)保持人工
max_priority
3
优先级上限(1 最急)
越不信任循环,上限越紧
daily_cap
3
每日自动准入上限
从 1–3 起步;监督带宽增加后再放宽
llm_review
true
是否启用 LLM 复核
关闭后回落到纯机械,仍是安全底线
LLM 超时
300s
复核命令超时
超时即降级,绝不阻塞调度

5.4 坑位清单

表现
机械对策
超上限整批拒绝
LLM 批准数 > cap 时零准入
上限裁剪前移,按紧急度截断
LLM 输出带噪声
CLI 横幅/警告污染 stdout,JSON 解析失败
解析失败即降级并注明;或提取最后一个 JSON 对象
身份混淆
循环进程拿到追加器凭据
追加器凭据与循环凭据分账户存放,白名单在代码内校验
幽灵 ID 入白名单
闸门对不存在的 ID 无感,审计失真
追加前校验工单开放,失败大声
出处行破坏解析
审计信息混入状态行
出处用注释行,读取端跳过 #
静默失败
通知失败无人知晓
通知尽力而为,但降级与熔断必须进入通知通道

5.5 验收清单

  • 循环账号调用追加器被拒绝,且有对应 pin 测试;
  • 超上限批次被截断而非整批拒绝,顺延项可见于输出;
  • LLM 批准的 ID 全部属于机械候选集(子集约束有测试);
  • LLM 不可用时作业仍完成机械准入,且通知注明降级;
  • 幽灵/已关闭 ID 追加被拒绝,退出码非零;
  • 白名单文件含出处行且闸门解析不受影响;
  • 删行否决在下一次闸门读取即生效;
  • --suspend
     后作业跳过,--resume 后恢复;
  • 首轮以有界真实运行(cap=1)验收,而非仅 dry-run。

总结

自主系统的闭环从来不是“让 AI 自己决定做什么”,而是“让决定做什么的权力,永远住在产生工作的进程之外”。ppt-bot-v2 的准入层用四个机械结构实现了这一点:身份白名单让职责分离不可伪造,子集约束让 LLM 只能做减法,失败分类让系统在每种故障下都有明确姿态,出处同址让监督者打开一个文件就能看见全部真相。23 个 pin 测试与一次首轮生产缺陷修复表明,这套设计的价值不在于它多聪明,而在于它把“AI 审批 AI”这件危险的事,变成了一组可以被测试、被审计、被否决、被熔断的普通代码。闭环由此闭合——而批准权,始终没有回到生产者手里。


联系方式: linmk@tup.tsinghua.edu.cn