如何打破人工智能编程代理的循环机制
你很可能已经看过这样的情景:在使用人工智能编码工具构建的应用程序中,出现了某些故障。你让这个工具去修复这些问题,它似乎也“修复”了它们,但实际上错误依然存在,或者又出现了新的问题。 于是你会说“还是不行”。工具会再次尝试修复。二十分钟后,你会发现代码中的错误更多了,可用的功能也更少了,而且完全不知道该如何让应用程序恢复到正常状态。 人们常常用“人工智能反而使问题更加严重”或者“工具陷入了无限循环的修复模式”这样的表述来描述这种情况。这并不是某个特定产品的缺陷,像Replit、Cursor、Claude Code、Base44这类工具都会出现同样的问题,因为这种故障的本质是系统性的。 本教程将解
你很可能已经看过这样的情景:在使用人工智能编码工具构建的应用程序中,出现了某些故障。你让这个工具去修复这些问题,它似乎也“修复”了它们,但实际上错误依然存在,或者又出现了新的问题。
于是你会说“还是不行”。工具会再次尝试修复。二十分钟后,你会发现代码中的错误更多了,可用的功能也更少了,而且完全不知道该如何让应用程序恢复到正常状态。
人们常常用“人工智能反而使问题更加严重”或者“工具陷入了无限循环的修复模式”这样的表述来描述这种情况。这并不是某个特定产品的缺陷,像Replit、Cursor、Claude Code、Base44这类工具都会出现同样的问题,因为这种故障的本质是系统性的。
本教程将解释为什么会出现这种循环现象,并提供一种具体的操作步骤,帮助你在这些工具中打破这种循环。
我们将涵盖以下内容:
你将学到什么
如何及早发现人工智能工具陷入的修复循环
为什么含糊不清的反馈会使得下一次尝试更加失败
一个包含五个步骤的方法:停止当前操作、回退到之前的状态、重新描述问题、定位具体故障位置,然后进行验证
如何编写能够提供足够信息、帮助工具成功解决问题的指令
先决条件
你不需要是专业工程师才能跟随本教程学习。但你需要具备以下条件:
一款正在使用人工智能编码工具构建的应用程序(比如Cursor、Claude Code、Replit Agent、Lovable之类的工具)
能够访问应用程序的版本历史记录、检查点信息,或者熟悉Git系统,这样就可以在出现问题时撤销错误的修改
有一种方法可以自己运行或预览这个应用程序(比如通过浏览器、本地服务器,或者已部署的URL来查看)
修复循环的具体表现形式
如果去掉这些工具的品牌标识,你会发现这种循环在所有情况下都是相同的:
正在运行的应用程序中出现了某些故障。
你让人工智能工具去修复这些问题,可能会给出像“程序出错了”或者“再试一次”这样的简单指令,也可以点击“尝试修复”按钮。
工具会生成一些修改代码的指令,看起来似乎很有效果。
但原来的问题依然存在,或者又出现了其他新的故障。
你再次使用类似的反馈让工具尝试修复,结果还是失败。
每次失败的尝试都会留在之前的对话记录中,因此下一次尝试时会继续在这些错误的信息基础上进行操作,从而导致更多的问题。
不同的工具会通过不同的用户界面来呈现这一现象。有些工具会提供一个明确的“重试”按钮,有些则会显示“正在处理中”的提示,而还有一些则会悄悄地撤销你已经确认的修改。表面上的表现可能有所不同,但背后的机制其实是一样的。
为什么会出现这种循环现象
大致上有四种因素在共同作用下导致了这一现象的发生。
1. 随着会话的进行,上下文信息会逐渐退化
每一条消息、每一次差异对比,以及那些“不对,并非如此”的反馈,都会为智能体需要处理的信息量增加负担。在会话刚开始时,智能体对你所使用的应用程序的理解还相对清晰;但经过二十次交互之后,它所依据的就已经是所有发生过的事件的模糊平均值了,其中当然也包括那些错误的操作。
2. 模糊的重试操作只会增加干扰,而不会提供有用的信息
“还是不行。”“再试一次。”“那个不对。”这些对你来说可能是反馈信息,但对智能体而言,它们只不过是一些几乎没有新信息的指令。这些指令并没有说明到底哪里出了问题,也没有指出正确的解决方案应该是什么。
智能体通常会稍微调整它之前的猜测结果,因此循环往往会在两个几乎相同的错误修复方案之间反复切换,而无法达成正确的解决方案。
3. 智能体无法像你一样理解你的应用程序
你看到的是一个已经渲染好的页面,并且是通过点击按钮来依次执行各项操作的;而智能体则是通过分析代码以及你对所看到内容的文字描述来进行推理的。
用文字来描述用户界面的缺陷本身就会导致信息丢失。当描述与实际的界面情况不一致时,智能体就会尝试针对一个错误的问题来提供解决方案,从而导致错误持续发生。
4>失败的尝试会干扰下一次的尝试
正是这一点使得一个错误会演变成一个循环。第二次尝试并不会从零开始,而是会基于上一次尝试中出现的错误、你进行的纠正操作,以及智能体对自己所做修改的理解来继续进行。第三次尝试就会继承所有这些错误信息,因此这个循环是不断累积的,而不是随机发生的。
综合来看:随着会话时间的延长,你的指令会变得越来越模糊,而这个时候智能体恰恰最需要清晰、具体的信息来进行正确的操作。
如何打破这种循环
这些方法都不需要更换工具,它们直接针对导致循环现象的机制进行干预。
步骤1:停止为这个循环提供输入信息
永远不要连续两次发送“再试一次”或“还是不行”的指令。如果第一次重试失败了,那问题并不在于智能体还需要再进行一次盲目的尝试——你的指令中缺乏足够的信息来改变它的判断结果。第三次仍然包含少量信息的提示只会让情况更加混乱。
当你发现自己即将再次输入同样的反馈信息时,立刻停止输入,然后进入下一步操作。
步骤2:恢复到上一次正确的状态
在再次尝试之前,先撤销之前失败的修复操作(如果循环已经持续了一段时间,那么就需要撤销之前的几次修复操作)。不要在已经出错的基础上再进行新的尝试,否则错误只会越来越多。
使用工具所提供的任何功能即可:
Git:使用`git checkout -- path/to/file`或`git restore`命令,或者恢复到某个你信任的提交版本
文本编辑器/类似的集成开发环境:这些工具通常会提供文件的本地历史记录或时间线功能
Replit、Lovable等代码构建工具:可以查看检查点或版本历史记录,然后恢复到最新的正常状态
只有当应用程序恢复到你能够识别的正常状态之后,才应该提交新的修复请求。
步骤3:明确问题范围,从头开始重新描述问题
这是最有效的方法。当工具允许时,最好进行全新的讨论或创建一个新的讨论主题。编写描述时,请务必包含以下三部分内容:
文件或组件的具体名称(请提供准确的路径,而不是视觉上的描述)
问题具体表现为哪些可观察到的现象
正确的状态应该是怎样的,也需要用具体的方式描述出来
示例1:模糊的描述:
程序还是无法正常运行。请修复提交按钮的问题。
示例2:明确的描述:
在`CheckoutForm.tsx`文件中,提交按钮会调用`handleSubmit`函数,但当请求失败时,加载状态并不会恢复。请求失败后,按钮会保持禁用状态,且不会显示任何错误信息。应该修复这个问题,使按钮能够重新启用,并在按钮下方显示服务器返回的错误信息。
示例2中的描述为处理问题提供了足够的信息——具体的文件名、出现的问题以及成功的判断标准,这样处理者就可以直接采取行动,而无需进行猜测。
步骤4:一次只修改一个部分
不要在修复请求中同时提出“顺便也修复一下头部内容”这样的要求。这样做会导致处理者在有限的资源下同时处理两个问题,结果可能会使第一个问题的修复反而引发第二个问题。
先完成一个问题的修复,然后进行验证。之后再为下一个问题提交单独的修复请求。
步骤5:根据真实应用程序进行验证,而不是根据处理者的描述
处理问题时,处理者往往对自己的修复是否有效充满信心,但实际上他们只是检查了自己的推理过程,并没有使用你的实际运行中的产品来进行测试。
在完成问题的修复之前,请务必进行以下操作:
重新加载预览页面或强制刷新页面
按照实际用户会遇到的流程,逐一测试相关功能
如果问题与数据或API有关,那么请检查数据库记录、网络请求日志等相关信息
确认你在步骤3中描述的成功判断条件是否成立
只有满足所有这些条件后,才能认为这个问题已经得到解决。
完整流程示例:双重收费问题
以下是一个真实存在的问题的处理流程。某个AI处理程序编写了一个简单的结账接口,但客户反映在网络连接速度较慢的情况下会被重复收费两次。你可以使用Node 20以及没有任何依赖库来运行下面的测试步骤。
由AI处理程序编写的代码:
// 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函数,从而重新进行扣款操作。
循环现象
你向技术人员反映:“顾客被收取了两次费用,请解决这个问题。”
第一次尝试:技术人员在扣款之前会先检查是否已有订单存在:
const existing = await findOrder(cartId);
if (existing) {
return { status: 200, body: { orderId: existing.id } };
}
你通过间隔几秒再次点击“付款”按钮来测试这个机制,结果发现它确实有效。但是当两个请求同时到达服务器时,这个问题仍然存在,因为在其中一个请求被处理之前,另一个请求就已经开始进行了扣款操作。你再次向技术人员报告:“有时候还是会出现两次扣款的情况。”
第二次尝试:技术人员在第一次点击“付款”按钮后禁用了该按钮。不过这种修改仅适用于客户端,因此无法阻止因浏览器超时或打开另一个标签页而导致的重试请求。你仍然反馈说:“这个问题依然存在。”
第三次尝试:技术人员将扣款操作放在try/catch块中,以便在出现错误时能够向用户显示友好的提示信息。虽然这种修改使得问题不再那么明显,但顾客仍然会被收取两次费用。此时,我们应该停止进一步的调试了。
步骤1与步骤2:停止当前操作并恢复原始状态
不要再次提交第四次反馈,说明问题仍未解决。应该从Git中恢复原来的checkout.js文件(使用命令git restore checkout.js),这样你就可以专注于修复一个错误,而不是四个错误。
步骤3:在请求修复之前编写测试用例
向技术人员提供的最有用的信息就是那些能够准确反映问题本质的测试用例。以下是三个测试用例:前两个用于描述存在的问题,第三个则用于验证系统的正常行为:
// 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("重试请求只会被执行一次扣款操作", async () => {
const fakes = createFakes();
const { handleCheckout } = createCheckout(fakes);
const first = await handleCheckout(req("key-1"));
const retry = await handleCheckout(req("key-1")); // 客户端超时后重新发送请求
assert.equal(fakes.charges.length, 1);
assert_equal(fakes.orders.length, 1);
assert.deepEqual(retry.body, first.body);
});
test("同时发送的两个请求仍然只会被执行一次扣款操作", 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("使用不同的键进行请求时,系统会分别处理两次购买操作", async () => {
const fakes = createFakes();
const { handleCheckout } = createCheckout(fakes);
await handleCheckout(req("key-3"));
await handleCheckout(req("key-4"));
assert.equal(fakes.charges.length, 2);
});
这些测试使用了模拟的支付提供商和数据库数据,在内存中存储这些模拟数据,因此测试执行速度非常快,仅需几毫秒即可完成:
// fakes.js
export function createFakes() {
const charges = [];
const orders = [];
return {
charges,
orders,
async chargeCard(amount) {
await new Promise((r) => setTimeout(r, 10)); // 模拟缓慢的支付处理过程
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 命令。前两项测试会失败,而这正是证明存在缺陷的证据:
错误结果 1:重试请求仅会收取一次费用
错误结果 2:同时发送的两个请求仍然只会被计费一次
正确结果 3:不同的键代表不同的购买记录
需要注意的是,第二项测试恰恰揭示了第一项测试所忽略的问题。代理程序无法检测到这种情况,但测试却能够发现它。
步骤 3 & 4:为代理程序提供明确的提示,并要求进行一项修改
启动一个新的聊天对话,然后粘贴以下内容:
在
checkout.js文件中,handleCheckout函数每次被调用时都会从客户的卡中扣款,因此如果客户重新尝试支付,系统会再次扣费。为了解决这个问题,可以使用Idempotency-Key请求头来实现功能幂等性:相同的键必须只会生成一次扣款记录和一条订单信息;即使同时收到两个带有相同键的请求,系统也必须返回相同的响应结果。如果没有提供这个键,系统应该返回状态码 400。请不要修改fakes.js文件或相关的测试代码,再次运行node --test命令,并将输出结果展示给我看。
这条提示指出了存在的问题以及正确的处理方式,同时也考虑到了并发请求的情况,并要求进行一项修改。
修复方案
// checkout.js
export function createCheckout({ chargeCard, saveOrder }) {
const requests = new Map(); // 使用 “Idempotency-Key” 来确保功能幂等性
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 – 不同的密钥代表不同的购买操作
# 测试项3
# 通过3项测试
# 失败0项
接下来,你也需要亲自进行验证:使用curl并相同的Idempotency-Key再次发送相同的请求,确认你的支付提供商的监控界面确实只显示了一次收费记录。
需要注意的一点是:当前这个版本会将密钥保存在内存中,因此它只能保护单个服务器进程。在实际生产环境中,你应该将密钥存储在数据库或Redis中,并为它们设置过期时间。由于测试代码本身没有变化,你可以要求代理在未来通过单独的请求来修改这一设置。
这个操作指南展示了什么
实际上,修复这个问题的代码只有十二行而已。导致循环无法正常运行的原因并不是提示信息不够智能,而是那些失败的测试用例——它们将“有时仍然会重复收费”这一现象转化成了代理能够理解的结果,从而揪出了之前所有模糊的尝试都未能发现的错误。
一个可以重复使用的检查清单
可以将这份清单复制下来,以便在下次遇到类似问题时使用:
[ ] 在第一次失败的尝试后立即停止操作
[ ] 将系统恢复到已知正常的状态
[ ] 明确指出出现问题的具体文件或组件
[ ] 将错误行为描述为一个可观察到的现象
[ ] 将正确行为描述为一个可观察到的现象
[ ] 只要求进行一次修改
[ ] 自己亲自在真实的用户界面、数据或日志中验证结果
结论
AI编码代理在帮助创建应用程序的初始版本方面表现得非常出色。但进行迭代修改却较为困难,因为一个有效的修复请求需要精确性,而那些抱怨“系统仍然出问题”的用户往往无法提供这种精确的信息;此外,代理也无法像人类一样看到你的屏幕显示内容。
修复问题的这个循环过程并不能证明你在使用这个工具时存在根本性的错误。它只是说明当前的提示信息还需要包含更多有用的信息,而不仅仅是“再试一次”这么简单。
下次当你尝试了三次仍然无法解决问题时,请停止当前的操作,将系统恢复到正常状态,然后从头开始、明确限定修改范围再进行尝试。虽然这种方式一开始会让人感觉效率较低,但实际上它比陷入无限循环要高效得多。
如果你希望了解如何在特定工具中应用这种解决问题的方法,我还在Vibe Coder Daily上发表了一篇实用的文章。
相关文章
如何向英国税务海关总署的“数字化纳税”API提交季度更新报告
每年有四次,所有参与“税收数字化”计划的个体经营者和房东都必须向英国税务海关总署提交一份收入与支出的汇总报表。 2026-27纳税年度的第一个截止日期是8月7日,这个期限已经过去;第二个截止日期则是11月7日。每份汇总报表都需要通过软件向英国税务海关总署发送一次API请求,而本教程正是专门讲解如何正确完成这些请求操作的。 您无需事先阅读任何其他资料即可跟随本教程进行操作。每次进行“税收数字化”集成时所需的一次性设置信息(包括沙箱应用程序、OAuth 2.0访问令牌以及防欺诈相关配置)已在下方的简要总结中列出,同时每段代码示例中也都会使用到一些辅助函数。 如果您想深入了解这些设置步骤,我曾在 之
阅读全文
如何使用 Next.js 和 Jev 构建一个能够自动将错误信息发送到 GitHub 的人工智能支持系统
每个网站都会收到用户的反馈,而其中大部分反馈最终都会被搁置在某个不太合适的地方。有的访客会发现某个按钮无法正常使用,然后会通过电子邮件联系你;还有人会在社交媒体上留言,反映他们的手机无法打开某个页面;再有人则会填写你的联系表格,提出一些功能改进的建议,而这些建议就会和新闻通讯、收据之类的信息一起堆在你的收件箱里。 当你终于坐下来准备处理这些反馈时,你会发现那些错误报告分散在三个不同的地方。其中有一半的报告缺少必要的详细信息,而那些被上传到GitHub上的报告,也是有人手动复制过去的,有时甚至还会把访客的电子邮件地址也粘贴在里面。 因为我想为自己的项目提供更好的支持系统,所以我决定自己动手开发它
阅读全文
如何利用Claude API构建一个可靠的人工智能助手
大型语言模型能够回答问题、总结文档、编写代码,还能与外部系统进行交互。但构建一个可靠的人工智能应用,仅仅发送指令并显示响应是远远不够的。 一个可用于实际生产的应用程序必须能够管理对话历史记录、提供相关的上下文信息、安全地使用各种工具、处理不同类型的响应,并判断生成的内容是否有用。 在本教程中,我们将构建 ShopHelper ——这个为虚构在线商店设计的客户支持助手。完成制作后,ShopHelper将能够做到以下几件事: 以统一的语气回答常见问题 记住顾客之前说过的话 通过调用代码中的函数来查询订单状态 安全地处理Claude给出的多段式响应 利用工作流程来处理客户支持请求 评估修改提示语后效
阅读全文
如何使用NestJS的观察功能:开发者必备的可观测性实践指南
可观测性并不是一个新出现的问题,NestJS也绝不是第一个试图解决这个问题的生态系统。 但NestJS Observe之所以值得关注,是因为它提出了一个更为具体的问题:当可观测性系统能够理解其所监控的框架时,会发生什么呢? 在了解其实现机制之前,我们首先需要明确这个问题,以及为什么框架的上下文会对此产生重要影响。 可观测性问题 如果你曾经长期开发并维护过服务器应用程序,你肯定遇到过这样的情况:某个API莫名其妙地运行速度变慢了,但你却不知道原因何在。 你检查了日志,却发现没有任何明显的异常。数据库运行正常,也没有任何错误信息;在你的本地机器上,这个接口也能正常工作。 于是你添加了一些日志记录:
阅读全文