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

AI 调用了一个并不存在的方法:参数顺序错、选项名瞎编、返回类型臆造。对照真实 .d.ts 定位,并用当前文档锚定后续生成。

你让 Cursor、Claude Code 或 Codex 接入一个库,生成的代码看上去合情合理,甚至能通过 lint,然后运行时直接炸了:TypeError: client.queryRecords is not a function,或 TypeScript 报 Argument of type 'string' is not assignable to parameter of type 'QueryOptions'。这个方法压根不存在;选项名拼错;参数顺序颠倒。AI 把三个相似库的 API 揉在一起,凭空造出了第四个看起来”应该有”的库。

最快的修法:打开库真实的类型定义(node_modules/<package>/dist/index.d.ts),或运行 node -e "console.log(Object.keys(require('<package>')))",把真实签名粘回对话框,让模型按这个签名重写调用。这一步锚定就能修好当前这处调用。本文后面的部分负责让修复在整份文件里都生效,并预防下一次再发生。

这并不是纯粹的模型能力问题,而是上下文锚定缺失——一种几乎所有 AI 编码场景都会遇到的失败模式。即便是 2026 年 6 月最强的模型(GPT-5.5、Claude Opus 4.7、Gemini 3.1 Pro),一旦被迫依赖训练记忆而非你已安装的源码,照样会幻觉签名。锚定是你能控制的环节。

先对号入座

把你看到的现象对到对应原因,再跳到下面相应的小节。

你看到的现象最可能的原因
运行时 X is not a function / 方法不存在凭空捏造的方法(原因 2 或 6)或版本漂移(原因 1)
方法能跑,但某个选项被悄悄忽略选项 key 幻觉(原因 3)
解构出来的字段是 undefined返回类型臆造(原因 4)
数据落错位置(文件名变成了内容本身等)参数顺序颠倒(原因 5)
TS 报 ... is not assignable to parameter of type ...参数类型不对,常见于版本漂移(原因 1)或串味 API(原因 2)

常见原因

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

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 锁定在已安装版本

加一条规则,让模型在写任何调用前先去查真实源码:

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

截至 2026 年 6 月,这条规则该放哪里:

  • Cursor:.cursor/rules/<name>.mdc 创建一条项目规则(单文件的 .cursorrules 格式已弃用,且在 Agent 模式下会被静默忽略——也就是说在 agentic 对话里 .cursorrules 里的规则会悄无声息地不生效)。.mdc 文件带 frontmatter:description(让 agent 知道何时套用)、globs(如 **/*.ts 用来限定范围)、以及 alwaysApply: true(每次会话都加载)。
  • **Claude Code:**写到仓库根目录的 CLAUDE.md,每次会话开始时都会被读取。内容尽量精简,它和你的代码共享上下文预算。
  • **Codex / 其他 agent:**用它各自的项目指令文件(AGENTS.md 或该工具的 system-prompt / 规则设置)。

第 4b 步:用 MCP 文档服务器喂实时文档(长期锚定的根治法)

根因是训练记忆过时。2026 年采用最广的根治办法,是给 agent 接一个实时文档源,而不是让它靠记忆硬猜。Context7 就是这样一个 MCP 服务器:它为数千个常用库抓取当前的、对应版本的文档,并通过 get-library-docs 工具按需注入上下文。在 Cursor 或 Claude Code 里配好后,提示里加一句 “use context7”(或让 agent 自行调用),模型就会按今天真实的 API 来写,而不是两年前的旧形状。

它和规则文件互补:规则告诉 agent 去查源码,MCP 服务器则把可查的源码递到它手里。

第 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(或 .cursor/rules/*.mdc 项目规则)里固化已安装包的版本,以及”写调用点前先查 .d.ts”这条硬规定。agentic 工作流不要再依赖老的 .cursorrules 文件。
  • 接一个实时文档 MCP 服务器(Context7 或同类),让 agent 按需拉取对应版本的文档,而不是凭记忆猜。
  • 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 漏看了项目上下文

外部参考:Cursor Rules 文档(当前的 .mdc 规则格式)与 Context7 MCP 服务器(为 agent 提供实时、对应版本的库文档)。

FAQ

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

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

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

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

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

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

Q:有没有一套配置能从根上避免这种情况?

最接近的方案是接一个实时文档 MCP 服务器,比如 Context7。它把当前的、对应版本的文档注入 agent 的上下文,让模型按真实 API 来写,而不是凭训练记忆。再配上规则文件里”写调用前先查源码”的指令,大多数签名幻觉在初稿阶段就不会出现了。它不是 100% 保险,所以 strict 模式和测试还得留着。

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

把 TypeScript 严格模式 + 完整测试覆盖一起跑一遍。两者都过、且运行时行为符合规格,基本就干净了。类型检查加测试能抓住绝大多数签名幻觉;剩下的通常是弱类型的选项 key——靠第 5 步的运行时断言,加上随手 console.log(JSON.stringify(result)),就能把它们揪出来。

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