page contents
侧边栏壁纸
博主头像
seabell-贝海运维站-分享技术干货与行业动态

残雪凝辉冷画屏,落梅横笛已三更,更无人处月胧明

  • 累计撰写 27 篇文章
  • 累计创建 5 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

How to Fix Any Bug 如何修复任何漏洞

seabell
2025-11-12 / 0 评论 / 0 点赞 / 22 阅读 / 0 字

我一直在对一个小应用程序进行振动编码,几天前我遇到了一个错误。

这个错误是这样的。想象一下 Web 应用程序中的一条路由。该路由显示一系列步骤——本质上是卡片。每张卡片都有一个按钮,可以向下滚动到下一张卡片。一切都很好。但是,一旦我尝试从该按钮调用服务器,滚动就不再有效。它会抖动和中断。

因此,添加远程呼叫会以某种方式破坏滚动。

我不确定是什么导致了这个错误。显然,新添加的远程服务器调用(我通过 React Router作执行)以某种方式干扰了我的 scrollIntoView 调用。但我不知道是怎么回事。我最初认为问题在于 React Router 重新渲染我的页面(一个作会导致数据重新获取),但原则上没有理由重新获取会干扰正在进行的滚动。服务器返回相同的项目,因此它不应该更改任何内容。

在 React 中,重新渲染应该始终是安全的。其他问题——要么在我的代码中,要么在 React Router 中,要么在 React 中,甚至在浏览器本身中。

如何修复此错误?

我可以让 Claude 修复它吗?


第 0 步:只需修复它

我告诉克劳德解决这个问题。

Claude 尝试了一些方法。它重写了包含调用的条件,并说错误已修复。但这无济于事。然后它尝试将滚动更改为 ,以及其他一些事情。useEffectscrollIntoViewsmoothinstant

每次,克劳德都会自豪地宣布问题已经解决。

但事实并非如此!

虫子还在那里。

这听起来像是在抱怨 Claude,但真正写这篇文章的动力是我看到人类工程师(包括我自己)也犯了同样的错误。所以我想记录我通常遵循的修复错误的过程。

为什么克劳德一再犯错?

Claude 一再犯错,因为它没有复制


第 1 步:查找复制品

重现或重现案例是一系列指令,然后遵循后,为您提供一种可靠的方法来判断错误是否仍在发生。这是“测试”。重现说明要做什么、预期会发生什么以及实际发生什么。

从我的角度来看,我已经有一个很好的重现:

  1. 单击按钮。

  2. 预期行为是向下滚动,但实际行为是滚动抖动。

更好的是,这个错误每次都发生。

如果我的重现不可靠(例如,如果它只发生了 30% 的尝试),我要么必须逐渐消除不同的不确定性来源(例如,记录网络并在未来的尝试中模拟它),要么忍受必须多次测试每个潜在修复的生产性打击。但幸运的是,我的重现是可靠的。

然而,对克劳德来说,我的复制品基本上不存在。

问题是,我的重现中的“滚动抖动”对 Claude 来说没有任何意义。Claude 没有眼睛或其他方式来直接感知抖动。因此,Claude 基本上是在没有重现的情况下运行的——它试图修复错误,但没有做任何具体的事情来验证它。这太常见了,即使对于我们中最好的人来说也是如此。

在这种情况下,Claude 不可能完全遵循我的复制,因为它无法“看”屏幕(拍几张屏幕截图不会捕获它)。因此,如果我想让 Claude 修复它,我的第一个复制是不合适的。这似乎是 Claude 的问题,但在与其他人合作时实际上并不少见——有时错误只发生在一台机器上,或者针对特定用户,或者使用特定设置。

幸运的是,有一个技巧。您可以用一个重现换取另一个重现,只要您能够说服自己这将帮助您在原始问题上取得进展。

下面介绍了如何更改重现,以及一些需要注意的事项。


第 2 步:缩小重现范围

更改您正在使用的重现始终是一种风险。风险在于,新的重现与您的原始错误无关,解决它是浪费时间。

然而,有时更改复制品是不可避免的(Claude 无法查看我的屏幕,所以我必须想出其他方法)。有时它对迭代非常有益(例如,需要 10 秒的重现比需要 10 分钟的重现更有价值)。因此,学习更改复制品很重要。

理想情况下,你会将重现换成更简单、更窄、更直接的重现。

这是我向 Claude 建议的想法:

  1. 测量文档滚动位置。

  2. 单击按钮。

  3. 再次测量文档滚动位置。

  4. 预期行为是存在增量,实际行为是没有。

我的想法是,这似乎与我亲眼看到的问题大致相当。尽管此重现不会捕获抖动,但无法向下滚动可能与此有关。即使这不是唯一的问题,也值得自行解决。

Claude 添加了一些 s,通过 Playwright MCP 打开页面,然后点击。事实上,尽管单击了按钮,滚动位置并没有改变。console.log

好的,现在 Claude 能够验证错误是否存在!

我们找到重现了吗?

其实,我们不是!

缩小重现范围的一个常见陷阱是,你认为你找到了一个好的重现,但实际上你的新重现捕获了一些以类似方式呈现的不相关的问题。这是一个代价高昂的错误,因为您可能会浪费数小时来寻找与您想要解决的问题不同的问题的解决方案。

例如,Claude 可能只是过地读取了滚动位置,即使错误得到了修复,它仍然会“看到”位置不变。这将非常具有误导性——即使进行了正确的修复,测试也会说它仍然存在错误,而 Claude 会错过正确的修复!这也发生在人类工程师身上。

这就是为什么每当您缩小重现范围时,您还应该确认使用新的重现仍然可以获得积极的结果(“一切正常”)。

这更容易通过一个例子来解释。

我告诉 Claude 注释掉网络调用(最初是浮出水面的错误)。如果新的重现(“测量滚动,点击按钮,再次测量滚动”)确实捕获了我想要修复的错误(“单击时滚动抖动”),我们应该期待我已经验证过的更改来修复该错误(注释掉作调用)以修复新重现中的行为(滚动位置现在应该不同)。

事情就这样发生了!事实上,暂时注释掉网络调用也修复了 Claude 正在执行的测试——滚动位置现在不同了。

此时,值得尝试在任一方向上更改代码几次 (注释中,注释掉) ,以验证每次编辑是否预测了新的重现结果。(也值得进行其他编辑以排除每两次编辑都有效。

滚动测量似乎仍然与该网络调用相关。

这仍然不意味着新的重现代表相同的问题。它可能是不相关的东西,或者只是其中的一部分。更改重现始终存在风险。但是,与滚动位置有关的网络调用至少与原始问题(滚动抖动)一样可疑,并且值得自行解决。这是继续使用新重现的充分理由。


第 3 步:删除其他所有内容

我创建了一个新分支,并要求 Claude 遵循以下工作流程:

  1. 运行重现以验证是否存在 bug。

  2. 从相关代码中删除某些内容(删除组件、删除事件处理程序、简化条件、删除样式、删除导入等)。

  3. 再次运行重现以验证错误是否仍然存在。

  4. 如果错误仍然存在,请提交更改。

  5. 如果错误存在,请写下一个关于可能“解决它”的理论,然后重置为最后一次提交并尝试删除较小的块。

我正要走出去,所以我告诉 Claude 继续做下去,不要休息,直到它把重现范围缩小到无法进一步减少的东西——一个没有额外库(甚至没有 React Router)和最小逻辑的 React 组件。

当我回来时,Claude 为我创建了一些复制案例,但令人沮丧的是,它们都没有表现出错误:

⏺ I've done extensive investigation and created three progressively more
  accurate minimal reproductions. All of them work correctly, but the real
  page fails. I've tested:

  - Plain React state updates
  - React Router revalidation
  - Component remounting
  - New object references
  - Synchronous loaders
  - Various dependency arrays
  - Multiple renders during scroll

  Everything I can think of has been tested and ruled out. The bug
  consistently reproduces in the real app but not in any repro. This
  suggests there's something very specific about the page
  environment that I haven't captured.

  The workaround (deferring network call with setTimeout) works reliably,
  which confirms that the issue is the timing between the
  mutation/revalidation and the scroll. But I cannot determine WHY React
  Router's revalidation breaks scrollIntoView in the real app when it
  doesn't in the repros.

  I've committed all the repros and investigation notes for future
  reference.

这是否意味着缩小错误范围并不总是有效?

不。

这意味着克劳德没有听从我的指示。但它未能遵循它们的方式很有趣,因为人们(包括我)经常犯同样的错误。

当克劳德简化代码时,它开始形成理论。也许这个效果有问题。也许与重新安装有关。也许 React 在做一些奇怪的事情。它开始测试这些理论,创建专注于它们的孤立复制案例,看看它们是否表现出错误。

创建理论并测试它们很棒!我们绝对应该这样做。

但是再看看我的说明:

  1. 运行重现以验证是否存在 bug。

  2. 从相关代码中删除某些内容(删除组件、删除事件处理程序、简化条件、删除样式、删除导入等)。

  3. 再次运行重现以验证错误是否仍然存在。

  4. 如果错误仍然存在,请提交更改。

  5. 如果错误存在,请写下一个关于可能“解决它”的理论,然后重置为最后一次提交并尝试删除较小的块。

我试图让它做一些具体的事情。我们试图确保的是,在每个时间点,我们都有一个检查点,其中错误仍在发生,并且每一步,我们都在减少该错误的表面积。

Claude 太过专注于测试自己的理论,最终得到了一堆实际上没有表现出错误的测试用例。同样,测试新理论并不是一个坏主意,但如果它们失败了,正确的做法是回到原来的情况(这仍然会消除错误!)并不断删除东西,直到我们找到原因。

这让我想起了有根据的递归的概念。考虑一下实现一个应该计算斐波那契数列的函数的尝试:fib(n)

function fib(n) {  if (n <= 1) {    return n;  } else {    return fib(n) + fib(n - 1);  }}

实际上,这个函数有错误——它将永远挂起。错误地,我写了 ,而不是 ,所以会调用 ,它会调用 ,依此类推。它永远不会摆脱递归,因为永远不会“变小”。fib(n)fib(n - 2)fib(n)fib(n)fib(n)n

理解有根据的递归的语言不会让我犯这个错误。例如,在精益中,这将是一个类型错误

def fib (n : Nat) : Nat := /- error: fail to show termination for fib -/  if n ≤ 1 then    n  else    fib n + fib (n - 2)

精益知道“不会变小”(更准确地看这里),所以它知道这个递归将永远挂起。它不会“随着时间的推移而接近”。n

这不是一个精益教程,但我希望你能原谅这个轻浮的比喻。

我认为减少重现案例的过程也是如此。你想知道你总是在不断取得渐进式进展,而重现却在不断变小。这意味着您必须保持纪律并一点一点地删除碎片,只有在错误仍然存在时才提交。在某些时候,你必然会用完要删除的东西,这要么是代码中的错误,要么是你无法进一步减少的错误(例如 React)。

重复直到找到它。


第 4 步:找到根本原因

克劳德最终没有解决这个问题,但它让我非常接近。

在我告诉它实际按照我的指示进行作,并且只删除内容后,它删除了足够多的代码,使问题包含在单个文件中。我将该文件移至路由器之外,突然相同的代码起作用了。然后我把它移回路由器,它又坏了。然后我把它做成一个顶级路线,它奏效了。

当它嵌套在根布局中时,某些东西正在损坏。

我的根布局是这样的:

import { Outlet, ScrollRestoration } from "react-router-dom"; export function RootLayout() {  return (    <div>      <ScrollRestoration />      <Outlet />    </div>  );}

啊哈。事实证明,曾经有一个错误(已经在 6 月修复),导致 React Router 在每次重新验证时激活,而不是在每次路由更改时激活。由于我的网络调用(通过作)重新验证了路由,因此它在 期间触发,导致抖动。ScrollRestorationScrollRestorationscrollIntoView

这个确切的工作流程——一个一个地删除东西,同时确保错误仍然存在——多次拯救了我的屁股。(我曾经花了一周时间删除了 Facebook 的一半 React 树来追踪一个错误。最终的重现是 ~50 行代码。我不知道还有什么方法在你用完理论后如此有效。

如果我自己设置项目,我会使用最新版本的 React Router,并且不会遇到这个错误。但该项目是由 Claude 建立的,出于某种莫名其妙的原因,他决定我应该使用旧版本的核心依赖项。

0

评论区