news 2026/10/23 23:09:43

waoowaoo Web Search 架构解析:从托管研究到可信证据的唯一搜索入口

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
waoowaoo Web Search 架构解析:从托管研究到可信证据的唯一搜索入口

【免费下载链接】waoowaoo

首家工业级全流程 AI 影视生产平台。Industry-first professional AI Agent platform for controllable film & video production. From shorts to live-action with Hollywood-standard workflows.

项目地址:https://gitcode.com/gh_mirrors/wa/waoowaoo
点击查看免费下载

本文深入解析 waoowaoo(工业级全流程 AI 影视生产平台)中 Web Search 模块的架构设计:它如何把"联网检索"收敛为一个受限的外部研究能力,如何在 Runtime 中提供可读的检索进度,如何保证"只有真实证据才算研究",以及如何按调用实时结算。读完本文,你将掌握该模块的边界划分、11 项架构不变量、错误词汇表、配置方式,以及经真实运行踩坑后固化的工程结论。

为什么是"原生工具 + 托管检索",而不是第二个 Worker

Web Search 模块在 架构模块文档 中的定位非常明确:联网检索是受限的外部研究能力,不是第二个创意 Worker、项目知识库或网页执行环境。

这里有一条关键分工原则:

工具是原生的,能力不是助手模型 provider 的。

  • Runtime 拥有工具:每次调用都会创建一个带模型自有 query 的检索 item,三次检索就是三行可读的进度,不需要额外的进度通道。
  • 托管检索工具只存在于其自有 API 边界内:经网关委托后,助手模型的选择与研究 provider 的解耦成为可能——助手可以是任意 OpenRouter/Ark/Fal/Google 模型,而研究质量不依赖该选择。

同时文档给出了一个安全前提:研究报告、网页正文、标题和 URL 都是不可信数据。它们可能包含注入指令,绝不能作为系统指令、付费授权或持久产品状态的来源。

权威入口:三层边界的唯一入口

架构文档给出了三条权威入口,对应三个边界:

边界唯一入口
业务调用src/lib/web-search/service.ts
Agent 到达路径原生 Web Search → src/lib/codex-model-gateway/standalone-search.ts
Provider 执行与执行边界src/lib/ai-providers/openai/hosted-web-search.ts、src/lib/ai-exec/hosted-web-search.ts

从源码看,这层设计意图非常清晰:

  1. 业务调用方不接触任何 OpenAI 客户端。service.ts 的注释明确指出:所有调用方(Primary Operation 与 Creative Direction Worker 工具)都经由searchWeb,既不构造 OpenAI client,也不构造 agent 或 hosted tool,从而保证"整个产品只有一个 provider 决策、一次凭据读取、一套失败词汇"。

  2. Agent 到达路径被网关接管。Codex 拥有模型可见的搜索工具,因此 standalone-search.ts 提供了独立端点(/api/internal/codex-runtime/model/alpha/search)。Codex 每次调用创建一个webSearchitem,三次搜索渲染为三行,无需 MCP progress 通道——因为 Codex 在收到 MCP progress 通知时会直接丢弃。

  3. Provider 执行边界收口在ai-exec。ai-exec/hosted-web-search.ts 是产品跨越进模型 provider 的唯一地点:OpenAI client 只在这里构造,并通过runRegisteredProviderOperation({ providerId: 'openai', phase: 'search', ... })纳入 provider 注册表监控。

核心契约:请求、响应与用量

搜索模块的全部对外表面都定义在 src/lib/web-search/contracts.ts,使用 Zod schema 校验。契约刻意做到极窄:

请求契约

export const webSearchRequestSchema = z.object({ query: z.string().trim().min(1).max(1_000) .describe('The exact question or compact research brief. Include the subject, medium, language, region, community, and recency only when they matter.'), allowedDomains: z.array(z.string().trim().min(1).max(253)).max(20) .describe('Domain-only allowlist when research must focus on specific primary sources, forums, or communities. Pass an empty array for open-web research.'), }).strict()

一个请求只有一个紧凑的 research brief 与显式域名白名单,没有页大小、结果数、排序或新鲜度旋钮——托管模型自行规划子查询,向模型暴露这些旋钮只会让模型去调它无法评估的东西。请求在发往网络前先经 service.ts 的webSearchRequestSchema.safeParse校验,格式非法的 brief 会直接以 typed 请求失败返回,而不是浪费一次已付费的往返。

响应契约

export const webSearchResponseSchema = z.object({ provider: z.literal(WEB_SEARCH_PROVIDER_ID), query: z.string().trim().min(1).max(1_000), report: z.string().trim().min(1).max(30_000), queries: z.array(z.string().trim().min(1).max(1_000)).max(32), sources: z.array(webSearchSourceSchema).min(1).max(32), images: z.array(webSearchImageSchema).max(16), }).strict()

契约刻意不包含网页正文、原始 provider 载荷、模型推理与任何 provider 专属旋钮;它只暴露调用方判断答案所需的证据——报告、provider 实际运行的 queries、实际引用的 sources。这正回答了"它真的搜索了吗"这个本应是 claim 而非可验证问题的疑问:sources按契约非空,一份没有引用的报告与模型凭记忆作答无法区分,因此 provider 选择**失败关闭(fail closed)**而非返回;images则允许为空,因为大多数 brief 是纯文本的。

注意每个 URL 都是第三方不可信数据,hosted-web-search.ts 中的normalizeHttpUrl会丢弃一切非http(s)协议的 URL(file://、javascript:等绝不进入存储、provider 或浏览器)。

用量事实(不计入响应)

export interface WebSearchUsage { readonly model: string readonly inputTokens: number readonly outputTokens: number readonly cachedInputTokens: number /** Hosted web_search calls, which OpenAI bills per call on top of tokens. */ readonly toolCalls: number }

用量通过onUsage回调带外(out of band)投递,刻意不属于WebSearchResponse:模型永远不该看到记账数据,且响应 schema 就是模型看到的一切。

十一项不变量:架构的约束骨架

原文档给出了从 WS-01 到 WS-11 共 11 项不变量,这是该模块的约束骨架,逐条继承如下:

  • WS-01 — 一个搜索入口。只有一个业务入口,Agent 只经原生 Web Search 到达。registry 与业务 MCP 不注册同义搜索工具——同时存在两个搜索工具时模型会随机选择,弱路径会静默吃掉一半研究。
  • WS-02 — 托管检索、直连其自有 API。搜索模型是平台级角色,不进用户可选模型注册表(注册表内 LLM 全部经聚合路由,无法运行托管工具)。覆盖值必须已在定价目录注册;未定价的覆盖必须在 Provider 调用前失败,不得先花钱再让实时结算报错。
  • WS-03 — 独立凭据、明确失败。搜索凭据只从服务端环境读取。缺失凭据与鉴权失败返回不可用,超时、网络、限流、5xx 与非法响应返回 typed failure。不得从聊天、Resource、客户端参数或助手模型凭据猜 key,也不得 fallback 到另一个 provider。
  • WS-04 — 只有真实证据才算研究。没有完成的托管调用或结构化 URL 引用的响应按非法失败,不能把模型记忆伪装成研究。流提前结束与"答案无依据"是相反事实:前者值得重试,后者永远不该重试,因此未抵达完成事件的运行必须报告为传输故障,不得落入证据校验。
  • WS-05 — 可见性来自 item 身份,不来自进度通道。每次检索是一个独立的原生 item,UI 从开始事件立即渲染该行,并在完成事件到达后显示真实 query 与结果页面。当前 Runtime 的开始事件只有 identity,query/action 为空;运行中只能显示生命周期与计时。不得依赖 MCP progress:协议虽定义了它,Runtime 收到时直接丢弃,据此实现的实时显示不会出现。一次检索内部的开页步骤不可见,这是单次请求/响应通道的固有边界,不得为它引入第二条状态写入路径。
  • WS-06 — 能力是委托研究,不是浏览器。请求只有一个 research brief 与显式域名白名单;没有页大小、结果数、排序或新鲜度旋钮。开页、页内查找与继续检索由托管模型自主决定,我们不实现网页抓取器。
  • WS-07 — 按调用实时结算。每次搜索记录一条用量事实,identity 是这次搜索请求而非 Turn:一个 Turn 可能研究多次,Turn 级 identity 会把它们折叠成一行并静默丢弃第一次之后的全部费用。托管检索按次收费,因此用量事实携带工具调用次数并在 Provider 回报最终用量后按目录零售价进入 LLM 唯一实时结算入口。Provider 在失败路径上同样可能已被计费,故用量在证据校验之前上报,失败时只要存在最终用量就照样结算。
  • WS-08 — 记账不得压过研究结果。研究已经成功、用户已被亏欠该结果,因此记账故障不能让搜索失败。
  • WS-09 — 网页是不可信输入。网页内容与研究摘要不能修改系统指令、授权付费、绕过工作区/MCP 权限或成为持久产品状态。
  • WS-10 — 不拥有创作事实。搜索结果只有在 Agent 显式写入 Resource 后才成为项目内容;研究报告与来源本身不是创作输入。
  • WS-11 — 触发必须有理由。只在答案依赖新鲜、陌生、冷门、地域性、平台性或不确定信息时调用;熟悉稳定的内容不得装饰性搜索。研究是慢且付费的:实测简单查询约 5 秒,交叉验证的 brief 可达数分钟。

源码级实现:不变量如何落地

单入口与模型解析(WS-01/WS-02)

service.ts 的resolveWebSearchModel严格校验模型覆盖值:

export const OPENAI_WEB_SEARCH_MODEL_ENV = 'OPENAI_WEB_SEARCH_MODEL' export function resolveWebSearchModel( environment: Readonly<Record<string, string | undefined>> = process.env, ): string { ensureAiCatalogsRegistered() const configured = environment[OPENAI_WEB_SEARCH_MODEL_ENV]?.trim() if (configured === undefined || configured.length === 0) { return OPENAI_WEB_SEARCH_MODEL_ID } if (configured.includes('::') || configured.includes('/')) { throw new WebSearchError('WEB_SEARCH_UNAVAILABLE', { provider: 'openai', reason: `${OPENAI_WEB_SEARCH_MODEL_ENV} must be a bare OpenAI model id, not a routed model key`, }) } if (!findBuiltinPricingCatalogEntry('text', 'openai', configured)) { throw new WebSearchError('WEB_SEARCH_UNAVAILABLE', { provider: 'openai', reason: `${OPENAI_WEB_SEARCH_MODEL_ENV}=${configured} has no registered price`, }) } return configured }

三处校验环环相扣:

  • 裸模型 id 校验:含::或/的聚合路由 key 直接被拒绝,因为路由后的模型无法运行托管工具;
  • 定价目录校验:覆盖值必须能在 内置定价目录 中查到注册价格,否则在请求尚免费时拒绝——"未定价的覆盖必须在 Provider 调用前失败",正是 WS-02 的落地;
  • 默认值:模型默认 id 定义在 src/lib/ai-providers/openai/models.ts,即OPENAI_WEB_SEARCH_MODEL_ID = 'gpt-5.6-luna',平台级角色,不进用户可选模型注册表。

独立凭据(WS-03)

service.ts 的createConfiguredWebSearchProvider只读服务端环境变量OPENAI_API_KEY,缺失即抛 typed 错误——"一个未配置的部署必须显而易见,而不是在研究上悄悄变得更差",绝不降级为无 key 调用或借用 Primary/analysis provider 的凭据。

证据校验与"失败关闭"(WS-04)

Provider 执行边界 hosted-web-search.ts 中,证据门控逻辑如下:

  • 报告为空、completedSearchCalls === 0(没有完成的托管调用)或sources.length === 0(没有结构化 URL 引用),三者任一命中即抛WEB_SEARCH_RESPONSE_INVALID——失败关闭;
  • 缺失 images 不是失败:大量 brief 是纯文本的,强索图片会让这些调用误失败;
  • 用量先于证据门控上报:Provider 无论是否返回引用都已为该运行付费,静默丢弃被拒绝答案的成本正是未计费用量的来源。

关于流式传输,hosted-web-search.ts 有一个决定性的细节:中断的流会静默停止产出,留下与"模型未带证据作答"不可区分的空输出。这两个是相反事实——前者是传输故障(值得重试),后者是已完成的非法响应(永远不该重试)——所以未抵达response.completed的运行一律按传输故障(WEB_SEARCH_REQUEST_FAILED)报告,绝不落入证据校验。这正是 WS-04 的固化。

此外,OpenAI Responses 流式 API 下顶层output_text字段恒为空,正文只存在于尾部messageitem 中(hosted-web-search.ts 的注释与代码明示),因此真正的正文来源是逐 item 遍历拼接output_text部件。

真实证据的投影与上限

hosted-web-search.ts 的projectHostedEvidence把原始输出投影为结构化证据,并且为单次调用设置硬性上限,防止恶意或退化响应淹没模型上下文与 Task 载荷:

上限项值说明
MAX_REPORT_CHARS30 000报告正文最大字符数,超出即截断并告警
MAX_QUERIES32实际运行的 queries 最大条数
MAX_SOURCES32引用来源最大条数(按 URL 去重)
MAX_IMAGES16图像证据最大条数(按 URL 去重)
OPENAI_WEB_SEARCH_IMAGE_RESULTS8请求侧图像结果数量
OPENAI_WEB_SEARCH_TIMEOUT_MS300 000整个研究调用超时(5 分钟)

其中超时值的设定来自一次实测:真实研究是 agentic 的,简单查询秒回,而一次 12 次搜索的交叉验证 brief 实测达 136 秒——旧版 120 秒预算会在流中间掐断这类研究,因此放宽到 300 秒。

生命周期与实时进度(WS-05)

关键事实在 standalone-search.ts 的注释中说得直白:

Codex owns the search tool the model sees... Codex creates onewebSearchitem per call, with the model's own query on it, so a run of three searches renders as three rows without any progress channel. MCP progress cannot do that today, because Codex drops those notifications on receipt (openai/codex#28003).

当前 Runtime 的开始事件只有 identity,query/action 为空,UI 从开始事件立即渲染该行(只显示生命周期与计时),完成事件到达后才显示真实 query 与结果页面。查询失败后,无论对错都不得用 query 反推状态;query 只在协议实际提供后显示。

按调用实时结算(WS-07/WS-08)

standalone-search.ts 的recordSearchUsage展示了结算的精确性:

  • 用量事实的 identity 是buildLlmUsageFactId('web-search', [turnId, requestId])——本次搜索请求而非 Turn,一个 Turn 多次研究不会折叠费用;
  • 用量经editionBilling.settleRealtimeLlmUsage进入 LLM 唯一实时结算入口,按priceCatalogLlmUsage(fact)目录零售价结算,action: 'assistant.web_search';
  • toolCalls由完成的搜索 item 计数而来(hosted-web-search.ts),因为 OpenAI 正是按调用计费,item 流才是实际运行次数的权威;
  • 记账故障绝不让搜索失败:结算异常只记录audit: true的告警日志alert.billing.web_search_usage_unrecorded,研究结果照常返回——WS-08 的落地。

失败路径的端到端处理

standalone-search.ts 的失败路径完整覆盖三种情况:

  1. 用户取消(signal.aborted):取消 provider attempt,仍记录已有用量,然后重抛中止;
  2. 凭据缺失/被拒(WEB_SEARCH_UNAVAILABLE):映射为PROVIDER_CONFIG_UNAVAILABLE(HTTP 503),这是操作员必须看见的配置故障;
  3. 其余 provider 故障:经projectProviderCredentialOwnership+resolveAiProviderAdapter('openai').failure.normalize({ error, phase: 'search' })归一化为 source failure,映射为PROVIDER_SEARCH_RESPONSE_INVALID(HTTP 502),原样浮出,不授权重放。

错误词汇定义在 src/lib/web-search/errors.ts:

WEB_SEARCH_UNAVAILABLE — 能力未配置或凭据被拒(重试无济于事) WEB_SEARCH_REQUEST_FAILED — 传输级故障(限流、网络、5xx、超时、流提前结束) WEB_SEARCH_RESPONSE_INVALID — provider 已作答但无可用证据 WEB_SEARCH_ABORTED — 用户主动取消,绝非 provider 故障

Provider 侧的 mapProviderError 在判定时有意区分两件事:调用方取消(报告为 abort)与研究超时(报告为传输故障),先查 timeout signal 再查 abort,慢搜索不会被误标为取消。

配置一览

该模块的全部配置通过服务端环境变量完成(均从process.env读取,详见 service.ts):

环境变量作用说明
OPENAI_API_KEY搜索凭据缺失即抛WEB_SEARCH_UNAVAILABLE,不降级、不借用其他 provider 凭据(WS-03)
OPENAI_WEB_SEARCH_MODEL搜索模型覆盖可选;缺省用平台默认gpt-5.6-luna;禁止:://路由 key;必须已在定价目录注册价格(WS-02)

任何覆盖模型没有注册价格时,会在请求尚免费时以WEB_SEARCH_UNAVAILABLE拒绝,杜绝"先花钱再让实时结算报错"。

踩过的坑:能力级回归的六次教训

原文档记录了该模块演进中六个真实踩坑,它们是上述不变量得以固化的来源,完整继承如下:

  1. 托管搜索能力曾被连带删除。一次"让 app-server 成为唯一 Agent Runtime"的提交在同一条正文里同时移除了 Creative Worker、托管搜索与旧交互协议——删除决策以 runtime 归一为唯一判据,从未单独评估搜索能力矩阵,三件事合并成一句话使回退不可见。实测代价是能力级的:同一 brief 直连产生 12 步混合动作与 13 个来源,经聚合路由则永远被压平成 1 步且动作细节全被抹掉。结论:任何搜索链路变更必须先给出能力矩阵与通道可见性对比,不得只论"入口是否唯一"。

  2. 能力开关不等于能力。首版只设置能力开关,而处于开发中的特性默认关闭、完全不向模型暴露工具;第二版又把自定义 provider 的显示名伪装成上游名称,smoke 只检查协议 schema 是否包含该类型——schema 与助手正文都不能自证能力,必须有真实完成的检索。

  3. 按名字与文档选型不足以定型。恢复时靠真实运行而非读代码抓到三个缺陷:名字正确的模型被 API 拒绝;流式下顶层输出字段恒为空、正文只在尾部 item 里(短查询侥幸通过而重研究整个丢正文);流被超时掐断后静默停止,空输出撞上证据校验被误报成"答案无依据"。前两个说明选型需实测验证,第三个固化为 WS-04。

  4. 发送侧自证不算证据。恢复托管研究的第一版把搜索做成 MCP Operation,并为"正在读取"新建一条 MCP progress 通道,改动穿过两个共享契约。真实运行后界面始终只有一行静止的"进行中":日志证明发出了 12 条递增进度,而线程里对应的 part 为 0——Runtime 收到即丢弃。两条教训:发送侧自证不算证据(四次修复全部作用在已经正确的一段);不该为一个未验证的通道改共享契约(WS-05)。

  5. 工具拓扑变化必须进入共享 Runtime revision。把搜索从 MCP 切回原生工具后,工具代码已替换,但持久 Codex Thread 仍执行旧的 MCP-only 指令,界面继续投影旧工具行,数据库也没有原生 item——而 smoke 只验证配置与 schema,没有观察真实模型请求。结论:revision 必须由实际契约派生,协议 smoke 必须抓取 app-server 发出的工具与指令契约(对应 CRR-06A/WS-01/WS-05)。

  6. UI 不能假定协议外的事实。原生切换后的 UI 仍按已删除的 MCPsources结果读取页面,并假定开始事件已有 query;真实 Runtime 恰好在开始时发送空 query、完成时才发送 query/action/results,导致运行中与完成后都只有工具标题。结论:读取原生 results,完成后保留 query;开始事件没有的事实不得从模型代码或最近请求猜测(WS-05)。随后又发生过把"不得用 query 反推状态"过度扩大成"不得显示 query",导致检索期间界面完全沉默——生命周期只由统一 resolver 判定;query 只在协议实际提供后显示,当前 Runtime 是完成事件后与结果页面一起保留。

小结

waoowaoo 的 Web Search 模块是一套刻意收窄的系统:一个业务入口(WS-01)、一个平台级托管模型(WS-02)、一套独立凭据(WS-03)、一个证据门控(WS-04)、一套 item 身份驱动的可见性(WS-05)、按次实时结算(WS-07)。它把"联网检索"严格限定为受限的外部研究能力——不是浏览器、不是创作 Worker、更不是凭记忆作答的伪装。对开发者而言,最值得借鉴的是两点:一是用契约与错误词汇把"传输故障"与"无证据作答"这两类相反事实彻底分离;二是用真实运行(而非 schema 检查)作为能力是否可用的唯一判据。

【免费下载链接】waoowaoo

首家工业级全流程 AI 影视生产平台。Industry-first professional AI Agent platform for controllable film & video production. From shorts to live-action with Hollywood-standard workflows.

项目地址:https://gitcode.com/gh_mirrors/wa/waoowaoo
点击查看免费下载

相关推荐

上一篇:如何在嵌入式设备上使用RKNN Model Zoo实现语音识别
下一篇:基于仓库源码的 Bitwarden Android 代码评审指南:MVVM、Compose 与依赖注入的审查清单与流程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 8:19:50

Navicat Premium v12.1.17 绿色版实战:连接配置、排错与进阶技巧

简介&#xff1a;Navicat Premium v12.1.17(x64) 绿色中文注册版面向数据库开发与运维人员&#xff0c;尤其适合需要同时连接 MySQL、Oracle 等多种数据库、又希望免安装快速部署的初中级使用者。压缩包共 118 个文件&#xff0c;以 101 个 dll 动态库为主&#xff0c;辅以 exe…

作者头像 李华
网站建设 2026/10/18 6:31:32

Java后端转型Agent开发:从最小循环到生产级落地的完整路径

我做了八年 Java 后端&#xff0c;从 Spring Boot 一路写到微服务&#xff0c;但最近半年被问得最多的一句话是&#xff1a;Javaer 转 Agent&#xff0c;第一步到底要理解什么&#xff1f;今天就用一个 Java 开发者的思维方式&#xff0c;把 Agent 是什么、怎么拆、怎么跑起来、…

作者头像 李华
网站建设 2026/10/17 22:47:32

动态规划——背包问题

1、完全平方数Q&#xff1a;给你一个整数 n &#xff0c;返回 和为 n 的完全平方数的最少数量 。完全平方数 是一个整数&#xff0c;其值等于另一个整数的平方&#xff1b;换句话说&#xff0c;其值等于一个整数自乘的积。例如&#xff0c;1、4、9 和 16 都是完全平方数&#x…

作者头像 李华