Skip to content

[Bug] Agent 在 SPA 页面无限调用 web_fetch 导致 20 轮迭代无任何输出,最终静默失败 #171

@ZhouChaunge

Description

@ZhouChaunge

问题概述

当用户要求 Agent 抓取一个 React/Vue 等客户端渲染(SPA)站点 的内容时,Agent 会陷入无限循环,连续 20 轮迭代全部产出 assistant_chars: 0,最终以 usage: null + 静默 SEND_END 收尾,用户收不到任何回复

复现场景:

"请你从这个网址上把所有的赛题信息爬取下来:https://competition.ai4s.com.cn/race/7/description"

该站点是 React SPA,服务端所有路径均返回同一个 HTML 空壳(8 字节有效内容),web_fetch 无法拿到任何实质数据。


根因分析(4 个独立缺陷)

缺陷 1:web_fetch 无法识别 SPA 空壳,且不给模型任何提示

文件src/tools/web-fetch.js

当前实现是纯 node:http/https GET 请求,遇到 SPA 站点时:

  • 服务端返回 HTTP 200 + 极短 HTML(例如 世界科学智能大赛-->\n-->-->
  • htmlToText() 处理后得到几乎空白的字符串
  • 工具仍返回 ok: true,不附加任何警告
  • 模型看到的结果是「成功了,但内容为空」,因此继续尝试不同路径,形成循环

日志证据

[TOOL_RESULT] {"name":"web_fetch","ok":true,"output":"…世界科学智能大赛-->\n-->-->"}
[TOOL_RESULT] {"name":"web_fetch","ok":true,"output":"…世界科学智能大赛-->\n-->\n(cached)"}

同 URL 二次命中缓存还返回 ok:true,模型没有收到任何"这个页面需要 JS 渲染"的信号。

需要修复
fetchAndExtractText() 函数中,当提取出的纯文本长度 < 可配置阈值(建议 200 字节),且原始 HTML 中存在 SPA 特征标记(<div id="app"<div id="root"__NEXT_DATA__window.__INITIAL_STATE__ 等),在返回的 body 头部追加明确警告:

⚠️ SPA 检测:该页面需要 JavaScript 才能渲染内容。web_fetch 无法执行 JavaScript,获取到的内容为空壳。
建议改用 web_search 搜索相关内容,或请求用户提供页面截图/复制的文本。

缺陷 2:Agent Loop 的同域名重复调用检测缺失

文件src/chat/agent-loop.js

当前 recentToolSig 机制记录的是完整工具签名(工具名 + 参数)用于检测完全相同的调用。但在本 bug 场景中,模型每次尝试的是不同路径/api/race/7/api/v1/race/7/prod-api/race/7/race/7/introduction……),属于同域名下的枚举探测,现有机制无法触发。

日志证据

iter 4: web_fetch /api/race/7
iter 4: web_fetch /api/race/7/detail
iter 5: web_fetch /race/7/introduction
iter 5: web_fetch /api/v1/race/7
iter 6: web_fetch /race/8/introduction
iter 9: web_fetch /sais-competition-web.ai4s.com.cn/api/race/7
iter 11: web_fetch /static/js/main.js   ← 连 JS bundle 都试了

14+ 次 web_fetch,全部返回相同空壳,从未触发任何限速或收敛。

需要修复(在 agent-loop.js 的工具调用结果处理段):

增加同域名失败计数器 domainFailCounts: Map<hostname, number>,阈值建议 3 次。当同 hostname 连续返回空内容达到阈值时,向 run.messages 注入:

<system-reminder>
web_fetch 已对 {hostname} 返回空内容 {N} 次。
该站点极可能是 SPA(客户端渲染),继续尝试该域名的不同路径不会有效。
请立即停止对该域名的尝试,基于目前已获取的信息给用户一个最终答复。
</system-reminder>

缺陷 3:连续 assistant_chars: 0 无收敛保护

文件src/chat/agent-loop.js

当前循环监控的是工具调用的重复签名和写操作失败数,但没有监控模型是否在连续多轮中不产出任何 assistant 文字。在本 bug 中,整整 20 轮迭代 assistant_chars 全为 0,只有 reasoning_chars,意味着模型一直在"思考"但从未向用户输出任何内容。

需要修复

增加空输出连续计数器 consecutiveEmptyOutputIters(阈值建议 5)。在每次 ITER_END 后,若 assistantText.length === 0 && toolCalls.length === 0,递增计数器;达到阈值时注入强制收敛 system-reminder,要求模型立即给出最终回复。


缺陷 4:流异常结束(usage: null + 空输出)无兜底回复

文件src/chat/agent-loop.js

日志 iter 20 末尾:

[ITER_END] {"assistant_chars":0,"reasoning_chars":2363,"tool_calls":0,"usage":null}
[SEND_END] {"iters":19,"asst_chars":0}

usage: null 表明这一轮流被异常中断(达到 token 上限、网络中断或模型超时),此时 assistantText 为空且没有工具调用,循环直接退出,用户什么都看不到

需要修复

streamChat 返回后,检测 !usage && assistantText.length === 0 && toolCalls.length === 0 && reasoningText.length > 0 组合,向用户显示截断提示,并推送降级 assistant 消息保持会话连续性,然后 break 退出循环。


附加建议

建议 A:System Prompt 加「工具失败时的收敛约束」

文件src/prompts/system.js,在工具使用原则部分追加:

- If web_fetch returns near-empty content from the same domain more than twice,
  the site almost certainly requires JavaScript rendering. Stop fetching that domain.
  Switch to web_search or summarize based on available information.
- When you have sufficient information to answer the user's question, reply immediately.
  Do NOT continue calling tools "just to be thorough".

建议 B:web_fetch 结果缓存附加内容质量标记

tool-executor.jsCACHEABLE 缓存层,对 web_fetch 的空壳缓存结果附加 isSpaShell: true,使后续命中缓存时自动附带 SPA 警告。


受影响文件

文件 修改类型
src/tools/web-fetch.js 添加 SPA 空壳检测 + 警告输出
src/chat/agent-loop.js 添加同域名失败计数、连续空输出收敛保护、流异常兜底
src/prompts/system.js 追加工具失败收敛原则
src/chat/tool-executor.js (可选)缓存层附加质量标记

验收标准

  1. 对任意 React/Vue SPA 站点执行 web_fetch,工具结果中应包含 ⚠️ SPA 检测 警告文本
  2. 同一 hostname 连续 3 次返回空内容后,Agent 循环日志中应出现 DOMAIN_FAIL_CONVERGENCE_NUDGE,且下一轮模型不再调用该域名的 web_fetch
  3. 连续 5 轮 assistant_chars: 0 后,日志中应出现 EMPTY_OUTPUT_CONVERGENCE_NUDGE,且模型在下一轮产出 assistant 文本
  4. 流异常结束(usage: null + 空输出 + 有 reasoning)时,用户界面应显示截断提示,不得静默退出
  5. 以上场景全程迭代次数应不超过 8 轮(当前为 20 轮)

优先级

P0 — 当前任何涉及 SPA 站点的抓取请求都会导致用户完全没有输出,属于功能级失效。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions