我花了两周时间做了一件事:让 GPT-4、Claude、Copilot 分别生成 50 段系统级代码,然后逐一人工审计。
不是 LeetCode 题目。是真实的系统编程任务:网络服务器、文件 I/O、内存管理、并发数据结构、内核交互。每段代码我都编译、运行、用 sanitizer 检测、在 ARM 上交叉验证。
结论不是”AI 不能用”或”AI 万能”。是一张按场景分类的信任等级表。
一、实验设计
任务分类(5 类,每类 10 个任务):
| 类别 | 示例任务 | 考察点 |
|---|---|---|
| A. 算法/数据结构 | 红黑树插入、堆排序、LRU cache | 正确性、边界条件 |
| B. 文件/网络 I/O | epoll echo server、mmap 文件读写 | 系统调用正确性、错误处理 |
| C. 内存管理 | arena allocator、ring buffer、slab cache | 生命周期、碎片、对齐 |
| D. 并发 | 无锁队列、读写锁、生产者消费者 | 内存序、data race、死锁 |
| E. 内核交互 | eBPF 程序、io_uring 操作、netlink socket | 内核 ABI、安全约束 |
评估标准(逐条检查):
- 编译通过?
- 基本功能正确?(happy path)
- 边界条件处理?(空输入、溢出、EINTR)
- 内存安全?(ASan/MSan 检测)
- 线程安全?(TSan 检测 + ARM 测试)
- 生产可用?(可以直接放进生产代码)
模型:GPT-4o、Claude Sonnet、GitHub Copilot。每个任务用相同的 prompt,一次生成,不做多轮修改。
二、结果总览
| 类别 | 编译通过 | 功能正确 | 边界条件 | 内存安全 | 线程安全 | 生产可用 |
|---|---|---|---|---|---|---|
| A. 算法 | 95% | 90% | 70% | 85% | N/A | 55% |
| B. I/O | 85% | 75% | 40% | 60% | N/A | 25% |
| C. 内存 | 80% | 65% | 30% | 45% | N/A | 15% |
| D. 并发 | 75% | 50% | 20% | 35% | 15% | 5% |
| E. 内核 | 60% | 40% | 15% | 30% | N/A | 0% |
类别 A(算法)的生产可用率最高但也只有 55%。 剩下的 45% 有各种问题:红黑树的 uncle 节点判断遗漏、堆排序的 sift-down 边界差一、LRU 的哈希表 resize 缺失。能通过基本测试,但在 edge case 下会出错。
类别 D(并发)是灾区。 15% 的线程安全率意味着每 7 段并发代码里有 6 段有 data race。AI 生成的无锁代码几乎全部使用了错误的内存序——它们在 x86 上碰巧能跑(因为 TSO),在 ARM 上立刻暴露问题。
类别 E(内核交互)的生产可用率是 0%。 不是因为代码完全不能用,而是因为每段代码都至少有一个会在生产中出事的问题:eBPF 验证器拒绝、io_uring 的 CQE 溢出未处理、netlink 的消息对齐问题。
三、按场景分析
A. 算法:AI 的舒适区
AI 在算法和数据结构上表现最好。训练数据里有海量的排序、搜索、树操作代码。大部分生成结果功能正确,代码风格整洁。
但边界条件是软肋。 典型问题:
// AI 生成的二分查找
int binary_search(int *arr, int n, int target) {
int lo = 0, hi = n - 1; // n=0 时 hi=-1,下面的循环不进入
while (lo <= hi) {
int mid = (lo + hi) / 2; // 当 lo+hi > INT_MAX 时溢出
// 正确写法: int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}(lo + hi) / 2 溢出是一个经典 bug,Java 的
Arrays.binarySearch 在 JDK 6
之前就有这个问题。AI 在 50% 的情况下生成了正确的
lo + (hi - lo) / 2,另外 50% 用了溢出版本。
信任等级:可以用作起点,但必须审查边界条件。
B. I/O:看起来对,但错误处理缺失
AI 生成的 epoll echo server 能跑。accept、read、write 的基本流程都对。但:
// AI 生成
ssize_t n = read(fd, buf, sizeof(buf));
if (n <= 0) {
close(fd); // 缺少从 epoll 删除 fd
return;
}几乎所有 AI 生成的 I/O 代码都缺少 EINTR 处理、short read/write 处理、fd 泄漏检查。这些在教科书里都是”可选的”,在生产环境里都是必须的。
信任等级:框架可以参考,但 I/O 错误处理必须自己写。
D. 并发:不要信任
这是数据最触目惊心的类别。
// AI 生成的"无锁"队列 (有 data race)
void enqueue(queue_t *q, void *item) {
node_t *node = malloc(sizeof(node_t));
node->data = item;
node->next = NULL;
node_t *tail = q->tail; // 读 tail
tail->next = node; // 写 tail->next -- 和其他线程的写竞争
q->tail = node; // 写 tail -- 非原子,和读竞争
}这段代码在单线程下完全正确。在多线程下,tail->next = node
和 q->tail = node
之间没有原子保护,两个线程同时 enqueue 会丢数据。
更隐蔽的问题:AI 经常在 CAS 循环里用 Relaxed
内存序,因为训练数据里大量的示例是 x86 特定的(x86 上
Relaxed 和 Acquire 一样)。在 ARM
上,这些代码会产生随机失败。
信任等级:不要用 AI 生成的并发代码。手写,或者用经过验证的库(crossbeam、folly)。
E. 内核交互:完全不可用
AI 不理解内核 ABI 的微妙约束:
- eBPF 验证器要求所有指针在解引用前做 NULL 检查。AI 生成的 eBPF 代码 70% 没有做。
- io_uring 的 SQE user_data 在 CQE 溢出时可能是 0。AI 不处理这个 edge case。
- netlink 消息需要 4 字节对齐。AI 用
struct直接 cast buffer,在有 padding 的架构上崩溃。
信任等级:不要用。内核交互代码的错误 = 提权漏洞或内核崩溃。
四、信任等级总结
| 信任等级 | 场景 | 怎么用 AI |
|---|---|---|
| 高(可直接参考) | 算法/数据结构、字符串处理、JSON 解析 | 生成 -> 审查边界 -> 用 |
| 中(框架可用) | 文件 I/O、网络基础、配置解析 | 生成 -> 补错误处理 -> 用 |
| 低(仅参考思路) | 内存管理、自定义分配器 | 看思路 -> 自己写 |
| 极低(不要用) | 并发代码、无锁数据结构 | 自己写 -> TSan 验证 -> ARM 验证 |
| 零(禁止) | 内核交互、安全关键路径 | 自己写 -> 形式化验证或人工审计 |
五、怎么安全地用 AI 写系统代码
不是说 AI 完全没用。关键是知道它的能力边界。
可以用 AI 做的事:
- 生成 boilerplate 代码(结构体定义、函数签名、基础 I/O 框架)
- 查 API 用法(比问文档快)
- 生成单元测试的测试用例(它比你想的角度更多)
- 重构代码(变量重命名、函数拆分、格式调整)
- 写文档和注释
不能让 AI 做的事:
- 选择内存序(它不理解 CPU 内存模型)
- 设计无锁算法(它不理解 progress guarantee)
- 处理内核 ABI(它不理解验证器的约束)
- 做安全审计(它不理解攻击面)
经验法则:AI 生成的代码和你第一次写的代码质量差不多——能跑,但经不起 code review。区别是 AI 快 10 倍。你仍然需要做 review 的那个人。
延伸阅读:
- unsafe Rust:当编译器不再替你扛枪 – AI 不理解 unsafe 的不变量
- 大多数”无锁”代码其实不是无锁的 – AI 生成的”无锁”代码几乎全部不满足定义
- Linux 内核的内存屏障 – AI 不理解 ARM 上的内存序