展示如何用 Claude Code 跨越多本书籍进行知识提取和连接,显著提升学习和代码理解效率。
LLM 常被过度用于总结,却很少被用来帮助我们深入阅读。
为了探索 LLM 如何丰富阅读,而不是简化阅读,我为 Claude Code 配备了一套工具,让它挖掘一个包含 100 本非虚构类书籍的资料库。它找出了一系列由有趣观点串联起来的摘录,我称之为「阅读路径」(trail)。
下面是其中一条路径的片段,它把创业世界中的欺骗行为,与群众运动的社会心理学联系了起来(我尤其喜欢从 Jobs 跳转到 Theranos 的那一段):
这些书选自 Hacker News 用户最喜爱的书籍。我此前抓取过这份书单,并将其可视化。
Claude 每次浏览书中的一个文本块(chunk)。每个文本块约有 500 个英文单词,并尽可能与自然段落的边界对齐。这个长度在节省 token 和保留足够语境、让观点充分展开之间取得了很好的平衡。
每个文本块都会按主题建立索引,而主题本身也会被索引,以供搜索。这样一来,我们就能轻松找出整个语料库中所有与某个主题相关的段落,比如「欺骗」。
当你知道自己要找什么时,这种方式很有效。但仅靠搜索,无法告诉你语料库里究竟有哪些主题。系统一共提取出了超过 10 万个主题,数量太多,不可能直接浏览。为了支持探索,这些主题被组织成了层次化的树状结构。
最终得到了大约 1,000 个顶层主题。它们由较低层级的主题合并而来,但并非每一个都同样有用:
让 Ev Williams 感到沮丧的事件
以「Da」开头的名字
1971 至 1974 年间发生的事件
不过,这套颇具博尔赫斯风格的分类体系已经足够好,Claude 可以借此拼凑出这些书大致都在讲什么。
Claude 通过几个 CLI 工具使用主题树和搜索功能。这些工具让它能够:
找出与某个查询相似的主题所关联的全部文本块。
找出在某个给定主题前后一定文本块窗口内出现的主题。
找出在多本书中共同出现的主题。
浏览主题树中互为同级节点的主题和文本块。
为了生成这些阅读路径,Agent 会分阶段工作。
首先,它扫描资料库和已有的阅读路径,提出新颖的路径构想。这个阶段主要通过浏览主题树来发现尚未探索的领域,很少深入阅读全文块。
接着,它选取一个具体构想,并把它转化为一条阅读路径。它会接收上一阶段提供的种子主题,然后浏览大量文本块。它从中提取摘录——也就是具体的连续句子——并判断怎样排列它们,才能最好地支撑某个洞见。
最后,它会为摘录添加重点标记,并在前后相邻的摘录之间建立连接。
尽管我已经用 Claude Code 做了几个月的开发,但面对这个项目时,我的第一反应仍然是把它设计成一条传统 pipeline,由多个彼此独立的阶段组成。我最初尝试构建的系统包含多个 LLM 模块,每个模块的上下文都由我仔细手工组装。
一时兴起,我让 Claude 使用此前用于调试的那些工具,只给了它一个极简的 prompt:「找点有意思的东西。」它立刻就比我那个正在手工调优的 pipeline 做得更好:它知道自己需要什么,并能主动把相关信息找出来,同时需要的编排工作少得多。显然,尽可能把工作推入 Agent 自身的循环中,效果更好。
最后,我把 Claude 当成了这个项目的主要交互界面。最初这样做,是因为它能比我回忆起调用顺序更快地推断出我想执行哪些 CLI 命令。后来,我开始用它自动化那些不够固定、难以通过传统脚本实现的任务。
后者带来了许多我以前不会考虑的可能性。例如,我改变了对摘录长度的想法,希望摘录能更短一些。我把新的偏好告诉 Claude,它随后检查了所有已有的阅读路径,并在必要时进行编辑,同时权衡修改会如何影响整条路径的总体含义。过去,我很可能会直接把之前生成的所有路径视为过时内容,然后重新生成,因为所需的编辑实在太微妙,很难用明确的规则描述。
总体来说,Agent 扩展了我的野心。它替我处理了样板式工作,因此我不再回避那些烦琐的部分。修改的成本变得很低,所以我不必因为撤销某个选择代价太高,就硬着头皮沿着次优方案继续推进。反过来,这也让我能够保持势头,把注意力集中在工作中令人愉悦、富有创造力的部分。
我的关注点从优化 prompt,转向为 Claude 实现更好用的工具,相当于在抽象阶梯上又向上迈了一级。
我对 AI 组件的心智模型也发生了变化:它不再是一个把输入映射到输出的函数,而是一个由我提供协助的同事。我开始思考什么样的可供性(affordance)可以让工作流变得更好,就像是在为自己设计工具一样。至于这些工具最终是由 Agent 使用,不过是一个无关紧要的细节。
之所以能这么做,是因为如今的 Agent 已经足够聪明,它使用这些工具的方式与我自己的心智模型存在很大重叠。通常来说,我们很容易站在它的角度思考,也很容易预测它接下来会做什么。
最初,我会密切查看 Claude 的日志,试图猜测它缺少哪种能力。后来我意识到,可以直接要求它在工作结束时提供反馈,并列出它希望拥有的功能。Claude 非常擅长提出新的命令和能力,让工作变得更高效。
Claude 提出改进建议,再由 Claude 实现这些改进,让 Claude 能把工作做得更好。至少现在还需要我来支付 token 费用——暂时如此。
「有趣程度」很难被量化成一个可供优化的目标。《Why Greatness Cannot Be Planned》提出,追求新颖性往往是一条更有成效的路径。尽管书中的结论存在争议,但我发现这个观点很适合本项目。
颇具时代特色的是,这套新颖性搜索通过两种方式实现:
让搜索算法偏向探索不足的主题和书籍。
好声好气地拜托 Claude。
一个主题的新颖性分数,通过计算其 embedding 与 k 个最近邻之间距离的平均值得出。一本书的新颖性分数,则是它所包含的独有主题的新颖性平均值。这个数值会用于搜索结果排序,让那些既相关又新颖的结果更有可能被看到。
在 prompt 层面,Claude 会先查看所有已有的阅读路径,再开始构思阶段;它还会被要求避免任何概念上的重叠。这个方法相当有效,不过它经常被任何与秘密、系统论或隐性知识有关的主题吸引走注意力。
仿佛只要开始在语料库中寻找联系,就会召唤出 Umberto Eco 的灵魂,把阴谋论式的思维调到最大功率。
EPUB 使用 selectolax 解析。相比 BeautifulSoup,我选择它是因为速度更快,API 也更简单。
从纯文本到主题树,所有内容都存储在 SQLite 中。Embedding 则使用 sqlite-vec 存储。
文本使用 wtpsplit(sat-6l-sm model)切分成句子。这些句子随后被组合成文本块,在不拆开段落的前提下,尽可能接近 500 个英文单词。
我使用 DSPy 调用 LLM。它很适合提取结构化数据,而且可以很方便地更换不同的模型进行实验。在彻底转向 Agent 模式之前,我还尝试过它的 prompt optimizer,结果非常令人期待。
最终,我选择 Gemini 2.5 Flash Lite 来提取主题。模型会收到一个文本块,并被要求返回 3~5 个主题。它还需要判断这个文本块是否有用,以便过滤索引条目、致谢、孤立标题等内容。提取结果的稳定性令我惊讶:相似的文本块经常会共享一些完全相同的主题标签。处理 100 本书共使用了约 6,000 万个输入 token,总成本约为 10 英镑。
在索引了几本书后,我把处理结果连同原始 prompt 一起交给 Claude Opus,请它改进 prompt。这只是一次并不完善的单轮尝试,类似于 DSPy 实现的那种 prompt 优化,但效果相当不错。
距离低于某个阈值的主题对会被合并。这样就能处理「Startup founder」「Startup founders」和「Founder of startups」之类近乎重复的主题。
CLI 输出使用一种半 XML 格式。为了鼓励导航,大多数输出都会与相关内容嵌套在一起。例如,搜索某个主题时,系统会显示相关的文本块,以及这些文本块包含的其他主题。这样,我们既能大致了解文本块的内容,也能发现还有哪些主题可能值得探索。或许存在 token 利用率更高的格式,但我从未触及 context window 的上限。
<topics query="deception" count="1">
<topic id="47193" books="7" score="0.0173" label="Deception">
<chunk id="186" book="1">
<topic id="47192" label="Business deal"/>
<topic id="47108" label="Internal conflict"/>
<topic id="46623" label="Startup founders"/>
</chunk>
<chunk id="1484" book="4">
<topic id="51835" label="Gawker Media"/>
<topic id="53006" label="Legal Action"/>
<topic id="52934" label="Maskirovka"/>
<topic id="52181" label="Strategy"/>
</chunk>
<chunk id="2913" book="9">
<topic id="59348" label="Blood testing system"/>
<topic id="59329" label="Elizabeth Holmes"/>
<topic id="59352" label="Investor demo"/>
<topic id="59349" label="Theranos"/>
</chunk>
</topic>
</topics>
主题使用 google/embeddinggemma-300m 生成 embedding,并使用 BAAI/bge-reranker-v2-m3 重新排序。
主题使用 google/embeddinggemma-300m 生成 embedding,并使用 BAAI/bge-reranker-v2-m3 重新排序。
许多 CLI 工具都需要加载 embedding model 和其他开销较大的状态。第一次调用时,系统会透明地启动一个独立的 server process,一次性加载所有这些资源,并将它们保留一段时间。后续的 CLI 调用会通过 Python 的 multiprocessing.connection 使用这个 server。
许多 CLI 工具都需要加载 embedding model 和其他开销较大的状态。第一次调用时,系统会透明地启动一个独立的 server process,一次性加载所有这些资源,并将它们保留一段时间。后续的 CLI 调用会通过 Python 的 multiprocessing.connection 使用这个 server。
系统会根据主题 embedding 的相似度,以及主题共同出现时的逐点互信息(point-wise mutual information)添加边,从而把主题集合转化成一个图,并由 igraph 提供底层支持。
系统会根据主题 embedding 的相似度,以及主题共同出现时的逐点互信息(point-wise mutual information)添加边,从而把主题集合转化成一个图,并由 igraph 提供底层支持。
接着,系统递归应用 Leiden partitioning,直到达到最小规模,将图转化成树。我尝试了 Surprise quality function,因为它没有需要调整的参数,实际效果也足够好。Gemini 会根据每个分组包含的全部主题,为其生成标签。
接着,系统递归应用 Leiden partitioning,直到达到最小规模,将图转化成树。我尝试了 Surprise quality function,因为它没有需要调整的参数,实际效果也足够好。Gemini 会根据每个分组包含的全部主题,为其生成标签。
摘录会交由 Gemini 清理,移除 EPUB 残留内容、解析错误、页眉、脚注等。只处理那些实际会被展示的摘录,而不是在预处理阶段清理全部内容,节省了大量 token。
摘录会交由 Gemini 清理,移除 EPUB 残留内容、解析错误、页眉、脚注等。只处理那些实际会被展示的摘录,而不是在预处理阶段清理全部内容,节省了大量 token。