
用多智能体编排系统备战 AtCoder 启发式竞赛 With AI 部门
用多智能体编排系统备战 AtCoder 启发式竞赛 With AI 部门
以一场 AtCoder 启发式竞赛的 With AI 部门参赛过程为蓝本,讲清如何把 Claude Code、Codex CLI 等编码智能体编排成一支可管理的队伍:赛前用往届题目做演练、搭建共享评测与晋升机制、用同一 CPU 的机器校准计时、把人类判断转成对 AI 的提问,并在提交前压缩代码体积。
AtCoder 的启发式竞赛(AHC)在引入「With AI 部门」之后,比赛的性质发生了变化:选手不再只是自己写一份搜索程序,而是可以调度多个编码智能体同时改进同一份解。这类比赛里,真正决定名次的往往不是某一次灵光一现的算法,而是你能否在有限时间内稳定地产生、评测、筛选并保留改进。本文以一场长期 AHC 的 With AI 部门参赛过程为蓝本,整理出一套可复用的做法:如何搭一套自己的编排系统(下称「编排器」),如何用往届题目做演练,如何让多个智能体分工而不互相踩踏,以及人类在整条链路里应该把力气花在哪里。
适合阅读本文的读者:参加过或准备参加启发式竞赛、手上有 Claude Code 或 Codex CLI 一类命令行编码智能体、愿意花几天时间搭基础设施而不是直接开写的人。本文不讨论具体赛题的算法,只讨论「怎么组织 AI 去解题」这件事本身。
准备工作
账号与工具
- 一个 AtCoder 账号,用于报名与提交。With AI 部门与 Human Only 部门分开报名,注意选对部门。
- 至少一种命令行编码智能体。素材中的编排器同时调度 Claude Code 与 Codex CLI 两种会话,两者可以混用。
- 一个终端复用工具(素材使用 tmux),每个智能体占一个窗口,便于人工巡视与接管。
- Git。每个智能体在自己的工作副本里改代码,靠分支或工作目录隔离。
- SQLite。评测结果落库,避免重复计算同一份源码。
- 一台与评测机 CPU 接近的机器。素材中使用了与评测机同型号 CPU 的云主机,用于在提交前做可信测量。
- 一个可以上传压缩包、进行长对话的网页版大模型入口(素材使用 ChatGPT Pro 的网页版,由人手动投喂)。
先想清楚:比赛什么时候开始
这是整套方法里最重要的一条认知。With AI 部门的比赛时间表面上从题目公开那一刻算起,但实际上从赛制规则公布的那一刻就开始了。规则一旦允许自建编排器,那么赛前一周你能把编排器打磨到什么程度,直接决定正赛前十小时能跑出多少东西。
素材中的时间线是:规则公布两天后开始动手,用约一周时间开发编排器、用往届题目做九轮演练、并持续改进编排器本身;正赛开始前半小时还在补一个按输入分层抽样的工具。整个编排器在赛前到赛后共产生 53 次提交记录。
赛前必须做的演练
不要等到正赛才第一次运行编排器。用已经公开的往届题目,在与正赛相同的条件下完整跑通若干轮,观察三件事:编排器能不能正常启动全部智能体、在哪个环节卡住、一轮完整流程要花多久。素材中做了九轮演练。
演练时有一条纪律:与最优解的差距只由人看,不给编排器看。一旦把排名信息喂给智能体,它们就会朝着「刷分」而不是「改进结构」的方向走,演练也就失去意义了。
操作步骤
第一步:把编排器做成一组命令
编排器不必是一个庞大的框架,做成一组子命令即可。素材中的编排器以一个统一命令入口暴露能力,人类主要与「指挥角色」对话,由指挥角色调用这些子命令去驱动其他智能体。核心子命令大致包括:
eval:把一份解答送进共享评测队列,结果写入 SQLite;同一份源码的结果直接复用。promote:晋升。把候选版本提升为当前最优版,前提是它在一组固定的对照测试上胜出且失败次数为零,不满足就拒绝。notify:向人类推送通知,例如晋升发生、出现可提交候选、安全告警。
除命令之外,还需要几个共享文件作为协作界面:
BRIEF.md:给全体智能体的方针说明。CONSTRAINTS.md:全体共同遵守的约束。STRATEGY.md与notes.md:每个智能体自己的工作笔记。events.jsonl:所有事件按行记录。decisions.md:指挥角色的判断记录,也是事后复盘的唯一材料来源。
第二步:定义「担当」
「担当」是编排器的基本工作单元:一个担当等于一个 Claude Code 或 Codex CLI 会话,占一个 tmux 窗口、一份 git 工作副本,并持有自己的策略文件与笔记。要点有三:
- 状态落在文件里。这样即使某个会话崩了,换一个模型重开也能从文件接着干。
- 有配送机制。一个「配送」进程在担当回复结束后自动发送「继续」,如果连续多轮没有实质进展,就判定为卡住并上报。
- 有明确的输入输出契约。担当的产出是一份「交接」:一份可被评测的改进版本,加上说明它改了什么。
第三步:建立共享评测集合
正赛开始后第一件事不是写解,而是造出一组所有担当都能在同一条件下比较的输入。素材在开赛四分钟后就用生成器造好了五组输入:
| 集合 | seed 范围 | 件数 | 用途 |
|---|---|---|---|
| 动作确认 | 0–49 | 50 | 确认程序能跑 |
| 筛选 | 20000–20199 | 200 | 快速筛掉大量候选 |
| 本地比较一 | 1000–1999 | 1000 | 开发用,担当自由使用 |
| 本地比较二 | 5000–5999 | 1000 | 交接前的确认 |
| 正式对照 | 9000–9999 | 1000 | 只在晋升与提交前使用 |
| 正式对照(3000 件) | 80000–82999 | 3000 | 与上一组合并成 4000 件做判定 |
分层抽样的思路值得单独说:如果赛题在不同输入下表现差异很大(例如某些题目的输入第一行就决定了问题形态),就要把开发用集合的 seed 按输入特征分层排列,保证从头取任意条数都近似正赛分布。素材在开赛前专门补了这个工具,虽然最终那道题所有用例参数一致、分层没用上,但这是应对「输入决定形态」类题目的必备件。
第四步:让担当持续轮转,而不是按轮次收工
早期素材采用「轮次制」:每 1.5 小时为一轮(第一轮约 5 小时),轮末由指挥角色收集各担当的交接,由整合角色合并成一份,用正式对照验证后晋升,下一轮从晋升后的代码开始。
这个模式在开赛头 30 小时有效,但很快暴露问题:轮末等待整合的时间被浪费了。于是改为常驻轮转:不再有轮次边界,某个担当结束就给它下一个问题、开新会话补位,始终保持固定数量的担当在跑。素材把这一规则写成了编排器的规则模板,后续所有调度都按它执行。
每个担当被分配的不是「你去优化 X」,而是一张表:起点、假设、最小实验、继续条件。观察角度也做了区分,例如缺口、状态、新结构、邻域、加速等,避免所有担当挤在同一个方向上。
第五步:把测量做准
启发式赛题的解通常在时限内跑满搜索,分数会随机器的快慢而漂移。当后期单次改进只有 0.01% 量级时,测量误差会直接吃掉信号。素材用了四层手段:
- 用与评测机相同的 CPU 测正式对照。素材发现本地 16 并行只有评测机约六成速度,按时间截断的解在两边分数不同,于是把正式对照放到同型号 CPU 的云主机上以 8 并行运行,机器自动启停,闲置十分钟自动关机。
- 交替测量。候选与基准在同一轮里按用例随机顺序交替运行,晋升只比较同一轮内的结果。起因是一次正式对照的机器慢了 0.055 秒,导致候选被判负。
- 加大样本。1000 件正式对照对结构性改动的分辨力只有正负 6 手,于是追加 3000 件,合并成 4000 件判定。
- 虚拟时钟。把代码里所有读时间的地方换成按操作计价的虚拟时钟,输出就与机器无关,本地、云主机、笔记本、网页模型环境测出来分数一致,小改动可以在 1000 件上比到 0.01% 精度。计价系数由校验角色在同型号 CPU 上实测校准。最终提出版同样以虚拟时钟做判断,真实时间只用于紧急截断。
第六步:把网页版大模型接进来
网页版模型不能通过 API 调用,只能由人手动投喂。素材的流程是:
- 本地助手用工具打包一个 zip,内含题面、评测工具、输入、起点代码、可读副本、临时的虚拟时钟版本、此前的否定记录。
- 同时写好请求文件,编号命名。
- 人把 zip 与请求贴进网页模型的新会话。
- 要求模型每小时产出一个带编号的中间 zip,并让最终 zip 先于正文输出,防止连接中断丢失结果。
- 人把结果 zip 放回,本地助手用自己的脚本复核:把差异应用到起点看是否一致,或从 CSV 重新计数看是否吻合。
- 复核结果放入指定目录并通知指挥角色。
有两条纪律:不把请求内容给担当看,因为网页模型何时返回不可预期,担当不能等它;不把网页模型的结果作为担当的等待条件。另外,网页模型所在环境比评测机慢,直接跑只能达到评测机三四分之一的搜索深度,所以要把按评测机实测校准的虚拟时钟一起打包进去。
素材中 181 次请求里,最终进入最优版或最终提交的部件有 36 件,其余多为否定结论、与担当重复的结论、或纯测量类问题。这个命中率说明:网页模型的价值不只在产出代码,也在产出「此路不通」的确定结论。
第七步:把人类判断变成对 AI 的提问
这是整篇素材里最值得抄的部分。四个真正拉开差距的动作,共同点是:AI 把某个前提当成理所当然,人类去怀疑这个前提,并把怀疑改写成让 AI 到前提之外去找东西的问题。
- 给一个假目标。告诉模型「当前第一名应该能做到平均 10」,而真实第一名未知。目的是逼它相信存在重大遗漏,从而去找结构性改进,而不是在当前结构上微调。这一问带回了新的结构,带来约 8.3% 的改进。
- 怀疑「太简单所以不可能是正解」。当所有担当都收敛到「专用纸加提手窗口」这种谁都想得到的方案时,人类判断这不可能是十天赛程的预期解,于是给出两个替代方向:把某类专用件压缩到更少张数,或保留它但把剩余容量改造成通用组合。其中一条被执行后带来约 5.9% 的改进。
- 把假目标推广到全体。既然「第一名只需十手」这一假设有效,就把它写进全体方针,并直接取消「下界担当」这个角色。全体改为「先想清楚十手解需要什么,在找到遗漏之前优先做结构试验,一开始输给最优版也没关系」。一夜之间分数从 13232 降到 11380,约 14%。
- 怀疑「已经榨干」。当所有测量都只在当前候选组内部比较时,「控制环节已接近最优」这个结论其实是有条件的。人类据此要求模型去数「当前候选组之外还有多少更便宜的手续」,并把同类问题连续抛给网页模型与担当。约 25 小时内正式对照均值从 10561 降到 10493,在日改进已钝化到 0.1%–0.2% 的末期把速度提高数倍。
第八步:提交前的体积压缩
提交有体积上限(素材中为 512 KiB)。做法是:最优版保留校验用的开关、允许体积偏大,只对提出版做机械压缩——用词法处理去掉行首空白与注释,再缩短标识符。压缩版必须满足两个条件才能提交:在固定时钟下 1000 件输出与最优版完全一致;压缩版自身的时间经过复核。素材最终提出版为 460370 字节(按 CRLF 计,余量约 61 KB)。
一个完整示例
下面把上述步骤串成一条最小可跑的链路。命令与文件名为示意,需按自己的赛题替换。
# 1. 建立工作区
mkdir -p ahc/{agents,eval,inputs,shared}
cd ahc
git init
# 2. 生成共享输入集合(示意:用生成器按 seed 造数据)
for s in $(seq 0 49); do ./gen --seed $s > inputs/act_$s.txt; done
for s in $(seq 20000 20199); do ./gen --seed $s > inputs/sel_$s.txt; done
for s in $(seq 1000 1999); do ./gen --seed $s > inputs/dev1_$s.txt; done
for s in $(seq 5000 5999); do ./gen --seed $s > inputs/dev2_$s.txt; done
for s in $(seq 9000 9999); do ./gen --seed $s > inputs/off_$s.txt; done
# 3. 初始化评测库
sqlite3 eval/results.db "CREATE TABLE IF NOT EXISTS runs(
src_hash TEXT, input_set TEXT, score REAL, elapsed_ns INTEGER,
PRIMARY KEY(src_hash, input_set));"
# 4. 起一个担当(tmux 窗口 + 独立工作副本)
git worktree add agents/a01 -b agent/a01
tmux new-session -d -s a01 -c agents/a01
# 5. 给担当写方针与约束
cat > shared/BRIEF.md <<'EOF'
目标:降低正式对照集合上的平均手数。
方法:先想清楚「十手解」需要什么结构;在找到遗漏之前优先做结构试验。
一开始输给当前最优版是允许的。
产出:一份可评测的改进版本 + 说明改了什么。
EOF
# 6. 评测一份候选(结果落库,同源码复用)
./ahc eval --src agents/a01/sol.cpp --set dev2 --db eval/results.db
# 7. 晋升:必须在正式对照上胜出且失败为 0
./ahc promote --cand agents/a01/sol.cpp --base shared/best.cpp \
--set off --db eval/results.db --require-zero-fail
# 8. 提交前压缩并校验
./ahc shrink --in shared/best.cpp --out submit/sol.cpp
./ahc eval --src submit/sol.cpp --set dev2 --fixed-clock --diff shared/best.cpp
# 9. 通知
./ahc notify --msg "promoted h278; submit candidate ready"这条链路的要点在于:评测结果按源码哈希缓存,晋升有硬门槛,压缩有等价性校验,所有动作都留痕。跑通之后,把担当数量从 1 加到 20 左右,就是素材中正赛时的形态。
注意事项
- 提交必须由人手动完成,并遵守提交间隔限制(素材中为 30 分钟以上)。素材中 52 次提交全部由人手动执行,每次提交后保存排名表快照,记录暂定分与最大运行时间。
- 晋升线要在测量之前登记,写进判断记录,结果出来之后不得再移动这条线。素材后期允许指挥角色在满足全部已登记条件时自行晋升,36 次晋升中大部分如此,人类只介入了约 6 次涉及移动时间线或增加工作量的版本。
- 不要用暂定排名做取舍。暂定 50 件的单次波动约正负 0.09%,与后期单次改进幅度同量级,在排名表上根本分辨不出来。取舍一律用正式对照。
- 网页模型环境更慢,直接使用只能达到评测机三四分之一的搜索深度,必须把校准过的虚拟时钟一起打包。
- 网页模型返回不可预期,不要让担当等它,也不要把它作为晋升的必要条件。
- 否定结论要带重启条件记录下来。素材把失败结果整理成「否定卡片」,写明在什么条件下可以重新尝试,避免同一个问题被反复提出。
- 判断记录会膨胀到人读不完。素材的判断记录最终约 150 万字、10700 行,因此需要让 AI 定期生成面向人的摘要:每日一份「给人看」的段落、一页纸的判断表、早晨计划、进度五行、一页纸总览、想法库存、会议纪要。这些摘要本身也是新担当的入口材料。
- 赛题形态决定要不要做输入分层。如果所有用例参数一致,分层工具用不上;如果输入第一行就决定问题形态,就必须按形态分担当,最后在入口处判定形态并分派到专用解。
- 价格、配额、模型版本、提交上限等易变信息,请以各平台官网当前公布的信息为准,本文不给出具体数值承诺。
最后回到那条最核心的经验:编排器、评测、虚拟时钟这些基础设施决定了你的下限,而真正决定上限的,是人类能否在 AI 认为理所当然的地方提出一个它没想到的问题。四个关键判断的共同结构都是「怀疑前提,并把怀疑改写成可执行的提问」——这件事目前仍然只能由人来做。