T31 · AI-assisted Crypto Development
AI 写的合约,能不能上主网?
- 练习的能力
- AI LiteracyBuilder
- 动手
- 让 AI 写一个模块,逐行 Review,统计它编造了几个不存在的 API。
- AI Lab
- 把一笔失败交易的 Trace 交给 AI 定位原因,自己用调试工具验证它的结论。
一个现实问题
周五下午四点,你需要一个质押模块:用户存入代币,按时间累积奖励,随时可以取出。
你把需求描述给一个模型,四十秒后拿到两百行代码。编译通过。你让它顺手把测试也写了,十二个用例,全绿。
你盯着屏幕。这两百行代码你一行都没读过,但它们看起来比你自己半天能写出来的更整齐:注释完整、命名规范、还贴心地加了重入保护。
周一要上线。你要不要部署?
先别急着回答,先回答两个更小的问题:
- 编译通过,证明了什么? 证明每个被调用的东西都存在,类型对得上。它没有证明任何一行代码做的是你想要的事。
- 测试通过,证明了什么? 这十二个用例是同一个模型、在同一次对话里、基于同一份(可能错误的)理解写出来的。它们通过,只证明实现和测试彼此一致。
这一章要处理的就是这个落差:你的产出速度提高了二十倍,而你验证它的速度,一点没变。
思想实验
把模型换成一个人,问题会清楚得多。
假设你团队来了一位新同事。他有三个特点:
- 产出极快。 你描述一个需求,他半小时给你一个完整实现。
- 知识面极广,但停在某个时间点。 他熟悉两年前的生态,对上个月发布的东西一无所知——而且他不知道自己不知道。
- 从不说「我不确定」。 想不起某个接口的确切名字时,他会写一个听起来最合理的名字,然后继续往下写,语气和平时完全一样。
现在问题来了:你会怎么和他协作?
有一件事很明确:你不会因为他写得快,就少读一行代码。 恰恰相反,因为第 3 条,你读他代码的方式还要变——你不仅要检查逻辑,还要检查他用到的东西是不是真的存在。这是你 Review 人类同事时从来不做的一件事。
再往下推一步。他一周能产出五千行,你一周能认真读八百行。
这个差额不会消失。它只有三个去处:
- 你熬夜读完(几周后你会开始糊弄);
- 你只读一部分(那就得决定读哪一部分);
- 你不读(那你就是在赌)。
产能提高了,验收能力没提高,瓶颈就从「写」搬到了「读」。 这是这一章真正的主题。
你来决定
回到那个质押模块。周一要上线,你只有两个小时。你怎么用这两个小时?
观察结果
把四种做法能抓到的东西摆在一起,会看到它们抓的根本不是同一类问题。
| 做法 | 能发现 | 发现不了 | 单位成本 |
|---|---|---|---|
| 测试网手动跑 | 主流程硬故障、部署配置错误 | 顺序问题、边界、经济逻辑 | 很低 |
| 另一个模型 Review | 常见模式缺失、明显反模式 | 幻觉 API、与需求不符、同源错误 | 很低 |
| 逐行读 + 核对文档 | 幻觉 API、版本漂移、语义错误 | 全局不变量、跨函数的状态顺序 | 很高 |
| 分级读 | 资金相关的全部三类错误 | 非资金路径上的错误 | 中 |
| 编译器 | 不存在的符号、类型不匹配 | 存在但语义不对的一切 | 零 |
| 模型自己写的测试 | 与模型的理解不符的实现 | 与模型的误解一致的实现 | 零 |
最后两行最值得盯着看。
编译器是免费的,而且它恰好能抓到模型最典型的一类错误——引用了不存在的东西。所以在有编译器的语言里,纯粹的「函数名瞎编」活不过三十秒。
真正危险的是编译器和测试都拦不住的那一类:符号存在、类型正确、测试通过,但它做的不是你要的事。 模型写的测试在这里帮不上忙,因为测试和实现来自同一次理解。如果它把「奖励按秒累积」理解成了「奖励按区块累积」,那么实现是按区块的,测试也是按区块的,十二个用例全绿。
一句话:AI 把「写错」的成本降到了几乎为零,却没有降低「验错」的成本。 于是错误的构成变了——今天工程里的绝大部分风险,集中在「看起来完全正常」的那部分代码里。
建立模型
先给 AI 生成代码的错误分类。这四类的发现者完全不同,这是这一章最有用的一张表。
| 类型 | 长什么样 | 谁能发现 | 危险度 |
|---|---|---|---|
| 幻觉 API | 调用了一个不存在的方法、参数、库或事件 | 编译器 / 构建工具 | 低(吵闹但无害) |
| 版本漂移 | 符号存在,但语义在某个版本之后变了 | 人,核对文档 | 高 |
| 语义偏差 | 代码正确地实现了一个错误的理解 | 人,回到需求 | 最高 |
| 同源测试 | 测试和实现基于同一个误解,互相印证 | 人,或来自外部的不变量 | 最高 |
注意危险度和「看起来有多离谱」是反过来的。凭空编造的函数名最显眼,也最无害——它连编译都过不了。真正让协议出事的,是那些写得漂漂亮亮、跑得好好的语义偏差。
把验收流程排成一条链,每一环只做它擅长的那件事:
- 生成
- 编译
- 静态分析
- 分级人工 Review
- 外部不变量测试
- Fork 测试
- 安全评审
这条链上有一条铁律,和下一章的架构是同一条:
谁部署,谁负责。Review 的责任不随产出方式转移。
模型没有账户、没有资产、也不会被追责。你按下部署按钮的那一刻,这两百行代码就变成了你写的代码,和它是谁敲出来的毫无关系。
最后是那个绕不开的算术。设你一周能认真验收的代码量是一个固定值,记作 Review 预算:
可上线的代码量 = min(生成量, Review 预算)
AI 把「生成量」提高了 10 到 20 倍。
它对「Review 预算」的提升,乐观估计不超过 2 倍。
所以:可上线的代码量 ≈ Review 预算,几乎没变。这个结论听起来很扫兴,但它指向了正确的用法:AI 最大的价值不在于让你写更多代码,而在于让你花更少时间产出同样多的、你有能力验收的代码;以及——它在「读」这一侧也能帮上忙,比如解释一段别人写的合约、分析一笔失败交易的 Trace、从实现反推不变量。后面这些用法直接抬高 Review 预算,比多生成两千行有价值得多。
它叫什么
模型输出了一个并不存在的函数、参数、事件、配置项或依赖包,但名字极其合理。
它源于模型是按「最可能出现的下一个词」生成的:一个叫 safeTransferWithDeadline 的方法在语言模型看来完全合理,哪怕现实中没有。
在强类型编译语言里它基本无害,在配置文件、脚本语言和依赖清单里则相反——那些地方没有编译器兜底。
模型的知识有一个截止时间,而且它通常不会告诉你这一点。
在 Crypto 里这一类错误特别贵,因为生态迭代快:编译器的默认行为变过、标准库的函数签名变过、某些曾经的最佳实践现在已经是反模式。
判断方法只有一个:去看官方文档的更新时间,而不是问模型「这是最新的吗」。 它会说是。
代码正确地实现了一个错误的需求理解。
编译器、静态分析、类型系统全部无能为力,因为形式上完全正确。唯一的发现方式是回到需求本身,逐条问「这一行对应需求里的哪一句」。
测试与实现出自同一次理解,于是测试只是把实现又写了一遍。
典型征兆:测试里的期望值是从实现反推出来的,而不是从需求算出来的。比如断言「奖励等于 rewardPerBlock 乘以区块数」——这句话直接抄自实现,无论实现对不对它都成立。
破解方式是让期望值有独立的来源:手算、白皮书里的公式、另一个实现、或者不变量。
一笔交易执行时的完整调用轨迹:谁调用了谁、传了什么参数、在哪一步 revert、消耗了多少 Gas。
链上失败信息往往极其简陋,可能只有一个四字节的错误标识。把完整 Trace 交给模型做定位是它最强的场景之一,因为这本质上是模式匹配,而且结论可以立刻被验证——这是关键。
单位时间内你能真正验收的代码量。它由你的知识、注意力和时间决定,不由工具决定。
把它显式写出来的好处是:你会开始主动分配它,而不是被产出量推着走。
动手
这个 Lab 的产出不是那个模块,而是一张你自己的错误分布表。
先把需求写成规格,再给模型。 不要口头描述,写成一份三十行以内的规格,包含:
状态:谁的余额、什么时候更新
动作:deposit / withdraw / claim,各自的前置条件
数学:奖励怎么算,用什么单位,精度多少位
边界:第一个存入的人、最后一个退出的人、同区块内存取
不变量:合约余额 ≥ 所有人的可取余额之和这一步不能省。没有规格,你后面就没有任何东西可以拿来对照——你只能对照模型自己的解释,那等于没对照。
让模型一次性生成实现。 不要在对话里反复修,就要那个「四十秒两百行」的原始产物,越像真实场景越好。
把这个版本单独存一份。后面所有统计都基于它。
逐行读,给每一行打标记。 准备一张四列的表:行号、类型、我怎么发现的、危险度。
类型用上面那四类:幻觉 API、版本漂移、语义偏差、同源测试。加一个「正确」。
规则:每一个外部符号都要去官方文档核对,包括函数名、参数顺序、事件签名、修饰符、以及它引入的每一个依赖。不要用搜索引擎的摘要,打开文档本身。
统计。 至少统计这四个数:
- 幻觉 API 几个,其中编译器抓到几个
- 版本漂移几个,分别漂移了多久
- 语义偏差几个,都出现在哪一类逻辑里
- 你读完这两百行花了多少分钟
第四个数是你的 Review 预算基准值,后面每一章都用得上。
验证那些测试。 拿模型写的十二个测试,逐个问一句:这个断言的期望值,是从需求算出来的,还是从实现抄出来的?
从实现抄出来的,全部删掉重写。这一步通常会砍掉一半以上。
部署到测试网,只做三件事: 存入、等待、取出。对着你规格里的数学手算一遍奖励,和链上读到的数字对。
全程测试网,不要用任何真实资产。 这个模块的正确性还没有被独立验证过,主网版本必须先过 T28、T29、T30 的安全流程。
做完以后,把那张表贴在你自己的文档里。它比任何一篇「AI 编程最佳实践」都更有用,因为它是你手上这个模型、在你这个技术栈上的真实错误分布。
AI Lab
Trace 分析是 AI 在 Crypto 开发里最靠谱的用法之一,原因很实在:它的结论可以在几分钟内被独立验证。 模型说「在第 7 层调用 revert 了」,你重放一次就知道它对不对。这和让它凭空写代码完全是两种风险。
先自己造一笔失败交易。在测试网上故意触发一次:额度不够的转账、滑点保护被触发的兑换、或者一次访问控制拒绝。记下交易哈希。
然后把材料完整交给模型:
这是一笔在测试网上失败的交易。
[完整的调用 Trace,包含每一层的 to、函数选择器、calldata、返回值]
[交易的 from、to、value、gas limit、实际 gas used]
[涉及到的合约源码,或至少是 ABI]
[我以为它应该做什么]
请回答:
1. 它在哪一层、哪一个调用上失败了
2. 那个错误标识对应源码里的哪一个 error 定义
3. 触发这个条件的直接原因是什么
4. 更上游的原因可能是什么(列成假设,标注各自的可信度)
5. 我应该怎么改,以及怎么验证这个改动有效
对第 3、4 两问,明确区分「Trace 里直接能看到的」和「你推测的」。最后一句是整段提示词里最重要的一句。没有它,模型会把推测和事实混在同一段话里,语气完全一致。
验证的顺序也有讲究: 先验它指认的位置(最容易验,一分钟),再验错误标识(查源码,五分钟),最后才验因果推断(最难验,也最容易错)。前两步错了,第三步不用看了。
跑过五六次以后你会发现一个规律:模型在「定位」上几乎不出错,在「归因」上经常自信地跑偏。这个分布本身就是本章要你带走的东西。
AI 说完之后,你必须自己验证
- 它指认的 revert 位置,你在调试工具里重放过一次,确认就是那一行
- 它给出的错误标识(selector 或错误名)你在合约源码里找到了对应的定义
- 它引用的函数签名、参数顺序、事件定义,你逐个回到源码或官方文档核对过
- 它提出的修复方案,你能说清为什么这个改动会让那一步不再 revert
- 它有没有把「可能的原因」说成「确定的原因」——把每条结论重新标注成假设或事实
- 按它的修复改完之后,你在 Fork 环境重放了同一笔交易,确认真的通过
真实案例
模型在生成代码时会建议依赖包。其中一部分包名是编造的——名字合理、用途明确,但从未有人发布过。
安全研究者反复报告过同一条攻击路径:统计模型高频编造的包名,抢先把它们注册到公共仓库里,塞进恶意代码,然后等开发者照着模型的建议执行安装命令。
这类风险绕过了编译器,因为一旦包真的存在,它就能被正常安装、正常编译。
防御很朴素:依赖清单必须逐行人工确认,确认发布者、下载量、仓库地址与最近更新时间。在处理私钥和资金的项目里,这一步的优先级高于任何功能开发。
一个费用计算模块,需求是「按年化收取,不足一年按天折算」。模型把它实现成了「按调用次数折算」——然后写了一组完美印证这个实现的测试。
全绿。Review 的人扫了一眼测试名和覆盖率,通过了。
这类问题的共同特征是:没有任何一个数字有独立来源。 期望值抄自实现,实现来自误解,闭环自洽。
唯一的破法是引入外部锚点:手算一个值、用白皮书的公式算一个值、或者写一条与实现无关的不变量(「任何时刻收取的费用不超过本金的 X%」)。T30 的不变量方法在这里直接可用。
智能合约语言的一次主版本更新,把整数溢出检查变成了默认行为。在那之前,几乎所有生产代码都要引入一个安全数学库;在那之后,继续引入它属于冗余。
模型的行为呈现两个方向的错误:在新版本代码里塞进已经不需要的旧库(浪费 Gas,但无害),以及在旧版本代码里假设溢出会被自动检查(真正的漏洞)。
后一种的危险在于它完全没有症状——代码更短、更干净、更现代,唯独在一个具体的版本号下是错的。
核对版本号,永远比核对代码本身重要。
心理学上有一个稳定的现象:当自动化系统给出的输出看起来专业时,人的审查强度会下降。
AI 生成的代码恰好触发了这一点:命名规范、注释完整、结构对称、还带着恰到好处的防御性检查。它让人产生一种「已经被某种专业性背书过」的感觉。
真实的 Review 记录会暴露这件事:同一个人,读自己同事写的两百行乱糟糟的代码平均提出九条意见,读模型生成的两百行整齐代码平均提出两条。
对策是换一种读法:不要顺着读,倒着读——从每一处状态变更出发,往回追「这个值从哪来、谁能影响它」。倒着读的时候,代码好不好看不再影响你。
改一个变量
幻觉 API 会显著减少,版本漂移会减少一些,语义偏差几乎不变。
原因是前两类错误来自知识的缺口,模型变强就是在补这个缺口;而语义偏差来自你的需求描述不够精确,这个缺口在你那边,模型再强也补不上。
所以一个反直觉的结论:模型越强,写规格的重要性越高。弱模型的错误显而易见,强模型会把你没说清楚的地方补得天衣无缝——补成它以为的样子。
风险等级下降一到两档,但不会归零。
合约的错误不可逆,前端的错误可以重新部署,所以直觉上放松是合理的。但有两个地方不能放松:任何构造交易的代码(金额单位、精度、接收地址、滑点参数——这些一旦错了,用户的钱真的会没),以及任何依赖清单(前端的依赖树比合约大两个数量级,投毒的机会也多两个数量级)。
索引脚本的典型风险则是另一类:模型写的重组处理逻辑往往只考虑了正常路径。T17 讲过这件事。
这是性价比最高的一种用法。
因为实现是你写的,测试来自模型,同源问题自动消失了:它不知道你脑子里的误解是什么,于是它按需求描述去想边界,经常能想到你没想到的组合。
要注意的只有一点:模型倾向于写「能通过的测试」。如果你把实现一起给它,它会照着实现写断言,同源问题又回来了。只给规格,不给实现。
这条线一旦跨过去,本章的所有讨论全部作废,因为讨论的前提是「有人在读」。
而且它会退化成下一章明确禁止的那个结构:模型的输出直接获得了不可逆的执行力,中间没有任何一个位置可以发现错误。
如果你真的需要自动化部署,那么被自动化的必须是已经通过人工 Review 的产物,而不是模型的原始输出。T32 会把这件事讲成一个通用架构,它适用于代码,也适用于交易。
带走的问题
AI 在这里降低的是生成成本,不是验证成本。这一章的全部结论都从这个不对称里长出来:便宜的那一侧产能暴涨,昂贵的那一侧原地不动,瓶颈就搬了家。判断任何一个 AI 编程工具好不好用,就问它降的是哪一侧的成本。
AI 写错的合约把钱搞没了,损失由部署它的人承担。这不是一句道德劝诫,而是一个法律和工程上的事实:模型没有账户、没有资产、不是任何合同的一方。这意味着 Review 记录本身是有价值的资产——出事时它是你唯一能拿出来的东西。
在开发流程里,至少三件事必须保留人工:依赖清单的确认(供应链风险无法自动化排除)、改变状态与转移资金的那部分代码(语义偏差在这里最贵)、部署动作本身(它是整条链上唯一不可逆的一步)。这三件事写进你的 CI 检查清单,不要写进团队约定。
这一章里模型的权限是 2 级:它可以读代码、写代码、分析 Trace,但它碰不到任何密钥、任何部署凭证、任何生产环境。把这条边界画清楚,是读 T32 的前提——那一章要把同一条边界画在运行时。
本章自测
编译通过排除的是符号层面的错误:被调用的东西存在、类型对得上、签名匹配。它恰好覆盖了幻觉 API 这一类,所以在强类型语言里,纯粹的函数名编造活不过三十秒。
测试通过排除的是与测试期望不符的实现。注意这句话的限定:它排除的是与测试不符,而不是与需求不符。当测试和实现出自同一次理解时,这两者的差别就是全部的风险所在。
两者加起来,都不能排除语义偏差——代码正确地实现了一个错误的需求。
可以用它做初筛,不能用它做验收。差别在于谁签字。
技术上的原因有两条:两个模型的训练数据高度重叠,容易在同一处犯同样的错;以及第二个模型同样会产生幻觉,于是你得到的是两份都需要人工核对的材料,而不是一份验证过的结论。
责任上的原因更根本:模型不承担任何后果。部署按钮是你按的,所以验收必须在你这里终结。这和 T32 里「不要用模型去校验模型的输出」是同一条规则的两个应用场景。
看它的期望值从哪里来。
问一句:如果实现是错的,这个断言还会失败吗?
- 断言写「奖励等于
rewardPerBlock乘以经过的区块数」——这句话抄自实现,实现错了它也不会失败。同源。 - 断言写「存入 100 个代币、等待 30 天、按年化 5% 应该得到约 0.41 个」——这个数字是从需求手算出来的,实现错了它就失败。独立。
另一个更省事的办法是引入不变量:不变量描述的是「任何时刻都应该成立的性质」,它天然来自需求而不是实现。
先分档,再读。
第一档是会改变状态、转移资金或修改权限的行。逐行读,每一处数学手算一遍,每一个外部调用核对官方文档。两百行里通常是二三十行,占掉你八成时间。
第二档是视图函数与读取路径。快速扫,重点看第一档有没有信任它们的返回值——被信任的读取路径,实际上属于第一档。
第三档是注释、事件、日志、格式。几乎不用读,但要确认事件参数和实际状态变更一致,否则你的索引器会拿到错的数据。
关键不在于这个分法多精确,而在于你显式地知道自己漏掉了什么。没分档地读两百行,你最后也只真读了三十行,区别只是你不知道是哪三十行。
没有标准答案,检查这几件事:
- 这个比例你能答上来吗?答不上来说明你根本没在跟踪它——那才是真正的问题。
- 没读的那部分,有没有落在「会改变状态或移动资金」的路径上?如果有,你现在就有一个未知大小的敞口。
- 你的 Review 速度(行 / 小时)这个月有变化吗?如果明显变快了,先别高兴,确认一下是因为你更熟练了,还是因为你开始扫而不是读了。
- 你有没有把「读」这一侧也交给 AI 提速——解释陌生合约、反推不变量、分析 Trace?只在「写」这一侧用 AI,是把瓶颈越堆越高。
第 4 条是这一章最想让你带走的操作建议。
一句话带走
AI 提高的是产出速度,Review 的责任一点没有转移。