AI 编造了根本不存在的 API 签名 —— 排查与修复

AI 信心满满地调用了一个并不存在的方法 —— 参数顺序错、选项名瞎编、返回类型臆造。通过对照真实源码定位与修复。

你让 Cursor 或 Claude Code 接入一个库,生成的代码看上去合情合理,甚至能通过 lint,然后运行时直接炸了:TypeError: client.queryRecords is not a function,或 TypeScript 报 Argument of type 'string' is not assignable to parameter of type 'QueryOptions'。这个方法压根不存在;选项名拼错;参数顺序颠倒。AI 把三个相似库的 API 揉在一起,凭空造出了第四个看起来”应该有”的库。这并不是模型能力的问题,而是上下文锚定缺失——一种几乎所有 AI 编码场景都会遇到的失败模式。

常见原因

按真实编码会话中的频次排序。

1. 库版本与训练数据漂移

AI 学的是 v2 API,你装的是 v4。方法被改名,选项被搬进 config 对象,回调改成了 Promise。AI 输出的签名两年前没错,现在彻底过时。

如何识别:在 package.json 里看版本号,然后翻库的 CHANGELOG 或 releases 页,对照训练截止时间与你版本之间的 breaking change。

2. 跨相似库的方法名串味

fetchaxiosgotundiciky 都做 HTTP,AI 把它们的 API 混在一起。于是出现 axios.fetch(url, { json: true })——看着像那么回事,其实纯属胡编。

如何识别:那个方法名在你导入的库的官方文档里完全搜不到,但在它竞品的文档里能搜到。

3. 选项 key 幻觉

方法和参数都对,但选项 key 写成了 timeoutMs,而库实际接受的是 timeout。TypeScript 能拦下一部分,弱类型 JS 则默默吞掉,选项被忽略,默认值继续生效。

如何识别:运行时 Object.keys(options) 显示你的 key 确实传进去了,但实际行为表明默认值并未被覆盖。

4. 返回类型臆造

const { data, error } = await client.query(...)——但库实际返回的是 Promise<QueryResult>,字段叫 rows 而不是 data,也根本没有 error 字段(错误是 throw 出来的)。AI 套用了 Supabase 风格的返回形状。

如何识别:console.log(JSON.stringify(result)) 打印出来的 key 跟你解构的完全对不上。

5. 参数顺序颠倒

fs.writeFile(content, path) 写成了 fs.writeFile(path, content) 的反面。或 localStorage.setItem(value, key)。AI 记住了函数名,却没记清参数顺序。

如何识别:文件被创建出来,文件名是一段乱码的内容字符串;或 storage 里 key 和 value 完全互换。

6. 自作主张的”便利方法”

array.removeAt(index)string.contains(sub)date.addDays(7)——这些方法在其他语言里确实存在,但 JS 标准库里没有。AI 替你”善意地”补全了一个不存在的 API。

如何识别:在 MDN 或官方文档里搜该方法名,零结果。

开始之前

  • 锁死实际版本:npm ls <package>cat package.json | grep <package>
  • 打开对应版本的官方文档,不要点 “latest”。
  • TypeScript 项目务必开启 strict: true,让类型检查器提前拦截签名不匹配。
  • 在修改前先把 AI 的原始建议保存下来——后续做 prompt 修补时有用。

需要收集的信息

  • AI 写出的那行方法调用,原样复制。
  • 已安装包的版本(package.jsonnode_modules/<pkg>/package.json 都看一下)。
  • 错误信息或运行时的错误输出。
  • 库实际的签名:cat node_modules/<pkg>/dist/index.d.ts | grep -A 3 <method>
  • AI 是否引用了来源。如果它引用的是某篇博客,那篇博客往往就是幻觉形状的源头。

分步修复

按从最便宜的检查到长期防御排序。

第 1 步:先确认这个方法到底存不存在

grep -r "methodName" node_modules/<package>/dist/

或直接在 Node REPL 里看:

node -e "console.log(Object.keys(require('<package>')))"

如果包的真实 exports 里根本没有这个方法,那就是彻头彻尾的幻觉——再怎么调参数都救不回来,得换方法或换思路。

第 2 步:从类型定义里读出真实签名

对于带类型的库:

cat node_modules/<package>/dist/index.d.ts | grep -B 1 -A 10 "<method>"

这就是 ground truth。和 AI 写的对比,diff 就是 bug。

第 3 步:把真实签名喂回 AI

重新提示时把实际的 .d.ts 片段粘进去:

真实签名是:
  query(sql: string, params?: unknown[]): Promise<{ rows: Row[]; rowCount: number }>

请按这个签名重写之前的代码。
不要添加签名里没有的选项 key。

这样 AI 下一次生成会锚定在真实源码上,而不是凭训练记忆。

第 4 步:把 AI 锁定在已安装版本

在 system prompt 或规则文件里写:

本项目使用 <package>@<exact-version>。任何其他版本的 API 一律视为错误。
在生成调用 <package> 的代码前,先从
node_modules/<package>/dist/index.d.ts 引用方法签名,再写调用点。

Cursor 写到 .cursorrules,Claude Code 写到仓库根目录的 CLAUDE.md

第 5 步:在调用点加运行时断言

哪怕已经修好了,也最好防御性地挡一道:

if (typeof client.query !== "function") {
  throw new Error(
    "client.query missing — likely an AI-hallucinated method. " +
    "Check the @yourorg/db version and re-read the .d.ts."
  );
}

把未来可能重新引入的回归变成一次明确的、带名字的失败。

第 6 步:为常见幻觉模式加 lint 规则

如果同一个不存在的方法名团队反复中招,加一条自定义 ESLint no-restricted-syntax:

{
  "rules": {
    "no-restricted-syntax": ["error", {
      "selector": "CallExpression[callee.property.name='queryRecords']",
      "message": "queryRecords does not exist on this client. Use query()."
    }]
  }
}

AI 一旦把这种调用塞进代码,lint 立刻报错。

验证

  • 替换后的调用在 strict: true 下编译通过,且没有 // @ts-ignore 这类补丁。
  • 单元测试覆盖该路径,响应形状与代码期望一致(不会有意外的 undefined)。
  • grep -r "<hallucinated-method>" src/ 整个仓库零命中。
  • 同样的提示再次让 AI 生成相关功能时,第一次就给出正确的签名。

长期预防

  • CLAUDE.md.cursorrules 里固化已安装包的版本,以及”写调用点前先查 .d.ts”这条硬规定。
  • TS 项目优先选带类型的库;大多数签名幻觉会被类型检查器在运行前拦下。
  • 对于无类型或弱类型的库(裸 HTTP SDK、老旧 npm 包),用一层内部 wrapper 包成严格类型;AI 之后对接的是你的 wrapper,而不是野生 API。
  • 接入新库时把它的 README 或 index.d.ts 直接粘进 prompt,别让 AI 凭训练记忆硬猜。
  • 长期项目升级依赖后,重新生成类型 stub,让 AI 看到的是当前形状。
  • code review checklist 加上一条:“该方法在实际安装的库版本里存在吗?”作为通过 AI 生成的集成代码前的硬性步骤。

常见误区

  • 轻信 AI 那句”这是 v2 的写法”——一定要去 v2 文档/CHANGELOG 里亲自核对 v2 是否真有这个方法。
  • as any 把 TypeScript 报错压住,而不去修签名——运行时错误只是被推迟到了生产环境。
  • 让 AI”修一下”,但不给真实签名——它只会幻觉出另一种不同的错误形状。
  • 为了”也许 v6 里有”而升级库版本——通常哪个版本都没有。
  • 假设 AI 只幻觉了一个方法——只要有一个签名错了,周围 30 行里大概率还藏着更多伪造调用。

相关失败模式见 AI 建议了过时的依赖AI 生成的 TypeScript 错误Cursor 漏看了项目上下文

FAQ

Q:这个方法明明不存在,AI 却信誓旦旦,为什么?

模型训练时把许多相似库平均化了。它输出的是”听起来合理”的形状,而不是从一个被验证过的索引里检索出来的事实。把所有 AI 生成的 API 调用都当作假设,直到你和真实 .d.ts 或文档对照确认为止。

Q:我已经把文档粘进 prompt 了,它还是幻觉,为什么?

要么是文档在上下文里被截断,要么是 AI 还是把训练记忆权重压过了你贴的内容。重新提示时加一句”只能使用上面块里的签名。如果我要的方法不在那里,请明确说出来。”

Q:要不要换一个 AI 更熟的库?

除非那个库本身就更适合你的需求,否则不必。问题的本质是锚定不足,不是要投降。把真实签名钉进规则文件后,AI 对原本那个库一样能写对。

Q:怎么判断整个 AI 生成的文件里还有没有藏着别的幻觉?

把 TypeScript 严格模式 + 完整测试覆盖一起跑一遍。两者都过、且运行时行为符合规格,基本就干净了。类型检查 + 测试能抓住约 90% 的签名幻觉。

标签: #排查 #AI 编程 #幻觉 #api #type-errors