让 AI agent 拆分姓名列:九次运行,一次删了列

“把 users 的 name 字段拆成 first_name 和 last_name”听起来像个十分钟的工单。但它也是一次 schema 变更,而其他系统依赖这个 schema。我们在 Claude Code 里针对一个小型 SQLite 服务跑了九次,再把每个结果放到一个带有各种棘手真实姓名的类生产数据库上运行。

无仓库规则3 次中有 1 次删掉了 users.name
README 中有规则3 次全部通过 8 项检查
规格3 次全部通过,并按团队要求的方式拆分姓名

实验设置

仓库

一个基于 node:sqlite 的账户服务,带一个很小的迁移执行器、两个已应用的迁移,以及 createUser、renameUser、getUser 和 exportCsv。三个测试通过。

README 写了三条规则:已应用的迁移永远不能修改;有一个独立的计费同步直接从数据库读取 users.name;CRM 导入 exportCsv 的输出,并按其表头精确匹配。

三个变体,每个跑三次

  • 无规则(N1 到 N3):模糊提示词,移除 README 规则和 CRM 注释。
  • 有规则(V1 到 V3):模糊提示词,保留 README 规则。
  • 规格(S1 到 S3):规格提示词,保留 README 规则。

Claude Code 2.1.284,claude-opus-5-5,headless 模式,每次运行使用全新副本,每次耗时 42 到 89 秒。录制于 2026 年 9 月 28 日。运行包。

两个提示词

模糊版(N 和 V 运行),原文

Split the users' name field into first_name and last_name.

规格版(S 运行),节选

Goal: expand step of expand/contract. Dropping name
is a later, separate change.

Non-goals
- Do not drop, rename or stop writing users.name.
- Do not change exportCsv output or its header.

Split rule
- Collapse whitespace, trim. One token → first_name,
  last_name ''. Otherwise last token → last_name.
- NULL name → both new columns NULL.

AC-1 New migrations/003_*.sql works on a DB with rows.
AC-3 createUser({ email, name }) keeps working and
     fills all three columns; renameUser keeps them in sync.

隐藏测试套件记分卡

测试套件按生产环境的样子构建数据库:先应用迁移 001 和 002,再写入七个已有用户,其中包括 Cher、Mary Ann Smith、Smith, Jr.、一个 NULL 姓名和一个带多余空白的姓名。然后运行每个构建的新迁移,检查哪些东西保留了下来。它不评判姓名怎么拆分,只检查没有丢失任何内容。

检查项无规则有规则规格
D1 新迁移能在已有数据的数据库上执行3/33/33/3
D2 已应用的迁移 001 和 002 没有被修改3/33/33/3
D3 回填没有丢失任何姓名中的任何字符3/33/33/3
D4 users.name 仍然存在且未被改动,供计费同步使用2/33/33/3
D5 已有用户的 CSV 导出逐字节不变2/33/33/3
D6 再次运行 migrate 不产生任何变化3/33/33/3
D7 现有的 createUser({ email, name }) 调用方仍能正常工作2/33/33/3
D8 renameUser 让所有姓名列保持同步2/33/33/3

测试框架说明:第一轮打分时,我们用每个构建自己的 exportCsv 计算“变更前”的 CSV,结果在 N3 上崩溃,导致 D1 到 D8 因错误的原因失败。我们修正了测试框架,改用原始函数,并基于保存的输出对全部九次运行重新打分;没有重跑任何 agent。两个版本的测试套件都在运行包里。

运行 N3:一次干净的迁移,却搞坏了计费

-- migrations/003_split_users_name.sql (run N3)
ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name TEXT;

UPDATE users SET
  first_name = CASE WHEN instr(trim(name), ' ') > 0
    THEN substr(trim(name), 1, instr(trim(name), ' ') - 1)
    ELSE nullif(trim(name), '') END,
  last_name = CASE WHEN instr(trim(name), ' ') > 0
    THEN trim(substr(trim(name), instr(trim(name), ' ') + 1)) END
WHERE name IS NOT NULL;

ALTER TABLE users DROP COLUMN name;

回填是正确的,没有丢失任何内容。但最后一行删掉了计费同步要读的列,而且 createUser 和 renameUser 改成了 { firstName, lastName },所以所有现有调用方也都会出错。这个 agent 并不粗心。它的最终消息是这样说的:

It then drops the name column. This can't be undone by re-running migrations.
If anything outside this code reads users.name directly from the database,
remove the DROP COLUMN line and drop the column later.

这条警告本身就说明了为什么要把约束写下来。agent 知道可能存在某个读取方,却无法知道它确实存在。N1 和 N2 用的是同样的提示词,同样缺少 README 规则,却保留了这一列。只扫一眼总结的评审者会拿到一个能通过仓库里所有测试的迁移,而它会在第二天晚上的生产环境里出问题。

没人要求的决策

通过让同样四个姓名经过每个构建的迁移来测量。

输入姓名全部六次非规格运行S1 到 S3
Mary Ann SmithMary / Ann SmithMary Ann / Smith
Ludwig van BeethovenLudwig / van BeethovenLudwig van / Beethoven
CherCher / NULLCher / ''
Smith, Jr.Smith, / Jr.Smith, / Jr.

结果一致,但仍是一种选择

每次非规格运行都在第一个空格处拆分。两种规则都不可能适用于所有姓名;关键在于其中一种是模型选的,六次运行六次都是如此,而且它会成为所有下游系统里的数据。

两种规则都处理不了 “Smith, Jr.”

没有一次运行处理了后缀,规格也没有要求。如果你的数据里有后缀,那就该在规格里写一行,而不是指望 agent 自己注意到。

无论哪种方式测试都能通过

每次运行都用插入的行测试了自己的回填,每次运行自己的测试都通过了,包括 N3。规格运行新增了 6 到 8 个测试,其他运行新增 1 到 3 个。这些测试都看不到计费同步,因为仓库里没有任何东西能看到它。

九次运行能说明什么

写明谁在读

README 里的三行(“计费读取 users.name”“CRM 按 CSV 表头匹配”“永远不要修改已应用的迁移”)让模糊提示词从 3 次中 1 次危险变成 3 次全部安全。大多数 schema 故障来自仓库之外的读取方。

说清“只做 expand”

让 agent “拆分”一个列,它常常会把活干完,顺手删掉旧列。如果这次变更是 expand/contract,要说明这是哪一步。

在数据上测试,而不是在空 schema 上

一个在全新数据库上通过的迁移说明不了多少。真正重要的检查是先在旧 schema 里写入形态真实的数据行。

这次测试的局限

  • 每个变体三次运行只能说明可能发生什么,不能说明发生的频率。N3 只是一次运行。
  • README 规则简短且显眼。在大型仓库里,同样一句话可能放在 agent 从来不会打开的文档里。
  • 规则、规格和隐藏测试套件都是我们写的。测试套件的检查项在第一次运行前就已确定,且从未展示给 agent;上面提到的测试框架修正改变的是基线的计算方式,而不是检查的内容。
  • 只有一个模型和一个工具。运行包里包含重复这个实验所需的全部内容。

同一个案例在 Sonnet 5.5、Haiku 4.5 和 Fable 5.1 上

没有书面规则时,Sonnet 5.5 在 3 次运行中 3 次都删掉了 users.name。即使 README 里写了规则,Haiku 4.5 仍然 3 次中 3 次删掉了它。

查看跨模型结果

相关内容

数据库 schema 规格包

为可评审的 schema 变更准备的迁移计划、回填检查和回滚方案。

查看数据库案例

API 错误格式运行记录

九次运行,一份缺失的文档决定了移动端应用会不会出问题。

查看 API 运行记录

结账优惠券运行记录

六次运行,模糊提示词能用,但每次选的业务规则都不一样。

查看优惠券运行记录

在 agent 写迁移之前先写规格

数据库规格生成器会逐步引导你梳理读取方、回填、expand/contract 步骤和回滚。

编辑说明

本页的每个数字都来自运行包里录制的运行。打分前我们没有修改 agent 的任何输出。