主题
给 Agent 加两个联网工具——web_search(搜实时信息)和 web_fetch(抓网页正文),背后是「多 provider 抽象 + SSRF 安全策略 + HTML 正文提取」三层设计。
联网搜索实现
两条工具链。
链路一:web_search。
- 先判断是否
shouldPreferStepSearch——当前模型是step-3.7-flash且 StepSearch MCP 就绪时,优先转调 MCP 的web_search,失败再回退; - 否则走
SearchProviderFactory.create选一个搜索 provider(智谱 / SerpAPI / SearXNG 三家); provider.search(query, topK)执行,返回带position(编号,方便 LLM 引用)/title/url/snippet/source(域名)的结果;- 格式化回给 LLM。
链路二:web_fetch。
- 安全校验:
NetworkPolicy.checkUrl先查 URL(scheme 白名单 http/https + 主机黑名单防 SSRF),再acquire申请限流配额; WebFetcher.fetch拿 HTML(5MB 上限、30 秒超时);HtmlExtractor.extract用 Jsoup 做「极简版 readability」,把 HTML 提取成正文 Markdown;- 按
max_chars截断,格式化返回。
【主动抛深度点 1:多 provider 抽象 + 工厂自动选择】 搜索 provider 用 SearchProvider 接口统一(name / isReady / search),三家实现:智谱(国内首选,和 GLM 推理共用 key)、SerpAPI(商业聚合、开箱即用)、SearXNG(开源自托管、免费)。工厂的自动选择优先级是:有 GLM_API_KEY → 智谱,有 SERPAPI_KEY → SerpAPI,有 SEARXNG_URL → SearXNG,都没有 → 占位 provider(isReady=false,由调用方提示用户配 key)。加新 provider(Brave/Tavily/Exa)只要实现接口,调用方零改动——这和上一期多模型适配是同一个策略模式思路。
【主动抛深度点 2:NetworkPolicy 的 SSRF 防护】 web_fetch 最大的安全风险是 SSRF(服务端请求伪造)——Agent 可能被诱导去抓 http://localhost:6379 这种内网地址。所以 checkHost 做了防护:字面量黑名单(localhost / 0.0.0.0)+ DNS 解析后检查真实 IP(loopback / site-local / link-local / any-local 全部拒绝,)。再加一个 token bucket 限流(60 秒 30 次),防止 Agent 死循环狂发请求。
搜索 provider 怎么选的?为什么要三家?
工厂 SearchProviderFactory 自动选择:按 GLM_API_KEY → SERPAPI_KEY → SEARXNG_URL 的优先级,也可以显式 SEARCH_PROVIDER 指定。三家对应三种需求:智谱国内可用、SerpAPI 国际付费、SearXNG 自托管免费。核心是让用户按成本 / 数据合规 / 离线需求自由切换,而不绑定单一搜索引擎。
web_fetch 怎么防 SSRF?
NetworkPolicy.checkUrl 两层:scheme 白名单只放 http/https;主机校验分两步——先字面量黑名单(localhost、0.0.0.0),再 InetAddress.getAllByName 做真实 DNS 解析,检查解析出来的 IP 是不是 loopback / site-local / link-local / any-local,是就拒绝。这防的是「域名解析到内网地址」这种绕过。文档里也诚实说明:面向严苛企业环境还要补 DNS rebinding 防护和 CIDR 白名单。
HTML 怎么转成正文 Markdown?
HtmlExtractor 用 Jsoup 实现「极简版 readability」:先清掉噪音标签(script/style/nav/footer 等)和噪音 class(sidebar/menu/ad 等),再给每个元素打分(文本密度、标签权重),选出「主正文元素」,最后递归渲染成 Markdown(标题/列表/表格)。本质是正文提取,不是完整 HTML 转 Markdown。
遇到 JS 渲染或防爬墙的页面怎么办?
这期明确不重试。FetchResult 里有 bodyEmpty 和 hint 字段,抓到了 HTML 但提取不出正文时,返回「可能是 JS 渲染或防爬墙,本期范围内不再重试」的提示,让 LLM 知道这是已知边界、不要反复重试。真正解决要等后面 Chrome DevTools MCP(浏览器渲染)那期。这体现了「每期划清边界」的工程态度。
step_search 转调是什么?
一个特判优化:当当前模型是 step-3.7-flash 且 StepSearch MCP 工具就绪时,内置 web_search / web_fetch 会优先转调 MCP 版的工具(StepSearch 的搜索结果质量更好),失败再回退到内置的 SearchProvider / WebFetcher。shouldPreferStepSearch 就是那个开关,putIfStepToolAccepts 负责把 topK/maxChars 参数适配到 MCP 工具自己的参数名。
源码锚点(读码定位用,面试不主动讲)
ToolRegistry.java:847ToolRegistry.java:910ToolRegistry.java:914-921ToolRegistry.java:935-936NetworkPolicy.java:98