过去一个月里,我用 Claude Code 搭了一个大型 R 研究流水线。问卷数据来自多个来源,既有潜变量模型,也有多重插补和回归分析。项目一旦长起来,就会变成 40 多个 R 文件、数百个变量,散落在配置文件和模型规格里。
Claude Code 会把每个会话都记成结构化 JSON 转录。33 天里一共 1,237 个会话,覆盖了我手头所有研究项目。我写了一个 Python 脚本,把这些转录里的 R 错误逐条抓出来,再按 stderr 输出和错误模式去匹配。最后一共抓到 168 个会话里的 713 个独立错误。
我原本以为会看到一些很“AI”的失败,比如凭空编造函数名、写出根本不可能的语法。结果看到的,却是我自己的老毛病在屏幕上回望我。
数字先说话
86% 的会话里没有任何 R 错误。AI 并不是天天在把事情搞坏。但剩下 14% 里 713 个错误,讲的是另一回事,而且故事并不主要关于 AI。
| 错误类别 | 数量 | % | 直白地说 |
|---|---|---|---|
| 变量/对象未找到 | 160 | 22% | 一个文件改名,另一个文件忘了同步 |
| 类型与维度错误 | 137 | 19% | haven_labelled、subscript out of bounds、factor/numeric 混淆 |
| 文件路径与 IO 错误 | 123 | 17% | 路径里有空格、根目录弄错、文件损坏 |
| 包与 API 问题 | 65 | 9% | 缺包、API 改名、Quarto / igraph 变动 |
| 语法错误 | 47 | 7% | 转义字符、意外符号 |
| 运行时逻辑错误 | 44 | 6% | 对 NA 用 if()、pipe 链断掉、grid / ggplot 相关问题 |
| 函数参数写错 | 29 | 4% | 多写了没用的参数、选项配置错误 |
| 基础设施故障 | 51 | 7% | HPC worker、targets 锁文件、C++ 编译 |
| 数值 / 收敛问题 | 24 | 3% | 奇异矩阵、内存限制、hIRT 初始化 |
| 已知陷阱(mice) | 7 | 1% | 文档里写过的坑,还是一再踩到 |
| 缺失函数 / 方法 | 25 | 4% | 函数被移除,或者根本没导出 |
排在最前面的三类错误——名字错、类型错、路径错——一共占了全部错误的 58%。做 R 的人一看就知道这是怎么回事。它们共享同一个根因:R 不会在代码真正运行之前,给你任何关于正确性的结构化反馈。这也是这篇文章真正要讲的。
R 没有编译器,这会改变一切
最大的一类是命名错误:160 个,占总数的 22%。AI 可能在一个文件里把变量改名,比如把数据清洗脚本里的 income_level 改成 income_percentile,却漏掉下游引用。回归公式、配置文件,甚至后面好几步才会执行到的可视化脚本里,都可能还在用旧名字。
x Column `income_level` doesn't exist.
object 'birthyear' not found
这不是 AI 独有的问题,而是每个 R 程序员都会遇到的老问题。我自己也犯过无数次,只是不愿意承认。它真正说明的是,为什么 R 跟那些 AI 助手最擅长的语言不一样。
在 Python 里,引用一个不存在的名字会立刻抛出 NameError。在 Go 或 Rust 里,编译器根本不会让你过。在 R 里,流水线可能已经跑了 30 分钟,数据也读了、模型也拟了、插补也做了,直到第 31 分钟才在那个旧名字上炸掉。
# 这段代码会跑 30 分钟,然后才失败
data <- load_all_surveys() # 2 min
data <- harmonize_variables(data) # 3 min
models <- fit_irt_models(data) # 10 min
imps <- run_mice(models, m = 20) # 15 min
results <- run_regressions(imps)
# Error: object 'income_level' not found # 第 31 分钟
AI 没有办法“编译” R 代码。它把改动写出来,看上去也没问题,但没有任何东西会在运行前替你验证。也正因为如此,命名错误是整个一个月里唯一持续反复出现的类别。类型错误修过一次就基本消失了,路径问题修过一次也基本不回来;只有命名错误会一周接一周地回来,因为 R 本来就没有静态分析这道保险。
慢性问题 vs. 急性问题
大多数错误类别都更像“急性病”:它们会在某次重构里集中爆发,修完就过去,不会再回来。haven_labelled 的类型错误是在 1 月下旬出现的,修掉之后就不再出现;mice 的配置 bug 也是一样。只有命名错误更像“慢性病”——在一门没有编译步骤的语言里工作,这几乎是永久条件。
你的数据在类型上撒谎
这段基本就是写给做定量社会科学的人看的。
137 个错误,占总数的 19%,来自类型和维度不匹配。最典型的例子来自问卷数据。用 haven::read_dta() 读进 Stata 的 .dta 文件时,返回的并不是普通 data frame。那些列里带着看不见的元数据——取值标签、变量标签、格式信息——于是被编码成一种特殊的 haven_labelled 类型。它看起来像数值向量,打印出来也像,但它不是。
连基本运算都会失败,bind_rows() 也会失败。数据看起来是数值,行为上却不是。修复其实只要一个函数:haven::zap_labels()。但你必须在刚读入时就立刻调用它,不能等标签沿着流水线扩散下去。要是拖上三份文件、两小时之后才炸,那往往已经不是表面上的类型错误,而是清洗脚本第 10 行本来就该处理掉、却一路被放大的问题。
这类坑主要影响那些处理 Stata 或 SPSS 问卷数据的人,也就是很大一部分定量社会科学研究者。AI 不会自己知道 haven_labelled 是什么,除非你明确告诉它。它看见一个 data frame,就默认它是普通 data frame,然后写出一段完全说得通、但会因为隐藏原因失败的代码。
更广义的类型陷阱还包括:过滤之后出现的 subscript-out-of-bounds、列在静默强制转换后变成 factor/numeric 混淆、合并之后丢行导致的维度不一致。它们在运行前都看不出来,直到运行时才露馅。代码看上去没问题,运行出来的数据却不对。
代码和电脑之间有一道缝
174 个错误,占总数的 24%,和分析逻辑本身没多大关系。它们来自研究计算的真实脏活:文件、路径、集群、编译器、锁文件。
其中最大的子类是文件路径,123 个错误。如果你在 macOS 上做项目,代码又放在 Dropbox 里,大概率会有这种路径:
/Users/you/Dropbox/My Research Project/code/R/model.R
“My Research Project” 里的那个空格,就是一颗定时炸弹。在 R 里,here::here() 能处理得很好;但只要路径被 system() 或 Rscript 传给 shell,空格就会开始作怪:
AI 往往会像我一样,直接写一个不加引号的 system() 调用。修复方式其实一直都一样:把路径放进引号里。但 AI 不会把这个教训跨会话记住,因为每一个新的 system() 调用都是新的上下文。更麻烦的是,R 的 here::here() 在某些情况下会因为子目录里的 renv.lock 而解析到错误的项目根目录。我们的项目里,这会把所有文件查找都导向 code/Data/,而不是 Data/。后来是靠一个符号链接修好的,但 AI 还是得一遍遍重新发现这个问题。
另一类是基础设施故障,51 个错误:代码本身没错,出问题的是环境。HPC worker 会在中途崩掉,节点间的网络会断,targets 管线会堆出旧锁文件,新任务被卡住,远端集群上的旧工具链又会让 C++ 依赖编译失败。这里没有 AI 的锅,但 AI 分不清“我的代码错了”和“基础设施出岔子了”,两种情况在日志里长得几乎一模一样,也会把它带去同一条错误的排障路径。
AI 记住了什么,也没记住什么
7 个错误,勉强才占总数的 1%,但它们能说明 AI 编码助手到底怎么工作。
做多重插补的 mice 包有个老问题:使用并行版本 futuremice() 时,不能传 printFlag 参数,因为函数内部已经会传。你要是还传,R 就会报:
这条注意事项从一开始就在项目笔记里写着。可每次 AI 因为别的原因碰到插补代码时,它还是会把 printFlag = FALSE 加回来。从 AI 的角度看,给一个类似 mice 的函数传 printFlag = FALSE 显然是“正确写法”,因为训练数据里这种模式它见得太多了。
这正是 AI 辅助编码的核心张力:模型从训练数据里学到的强先验,可能会覆盖掉项目特定的知识。“不要给 futuremice 传 printFlag”这条项目笔记,和一个极强的统计先验在对抗。结果通常是先验赢,而且会一赢再赢,直到你把代码结构改到让错误根本做不出来,而不只是“不建议这么做”。
错误是跟着日历走的
错误分布并不均匀,它们会跟着重大重构节点一起爆发:
全规模插补运行 + 回归模型重写
33 份问卷统一读入一个函数 + 新分析模块
可视化重构 + beamer 演示流水线
第一次全规模多重插补运行(m=20,分布在 HPC 上)
四天,贡献了全部错误的 42%。模式很清楚:错误会在项目重构的时候激增,而不是在 AI 正常做常规分析的时候。重构期间最占主导的,几乎全是命名错误——AI 能把它碰到的文件重构得很好,却会漏掉那些它没想到要一起改的文件。这也是所有人类程序员共有的盲点:你会记得更新眼前打开的那三份文件,却忘了两周前最后碰过的配置文件。
所以呢?
我最后一直在想:R 里的 AI 编码错误,其实不是 AI 特有的错误,而是 R 特有的错误。AI 并没有凭空捏造函数名,也没有发明不可能的语法;它只是用和我一样的方式,在同样的结构条件下犯同样的错。
R 是动态类型、惰性求值,而且没有编译步骤。拼错的变量名只有在那一行真正执行时才会被发现,可能已经是整条流水线跑到一半甚至更晚。类型不匹配——比如 haven_labelled 和 double——要等操作失败才会暴露。带空格的文件路径,在 R 里能用,传给 shell 却会炸。这些东西都没有编译期等价物,也就意味着你没法在跑之前先“构建”一个多文件 R 项目,检查它是否一致。
这不是 R 的 bug,而是它的设计选择:强调交互性和灵活性,也正是这让 R 非常适合探索性分析。但代价是,直到运行出问题之前,人和 AI 都拿不到结构化反馈。在有静态工具链的语言里,比如 TypeScript、Rust、Go,同一个 AI 助手会少犯很多这类错,因为编译器会在执行前就把它拦下来。R 没有这道反馈回路。
我接下来在试什么
如果 AI 在 R 里的最大弱点和我一样,都是“运行前没有结构化反馈”,那解决方案大概率不是把 AI 变得更聪明,而是给它更好的工具。R 的 Language Server 已经为 IDE 用户提供了其中一部分能力:未定义变量检测、跨文件引用、范围感知重命名。我一直在尝试把这些能力直接暴露给 AI 助手。这样是否真的能降低错误率,后面我会再写一篇文章继续讲。
郑思尧是上海交通大学国际与公共事务学院助理教授,研究关注 AI for Social Science 和 Digital Politics。错误数据与提取脚本可在 GitHub 上找到。