【免费下载链接】waoowaoo
首家工业级全流程 AI 影视生产平台。Industry-first professional AI Agent platform for controllable film & video production. From shorts to live-action with Hollywood-standard workflows.
本文深入解析 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 |
从源码看,这层设计意图非常清晰:
业务调用方不接触任何 OpenAI 客户端。service.ts 的注释明确指出:所有调用方(Primary Operation 与 Creative Direction Worker 工具)都经由
searchWeb,既不构造 OpenAI client,也不构造 agent 或 hosted tool,从而保证"整个产品只有一个 provider 决策、一次凭据读取、一套失败词汇"。Agent 到达路径被网关接管。Codex 拥有模型可见的搜索工具,因此 standalone-search.ts 提供了独立端点(
/api/internal/codex-runtime/model/alpha/search)。Codex 每次调用创建一个webSearchitem,三次搜索渲染为三行,无需 MCP progress 通道——因为 Codex 在收到 MCP progress 通知时会直接丢弃。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_CHARS | 30 000 | 报告正文最大字符数,超出即截断并告警 |
MAX_QUERIES | 32 | 实际运行的 queries 最大条数 |
MAX_SOURCES | 32 | 引用来源最大条数(按 URL 去重) |
MAX_IMAGES | 16 | 图像证据最大条数(按 URL 去重) |
OPENAI_WEB_SEARCH_IMAGE_RESULTS | 8 | 请求侧图像结果数量 |
OPENAI_WEB_SEARCH_TIMEOUT_MS | 300 000 | 整个研究调用超时(5 分钟) |
其中超时值的设定来自一次实测:真实研究是 agentic 的,简单查询秒回,而一次 12 次搜索的交叉验证 brief 实测达 136 秒——旧版 120 秒预算会在流中间掐断这类研究,因此放宽到 300 秒。
生命周期与实时进度(WS-05)
关键事实在 standalone-search.ts 的注释中说得直白:
Codex owns the search tool the model sees... Codex creates one
webSearchitem 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 的失败路径完整覆盖三种情况:
- 用户取消(
signal.aborted):取消 provider attempt,仍记录已有用量,然后重抛中止; - 凭据缺失/被拒(
WEB_SEARCH_UNAVAILABLE):映射为PROVIDER_CONFIG_UNAVAILABLE(HTTP 503),这是操作员必须看见的配置故障; - 其余 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拒绝,杜绝"先花钱再让实时结算报错"。
踩过的坑:能力级回归的六次教训
原文档记录了该模块演进中六个真实踩坑,它们是上述不变量得以固化的来源,完整继承如下:
托管搜索能力曾被连带删除。一次"让 app-server 成为唯一 Agent Runtime"的提交在同一条正文里同时移除了 Creative Worker、托管搜索与旧交互协议——删除决策以 runtime 归一为唯一判据,从未单独评估搜索能力矩阵,三件事合并成一句话使回退不可见。实测代价是能力级的:同一 brief 直连产生 12 步混合动作与 13 个来源,经聚合路由则永远被压平成 1 步且动作细节全被抹掉。结论:任何搜索链路变更必须先给出能力矩阵与通道可见性对比,不得只论"入口是否唯一"。
能力开关不等于能力。首版只设置能力开关,而处于开发中的特性默认关闭、完全不向模型暴露工具;第二版又把自定义 provider 的显示名伪装成上游名称,smoke 只检查协议 schema 是否包含该类型——schema 与助手正文都不能自证能力,必须有真实完成的检索。
按名字与文档选型不足以定型。恢复时靠真实运行而非读代码抓到三个缺陷:名字正确的模型被 API 拒绝;流式下顶层输出字段恒为空、正文只在尾部 item 里(短查询侥幸通过而重研究整个丢正文);流被超时掐断后静默停止,空输出撞上证据校验被误报成"答案无依据"。前两个说明选型需实测验证,第三个固化为 WS-04。
发送侧自证不算证据。恢复托管研究的第一版把搜索做成 MCP Operation,并为"正在读取"新建一条 MCP progress 通道,改动穿过两个共享契约。真实运行后界面始终只有一行静止的"进行中":日志证明发出了 12 条递增进度,而线程里对应的 part 为 0——Runtime 收到即丢弃。两条教训:发送侧自证不算证据(四次修复全部作用在已经正确的一段);不该为一个未验证的通道改共享契约(WS-05)。
工具拓扑变化必须进入共享 Runtime revision。把搜索从 MCP 切回原生工具后,工具代码已替换,但持久 Codex Thread 仍执行旧的 MCP-only 指令,界面继续投影旧工具行,数据库也没有原生 item——而 smoke 只验证配置与 schema,没有观察真实模型请求。结论:revision 必须由实际契约派生,协议 smoke 必须抓取 app-server 发出的工具与指令契约(对应 CRR-06A/WS-01/WS-05)。
UI 不能假定协议外的事实。原生切换后的 UI 仍按已删除的 MCP
sources结果读取页面,并假定开始事件已有 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.
相关推荐
使用 Agno 构建 Parallel 驱动的 Web 研究 Agent:从单 Agent 搜索到可部署的 AgentOS 研究应用
使用 Agno 构建 Parallel 驱动的 Web 研究 Agent:从单 Agent 搜索到可部署的 AgentOS 研究应用 本文基于 cookbook
人工智能大模型AI AgentAgent 框架多智能体工具调用RAGAgent 工作流Agent 记忆Backstage Search 架构深度解析:可插拔搜索引擎与统一索引流水线
Backstage Search 架构深度解析:可插拔搜索引擎与统一索引流水线 导读:本文基于 Backstage 官方文档《Search Architectu
开发者门户后端前端用 Win11Debloat 免费完成 Windows 系统优化:一次批量移除 150+ 预装应用
用 Win11Debloat 免费完成 Windows 系统优化:一次批量移除 150+ 预装应用 新装的 Windows 11 用不了多久就会暴露问题:C 盘
前端UI组件3D渲染跨平台游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考