AI 编程常见失败模式:不是不会写,而是边界错了

0阅读10分钟

AI 编程的失败,多半不是代码问题

AI 编程的失败看起来五花八门——样式被顺手重构、环境变量散落各处、测试文件放错目录、一个功能改出十几个文件的 diff——但根因往往在于人机协作的边界没有划对,模型本身写不出正确代码的情况反而没想象中多。

数据印证了这个判断。VentureBeat 2026 年的调查显示,43% 的 AI 生成代码变更需要在生产环境额外调试[^venturebeat];arXiv 的研究也发现,编码 Agent 在多文件协作中频繁引入回归,打破原本通过的测试[^arxiv-regression]。

更值得关注的是人在其中的角色。arXiv 2026 年 1 月的一项研究发现,48.8% 的 AI 辅助编程动作涉及认知偏差,其中 56.4% 的偏差直接产生于人与 AI 的交互过程[^arxiv-bias]——不审查就粘贴、盲目接受建议、在错误方向上反复重试。AI 编码的失败,往往是协作方式先出了问题,然后才是模型能力的问题。

把这些失败归纳起来,有四种模式会稳定出现:上下文遗漏、范围扩张、验证缺口、抽象过度。它们的共性都指向边界问题,而非代码能力。

失败模式的深层根因

这四种模式反复出现,根源在于当前 LLM 编码的结构性缺陷,而非某个模型的 bug。理解了这一点,后面的案例才有解释框架。

认知偏差:人机协作中的信任陷阱

arXiv 2026 年 1 月的论文《Cognitive Biases in LLM-Assisted Software Development》跟踪了开发者与 LLM 的协作过程,发现了一个值得关注的数据:48.8% 的编程动作涉及认知偏差,其中与 AI 交互产生的偏差占所有偏差动作的 56.4%[^arxiv-bias]。

最突出的三种偏差:

  • 即时满足偏差(Instant Gratification):19.3% 的 AI 辅助任务中出现。我们直接粘贴 AI 生成的代码而不审查,因为「它能跑」比「它是否正确」更有吸引力。
  • 建议者偏好(Suggester Preference):12.6% 的任务中出现。我们盲目接受 AI 输出,放弃了自己的判断。
  • 固着偏差(Fixation):导致 43.4% 的操作被废弃。我们在错误的方向上反复重试,不愿回退重新思考。

这些偏差让我们在人机协作中成了「共犯」——错误并非单方面来自 AI,协作的反馈回路反而放大了它。

上下文窗口限制:看不见就不存在

LLM 的上下文窗口是硬约束。即使最新的模型支持 200K+ token,在实际编码场景中,有效上下文往往远小于标称值。Inkeep 的研究发现了「上下文焦虑」现象:当模型认为自己接近上下文窗口末尾时,会走捷径、跳过关键检查、留下未完成的任务[^inkeep-context]。具体表现:

  • Agent 没有读到项目根目录的 AGENTS.md 约束文件。
  • Agent 读到了约束,但在生成代码时已经「遗忘」了窗口早期加载的内容。
  • 多文件项目中,Agent 无法同时持有所有相关文件,只能看到被检索到的片段。

训练数据偏差:「能跑」不等于「正确」

arXiv 2026 年 5 月的论文《AI-Generated Smells》发现,LLM 生成的代码有一种独特的「机器签名」式缺陷——与人类开发者犯的错误截然不同[^arxiv-smells]:

  • 人类开发者倾向于产生状态管理问题(如 Temporal Field);LLM 则倾向于产生过程膨胀(Long Method、High Cyclomatic Complexity)。
  • 更强的模型反而产生更差的代码结构——Qwen-480b 在 zero-shot 设置下产生了 11 个 Long Method smell,而人类只有 1 个。原因是大模型「试图用大量过程逻辑处理复杂的边缘情况」。
  • 功能正确 ≠ 代码质量:「能运行的代码和不能运行的代码一样可能有结构缺陷」。

这解释了为什么 AI 生成的代码经常「本地跑通了,合并后出问题」——它优化的是「看起来像正确答案」,而不是「符合工程规范的答案」。

优化目标偏差:「做得更好」的陷阱

LLM 的训练目标是生成「好的回答」,但在编码场景中,「好」的定义是模糊的。Agent 可能把「好」理解为:

  • 更多的功能覆盖(→ 范围扩张)
  • 更抽象的设计(→ 抽象过度)
  • 更快的完成速度(→ 验证缺口)
  • 更少的文件搜索(→ 上下文遗漏)

Agent 并没有在「偷懒」或「捣乱」,只是它的优化目标跟我们的工程目标对不上。

四种失败模式的完整案例

模式一:上下文遗漏

表现:代码独立看没问题,但违反了项目已有的规范、边界或约束。

失败场景

一个典型的例子:让 Agent 给文章列表加一个环境变量配置项,它写出这样的代码:

// ❌ 错误:直接在组件中读取环境变量
export function ArticleList() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL
  const { data } = useSWR(`${apiUrl}/articles`, fetcher)
  // ...
}

代码能跑。但项目的环境变量读取有统一入口——apps/src/config/env/,所有环境变量必须通过这个模块访问,以便做类型校验、默认值填充和运行时检查。Agent 没读到这条约束,或者读了却忽略了。

根因分析

Agent 的上下文中缺少项目约束,或者约束被淹没在大量代码上下文中。arXiv 研究中提到的「上下文焦虑」——Agent 在处理长上下文时丢失了早期加载的关键信息——正好对应了这种情况。

正确做法

// ✅ 正确:通过项目统一的配置模块读取
import { appConfig } from '@/config/env'
 
export function ArticleList() {
  const { data } = useSWR(`${appConfig.apiUrl}/articles`, fetcher)
  // ...
}

appConfig 模块集中管理所有环境变量的读取、校验和默认值,保证了一致性和类型安全。

验证方法

  • CI 中加入 lint 规则,禁止在 config/env/ 之外直接使用 process.env
  • 要求 Agent 在编码前先搜索项目中已有的实现模式。

模式二:范围扩张

表现:为了一个小需求改了大量无关文件,改动范围远超预期。

失败场景

需求是「修改文章排序逻辑,支持按发布时间倒序」。Agent 返回的 PR diff 显示改了 12 个文件——除了排序逻辑,还顺手重构了 ArticleCard 样式、列表布局、分页组件,新增了三个「可能有用」的排序选项,甚至改了全局 CSS 变量。

合并时觉得「整体优化挺好的」,上线后才发现:分页组件的重构破坏了 infinite scroll 逻辑,新加的排序选项在前端没有对应 UI,全局 CSS 变量改动影响了其他页面。

根因分析

Addy Osmani 在他的 LLM 编码工作流文章中指出:「模糊的 prompt 是 AI 编码最常见的错误起点」[^addy-osmani]。需求边界不清晰时,Agent 倾向于「做得更好」——它分不清「只改排序」和「整体优化列表体验」的区别。

背后还有认知偏差中的「即时满足」因素——Agent 试图一次性产出看起来更完整的方案,而不是精确地完成当前任务。

正确做法

// ✅ 正确:精确控制改动范围
// Prompt 应该这样写:
// 「修改 sortArticles 函数,增加按 publishedAt 倒序的支持。
//   只改 apps/src/utils/sort.ts 这一个文件。
//   不要改样式、布局、分页或其他组件。
//   不要添加新的排序选项。」
 
// 改动后的代码
export function sortArticles(
  articles: Article[],
  sortBy: 'createdAt' | 'publishedAt' | 'title' = 'createdAt',
  order: 'asc' | 'desc' = 'desc'
): Article[] {
  return [...articles].sort((a, b) => {
    const valA = a[sortBy]
    const valB = b[sortBy]
    if (valA < valB) return order === 'asc' ? -1 : 1
    if (valA > valB) return order === 'asc' ? 1 : -1
    return 0
  })
}

验证方法

  • 在 Prompt 中明确写出「非目标」——哪些不做。
  • 要求 Agent 先列出计划修改的文件清单,审查后再动手。
  • PR 审查时对比 diff 范围与预期范围。

模式三:验证缺口

表现:主路径能跑,但边界条件、详情页、移动端或异常状态没有验证。

失败场景

Agent 实现了一个文章搜索功能,报告「已测试,搜索正常」:

// ❌ 错误:只处理了正常路径,没有错误处理和边界检查
export async function searchArticles(query: string) {
  const res = await fetch(`/api/articles?search=${query}`)
  const data = await res.json()
  return data.articles
}

本地测试搜索「React」,结果正常。看起来没问题。但:

  • 搜索框输入空字符串 → 返回全部文章,但 UI 没有提示「显示全部」。
  • 搜索无结果 → 页面空白,没有「未找到结果」的提示。
  • API 返回 500 → 页面崩溃,因为 data.articlesundefined
  • 移动端键盘弹出 → 搜索框被遮挡。

Agent 的「已测试」只意味着它在正常输入下跑了一遍。

根因分析

LLM 的「验证」通常只覆盖 typecheck 或主路径运行。arXiv 论文《AI-Generated Smells》的发现印证了这一点:Agent 优化的是「功能正确性」,功能正确性 ≠ 工程质量[^arxiv-smells]。NextFuture 的研究也指出,AI Agent 在生产环境中最常见的失败之一就是「忽略实时系统上下文」——它只测试了本地环境中的 happy path[^nextfuture]。

正确做法

// ✅ 正确:完整的错误处理和边界检查
export async function searchArticles(query: string): Promise<SearchResult> {
  // 边界条件:空查询
  if (!query.trim()) return { articles: [], isEmpty: true, message: '请输入搜索关键词' }
 
  try {
    const res = await fetch(`/api/articles?search=${encodeURIComponent(query)}`)
 
    // 错误状态处理
    if (!res.ok)
      return { articles: [], isEmpty: true, error: `搜索失败(${res.status}),请稍后重试` }
 
    const data = await res.json()
 
    // 无结果处理
    if (!data.articles?.length)
      return { articles: [], isEmpty: true, message: `未找到「${query}」相关文章` }
 
    return { articles: data.articles, isEmpty: false }
  } catch {
    // 网络异常处理
    return { articles: [], isEmpty: true, error: '网络连接异常,请检查网络后重试' }
  }
}

验证方法

  • 验收标准前置:在 Prompt 中明确要求「处理空输入、无结果、网络异常、服务端错误四种情况」。
  • 不接受「已测试」作为结论,要求具体的验证命令和输出截图。
  • 要求 Agent 在桌面端和移动端分别验证。

模式四:抽象过度

表现:为单一场景新增通用框架、helper 或配置层。短期看很整洁,长期增加理解成本。

失败场景

Agent 为文章列表的排序功能建了一个「排序引擎」:

// ❌ 错误:只有一处排序,却抽象了完整的策略模式框架(5 个文件,300+ 行)
// 完整实现包含:
// 1. sort-strategy.interface.ts — SortStrategy<T> 接口 + SortOrder 类型
// 2. sort-context.ts — SortContext<T> 类,管理策略注册、优先级排序和调度
// 3. article-sort-strategy.ts — ArticlePublishedAtStrategy 实现类
// 4. sort-registry.ts — 策略注册表和工厂函数
 
// 以下是核心片段,展示抽象的复杂度:
export interface SortStrategy<T> {
  sort(items: T[], order: SortOrder): T[]
  canHandle(field: string): boolean
  getPriority(): number
}
 
export class SortContext<T> {
  private strategies: SortStrategy<T>[] = []
  register(strategy: SortStrategy<T>): void { /* 注册并按优先级排序 */ }
  sort(items: T[], field: string, order: SortOrder): T[] {
    const strategy = this.strategies.find((s) => s.canHandle(field))
    if (!strategy) throw new Error(`No strategy for field: ${field}`)
    return strategy.sort(items, order)
  }
}
// 整个项目只有 publishedAt 一个排序场景,这个框架永远不会被复用

5 个文件,300+ 行代码。整个项目里只有文章列表的 publishedAt 一个排序场景。这个框架永远不会被复用。

根因分析

arXiv 论文《AI-Generated Smells》的研究印证了这一现象:大模型「试图用大量过程逻辑处理复杂边缘情况,将其整合到单一代码块中」[^arxiv-smells]。Agent 在解决它想象中「一个完整的排序系统应该解决的问题」,而不是你提出的那个具体需求。

违反 YAGNI 原则(You Aren't Gonna Need It)的典型场景。Martin Fowler 在其博客中也指出,AI 生成的代码经常产生一种「模块化幻觉」——代码被拆分到多个文件,但缺乏语义内聚[^martin-fowler]。

正确做法

// ✅ 正确:YAGNI,只实现当前需要的排序逻辑
export function sortArticlesByPublishedAt(
  articles: Article[],
  order: 'asc' | 'desc' = 'desc'
): Article[] {
  return [...articles].sort((a, b) =>
    order === 'asc'
      ? a.publishedAt.getTime() - b.publishedAt.getTime()
      : b.publishedAt.getTime() - a.publishedAt.getTime()
  )
}

10 行代码,职责清晰。如果将来需要第二个排序字段,再考虑抽象——到那时你会有真实的复用需求,而不是想象中的。

验证方法

  • 在 Prompt 中明确「只实现当前需要的功能,不要为未来扩展做抽象」。
  • Code Review 时检查:如果只有一处使用,优先内联。
  • 用「三次规则」判断:同样的代码出现三次以上再考虑抽象。

失败模式识别与预防流程

流程图画布 · 115%
Mermaid 流程图加载中...

诊断速查与处理方法

失败模式典型症状一句话处理
上下文遗漏代码独立能跑,但违反项目规范(直接读 process.env、测试放错目录)把约束写进 AGENTS.md,CI 加 lint 自动拦截
范围扩张PR diff 比预期大 3 倍以上,Agent 顺手重构了不相关文件Prompt 写明「非目标」,锁定可改文件清单
验证缺口Agent 说「已测试」但浏览器发现问题,空状态和异常路径没处理前置验收标准,要求具体命令输出 + 浏览器截图
抽象过度只有一处使用,却新增了 3 个以上文件(策略模式、注册表、工厂)遵循 YAGNI,一次使用则内联,Code Review 加入检查项

关键实践对比

维度❌ 坏实践✅ 好实践
项目约束口头约定,不写文件写在 AGENTS.md / CLAUDE.md,CI 自动检查
Prompt 写法「给列表加排序」「修改 sortArticles,只改 sort.ts,不改样式和分页」
验证标准接受「已测试」要求具体命令输出 + 浏览器截图
错误处理只处理 happy path明确列出空输入、无结果、网络异常、服务端错误
Code Review只看逻辑对不对检查范围、约束、验证、抽象四个维度
环境变量process.env.XXX 散落各处统一通过 config/env/ 模块读取
测试文件放在 src/ 目录下放在 tests/ 目录下,镜像 src/ 结构

结论:管理边界,而不是管理代码

AI 编程的失败模式是稳定的。根源在于模型优化的目标与工程实践的要求之间存在结构性错位:Agent 优化「功能正确」,但工程还需要「合规」,所以需要上下文约束;Agent 倾向「做得更多」,我们只需要「精确」,所以需要范围控制;Agent 认为「跑通就算」,我们知道「跑通只是开始」,所以需要验证标准;Agent 偏好抽象设计,我们清楚「简单够用就好」,所以需要 YAGNI 原则

下次 AI 编程失败时,我会先问自己:这是上下文遗漏、范围扩张、验证缺口,还是抽象过度? 找到模式后,补充对应的约束,问题就会消失。

记住:AI 编程的核心能力在于管理边界,而非写 Prompt。


参考资料

评论 0

0 / 1000