Web 搜索与抓取
模型访问互联网走两条有界的 seam:搜索返回带标题与 URL 的命中列表,抓取把页面取回并分类成可读的文本。
对应实现:src/Tether.Web/(seam)、src/Tether.Web.Http/(providers)与 src/Tether.Web.Tools/(工具)。
seam 与确定性选择
public interface IWebService
{
IDisposable RegisterSearchProvider(IWebSearchProvider provider);
IDisposable RegisterFetchProvider(IWebFetchProvider provider);
ValueTask<WebSearchResult> SearchAsync(WebSearchRequest request, CancellationToken ct = default);
ValueTask<WebFetchResult> FetchAsync(string url, CancellationToken ct = default);
}
Provider 选择是确定性的:按配置的偏好顺序(SearchPreference / FetchPreference),同 id 以字典序破平——
永远不看注册顺序。注册同一个 id 是替换而非叠加,返回的 IDisposable 只摘除自己那份登记。
工具是否启用与 provider 是否可用是两件事:IsAvailableAsync(例如凭据此刻能否解析出来)只影响运行期选择,
工具本身始终注册着,不可用时以稳定码失败而不是让工具神秘消失。
内置搜索 provider
Tether.Web.Http 带三个 HTTP provider;凭据在每次请求前重新解析,因此轮换后的下一次请求立即生效:
- Exa、Perplexity —— 各家 wire 形状封在各自 provider 内;
- DeepSeek(provider id
deepseek-official)——单 query provider 私有地调用 Anthropic MessagesPOST /messages,默认 basehttps://api.deepseek.com/anthropic/v1、modeldeepseek-v4-flash、max_tokens: 4096,并携带{ type: "web_search_20250305", name: "web_search", max_uses: 5 }native server tool。请求同时发送x-api-key、Authorization: Bearer与anthropic-version: 2023-06-01;这些 secret/header 不进入事件或快照。
先 durable,再派发
每次 DeepSeek dispatch 都先 append 并 flush web/deepseek-search-llm-request,记录 provider/model、
exact messages、native tool config、query/source attribution、credential reference 与所有非 secret resolved options。
只有持久化成功后才允许发网络请求;失败或取消不会留下逃逸的 model-visible auxiliary input。该契约只依赖
Coordinator 提供的 ISessionPersistence;composition 在 JSONL 与 SQLite 之间替换物理
ISessionEventStore 时,这条 consumer 侧 durability fence 不变。
响应只接受真实 structured web_search_tool_result。命中按 URL 去重,title / url / page_age 来自 result item,
snippet 从 text block 的 citation 按 URL 关联;malformed shape、HTTP error、provider refusal、partial result 与 abrupt EOF
都归一成稳定的 WEB_PROVIDER,调用方取消仍原样传播且永不返回 partial success。
请求发出之后的这些失败还会带上 endpoint 恢复指引:消息尾部追加已解析的 Messages endpoint 绝对 URI、
“搜索 endpoint 与 chat 模型配置相互独立”的说明、承载它的 web-search-deepseek 组合行 baseUrl 配置项,
以及”只有用户可以选择或更改 endpoint”。非 2xx 响应体里的 error.message / error / message detail 是追加在
HTTP 状态描述之后,读不出来时只损失更丰富的消息、不改变失败本身。凭据缺失与取消发生在 dispatch 之前或之外,
不带指引。指引同时存在于 WebException.Guidance 结构化字段里:Tether.Web.Tools 仍然把 provider 原始消息
替换成稳定文本(远端错误体不进入模型可见输出),但会把这段 secret-free 指引一起交给模型。
抓取侧 HttpFetchProvider 是纯 HttpClient 实现:重定向、字节数、字符数、时间四重上限(默认最多 5 次重定向),
取回的 body 分类成 WebFetchKind——可解码为文本则给出 Text,二进制以 WEB_BINARY 拒绝。
有界多查询搜索
web_search 工具保留 #27 对齐的 bounded multi-query 语义,接受 1–4 个 query 的数组而不是单字符串:
- 归一化。 去重、去空白;超上限直接参数错误,不静默截断。
- 并发扇出。 每个 query 独立成一次
WebSearchRequest并发执行,整体有超时;任一失败立即 abort 其余——部分结果不凑数。 - round-robin 合并。 各 query 的命中按轮询交错合并、按 URL 去重,总结果封顶(默认 8 条)——避免第一个 query 的好结果把后面的全挤掉。
- 有界答案。 provider 附带的 answer 摘要截断到 4 000 字符。
搜索结果要求模型在回复里内联引用 URL;多查询的形状让模型一次调用覆盖一个话题的多个侧面,而不是串行发四次请求。
稳定失败码
| 码 | 含义 |
|---|---|
WEB_NO_PROVIDER | 没有任何可用 provider |
WEB_BAD_URL / WEB_HTTP_STATUS | URL 非法 / 上游非成功状态 |
WEB_TOO_MANY_REDIRECTS / WEB_TOO_LARGE | 抓取越限 |
WEB_BINARY | body 不是可分类文本 |
WEB_TIMEOUT | 超时 |
WEB_MISSING_CREDENTIAL | provider 需要但解析不到凭据 |
WEB_PROVIDER | provider 侧其它失败 |