过去一个月里,我用 Claude Code 搭了一个大型 R 研究流水线。问卷数据来自多个来源,既有潜变量模型,也有多重插补和回归分析。项目一旦长起来,就会变成 40 多个 R 文件、数百个变量,散落在配置文件和模型规格里。

Claude Code 会把每个会话都记成结构化 JSON 转录。33 天里一共 1,237 个会话,覆盖了我手头所有研究项目。我写了一个 Python 脚本,把这些转录里的 R 错误逐条抓出来,再按 stderr 输出和错误模式去匹配。最后一共抓到 168 个会话里的 713 个独立错误。

我原本以为会看到一些很“AI”的失败,比如凭空编造函数名、写出根本不可能的语法。结果看到的,却是我自己的老毛病在屏幕上回望我。

数字先说话

1,237
编码会话
713
R 错误
86%
无错误会话

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,却漏掉下游引用。回归公式、配置文件,甚至后面好几步才会执行到的可视化脚本里,都可能还在用旧名字。

Error: Can't subset columns that don't exist.
x Column `income_level` doesn't exist.
Error in eval(expr, envir, enclos):
  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 类型。它看起来像数值向量,打印出来也像,但它不是。

Error: <haven_labelled> - <haven_labelled> is not permitted
Error: Can't combine <haven_labelled> and <double>.

连基本运算都会失败,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,空格就会开始作怪:

Fatal error: cannot open file '/Users/you/Dropbox/My': No such file or directory

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 就会报:

Error: formal argument "printFlag" matched by multiple actual arguments

这条注意事项从一开始就在项目笔记里写着。可每次 AI 因为别的原因碰到插补代码时,它还是会把 printFlag = FALSE 加回来。从 AI 的角度看,给一个类似 mice 的函数传 printFlag = FALSE 显然是“正确写法”,因为训练数据里这种模式它见得太多了。

这正是 AI 辅助编码的核心张力:模型从训练数据里学到的强先验,可能会覆盖掉项目特定的知识。“不要给 futuremice 传 printFlag”这条项目笔记,和一个极强的统计先验在对抗。结果通常是先验赢,而且会一赢再赢,直到你把代码结构改到让错误根本做不出来,而不只是“不建议这么做”。

错误是跟着日历走的

错误分布并不均匀,它们会跟着重大重构节点一起爆发:

2 月 1 日 — 94 个错误

全规模插补运行 + 回归模型重写

1 月 31 日 — 88 个错误

33 份问卷统一读入一个函数 + 新分析模块

2 月 10 日 — 66 个错误

可视化重构 + beamer 演示流水线

2 月 2 日 — 55 个错误

第一次全规模多重插补运行(m=20,分布在 HPC 上)

四天,贡献了全部错误的 42%。模式很清楚:错误会在项目重构的时候激增,而不是在 AI 正常做常规分析的时候。重构期间最占主导的,几乎全是命名错误——AI 能把它碰到的文件重构得很好,却会漏掉那些它没想到要一起改的文件。这也是所有人类程序员共有的盲点:你会记得更新眼前打开的那三份文件,却忘了两周前最后碰过的配置文件。

所以呢?

我最后一直在想:R 里的 AI 编码错误,其实不是 AI 特有的错误,而是 R 特有的错误。AI 并没有凭空捏造函数名,也没有发明不可能的语法;它只是用和我一样的方式,在同样的结构条件下犯同样的错。

R 是动态类型、惰性求值,而且没有编译步骤。拼错的变量名只有在那一行真正执行时才会被发现,可能已经是整条流水线跑到一半甚至更晚。类型不匹配——比如 haven_labelleddouble——要等操作失败才会暴露。带空格的文件路径,在 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 上找到。