序言
注意:本篇文章经过AI润色
Awesome Tech Share是一个基于 MkDocs 的静态知识库,通过 GitHub Actions 自动化部署,涵盖 AI、CS、开发、学习成长等多个领域,内部积累了相当数量的 Markdown 文件,每篇文件里都手工嵌入了大量外部 URL。
随着文件数量的增长,手动整理的成本开始以指数级上升。于是我试图构建一个"URL 驱动的、可自我生长的知识录入系统":输入一个 URL,程序自动抓取、提取、分类,并将内容以 Markdown 的形式写入知识库,最终通过 MkDocs 重新生成静态网站。
一 项目起盘
项目的初始设计围绕四个模块展开。
解析层(spider.py) 负责接收 URL 并抓取原始网页内容。
语义处理层(brain.py) 接入大语言模型,对抓取内容进行摘要提取,并与现有的知识树进行语义对齐,决定内容应写入哪个具体的 .md 文件。
配置更新层(mkdocs_updater.py) 使用 ruamel.yaml 来安全修改 mkdocs.yml 的 nav 导航结构,确保新生成的内容能够在前端正确挂载。最后,一个
主流程脚本(grow_knowledge.py) 将三者串联,提供 CLI 入口,并支持 --dry-run、--url-file、--rebuild-from-docs 等操作模式。
二 爬虫选型:错误的决策
在整个项目中,爬虫层是第一个暴露出深层问题的地方,也是最容易被轻视的地方。
Python 爬虫工具的演进与现实处境
在 Python 生态中,爬虫工具的演进大致经历了三个阶段,每一代工具都在解决上一代的核心瓶颈,但也带来了新的复杂度。
最早一代是以 requests + BeautifulSoup 为代表的静态解析方案。它的优点是极轻量、可控性强,但面对任何 JavaScript 渲染的页面就完全失效。现代互联网中,大量内容(包括技术博客、论文索引页、GitHub 项目页)都依赖客户端渲染,这使得静态解析在实际场景中的覆盖率非常有限。
第二代是以 Scrapy 为代表的工程化框架。它引入了 Spider、Pipeline、Middleware 的分层概念,适合大规模结构化抓取任务。但 Scrapy 的核心设计是面向"批量同质化数据"的,对于 URL 来源极度杂乱、目标网站千差万别的场景,Scrapy 的配置复杂度远超收益。与此同时,Scrapy 对 JavaScript 渲染的支持依赖第三方中间件(如 scrapy-playwright),集成成本不低。
第三代是以 Playwright、Selenium 以及近年兴起的 Scrapling、DrissionPage 为代表的"浏览器自动化+智能解析"方案。Scrapling 的特点在于它将高性能的 CSS/XPath 解析与对 curl_cffi(一个模拟真实浏览器 TLS 指纹的底层库)的支持结合在一起,同时可以无缝切换到 Playwright 引擎处理动态页面。这使它在应对反爬方面比传统工具更具竞争力。
Scrapling 在这个项目里遭遇了什么
选择 Scrapling 是合理的,但项目在工程落地上犯了几个具体的错误。
第一是动态加载与静态解析的边界没有被预先划定。 我们的 URL 来源覆盖了学术论文页、GitHub 仓库、技术博客、视频平台等多种类型。Scrapling 在处理静态或轻度动态的页面时表现出色,但对于需要完整浏览器环境才能渲染的 SPA 页面,仅靠 curl_cffi 的 TLS 模拟是不够的,必须切换到 Playwright 引擎。项目没有设计这个自动降级的路由逻辑,导致部分 URL 的抓取结果是不完整的空壳。
第二是抓取结果的质量验证层完全缺失。 爬虫抓取回来的是原始 HTML,里面混杂了导航栏、侧边栏、广告、评论区和各种脚本。项目没有设计通用的"正文提取器",而是直接将原始内容送入 LLM,这意味着 AI 每次处理的都是一个充满噪音的"盲盒"。
应该怎么做:构建分层的解析流水线
一个更健壮的爬虫层设计,应该是分层的、可降级的、带质量验证的。
在抓取策略上,应该为不同类型的 URL 设计不同的解析路径。对于结构简单的静态页面(如 arXiv 论文页、GitHub README),直接使用 Scrapling 的 curl_cffi 引擎即可,速度快且成功率高。对于需要 JS 渲染的页面,自动降级到 Playwright 引擎。这个路由判断可以通过一次轻量级的 HEAD 请求来完成——检查响应头中的 content-type 和页面体积,如果页面主体过小(通常意味着内容由 JS 动态注入),则触发 Playwright。
在正文提取上,应该在 Scrapling 之后增加一个专门的"正文提纯层"。trafilatura 是目前在多个基准测试中表现最好的正文提取库,它基于机器学习模型对页面结构进行分析,在准确率上明显优于基于规则的 newspaper3k 和 boilerpy3。经过 trafilatura 处理后,送入 LLM 的内容将是干净的纯文本,而不是充满噪音的原始 HTML,这对后续摘要和分类的质量有决定性影响。
此外,还应该引入一个抓取结果的质量评分机制。对于提取到的正文,检查其字符数是否超过阈值(例如 200 字),是否包含有意义的句子结构。如果质量不达标,将该 URL 标记为"需要人工处理",而不是将低质量内容直接送入后续流程。
三 LLM 接入
解析层的数据质量问题直接影响了 LLM 层的表现,但 LLM 层自身也存在独立的工程问题。
API 接入的混乱
在实际调试中,出现了 401 Unauthorized、HTTP 403 error code 1010、.env 文件格式解析失败等一系列连锁错误。缺乏环境配置的校验层原因是系统在启动时没有对 .env 进行格式验证,而是直接传入运行时,导致错误在最深处才被发现。
同时,brain.py 里维护了 OpenAI 和 Anthropic 两套 HTTP 请求逻辑,试图"兼容多种接口"。这种双轨并行的设计在看起来灵活的同时,实际上是在为后续的调试和维护埋下隐患——两套逻辑的行为差异、错误处理方式和超时策略各不相同,一旦出现问题,定位成本会成倍增加。
上下文治理的根本性缺失
比 API 接入更深层的问题,是系统对"上下文"的管理几乎是空白的。
在 --rebuild-from-docs 模式下,系统需要扫描 35 个现有的 Markdown 文件,理解它们的内容,并基于此重新规划整个知识树。从预演结果来看,AI 的重组决策出现了明显的语义漂移:开发/项目.md 被重新归类为 AI/AI开源项目与前沿实践索引.md,原有目录结构所承载的人工语义被 AI 的"过度创意"所覆盖。
更严重的问题是系统没有任何"共享记忆"机制。在处理第 20 个 URL 时,AI 并不知道前 19 个 URL 是如何被分类的,导致同一个知识领域可能在不同的运行批次中被创建为两个语义相近但路径不同的目录节点,而系统没有任何机制去检测和消除这种冗余。
应该怎么做:建立可治理的 LLM 调用层
一个成熟的 LLM 接入设计,应该在"灵活性"和"可控性"之间找到明确的平衡点。
在接口设计上,应该统一为单一的 OpenAI 兼容接口规范。 目前主流的大模型服务(包括各类中转 API)都支持 OpenAI 的 /chat/completions 接口格式。与其维护多套适配逻辑,不如将接口层抽象为一个标准的 LLMClient 类,通过环境变量切换 base_url 和 model,内部统一使用 OpenAI SDK。这样既保留了切换模型的灵活性,又避免了多套逻辑并存的维护噩梦。
在上下文治理上,应该引入持久化的知识树状态。 每次分类决策完成后,将"URL → 分类路径"的映射写入一个本地的 knowledge_index.json。在下一次运行时,系统先读取这个索引,将其作为 AI 决策的上下文输入,明确告知 AI"这些路径已经存在,请优先复用"。这是一种轻量级的 Shared Memory 实现,不需要引入向量数据库,却能有效解决分类冗余的问题。
在提示词设计上,应该从"自由发挥"转向"规范约束"。 给 AI 的分类指令不应只是"请将以下内容分类到合适的目录",而应包含明确的输出 Schema(要求返回 JSON 格式的 {path, confidence, reason})、现有目录树的完整列表、以及一套"当置信度低于阈值时归入待审核队列"的兜底规则。这样的设计让 AI 的决策变得可审计、可回溯,而不是每次都是一个黑盒输出。
在任务分层上,应该将摘要和分类解耦为两次独立的 LLM 调用。 第一次调用只做内容摘要,要求 AI 从正文中提取标题、核心概念和关键词;第二次调用只做分类决策,输入是第一次的结构化摘要(而不是原始正文),输出是目标路径。这种分层设计不仅降低了单次调用的上下文长度,还使得每一步的输入输出都可以独立验证。
四 静态框架与自动化矛盾
这个项目还暴露了一个在设计阶段容易被忽视的结构性矛盾:MkDocs 是为"人工维护"设计的静态框架,而我们试图用自动化程序去频繁修改它的核心配置。
mkdocs.yml 的 nav 字段是整个站点的路由核心,它对 YAML 格式的要求极为严格。ruamel.yaml 的引入是一个正确的工程决策,它能够在修改 YAML 的同时保留原有的注释和缩进风格,这比直接使用 PyYAML 要安全得多。但即便如此,自动化程序在处理复杂的嵌套 nav 结构时,仍然面临路径冲突和节点重复的风险。一旦 mkdocs.yml 被写入了错误的格式,GitHub Actions 的构建流水线就会直接失败。
应该怎么做:引入配置文件的防御性写入机制
解决这个矛盾的核心思路,是在"自动化写入"和"框架消费"之间增加一个缓冲层。
第一步,将 nav 的生成与 mkdocs.yml 的修改解耦。 不要直接修改 mkdocs.yml,而是维护一个独立的 nav_index.yml 文件,专门记录自动化系统管理的导航节点。在 mkdocs.yml 中通过 !include 指令(配合 mkdocs-monorepo-plugin 或 mkdocs-awesome-pages-plugin)引入这个文件。这样,自动化系统只需要维护自己管辖的那一部分导航,不会触碰人工维护的核心配置。
第二步,在写入前增加 Schema 校验步骤。 每次自动化系统生成新的导航节点后,先用 yamllint 或 pykwalify 对生成的 YAML 片段进行格式验证,只有通过验证才允许写入文件。这一步的成本极低,但能拦截绝大多数因格式错误导致的构建失败。
第三步,在 GitHub Actions 中增加自动回滚机制。 在部署工作流中增加一个 pre-deploy 步骤,将当前的 mkdocs.yml 和 docs/ 目录备份到一个临时分支。如果 mkdocs build 失败,自动触发回滚,将备份内容恢复到主分支。这确保了即使自动化系统产生了错误的输出,线上的知识库也不会受到影响。
五 脚本到真正的Agent的缺失
控制面资产的缺失与建立。 一个成熟的 Agent 系统需要一套明确的"控制面资产":分类决策的 SOP 文件、知识树演进的约束规则、以及对 AI 输出格式的严格 Schema 定义。在这个项目中,brain.py 里的提示词是临时拼接的,没有版本管理,也没有被提炼为可复用的规则集。正确的做法是将这些规则沉淀到一个 KNOWLEDGE_RULES.md 文件中,明确定义"什么类型的内容应该进入哪个目录"、“当内容跨越多个领域时如何处理”、“当置信度不足时如何降级"等决策准则,让 AI 每次调用时都以此为约束。
反脆弱机制的缺失与引入。 系统在遇到失败时,没有结构化的失败归因和状态恢复流程。更合理的设计是为每一个处理中的 URL 维护一个状态机:pending → crawling → extracting → classifying → writing → done,以及对应的失败状态 failed_crawl、failed_extract、failed_classify。每次运行时,系统只处理处于 pending 或失败状态的 URL,成功完成的 URL 不会被重复处理。这种幂等性设计是任何可靠的自动化系统的基础。
任务分层与异步化的缺失与重构。 将"抓取”、“清洗”、“摘要”、“分类”、“写入"五个性质完全不同的任务全部串联在一个同步流程里,使得任何一个环节的失败都会导致整条流水线中断。更合理的设计是将其拆解为独立的、可并行的任务队列。对于个人知识库这种规模的项目,不需要引入 Celery 这样的重型任务队列,使用 Python 的 asyncio 配合简单的 SQLite 状态表就足以实现基本的任务调度和断点续传。
六 结语
- 碎碎念(我又又又回来了)。
- 从项目的结局上来看,相当失败。最初我天真的认为通过几个Python 程序或者调用几个库,然后加上 LLM 就行了。但最后吗。(毕竟它看来似乎真的很简单,解析url,抓取内容,llm总结,然后分类)
- 附加的想法是 QQ 群中同时再次接入一个机器人,每次发送这个与URL相关的东西,就自动检测解析,导入到这个知识库。甚至我还想过ob结合到一起,我就输一个url,直接知识库,笔记,总结一条龙。(妈妈再也不用担心收藏不看了,不过这个想法我倒是有自知之明)。
- 我的问题。不过失败是也是必然的。我在试图用"脚本自动化"的思维去构建一个"智能体系统”,而这两者对工程严谨性的要求是完全不同的量级。
- 还是对Agent开发学少了。。。。。。。