V17 这轮迭代,最让我印象深的事情不是某个参数终于调对了,而是我把一个公开策略拆开看了好几层,才发现它里面实际运行的核心比表面看起来旧。
刚开始看 Kaggle 上的分数时,心情也挺复杂。V17 在一个中午的存档里只有 2254.7,V16 是 2564.3;到了当天晚上,V17 的一张快照变成了 2561.8,V16 则是 2528.3。数字变化很快,看起来像是策略突然变强了,但这种在线评分会随着对手和对局变化,不能单凭这几张快照证明代码改动带来了多少提升。
所以我后来把注意力拉回到本地实验:V17 到底换了什么,结果又是在什么条件下测出来的。
V17 是什么
Kaggriculture 是一个农场经营类 Agent 对战环境。每一回合,Agent 根据观测提交工人动作和市场订单;作物、库存、工人路线和出售时机都会影响最后的收益。一个完整对局有 720 个状态,光把策略包跑完,不代表策略就有效。还得确认动作合法、库存账对得上,并且结果能在相同条件下复现。
V17 最终提交包的策略组合可以概括成四部分:
- KoshinM 的七层公开调度包装。
- 完整的 Metav4 v13 核心。
- 有条件保护的三单位小麦开局。
- 本地实现的卖单顺序分配器。
这里有个容易误会的地方:最终 V17 的收益不能全部算到其中某一个改动上。冻结对照里的上一版 V17 已经包含 Metav4 核心和同一套小麦开局,因此最终对比衡量的是新增调度与市场分配组合带来的整体变化。
七层包装里的核心版本,得往里找
KoshinM 的公开策略使用了多层嵌入包装。代码一层套一层,里面的 BASENS 还保存着编码后的源码。只看 notebook 的标题和更新时间,很容易以为最深处的执行核心也是最新版本。
我把源码逐层解码,再做 AST 对比,发现被包装的旧核心缺少 Metav4 v13 中的 27 个函数,包括后续的市场协调和克隆对手处理逻辑。外层策略看着挺新,实际调用到的核心却少了一截。
V17 的核心替换过程做得比较克制:
- 递归解析七层
BASENS,逐层还原嵌入源码。 - 确认底层旧核心与已存档源码逐字节一致。
- 只替换最底层核心,再按原结构重新编码。
- 保留外层调度器和配置,并记录每层替换前后的 SHA256。
这样改完之后,七层包装还在,核心才换成完整 Metav4 v13。每一层的哈希和来源都记在 core-replacement.json 里,原文件也没有被覆盖。
小麦开局:先改路线,还得确认路线真能走
V17 继续保留了三单位闲置工人的小麦开局。这段策略沿用了 Dmitrii Gluzdov 的公开思路:调整早期工人路线,让第三天收获时拿到三单位小麦,再把它卖出去。
代码做的是对现有路线表的定点修改,包含播种、浇水、收获、恢复牧场和出售等步骤。它会先检查标准棋盘配置和路线上的预期动作;只有原路线对应位置仍符合预期,才安装改动。比如某个位置已经不是计划中的 PASS,安装就会停止。市场订单也有上限检查。
这些保护听起来有点啰嗦,实际很有必要。路线表被改过之后,如果动作顺序和预期对不上,Agent 可能继续按错误假设卖货或者占用工人。我更愿意让这段优化在条件不满足时跳过,也不想让它悄悄接管一局状态不一样的游戏。
另一个细节是出售量从当时的实际仓库库存读取。代码会确认工人到了预期位置、动作确实是 DROP,再把收获的小麦并入卖单。这让“路线计划会收多少”与“实际有多少货”保持分开,避免库存变化后按静态计划报出错误数量。
这段路线来自公开策略,本地做的工作主要是把它安全地接进版本,并验证每一步的实际收获、牧场恢复和出售行为。公开来源包括 Dmitrii Gluzdov 的 One More Wheat。
卖单顺序为什么值得单独优化
市场订单不只是“把仓库里的东西卖掉”。同一回合里,商品的出售顺序会影响价格曲线上的位置;对手也在提交卖单。数量相同,先后顺序不同,双方可能拿到不同的收益。
V17 的本地市场层只处理一个很窄的情形:当前回合至少有两种商品要卖,而且市场订单全是 SELL。如果混入买单、同一种商品出现多个待处理订单、库存无法确认,或者回合不在指定时间窗口内,代码就保留父策略原来的动作。优化器遇到异常也会回退到原动作。
对每个商品,代码根据当前市场库存和官方价格函数计算一段价格曲线。然后建立两种对手情景:
- 镜像情景:假设对手商品的出售顺序大致跟随当前订单位置。
- 抢先情景:假设对手优先卖出价格影响较大的商品。
两种情景的权重分别是 65% 和 35%。对每种商品、每个可用订单位置,计算一个预期收益差矩阵。每个格子表示:把商品 i 放到位置 s,预计给我方带来的收益减去对手收益是多少。
随后,优化器用子集动态规划找订单排列:
DP[mask ∪ {i}] = max(DP[mask ∪ {i}], DP[mask] + U[i, |mask|])
这里 mask 记录已经安排好的商品,|mask| 就是下一个订单位置。对每个状态,把还没安排的商品放到下一个槽位,保留当前分数最高的排列。商品数量通常不大,这种方法能在很小的搜索空间里比较完整地检查排列,而不用手写一堆商品优先级规则。
求出最优排列之后还要过两道门:
- 加权预测收益必须比原顺序高出至少 1。
- 镜像和抢先两种情景都不能比原顺序更差。
通过后,代码只重排卖单,商品和数量保持不变;没有实际商品的空订单可以顺手移除。测试统计里,市场层评估了 1753 个符合条件的回合,实际改序 139 次。它没有试图在每个回合都“聪明一下”,而是只在模型明确认为更好的时候动手。
这个模型仍然是简化的。它只用两个显式对手情景估算当回合的顺序收益,不能预测所有对手行为;最终胜率提升也不能单独归因于市场层。相关实现和方法记录见项目中的 research/v17round4/harness/marketoptimizer.py 与 research/v17_round4/METHODS.md。
中间几轮没有一次走通
V17 的研发过程有几次没过线,我把它们留在了记录里。
第二轮候选没有接回小麦开局,对上一版 V17 的结果出现回退。第三轮把开局接回来以后,候选在新世界上已经明显超过 V16,但对上一版 V17 的 96 局仍以 77 胜对 79 胜落后,没有达到预设标准。
后面做源码审计时,才定位到嵌入核心的问题。开发筛选一共 36 局,覆盖 4 个开发 seed、3 个对手和 3 个候选。只加本地市场求解器的候选是 4 胜 8 负;上一版 V17 也是 4 胜 8 负;更新核心并保留公开调度的组合则是 9 胜 3 负。
还有一次对手去重审计。名为 timing 的公共 notebook,在那份下载源码里实际打包的是 HybridOpening。它与 Pipe16 在第二轮的 96 个配对案例中,双方奖励、己方动作、物理动作和市场轨迹完全一致。这个结果只能说明这批案例里表现相同,不能证明两个策略在任意世界都等价。最终评测保留 Pipe16,并用 One More Wheat 替换了重复的 timing。
这一步对最终结果很重要:如果对手池里重复放进两个行为相同的策略,表面上的样本数会变大,实际覆盖的行为却没有增加。
最终对照:288 局,按 seed 成组看
最终候选冻结之后,评测使用 24 个全新随机世界、4 个实时响应的公开对手,比较 V16、上一版 V17 和本轮最终 V17。每个版本打 96 局,总计 288 局;席位按 seed 平衡,每个席位各覆盖 12 个世界。
胜计 1 分,平局计 0.5 分。结果如下:
| 版本 | 胜 / 平 / 负 | 平均胜分 | 平均金币分差 | |---|---:|---:|---:| | 已上传 V16 | 18 / 24 / 54 | 31.25% | -297.12 | | 上一版 V17 | 57 / 24 / 15 | 71.88% | +269.78 | | 最终 V17 | 82 / 0 / 14 | 85.42% | +695.15 |
在评测开始前,预设的晋级标准是:候选对 V16 和上一版 V17 的平均胜分都至少增加 5 个百分点,平均金币分差增量都为正,完整跑完对局,而且候选不能报告内部错误。最终 V17 按这套标准通过。
与 V16 配对时,平均胜分提高 54.17 个百分点,平均金币分差提高 992.27。按 24 个 seed 整组 bootstrap 得到的 95% 区间分别是 [43.75, 64.58] 个百分点和 [750.95, 1228.67] 金币。64 个配对结果改善、没有配对结果变差,也没有 V16 的胜局转成 V17 的败局;最差一局的金币分差仍回退了 941。
与上一版 V17 对比,平均胜分提高 13.54 个百分点,平均金币分差提高 425.36。对应区间是 [3.12, 22.92] 个百分点和 [203.50, 676.50] 金币。这里有 30 个结果改善、8 个变差,还有 5 个上一版的胜局转成最终版的败局。总体结果通过预设门槛,但单局退化没有消失;这也是我不愿意只贴总胜率的原因。
bootstrap 是按 seed 整组重采样的,一个 seed 下的四个对手案例一起移动。统计单位因此是 24 个初始世界,不是 96 个彼此独立的世界。四个对手也来自有共同谱系的公开策略,并不代表排行榜上所有私有 Agent。
工程验证和提交
最终候选的 288 局全部有效,候选没有报告内部错误;96 局都检查到了预期的小麦收获、牧场恢复和出售证据。市场收益模型还用官方逐单位结算核对了 360 个场景。
最终归档跑过双席完整对局。关闭本地新增层后,有 1438 次决策与对应父策略行为一致;官方执行物理动作后的真实仓库检查也确认,重排前后的商品数量没有变化。归档放进空目录后,官方文件入口加载、完整运行和奖励复现都通过。10 项测试全部通过。
提交包只包含一个 main.py,大小 840,548 字节,并做了确定性重建检查。记录的源码 SHA256 是:
c5393cc9f47e5e3bda677b9a16ad1d6e010cf80602c243e5f5460a62e57fbc5d
归档 SHA256 是:
a29bd3e9e7ef254233151d94df49f7f8cac86051c08bf4859056ac4cf0a242ec
本地回调的最大记录延迟是 327 毫秒;这只是本地机器上的数据,不能直接当作 Kaggle 运行时延迟。
研究结果文件记录的是冻结当时的状态。随后,固定归档于北京时间 2026 年 9 月 21 日 09:46 提交 Kaggle,提交编号 56410720;官方验证对局 111441850 完成,状态为 COMPLETE,没有错误。
本地提升和线上分数要分开读
V17 的本地结果来自固定对手池、固定引擎版本和受控 seed;Kaggle 的在线评分则来自另一批实际对局。两类数据可以互相提供线索,不能互相换算。
项目里保存的线上快照显示,2026 年 9 月 21 日 12:25 左右,V17 是 2254.7,V16 是 2564.3;当晚 22:01 左右,快照里 V17 是 2561.8,V16 是 2528.3。评分在变化,对局对象也在变化。这些数字适合用来观察当时的线上走势,不能证明某个本地改动独立贡献了多少分。
V17 后来由 V18 接替,项目随后继续迭代到了 V20。回头看,V17 这轮最有价值的部分,是它让我把“外层看起来是什么版本”与“实际调用的核心源码是什么版本”分开检查,也让我开始把对手重复、seed 独立性和归档哈希当作实验的一部分。
项目里的 V17 结果、来源说明、冻结清单和复现命令分别保存在 research/v17_round4/RESULTS.md、METHODS.md、FREEZE.json 和 REPRODUCE.md。V17 的归档是 dist/v17-improved-c5393cc9f47e.tar.gz。仓库根目录的 main.py 是历史 V8,复现或检查 V17 时应使用固定归档,不能从根目录重新打包。
公开来源与致谢
V17 保留并组合了公开策略中的工作:
- KoshinM Local Best:七层公开包装与调度。
- Thomas Tschinkel Metav4 v13:完整核心。
- Dmitrii Gluzdov One More Wheat:三单位小麦开局。
本地工作集中在版本组合、市场订单分配器、保护逻辑和对照验证。生产路线和已有公开调度器都保留原作者归属。