- 人工智能
- AI Agent
- 交互助手
- 本地部署
- 大模型
- 工具调用
- AI 技能
【免费下载链接】leon
🧠 Leon is your open-source personal assistant.
导读
本文围绕 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 优先队列,关键机制如下:
- 入队与排序:每个待抓取项携带
depth、来源页via、链接文本linkText和linkScore;每次循环前队列按linkScore降序排序,保证最相关的链接优先被抓取。 - 去重与域名配额:
visited集合记录已访问 URL(URL 会去掉 hash 片段后归一化);perDomainCounts按 hostname 计数,超过sameDomainLimit的域名直接跳过,防止单个站点耗尽预算。 - 链接打分
scoreLink(源码 crawl-web.mjs):查询短语先被分词(按非字母数字切分、过滤长度小于 2 的词元),然后:- 链接的
url + text命中任一词元:+4 分; - 链接的上下文文本(
context,即链接前后约 800 字符)命中:+1 分; - 与当前来源页同域名且已有得分:额外 +2 分(鼓励站内权威页优先)。
- 链接的
- 提前停止
shouldStopEarly(源码 crawl-web.mjs):当页面请求成功、存在 snippets、页面score >= 8(常量STRONG_MATCH_SCORE),并且已达到最大深度或当前页没有更相关的链接可跟进时,立即终止爬取——这对应技能文档"找到强匹配就尽早停下,优于广撒网"的设计原则。 - 停止原因:循环结束时
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> | summary | full时结果始终携带完整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 实体解码(
、&、"、'、<、>、十进制/十六进制字符引用)并归一化空白。
内容类型判断:通过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 执行时的完整工作流,整理如下:
- 澄清目标:仅当请求的信息或起始点存在歧义时,才向用户澄清目标;
- 尊重用户起点:用户提供了 URL 时,从该 URL 开始;
- 主流程:使用
scripts/crawl-web.mjs抓取页面、在内容中搜索、跟进相关链接; - 单页深查:对某个有希望的页面,使用
scripts/fetch-page.mjs做一次性或深入检视; - 渐进读取:先做紧凑抓取(预览、片段、链接),仅当片段/标题/链接显示页面可能相关时,再读取完整或后续文本块;
- 页内精确检索:在已抓取内容中搜索确切的名称、短语、日期、数字、标题或邻近同义词;
- 访问去重:记录已访问 URL,不重复访问同一页面;
- 及时收尾:一旦找到带足够上下文的目标信息,立即停止;
- 如实汇报:若到达限制仍未找到,报告已检查的内容与尚未解决的部分。
这条流程与代码实现一一对应:第 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"。
九、输出规范:先答后注
技能文档规定的回答格式是:
- 先直接给出答案(Answer directly first);
- 附上简明来源说明(concise source notes):
- 有用时列出已检查的页面;
- 给出决定性来源 URL(decisive source URL);
- 说明仍存在的不确定性(remaining uncertainty);
- 若目标未找到:明确说明,并总结已检查的最相关位置。
这种"先结论、后证据、留不确定性"的输出结构,既符合 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.
相关推荐
SwiftSoup数据提取实战:从网页中抓取结构化信息的终极指南
SwiftSoup数据提取实战:从网页中抓取结构化信息的终极指南 想要快速从网页中提取结构化数据?SwiftSoup是您的完美解决方案!🎯 这个纯Swift
网页爬虫后端Polar 前端重渲染优化实战:用 Memoized Components 配合 Early Return 消除不必要的昂贵计算
Polar 前端重渲染优化实战:用 Memoized Components 配合 Early Return 消除不必要的昂贵计算 本文基于仓库内置的 Verce
网页爬虫后端AI 应用Automa数据抓取实战:从网页提取信息的完整教程
Automa数据抓取实战:从网页提取信息的完整教程 Automa是一款强大的浏览器自动化工具,专门用于网页数据抓取和工作流程自动化。本文将为您详细介绍如何使用A
RPA工作流自动化浏览器控制网页爬虫
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考