Skip to content

docs(#1445): Exa MCP 失败信号实读 —— 票面那一种响应没有可消费的结构化信号 - #1452

Merged
jinjunnn merged 1 commit into
alphafrom
feat/1445-exa-fake-success
Sep 24, 2026
Merged

jinjunnn merged 1 commit into
alphafrom
feat/1445-exa-fake-success

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Refs #1445

这个 PR 不修 bug。 它落的是修之前必须先答的那一问的答案,以及一个要裁决的判据缺口。
#1445 票面自己写着:「有结构化信号 ⇒ 消费它;没有 ⇒ 明说没有,把可选方案与代价摆出来交裁决,
不要退回关键字匹配当默认解
」。实读之后落在第二条,所以这里用 Refs 不用 Fixes。

先答那一问

「这是一段错误提示」有没有结构化信号可消费?对票面那一种响应,没有。

实读 https://mcp.exa.ai/mcp 本体(请求与 packages/opencode/src/tool/mcp-websearch.ts
的 call() 逐字同形),2026-09-24 11:34–11:46Z:

# 触发 HTTP JSON-RPC isError content[0]._meta 生产现在怎么判 n
A 正常搜索 200 result 缺省 {searchTime} 成功(对) 27/27
B Exa 自己的 in-band 错误 200 result true 无 provider_error,响亮(对) 4/4
C 免费额度用尽(今天) 429 error -32000 — — unexpected_status,响亮(对) 43/43
D 免费额度用尽(票面那一次,06:39Z) 2xx result 非 true 未知 成功 ⇐ 这就是 #1445 本轮 0 次

对 D 逐个点名:isError 可由生产代码反推为非真;JSON-RPC 是 result 不是 error;
HTTP 2xx(所以 #489/E7「非 2xx 一律 LOUD」那条管不到它);tools/list 实读
两个工具都没声明 outputSchema ⇒ structuredContent 不构成成功判据。
唯一还站着的候选是 content[0]._meta —— 但 D 那一格从没被观测过。

bytes 校验:票面记录 bytes: 246,head 被截到 240 字;补回尾部 6 字得 YOUR_EXA_API_KEY,
长度逐字等于 246,前 240 字与记录逐字相同。负载与反推过程都在
docs/verification/2026-09-24-1445-exa-mcp-response-shapes/results/ticket-payload.json。

要裁决的

选项 1 —— 不落闸,只落勘破(本 PR)。 代码零改动。代价:D 若再出现,仍会以结果的形状到达模型;
今天它到不了(Exa 走 429),但那是 Exa 的当前版本决定的,不是我们的闸决定的。可逆,随时能在勘破之上加闸。

选项 2 —— 消费 content[0]._meta(「没有 provider 元数据的块不算搜索结果」)。
两份副本各写一份 + 一条钉住不漂的判据。只有 D 确实不带 _meta 时它才解决 #1445,而那一格没人量过。
两处站不住:① AC2 做不到 —— 我得先拼一个不带 _meta 的 envelope,再用「没有 _meta 即失败」判它,
期望值与被测对象同源;② 它把一个无契约的 vendor 字段变成致命前提,Exa 哪天不发 _meta,
闸门拒载的就是真实结果,本地搜网整条断掉(响亮,但不可用)。REQ-127 为同一形态付过账。

选项 3 —— 关键字匹配。 票面与 CLAUDE.md 明令禁止(#123/#36 一夜十四轮)。列在这里只为说明
它被评估过并拒绝,不是漏了。

选项 4 —— 先把 D 的 envelope 取到再设计。 在传输层加一条只记录、不判决的诊断:
2xx + result 时记一行「isError 在不在 / structuredContent 在不在 / content[i]._meta 有哪些键 /
限流类响应头」(不含正文)。下一次 D 出现,缺的那一格就有了。不解决 bug,只把「未知」变成「下次可知」。

我的推荐:选项 1,并把「拿到 D 的 envelope」列为 #1445 的前置。 理由:今天 D 在用户面上到不了;
唯一能修它的判据缺的正是 D 的那一格;在缺那一格时落闸,要么空转(D 带 _meta),要么把致命前提押在
vendor 字段上。拖着的代价:Exa 若改回去,这个 bug 会静默回来 —— 选项 4 的那一行日志正是为这个代价买的保险。

顺带量到、没有顺手改的

  1. packages/core/src/tool/websearch.ts 那份 V2 副本完全没有 E7 的那层收口。
    McpResult(:148)不含 isError;parseResponse(:171)读不出返回 undefined;
    调用方(:312)把它换成 NO_RESULTS(:68,一句编造的「没有结果」);
    出网用 HttpClient.filterStatusOk(:229)把全部非 2xx 压成不可辨的错误。
    实测(results/parsers-before.txt):把今天抓到的 B 臂真实负载(isError: true 的 401)喂进去,
    packages/opencode 判失败,packages/core 判成功,并把那句 401 原文当搜索结果交给模型。
    这是同一个「伪成功」类的另一个实例,触发条件与 #1445 不同 ⇒ 按边界不顺手改,开不开票是主 session 的事。
  2. Exa 当前的 web_search_exa inputSchema 必填 objective 且 additionalProperties: false,
    而两份生产副本都不发它(发的是 type / livecrawl / contextMaxCharacters)。今天服务端仍宽松接受;
    哪天不接受,两条腿同时坏。我没有旧 schema 快照,所以「Exa 改过」是推测不是实读。
  3. alpha 分支 tip 本身是红的,与本 PR 无关。 见下。

本地门

bash scripts/alpha-check.sh,worktree .worktrees/1445-exa-fake-success(已 bootstrap,
electron/dist 与 alpha_fence.node 两个未追踪产物均已补齐)。

  • base(57646831b,零改动)fail-set = 1:
    packages/ui-mac/src/main/mcp-server-derivation.test.ts :: #1381 端到端…,
    期望 X-Token: "t" 实得 "***"。根因是本次上游 sync 带进来的
    82d4c8903 fix(opencode): redact credentials in debug config (#50956) ——
    git merge-base --is-ancestor 82d4c8903 d287d7471 答 NO ⇒ 它随 57646831b 进入 alpha。
    整条 alpha 现在 push 不了,不只是这条 lane。
  • 本 PR 的 diff 全部落在 docs/(9 个文件,+599/-0;git diff --name-only origin/alpha HEAD
    对 ^packages/ 零命中,其中两个是探针脚本,不进构建、不被 typecheck 覆盖),
    与 base fail-set 逐条相同,零新增红。

完整输出在 PR 讨论区的下一条评论里。

文档影响

  • docs/architecture/2026-09-24-exa-mcp-failure-signal-recon.md —— 勘破结论(CLAUDE.md
    《勘破先于闸门设计》第 1 条要求的对照物);
  • docs/verification/2026-09-24-1445-exa-mcp-response-shapes/ —— 探针 + 原始输出 + 票面负载夹具;
  • docs/README.md —— 两行索引。

🤖 Generated with Claude Code

修「额度提示被当成搜索结果」之前只有一个问题要答:怎么判别「这是一段错误提示」
而不手写 Exa 的文法。实读端点本体(请求与生产 call() 逐字同形),量到四种形状:

- 正常结果:200 + content[0]._meta.searchTime,27/27;
- Exa 自己的 in-band 错误:200 + isError:true 且 content[0] 没有 _meta,4/4;
- 免费额度用尽(今天):429 + JSON-RPC error + retry-after/x-ratelimit-*,43/43,
  含五种 accept 头变体 —— 这一种生产已经响亮失败;
- 票面那一种(2xx + result + 未置 isError):本轮 0 次。

对票面那一种,可消费的结构化信号逐个点名后都不成立:isError 可由生产代码反推为非真、
JSON-RPC 是 result 不是 error、HTTP 2xx、两个工具在 tools/list 里都没声明 outputSchema。
唯一还站着的候选 content[0]._meta 有两处站不住:它的失败臂从没被观测过(拿它当闸就得先
假设票面负载不带 _meta,再用这个假设去判它,期望值与被测对象同源),以及它会把一个
无契约的 vendor 字段变成致命前提(不成立时闸门拒载的是真实结果)。

所以本次不落闸,只落勘破,判据交裁决。

顺带量到、不属于本轮:packages/core 那份 V2 副本缺整层 E7 收口(McpResult 不含 isError、
读不出就返回 undefined 再换成 NO_RESULTS 伪成功串、filterStatusOk 压掉全部非 2xx)——
把今天抓到的真实 isError:true 负载喂进去,opencode 判失败而 core 判成功。

Refs #1445

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jinjunnn
jinjunnn merged commit 24403cf into alpha Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant