wigolo 给 AI Agent 的不是搜索结果,是 byte 级证据链

发布于 2026年07月20日 00:36 #Github 解读#Agent 抓取 原文链接

wigolo 给 AI Agent 的不是搜索结果,是 byte 级证据链 封面图
  • wigolo 是本地优先的 MCP server,1667 star、AGPL-3.0,10 个 web 工具打成 stdio 接口,跟 Tavily/Exa 的真正分水岭是每条证据带 byte 偏移的 source_span,Agent 可拿 start/end 回原文切片比对
  • 18 个搜索引擎 adapter 按 snippet 质量分 high/medium/low 三档喂给 RRF,挂掉任意单引擎对结果几乎无感,还用 score-floor 砍掉 reranker 给出的近零垃圾结果
  • 三层 fetch 路由 plain HTTP → TLS 仿冒 → headless 浏览器靠观察信号升级,react.dev 这类 SSR 站被硬编码白名单强制第一跳走 Playwright,避免 hydration 前误判为空
  • 四条 README 没写的边界:SSRF 守卫只校验字面量不防 DNS rebinding(issue #206 未解决)、keyless 体验是 degraded 非完整、bus factor=1(主作者 1861 commits vs 第二名 7)、对比 benchmark 是 n=1 自测无统计意义

大家好,我是若风。

前两天我把 Claude Code 接到 web 上查个 PostgreSQL logical replication 的细节,它给我一段答得很流畅的总结,引用来源也给得很整齐。可当我想顺着引用回到原文那段话去比对的时候,发现那段「quote」其实是 Agent 自己拼出来的语义近似文本,原页面上根本没有完全一样的句子。

这就是现在大部分「AI Agent + Web」工具的通病,它们给你的是被 Agent 咀嚼过的答案,不是可以被你独立审计的证据。你想想看,如果你都不能验证来源,那个答案的可信度到底建立在什么上面?

Tavily、Exa、Firecrawl 这一票商用 API 解决了「Agent 能上网」这件事,但每条查询都按 metered 计费,key 必须配,数据得过它们的云。今天要拆的这个项目 wigolo(KnockOutEZ/wigolo,1667 star,2026 年 4 月才创建)想换一条路,本地优先,零 key,零账单,但它真正让我觉得有意思的不是这些营销词,而是它给 Agent 返回的每一行结果里都塞了一个东西,byte 偏移的 source_span

一句话定位

wigolo 是一个用 TypeScript 写的 MCP server,把搜索、抓取、爬取、抽取、缓存、相似度、研究、自主采集这 10 个 web 工具打成一个 stdio 接口,挂在 AI Agent 旁边。它的全部依赖是 Node ≥ 20 + 约 1.5GB 磁盘(一个浏览器引擎 + 几个本地 ML 模型),跑起来就在 ~/.wigolo/ 里自循环,不联任何第三方云。

跑医生一样跑一下。

npx wigolo init --agents=claude-code
npx wigolo doctor

init 默认 unattended,会下载浏览器引擎和本地模型、跑健康检查、按组件打印就绪报告。哪个组件没下来它当场告诉你,不会留到 Agent 第一次调用时静默失败。

先看整张图

往下拆之前,先把 wigolo 的 5 层架构摆出来,后面每个章节都在讲其中某一层。

wigolo 系统架构
wigolo 系统架构

最上层是接入,MCP stdio / REST / SDK 三种协议挂在同一个 Node 进程上。第二层是 10 个 tool handler,它们都很薄,真正的活全在第三层的 search/fetch 双引擎。再往下是本地资源层,SQLite + sqlite-vec 做缓存和向量检索,ONNX cross-encoder 做 rerank,全部躺在 ~/.wigolo/ 里。最底层是数据出口,公开 web 是默认通道,LLM 是 opt-in 的虚线分支。整个图里没有第三方云,这是 wigolo 跟 Tavily/Exa 最直观的物理差异。

不是搜索结果,是 byte 级证据链

这一段是 wigolo 最值得拆的地方,也是它跟 Tavily/Exa 这些服务的真正分水岭。

打开 src/search/evidence.ts,看它给 Agent 拼一条证据时干的事。每个 evidence item 都不是「这句话出现在这个 URL」,而是把原文里那一段 verbatim 截出来,配上 source_span 的 byte 偏移:

{
  "excerpt": "Logical replication is a method of replicating data objects…",
  "source_span": { "start": 1042, "end": 1305 },
  "evidence_score": { "final": 0.86, "semantic": 0.91, "lexical": 0.78, "engine_consensus": 3 }
}

意思是 Agent 拿到这段引用,可以拿着 start/end 这两个 byte 数直接回到原文切片比对。这种 byte 级溯源是 Tavily、Exa、Firecrawl 当前 response 里都没有的字段,也是 README 那张对比表里 wigolo 唯一独家打勾的列。这条不是营销话术,我读了源码,确实就是字面意义的 byte offset,由 extractHighlights 在原文上扫出来的。

更有意思的是 wigolo 在拼 evidence 时还做了两道过滤,避免把 nav、footer、sidebar 那种 boilerplate 当成证据返回:

const MIN_EVIDENCE_EXCERPT_CHARS = 40;
const MAX_LINK_MARKUP_RATIO = 0.5;

第一道是「太短的不算」,少于 40 字符的段落直接丢。第二道是「markdown 链接 markup 占比超过一半的直接丢」,因为这种文本通常是从导航/页脚抓出来的链接堆。这种工程细节是看 README 看不出来的,得翻源码才知道作者踩过多少次「Agent 引用了一段其实是面包屑」的坑。

再翻 src/search/core/score-floor.ts,有个 DEFAULT_SEARCH_SCORE_FLOOR = 0.05,专门砍掉 reranker 给出的「几乎为零」的垃圾结果。源码注释直接挂在一次 benchmark fixture 上,2026-06-14 那次 A1 测试,Cambridge 字典的结果被 cross-encoder 打到 0.0097 和 0.0003,reranker 知道它们不对,但没人删,它们就占了 top-N 槽位。这个 floor 就是补这一刀的。

这整套设计的潜台词是,工具的输出格式不是给人看的,是给 Agent 自己审阅的。可解释打分、可定位溯源、可标失败,Agent 拿到能自己判断哪条该信、哪条该扔、哪条是挑战页伪装成内容。这是个能迁移的判断,我后面还会再展开。

18 个引擎的 rank fusion

wigolo 的「多引擎搜索」不是宣传语,是 src/search/engines/ 下实打实的 18 个 adapter 文件,arxiv、bing、bing-news、brave、brave-image、ddg-image、devdocs、duckduckgo、github-code、hn-algolia、lobsters、marginalia、mdn、mojeek、semantic-scholar、stackoverflow、wikipedia,加上一个公共的 user-agents 配置。

但「18 个引擎」这个数字要打一个折扣。打开 src/search/core/engine-base.ts,作者自己把每个引擎按 snippet 质量分了三档,high / medium / low

  • high:有结构化 JSON/API、稳定 schema、rich snippet。举例 StackOverflow API、Wikipedia OpenSearch、MDN docs API。
  • medium:HTML 抓取或结构化 feed,snippet 有用但可能很薄或噪声多。Bing、DDG Lite、Brave web、HN Algolia、arXiv、Semantic Scholar 都在这一档。
  • low:snippet 稀薄或者只返回元数据没正文。devdocs 直接被注释标成「static slug table, no body content」,lobsters 标成「often returns N score / N comments rather than evidence」。

这个 tier 不是装饰,是直接喂给 RRF(reciprocal rank fusion)做权重的。engine-base.ts 里的 EngineEntry 接口还允许标记 secondary: true,专门用来防止 MDN 这种 code vertical 引擎在数据库/库查询里喧宾夺主。

RRF 本身不是 wigolo 的发明,是个老算法。但 wigolo 把它跟 quality tier + secondary 标记组合起来用,效果是「任意单个引擎挂掉对结果几乎无感」。README FAQ 里那句「any one failing barely moves results」我看了一遍 source 里的 engine-health.ts 和 orchestrator,确实是按这个目标构造的,挂掉的引擎会在 output 里被报告(engine_consensus 这个字段就是参与共识的引擎数),不会被偷偷藏起来。

三层 fetch 路由,靠观察信号升级

抓一个 URL,wigolo 不是一上来就开浏览器。src/fetch/router.ts 是一个三层阶梯,plain HTTP → TLS impersonation tier → headless browser。爬梯子的判断不是「这个 domain 估计要 JS」,而是看到了什么信号。

const KNOWN_SPA_DOMAINS = new Set<string>([
  'react.dev', 'nextjs.org', 'vuejs.org', 'svelte.dev',
  'angular.io', 'angular.dev', 'preactjs.com', 'solidjs.com',
  'remix.run', 'astro.build', 'nuxt.com',
]);

这个硬编码白名单有意思,它存在的原因写在注释里。react.dev 这类站点 SSR 阶段渲染了足够多的导航文本,HTTP-first 的「内容为空」阈值判断会误以为页面已经渲染完,但其实文章正文要等 hydration 之后才挂载。所以这些域名被强行第一跳直接走 Playwright。

你看,wigolo 的 SPA 检测对不在这个名单里的新站点,仍然依赖「内容看起来空」的启发式判断。这是个会误判的边界,作者没有假装自己解决完了。

爬到第三层还有一组配套机制。src/fetch/clearance-reuse.ts 里有个 CLEARANCE_COOKIE_NAME,过了 challenge 的 cookie 会被复用,并且按 domain 记录 tier 和 clearance,过期再丢。还有一个 wigolo tune list 命令,可以查 wigolo 对每个 domain 学到了什么(用哪个 fetch tier、challenge clearance、backoff 多久),可以手动 unlearn。这套机制是诚实设计,不藏着自己学到了什么。

本地 reranker 的三段 boost

src/search/rerank.ts 拆开看,rerank 流水线是这样的:

  1. 如果 config.reranker === 'onnx',用本地 ONNX cross-encoder 给每个 candidate 重新打分
  2. applyConsensusBoost:多个引擎都返回同一结果,加分(就是前面 engine_consensus 字段的来源)
  3. applyAuthorityBoost:权威性加成
  4. applyRecencyBoost:时效性加成
  5. 最后按 relevance_score 排序,过一遍 applyThreshold 阈值

这套设计的核心是「能确定的活不用 LLM」。README 里「Code beats model」这句不是空话,看 src/search/core/ 下面有 canonical-url、dedup、rare-terms、lexical-alignment、recency-boost 一堆纯 TypeScript 的确定性算法,LLM 只在 research / agent / search format=answer 这三个需要写自然语言总结的工具里被用到,而且是 opt-in 的。

这是 wigolo 跟很多 AI 工具的根本分野。它的「智能」不在 LLM 调用密度上,而在工程确定性上。canonical URL 归一化、rank fusion、dedup、schema 匹配,全是确定性算法解决。LLM 留给真正需要判断的事,并且每请求有 cap,LLM 填的字段还要回原文校验,校验不过直接 null。

埋在源码和 issue 里的几道边界

讲到这里不批判就不是 ruofeng 风格了。下面这几条都是我自己翻源码和 issue 区挖到的,README 里要么轻描淡写要么根本没提。

第一,SSRF 守卫只校验字面量。打开 src/watch/ssrf.ts,注释原话「DNS rebinding is out of scope — we never actually resolve here」。也就是说,如果你给一个 public hostname,它的 A/AAAA 记录指向云元数据地址 169.254.169.254,或者指向 RFC 1918 内网地址,这个守卫是挡不住的。serve 模式跨 loopback 部署时尤其危险,loopback 保护形同虚设。issue #206 就是来报这个的,作者很坦诚地说还在想是先做 resolved-IP re-check 还是直接在 connector 层 pin,但目前没有解决。如果你想把 wigolo 暴露到非本机用,这是一条必须自己补的洞。

第二,「无 key」是有条件的。README 顶部大字「no API keys, no cloud, $0/query」,但往下翻到「Recommended — a free key makes research & agent shine」这一段,作者自己说 research / agent / search format=answer 这三个工具不加 LLM 的话只返回 raw brief,体验「much thinner」。整个 surface 不是无条件的 keyless,是「keyless but degraded」。search / fetch / crawl / extract / cache / find_similar 这 6 个核心工具确实零 key,但要看完整体验还得配一个 free Gemini key。这条不读 README 中段根本发现不了。

第三,bus factor = 1gh api contributors 返回 5 个人,但 KnockOutEZ 一个人贡献了 1861 个 commit,第二名 ashrafulislambd 只有 7 个,剩下三个都是 1-3 个。README 里「the one developer who wrote the code」不是修辞,是事实描述。对于一个声称要长期维护的基础设施项目,这是必须诚实说出来的风险。作者自己也很坦白,靠 donations 和 AGPL 法律保护来防止 bait-and-switch,但没有第二个核心维护者这件事,是用户自己要掂量的。

第四,benchmark 是 n=1 的自测。README 那段看起来很硬的「wigolo vs WebSearch vs Tavily vs Exa」对比,原文写得很清楚,「one cold query, run live inside a single Claude Fable 5 session」。一次查询,一个 session,作者自己叫它「one honest query, not a leaderboard」。这是诚实的写法,但作为读者你要明白,这个 benchmark 的统计意义是零,它展示的是「这一天这一次,wigolo 拿到的字段比对手多」,不是「wigolo 整体质量胜过对手」。README 后面 FAQ 里作者也承认「the paid tools still win some deep-extraction edge cases」,这是真话。

这件事为什么值得

讲完所有边界,回到我最想说的那个方法论。

wigolo 给 AI Agent 设计工具输出格式的方式,是可以被命名的,我管它叫「Evidence-First Tool Output」。原则有三条:

  1. 可解释打分(explainable score):每条结果带分项打分(semantic / lexical / engine_consensus),Agent 能自己判断这条该信几分
  2. 可定位溯源(byte-pinned provenance):每段引用带 byte offset,Agent 拿到可以回原文切片比对
  3. 可标失败(labeled degradation):失败、stale cache、blocked_by_challenge 都在 output 里明确标注,不藏成「空但成功」

这套范式不是 wigolo 一家的发明,但它是目前我看到把这三条同时做实的少数项目。你下次给 AI Agent 设计工具的时候,无论是搜索、数据库查询、API 调用,都可以问自己一个问题:我返回的格式,是给 Agent 看的,还是给 Agent 自己审阅的?前者只需要让 Agent 能用,后者需要让 Agent 能不信。

如果你的场景是个人开发、本机用、想摆脱 metered 账单,wigolo 是当前最完整的本地优先 web 层。如果你的场景是公司内部多 Agent 共享、需要暴露到非 loopback,那 SSRF 那个洞你得自己补上再上,或者等作者把 issue #206 合掉。如果你做的是大规模爬取(harvesting),wigolo 不是为你设计的,作者在 FAQ 里明确说了它是「the polite end of the spectrum」。

AGPL-3.0 协议要单独提一句。本地用、公司内部用都没问题,只有你修改了 wigolo 并作为网络服务对外提供时,才需要公开你改的源码。这条 license 是 wigolo 防止被闭源 fork 的法律护城河,也是作者承诺「no paid tier and never will be」的底气来源。

# 想试的话
npx wigolo init --agents=claude-code
npx wigolo doctor

# 不要了干净卸载
npx wigolo config --uninstall --yes

一个 3 个月的项目,1667 颗星,7600 个测试,一个核心开发者。它能活多久要看社区和作者能撑多久,但它给 Agent 工具输出格式立的那根标杆,比这个项目本身的命运更值得记住。

评论互动

© 2026 王若风的技术博客 · Powered by Astro