☰
Leon 开源个人助理中的 Tiny Web Crawler 技能:有界网页抓取与证据驱动的信息检索实战指南
2026/10/1 16:57:41 网站建设 项目流程
  • 人工智能
  • AI Agent
  • 交互助手
  • 本地部署
  • 大模型
  • 工具调用
  • AI 技能

【免费下载链接】leon

🧠 Leon is your open-source personal assistant.

项目地址:https://gitcode.com/gh_mirrors/le/leon
点击查看免费下载

导读

本文围绕 Leon 开源个人助理(GitHub 加速计划镜像gh_mirrors/le/leon)内置的tiny-web-crawlerAgent 技能展开,系统讲解它如何从一个或多个起始 URL 出发抓取网页、抽取可读文本、在页面内搜索关键词并沿相关链接有界爬取,直到命中目标信息或到达资源上限。读完本文,你将掌握crawl-web.mjs与fetch-page.mjs两个脚本的全部命令行参数与默认限制、渐进式抓取的内部实现、链接打分与提前停止机制,以及如何遵循证据规则和输出规范让 Leon 在真实问答中给出可追溯、可验证的答案。

一、技能定位:Agent Skill 在 Leon 中的角色

Leon 将技能分为两类:原生技能(Native Skill,位于skills/native/<skill>/,通过skill.json+locales/+ action 入口实现具体动作)和 Agent 技能(Agent Skill,位于skills/agent/<skill>/SKILL.md)。根据 AGENTS.md 的定义,Agent 技能由三部分组成:

  • 发现前置元数据(discovery frontmatter):供服务器发现与加载;
  • 简洁的工作流指令:指导 Leon 已有的 Agent 主循环如何调用工具;
  • 可选的支撑脚本:放在scripts/目录,承担实际的执行逻辑。

tiny-web-crawler正是这份约定中明确点名的参照范例("Followtiny-web-crawler"),它的定位是引导 Leon 的现有 Agent 循环和工具调用,而不是重新实现一套独立的 Agent 循环。因此它的目录结构非常紧凑:

skills/agent/tiny-web-crawler/ ├── SKILL.md # 技能指令与工作流 └── scripts/ ├── fetch-page.mjs # 单页抓取 └── crawl-web.mjs # 有界多页爬取

SKILL.md的 frontmatter 声明了技能的元数据:name: tiny-web-crawler,description一句话概括其能力边界——"从一个或多个起始页面开始爬取,获取可读内容,在页面内搜索,跟进相关链接,在找到所需信息或达到有界上限时停止",版本1.0.0,作者为 Leon 的创建者 Louis Grenard。这段description会被服务器端读取,用于技能发现与意图匹配。

前端元数据如何被服务器解析

从源码结构看,服务器通过 skill-domain-helper.ts 中定义的AGENT_SKILL_FILENAME = 'SKILL.md'常量来定位 Agent 技能文档,并解析其 frontmatter(parseAgentSkillFrontmatter,见 skill-domain-helper.ts):它会校验首行必须是---边界符,提取name与description字段,并对 name 应用模式与长度校验、对 description 应用非空与最大长度校验。这意味着技能文档的 frontmatter 质量直接决定技能能否被 Leon 正确识别和按语义触发。

二、两个脚本的分工与运行方式

技能文档明确要求使用捆绑脚本完成实际抓取,二者分工互补:

脚本用途适用场景
scripts/fetch-page.mjs抓取单个页面,抽取紧凑可读文本、链接与查询片段单页检视;对某个有希望的页面做深入检查
scripts/crawl-web.mjs从一个或多个起始 URL 出发,跟进相关链接,在限制内或命中强匹配时停止需要跨多页寻找目标信息的有界爬取

使用 node 运行与运行时 shim

两个脚本均以#!/usr/bin/env node开头,通过node执行。技能文档特别说明:Leon 的 shell 工具会注入运行时 shim,因此这里的node在可用时会解析到 Leon 托管的 Node.js 二进制,只有当托管二进制缺失时才回退到PATH中的系统 node。文档同时提醒:不要手工拼接$LEON_HOME/bin/node/...或/bin/node/...路径——路径解析由 Leon 运行时统一处理,手工拼接会破坏版本隔离。

基本调用示例(直接照搬技能文档并可用):

node scripts/crawl-web.mjs --url "https://example.com" --query "target phrase" --max-pages 8 --max-depth 2

脚本输出均为 JSON,直接打印到 stdout,方便 Agent 或 LLM 解析后续处理。

三、crawl-web.mjs:有界多页爬取的完整实现

crawl-web.mjs是整个技能的执行引擎。它内置两个硬性前置校验(源码 crawl-web.mjs):--url/--urls与--query必须同时提供,缺一即报错——错误信息甚至明确提示"先用网页搜索,再把有希望的 URL 传进来",说明该技能设计为与 Leon 的搜索工具链配合使用,而非自建搜索入口。

命令行参数总览

参数默认值说明
--url <url>/--start-url <url>无(可重复传入)单个起始 URL,可多次出现
--urls <u1,u2,...>无逗号分隔的多个起始 URL,自动 trim 并过滤空值
--query <phrase>无(必填)目标查询短语,用于页面打分、链接筛选与片段提取
--max-pages <n>8最多抓取页数
--max-depth <n>2从起始页算起允许跟进的最大链接深度
--same-domain-limit <n>5同一域名最多抓取页数
--max-search-queries <n>3最大搜索查询数(保留在输出 limits 中)

参数解析逻辑见 crawl-web.mjs,所有数值型参数通过Number()转换,非法值不会导致崩溃。

内部抓取流程:优先级队列 + 链接打分

核心循环(源码 crawl-web.mjs)是一个按链接相关度排序的 BFS 优先队列,关键机制如下:

  1. 入队与排序:每个待抓取项携带depth、来源页via、链接文本linkText和linkScore;每次循环前队列按linkScore降序排序,保证最相关的链接优先被抓取。
  2. 去重与域名配额:visited集合记录已访问 URL(URL 会去掉 hash 片段后归一化);perDomainCounts按 hostname 计数,超过sameDomainLimit的域名直接跳过,防止单个站点耗尽预算。
  3. 链接打分scoreLink(源码 crawl-web.mjs):查询短语先被分词(按非字母数字切分、过滤长度小于 2 的词元),然后:
    • 链接的url + text命中任一词元:+4 分;
    • 链接的上下文文本(context,即链接前后约 800 字符)命中:+1 分;
    • 与当前来源页同域名且已有得分:额外 +2 分(鼓励站内权威页优先)。
  4. 提前停止shouldStopEarly(源码 crawl-web.mjs):当页面请求成功、存在 snippets、页面score >= 8(常量STRONG_MATCH_SCORE),并且已达到最大深度或当前页没有更相关的链接可跟进时,立即终止爬取——这对应技能文档"找到强匹配就尽早停下,优于广撒网"的设计原则。
  5. 停止原因:循环结束时stopReason取三值之一——limit_reached(达到页数上限)、strong_match_found(命中强匹配提前停止)、queue_exhausted(队列自然耗尽)。

输出结构

crawl-web.mjs的输出 JSON 包含:

{ "query": "target phrase", "limits": { "maxPages": 8, "maxDepth": 2, "sameDomainLimit": 5, "maxSearchQueries": 3 }, "stopReason": "strong_match_found", "pagesChecked": 4, "pages": [ { "url": "...", "status": 200, "ok": true, "title": "...", "depth": 0, "via": null, "linkText": "", "score": 9, "textLength": 12345, "chunk": { "offset": 0, "chars": 6000, "hasMore": true, "nextOffset": 6000 }, "rawTruncated": false, "snippets": ["..."], "error": null } ], "matches": [ /* 仅包含有 snippets 命中的页面摘要 */ ] }

pages数组记录每个访问页的摘要(标题、状态、深度、来源、得分、文本长度、片段、是否截断),matches只收录有查询片段命中的页面,方便 Agent 直接聚焦候选答案页。每次抓取实际是通过spawnSync以 summary 模式调用fetch-page.mjs(默认--max-text-chars 6000、--max-links 150,见 crawl-web.mjs),因此二者输出口径一致。

四、fetch-page.mjs:单页抓取与渐进式读取

fetch-page.mjs负责单页的抓取、清洗与结构化输出,是crawl-web.mjs的底层单元,也可独立用于单页检视。

命令行参数总览

参数默认值说明
--url <url>无(必填)目标页面 URL,缺失时直接抛错Missing --url
--query <phrase>空用于片段提取与打分;不传则snippets为空数组、score为 0
--mode <summary\|full>summaryfull时结果始终携带完整text字段
--include-text <bool>false为true时返回完整文本text,否则只返回textPreview
--offset <n>0从文本第 n 个字符处开始切片,用于分段读取长文
--timeout-ms <n>15000请求超时毫秒数
--max-raw-chars <n>500000原始 HTML 最大读取字符数(流式读取,超出即截断并置rawTruncated)
--max-text-chars <n>8000可读文本单次最大返回字符数
--max-links <n>150最多抽取链接数

参数解析与默认值定义见 fetch-page.mjs 与 fetch-page.mjs。所有数值参数经getPositiveInteger校验,非法值回退到默认值。

页面抓取与内容清洗管线

请求阶段(源码 fetch-page.mjs):

  • 使用AbortController实现超时中断,fetch时redirect: 'follow'跟随重定向,最终 URL 以响应后的实际地址为准;
  • 携带固定USER_AGENT(现代 Chrome 桌面 UA,见 fetch-page.mjs)与accept头,提升页面兼容性;
  • 原始响应体通过流式读取,达到maxRawChars即截断并await reader.cancel(),避免超大页面耗尽内存。

清洗阶段(源码 fetch-page.mjs):

  • 先剥离噪声块标签:script、style、noscript、svg、canvas、iframe、form、select、button、nav、footer、aside(REMOVED_BLOCK_TAGS)——导航、页脚、表单等非正文内容被整体移除;
  • 再移除 HTML 注释,将p/div/section/article/header/footer/main/li/tr/h1-h6等块级结束标签替换为换行(保留段落结构),随后剥掉剩余标签;
  • 最后做 HTML 实体解码(&nbsp;、&amp;、&quot;、&#39;、&lt;、&gt;、十进制/十六进制字符引用)并归一化空白。

内容类型判断:通过content-type头或正文中是否出现<html、<body、<a特征来判定 HTML;非 HTML(如纯文本、JSON)则按原文本直接归一化处理。

渐进式抓取机制(Progressive Fetching)

这是技能文档强调的核心特性——默认只返回紧凑输出,按需再取全文:

  • textPreview:可读文本前 1500 字符(DEFAULT_TEXT_PREVIEW_CHARS)的短预览;
  • snippets:查询词元在文本中的命中片段,每个词元取命中位置前后各 180 字符,去重后最多 8 条(源码 fetch-page.mjs);
  • links:归一化后的链接列表(去掉 hash、只保留 http/https、按 URL 去重合并文本与上下文),每条包含url、text(≤180 字符)、context(≤600 字符,链接前后各约 800 字符的周边文本);
  • chunk.hasMore/chunk.nextOffset:指示当前文本切片是否已读完、下一段从哪个字符偏移继续。

对应技能文档中的深查用法:

node scripts/fetch-page.mjs --url "https://example.com/docs" --query "key phrase" \ --include-text --offset 8000 --max-text-chars 8000

当预览、标题或链接表明某页高度相关时,才用--include-text --offset <n> --max-text-chars <n>逐段读取全文,避免一次性消耗过多 token。

输出 JSON 结构

{ "ok": true, "status": 200, "url": "https://example.com/", "contentType": "text/html", "title": "Example Domain", "mode": "summary", "rawTruncated": false, "textLength": 1200, "chunk": { "offset": 0, "chars": 1200, "hasMore": false, "nextOffset": null }, "score": 2, "snippets": ["..."], "links": [ { "url": "...", "text": "...", "context": "..." } ], "textPreview": "..." }

抓取失败时输出{ "ok": false, "error": "<错误信息>" }并设置退出码 1(源码 fetch-page.mjs)。

五、默认限制:资源有界原则

技能文档明确给出默认限制,除非用户另行指定:

限制项默认值对应参数
最大抓取页数8--max-pages
从起始页起最大链接深度2--max-depth
同一域名最多页数5--same-domain-limit

技能文档的指导原则是:倾向提前停止,而非广泛爬取("Prefer stopping early over crawling broadly")。这套有界设计在源码中体现为三重保险:页数上限(pages.length < maxPages)、域名配额(perDomainCounts)和强匹配提前退出(shouldStopEarly)。当达到上限仍未找到答案时,Agent 应如实汇报已检查的内容与未解决的疑点,而不是无限制扩大爬取范围。

六、技能工作流:九步标准化流程

SKILL.md给出了 Agent 执行时的完整工作流,整理如下:

  1. 澄清目标:仅当请求的信息或起始点存在歧义时,才向用户澄清目标;
  2. 尊重用户起点:用户提供了 URL 时,从该 URL 开始;
  3. 主流程:使用scripts/crawl-web.mjs抓取页面、在内容中搜索、跟进相关链接;
  4. 单页深查:对某个有希望的页面,使用scripts/fetch-page.mjs做一次性或深入检视;
  5. 渐进读取:先做紧凑抓取(预览、片段、链接),仅当片段/标题/链接显示页面可能相关时,再读取完整或后续文本块;
  6. 页内精确检索:在已抓取内容中搜索确切的名称、短语、日期、数字、标题或邻近同义词;
  7. 访问去重:记录已访问 URL,不重复访问同一页面;
  8. 及时收尾:一旦找到带足够上下文的目标信息,立即停止;
  9. 如实汇报:若到达限制仍未找到,报告已检查的内容与尚未解决的部分。

这条流程与代码实现一一对应:第 5 步对应fetch-page.mjs的textPreview/snippets/chunk.nextOffset渐进机制;第 7 步对应crawl-web.mjs的visited集合;第 8 步对应shouldStopEarly强匹配提前停止。

七、链接选择策略:什么值得跟

技能文档给出了清晰的链接优先级指引,Agent 应优先跟进满足以下条件的链接:

  • 文本、URL、标题、周边文本或页面结构中提到目标实体(实体、话题、产品、人物、组织、日期或标识符);
  • 与目标相关时,命中以下语义词:docs、documentation、reference、API、pricing、changelog、release、support、help、FAQ、blog、news、about、contact、terms、policy、source、repository、issue、discussion;
  • 站内权威页优先于第三方综述(canonical 内部页优先)。

同时应避开:明显无关的链接、重复链接、导航噪音、广告、跟踪链接、仅登录页、宽泛的分类页——除非它们是当前唯一可行的路径。

源码层的印证:scoreLink中"url + text命中 +4 分、上下文命中 +1 分、同域名 +2 分"的打分体系,正是把"链接文本/URL/周边上下文提及目标"与"站内权威优先"这两条策略机械化为可排序的数值,供优先队列自动择优。

八、证据规则:回答的可追溯性底线

SKILL.md对信息的使用方式提出了严格要求,防止 Agent 在缺乏证据时给出误导性回答:

  • 引用来源:回答时必须引用实际使用过的页面;
  • 一手优先:优先引用一手来源而非二手综述;
  • 区分事实与推断:明确区分"直接找到的事实"与"基于邻近证据的推断";
  • 不越界声明:若只找到邻近或部分证据,不得声称已找到该信息;
  • 冲突处理:当来源相互冲突时,如实说明,并在有发布时间/更新日期时对比日期;
  • 引述克制:引述原文要短,大部分内容用转述(paraphrase)。

这些规则与crawl-web.mjs的输出设计是配套的:matches数组只收录有片段命中的页面,pages保留每个页面的完整状态与得分,正是为了让 Agent 能逐页回溯、精确指明"决定性来源 URL"。

九、输出规范:先答后注

技能文档规定的回答格式是:

  1. 先直接给出答案(Answer directly first);
  2. 附上简明来源说明(concise source notes):
    • 有用时列出已检查的页面;
    • 给出决定性来源 URL(decisive source URL);
    • 说明仍存在的不确定性(remaining uncertainty);
  3. 若目标未找到:明确说明,并总结已检查的最相关位置。

这种"先结论、后证据、留不确定性"的输出结构,既符合 LLM 直接作答的体验,又保留了可核验的追溯路径。

十、技能边界与实战建议

综合文档与源码,使用该技能时有几点值得注意:

  • 它是编排层而非执行层:按照 AGENTS.md 的约定,Agent 技能只负责"指导 Leon 的 Agent 循环和工具调用",不要在此基础上再搭建第二个 Agent 循环或原生动作清单;
  • 与搜索工具配合:crawl-web.mjs要求先有候选 URL(其报错信息明确建议先用 web search 再传 URL),因此典型链路是"搜索定位 → crawl-web 有界爬取 → fetch-page 深查命中页";
  • 预算意识:默认 8 页 / 深度 2 / 同域 5 页是为了控制成本与延迟,实战中建议先小预算试探,命中迹象明显后再放宽--max-pages等参数;
  • 长文分段读取:利用chunk.nextOffset循环推进,配合--offset与--max-text-chars可无损读取超过单次上限的长文档。

结语

tiny-web-crawler是 Leon Agent 技能体系的一个小而完整的范本:SKILL.md用九步工作流、默认限制、渐进抓取、链接选择、证据规则与输出规范定义了"怎么爬、爬多少、何时停、怎么答",crawl-web.mjs与fetch-page.mjs则以优先队列打分、域名配额、强匹配提前停止和流式截断等机制把策略落成可运行的代码。理解这个技能,既能直接上手有界网页检索任务,也能作为在 Leon 中编写其他 Agent 技能(带 frontmatter 的SKILL.md+scripts/支撑脚本)的参照模板——正如 AGENTS.md 所言,它就是 Leon 官方指定的 Agent 技能范例。

  • 人工智能
  • AI Agent
  • 交互助手
  • 本地部署
  • 大模型
  • 工具调用
  • AI 技能

【免费下载链接】leon

🧠 Leon is your open-source personal assistant.

项目地址:https://gitcode.com/gh_mirrors/le/leon
点击查看免费下载

相关推荐

上一篇:自建 CouchDB 服务器完整指南:为 Obsidian Self-hosted LiveSync 搭建私有同步后端
下一篇:RIOT 内核同步原语深度解析:Mutex 数据结构、加解锁流程与优先级继承

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询