Claude 工作流实验室
独立第三方实践站 · 非 Anthropic 官方;域名中的 6 不代表官方模型版本独立中文指南
任务拆解

用 Claude 修复搜索结果乱跳:给异步请求加最新结果保护

构造旧请求后返回的确定性案例,用请求序号保护界面,并检查清空输入和过期错误这两个容易遗漏的分支。

搜索框输入“北”,接着输入“北京”,页面却最终显示“北”的结果。这类问题不一定是搜索服务查错了,而可能是第二个请求先完成、第一个请求后完成。只加防抖能减少请求数量,却不能保证返回顺序。和 Claude 协作时,应先复现这个顺序,再讨论取消请求或渲染保护。

一 定义界面的唯一规则

本例的规则是“只有最近一次输入对应的结果才能更新页面”。空输入必须清空结果,并使旧请求失去更新资格;旧请求失败也不能盖掉当前结果的状态。不要把规则写成“最后返回的请求生效”,这恰好会保留错误。

准备两组虚构结果,一组对应“北”,一组对应“北京”。不要依赖真实网络恰巧变慢;让测试主动决定哪个请求先结束,才能稳定复现。

二 把搜索逻辑从页面中拆出来

保存为 latest-search.mjs,运行 node latest-search.mjs。这段代码不访问网络,用可控制的 Promise 模拟异步响应。

js 示例代码
import assert from 'node:assert/strict';

function createSearch(load, render, showError) {
  let latest = 0;
  return async function search(query) {
    const current = ++latest;
    if (!query.trim()) { render([]); return; }
    try {
      const result = await load(query);
      if (current === latest) render(result);
    } catch (error) {
      if (current === latest) showError(error);
    }
  };
}
function deferred() {
  let resolve, reject;
  const promise = new Promise((a, b) => { resolve = a; reject = b; });
  return { promise, resolve, reject };
}
const queue = new Map();
const frames = [];
const errors = [];
const search = createSearch(q => {
  const item = deferred();
  queue.set(q, item);
  return item.promise;
}, value => frames.push(value), error => errors.push(error.message));

const old = search('北');
const newer = search('北京');
queue.get('北京').resolve(['北京结果']);
await newer;
queue.get('北').resolve(['过期结果']);
await old;
assert.deepEqual(frames, [['北京结果']]);

const pending = search('上海');
await search('');
queue.get('上海').reject(new Error('过期错误'));
await pending;
assert.deepEqual(frames.at(-1), []);
assert.deepEqual(errors, []);
console.log('latest search checks passed');

每次调用都会递增序号,清空输入也不例外。成功和失败分支都比较序号,这是两个同样重要的保护点。接入页面时,把 render 替换成你现有的渲染函数,监听输入事件后调用 search(input.value);不要在每次输入时重新创建 createSearch,否则序号会重置。

三 可复制的 Claude 排查提示

可复制提示词
我的搜索框会出现旧结果覆盖新结果。规则:只有最近一次输入能更新结果或错误状态;
清空输入立即清空列表,并使之前的请求失效。以下是现有事件处理与请求代码:[粘贴]。
请先用两个可控制 Promise 写出旧请求后完成的稳定复现,再给最小修复。
同时检查过期请求报错、清空输入、当前请求失败三个分支。
不要只建议防抖,不要重写整个组件。列出实际执行和仍需手动验证的项目。

如果模型只在成功分支加序号,应追问错误提示是否也会乱跳。若有加载状态,关闭加载提示的操作也要受当前序号保护,否则旧请求结束时会提前关闭新请求的加载动画。

四 取消请求与结果保护怎样配合

对于真实 fetch(),可以把上一次请求的 AbortController 中止,并把新控制器的 signal 传给新请求。MDN AbortController 文档说明了取消机制。不过,取消请求和限制谁能更新页面是不同责任;如果还有缓存读取、额外异步转换或不支持取消的操作,序号检查仍有意义。

本文的核心逻辑已在 Node.js 24.19.0 执行,通过乱序返回与清空后过期错误的检查;没有宣称浏览器界面或真实搜索接口已经测试。接入后还要验证中文输入法组合输入、键盘选择、网络失败和组件卸载。中文输入法事件处理应按你使用的框架与实际浏览器复现后决定,不能靠多加一个定时器猜测解决。

五 把加载状态也纳入同一条规则

接入实际组件后,给每次输入记录一个开发用序号,并观察开始、成功、失败和结束四类事件。这个序号不必包含用户查询词。若列表正确但加载指示闪烁,检查结束分支是否仍由旧请求触发。当前请求真正失败时,要展示错误,而不是因为防止旧错误就吞掉所有异常。建议再加入“新请求失败、旧请求成功”的组合:最终应保留当前失败状态,不能用旧结果假装恢复。

方法参考:Anthropic 提示工程概览。