导出还能用,为什么下一步却坏了?
团队把一段旧 CSV 导出代码改得更简洁。文件能打开,记录一条没少,随手跑个测试也通过了。但新版顺手给行排了序,而下游流程一直依赖原来的输入顺序。代码重构悄悄改掉了一个没人写清楚的约定。
让 AI 改造旧代码之前,先说清楚哪些结果不能变,以及另一个人怎样核对。Anthropic 在 2026 年 9 月 23 日的代码现代化文章中,把目标行为、证明改动符合目标的证据、进入生产环境的规则分开考虑。小团队同样可以用到这个思路。
下面是我们独立设计的练习:重构一个本地 CSV 序列化函数,同时保持约定行为。这只是迁移前兼容性核对的一次小演练,不是完整的语言或架构迁移。你会得到一份新实现、一组可重复检查,以及一份说明已验证范围和缺口的审查记录。所有记录都是虚构教学数据;参考代码是为练习编写的示例,其结果不代表某次 AI 运行已经成功。
先选一块能说清边界的代码
下载代码迁移练习,解压到一个新文件夹。运行检查需要 Node.js 20 或以上版本,无需安装依赖,也不用账号或联网。若在 Onevium 中让 AI 参与,还需先配置可用的模型连接。
通过 添加项目 → 打开已有目录 打开这个文件夹,再从项目行创建会话。发送改代码的任务前,先读 behavior.md。
| 文件 | 用途 |
|---|---|
legacy.mjs | 冻结的旧实现,作为被替换的对象 |
candidate.mjs | 起始副本;只在这个实现文件中修改 |
behavior.md | 已约定的对外行为,以及明确不覆盖的情况 |
cases.json | 九组虚构输入和独立写好的预期结果 |
verify.mjs | 将新旧实现分别与预期比较,并检查它们有没有改动输入 |
examples/ | 参考重构和故意写错的版本,用来检查验证器是否有效 |
本练习的函数接收一组 {id, name} 记录,返回以 id,name 为表头的 CSV 文本,使用 LF 换行,并保留输入顺序。没有记录时,返回表头加一个换行。这只是序列化函数的约定;产品界面是否允许下载空文件,可以另行决定。
先确认正确结果,再动代码
下面这组输入专门用来发现“顺手排序”的问题:
[
{"id": "B02", "name": "Bea"},
{"id": "A01", "name": "Ada"}
]
预期输出必须是:
id,name
B02,Bea
A01,Ada
Ada 后面还有一个 LF 换行。即使两条记录都在,顺序变了也不符合本次约定。如果排序确实是新需求,就把它作为单独的行为变更讨论,不要夹在重构里一起改。
其余用例覆盖:空输入、逗号与引号、名称里的换行、中文等 Unicode 文本、空名称、重复 ID、缺失名称、非字符串 ID。字段必须是字符串;无效记录应抛出 TypeError,错误消息与 behavior.md 中的约定完全一致。函数还不能修改传入的数据。这些是为了让练习可核对而选定的规则,你的实际产品可以有不同的错误约定。
请自己读一遍预期值。它们是预先写好的教学答案,不是在测试运行时拿旧函数临时算出来的。旧实现也可能有错。如果它与已确认的规则不一致,先请负责人判断规则,再开始改造。不能为了让新代码通过,就直接改掉预期答案。
在 Onevium 中交付一个范围明确的改动
先在项目终端运行起点检查:
node verify.mjs
刚解压的练习应显示 9/9 cases passed。候选文件起初是旧实现的副本,因此这只是确认基线可运行,并不代表 AI 已经带来改进。
在输入框用 @ 选择相关项目文件,然后发送:
重构 candidate.mjs:把 CSV 字段转义提取为有名称的辅助函数,
让记录转换过程更容易阅读。
先读 behavior.md、legacy.mjs、cases.json、verify.mjs。
自己尝试完成前,不读 examples/。
保留 behavior.md 要求的导出函数签名、返回文本、
错误类型与消息,以及不修改输入的约定。
只修改 candidate.mjs,并新建 review.md。
不要改 legacy.mjs、cases.json、behavior.md 或 verify.mjs。
不添加依赖,不顺手修改产品需求。
运行 node verify.mjs。在 review.md 中写明执行命令、
实际输出、是否改变行为,以及尚未覆盖的情况。
如果规则与旧行为冲突,停下说明冲突。
不要提交、推送或部署。
有用的交付是一份能核对的小范围改动和你能重新运行的检查。用 Onevium 的文件浏览分别打开新旧文件进行比较,确认冻结的输入与验证器没有被改动,读一遍候选实现,再自己运行一次命令。解压后的练习不是 Git 仓库;已有 Git 项目可以用 Review 看差异,这里若已安装 Git,可运行 git diff --no-index -- legacy.mjs candidate.mjs 比较两个文件。这个比较命令退出码为 1 只表示文件不同,不是验证器失败。
证明检查能拦住一个看起来合理的错误
接着运行下载包里故意排序的错误版本:
node verify.mjs --candidate=examples/sorted.mjs
预期显示 8/9 cases passed,input-order 用例失败,退出码为 1。记录都在,但行顺序变了。这是刻意设计的失败,不是安装问题。
再试第二种错误:
node verify.mjs --candidate=examples/mutates-input.mjs
预期显示 1/9 cases passed,退出码为 1。它会修改每组非空输入的第一条记录,即使返回的文本或错误看起来正确,只检查输出也会漏掉这种副作用。
作为对照,为练习编写的参考重构应通过全部九组用例:
node verify.mjs --candidate=examples/reference.mjs
这些命令不会覆盖你写的候选文件。验证器先将旧实现与固定预期比较,再将选定的新实现与同一组预期比较,同时核对新旧观察结果,并检查每次调用后的输入,包括抛出错误的调用。两个错误用例还会检查是否真的抛出 TypeError,不只相信对象的 name 字段。两份代码即使犯了相同的错,也不能因此通过独立预期值的检查。
九组用例通过,只能证明这些断言。它们没有覆盖所有 CSV 使用方、真实数据、性能边界或异常输入。CSV 引号转义也不能防止电子表格执行公式;本练习不处理公式,若实际导出包含不可信内容并会被电子表格打开,需要另行确定规则。
真实任务有独立工作时,再安排队伍
这个小练习用一个会话就足够。较大的迁移可以让一人梳理调用方和旧行为、一人改一个模块、一人按约定核对。验收答案不能仅由实现者给出的解释决定。
在 Onevium 中使用 @ → 队伍(Team) 前,先按队伍文档确认 team Skill 和 Sessions 工具。让负责人先提出文件分工和依赖关系;确认具体方案后再启动成员,后续阶段也按 Team 工作规约确认。
| 角色 | 输入与产物 | 何时进行 |
|---|---|---|
| 摸底成员 | 读调用方与旧代码,写 review/behavior-map.md,标明代码位置和待确认规则 | 负责人确认行为约定前 |
| 实现成员 | 使用已确认的约定,只修改分配给自己的模块 | 约定固定后 |
| 验证成员 | 读约定和候选实现,写 review/compatibility.md,记录命令、结果与缺口 | 实现后验证,也可提前准备独立用例 |
| 负责人 | 汇总报告、处理依赖、核对最终差异、整理待决定事项 | 每次交接时 |
共享文件只指定一名写入者。成员通过负责人汇总结果,不会自动共享全部上下文,也不会天然获得独立文件系统。“已报告完成”还需要核对。只有职责和交接清楚时,增加会话才有帮助。
把测试输出整理成别人能审查的决定
最后在 review.md 中留下这份简短记录:
范围:只替换 CSV 序列化实现。
约定:本次审阅版本的 behavior.md。
证据:确切命令、代码版本、保存的实际输出。
差异:逐项列出已同意或未解决的变化。
缺口:尚未测试的使用方、数据情况与运行限制。
结论:兼容性练习完成 / 仍需处理。
发布:还需要负责人确认和上线检查。
放到实际迁移中,再围绕真实风险补证据:经授权的历史输入、下游使用方、已存储数据、错误路径。敏感数据要留在获准环境中,同时考虑配置的模型服务商会接收哪些内容。若数据格式改变,还要单独测试新旧版本的读写兼容,CSV 输出成功不能证明数据库兼容。
先让一个小模块走完整条流程,再扩大范围。记录真正花在实现、核对、重试和人工审查上的时间,不预先承诺提效。提前约定由谁批准、如何观察小范围上线、失败后怎样恢复。如果新数据无法被旧版读取,“回滚代码”就不是完整的恢复方案。
下一步很具体:挑一个已有函数,写下一组必须保持的输出和一个错误场景,先证明故意写错的替代实现会被拦住,再让 AI 提出改动。