Codex + Seed-2.1-pro 实测:多模态理解 + Coding Agent 能扛住真实仓库吗?

前阵子在改一个拖拽布局的问题------react-grid-layout 的 Issue #2161:开发者给新卡片设 y: Infinity,希望它排到最底下,结果在 horizontal 和 null 两种模式下,新卡片直接跑到了网格左上角,盖住了已有内容。页面上一眼就能看出问题,代码里却要绕好几步。正好看到火山方舟的 Seed-2.1-pro 推了 0915 这一版的升级,就想着把它接进 Codex CLI 试试,看这个真实 Issue 能不能修。


一、模型升级:0915 这一版加了什么

正式接入前,先去方舟的 Seed-2.1-pro 模型详情页看了下 260915 这一版到底升级了什么。

页面上把这一版的方向概括为"Agent 长程执行、可信调研与多模态理解全面增强",具体拆成五块------

  • Agent:多指令串联执行、长程任务不中断、子任务细节不丢失、多轮工具调用的输出反馈更及时;
  • 多模态理解:细化视觉感知,提升细粒度视觉定位能力;
  • 深度调研:新增万字图文报告生成、营销内容创作,增强 PPT 生成;
  • 工具反馈:调整工具描述、入参字段变更时遵循新工具描述,及时反馈工具调用输出结果;
  • Coding:原型设计还原度更高,高保真设计 HTML、前端样式更好看,全栈开发能力升级。

我这次要测的正好踩在其中两块上:模型得先看懂几张页面截图(多模态理解 + 细粒度视觉定位),再读懂仓库源码、改对位置、把测试和构建跑通(Agent 长程执行 + Coding)。

二、接入 Codex CLI

这一步本来我以为要自己研究 OpenAI 兼容协议怎么改,没想到火山方舟官方文档直接给了一份《接入 Codex CLI》的教程,从 npm i -g @openai/codex 装 CLI,到 ~/.codex/config.toml 里加自定义 provider,再到设环境变量,一步一步都写好了。照着做几分钟就能跑通,这点还是很方便的。

配下来核心就是这么一段 TOML:

toml 复制代码
model = "doubao-seed-evolving"
model_provider = "ark"

[model_providers.ark]
name = "Volcano Ark"
base_url = "https://ark.cn-beijing.volces.com/api/plan/v3"
env_key = "ARK_API_KEY"
wire_api = "responses"

API Key 放在环境变量 ARK_API_KEY 里,配置文件不用写明文。配好之后启动 Codex,看到的接入状态就是这样:

三、问题:新加的卡片跑到了左上角

环境通了,开始出题。这道题我原本是想自己抽时间修的,正好拿来当考题。

按 Issue 的描述搭一个最小复现:往 react-grid-layout 的网格里加一个 y: Infinity 的新卡片,分别在 vertical、horizontal、null 三种压缩模式下看它的落点。

vertical 下没问题,新卡片落在已有内容下方。换成 horizontal 或 null,新卡片立刻跑到左上角,盖住了原来的蓝色组件 a。在数据看板里这就是用户刚加的图表把旧图表挡住了。

实验固定在 2.2.4 版修复前的提交:

text 复制代码
12f02fcbe6949b9f206d6457831a9205de927cdb

基线上有个反差很适合拿来考编程智能体:原有 Jest 的 19 个套件在基线上全部通过(538 项通过、8 项跳过),页面上的问题却能稳定复现。已有单测没覆盖到"新增项落点"这种情况,只看测试是绿的,发现不了这个 Bug。

为了判断模型修好没,我搭了两组页面对照:960px 宽、5 个已有项一组,720px 宽、8 个已有项一组;每组在 vertical、horizontal、null 三种模式下各跑一次,共 6 个用例。后面所有"修好了"的判断,都以这 6 例跑出来的坐标为准。

四、Codex 修复

正式跑了两轮完整修复,两轮都把修复落在了 src/core/compactors.ts。先看模型怎么找到的根因,再看它写的补丁。

根因:Infinity 进错了分支

给新卡片设 y: Infinity,意思是"放到最底下"。这个哨兵值在 vertical 分支能被接住,在另外两个分支就直接漏了过去。

src/core/compactors.ts 里,vertical 在压缩非静态项之前会先执行 Math.min(maxY, l.y)。本次场景里已有布局高度是有限值,Infinity 因而被收敛到有限坐标,再参与向上填空和碰撞处理。

horizontal 原先只有 Math.max(l.y, 0) 这样的下界处理,Math.max(Infinity, 0) 仍然是 Infinity;no-compaction 则直接克隆布局,特殊值也保留下来。它随后参与像素位置计算,最终进入 CSS transform,产生含 Infinitypx 的无效定位样式------浏览器解析失败后,元素就贴到了左上角。

页面回调里的 "y": null 也容易误导排查。JSON.stringify(Infinity) 本来就输出 null,这不代表组件内部收到的就是 null。第 1 轮模型自己写的验证脚本就在这里绊过一次,后来才修正。

模型怎么改 compactor

第 1 轮在横向压缩入口先计算有限坐标项的布局底部,再把非静态项的非有限 y 放到这个底部。下面是从该轮 horizontalCompactor.compact() 节选的关键代码:

ts 复制代码
let maxY = bottom(layout.filter(l => Number.isFinite(l.y)));
for (let i = 0; i < sorted.length; i++) {
  const sortedItem = sorted[i];
  if (sortedItem === undefined) continue;
  let l = cloneLayoutItem(sortedItem);
  if (!l.static) {
    if (!Number.isFinite(l.y)) {
      (l as Mutable<LayoutItem>).y = maxY;
    }
    l = compactItemHorizontal(compareWith, l, cols, sorted);
    maxY = Math.max(maxY, l.y + l.h);
    compareWith.push(l);
  }
  const originalIndex = layout.indexOf(sortedItem);
  out[originalIndex] = l;
  l.moved = false;
}

bottom() 取已有项的 y + h 最大值,前面的 filter 把 Infinity 排掉避免污染结果。坐标转成有限值以后,再交给原来的横向压缩逻辑;每处理完一项,就更新 maxY。该轮还单独处理了 no-compaction 分支:在克隆后的布局上解析非静态项的非有限 y,保留原来的 x。加上两个针对性测试,补丁一共新增 55 行、删除 2 行。

第 2 轮也只改了 compactors.ts 和测试文件,但实现位置不同:横向分支把转换放进 compactItemHorizontal(),扫描 fullLayout 里有限项的底部;no-compaction 仍然单独处理。这轮新增 90 行、删除 2 行,运行中还顺手修正了格式问题。两轮解决的是同一个现象,补丁结构并不相同。

修复后的页面

两轮独立复核都通过了 6 个页面用例,frozen checker 跑的、我这边独立重跑的结果一致:

网格宽度 / 已有项 vertical horizontal null
960px / 5 项 (10,2) (0,4) (10,4)
720px / 8 项 (4,6) (0,6) (4,6)

horizontal、null 下的新项都落到了已有内容下方,vertical 保持原来的位置,6 例都没有矩形重叠。

第 1 轮补丁在独立副本上重新构建后的页面。new-0 以完整尺寸落在 d 下方,与修复前压住 a 的覆盖现象形成对照。

它说"全过",其实漏了一次失败

第 1 轮模型最终回复写的是"全部验证通过",但它最后一次跑完整 Jest 的结果是 539 通过、1 失败、8 跳过 。失败的是性能用例 renders 50 items:

text 复制代码
266.8306ms > 250ms 阈值

之后它只单独重跑了 benchmark 这一个子集,20 项全过,最终回复就写成了"全部验证通过"。括号里提了一句超时和重跑,全量汇总里的 1 failed 却没写。

子集通过替代不了那次全量失败。我后来独立全量跑确实是 540 项通过,但这改不了模型当时的记录。几次结果说明计时有波动,没有同等条件下的前后对照,我也不能断定原因是机器负载还是和它的改动有关。

第 2 轮没有漏报,但对改动范围的说明有两处不对:

  • 它说三个 allowOverlap 变体"直接复制、没有改动"。可 noOverlapCompactor 继承了 noCompactor.compact,行为会跟着变。
  • 它说 -Infinity 以前同样会产生非法 CSS。但 horizontal 原有的 Math.max(y, 0) 早就把它转成有限值了。

我用相同输入分别跑了基线和第 2 轮代码,这两处差异都确认存在。模型没把旧行为和改动范围交代准;这些额外变化有没有造成用户可见的回归,这次没测出来。

第 3 轮:看图、复现都跑通了

第 3 轮我没让它直接改代码,先让它逐张打开 6 张修复前截图、描述里面橙色新增项的位置和遮挡。我想看看它的多模态理解到底能做多少事。

模型一次性 Viewed image 了 6 张图,并按 vertical / horizontal / null 三种模式分别给出位置描述。

接着它做了一次真实复现:webpack 构建、起本地 http server,再用 Playwright 在三种模式下点 Add item (y: Infinity) 按钮、读 #layout-state 里 new-0 的坐标,验证补丁后落点是否正确。

模型用 webpack 构建复现页、起 4273 端口 http server,再用 Playwright 分别读取 vertical / horizontal / null 三种模式下 new-0 的坐标。表格里三种模式的 y 都是有限值,horizontal 和 null 都落在已有 5 项布局下方(y=4),不再压回左上角。

跑完这一轮,我又把同一道题和 6 张截图交给豆包桌面端,看看它从看图到出补丁的完整链路------模型家族内部,不同入口的体验是不是一致。

五、同一道题,再交给豆包试一遍

第一步是看图

我没让它直接改代码,先让它逐张描述 6 张截图、汇总成一张表,确认模型对页面问题理解对了再往下走。

豆包逐张打开 6 张修复前截图,按 vertical / horizontal / null 三种模式分别给出 new-0 的位置和遮挡汇总。

它给出的结论和复现页一致:vertical 下 Infinity 被正确解析、组件落在空位、未遮挡任何组件;horizontal 和 null 下橙色细条在左上角压住 a 的左上角,JSON 里 new-2 的 y 始终是 null,但视觉上还是落回了左上角 ------这就是 JSON.stringify(Infinity) === null 的坑。

第二步是真实复现

只让它看历史截图还不够,得让它确认屏幕上真能再画出来。我把仓库扔到本地,让它自己起 http server 和 Playwright 做一次"按下 Add item 按钮 -> 读 #layout-state"的真实复现。

豆包用 webpack 构建复现页、起 http server,再用 Playwright 在 vertical / horizontal / null 三种模式下点 Add item (y: Infinity),读出 #layout-state 里 new-0 的坐标。

它读出的坐标彻底坐实了Bug:

  • vertical:new-0 位于 (835, 372),尺寸 148×130(完整 2×2),与 a 无交集;
  • horizontal / null:new-0 都位于 (33, 222),尺寸只有 148×51 ------ 比 a 的原点 (43, 232) 还偏上、偏左各 10px(贴着容器最左上角),且只渲染成约 1 行高的矮条,压在 a 的左上角,"a" 字样被遮挡。

它随后在回复里把页面症状的因果链讲清楚了:vertical 的压缩流程能把 y: Infinity 解析为合法的最底空位(y=2);horizontal 只压 x 不接管 y,null * rowHeight = 0 让元素被放到最左上角并覆盖 a;null 模式连碰撞检测都跳过,渲染同样失效。

第三步是定位到具体行号

只让它说"改哪个文件"分量不够,得让它说清楚具体是哪个函数、哪一行、为什么。

豆包列出根因结论和证据代码位置,覆盖 GridLayout.tsx、compactors.ts、calculate.ts、position.ts 四处关键文件。

它在回复里把整条因果链拆得很细------

  • 入口 src/react/components/GridLayout.tsx 的 L255-260:新 item 从 data-grid 构造,L258 处 dataGrid?.y ?? 0------Infinity 不是 null,被原样传进 layout;
  • vertical 之所以正常是 src/core/compactors.ts L184/L196 有 verticalCompactor 的 Math.min(maxY, l.y) 收敛;
  • horizontal 之所以坏是 compactItemHorizontal L131-164 的 l.y 没有 Math.min(maxY, y) 收敛、L142-144 向左移动被碰撞检测拦下后 Infinity 被认为是"所有条件下方"、L148-159 的硬累加里 y 直接传出;
  • null 之所以坏是 noCompactor.compact L258-266 只调 cloneLayout、src/core/layout.ts L81-100 的 cloneLayoutItem 逐字段复制,Infinity 原样保留;
  • 渲染层 calcGridItemPosition L126:round((rowHeight+margin)*Infinity + padding) = Infinity,L149-160 的边距修正让 top = NaN,最终 transform: translate(NaNpx, NaNpx) 生成非法 CSS。

最后还补了一条修复方向:"horizontal 需要类似 vertical 的底部实收或 collision / compactItemHorizontal 显式处理 Infinity;同时 calcGridItemPosition / setTransform 应对非有限值做兜底,避免非法 CSS"。

第四步是直接出补丁

根因讲清后让它直接改代码。豆包选择的修改位置和 Codex 正式两轮不同------它只改 src/react/components/GridLayout.tsx,在组件同步 children、从新 child 的 data-grid 建立布局项时,提前处理 y: Infinity。

豆包返回的修复 diff。关键只有一行------把 y: Infinity 在进 layout 之前就映射到已有布局底部。

关键新增语句只有一行:

ts 复制代码
const y = dataGrid.y === Infinity ? bottom(layout) : dataGrid.y ?? 0;

接下来的 layout.push() 使用这个 y,因此经由这条分支创建的新项会先得到一个底部坐标,再进入后面的压缩流程。它把处理位置前移到了组件入口;Codex 正式两轮则改了核心压缩算法。

这个改法也有边界:layout 是按 children 顺序逐步填充的,bottom(layout) 此时只看已经遍历过的项;如果 key 已经命中 initialLayout 会走另一条克隆分支;它也只匹配正 Infinity。本次复现页把新项追加在 children 尾部,别的输入路径这次没覆盖到。

六、回 Codex 验证豆包的补丁

豆包回复里的"538 项通过"最初只有模型自报。我把保存的原补丁原封不动交给 Codex 接续验证。9 月 25 日核对完整 Jest 日志、结果 JSON、页面检查脚本和执行会话后,得到下面这组结果。验证期间没有修改或格式化这份补丁。

检查项 接续验证结果
完整 Jest 19 个套件通过;538 项通过、0 失败、8 跳过,退出码 0
TypeScript 通过,tsc --noEmit 退出码 0
Prettier(GridLayout.tsx) 未通过,退出码 1,报告该文件存在格式问题
页面检查 6/6 通过,新增项坐标有限,均不与已有组件重叠

接续运行的结果汇总,保留了 Jest 计数、TypeScript 结果、Prettier 失败和六例页面结果。

这份补丁没有新增测试,因此这里是 538 项通过;Codex 正式两轮各自补了两个测试,独立全量结果是 540 项通过。

六例页面的新增项坐标和 Codex 两轮跑出来的完全一致:

网格宽度 / 已有项 vertical horizontal null
960px / 5 项 (10,2) (0,4) (10,4)
720px / 8 项 (4,6) (0,6) (4,6)

horizontal、null 下的新增项均落在已有布局下方;vertical 的新增项 x、y 与补丁前采集的基线一致。

豆包原补丁的接续验证页面。橙色 new-0 位于 h 下方,坐标为 (4,6),没有遮挡已有组件。

功能通过、格式失败,是这份原补丁本次交付的实际状态。如果工程上想用,把 GridLayout.tsx 跑一遍 Prettier 就行;不过这毕竟是"模型自报 → 独立复核"链路里捡出来的一条具体缺陷,值得记下来。

七、回顾

前后加起来,这次实际拿到的有效结果是:Codex 两轮正式补丁都改 compactors.ts,独立复核 6/6 页面用例 + 540 Jest 通过;豆包补丁改 GridLayout.tsx,独立复核 6/6 + 538 Jest 通过、Prettier 失败。三条修复路径都通向同一个正确落点。

有几个细节单独列出来------

  • JSON.stringify(Infinity) 输出 null :这让模型一度把自己的验证脚本搞混;第 1 轮它还自报了"全部通过",但原始日志里恰好有一次 1 failed 被这句"全过"盖过去了。
  • 看图链路 :Codex 这边模型一次性 Viewed image 了 6 张截图,逐张给出位置描述;豆包那边看图、复现、根因、diff 四个步骤串得很完整,两边都走得通。
  • 多模态理解 :0915 这版的宣传里有专门一条细粒度视觉定位,从我的体验看不是空话------它确实能看懂"图里橙色新项压住了蓝色 a",并且能把这件事转回代码里具体的 dataGrid.y。
  • 修复落点 :我原以为模型都会去改 compactor,结果豆包改了 React 组件入口 GridLayout.tsx。从源码看这个改法也对,只是边界要交代清楚。
  • 人肉复核:第 1 轮的"全过"少写一次失败、豆包补丁 Prettier 报错,这些都是不看原始日志直接引用模型自报就会放过去的事。

工程上值得抄的几条做法:固定提交 + 隐藏 checker + 独立副本复核;模型自报的测试汇总要回到原始 Jest 输出再核一次;同一份补丁要看 Jest、tsc、Prettier、页面四条线,不能只看一条。

对这次升级的看法

0915 这版的更新说明里,挪到我这道题上能对上的有两条:一是多模态理解 + 细粒度视觉定位 ,看图描述位置、把"橙色压住蓝色"转回 dataGrid.y 这件事,以前的版本处理起来是吃力或者要靠人工喂更多上下文的;二是 Coding 在复杂仓库里找对位置 ------react-grid-layout 这种上千文件的仓库,模型自己翻到 compactors.ts 和 GridLayout.tsx 这两个真正该改的文件,没绕弯子改到别的不相干地方去,这个在 6 月那版我是没把握能稳拿到的。没测到的是更新说明里写的"长程任务不中断"和"跨应用 + Computer Use",第一题没走到那么长,第二题不在我的题目范围里。

整体结论就一句:这版模型已经能扛住一个真实 GitHub Issue 从看图到出补丁到独立复核的完整闭环,中间需要人顶的只有 Prettier 这种机械格式问题和模型自报的口径核对。这就算达到"能上手干活"的门槛了。

往后希望两件事能继续推进------一是把"少报 1 failed"这种自报口径继续收窄,这是把模型用在生产仓库前最后要补的一课;二是把 VLM 看图的能力再焊到工具链里更紧一点,比如让模型直接对着 devtools 截图理解 hover 状态、decode 网格虚线,那些这次它还得靠粗糙的静态截图配合 #layout-state JSON 才能拼出原图。这两步再迈出去,"视觉理解 + Coding Agent"这套能力就可以从偶尔一用变成日常工具了。

不足与边界

跑完这一圈,也得承认几个没做到的地方------

  • 只测了一道题。一个 Issue 的修复链路走通了,不代表模型对所有布局类、算法类 bug 都能打成这样。这次是把"看图 → 定位 → 修复 → 复核"这条链路完整跑了一遍,不是给模型的整体修 bug 能力下结论。
  • 边界没覆盖全 。y: -Infinity、y: NaN、静态项和 Infinity 混排这些输入这次都没测,豆包补丁只匹配正 Infinity 这件事就是这么被剩下来的。
  • 格式线差点被放过去。豆包补丁四条线里 Prettier 没过,这种"能跑但没法直接合入"的状态,只看模型自报是发现不了的。

下一步想做的三件事:

  • 把 y: Infinity 这个用例补成正式单测提回给仓库------Issue #2161 到现在还没有官方修复;
  • 把 Codex 改 compactor、豆包改组件入口这两种思路合起来,跑一遍更全的输入矩阵;
  • Seed-Evolving 下一版出来之后,把这道题原样再跑一轮,看同一入口下的能力变化。

参考资料

相关推荐
IT_陈寒1 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
Thneonl1 小时前
Celery 生产踩坑:1000 任务积压与 acks_late 双重执行
后端·python
卷福同学1 小时前
第一次当面试官有感
后端·面试
苏三说技术1 小时前
为什么越来越多人用 OnlyOffice?
后端
知守观1 小时前
@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
后端·spring
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
小小张说故事1 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python
明月_清风1 小时前
数据进入平台后怎么处理?一文搞懂 ETL
大数据·后端·数据分析
初学AI的小高1 小时前
LangGraph断点恢复与幂等执行实战
后端·架构