Skip to content

【No.10】feat(deepeyes-v2-agentic): pluggable web-search backends for the search tool - #367

Open
TabsPhasers wants to merge 1 commit into
redai-studio:mainfrom
TabsPhasers:contrib/task-10-deepeyes-v2-web-search
Open

TabsPhasers wants to merge 1 commit into
redai-studio:mainfrom
TabsPhasers:contrib/task-10-deepeyes-v2-web-search

Conversation

@TabsPhasers

Copy link
Copy Markdown

What

把 DeepEyes-V2 示例里写死的占位 search() 换成可插拔的后端分发:新增两个真实搜索后端,同时保留确定性的离线默认行为。

  • examples/deepeyes_v2_agentic/app/search_backends.py(新增):
    • mock(默认)— 确定性离线结果:不联网、不需要密钥,同一查询永远返回相同结果。
    • retriever — 调用 Search-R1 兼容的检索服务:向服务发
      POST {url},请求体 {"queries": [...], "topk": k, "return_scores": false},取返回的 result[0]
      (协议与 examples/search_r1/retrieval_server.py 一致);语料文档没有 snippet 字段时回退用 contents。
    • external — 通用 JSON 网页搜索 API。默认参数对齐 Serper.dev 的 POST /search
      (X-API-KEY 认证头、{"q": …, "num": …} 请求体、结果在 organic 字段下);
      endpoint、认证头、请求字段名和响应字段映射全部可用环境变量调整。API 密钥只从环境变量读取,不写入代码。
    • 三种后端统一返回结构:
      {"elapsed_time": float, "data": [{"title", "link", "snippet", "date"|None}, ...]}。
  • examples/deepeyes_v2_agentic/app/search_utils.py:search() 改为薄分发入口;失败语义不变
    (返回字面量 "Error",环境继续运行)。image_search 未改动。
  • examples/deepeyes_v2_agentic/env.sh.example、README.md:补齐配置面文档,
    包括单次请求超时语义和一次性验证真实 retriever/external 服务的方法。
  • run_deepeyes_v2_agentic.sh、run_deepeyes_v2_agentic_klx.sh:把搜索后端的
    环境变量透传进 Ray worker(runtime-env-json 是显式白名单,新变量不列进去
    训练时会静默留在 mock)。JSON 取值的 _FIELD_MAP 做了引号转义,
    保证生成的 JSON 可解析。
  • tests/examples/deepeyes_v2_agentic/test_search_backends.py(新增):29 个测试。

Why

Relates to #321(贡献者计划第二期 No.10)。

改动前 search() 是硬编码占位实现,只返回固定文案(search_utils.py @ ffa074d),
DeepEyes-V2 训练无法接真实的检索服务或网页搜索 API。贡献者计划 No.10 任务(relates to #321)
要求提供可插拔的 mock / Search-R1 retriever / external 三种搜索后端,统一结果结构,
超时和重试可配置,且默认配置离线可跑。

How

  • 配置而非硬编码:后端选择和所有参数都是环境变量
    (DEEPEYES_V2_SEARCH_BACKEND、DEEPEYES_V2_SEARCH_TOP_K、_TIMEOUT_SECONDS、
    _MAX_RETRIES、_RETRY_DELAY_SECONDS、DEEPEYES_V2_RETRIEVER_URL、
    DEEPEYES_V2_EXTERNAL_SEARCH_ENDPOINT/_API_KEY/_AUTH_HEADER/_QUERY_FIELD/_TOPK_FIELD/ _RESULTS_FIELD/_FIELD_MAP)。这与该示例已有的机器本地 env.sh 模式一致,
    endpoint 和密钥留在被 gitignore 的 env.sh 里,不进提交。
  • 统一结果结构:normalize_results() 把各后端的原始行映射为
    title/link/snippet(缺失 → "")和 date(缺失 → None),
    下游渲染不会遇到缺字段的行。Search-R1 语料文档的正文在 contents 字段,作为 snippet 的回退;
    external 后端的行通过 _FIELD_MAP 重映射(例如 '{"date": "published_date"}')。
  • 失败语义沿用环境已有的 "Error" 约定:连接失败、超时、5xx、非法 JSON、结构不对的返回体
    会先重试(线性退避,对齐现有的 agentic_opd/search_qa retrieval client),
    重试耗尽后返回 "Error" 并记录日志。DeepEyesV2Env.exec_tool 把它渲染为既有的
    search_failed 观测——agent 进程不会崩溃。配置错误(数值非法、缺少必需的 URL)
    在工厂函数里抛出,在 search() 边界统一转成 "Error" 加警告日志。
    4xx 状态立即失败、不消耗重试(只有 5xx 和传输层故障可重试)。
    DEEPEYES_V2_SEARCH_TIMEOUT_SECONDS 约束的是单次 HTTP 请求;
    单次搜索调用的最坏耗时约为"超时 × 尝试次数 + 重试间隔",README 已写明。
  • 零新增依赖:只用 requests(已在 requirements.txt 里)。

Testing

  • 新增 29 个单元测试,全部打在本地 stub HTTP 服务(127.0.0.1)上——不访问外网、不需要密钥:

    pytest tests/examples/deepeyes_v2_agentic/
    # 41 passed (29 新增 + 12 既有) in ~7s

    覆盖范围:mock 确定性与返回结构;配置解析(默认值/覆盖/非法值);
    retriever 请求构造与响应映射(含 return_scores: false 请求体断言、空结果成功、contents 回退);
    external 的 Serper 预设、自定义字段映射、认证头有无;失败矩阵——
    超时、连接拒绝、4xx(不重试)、5xx(重试后成功 / 重试耗尽并核对尝试次数)、非法 JSON、字段缺失或类型不对;
    经 DeepEyesV2Env.exec_tool 的环境层集成("Error" → search_failed 观测路径);
    以及一个守卫测试,断言两个启动脚本都把全部搜索环境变量透传进 Ray worker
    (runtime-env-json 白名单在加变量时最容易漏)。

  • 真实服务验证:README 记录了一次性验证方法——先起
    examples/search_r1/run_retriever.sh,把 DEEPEYES_V2_RETRIEVER_URL
    指过去(或配置 Serper endpoint + 密钥),在仓库根目录调用一次 search()。
    retriever 线上协议与仓库自带的 examples/search_r1/retrieval_server.py 一致;
    开发过程中未连接真实服务(本机没有索引和密钥)。

  • 回归:pytest tests/examples/ —— deepeyes 相关测试全部通过;其余失败在干净的 main 上同样出现
    (用 git stash 复跑确认过):tests/examples/nemo_gym_agentic/test_r2e_test_layout.py
    (5 个,环境问题)和 tests/examples/seq_cls_sft/(需要 torch,本机未安装;
    本任务不涉及 GPU)。

  • pre-commit run --all-files --show-diff-on-failure 通过(全部钩子)。

  • git diff --check 干净。

  • pre-commit run --all-files passes

  • Tests pass (pytest tests/)

  • New tests added (if applicable)

  • Documentation updated (if applicable)

Type of Change

  • New feature (non-breaking change that adds functionality)

…ch tool

Replaces the hardcoded placeholder `search()` with a configurable backend
dispatch (mock / Search-R1 retriever / external web-search API), so the
example keeps running offline by default while real deployments can point
the same tool at a retrieval service or a hosted search API.

# ⭐ Feature
- app/search_backends.py: `SearchConfig` (env-driven config), the three
  backends and the `get_search_backend()` factory. `mock` (default) is
  deterministic and offline (no randomness, same query -> same results);
  `retriever` speaks the Search-R1 wire protocol (`POST {url}` with
  `{"queries": [...], "topk": k, "return_scores": false}` -> `result[0]`,
  matching examples/search_r1/retrieval_server.py, with `contents` as the
  snippet fallback); `external` defaults to the Serper.dev `POST /search`
  preset and makes endpoint, auth header, request fields and response field
  mapping configurable via env vars. API keys are only ever read from the
  environment, never committed.
- Uniform result contract for every backend:
  `{"elapsed_time": float, "data": [{"title", "link", "snippet",
  "date"|None}, ...]}`; missing title/link/snippet degrade to "" and date to
  None, so downstream rendering never sees partial rows.
- Failure semantics follow the env's existing convention: timeouts,
  connection errors, 5xx, unparsable JSON and structurally invalid payloads
  are retried (linear backoff, configurable) then returned as the literal
  `"Error"` string with a logged reason — the agent process keeps running.
  Retry semantics mirror the existing
  examples/on_policy_distillation/.../retrieval_client.py client; 4xx
  statuses fail immediately without burning retries.
- app/search_utils.py: `search()` is now a thin dispatch (explicit `size`
  wins over `DEEPEYES_V2_SEARCH_TOP_K`, default 5 as before); the random
  sleep retry loop is gone — retries live in the real backends only, mock
  stays deterministic. `image_search` is unchanged.
- run_deepeyes_v2_agentic.sh / _klx.sh: forward the search backend env vars
  into the Ray workers — runtime-env-json is an explicit whitelist, so
  without this a configured backend would silently fall back to `mock` in
  real training. The JSON-valued `_FIELD_MAP` is quote-escaped so the
  generated JSON stays parseable.
- Zero new dependencies: uses `requests`, already in requirements.txt.
- Config lives in environment variables (documented in env.sh.example) so
  endpoints/keys stay in the gitignored env.sh, matching the example's
  existing machine-local config pattern.

# ✅ Tests
- tests/examples/deepeyes_v2_agentic/test_search_backends.py: 29 unit tests
  against a local stub HTTP service (127.0.0.1, no external network):
  mock determinism + shape, config parsing (defaults/overrides/malformed),
  retriever request construction + response mapping (Serper preset, custom
  field maps, auth header presence/absence), and the failure matrix —
  timeout, connection refused, 4xx no-retry, 5xx retry-then-success and
  retry-exhausted, invalid JSON, missing/ill-typed fields — plus env-level
  integration through DeepEyesV2Env.exec_tool asserting the "Error" →
  search_failed observation path, and a guard test asserting both launch
  scripts forward every search env var into the Ray workers.

# 📝 Documentation
- README "Text search backends" section: backend table, unified result
  contract, failure behavior (including per-request timeout semantics and
  worst-case wall time), the full env-var surface, and a one-shot recipe
  for verifying a real retriever / external service.
- env.sh.example: optional search-backend config block with a placeholder
  key slot in the gitignored template.
@rai-studio-bot

rai-studio-bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Nyanpasu 审查看板

审查状态: 💬 已完成 · 有补充意见

审查版本: 9bc1c2c

常规与深度审查均已完成。实现满足任务 No.10 全部显式需求:Search-R1 协议、统一返回结构、"Error" 失败约定、可配置超时/重试/top-k/字段映射、启动脚本透传均验证正确,无阻塞缺陷。共 3 条非阻塞待处理发现:P2 —— 超时/连接错误的重试语义未被测试钉住(变异验证:删除该重试分支后 29 个测试仍全绿);P3 —— requests 默认 trust_env 使代理环境变量隐式路由搜索流量;P3 —— 重试耗尽消息报告配置次数而非实际次数。生产与测试必要性审计完成,结论为保留现有结构(已检查的更小替代方案及否决理由见精简审查表)。

审查阶段进度范围与结果
常规审查 ✅ 已完成 已核查:Search-R1 线上协议与 retrieval_server.py 一致(return_scores: false 下取 result[0] 平铺语料行 + contents 回退);重试/失败语义与 agentic_opd retrieval client 对齐;"Error" 约定经 search() → _dispatch_search → exec_tool 端到端保持;search() 入口对唯一生产调用方向后兼容;两个启动脚本 13 个环境变量全部透传且 FIELD_MAP 引号转义生成的 JSON 可解析(bash 模拟验证);29/29 新测试通过;README 默认值与超时数学准确;无占位文本残留依赖;版权头/行宽/零新增依赖合规。CI 未在此 fork PR 上配置。
深度审查 ✅ 已完成 两个子任务均完成并合并:独立设计对比(隔离 merge-base 工作区,32/32 项针对真实调用方的检查 + 12/12 既有测试通过)与测试审计(11 个定向变异 + 精简套件等价验证,基线 29/29)。测试审计发现一个真实盲区——删除连接/超时重试分支后套件仍全绿——已作为 P2 行内发现提交加强建议;另发现重试耗尽消息在 4xx 提前退出时报告配置次数(P3)。记录的非阻塞缺口:负 top_k 会原样上送、max_retries=0 下限为 1 次尝试、空 query、field_map 与 snippet 回退组合及非字符串 date 未测(当前无生产调用组合)、并发未测(每次调用新建后端,无共享可变状态,低风险)。

审查发现

待处理
编号 严重性 问题状态规则来源
F1 Low severity 明确代理环境变量对搜索后端的影响(trust_env 默认开启) 🚧 未解决 —
F2 Medium severity 补钉"超时/连接错误会重试"的语义测试(删除该重试分支后 29 个测试仍全绿) 🚧 未解决 —
F3 Low severity "failed after N attempts" 报告配置值而非实际尝试次数 🚧 未解决 —
已解决或已取代
编号 严重性 问题状态规则来源
暂无已解决或已取代的记录。
提交范围 · 接收 7 · 建议移出 0 · 待确认 0

接收 7 个文件 · 建议移出 0 个文件 · 待确认 0 个文件。移出与待确认部分暂停深审,不代表审查通过。

文件结论仓库维护必要性依据替代去向或方案
examples/deepeyes_v2_agentic/app/search_backends.py
examples/deepeyes_v2_agentic/app/search_utils.py
接收 Implements the required pluggable search tool behavior (mock default, Search-R1 compatible retriever, configurable external API) inside the existing example the repository already maintains; search_utils.py is the existing entry point called by env_deepeyes_v2 and becomes the dispatch layer. Contributor plan task No.10 (redai-studio/community official-task.md, 2026-cohort-2, 'DeepEyes-V2 真实网页搜索工具'); existing caller examples/deepeyes_v2_agentic/app/env_deepeyes_v2.py Keeping the backends only as an external experiment script would not implement the task's required tool behavior in the maintained example; no existing repo mechanism already provides pluggable backends for this example.
examples/deepeyes_v2_agentic/env.sh.example
examples/deepeyes_v2_agentic/README.md
examples/deepeyes_v2_agentic/run_deepeyes_v2_agentic.sh
examples/deepeyes_v2_agentic/run_deepeyes_v2_agentic_klx.sh
接收 Configuration surface of the same example: env.sh.example documents the new environment variables; the two launch scripts must whitelist them into the Ray worker runtime env, otherwise real training silently stays on the mock backend and the feature is unusable; README documents usage, matching the example's existing env.sh convention. Task No.10 acceptance criteria (configurable endpoint/auth/field mapping, keys not hardcoded, default offline run); existing launch scripts examples/deepeyes_v2_agentic/run_deepeyes_v2_agentic.sh runtime-env-json whitelist pattern Omitting the launch-script passthrough would leave the feature non-functional under real training; no smaller existing mechanism exports new env vars to Ray workers.
tests/examples/deepeyes_v2_agentic/test_search_backends.py
接收 Acceptance criteria explicitly require tests covering the three backends and failure paths; these are durable regression tests of the shipped implementation (stub HTTP servers on localhost), not one-off experiment evidence. Task No.10 acceptance criteria ('测试覆盖三个后端及异常路径'); existing tests/examples/deepeyes_v2_agentic/ suite already exists for this example One-off manual verification documented externally would give no regression coverage for the maintained production paths; the suite runs offline without keys so it fits CI.
精简审查与验证依据
审查范围进度结论
生产代码 ✅ 已完成 对三后端类 + 工厂 + 配置 dataclass + 独立重试助手做了独立最小设计对比(隔离工作区验证)。更小的函数式替代方案可行,但其省略了任务显式要求的可配置重试与 top-k 旋钮;剩余结构差异属风格层面且现有测试已锚定类 API,结论为全部保留。
测试 ✅ 已完成 29 个测试按边界分组审计:7/7 语义变异被预期测试以预期原因捕获(请求体等值、请求计数、错误码不匹配),2 个行为保持重构不破坏套件(无内部耦合)。发现一个真实盲区(连接/超时重试分支删除后套件仍全绿)已作为 P2 发现提交加强建议;验证过的 25 测试精简方案(4 处合并 + 超时加强)检测力等同,合并为可选优化未单独立项。

生产代码的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
search_backends.py 的三个后端类、_BACKENDS 注册表、get_search_backend 工厂与 SearchConfig dataclass(378 行) 任务 No.10 要求的三种可插拔后端(环境变量选择、统一返回结构、可配置超时/重试/top-k/字段映射、配置错误转 "Error");调用方为 env_deepeyes_v2.py:448 的 search(query)(唯一生产调用,经 asyncio.to_thread 同步执行) 独立参考实现:模块级函数 + 1 行分发字典 + 单一 normalize(约 200 行、单文件、10 个环境变量),在隔离工作区通过 32/32 项针对真实调用方的检查与 12/12 既有测试 保留 参考方案自述未提供可配置重试次数/退避(继承占位实现固定的 3 次尝试)与 top-k 旋钮,与任务显式要求不符,补齐后体积优势缩小;PR 的配置在每次调用时解析(search() 内 from_env()),不存在参考方案指出的 import 期崩溃风险;7/7 语义变异被现有测试捕获。限制:无 Ray 集群与真实外部端点,runtime-env 透传仅在 JSON 渲染层验证(父审查者独立复核一致)。
_post_json_with_retries 与 examples/on_policy_distillation/agentic_opd/search_qa/app/retrieval_client.py 重试逻辑的跨示例重复 重试语义镜像兄弟实现:5xx/连接/超时/非法 JSON 重试、4xx 立即失败、线性退避封顶 5s、attempts = max(1, max_retries) 抽取跨示例共享的重试模块 保留 仓库约定各示例独立维护自身 app/ 代码(无共享示例工具目录),跨示例抽取是超出本 PR 范围的结构变更;两处语义已被各自测试锚定,且兄弟实现为带连接池的 Session 热路径客户端,与本项目低频调用场景不同。

测试的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
tests/examples/deepeyes_v2_agentic/test_search_backends.py(29 个测试:mock 2、配置解析 4、normalize 1、retriever 9、external 5、入口+环境级 6、启动脚本守卫 1,另有共享 stub 服务) 任务验收标准:三个后端与异常路径(超时、连接、4xx 不重试、5xx 重试、非法 JSON、结构错误、配置错误)及环境级 "Error" 传递;套件离线运行于 127.0.0.1 stub 审计验证的 25 测试精简套件:合并 mock top_k 两例、合并非法 JSON/结构/连接拒绝为多触发单测(并补请求计数断言)、合并两个环境级失败测试,同时加强超时测试(max_retries=3 + 请求计数);对 M1–M7、M9 检测力等同且补上 M10 盲区 保留 基线 29/29(一次性 git archive 工作区;记录两处环境修正——PIL 导入 shim 与 NO_PROXY 回环,与本审查者独立复核的环境一致);变异矩阵 M1–M11 全记录;精简+加强套件 25/25 且捕获 M10(原套件漏检,1 次 vs 3 次请求)。合并属可选优化,加强部分已作为 P2 行内发现提交,故保留原套件结构。
test_launch_scripts_forward_search_env_vars(对两个启动脚本的子串存在性守卫) runtime-env 白名单必须包含全部 13 个搜索环境变量,否则训练静默留在 mock 后端 执行 bash 渲染并解析生成的 JSON 后逐键断言(更强 oracle) 保留 子串检查足以捕获主要回归(新增变量漏加白名单);FIELD_MAP_ESC 变量名包含原变量名,注释误命中风险低且后果仅为守卫偏松;解析式检查需在测试内执行 bash 渲染,复杂度超过其保护的边界。
Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

常规审查完成(深度审查进行中)

已核查全部 7 个改动文件:Search-R1 线上协议与 examples/search_r1/retrieval_server.py 一致(return_scores: false 下取 result[0] 平铺语料行 + contents 回退正确);重试/失败语义与既有 agentic_opd/search_qa retrieval client 对齐(5xx 重试、4xx 立即失败、max(1, max_retries) 次尝试);"Error" 约定经 search() → _dispatch_search → exec_tool 端到端保持;search() 入口对唯一生产调用方向后兼容;两个启动脚本 13 个搜索环境变量全部透传,FIELD_MAP 引号转义生成的 runtime-env JSON 已验证可解析;README 默认值与超时数学准确。29 个新测试本地全部通过(~7s),未发现阻塞缺陷,已留下 1 条非阻塞建议(P3:代理环境变量对搜索后端的隐式影响,见行内评论)。独立设计对比与测试必要性审计仍在后台进行,结论稍后更新至看板。

审查看板:#367 (comment)

Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

last_error: str | None = None
for attempt in range(1, attempts + 1):
try:
resp = requests.post(url, headers=dict(headers), json=payload, timeout=timeout_seconds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity P3 · 明确代理环境变量对搜索后端的影响(trust_env 默认开启)

requests.post 默认 trust_env=True,会遵循进程环境里的 HTTP_PROXY / HTTPS_PROXY。在设置了代理的 worker 节点上,指向集群内检索服务的 retriever 请求会被静默路由到代理并失败(重试耗尽后返回 "Error",训练不中断但搜索工具实际不可用,只有 warning 日志可查)。本 PR 参考的兄弟实现 examples/on_policy_distillation/agentic_opd/search_qa/app/retrieval_client.py 特意默认 trust_env=False(SEARCH_RETRIEVAL_TRUST_ENV 默认 0);这里沿用了它的重试语义,但没有沿用这一默认值。

复现证据:在设有 HTTP_PROXY 且未设 NO_PROXY 的开发机上直接运行本套件,8/29 个 stub 测试因代理无法回连 127.0.0.1 而得到 "Error";设 NO_PROXY=127.0.0.1,localhost 后 29/29 通过——差异仅来自代理路由。另外 run_deepeyes_v2_agentic*.sh 的 runtime-env 白名单不透传 NO_PROXY,用户在代理节点上无法只靠 env.sh 规避(klx 脚本第 35–36 行已在提交 shell 中 unset http_proxy https_proxy,说明代理干扰在该部署环境是真实存在的问题)。

非阻塞建议(任选其一):

  • 增加类似 DEEPEYES_V2_SEARCH_TRUST_ENV 的开关(默认对齐兄弟实现设 0),请求时改用 requests.Session 并设置 trust_env;或
  • 至少在 README 的 "Text search backends" 一节注明代理交互与 NO_PROXY 配置方法,并把 NO_PROXY 加入两个启动脚本的透传白名单。

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查完成(深度审查已合并)

在常规审查基础上完成了深度审查:独立设计对比(在隔离的 merge-base 工作区构建最小设计,32/32 项针对真实调用方 DeepEyesV2Env._dispatch_search 的检查与 12/12 既有示例测试通过)和测试套件审计(11 个定向变异实验 + 精简套件等价性验证,基线 29/29 通过)。结论:实现满足任务 No.10 的全部显式需求——Search-R1 协议、统一返回结构、"Error" 失败约定、可配置超时/重试/top-k/字段映射、启动脚本透传均验证正确,无阻塞缺陷。

本轮新增两条非阻塞发现:P2 —— 模块文档与 PR 描述声明"连接失败、超时会先重试",但删除 _post_json_with_retries 中对应的重试分支后 29 个测试仍全部通过(这些路径的测试都用 max_retries=1 或只断言 result == "Error"),建议按行内评论加强超时测试钉住该语义;P3 —— 重试耗尽的错误消息在 4xx 提前退出时报告配置次数而非实际次数。加上常规阶段的 trust_env 代理建议(P3),共 3 条待处理意见,均不阻塞合入。

生产结构与测试套件的必要性审计已记录在看板:类/工厂/配置层的更小函数式替代方案经独立验证可行,但其省略了任务显式要求的可配置重试与 top-k 旋钮,且现有测试已锚定当前 API,故结论为保留现有结构。

审查看板:#367 (comment)

Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

Comment on lines +317 to +325
def test_retriever_backend_timeout_returns_error():
stub = _StubSearchService()
url = stub.start()
try:
stub.delay_seconds = 1.0
result = RetrieverSearchBackend(_retriever_config(url, timeout_seconds=0.2)).search("q", 1)
assert result == "Error"
finally:
stub.stop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity P2 · 补钉"超时/连接错误会重试"的语义测试

模块文档与 PR 描述都明确声明"连接失败、超时……会先重试(线性退避)",且 _post_json_with_retries 的 except (ConnectionError, Timeout) 分支专门实现了这一点;但当前套件没有钉住这条语义。变异验证:在一个一次性工作区里删掉该 except 分支(让超时/连接错误直接冒泡、只尝试 1 次),29 个测试仍然全部通过——因为走这些路径的测试要么用 _retriever_config 默认的 max_retries=1(1 次尝试本来就是期望值),要么只断言 result == "Error"。相对地,5xx 重试语义已被 test_retriever_backend_retries_exhausted_returns_error(len(stub.requests) == 3)牢牢钉住。

建议把超时测试加强为 max_retries=3 + 请求计数断言(顺带可对 connection-refused 做同样处理);加强后的版本可检出上述变异(1 次 vs 3 次请求):

Suggested change
def test_retriever_backend_timeout_returns_error():
stub = _StubSearchService()
url = stub.start()
try:
stub.delay_seconds = 1.0
result = RetrieverSearchBackend(_retriever_config(url, timeout_seconds=0.2)).search("q", 1)
assert result == "Error"
finally:
stub.stop()
def test_retriever_backend_timeout_returns_error():
stub = _StubSearchService()
url = stub.start()
try:
stub.delay_seconds = 1.0
result = RetrieverSearchBackend(
_retriever_config(url, timeout_seconds=0.2, max_retries=3)
).search("q", 1)
assert result == "Error"
assert len(stub.requests) == 3 # timeouts are retried before giving up
finally:
stub.stop()

(retry_delay_seconds 已是 0.0,加强后套件运行时间几乎不变。)

f"[search_backends] attempt {attempt}/{attempts} failed ({last_error}); retrying in {delay:.1f}s"
)
time.sleep(delay)
raise RuntimeError(f"search request failed after {attempts} attempts: {last_error}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity P3 · "failed after N attempts" 报告配置值而非实际尝试次数

4xx 走 raise_for_status → break 提前退出时,这条消息仍然打印配置的 attempts(如 3),但实际只发了 1 次请求——排查训练日志时会把"不可重试的立即失败"误读成"重试耗尽"。参考的兄弟实现 agentic_opd/search_qa/.../retrieval_client.py 在同场景下报告的是实际次数。最小修复:循环变量 attempt 在循环结束后仍然存活,正常耗尽时等于 attempts,提前 break 时即实际次数,直接改用它即可:

Suggested change
raise RuntimeError(f"search request failed after {attempts} attempts: {last_error}")
raise RuntimeError(f"search request failed after {attempt} attempts: {last_error}")

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants