面向哲思的编程与架构
笔记哲思阅读动态搜索RSS 订阅
切换到深色模式
搜索
RSS 订阅
切换到深色模式
© 2026 Vic Chen. All rights reserved.CC BY-NC-ND 4.0
← 笔记
从阅读到知识(二):脱离原书之后

从阅读到知识(二):脱离原书之后

2026年6月19日2,8599分钟

读完一本书三个月后,翻开划线列表,很多句子已经不知道当时为什么觉得重要了。问题不是记忆力,而是划线这种形式本身依赖上下文——而上下文会消失。修复这个问题的路径是把划线转化为卡片。这篇讲的是筛选的标准、卡片的结构,以及 AI 在这个过程中可以发挥的作用。


目录
  • TL;DR
  • 1. 划线为什么容易失效
  • 2. 什么值得变成卡片
  • 3. 卡片的结构
  • 4. 转化的过程
  • 5. 系统实现
  • 5.1 存储结构
  • 5.2 UI 展示
  • 5.3 获取划线原文
  • 6. 现在能做什么,还不能做什么
目录
  • TL;DR
  • 1. 划线为什么容易失效
  • 2. 什么值得变成卡片
  • 3. 卡片的结构
  • 4. 转化的过程
  • 5. 系统实现
  • 5.1 存储结构
  • 5.2 UI 展示
  • 5.3 获取划线原文
  • 6. 现在能做什么,还不能做什么
目录
  1. TL;DR
  2. 1. 划线为什么容易失效
  3. 2. 什么值得变成卡片
  4. 3. 卡片的结构
  5. 4. 转化的过程
  6. 5. 系统实现
  7. 5.1 存储结构
  8. 5.2 UI 展示
  9. 5.3 获取划线原文
  10. 6. 现在能做什么,还不能做什么
阅读
相关文章
  • 01
    从阅读到知识(三):孤岛与碰撞2026/06
  • 02
    从阅读到知识(一):个人阅读档案的设计与实现2026/06
  • 03
    给网站加朗读功能(二):声音复刻而不是通用音色2026/07
← 上一篇网页内容导出(二):图片导出的 DOM 重建与批注内联
下一篇 →从阅读到知识(三):孤岛与碰撞

评论

© 2026 Vic Chen · 面向哲思的编程与架构CC BY-NC-ND 4.0

TL;DR

这是「从阅读到知识」系列的第二篇。上一篇在结尾留了三个问题。这篇回答第一个:我从这些书里真正记住了什么。

答案不乐观——绝大部分划线,在读完之后三个月就没法回忆起来了。问题不是记忆力,而是划线这种形式本身有结构性缺陷:它太依赖上下文,而上下文会随时间消失。

我认为,修复这个缺陷的路径是把划线转化为卡片。但转化本身不是难点,难点是决定什么值得转化,以及如何让卡片在脱离原书之后还能独立存在。


1. 划线为什么容易失效

划线时是阅读者在「当前语境」里做标记,已经读了前面几章,知道作者的框架,理解这句话是在什么前提下说的。

六个月后打开划线列表,这些前提都不在了(Chen 注:这个问题在 AI 身上有一个精确的类比。每次开启新的对话,上下文窗口是空的——它不知道你上次聊了什么,也不知道「你」是谁。如果你把一段对话记录扔给它,它能读懂每一句话,但很难还原出你当时提问的真实意图。划线的处境一样:内容还在,但当时阅读它的那个「我」已经不在了。)。很多当时觉得「这个重要」的句子,现在看完全不知道重要在哪里。

「系统的边界不是物理边界,而是决策边界。」

甚至可能当时划线的只是「决策边界」这样的短语。这句话是从哪本书里划的?在讲什么?在反驳什么?脱离了上下文,它只是一句听起来不错但很难用的格言。

这是划线的结构性问题:它记录了你的注意力,但没有记录你的理解。注意力是瞬时的,理解才是可以持续使用的东西。


2. 什么值得变成卡片

不是所有划线都值得转化。划线其实分两类:

  • 第一类:「作者写得好」——语言精准、表达优雅、句子漂亮。这类划线是审美反应,转化成卡片意义不大,因为离开原文的行文节奏,单独的一句往往失去了原来的力量。

  • 第二类:「这个观点值得记住」——这里有一个判断、一个反直觉的结论、一个可以迁移到其他领域的框架。这类才是卡片的原材料。

筛选标准可以简化成一个问题:如果我从没读过这本书,这句话能让我理解一个新的东西吗? 能,就值得转化。不能,就保留在划线列表里欣赏。

实践下来,大概 20% 的划线值得变成卡片。这个比例比我最初预期的低得多——我以为自己划的都是精华。


3. 卡片的结构

卡片和划线的根本区别在于:卡片不能依赖原书。Niklas Luhmann 的卡片盒方法(Chen 注:Niklas Luhmann 是德国社会学家,用卡片盒(Zettelkasten)方法管理知识长达数十年,最终产出了大量著作。他的卡片盒里有约 9 万张卡片,每张只记录一个想法,卡片之间通过编号互相引用,形成网状结构而非层级结构。)有一条原则:每张卡片必须能独立被理解,不需要回头查阅来源。这条原则的背后逻辑是,一张需要追溯上下文才能理解的卡片,和一条划线没有本质区别。

现在「阅读」页面里每张卡片由两层构成:

  • 原文:划线的原始文字,完整保留
  • 哲思:从原文提炼的一句话洞见,30–60 字,直接陈述观点

两层都需要展示。只有哲思没有原文,读者无从判断提炼是否准确;只有原文没有哲思,就退回到了划线本身——上下文依赖的问题没有解决。

「哲思」这一层的写法要求比较具体:不是复述,不是加形容词,是重新提炼出一个可以被单独引用的判断。如果对一段划线提炼不出这样的判断,说明这段划线属于第一类——写得好但不是独立观点,不值得变成卡片。


4. 转化的过程

把划线变成「哲思」,需要做的事情其实很机械:

  • 去掉上下文依赖(「如前文所述」「作者认为」之类的指代)
  • 把观点从句子里剥离出来,改写成可独立成立的陈述
  • 判断它属于第一类还是第二类划线

这些步骤可以手动做,也可以借助 AI。AI 的好处是快,而且不会因为「这段文字我还蛮欣赏的」就偷懒不改。但 AI 只是提速工具,核心判断——这段话值不值得变成卡片,提炼出来的观点有没有偏差——依然是人在做。

如果借助 API 来处理,可以用 tool_use(Chen 注:tool_use 是 Anthropic API 的一种调用方式:你定义一个带 JSON schema 的「工具」,模型被约束只能通过调用这个工具来响应。好处是返回值的结构是强制的,不需要再解析自由文本。) 约束输出格式,直接得到结构化的卡片数据:

src/app/api/cron/sync-cards-book/route.ts
async function generateCards(bookTitle, author, highlights) {
  const input = highlights
    .map((h, i) => `[${i + 1}] bookmarkId: ${h.bookmarkId}\n章节:${h.chapterTitle ?? ""}\n原文:${h.markText}`)




























这套实现目前跑的是定时任务:每天凌晨通过 QStash fan-out(Chen 注:QStash 是 Upstash 提供的消息队列服务。这里用作任务分发:dispatcher 拉取最多 200 本书的 notebook 列表,给每本书投递一条消息;QStash 并发分发,每本书由独立的 serverless 函数处理,互不阻塞。) 把任务分发到每本书独立处理——先拉该书的全部划线,跳过已存在的,对新划线逐条调用 API 生成卡片再将新划线每 100 条打包为一次 Claude 请求批量生成对新划线逐条调用 API 生成卡片(Chen 注:原实现每条划线单独一次 Claude 调用,划线量大时会超时。现改为批量:每 100 条打包为一次请求,通过 tool_use 返回整批的卡片数组,`bookmarkId` 原样返回用于对应。),写入 Redis。也可以不自动化,改成读完一本书手动触发一次,在审查草稿之后再存入——取决于你愿意在这件事上花多少摩擦。

✓
实践下来最重要的发现:AI 提炼划线本身不难,难的是它迫使你重新面对这些划线。偶尔翻看「今日」卡片的过程,就是重新回忆某本书的过程。即使那天的卡片只是一句普通的话,触发的回忆往往不止于此。

5. 系统实现

5.1 存储结构

卡片存在 Redis 里,没有本地文件。每张卡片对应一个 hash key,用 bookmarkId(微信读书划线的唯一 ID)作为主键:

weread:cards          ← sorted set,score = 划线时间戳,用于按时间排序
weread:card:{id}      ← hash,存卡片的所有字段

写入时用 pipeline 保证原子性:

src/lib/cards.ts
export async function storeCard(
  bookmarkId: string,
  card: StoredCard,
  timestamp: number
) {
  const pipeline = redis.pipeline();
  pipeline.set(CARD_KEY(bookmarkId), card);
  pipeline.zadd(CARDS_INDEX, { score: timestamp, member: bookmarkId });
  await pipeline.exec();
}

每次同步前先查 EXISTS weread:card:{bookmarkId},已存在的跳过,避免重复生成。全量读取时用 ZRANGE … REV 按时间倒序拿 ID 列表,再 MGET 批量取卡片内容。

存在 Redis 而不是本地文件,有两个好处:不需要维护代码仓库(卡片不进 git),以及可以在 Vercel 的 serverless 环境里直接读写。本地文件方案在 serverless 上是只读的,每次部署才能更新;Redis 的卡片库随时可以新增,不依赖部署。

5.2 UI 展示

卡片的入口在「阅读」页面的卡片 tab(Chen 注:第一篇设计的版本没有 tab 结构。书架是页面主体,统计数据排在底部。加入卡片之后,整个页面重构为三个 tab:卡片、书架、统计。主次因此变清晰了:卡片是理解和沉淀的载体,是这个页面真正的重点;书架和统计退为辅助信息,切过去看即可。页面的焦点从「我读了多少」转移到「我理解了什么」。),分两个区块:

  • 今日:每天一张,用日期字符串做种子,对所有卡片做哈希,挑出得分最小的那张。同一天不管刷新多少次,结果不变;第二天自动换一张。这比「今天的新划线」更合理,因为跑批是凌晨的定时任务,不一定每天都有新内容。

  • 发现:从所有卡片里跨分类各取一张,共五张,混排展示。有「换一批」按钮,点击重新随机。这个随机是客户端种子驱动的,刷新页面不会变——只有点换一批才会变。

每张卡片的视觉结构也对应双层设计:上方是「原文」胶囊,下方是「哲思」胶囊。封面 hover 会弹出 BookModal,里面有书名、分类、标签以及从微信读书 API 实时拉取的前三条划线预览。

这样的展示逻辑,是让卡片在没有刻意检索的情况下也能「出现」。读者不需要有目的地去翻,只是浏览页面就会碰到某张卡片,然后可能想起那本书,或者觉得这个判断和某件事有关。随机出现的价值不比主动检索低,有时候更高。

5.3 获取划线原文

所有划线从微信读书 API 拉取。上一篇里提到的 /api/weread 代理支持 /book/bookmarklist,返回某本书的全部划线。目前传给 AI 的上下文是书名和作者,已经足够让模型判断领域和风格。

如果想进一步提升提炼质量,可以额外拉 /book/chapterinfo 把章节标题也带进去——很多划线放回章节标题下就能立刻理解背景。目前没做这一步,因为单靠书名作者生成的卡片质量已经够用。现已实现,章节标题同时进入 prompt 和卡片展示层。目前没做这一步,因为单靠书名作者生成的卡片质量已经够用。(Chen 注:现已实现:处理每本书时额外调用 `/book/chapterinfo`,建立 chapterUid → title 的映射,章节标题随划线一起传入 prompt。卡片展示时也在原文末尾以 `# 章节名` 的形式标注来源章节。)


6. 现在能做什么,还不能做什么

这套系统现在能回答的:

  • 我读完这本书之后,沉淀了哪些我认为值得记住的观点
  • 这些观点现在是否还站得住脚(卡片里有来源,可以回查)
  • 某个主题下,我已经有哪些「库存观点」

但它还不能回答:

  • 这张卡片和那张卡片之间有没有联系——它们互相支持还是互相矛盾(从阅读到知识(三):孤岛与碰撞)
  • 某个领域里,我的观点集合有没有明显的盲区
  • 读另一本书时,已有的卡片能不能帮我提出更好的问题

这些问题需要卡片之间产生连接,而不只是堆在一个目录里。第一个问题加上上一篇还没解决的「不同书的观点怎么呼应或矛盾」,已在第三篇里处理。最后两个问题——盲区和阅读路径——留到第四篇。

.join("\n\n");
const msg = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 8192,
tools: [{ name: "save_cards", input_schema: {
type: "object",
properties: {
cards: { type: "array", items: {
type: "object",
properties: {
bookmarkId: { type: "string" },
content: { type: "string", description: "一句话洞见,30-60字" },
tags: { type: "array", items: { type: "string" }, description: "2-4个标签" },
},
required: ["bookmarkId", "content", "tags"],
}},
},
required: ["cards"],
}}],
tool_choice: { type: "tool", name: "save_cards" },
messages: [{ role: "user", content:
`书名:《${bookTitle}》作者:${author}\n\n${highlights.length} 条划线,逐条提炼,原样返回 bookmarkId:\n\n${input}`
}],
});
const block = msg.content.find((b) => b.type === "tool_use");
return block?.input?.cards ?? [];
}