|
| 1 | +# WEB_SEARCH_TOOL — 网页搜索工具 |
| 2 | + |
| 3 | +> 实现状态:适配器架构完成,Bing 适配器为当前默认后端 |
| 4 | +> 引用数:核心工具,无 feature flag 门控(始终启用) |
| 5 | +
|
| 6 | +## 一、功能概述 |
| 7 | + |
| 8 | +WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool),在第三方代理端点下不可用。现已重构为适配器架构,新增 Bing 搜索页面解析作为 fallback,确保任何 API 端点都能使用搜索功能。 |
| 9 | + |
| 10 | +## 二、实现架构 |
| 11 | + |
| 12 | +### 2.1 适配器模式 |
| 13 | + |
| 14 | +``` |
| 15 | +WebSearchTool.call() |
| 16 | + │ |
| 17 | + ▼ |
| 18 | + createAdapter() ← 适配器工厂 |
| 19 | + │ |
| 20 | + ├── ApiSearchAdapter — Anthropic 官方 API 服务端搜索 |
| 21 | + │ └── 使用 web_search_20250305 server tool |
| 22 | + │ 通过 queryModelWithStreaming 二次调用 API |
| 23 | + │ |
| 24 | + └── BingSearchAdapter — Bing HTML 抓取 + 正则提取(当前默认) |
| 25 | + └── 直接抓取 Bing 搜索页 HTML |
| 26 | + 正则提取 b_algo 块中的标题/URL/摘要 |
| 27 | +``` |
| 28 | + |
| 29 | +### 2.2 模块结构 |
| 30 | + |
| 31 | +| 模块 | 文件 | 说明 | |
| 32 | +|------|------|------| |
| 33 | +| 工具入口 | `src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 | |
| 34 | +| 工具 prompt | `src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 | |
| 35 | +| UI 渲染 | `src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 | |
| 36 | +| 适配器接口 | `src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 | |
| 37 | +| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 | |
| 38 | +| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool | |
| 39 | +| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 | |
| 40 | +| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 32 个测试用例 | |
| 41 | +| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 真实网络请求验证 | |
| 42 | + |
| 43 | +### 2.3 数据流 |
| 44 | + |
| 45 | +``` |
| 46 | +模型调用 WebSearchTool(query, allowed_domains, blocked_domains) |
| 47 | + │ |
| 48 | + ▼ |
| 49 | + validateInput() — 校验 query 非空、allowed/block 不共存 |
| 50 | + │ |
| 51 | + ▼ |
| 52 | + createAdapter() → BingSearchAdapter(当前硬编码) |
| 53 | + │ |
| 54 | + ▼ |
| 55 | + adapter.search(query, { allowedDomains, blockedDomains, signal, onProgress }) |
| 56 | + │ |
| 57 | + ├── onProgress({ type: 'query_update', query }) |
| 58 | + │ |
| 59 | + ├── axios.get(bing.com/search?q=...&setmkt=en-US) |
| 60 | + │ └── 13 个 Edge 浏览器请求头 |
| 61 | + │ |
| 62 | + ├── extractBingResults(html) — 正则提取 <li class="b_algo"> 块 |
| 63 | + │ ├── resolveBingUrl() — 解码 base64 重定向 URL |
| 64 | + │ ├── extractSnippet() — 三级降级摘要提取 |
| 65 | + │ └── decodeHtmlEntities() — he.decode |
| 66 | + │ |
| 67 | + ├── 客户端域名过滤 (allowedDomains / blockedDomains) |
| 68 | + │ |
| 69 | + ├── onProgress({ type: 'search_results_received', resultCount }) |
| 70 | + │ |
| 71 | + ▼ |
| 72 | + 格式化为 markdown 链接列表返回给模型 |
| 73 | +``` |
| 74 | + |
| 75 | +## 三、Bing 适配器技术细节 |
| 76 | + |
| 77 | +### 3.1 反爬绕过 |
| 78 | + |
| 79 | +使用 13 个 Edge 浏览器请求头(含 `Sec-Ch-Ua`、`Sec-Fetch-*` 等),避免 Bing 返回 JS 渲染的空页面: |
| 80 | + |
| 81 | +```typescript |
| 82 | +const BROWSER_HEADERS = { |
| 83 | + 'User-Agent': '...Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0', |
| 84 | + 'Sec-Ch-Ua': '"Microsoft Edge";v="131", "Chromium";v="131", ...', |
| 85 | + 'Sec-Fetch-Dest': 'document', |
| 86 | + 'Sec-Fetch-Mode': 'navigate', |
| 87 | + 'Sec-Fetch-Site': 'none', |
| 88 | + 'Sec-Fetch-User': '?1', |
| 89 | + // ... 共 13 个标头 |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +`setmkt=en-US` 参数强制美式英语市场,避免 IP 地理定位导致区域化结果。 |
| 94 | + |
| 95 | +### 3.2 URL 解码(`resolveBingUrl()`) |
| 96 | + |
| 97 | +Bing 返回的重定向 URL 格式:`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...` |
| 98 | + |
| 99 | +- `u` 参数前 2 字符为协议前缀:`a1` = https,`a0` = http |
| 100 | +- 剩余部分为 base64url 编码的真实 URL |
| 101 | +- Bing 内部链接和相对路径被过滤返回 `undefined` |
| 102 | + |
| 103 | +### 3.3 摘要提取(`extractSnippet()`) |
| 104 | + |
| 105 | +三级降级策略: |
| 106 | + |
| 107 | +1. `<p class="b_lineclamp...">` — Bing 的搜索摘要段落 |
| 108 | +2. `<div class="b_caption">` 内的 `<p>` — 备选摘要位置 |
| 109 | +3. `<div class="b_caption">` 直接文本 — 最终 fallback |
| 110 | + |
| 111 | +### 3.4 域名过滤 |
| 112 | + |
| 113 | +客户端侧实现,支持子域名匹配: |
| 114 | +- `allowedDomains`:白名单,结果域名必须匹配列表中的某项(含子域名) |
| 115 | +- `blockedDomains`:黑名单,匹配的结果被过滤 |
| 116 | +- 两者不可同时使用(`validateInput` 校验) |
| 117 | + |
| 118 | +## 四、适配器选择逻辑 |
| 119 | + |
| 120 | +当前 `createAdapter()` 硬编码返回 `BingSearchAdapter`,原逻辑已注释保留: |
| 121 | + |
| 122 | +```typescript |
| 123 | +export function createAdapter(): WebSearchAdapter { |
| 124 | + return new BingSearchAdapter() |
| 125 | + // 注释保留的选择逻辑: |
| 126 | + // 1. WEB_SEARCH_ADAPTER 环境变量强制指定 api|bing |
| 127 | + // 2. isFirstPartyAnthropicBaseUrl() → API 适配器 |
| 128 | + // 3. 第三方端点 → Bing 适配器 |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +恢复自动选择:取消 `index.ts` 中的注释即可。 |
| 133 | + |
| 134 | +## 五、接口定义 |
| 135 | + |
| 136 | +### WebSearchAdapter |
| 137 | + |
| 138 | +```typescript |
| 139 | +interface WebSearchAdapter { |
| 140 | + search(query: string, options: SearchOptions): Promise<SearchResult[]> |
| 141 | +} |
| 142 | + |
| 143 | +interface SearchResult { |
| 144 | + title: string |
| 145 | + url: string |
| 146 | + snippet?: string |
| 147 | +} |
| 148 | + |
| 149 | +interface SearchOptions { |
| 150 | + allowedDomains?: string[] |
| 151 | + blockedDomains?: string[] |
| 152 | + signal?: AbortSignal |
| 153 | + onProgress?: (progress: SearchProgress) => void |
| 154 | +} |
| 155 | + |
| 156 | +interface SearchProgress { |
| 157 | + type: 'query_update' | 'search_results_received' |
| 158 | + query?: string |
| 159 | + resultCount?: number |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +### 工具 Input Schema |
| 164 | + |
| 165 | +```typescript |
| 166 | +{ |
| 167 | + query: string // 搜索关键词,最少 2 字符 |
| 168 | + allowed_domains?: string[] // 域名白名单 |
| 169 | + blocked_domains?: string[] // 域名黑名单 |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## 六、文件索引 |
| 174 | + |
| 175 | +| 文件 | 职责 | |
| 176 | +|------|------| |
| 177 | +| `src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 | |
| 178 | +| `src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt | |
| 179 | +| `src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 | |
| 180 | +| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 | |
| 181 | +| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 | |
| 182 | +| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 | |
| 183 | +| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 | |
| 184 | +| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) | |
| 185 | +| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 | |
| 186 | +| `src/tools.ts` | 工具注册 | |
0 commit comments