← 文章 / AI技术
freeCodeCamp 5小时前 · 2026-10-06 02:31:49 · 8 阅读

如何打破 AI 编程 Agent 的修复死循环

你可能见过这一幕:用 AI 编程 Agent 开发的应用出现了问题。你让 Agent 修复,它声称已“修复”,但错误依然存在,甚至又冒出了第二个 bug。

于是你告诉它“还是不行”,Agent 再次尝试。二十分钟后,代码越改越乱,额度消耗殆尽,却无法回到正常状态。

人们常搜索“AI 让 bug 越修越多”或“Agent 陷入修复循环”等表述。这不是某款产品的怪癖,而是结构性失效。Lovable、Replit、Cursor、Claude Code、Base44 等工具均会出现这一模式。

本教程将解释循环发生的原因,并提供一套具体步骤,帮助你在上述任一工具中打破该循环。

内容概要:

你将学到什么

  • 如何尽早识别 AI 修复循环

  • 为什么模糊的重试会让后续尝试更糟

  • 一套五步流程:停止、回滚、重述、隔离、验证

  • 如何撰写信息充足的修复提示词,确保 Agent 成功解决

前置条件

你无需是资深工程师即可跟随本教程,但应具备以下条件:

  • 一个正在使用 AI 编程 Agent(如 Cursor、Claude Code、Replit Agent、Lovable 或类似工具)构建的应用

  • 版本历史、检查点或 Git 访问权限,以便撤销错误变更

  • 自行运行或预览应用的方式(浏览器预览、本地服务器或已部署 URL)

修复循环的表现

抛开产品品牌,所有场景中的循环模式都相同:

  1. 运行中的应用出现异常。

  2. 你让 agent 去修,只发一句简短的话("坏了"、"再试一次",或者点一下"Try to Fix"按钮)。

  3. agent 给出一个听起来很有把握的改动。

  4. 原问题还在,或者旁边的地方又坏了。

  5. 你用同样模糊的反馈再试。

  6. 每次失败的尝试都留在会话上下文里,于是下一次尝试是在一堆噪音上推理的。

不同工具的界面表现形式不同:有的真的有个重试按钮,有的一直卡在"Thinking",有的悄悄把你已经确认过的改动回滚了。表面千差万别,机制却是一样的。

为什么会出现死循环

大致按以下顺序,四种因素叠加在一起。

1. 会话越长,上下文越差

每条消息、每个 diff、每句"不对,不是这个"都会增加 agent 需要承载的 token。会话刚开始时,agent 对你应用的认知还比较清晰;聊了二十轮之后,它是在基于之前所有内容的模糊平均值在工作,包括那些走错的弯路。

2. 模糊的重试只添噪音,不给信息

"还是不行。""再试一次。""不是这样。"在你看来这是反馈,但对 agent 来说,这些指令几乎不含新信息,没说哪里还有问题、涉及哪个文件、"正确"长什么样。

于是 agent 通常只在上一次猜测的基础上稍作改动。这就是为什么死循环常常在两个几乎一样的错误修法之间来回打转,而不是逐渐收敛。

3. Agent 看不到你眼中的应用

你看到的是渲染出来的页面,实际点着操作流程走;agent 面对的只是代码,加上你对所见现象的文字描述。

用文字描述一个 UI bug 必然有损耗。当描述和真实 UI 对不上时,agent 就会去优化一个看似合理、实则修错了的问题。

4. 失败的尝试会毒害下一次尝试

正是这一点把一次错误变成了死循环。第二次尝试并不是从零开始,它的上下文里已经装着第一次的错误 diff、你气急败坏的纠正,以及 agent 对自己"修了什么"的解释。第三次又继承了这一切噪音。这个循环是在不断滚雪球,而不是随机碰运气。

综合起来就是:会话越长,模型的精确度越低,而这时你的提示恰恰越来越模糊——正处在 agent 最需要干净、具体信号的时刻。

如何打破这个循环

这些步骤都不需要切换工具,它们直击机制本身。

第一步:停止向循环供料

绝不要在短时间内连续发送"再试一次"或"还是不行"。如果第一次重试失败,问题在于 Agent 不需要盲目再尝试一次,而是你的指令缺乏足够信息来改变其输出。第三次低信息量的提示只会增加更多噪音。

当你意识到自己又要重复同样的抱怨时,停下来,转向第二步。

第二步:回退到上一个可用状态

在再次尝试之前,先撤销失败的修复(如果循环已运行多次,则撤销最近几次修复)。不要在一个损坏的基础上叠加新的尝试。这正是从一个 Bug 演变成三个 Bug 的根源。

使用工具提供的相应功能:

  • Git:git checkout -- path/to/file 或 git restore,或者重置到你信任的提交

  • Cursor / 类似 IDE:文件的本地历史或时间线

  • Replit、Lovable 及类似构建工具:检查点或版本历史,然后恢复到上一个可用状态

只有当应用回到你认可的状态时,再输入新的修复请求。

第三步:从零开始,精准界定问题

这是杠杆率最高的操作。如果工具允许,优先使用新对话或干净的线程。编写包含以下三点的提示:

  1. 确切的文件或组件名称(使用实际路径,而非视觉描述)

  2. 哪里出了问题,描述为具体可观察的事实

  3. "正确"的样子是什么,同样具体陈述

弱提示:

还是坏的。修复提交按钮。

强提示:

在 CheckoutForm.tsx 中,提交按钮调用 handleSubmit,但当请求失败时,加载状态从未重置。提交失败后按钮保持禁用状态,且不显示错误消息。它应该重新启用,并在按钮下方显示服务器错误字符串。

第二个提示为 Agent 提供了文件、行为表现和成功条件。这就足以使其在不猜测的情况下行动。

第四步:每次只改变一件事

不要在 Bug 修复提示中捆绑"顺便把页眉也修好"。这会给 Agent 两个问题,却只有一个上下文预算。对问题 A 的干净修复可能会悄悄重新引入问题 B。

提交修复,验证结果,然后针对下一个问题另开一个工单。

第 5 步:在真实应用中验证,而非轻信 Agent 的说法

Agent 常常自信满满却判断失误,因为它们检查的是自己的推理逻辑,而非你正在运行的产品。

在关闭当前循环之前:

  1. 重新加载预览或强制刷新页面

  2. 点击遍历那个失败的实际用户流程

  3. 如果 Bug 涉及数据或 API,检查数据库记录、网络标签页或日志

  4. 确认你在第 3 步中定义的验收条件

直到这一步,才算真正解决该问题。

完整演练:重复扣款

以下用一个真实的 Bug 演示这个循环。你有一个由 AI Agent 编写的小型结算接口。用户反馈网络连接慢时会被扣款两次。下面的内容只需 Node 20 且无额外依赖即可运行。

Agent 生成的代码:

// checkout.js
export function createCheckout({ chargeCard, saveOrder }) {
  async function handleCheckout(req) {
    const { cartId, amount } = req.body;

    const charge = await chargeCard(amount);
    const order = await saveOrder({ cartId, chargeId: charge.id, amount });

    return { status: 201, body: { orderId: order.id } };
  }

  return { handleCheckout };
}

当浏览器超时,客户再次点击“支付”时,服务器会第二次运行 handleCheckout,导致再次扣款。

循环过程

你告诉 Agent:“用户被重复扣款了,请修复。”

尝试 1。 Agent 在扣款前检查是否已存在订单:

const existing = await findOrder(cartId);
if (existing) {
  return { status: 200, body: { orderId: existing.id } };
}

你通过间隔几秒点击两次“支付”来测试,似乎有效。但如果两个请求同时到达,这就失效了,因为当其中一个请求检查时,另一个请求尚未保存订单。你反馈:“有时仍会重复扣款。”

尝试 2。 Agent 在首次点击后禁用支付按钮。这是客户端改动,无法阻止超时重试或来自第二个浏览器标签页的请求。你反馈:“问题依然存在。”

第三次尝试。 Agent 给扣款逻辑包上 try/catch,出错时返回一条友好的提示。现在 bug 变安静了,但卡还是被扣了两次。到这一步就该停手了。

第 1、2 步:停下来,回滚代码

别再发第四条"还是不行"。先用 Git 恢复原始的 checkout.js(git restore checkout.js),这样你调试的只是一个 bug,而不是四个。

第 3 步:在要求修复之前,先写一个会失败的测试

你能给 Agent 的最有用的东西,就是一个"因为正确的原因而失败"的测试。下面是三个测试:前两个描述这个 bug,第三个保护正常行为:

// checkout.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { createCheckout } from "./checkout.js";
import { createFakes } from "./fakes.js";

const req = (key) => ({
  headers: { "idempotency-key": key },
  body: { cartId: "cart_1", amount: 4900 },
});

test("a retried request charges the card only once", async () => {
  const fakes = createFakes();
  const { handleCheckout } = createCheckout(fakes);

  const first = await handleCheckout(req("key-1"));
  const retry = await handleCheckout(req("key-1")); // client timed out and retried

  assert.equal(fakes.charges.length, 1);
  assert.equal(fakes.orders.length, 1);
  assert.deepEqual(retry.body, first.body);
});

test("two requests sent at the same moment still charge once", async () => {
  const fakes = createFakes();
  const { handleCheckout } = createCheckout(fakes);

  await Promise.all([handleCheckout(req("key-2")), handleCheckout(req("key-2"))]);

  assert.equal(fakes.charges.length, 1);
});

test("different keys are different purchases", async () => {
  const fakes = createFakes();
  const { handleCheckout } = createCheckout(fakes);

  await handleCheckout(req("key-3"));
  await handleCheckout(req("key-4"));

  assert.equal(fakes.charges.length, 2);
});

这些测试用轻量的内存 fake 模拟支付服务商和数据库,运行只需几毫秒:

// fakes.js
export function createFakes() {
  const charges = [];
  const orders = [];

  return {
    charges,
    orders,
    async chargeCard(amount) {
      await new Promise((r) => setTimeout(r, 10)); // simulate a slow provider
      const charge = { id: `ch_${charges.length + 1}`, amount };
      charges.push(charge);
      return charge;
    },
    async saveOrder(data) {
      const order = { id: `ord_${orders.length + 1}`, ...data };
      orders.push(order);
      return order;
    },
  };
}

运行 node --test。前两个测试会失败,这正是 Bug 存在的证据:

not ok 1 - a retried request charges the card only once
not ok 2 - two requests sent at the same moment still charge once
ok 3 - different keys are different purchases

注意,第二个测试恰好描述了第一次尝试所遗漏的场景。Agent 无法察觉到该情况,但测试可以。

步骤 3 & 4:给 Agent 一个精准的提示词,且仅做一处修改

开启一个新会话并粘贴以下内容:

在 checkout.js 中,handleCheckout 每次被调用都会扣款,导致客户端重试时用户被重复扣款。请使用 Idempotency-Key 请求头使其具备幂等性:相同的 Key 必须产生一次扣款和一笔订单,即便两个携带相同 Key 的请求同时到达也是如此,且必须返回相同的响应体。未携带 Key 的请求应返回状态码 400。不要修改 fakes.js 或测试。运行 node --test 并展示输出结果。

该提示词明确了文件和函数,阐述了错误行为与正确行为,涵盖了并发场景,并要求只做一处修改。

修复方案

// checkout.js
export function createCheckout({ chargeCard, saveOrder }) {
  const requests = new Map(); // 幂等键 -> 响应 Promise

  async function process(req) {
    const { cartId, amount } = req.body;

    const charge = await chargeCard(amount);
    const order = await saveOrder({ cartId, chargeId: charge.id, amount });

    return { status: 201, body: { orderId: order.id } };
  }

  async function handleCheckout(req) {
    const key = req.headers["idempotency-key"];
    if (!key) {
      return { status: 400, body: { error: "缺少 Idempotency-Key 请求头" } };
    }

    if (!requests.has(key)) {
      const promise = process(req);
      requests.set(key, promise);
      promise.catch(() => requests.delete(key)); // 失败请求允许重试
    }

    return requests.get(key);
  }

  return { handleCheckout };
}

核心在于 Map 存储的是 Promise,而非最终结果。相同键的第二个请求会拿到同一个正在执行的 Promise,从而等待首次扣款完成,而不是发起新扣款。正是这一步补上了并发场景下第一次尝试的疏漏。

第 5 步:验证

ok 1 - 重试请求只扣款一次
ok 2 - 同一时刻的两个请求也只扣款一次
ok 3 - 不同键对应不同交易
# tests 3
# pass 3
# fail 0

也要实测真实场景:用 curl 发送两次相同请求并携带相同的 Idempotency-Key,确认支付服务商后台只记录了一笔扣款。

注意:此版本将键保存在内存中,仅能保护单个服务器进程。生产环境应将键存入数据库或 Redis 并设置过期时间。测试无需改动,可让 agent 作为独立需求执行此修改。

本例说明

修复代码仅 12 行。打破循环的关键不在于更巧妙的提示词,而在于失败的测试。它将"偶尔仍扣两次款"转化为 agent 可读的结果,并抓住了所有模糊重试都未覆盖的边界场景。

可复用的检查清单

复制以下清单应对下次事故:

  • [ ] 我仅重试一次失败请求即停止

  • [ ] 我回滚到已知正常状态

  • [ ] 我指定了具体文件或组件

  • [ ] 我将错误行为描述为可观测事实

  • [ ] 我把正确行为描述成了可观察的事实

  • [ ] 我只要求修改一处

  • [ ] 我自己在真实界面 / 数据 / 日志中验证过了

结语

AI coding agent 很擅长做出应用的第一版,但迭代就难多了——因为一个高质量的修复请求需要足够的精确度,一句气急败坏的“还是不行”提供不了,而且 agent 看不到你的屏幕,无法像你一样了解情况。

陷入修复循环并不代表你在深层次上“用错了工具”,它只是在提示你:prompt 需要比“再试一次”更多的信息。

下次当你连续修了三次还没修好时,停下来,回退,然后从头重新描述问题,明确范围。一开始感觉更慢,但总比陷入死循环快。

如果你想看一份针对具体工具的简明实操版指南,我在 Vibe Coder Daily 上也发过一篇实战文章。

原始来源: freeCodeCamp

评论 (0)