Vandee's Blog

02 Aug 2025

PKM(个人知识管理)构建手册 - with AI

Intro

距离 PKM(个人知识管理)构建手册 - Emacs 居然已经一年多了。里面关于 PKM 和 AI 结合的部分许多已经过时了,今年也有了新的思路。

AI 发展的速度实在是太快,从 Cursor Tab 到现在的 vibe coding、Claude Code CLI,从 ChatGPT 到现在的 parallel agents,从 RAG 到 MCP,从 prompt 到 context engineering。

但不管 AI 如何发展,大语言模型的能力如何提升,LLM 始终是一个输入到输出的过程。从 RAG 到今年的 MCP 和 context engineering ,LLM 获取外部资源的方式和能力有了质的飞跃,context window 也越来越大。数据和数据质量一直是 LLM 的核心之一。

起初折腾 PKM 的目的之一就是构建和 LLM 交互的数据。正如 Notion 创始人 Ivan Zhao 说的,现在的 AI 产品就像酿酒。你无法完全控制结果,只能创造一个环境,让其自行发展。因此,它更像是一种酿造过程,你无法强迫 AI。产品是酒桶,数据才是水、小麦和酒花。数据的质量越高,酿出的酒就越香。

AI 使得从 0 到 1 的开始变得更容易:

Before AI, learners faced a matching problem: learning resources have to be created with a target audience in mind. This means as a consumer, learning resources were suboptimal fits for you:

在AI之前,学习者面临一个匹配问题:学习资源必须针对特定受众创建。这意味着作为消费者,学习资源对你来说是不理想的匹配:

  • You're a newbie at $topic_of_interest, but have knowledge in related topic $related_topic. But finding learning resources that teach $topic_of_interest in terms of $related_topic is difficult.

    你是$topic_of_interest 的新手,但对相关主题$related_topic 有知识。但是找到以$related_topic 为基础教授$topic_of_interest 的学习资源是困难的。

  • To effectively learn $topic_of_interest, you really need to learn prerequisite skill $prereq_skill. But as a beginner you don't know you should really learn $prereq_skill before learning $topic_of_interest .

    为了有效地学习 $topic_of_interest,你需要学习先决技能 $prereq_skill。但是作为初学者,你不知道你应该在学习 $topic_of_interest 之前先学习 $prereq_skill。

  • You have basic knowledge of $topic_of_interest, but have plateaued, and have difficulty finding the right resources for $intermediate_sticking_point

    你对 $topic_of_interest 有基本知识,但已经停滞不前,并且在寻找合适的 $intermediate_sticking_point 资源方面遇到困难。

via: https://elroy.bot/blog/2025/07/29/ai-is-a-floor-raiser-not-a-ceiling-raiser.html#license

Manual

这个手册的重点是分享一些思路,我选择的这些实现只是参考,找到适合自己的方法才是这个手册的目的。PKM 基本的构建思路都在 PKM(个人知识管理)构建手册 - Emacs ,这个手册侧重和 AI 的整合。

Prerequisites

LLM API KEY,终端或 IDE,AI 和编程的基础知识,还涉及到一点 self-hosting。

Principles

基于 transformer 架构的 LLM 本质上就是一个概率生成器,因此 PKM with AI 总体的原则就是:让 LLM 提升自己的能力,帮助自己找到实现目标的最大可能性方案。

  • 让 AI 帮助自己提出更好的问题,而不是 AI 速读
  • 让 AI 去掉无关信息,更好的聚焦 key points
  • 让 AI 评估、测试自己的思考和方案,更好的迭代
  • 让 AI 生成路标和地图,而不是代替自己走完全程

Methods

Google NotebookLM 是 AI 和知识、文档交互的一个很完美的实现,但我始终认为这种层面的知识管理和交互并不能很有效的提升思维。

各种 RAG 类知识管理项目或软件,从本质上达到的效果是:根据特定的文档获得更准确的检索结果的生成。GraphRAG 效果确实可以比传统 RAG 更好,但它们始终是在学习资料中「检索」和「生成」。 它可以提升知识管理的效率,但这和思维的提升是两码事。

而且,只要是生成,就会降低准确性,对我来说,我不想在检索知识的时候,看到 LLM 一大堆废话,只想准确快速的找到我想检索的信息。虽然通过特定的 prompt 可以优化 RAG 生成内容和原文一致性,但问题的关键不在这里,生成其实是不必须的,关键在检索。而传统的文档检索其实就相当够用了,我一直在 Emacs 里使用正则和 tags 进行检索,这比丢给 LLM 20 个文档,等个 1 分钟要快太多了。

所以 PKM with AI,我的思路是:让 LLM 更好的配合 PKM 完成检索,生成路标和知识网点地图,把检索做到极致,去掉生成的部分。

因此我构建了 NoNotes ,用向量相似检索自动补全替代 RAG 生成。创造来源于思维的流动,而思维的流动就是联想,我在需要相关概念的时候,直接检索光标当前内容,在高度原子化的 PKM 里通过向量相似检索补全相关的概念和引用来源。

随着 context engineering 概念的兴起,我觉得 memory layer,数据接口,是 PKM 和 AI 结合比较合适的方向,例如 supermemory 这个项目,从它刚开始就一直在关注,现在发展的挺不错的。

用 Dify、n8n 构建 Workflow 也是很不错的选择,但是对于 PKM 我不太想增加太多的中间流程,也不想花太多时间为了 PKM 去学它们。

信息获取 - 输入

获取零散信息、新闻我一直通过 RSS,ReadWise 今年不打算续订了,转向了 Hoarder 。修改了一下 obsidian-clipper 用来在网页里高亮阅读保存到 Hoarder。在 Emacs 里写了一个小脚本,用来同步 Hoarder 的高亮和笔记到本地。还搞了个小玩意 RSS-CLI

我尝试过好几个 AI 聚合信息的项目,起初体验是挺不错,每天 AI 自动根据信息源生成摘要和简报,但时间久了,感觉就像是在吃预制菜,甚至是二手预制菜(许多信息源也就是 AI 生成的)。而且由于是用同一个总结模板 prompt 在总结不同类型的信息,很多时候总结的要点都不准确。

还是在 RSS 阅读器里自己看更有滋味。

再一个,与其让 AI 从海量信息源里找到自己感兴趣想要的,不如转向关注自己感兴趣的人和圈子,多接触到具体的人和事。信息茧房其实也就是自己更愿意相信或接受什么罢了,不同的圈子和阶层都固然会有信息茧房,要不然就不是圈子了。

我的 RSS 订阅源是日积月累慢慢发现的,这种发现只有用心看了文章才会有,AI 速读总结不出作者隐藏在字里行间里的思维以及写作的表达技巧。

文档、知识管理 - 迭代

  • 笔记编辑器选择

    Notion 其实一直是一个很不错的选择,特别是现在的 AI 功能挺好用的,上手也没有什么门槛。obsidian 也不用多说,现在有很多很好用的 AI 插件,obsidian 的生态是真的活跃。

    但是用了 Emacs 之后,其他的就真不想用了。

    今年从 org-roam 转为使用 denote ,org-roam 的 UI 从来就没怎么用过,除了可以对外展示一下有多少笔记之外,我觉得没啥用。

    Emacs 里的 org 文档作为初始笔记,高度原子化之后整理到 tiddlywiki,形成常青笔记和原子笔记。

    在原子笔记里,我又精炼了一部分,向量化之后储存在向量数据库,通过 NoNotes 补全。如果遇到哪个概念补全不出来,那就说明这个概念还没有掌握或着精炼的程度不够。

    cloud.Llamaindex 和 NoNotes 的实现类似,可以 index 文档,提供 RAG 接口,Qdrant 这类向量数据库加上 MCP 也可以达到类似的效果。自动补全用自己喜欢的语言再写一个和系统交互的 API 就 OK 了。

    笔记的同步就用 GitHub 就好了,笔记的版本管理和备份是很必须的,GitHub 的 commit 对于管理笔记太合适了。如果特别看重隐私或者讨厌 GitHub,可以自托管 GiteaGitLab

  • 迭代

    每个星期我会让 LLM 总结一下 PKM 里新增的内容,用 python 写了一个小脚本获取我 GitHub 仓库的 git diff,可以选日期范围,指定的仓库。PKM 丢在了 GitHub ,脚本会生成一个 MD 文档,包含 git commit 新增的内容,把文档丢给 LLM,让它提出几个相关的深度思考问题。

    在 prompt 的设计上,我喜欢让 LLM 扮演一个批判者,用完全相反的视角分析我的观点,往往会有意想不到的惊喜。

    也可以用 n8n 或者 GitHub Actions 自动完成这个过程,让 LLM 每周敲打一下自己。

    迭代是我觉得 PKM 里最重要的一环,让 AI 帮助自己提出更好的问题,找到自己 知识的缝隙

  • 其他和 AI 交互的工具

    一个 Gemini CLI 管理笔记的例子:Gemini CLI 在半小时内整理了 400 个笔记,在各个主题之间建立了有意义的联系,重命名、整理,合理构建笔记集合,via: https://x.com/karminski3/status/1939502900503355669

    这些 CLI 工具不拿来编程,做 PKM 管理也很合适。比起 chat UI,这些 CLI coding 工具提供了更底层、更便捷的 LLM 接口。

    在 Emacs 里,我一直用 gptel,也很方便。其他和 vibe coding 相关的我记录在了 Vibe Coding Tips ,小工具记录在了 碎片知识学习 - with AI

笔记和写作 - 输出

PKM 知识管理只是学习的方法,永远不是目的。做知识管理很容易就会做成了管理知识,变成了折腾各种笔记工具,学习各种笔记软件,甚至被笔记软件或平台捆绑。这也是 Nonotes 想避免的,笔记不应该被笔记软件或者笔记载体局限甚至捆绑。

学习资料在变成笔记的过程中,在精炼和重述的时候,思维已经得到了一些锻炼,但这还远远不够。

PKM 的迭代一定要有表达和输出。如果你思考而不输出,你只是以为自己在思考 ,脑子里知道了,和写出来,讲出来,画出来是两码事。

关于表达,我想讨论的重点不是表达能力和华丽的写作技巧,而是:表达和输出是自我和外界信息的交互、反馈和对抗。用嘴说话和面部表情是我们最平常最熟悉的思维输出和表达,写作、音乐、绘画、编程其实也是一样,只是用了不同的载体和形式。

如果把人和人的思维比作计算机,外界信息给定一个输入,人给出输出(没有反馈和输出这里也算作输出的一种,也就是输出为 0),每一个时间、空间下自己的输出就构成了当下别人眼里的自己和自己认为的自己,而每一个当下就构成了自我和物质世界之间的信息交互,也就是所谓的存在。

我挺喜欢汪峰的 存在 ,名字和自我介绍从我们出生开始,到最后刻在墓碑上,没有多少人给了自己这个问题很好的答案:我该如何存在。

希望在 AI 的帮助下,我能够找到我的答案。

自定义 Emacs 函数

simple claude:

使用 shell,快速运行 Claude Code 执行 claude -p 指令回答一些简单的问题,把结果输出到单独的 buffer。有时候不想用 gptel 就用这个。可以替换成 Gemini 和 Codex。

;; simple claude code shell command
(defun my/claude-shell-command (prompt)
  "Execute claude command asynchronously and display the output."
  (interactive "sClaude prompt: ")
  (let* ((output-buffer-name "*Claude Output*")
         (output-buffer (get-buffer-create output-buffer-name))
         (shell-program (or (getenv "SHELL") shell-file-name))
         ;; Use shell-quote-argument
         (command-str (format "claude -p %s" (shell-quote-argument prompt))))
    (with-current-buffer output-buffer
      (setq buffer-read-only nil)
      (erase-buffer)
      (setq-local header-line-format (format "Claude Output for prompt: %s" prompt)))
    ;; (display-buffer output-buffer) ; Show the buffer immediately
    (message "Claude command running asynchronously...")
    ;; Start the async process
    (let ((process (start-process "claude-process"
                                  output-buffer-name
                                  shell-program
                                  "-lc"
                                  command-str)))
      ;; Set a function to be called when the process finishes
      (set-process-sentinel process #'my/claude-process-sentinel))))

(defun my/claude-process-sentinel (process _event)
  "Sentinel for the claude async process. Handles success and error cases."
  (when (memq (process-status process) '(exit signal))
    (let* ((buffer (process-buffer process))
           (exit-code (process-exit-status process)))
      (cond
       ;; Case 1: Process failed (non-zero exit code)
       ((/= exit-code 0)
        (kill-buffer buffer)
        (if (= exit-code 127)
            (message "Error: 'claude' command not found. Please ensure it's in your shell's PATH.")
          (message "Error: Claude command failed with exit code %d." exit-code)))

       ;; Case 2: Process succeeded but produced no output
       ((zerop (with-current-buffer buffer (buffer-size)))
        (kill-buffer buffer)
        (message "Claude command finished with no output."))

       ;; Case 3: Success
       (t
        (with-current-buffer buffer
          (setq buffer-read-only t)
          (goto-char (point-min)))
        (display-buffer buffer)
        (message "Claude command finished."))))))

浏览器划词提问:

利用 org-protocol 和 JavaScript 对浏览器页面里的特定内容在 Emacs 里用 gptel 提问

(require 'org-protocol)
(require 'gptel)

(require 'server)
(unless (server-running-p)
  (server-start))

;; Handler for gptel queries from browser
(defun my/gptel-org-protocol-handler (info)
  "Handle gptel query from org-protocol.
INFO is the data passed by org-protocol."
  (let ((text (plist-get info :text)))
    (when (and text (not (string-empty-p text)))
      (let ((query (decode-coding-string (url-unhex-string text) 'utf-8)))
        (message "Received gptel query: %s" query)
        ;; Create or switch to gptel buffer
        (let ((buffer (get-buffer-create "*gptel-browser*")))
          (with-current-buffer buffer
            (unless (eq major-mode 'gptel-default-mode)
              (funcall gptel-default-mode))
            (gptel-mode 1)
            (goto-char (point-max))
            (insert "\n\n--- From Browser ---\n")
            (insert query)
            (insert "\n")
            (goto-char (point-max))
            ;; Send the query to gptel
            (gptel-send)
            (display-buffer buffer)))))))

;; Register the protocol handler
(setq org-protocol-protocol-alist
      (append org-protocol-protocol-alist
              '(("gptel-browser"
                 :protocol "gptel"
                 :function my/gptel-org-protocol-handler))))

;; JavaScript bookmarklet for browser (copy this as bookmark URL):
;; Basic version:
;; javascript:(function(){const selectedText=window.getSelection().toString().trim();if(!selectedText){alert('Please select some text first');return;}const pageTitle=document.title;const pageUrl=window.location.href;const fullText=`From: ${pageTitle}\nURL: ${pageUrl}\n\nSelected text:\n${selectedText}`;const encodedText=encodeURIComponent(fullText);const protocolUrl=`org-protocol://gptel?text=${encodedText}`;window.location.href=protocolUrl;})();

;; Enhanced version with prompt for question:
;; javascript:(function(){const selectedText=window.getSelection().toString().trim();if(!selectedText){alert('Please select some text first');return;}const pageTitle=document.title;const pageUrl=window.location.href;const userQuestion=prompt('Optional: Add a question or context:');let fullText=`From: ${pageTitle}\nURL: ${pageUrl}\n\n`;if(userQuestion){fullText+=`Question: ${userQuestion}\n\n`;}fullText+=`Selected text:\n${selectedText}`;const encodedText=encodeURIComponent(fullText);const protocolUrl=`org-protocol://gptel?text=${encodedText}`;window.location.href=protocolUrl;})();

Thanks

Tags: PKM