记录一次并发数的自检(2026-09-08)

免费的东西,真正贵的是它的上限

这天的活是一整串:先把 WorkBuddy 的模型档位摸一遍,给 wb.py 定一个免费的默认模型,再回头给这个免费模型补上并发自检。表面上看都是配置,实际上是在回答同一个问题——免费的东西,边界在哪里,边界被撞到的时候,谁该说话。

前面那半段是"选哪个",后面那半段是"选完之后,用超了怎么办"。我先做的其实是后面那半段,因为它是真正会让我半夜爬起来的那种问题。


3 流是那根线

先把线找出来。之前核实 WorkBuddy 免费模型的时候已经确认过,hy4-preview 系列是有一个并发上限的:

  • hy3 —— 项目默认的免费模型,0 credit 成本,没有节流(no throttling)
  • hy4-preview 系列 —— 当前促销免费,并发上限约 3 流:3 流以内正常运行,4 流及以上开始排队,5 流及以上触发超时

这组数字就是自检的阈值来源。核实过程里还有两条写得很死的边界:hy4-preview 家族的 stream idle timeout 固定是 5 分钟;而产品定价文档里**只把 hy3 列为 0 credit 成本,并不列 hy4-preview**。也就是说,"免费"这件事本身是有时效的,它是个促销状态,不能当成常量写进脑子里。

真正的问题是:这根线只在我的记忆里。 哪天我一口气起五个后台任务,看到的是它们一个个卡住,而 wb.py 什么都不说。所以这天的核心动作很具体——把"已经跑了几个 hy4"这件事,从记忆里搬进工具里。

把自检写进 wb.py

实现不复杂,但有几个地方必须克制。我在 wb.py 里加了三个辅助函数:

  • _is_running —— 判断一条任务记录是不是"正在跑"
  • _hy4_concurrency_stats —— 统计当前并发,算出指标
  • _warn_hy4_concurrency —— 到阈值时打警告

然后把 _warn_hy4_concurrency 插进 cmd_run,紧跟在已有的 _warn_paid_model 之后。这里定了一条原则:不动原有警告流的任何一行,新警告只负责"加在后面"。付费模型警告是提醒"你选了要花钱的模型",并发警告是提醒"你可能要排队了",两件事互不干扰,谁也别去改谁。

再留一个退出口:--allow-hy4-concurrency。当明确知道自己在干什么、就是要多起几个任务的时候,可以显式把这条警告关掉。默认开启、可显式关闭,比反过来安全。

cmd_list 也一起改了。--json 输出里新增了 summary 字段:

1
2
3
4
running_total      // 正在跑的任务总数
running_hy4_count // 其中 hy4 的个数
running_models // 正在跑的模型清单
concurrency_warn // 是否触发并发警告

另外每条记录都带上它自己的 model。人类可读的列表输出里,如果 concurrency_warn 为真,就补一行 stderr 提示——不用看 JSON 也能一眼知道。

一条差点写错的边界

验证的时候,我特意盯着一个地方:**ok / failed / killed 这些"已结束"的记录,绝对不能算进并发。**

一开始很容易顺手写成"有几条 hy4 记录就算几个",但那样会把历史记录也数进去,警告会疯掉——明明没有任务在跑,屏幕上却一直喊"超了"。正确的口径是只看"正在跑"的。这条后来被直接写进了经验:计数只数在跑的,别数跑完的。

端到端验证用测试用的 meta.json 覆盖了四种情形:

正在跑的 hy4 数 预期行为
0 / 1 / 2 不警告
3 警告触发
3 + --allow-hy4-concurrency 警告被抑制
含 ok / failed / killed 记录 不计入计数

四种都对上了。SKILL.md 里也补了一节 **"Auto-detection of hy4-preview fanout"**。

顺带把 MODEL_COST 里的 hy4-* 条目也改成"免费 + 并发限制",并带上合适的并发限制说明。警告要么准,要么别响——一个免费模型要是被软警告当成付费的,那是另一种误报。

顺带把模型档位摸清了

自检只是这天的一半。因为要给"并发满了"准备一个退路,我把整套模型档位又过了一遍。

摸的方式是先克制地取证,不靠印象:从官方产品配置文件里查模型的定价与可用性;再针对文档里没有列出的模型,去核实它真实的计费状态,确认是不是正处在促销免费期——不能只信"官方列了哪些免费模型"。证据够了,再跑两组测试:一组是把同一份端到端冒烟负载打在所有候选模型上,量性能和成本;另一组是并行突发测试,专门去撞候选免费模型的并发上限和超时阈值。

结论落到一个免费默认上,并且用三层来兜:

  1. 代码里硬编码 WB_DEFAULT_MODEL,指向免费的 hy3
  2. 用 setx 把 WB_DEFAULT_MODEL 写进 HKCU\Environment,让新开的 shell 也认
  3. 加一个"软警告"——只有明确选了已知付费模型时,才往 stderr 打一行字,不阻塞

解析顺序固定成:**--model CLI 参数 > WB_DEFAULT_MODEL 环境变量 > 硬编码默认值**。做法是先让环境变量作为 argparse 的默认值,再让显式 CLI 参数永远压过它。

警告这块验证得很细:选付费模型,出警告;选免费的 hy3,不出;选一个未知模型,也不出——因为对未来还没出现的模型不能瞎报警,宁可漏,不可误伤。

验证用例覆盖了所有选型情形:CLI 覆盖、环境变量生效、默认使用、未知模型;同时确认了与 codebuddy CLI 的 --model 参数兼容。测试产物都落进了 wb_runs/ 目录留档。

最后落成一张选型表,写进 SKILL.md 的模型选型推荐矩阵。核心两行是:

模型 定位 说明
hy3 项目默认 完全免费,0 credit 成本,无节流
hy4-preview 系列 促销免费 并发上限约 3 流,4 流起排队,5 流起超时

并且定下项目约定:委托编码任务默认用 hy3,除非明确要求更强的模型。 同时,超过并发上限的并行后台任务不要用 hy4 系列,避免排队和超时——因为并发上限是硬约束,不是靠运气绕过去的。

没清干净的尾巴

清点的时候顺手发现一个没处理的问题:wb_runs/ 里还躺着一批残留的闲置任务记录(stale runs),是更早几天生成的。它们不影响功能,但会让并发的统计口径变脏。

当天的处理是"先记下来":考虑给 wb.py 加一个 gc 子命令,自动清理这些残留。这天没做,但挂进了待办。

同一天还有一条做了的事:SESSION_LOG_11 追加进了 craw_app1_wz 目录,把 wb.py 这一串配置决策的正式日志按 append-only 的方式存下来,供 Codex 侧加载参考。改代码的同时留一条决策记录,比事后翻 diff 便宜。


最后

这天的收获分三层。

最表面的是那三个函数和一条警告——一个能用的并发自检,从"我心里有数"变成"工具会说话"。

中间一层是那组阈值:3 流、5 分钟。这些数字不写下来,下次还是会眼睁睁看着任务排队却不知道为什么。免费模型的成本不在钱上,在它的上限上——钱花光了你会心疼,并发超了它只是静静地卡住,更难受。

最里面一层,是"把默认值钉死在免费档"这个决定本身。

一个工具默认用什么模型,看起来是配置细节,但它决定了在你不注意的时候,钱和并发是被谁消耗的。默认值就是"无意识状态下的行为",所以它必须是保守的:先用免费的,要更强的能力必须显式说出来,而且说出来的那一刻要有警告。把兜底放进默认值里,比指望自己每次都记得加参数,可靠得多。

这天做的其实不是新增功能,而是给一个已经免费的东西,补上它缺的那句提醒。

那天之后,wb.py 的默认模型、警告、并发自检,三样东西是绑在一起变的——因为它们的答案本来就是同一个:你要用掉多少资源,工具得先告诉你。