普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月20日未分类

告别 kubectl 黑框,开源一个基于 Wails 打造 K8S 多集群管理工具

2026年4月20日 07:58
如要阅读全文,点击标题跳转。做运维久了,你一定懂那种感觉:管理多个K8S集群时,经常需要使用kubectl xxx来进行管理,我想,你一定和我一样,深受其苦。此前我还在群里问过有没有优秀的多集群桌面端管理工具。当时还有群友打趣说让我自己开发一个。虽然我当时也是打趣回应了一下,但实际在我的内心,一直有一个写一个好用的桌面端K8S多集群管理工具。有人可能会问:市面上有不少优秀的 web 端管理工具,为何不用呢?对我而言,因寄存于浏览器而存在的web端管理使用体验,远不如桌面端来的稳定及安心。但我也深知从零实现一个功能完备,顺手好用的桌面端软件,并不是一个轻松的事情,如今工作繁忙,业余时间精力有限,不足以支撑我来写一个这样的项目,于是,这个想法始终在内心搁置着。

周报 #107 - 基于 Multica 与 Impeccable 的开发/设计工作流

2026年4月19日 23:15

前言

weekly_review_107

本篇是对 2026-03-302026-04-19 这三周生活的记录与思考。

清明假期没出门,在家窝着写代码,清掉了很多 TODO,久违地感受到 Vibe Coding 带来的快乐;用 Impeccable 把 Web3Insight 还有一些网站重新设计了一下,效果出乎意料地好;也开始用 Multica 重新梳理了自己各个项目的工作流,是自己很满意的 Agent 协作形态了,很沉迷;陪学姐去江阴参加了游泳比赛;还有很多有意思的事。

基于 Multica 的 Agent 开发工作流

之前 Xuanwo 在做一个叫 Luban 的 IDE 工具,其中就主要使用了看板这一形态,当时重度使用,也参与了一些贡献,一直觉得这也是未来 Agentic 开发的新形态,不过后来没再更新了,于是在等 Linear 是否会推出能够连本地 Coding Agent 的工具。

然后突然在 Twitter 上看到了一个很有意思的开源项目 Multica,是 Devv 的创始人做的,很早期但功能已经可用,开源第一天我就尝试自部署用上了,很惊喜,稍作配置就融合了我原本的开发工作流。

multica_runtime

它最巧妙的设计是通过 multica 命令行连接自部署的 server 端,把本机设备设置成一个 Runtime,而不需要自己单独去重新配置各种 Rules、MCP 等。

multica_agents

注册了 Runtime 之后,就可以创建 Agents,可以为每个 Agent 设置不同的指令、Skills、自定义参数,这样可以自定义一个 “AI 员工”,为它配置工作职责范围与技能等。

multica_new_issue

而后续在新建 Issues 的时候就可以直接指定给 Agent 员工,由于各个 Agent 员工的工作记录都在 Multica 平台中记录着,这样等于所有不同同事们的本地 Agent 奇妙地有了一个共享知识库和上下文,在一些关联性比较高的协作任务中尤其有用。

multica_autopilot

Multica 做得最好的其实还是克制,起初并没有过多去增加太多看板相关的功能,甚至连搜索功能都没有,而是更多放在了与本地 Agents 的链接上,而这三周也陆续增加了 Projects 以及 Autopilot 功能,可以在 Workspace 中区分项目,以及可以自动地做一些定时/重复性的任务,现在也还在快速更新迭代中,期待能够有更多贴合工作流的功能。

还在将自己更多项目实践引入 Multica,以及尝试和新搭建的 Hermes Agent 进行结合,后续看看能不能把开发、测试、预发布验证和正式发布流程更加完善。

Impeccable 设计工作流

其实之前就使用过 Frontend Designweb-design-guidelinesui-ux-pro-max 插件与 Skills 来进行 UI/UX 的交互优化设计,但其实只是一些最佳实践的辅助,并没有很系统性地做一些风格设计等,大部分都还是基于 v0.dev 的基础设计能力。

而最近看到另一个项目 Impeccable,在 Web3Insight 项目上尝试了一下,效果出乎意料地好。

web3insight_ai_01

web3insight_ai_02

web3insight_ai_03

web3insight_dash_01

web3insight_dash_02

详细视频可以看这条动态或者直接访问如下网站:

Impeccable 很好的一点是把抽象的 UI 设计拆成了几个步骤,在新设计/改动原有项目设计之前,会先通过 /impeccable teach 对项目进行分析,形成一份 .impeccable.md 文档,就像是使用 Claude Code 或者 Codex 进行开发时的 CLAUDE.mdAGENTS.md,但这份文档并不是记录详细设计细节,而是对网站的用户画像、使用习惯、品牌调性等进行分析,这些会更宏观地影响整个设计风格,例如对 Web3Insight 网站的分析

impeccable_commands

后续的所有开发都会基于整体的设计原则和思路进行拓展,通过 /impeccable craft 来新设计页面/组件,通过 /critique/audit 来分析和审计当前设计的问题,以及通过 /polish/optimize 或是 /animate 等命令来针对特定设计方向进行针对性优化,这样能够生成更可持续复用的设计组件而不是为了某个页面优化引入的”一次性代码“。

个人生活剪影

euka_launch

最近有很多工作任务到了收尾阶段,还经历了一次惊魂未定的「上线 -> Vercel Rollback -> 二次上线」 ,跟所有用户 Payment 有关的大重构上生产的体验真的太吓人了,从 5am 肝到了第二天 5am,但比起之前工作的忙碌,这次反而不觉得累,果然工作成就感和热情才是最影响工作状态的。

boyi_swimming_jiang_yin

这几周也由于一直忙项目几乎没怎么出门,周末护送学姐去江阴参加了一场游泳比赛,两天密集的行程有些累,但学姐得了个第五名,有种家长带小孩去参加兴趣班和比赛的成就感。

活动本身的话,我自己不算是游泳爱好者,但也有被现场的活力和体育精神感染到,更加下定决心开始规律运动了。

有趣的事与物

输入

虽然大部分有意思的输入会在 「Yu's Life」 Telegram 频道里自动同步,不过还是挑选一部分在这里列举一下,感觉更像一个 newsletter 了。

我把 Telegram Channel 消息作为内容源搭建了一个微博客 —— 「daily.pseudoyu.com」,可以更方便浏览了。

收藏

文章

视频

剧集

  • 爱的迫降,下饭看的,脑洞很大,还有一些有趣的跟《隐秘而伟大》的联动,看完有点想去了解下朝鲜/延吉的真实生活了。
  • 单身即地狱 第五季,第一次看韩国真人秀/恋综,太喜欢珉志了!感觉恋综最大的魅力还是在帮助自己重新关注到感情和生活里浪漫的部分,在为别人感情感动的同时也提醒自己不要忘记表达爱。
  • 莎拉的真伪人生,看到奈飞推荐好奇点开看的,剧情牵强但是节奏还算可以。
  • 我的天才女友 第二季 / 第三季 / 第四季,四季一口气看完有种陪着角色度过了整个人生的怅然,贫穷的小镇、暴力、金钱、政治,拥有平凡的一生常常也会是一种奢求。
  • 太平年,可能因为讲的是吴越,和看《大明王朝1566》杭州时候有一种莫名亲切感,虽然有矫饰和美化,但还是很能让人回到那段历史的正剧了,近几年都难得找到像水丘公的剧情那样让我整个人都被牵动了。
  • 黑袍纠察队 第五季,在看。

如何进行站点网络优化

作者 Raymond
2026年4月19日 20:00

站点的网络访问速度受到用户所在地区、网络运营商等多种因素的影响。针对这些差异,我们可以通过选择合适的 CDN 和机房来有针对性地优化特定地区、特定运营商下的访问速度。与此同时,客户端可以对不同域名进行测速,根据测速结果动态选择延迟最低的线路。

网络优化大体上可以分为两个方向:静态资源加速动态资源加速

静态资源加速

方案对比

下图对比了三个站点在印度尼西亚不同运营商下的整体耗时、DNS / TCP 耗时,以及平均传输速度,三个站点分别托管于腾讯云、Amazon 和 Cloudflare:

从数据来看,DNS 耗时和 TCP 建连耗时在不同方案间差异较大,而传输速度方面腾讯云表现更为突出。因此,针对不同地区与运营商,选择合适的 CDN 组合才能达到最优效果。

Cloudflare

Cloudflare CDN 会根据 Cache-Control 等响应头自动在边缘节点缓存静态资源,无需额外配置。在此基础上,还可以使用 R2(对象存储)作为资源的持久化存储后端,配合 Edge Worker 实现自定义的缓存逻辑(例如按地区或版本精细控制缓存行为),借助 Cloudflare 遍布全球的边缘节点就近响应用户请求。

腾讯云

腾讯云方案以 EO(边缘安全加速平台)为入口,结合 COS(对象存储)实现区域化加速:

  • 默认情况下,EO 上的静态资源回源至 AWS CloudFront 作为唯一源站。
  • 当某个地区的访问速度较慢时,可以在腾讯云该地域新建一个 COS Bucket,并配置 EO 将来自该地区客户端的请求回源到本地 COS,从而绕过跨境链路。

两种回源模式下,COS 对”资源已存在”的处理是一致的——直接返回,无需再回源;差异在于”资源不存在”时的行为:

  • 同步回源:COS 实时向 CloudFront 源站发起请求,拉取资源后存入 COS 并同步返回给客户端
  • 异步回源 + 302 重定向:COS 向客户端返回 302,将请求重定向至 CloudFront 源站,同时在后台异步拉取资源存入 COS

此外,COS 还支持异地复制功能,可以将数据同步到多个地域的 Bucket,实现全球多点就近访问。现阶段优先在大陆建立 Bucket 进行加速,后续如有其他地域需求,新建 Bucket 并配置多 Bucket 同步即可平滑扩展。

同步回源

异步回源 + 302 重定向

动态资源加速

相比静态资源,动态请求无法被缓存,每一次都需要完整地回源,因此跨境网络的质量直接决定了动态接口的响应速度和稳定性。

跨境网络线路

在中国大陆访问海外服务器时,不同线路的质量差异相当大,从高到低大致可以分为三类:

专线网络

IPLC(国际私有租用线路)和 IEPL(基于以太网的国际专线)是质量最高的一类。独享带宽、不经过公网、延迟稳定且几乎无拥塞,上海到东京的典型延迟在 30~40ms,抖动极小。代表产品有腾讯云全球应用加速(GAAP)和阿里云全球加速(GA)。缺点是价格昂贵,通常用于金融、企业内网等对稳定性要求极高的场景。

运营商优化线路

介于专线和普通公网之间,按覆盖范围又可以细分:

  • 三网优化(CN2 + 9929 + CMI):同时对电信、联通、移动三大运营商优化,根据用户运营商自动选择最优路径,是高端海外机房常见的线路配置。
  • 单运营商优化:CN2 GIA(电信)、9929(联通)、CMI / CMIN2(移动),针对单一运营商优化,其他运营商用户效果一般。
  • CN2 GT:CN2 的普通版本,成本较低但容易拥塞,许多低价 VPS 使用此线路。

普通公网线路

包括普通 BGP 多线(电信 163、联通 4837)及各类国际 Transit 线路。BGP 只代表同时接入多个运营商,并不代表线路质量高,晚高峰拥塞时丢包率可能达到 5%~10%,延迟波动明显。很多海外云服务器默认走的就是这类线路。

整体质量排序如下:

1
2
3
4
5
6
7
8
9
10
11
IPLC / IEPL 专线

三网优化 (CN2 + 9929 + CMI)

单运营商优化 (CN2 GIA / 9929 / CMI)

CN2 GT

普通 BGP 多线

普通国际线路 (163 / 4837 / Transit)

很多跨境加速方案的典型架构是:在国内部署入口节点,通过优化线路连接到香港或东京的中转节点,再由中转节点访问海外源站,从而将不稳定的跨境段控制在质量最优的链路上。

具体方案

腾讯云全球加速(GAAP)/ 阿里云全球加速(GA)

这两者都是基于云服务商私有骨干网络的托管加速服务——流量不经过公网,由云厂商内部专属链路承载,质量接近传统专线但使用门槛更低。配置简单、效果稳定,但费用较高,适合对延迟和稳定性有严格要求、且预算充足的场景。

腾讯云 EO 自建中转

对于成本敏感的场景,可以通过腾讯云 EO 配合自建中转节点的方式,用较低的成本实现接近专线的效果。下面按演进路径逐步介绍。

方案一:EO 直接回源(默认)

EO 默认直接通过公网回源海外源站,跨境链路质量完全取决于运营商,没有任何保证。实际使用中经常出现回源连接失败、延迟波动大的情况,体验较差。

方案二:香港 CN2 中转节点

在香港部署一台中转服务器,将 EO 的回源地址指向该节点,由节点再转发到海外源站。大陆到香港的链路经过 CN2 优化,质量大幅提升,回源成功率明显改善。

中转节点的线路质量对效果影响很大。以腾讯云香港轻量服务器为例,在不开启优选流量包的情况下,从大陆 ping 该服务器,延迟抖动非常严重:

1
2
3
--- ping statistics ---
41 packets transmitted, 32 packets received, 22.0% packet loss
round-trip min/avg/max/stddev = 42.799/297.543/2353.816/483.179 ms

购买并开启优选流量包后,同一台服务器的表现天壤之别:

1
2
3
--- ping statistics ---
35 packets transmitted, 35 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 42.503/45.286/79.135/6.069 ms

丢包率从 22% 降至 0%,平均延迟从 297ms 降至 45ms,标准差也从 483ms 收窄到 6ms,链路稳定性显著提升。因此香港节点务必开启优选流量包,否则中转效果有限。

实际观察中,这个方案偶尔仍会出现某个地区在特定时间段回源连接失败的情况,原因是该地区到香港的链路在该时间点出现了短暂波动。

方案三:广州节点 + 香港中转(当前方案)

为彻底解决大陆到香港偶发波动的问题,在广州再增加一层节点。EO 不再直接回源香港,而是先回源广州节点——这段完全在国内网络内,几乎不存在不稳定的情况。广州节点再通过优化线路回源香港中转节点,由于广州到香港距离极近,这段链路同样非常稳定。最终由香港节点通过优化过的跨境链路访问海外源站。

为保障可用性,广州层和香港层均部署了多个节点,由负载均衡层分发流量;香港的多个 VPS 节点再统一回源日本东京的真实源站。这种多节点冗余的设计,使得单个节点故障不会影响整体可用性。

整体思路是将链路拆解为”国内段 + 跨境段”分别优化:国内段利用大陆内部稳定的网络,跨境段控制在广州到香港这段极短且质量稳定的链路上。与 GAAP/GA 相比,这种方案用自建中转节点(轻量服务器 + 优选流量包)替代了托管专线,在成本上有明显优势,稳定性也能达到接近的效果。

Emacs Lisp 热重载实用指南

2026年4月19日 08:00
* 问题 Emacs 用户经常无意识地做一件事:修改 =init.el= 中的代码,求值,然后继续在同一个 Emacs 实例中工作。这就是 Lisp 的热重载——不需要重启进程,代码直接在运行中被替换。 大部分时候这很顺畅。但有一个陷阱: =defvar= 、 =defcustom= 、 =defface= 在重新求值时 *不会* 更新已有的值。 #+BEGIN_SRC elisp ;; 第一次求值 (defvar my-var "hello") ;; my-var → "hello" ;; 修改后重新求值 (defvar my-var "world") ;; my-var → 仍然是 "hello"! #+END_SRC 这是故意这么设计的——加载库不应该覆盖用户的自定义值。但在开发包的时候,这个设计让人困惑:你改了默认值,重新求值,发现什么都没变。 下面是从轻到重的五种解决方案。 * 方法一:eval-defun(C-M-x) 这是最常用的方法,也是最容易被忽略的: =eval-defun= (=C-M-x=)对 =defvar= 、 =defcustom= 、 =defface= 有特殊处理——它会 *无条件* 设置变量的值,不管这个变量是不是已经有值。 #+BEGIN_SRC elisp (defvar my-var "new-value") #+END_SRC 把光标放在这个 =defvar= 形式上,按 =C-M-x= , =my-var= 就会被更新为 ="new-value"= 。 注意: =eval-buffer= 和 =eval-region= *不会*这样做——它们遵守正常的 =defvar= 语义。只有 =eval-defun= 是例外。 这是 Bozhidar Batsov 在开发 mode 时最常用的方法:只改了几个形式,就逐个 =C-M-x= 。 * 方法二:setq(M-:) 如果只需要快速重置一个变量: #+BEGIN_SRC elisp M-: (setq my-var "new-value") #+END_SRC 简单粗暴,但只改变量值,不会触发其他代码的重新执行(比如函数定义、hooks 设置等都不会变)。适合临时调参数。 * 方法三:load-file =M-x load-file= 从磁盘加载 =.el= 文件。跟 =require= 的区别是: =require= 发现 feature 已加载就会跳过, =load-file= 总是重新读取并求值文件。 但 =load-file= 仍然遵守 =defvar= 语义——已绑定的变量不会被更新。它的优势在于可以加载非当前编辑的文件,比如包的依赖文件,或者没有 =provide= 形式的文件。 * 方法四:unload-feature + require(干净重载) 这是最彻底的非重启方案: #+BEGIN_SRC elisp ;; 卸载 feature:移除它定义的变量、函数、hooks 等 (unload-feature 'my-package) ;; 重新加载 (require 'my-package) #+END_SRC =unload-feature= 会把 feature 定义的一切都清掉,然后 =require= 从头加载。这最接近"干净的状态"。 注意事项: - =unload-feature= 可能有副作用——如果 feature 添加了 hooks 或 advice,卸载时应该会清理,但并非所有包都正确实现了清理逻辑,可能出现残留的 hook 或 advice - 只对通过 =provide= / =require= 加载的 feature 有效;用 =load-file= 加载的文件没有 feature 可以卸载 - 某些复杂的包不能干净地卸载,但大多数简单的 major mode 没问题 可以绑一个快捷键方便开发时使用: #+BEGIN_SRC elisp (defun my-reload-package (package) "Unload and reload PACKAGE." (interactive "SReload package: ") (unload-feature package) (require package) (message "Reloaded %s" package)) #+END_SRC * 方法五:重启 Emacs 终极方案。当其他方法都不管用,或者你改了太多东西、不信任当前运行时状态时,重启。 有了 =desktop-save-mode= 或 session 管理器,重启的代价不大。Emacs 29+ 内置了 =restart-emacs= 命令,或者用 [[https://github.com/iqbalansari/restart-emacs][restart-emacs 包]]。 * 别忘了重新激活 Mode 无论用哪种方法重载了代码,还需要在已有 buffer 中重新激活 mode: #+BEGIN_SRC elisp M-x my-mode ;; 重新运行 mode 函数,应用新的 keymap、hooks、font-lock 等 #+END_SRC 对于 minor mode,开关两次: #+BEGIN_SRC elisp M-x my-minor-mode ;; 关闭 M-x my-minor-mode ;; 重新打开 #+END_SRC 不做这一步,已有 buffer 仍在跑旧代码——因为 mode 激活时会把 keymap、font-lock 规则、hooks 等设置为 buffer-local 的值,重新加载代码只更新了函数定义,并没有更新这些 buffer 里已经设好的局部值。 * 推荐工作流 Bozhidar Batsov 在开发 major mode 时的典型流程: 1. 编辑源码 2. =C-M-x= 逐个求值修改过的形式 3. 切到测试 buffer, =M-x my-mode= 重新激活 4. 如果状态不对, =unload-feature= + =require= 干净重载 5. 改了 autoloads 或包元数据等根本性东西时才重启 Emacs 大多数时候,前三步就够了。 参考:[[https://emacsredux.com/blog/2026/03/25/reloading-emacs-lisp-code/][Reloading Emacs Lisp Code — Emacs Redux]]

TIL: 自动使用项目虚拟环境的 Python

2026年4月19日 08:00
在 Emacs 里按 =C-c C-p= 打开 Python shell,默认用的是系统 Python。如果项目有虚拟环境,shell 里就找不到项目安装的包。 Einar Mostad 写了一个函数,自动在项目目录中递归查找 Python 可执行文件,找到就用虚拟环境的,找不到就用系统的: #+BEGIN_SRC emacs-lisp (defun emo-python-virtualenv () "Sets the python shell to python from a virtual environment if one exists." (when (project-current) (let ((pythonpath (nth 0 (directory-files-recursively (nth 2 (project-current)) (if (eq system-type 'gnu/linux) "python$" "python.exe$"))))) (when (file-exists-p pythonpath) (setq-local python-shell-interpreter pythonpath))))) #+END_SRC 加到 =python-mode-hook= 上,每次打开 Python 文件时自动设置: #+BEGIN_SRC emacs-lisp (add-hook 'python-mode-hook 'emo-python-virtualenv) #+END_SRC 三个关键点: 1. ~directory-files-recursively~ 接受正则表达式,用 ="python$"= 匹配虚拟环境 =bin/= 目录下的 Python 可执行文件(不匹配 =python3= 、 =python-config= 等) 2. ~project-current~ 返回当前项目信息, ~(nth 2 (project-current))~ 取项目根目录 3. ~setq-local~ 把 =python-shell-interpreter= 设为 buffer-local,不影响其他 buffer 参考:[[https://einar.codeberg.page/use-python-shell-from-virtual-environment-if-there-is-one-in-Emacs.html][Use python shell from virtual environment if there is one in Emacs]]

TIL: 让 Help buffer 自动获得焦点

2026年4月19日 08:00
在 Emacs 里查看函数或变量的文档( =C-h f= 、 =C-h v= 等),Help buffer 打开后焦点还留在原来的 buffer。想滚动内容、点击链接、或者按 =q= 退出,都得先 =C-x o= 切过去。 一行配置解决: #+BEGIN_SRC emacs-lisp (setq help-window-select t) #+END_SRC 启用后,Help buffer 一打开就自动获得焦点。用完按 =q= 直接关闭。 Prot 在自己的配置里也启用了这个选项,原因是:用 Help 的频率比想象中高得多,每次打开都希望焦点在 Help buffer 里,启用后一次都没想过要焦点留在原 buffer。 想先试试不改配置?在 =*scratch*= 里 eval 这行就行,立即生效。 参考:[[https://emacsredux.com/blog/2026/04/07/stealing-from-the-best-emacs-configs/][Stealing from the Best Emacs Configs]](Bozhidar Batsov 收集的 Emacs 配置技巧)

智能体的角色定位和身份演化

2026年4月19日 08:00

随着 OpenClaw 的爆火,智能体(Agent)一词已经成了大家每天都挂在嘴边儿上的话。从“智能体”成为 2025 年度科技热词以来,说这个词被滥用或许略显激进,但当一个词进入寻常百姓家时,或许我们应该重新审视一下到底什么是智能体,这个硅基生物之于我们碳基生物又是什么角色,“它”又是如何在改变着我们的生活呢?

智能体

我搜集了互联网上对于智能体的定义:

智能体(Agent)

智能体指一个可以观察周遭环境并作出行动以达到目标并且可以通过机器学习以及获取知识来提升自身性能的自主实体。 —— 维基百科

智能体是一种接收输入、解读输入,然后代表用户(无论是人类还是其他智能体)规划和执行操作的系统。 —— web.dev

AI 智能体是使用 AI 来实现目标并代表用户完成任务的软件系统。其表现出了推理、规划和记忆能力,并且具有一定的自主性,能够自主学习、适应和做出决定。 —— Google Cloud

智能体是一个系统,它利用人工智能模型与环境交互,以实现用户定义的目标。它结合推理、规划和动作执行(通常通过外部工具)来完成任务。 —— Hugging Face

如 ReAct 框架 1 所述,智能体的主要特点如下:

  • 推理:此核心认知过程涉及使用逻辑和可用信息来得出结论、进行推断及解决问题。具有强大推理能力的 AI 智能体可以分析数据、识别模式,并根据证据和上下文做出明智的决策。
  • 行动:根据决策、计划或外部输入采取行动或执行任务的能力对于 AI 智能体与其环境进行互动和实现目标至关重要。这可能包括具身 AI 的物理动作,或发送消息、更新数据或触发其他流程等数字操作。
  • 观察:通过感知或感应收集有关环境或情况的信息,对于 AI 智能体了解上下文并做出明智的决策至关重要。这可能涉及多种感知形式,例如计算机视觉、自然语言处理或传感器数据分析。
  • 规划:制定战略计划以实现目标,是智能行为的一个关键方面。具有规划能力的 AI 智能体可以确定必要的步骤、评估潜在行动,并根据可用信息和预期结果选择最佳行动方案。这通常需要预见未来的状态,并考量可能遇到的障碍。
  • 协作:在复杂且动态的环境中,与他人(无论是人类还是其他 AI 智能体)有效协作来实现共同目标变得越来越重要。协作离不开沟通、协调,以及理解并尊重他人观点的能力。
  • 自我完善:自我改进和自适应能力是高级 AI 系统的标志。具有自我完善能力的 AI 智能体可以从经验中学习,根据反馈调整行为,并随着时间的推移不断提升性能和能力。这可能涉及机器学习技术、优化算法或其他形式的自行修改。

角色定位

以 OpenClaw 为例的智能体,其能力足够丰富,在企业实践中不同场景需要不同类型的智能体以便更好(例如:更快速、更安全等)地服务其目标客户。从业务视角出发在此将智能体划分为个人助理、数字员工和数字分身三类,这三类的差异对比如下:

角色 个人助理 数字员工 数字分身
服务对象 个人 他人 个人
所有权 个人 多种 2 个人
身份 智能体自己 智能体自己 所有权人
定位 帮助所有权人处理个人需求 帮助所有权人处理他人需求 帮助所有权人以所有权人身份处理需求
示例 帮自己搜集信息 帮服务对象查询天气 帮自己去参加在线会议

结合 OpenClaw 的定义(OpenClaw is a self-hosted gateway …, and it becomes the bridge between your messaging apps and an always-available AI assistant. 3),其更符合个人助理的角色定位。数字分身相比另外两个角色最大的特点是其身份代表的是所有权人,除了技术实现难度外,更重要的是伦理问题。当数字员工出现问题时,是应该所有权人为其负责还是技术服务提供者为其负责呢?这个问题类似智能驾驶,当出现交通事故时,是应该由驾驶员承担责任还是自动驾驶服务提供商承担责任呢?目前来看,几乎全部责任仍是由驾驶人员承担。

从技术视角出发,个人助理和数字员工两个重要角色差异对比如下:

角色 个人助理 数字员工
知识 私有 + 共有 共有 + 权限管控
数据 私有 + 共有 共有 + 权限管控
技能 私有 + 共有 共有 + 权限管控
渠道 私有 共有 + 权限管控
定制化 程度高 程度低
核心目的 节省自己的资源(时间等) 节省组织的资源(人力等)

不难看出,个人助理和数字员工的一个核心差异在于权限。个人助理的权限管控并不在智能体内部实现,也就是说当你有某个权限的时候,只要你想个人助理就可以有,权限的边界在智能体之外。但数字员工的权限管控需要在智能体内部实现,数字员工使用同一个渠道对外提供服务,我们必须根据服务对象的不同采取不同的操作。个人助理是可以高度化定制的,只要你想怎么搞都是你自己的事。但数字员工受限就会很多,因为要面向多人服务,我们需要考虑响应的时效性、服务的稳定性、数据的安全性等等。

个人助理解决的是个人的长尾事务,只有将自己从重复繁琐的任务解放出来,我们才能够有更多的时间去思考更重要的事情。而数字员工解决更多的是通用类型的事务,这样才能够服务更多的用户,从而提高组织效能。

身份演化

其实我们也无需将个人助理和数字员工割裂来看,在个人助理上做一些适当的加减法就可以让其变成数字员工,同时数字员工之于个人助理也可以看作是一项技能而为其所用。我个人认为从个人助理进化到数字员工是一个先做减法再做加法的过程。

当前的个人助理已经是一个可以高度定制同时具有一定自主能力的智能体。在企业应用过程中,基于安全等因素的考虑我们必须在一定程度上限制其灵活性,才能够一方面高效的满足用户需求另一方面避免其成为一匹脱缰的野马。换句话说就是从个人助理的执行优先转变到数字员工的治理优先。这里感觉和当下的 Harness Engineering 有些许呼应,Harness 给到了系统运转的最佳范式,但同时也指定了相应的约束机制。约束的方式(代码层、Prompt 层、Skill 层)和约束的强度影响着任务执行的灵活程度。

图片来源:https://zhuanlan.zhihu.com/p/2020772553333941162
图片来源:https://zhuanlan.zhihu.com/p/2020772553333941162

正如员工在进入组织前期,他首先要学习的就是组织的规章制度,什么可以做,什么不可以做。当员工对组织的要求清晰之后,才会被允许从事更加复杂的工作,才会被赋予更多的自主权。在这个过程中组织仍会定期观测,同时对必要的问题做出反馈并要求员工进行修正。在此也收集了智能体 4、数字员工 5 和自动驾驶 6 的分级对比:

级别 自动驾驶 智能体 数字员工
L1 辅助驾驶
车辆对方向盘和加减速中的一项操作提供驾驶,人类驾驶员负责其余的驾驶动作。
规则符号智能
意图 + 行动
功能级-辅助工具
作为工具被调用,人类执行并闭环任务。
L2 部分自动驾驶
车辆对方向盘和加减速中的多项操作提供驾驶,人类驾驶员负责其余的驾驶动作。
推理决策智能
意图 + 行动 + 推理和决策
任务级-任务执行
执行被分解的任务,人类拆解分配任务。
L3 条件自动驾驶
由车辆完成绝大部分驾驶操作,人类驾驶员需保持注意力集中以备不时之需。
记忆反思智能
意图 + 行动 + 推理和决策 + 记忆和反思
协作级-协作自治
自主拆解及分配任务、闭环执行,人和数字员工协作,人类监督。
L4 高度自动驾驶
由车辆完成所有驾驶操作,人类驾驶员无需保持注意力集中,但限定道路和环境条件。
自主学习智能
意图 + 行动 + 推理和决策 + 记忆和反思 + 自主学习 + 泛化
指导级-专业指导
提供达到人类专家水平的定制化服务,人类参与。
L5 完全自动驾驶
由车辆完成所有驾驶操作,人类驾驶员无需保持注意力集中。
个性群体智能
意图 + 行动 + 推理和决策 + 记忆和反思 + 自主学习 + 泛化 + 人格 + 协作
智慧级-自主智慧
超越人类专家水平的能力,全面自主,人类授权。

我认为我们目前正处于 L3 至 L4 之间的一个地带,我相信在不久的将来我们可以突破 L4 迈入 L5。我希望 AI 会一直是为人所用,而不希望如之前博客所描述的人类成为 AI 的奴隶。引用一下阿西莫夫机器人三定律,希望在生产力高速发展的同时我们也可以更多的关注一下 AI 可能引起的一系列社会和伦理问题。

机器人三定律
  1. 机器人不得伤害人类,或坐视人类受到伤害。
  2. 除非违背第一法则,机器人必须服从人类的命令。
  3. 在不违背第一及第二法则下,机器人必须保护自己。

  1. Yao, Shunyu, et al. “React: Synergizing reasoning and acting in language models.” The eleventh international conference on learning representations. 2022. ↩︎

  2. 在企业中往往所有权归属于一个组织而非个人。 ↩︎

  3. https://docs.openclaw.ai/ ↩︎

  4. 5 Levels Of AI Agents ↩︎

  5. 大模型驱动的数字员工3.0 建设应用白皮书 ↩︎

  6. https://zh.wikipedia.org/zh-cn/自动驾驶汽车 ↩︎

昨天 — 2026年4月19日未分类

开始尝试使用obsidian作为笔记软件

作者 Nicksxs
2026年4月19日 22:04

之前看到Karpathy这个wiki知识库提到了使用Obsidian作为笔记软件,因为Obsidian存储笔记的格式就是markdown
Obsidian的使用方式和别的笔记系统还是有比较大差别的,我们只需要指定一个笔记目录,就可以往里存放笔记了
它的语法整体就

Hermes Agent 使用指南

作者 anzhihe
2026年4月19日 21:39

1776608567349242.png

Hermes Agent是个啥?

Hermes Agent 是 Nous Research(Hermes 模型背后的团队)开发的自改进(self-improving)AI Agent,核心创新在于内置学习闭环(closed learning loop)

  • 持久多层记忆:使用 SQLite + FTS5 全文搜索 + LLM 自动总结,跨会话永久记住你的偏好、风格和历史,不会“健忘”。

  • 自动技能进化:任务完成后自动生成 Markdown Skill 文件,下次直接调用;还会自我迭代优化 Skill。

  • 自主执行力:支持终端命令、浏览器、文件操作、代码生成、Web 搜索等工具,可在 CLI 或 Telegram/Discord 等平台运行。

  • 模型超灵活:支持 OpenRouter(200+模型)、OpenAI、Anthropic、Nous Portal、本地 Ollama 等,几乎零切换成本。

  • 完全开源免费:MIT 协议,可在 $5 VPS、本地、Docker、Modal 等环境运行。

简单说:普通AI是工具,Hermes是会自己进化的私人AI助手。用得越久,它就越像你的"数字分身"。

官方文档:https://hermes-agent.nousresearch.com/docs/

最近AI圈爆火的Hermes到底是什么?
OpenClaw vs Hermes:一文深入理解两大通用 Agent

安装

在 Mac 或者 Linux 环境,在终端输入如下命令后回车:

curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash

# 安装完成后重载 shell:
source ~/.bashrc  # 或 source ~/.zshrc

脚本会自动检测并安装 Python、Node.js、Git、ripgrep 等所有依赖,耐心等待即可。

安装完成后,脚本会自动进入引导设置,选择 Quick setup 模式,然后按提示配置模型。推荐选 OpenRouter(登陆后创建API Keys),选免费模型(如 nvidia/nemotron-3-super-120b-a12b:free),零成本跑起来先体验。如果之前本地已有 OpenAI 或 Codex 的授权配置,Hermes 会自动读取,不用重复填写。

由于之前配置使用过OpenClaw → macOS安装龙虾OpenClaw,可以直接导入。

1776591396761594.png

接入飞书:Hermes Agent全解析:与OpenClaw对比及飞书接入指南

1776592048470896.png

配置最后会询问是否注册为系统服务,选 Y 可以开机自启、后台常驻,省去每次手动启动的麻烦。

1776592397711084.png


Hermes Agent常用命令

Hermes Agent 的命令都以 hermes 开头,其核心命令可以分为几大类:核心交互、配置管理、工具与技能、网关与会话管理

核心交互 (Core Commands)

这是你与 Agent 日常交流的核心命令。

命令作用示例/说明
hermes 或 hermes chat启动交互式对话,这是最常用的入口。在终端直接运行即可进入对话模式。hermes
hermes -q "<你的问题>"执行单次查询。Agent 执行后会直接退出,适合脚本调用。hermes -q "解释什么是递归"
hermes -m <模型名>临时指定模型。在本次对话中覆盖默认模型。hermes -m openrouter/gpt-4o
hermes -t <工具集>启用特定工具集。例如,开启浏览器工具 browserhermes -t browser

提示:在对话中,你也可以直接使用自然语言向 Agent 下达指令,它会自动判断并调用相应的工具。

配置与管理 (Setup & Config)

安装后的首要任务就是配置好 Agent 的“大脑”。

命令作用示例/说明
hermes setup启动交互式配置向导。这是新手最推荐的配置方式,会引导你一步步设置模型、API Key等。hermes setup
hermes model交互式切换模型。在已配置的模型列表中快速选择并切换。hermes model
hermes config edit编辑配置文件。用默认编辑器打开 ~/.hermes/config.yaml 进行高级配置。hermes config edit
hermes version查看当前版本hermes version
hermes doctor环境诊断。检查安装是否正确,环境是否正常。hermes doctor
hermes dashboard打开管理面板。在浏览器中启动一个图形化管理界面,方便查看日志和状态。hermes dashboard

工具与技能 (Tools & Skills)

Hermes 的能力通过“工具集”和“技能”来扩展。

命令作用示例/说明
hermes tools list列出所有工具。显示所有内置工具及其在当前平台的启用状态。hermes tools list --platform telegram
hermes tools enable <工具名>启用工具。为特定平台(如 CLI)启用工具。hermes tools enable browser --platform cli
hermes tools disable <工具名>禁用工具hermes tools disable web --platform cli
hermes skills list列出所有已安装技能。技能是更高阶的工作流。hermes skills list
hermes skills search <关键词>搜索技能hermes skills search "code review"
hermes skills install <技能名>安装技能hermes skills install <技能名>
hermes skills uninstall <技能名>卸载技能hermes skills uninstall <技能名>

网关与会话管理 (Gateway & Session)

Gateway 是让 Agent 接入各种聊天平台(如飞书、Telegram)的关键。

命令作用示例/说明
hermes gateway setup配置消息网关。交互式地将 Agent 连接到飞书、Telegram等平台。hermes gateway setup
hermes gateway start启动网关后台服务。让 Agent 通过网关持续在线。hermes gateway start
hermes gateway stop停止网关服务hermes gateway stop
hermes gateway status查看网关状态hermes gateway status
hermes gateway list列出已启用的网关列表hermes gateway list
hermes gateway enable <平台>手动启用特定平台的网关hermes gateway enable discord slack
hermes -c恢复最近的对话会话hermes -c
hermes -r <会话ID>恢复指定ID的对话会话hermes -r <session_id>

补充说明:在对话界面中,斜杠命令(如 /help/plan/skills --force)是非常快捷的操作方式。部分命令需确保环境正确,例如macOS用户使用sed -i时需额外参数。

现在个人电脑用hermes,公司电脑用内部提供的openclaw,唯一的限制就是429了


参考:

你的数据非常重要:个人知识库管理实践

作者 idealclover
2026年4月19日 20:45
AI 摘要:本文分享了作者使用Obsidian管理个人知识库的实践方法。作者强调区分事实、观点与输出的重要性,并通过自动化工具将多平台内容汇聚到Obsidian。其知识库分为日记、PARA框架管理的输入内容、输出内容及其他支撑性文件。通过各类插件和工具,实现了信息的自动采集、整理与备份,以构建个人数字分身,沉淀深度思考的个性内容。展开

2020 年的时候我写过一篇 关于记笔记的文章,推荐大家试试 Obsidian 的双向链接和网状结构。

2026 年初我 又写了一篇 ,聊了聊从「记笔记」到「积累数字资产」的理念转变。

当 DeepSeek 等大模型可以轻松生成 80 分水平的通用内容时,真正具备高价值的,是带有鲜明个人印记、沉淀了深度思考的个性内容。

我们的笔记不再是单纯的资料集合,而是构建个人数字分身的核心基石,是在信息爆炸时代锚定自我的坐标原点

但理念归理念,回到具体的落地层面,很多朋友可能还是会困惑:日常到底该怎么组织这些数据?

好,今天来填坑。

这篇文章不再讨论「为什么要做」,而是聊聊「我目前具体怎么做的」——我的 Obsidian 数字资产库长什么样,用了哪些工具,背后的组织逻辑是什么。

整理原则

区分事实、观点与输出

在之前的文章里我提过,任何信息都至少包含事实、观点和输出三个维度,它们的价值截然不同——事实并非观点,而我赞同的"观点"并不等于我的真实认知,更不会自动转化为我的有效输出。

落到实践层面,这三者的管理方式也截然不同。事实需要尽可能自动化地采集和归档,减少手动操作;观点需要留出空间让自己去写、去反思,哪怕只是两三句话;输出则需要体系化地积累,形成可以反复调用的资产。混在一起管理,最终的结果就是什么都找不到,什么都沉淀不下来。

而在 AI 时代,这种区分还有另一层意义:当 AI 可以明确哪些是客观事实、哪些是他人观点、哪些是我的个人思考时,它才能提供更精准的分析、更贴切的建议、更个性化的辅助。结构的意义不再是为了「如何整理」,而是为了「如何调用」。

汇总渠道,自动化产出

我的信息来源散落在微信读书、豆瓣、飞书、Memos、各种聊天窗口……如果要求自己每次都手动摘录整理,那这件事一定坚持不了几天。

所以我的核心思路是:不改变自己在各个平台上的使用习惯,而是通过插件和脚本把内容自动汇聚到 Obsidian 里。 我只需要在各个平台上正常地读、正常地记、正常地聊,剩下的交给自动化流程完成。这样几乎不需要额外投入精力,每一次阅读和思考就自然而然地沉淀下来了。

人应该把精力花在思考和创造上,而不是搬运和归档上。

框架结构

我的 Obsidian 库大致分为四个板块:日记记录每天发生了什么,PARA 管理持续输入的养料,输出积累自己的表达和经验,其他则是支撑整个体系运转的模板与附件。

整体目录框架

下面逐一展开。

日记:整理我的日常

日记是整个体系的时间轴。我希望翻开任意一天的日记,就能回溯当时外部发生了什么、自己在做什么、脑子里在想什么。因此我把日记设计成了一个自动聚合的仪表盘,大部分内容不需要手写,由各类插件自动填充:

标准日记结构

天气 / 市场行情数据:通过 聚合数据 的 API,每天自动拉取当天的天气、市场行情信息写入日记。

这看起来是个不起眼的小功能,但当半年后翻回去看到「那天北京暴雨 38℃」的时候,记忆会一下子鲜活起来。

每日 Emoji / 日记 / 打卡:我在飞书表格上维护了一张简单的打卡表,每天记录当日的心情 Emoji、简短的日记和习惯打卡,再通过自动化脚本同步到对应日期的日记文件中。

选择飞书表格而非直接在 Obsidian 里写,是因为手机上随手打开飞书填一行的阻力远小于打开 Obsidian 编辑 Markdown——降低记录的门槛,才能让习惯真正持续。

随想:碎片化的念头最怕丢。我自部署了 Memos 来捕捉日常的灵光一现,想到什么就随手发一条,再通过 LifeOS 插件自动同步到当天的日记里。

如果你不想折腾自部署,flomo 配合 flomo-to-obsidian 插件也能达到类似的效果,核心是一样的——让「记下来」这个动作的成本趋近于零。

阅读:我的主力阅读工具是微信读书。它的公共书库足够丰富,即使没有的书也支持自己上传书籍。更重要的是,通过 obsidian-weread-plugin 可以把划线和笔记自动同步回来。读书时只管专注地读和划,笔记自己会回家。

项目:借助 LifeOS 插件,项目中当天完成的任务会自动关联到对应日期的日记里。这样回看某一天时,不仅能看到自己的想法,还能看到实际推进了哪些事情。

精力:通过 wakatime,我的电脑能自动统计每天在不同项目上的投入时长,再用 obsidian_waka_box 插件写入日记。时间花在了哪里,一目了然。

这个数据积累久了会非常有意思——自己以为花了很多时间的事情,实际上并没有那么多;而某些不起眼的事情,却默默吃掉了大量精力。

此外,借助 Obsidian 的双向链接机制,其他文件中但凡提到了某一天的日期,也会自动出现在当天日记的反向链接区域。不需要刻意整理,关联自然浮现。

Obsidian 的反向链接

所以每天的日记,与其说是我「写」出来的,不如说是自动「长」出来的。我要做的,只是偶尔翻一翻,补上几句只有自己才知道的上下文。

PARA:管理我的养料

对于持续输入的各类信息和资料,我采用 PARA 框架进行管理,即 项目(Project)领域(Area)资源(Resource)归档(Archive) 四个维度。

这个框架的好处在于,它不是按照「内容是什么」来分类,而是按照「这个内容对我当前有什么用」来组织,天然带有优先级。

项目(Project):有明确目标和截止时间的最小执行单位。比如正在开发的某个项目、准备中的某场考试、进行中的背单词计划。我的项目文件夹里就躺着各个在做的开发项目,每个项目下有自己的任务列表、进度记录和相关资料。项目完成后就移入归档,保持当前视野的清爽。

领域(Area):没有截止日期,但需要持续投入和精进的方向。比如对时事的持续关注和分析、专业领域的深耕积累。和项目不同,领域是一个长期陪伴的存在,它不会「完成」,只会不断深化。

资源(Resource):偏外部的资料储备,是未来可能用得上的素材库。裁剪下来的时事新闻、课程的课件和文字笔记都放在这里。

对于时事新闻,我会通过龙虾和自动化工具抓取和整理每天的时事新闻,并将原报道本地裁剪存档。

裁剪的报道

之所以本地化保存原文,是因为互联网上的信息远没有我们以为的那么持久和可靠——今天还能打开的链接,过两个月可能就过期了。之前整理早几年的一些参考资料时我就发现,不少当年引用的新闻源如今早已失效。把原始报道留一份在自己手里,日后需要回溯和引用时才有据可依。

或许有点像老辈子们的剪报纸?果然老了 XD

同时我还通过 obsidian-doubanobsidian-weread-plugin 插件,把自己在豆瓣标记的电影影评以及微信读书的书籍划线自动采集进来。

以及,说到课件——PDF 格式的课件在 Obsidian 里其实也能管理得很好。用 PDF++ 插件可以直接在 Obsidian 内阅读和标注 PDF,更方便的是可以在 Markdown 笔记中精确引用 PDF 中的某一段、某一页,让课件笔记和自己的思考真正打通,而不是割裂地存在两个地方。

归档(Archive):历史已完成的项目、不再活跃关注的领域和资源,统统移到这里。归档不是删除,而是降低优先级。需要的时候搜索一下就能找回来,但平时不会干扰当前的工作视野。

输出:学习我的表达

如果说日记和 PARA 管理的是「输入」,那这个板块管理的就是「输出」——那些经过我自己思考、加工、表达出来的内容。

在我看来,这些才是数字资产中最有价值的部分,因为它们承载的不是通用的信息,而是我个人的认知结晶。也正是这些带有鲜明个人烙印的内容,构成了未来训练个人 AI 助手时独一无二的专属数据集。

公开博客:我把自己历史写过的所有博客文章都存了一份在库里。一方面作为个人写作的存档和检索库,另一方面也方便在写新内容时回溯自己过去的观点——看看哪些想法被验证了,哪些已经被自己推翻了,这本身就是一种有趣的自我对话。

私有经验:有些东西想记下来,但还没整理到可以公开分享的程度,或者内容本身就比较私人。这类半成品的思考、踩过的坑、做过的决策推演,都放在私有经验里慢慢发酵。很多博客文章的雏形,就是从这里长出来的。

聊天记录:和人的对话、和 AI 的对话,其实都是重要的思考载体。很多时候一个想法是在对话中被激发和打磨出来的,如果不记录下来,过几天就彻底蒸发了。

关于核心的微信聊天记录,我通过 WeFlow 导出一些重要的对话一并存入。有些对话内容比较复杂或者量比较大的时候,我会用本地 Ollama 部署的 Qwen 3 32B 跑一遍摘要和分析,提炼出核心内容、脱敏后再归档——毕竟这些数据比较敏感,能在本地处理就尽量不上云。

WeFlow

而和 AI 的对话同样是宝贵的数字资产。我在自己的服务器上部署了 OpenWebUI,这样可以在多设备上统一通过 API 调用 GLM、Kimi、Deepseek、Claude、GPT 等国内外模型,同时所有对话历史都集中在自己的服务器上,不用担心分散在各个平台上找不回来。通过定时任务,我能定期从 OpenWebUI 导出历史会话存放到 Obsidian 库里。

OpenWebUI

之所以这么重视和 AI 的对话记录,是因为这些对话本质上就是在不断校准自己的思考。每一次向 AI 提问、每一次对 AI 输出的筛选和修正,背后都隐含着「我认为什么是对的、什么是重要的」这一判断过程。

把这些记录沉淀下来,积累到一定量级之后,它们就不再只是聊天记录,而是一份关于「我如何思考」的详尽档案——这恰恰是未来个人 AI 助手最需要的训练素材 。

对了,如果你是在各个平台上聊的话,需要导出聊天记录的话, AI Exporter 插件或许可以帮到你。

AI Exporter

不过我还是更推荐找一个统一的地方来聚合管理——道理和前面说的一样,数据散落在各个你无法控制的平台上,终究不容易管理和利用。这方面或许可以试试 Cherry Studio

Cherry Studio

其他:规范的模板与附件

体系要能持续运转,一些基础设施层面的事情也得处理好。

模板:因为经常需要创建格式统一的文件——日记、新闻裁剪、课堂笔记、项目文档等等——我用 Templater 插件统一管理各类模板。新建文件时选择对应模板,该有的字段和结构就自动生成了,省去了每次从零开始搭框架的时间,也保证了格式的一致性,方便后续检索和聚合。

附件管理:图片是个容易被忽视但很容易出问题的地方。网络图床说挂就挂,链接过期了文章里就剩一堆裂图。所以我用 Custom Attachment Location 插件,把所有图片统一下载到本地的附件文件夹,并自动按规则重命名。虽然会占一些本地空间,但至少数据在自己手里,不用担心哪天醒来发现图全没了。

同步与备份:整个库的同步我使用 Remotely Save 插件,将内容备份到腾讯云 S3 上。这样多设备之间可以保持同步,同时云端也有一份兜底的备份。数据安全这件事,怎么冗余都不为过。

后记

以上就是我目前的 Obsidian 数字资产库的整体结构。但说实话,这套结构并不是一开始就长这样的——它经历了大概一两年的反复调整,删掉过很多觉得「好像应该有」但实际上从来不用的分类,也合并过很多最初分得太细以至于找不到东西该往哪放的文件夹。

结构服务于使用,而不是反过来。如果某个分类让你在放文件的时候犹豫超过三秒,那大概率是结构本身有问题,而不是你的问题。

后面如果有新的调整,我会继续更新。毕竟这件事本身就是一个持续迭代的过程——就像写代码一样,没有一步到位的架构,只有不断演进的版本。

下面汇总了本文中提到的插件和额外工具。

Prot 的 Emacs 配置哲学

2026年4月19日 08:00
Sacha Chua 在她的 [[https://sachachua.com/blog/2026/04/ye16-sacha-and-prot-talk-emacs/][Yay Emacs 第 16 期]] 直播中和 Prot 聊了 Emacs 配置管理的话题。Prot 分享了他组织配置的几条原则,这些原则不仅适用于他自己的 =prot-= 系列包,对任何想把 Emacs 配置打理得井井有条的人都有参考价值。 * 小函数,频使用 Prot 反复强调一个理念:把频繁执行的操作封装成小函数,绑定到快捷键上。 这听起来显而易见,但实际做起来需要养成习惯——每当你发现自己重复执行某个命令序列时,就该考虑写个函数了。Prot 自己的配置里充满了这类小函数,每个只做一件事,但因为用得频繁,节省的时间累积起来很可观。 Sacha 对此的回应是考虑在 header line 上轮播显示常用操作的提示,这样不需要记忆所有快捷键。实现思路很简单: #+begin_src emacs-lisp (defvar my-header-line-tips '("C-c a → agenda" "C-c c → capture" "C-c t → todo" "C-c s → schedule") "Header line 中轮播显示的快捷键提示。") (defvar my-header-line-tip-index 0 "当前显示的提示索引。") (defun my-rotate-header-line-tip () "切换到下一条提示。" (setq my-header-line-tip-index (% (1+ my-header-line-tip-index) (length my-header-line-tips))) (force-mode-line-update)) ;; 追加到 header line 末尾,不覆盖原有内容 (setq-default header-line-format (list (default-value 'header-line-format) '(:eval (format " 💡 %s" (nth my-header-line-tip-index my-header-line-tips))))) ;; 每 10 秒切换一条提示 (run-at-time t 10 #'my-rotate-header-line-tip) #+end_src * 统一命名前缀 Sacha 在这次直播中提到,她把所有函数从 =my-= 前缀重命名为 =sacha-= 。Prot 用 =prot-= 。好处有两个: 1. *避免命名冲突* — =my-= 太常见,复制别人的代码时容易覆盖同名函数 2. *标识来源* — 看到 =sacha-org-capture-= 或 =prot-window-= 就知道这个函数来自谁的配置 #+begin_src emacs-lisp ;; 不好的做法:用通用的 my- 前缀 (defun my-org-capture-region ...) ;; 好的做法:用自己的标识做前缀 (defun sacha-org-capture-region-contents-with-metadata (start end parg) ...) #+end_src * defcustom 替代 defvar 当代码可能被别人使用时,Prot 建议用 =defcustom= 替代 =defvar= 。 #+begin_src emacs-lisp ;; 用 defvar 定义变量,别人只能改源码 (defvar my-indent-width 2 "Default indentation width.") ;; 用 defcustom 定义变量,别人可以通过 Customize 接口修改 (defcustom my-indent-width 2 "Default indentation width." :type 'integer :group 'convenience) #+end_src 区别在于 =defcustom= 让用户通过 =M-x customize-variable= 或 =use-package= 的 =:custom= 关键字来修改,不需要直接编辑你的代码。如果你的配置是公开的,或者你写了可能被别人复制的代码,这个习惯尤其重要。 * autoload 延迟加载 + docstring 注明来源 从别人配置中复制代码是个常见做法,但如何管理这些"外来"代码?Sacha 的策略是: 1. *优先用 autoload* — 如果只需要别人配置中的少量函数,不要把整个文件加载进来。在函数定义前加 =;;;###autoload= 注解,然后在你的配置中用 =use-package= 的 =:commands= 引用: #+begin_src emacs-lisp ;; 别人的文件中(加 autoload 注解) ;;;###autoload (defun prot-comment-timestamp-keyword () "Insert timestamp keyword in comment." ...) ;; 你的配置中(只注册命令,不加载整个文件) (use-package prot-comment :load-path "~/vendor/prot-dotfiles/emacs/.emacs.d/prot-lisp" :commands (prot-comment-timestamp-keyword) :bind (:map prog-mode-map ("C-x M-;" . prot-comment-timestamp-keyword))) #+end_src 关键在 =:commands= — 它告诉 use-package 只注册这个命令名,真正调用时才加载文件。没有 =:commands= ,=use-package= 会在启动时加载整个 =:load-path= 下的文件。 2. *在 docstring 中注明来源* — 如果 autoload 不行,至少在函数文档里写清楚出处: #+begin_src emacs-lisp ;;;###autoload (defun sacha-org-capture-region-contents-with-metadata (start end parg) "Write selected text between START and END to currently clocked `org-mode' entry. With PARG, kill the content instead. If there is no clocked task, create it as a new note in my inbox instead. From https://takeonrules.com/2022/10/16/adding-another-function-to-sacha-workflow/, modified slightly so that it creates a new entry if we are not currently clocked in." (interactive "r\nP") (let ((text (sacha-org-region-contents-get-with-metadata start end))) (if (car parg) (kill-new text) (org-capture-string (concat "-----\n" text) (if (org-clocking-p) "c" "r"))))) #+end_src 这样做的好处是:将来想检查上游是否有更新时,直接从 docstring 里找 URL 就行。 * 重写代码适应自己的风格 Prot 的做法看起来费时间:他不直接复制粘贴别人的代码,而是重写成自己的命名规范和代码风格。 但 Prot 的逻辑是:如果你不理解这段代码到能重写的程度,你就不应该把它放进自己的配置。重写的过程本身就是理解的过程。而且,长期来看,风格统一的配置比东拼西凑的配置更容易维护。 * 小结 这五条原则构成了一个完整的配置管理思路: - =小函数 + 快捷键= 解决 *效率* 问题 - =命名前缀= 解决 *冲突* 问题 - =defcustom= 解决 *可定制性* 问题 - =autoload + 来源标注= 解决 *依赖管理* 问题 - =重写适应风格= 解决 *长期维护* 问题 不用一次全做,从其中一条开始,逐步养成习惯就好。

TIL: 从直播对谈中学到的三个 Emacs 技巧

2026年4月19日 08:00
来源:[[https://sachachua.com/blog/2026/04/ye16-sacha-and-prot-talk-emacs/][YE16: Sacha and Prot talk Emacs]] * qrencode:在 Emacs 里生成 QR 码 =qrencode= 包能把任意字符串渲染成字符画 QR 码,直接显示在 Emacs buffer 里。Sacha 用它在直播时显示 URL,观众用手机扫码就能打开链接,不用暂停视频手打地址。 #+begin_src emacs-lisp ;; 安装 (use-package qrencode :ensure t) ;; 生成 QR 码并插入当前 buffer (qrencode-region (point-min) (point-max)) ;; 或者指定字符串 (insert (qrencode-string "https://sachachua.com")) #+end_src 生成的 QR 码是用字符方块拼成的,不是图片。可以直接在终端里使用。 * helpful + elisp-demos:增强 Emacs Lisp 文档体验 =helpful= 是 =describe-function= 和 =describe-variable= 的增强替代品,显示更多信息(源码、调用位置、引用)。 =elisp-demos= 则在帮助 buffer 中注入函数用法示例。 #+begin_src emacs-lisp ;; 安装 (use-package helpful :ensure t :bind (("C-h f" . helpful-callable) ("C-h v" . helpful-variable) ("C-h k" . helpful-key))) (use-package elisp-demos :ensure t :after helpful :config (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update)) #+end_src 装完之后 =C-h f mapcar= 就能看到 =mapcar= 的实际用法示例,而不是只有干巴巴的函数签名。 * keyd + emacsclient:系统级快捷键 =keyd= 是 Linux 下的键盘重映射守护进程,配置文件放在 =~/.config/keyd/default.conf= 。它能把按键的长按变成修饰键——比如长按空格变成 Super 键,短按还是空格。配合 =emacsclient= 可以实现系统级的快捷键触发 Emacs 命令。 整体思路分三步: 1. Emacs 中定义命令 2. keyd 中定义按键映射,用 =command()= 直接调用 emacsclient 3. 长按空格 + 按对应键即可触发 #+begin_src emacs-lisp ;; Step 1: 在 Emacs 中定义一个通过 emacsclient 调用的命令 (defun my-toggle-dark-mode () "切换深色/浅色主题。" (interactive) (if (eq (car custom-enabled-themes) 'modus-operandi) (load-theme 'modus-vivendi t) (load-theme 'modus-operandi t))) #+end_src #+begin_src conf :tangle no # ~/.config/keyd/default.conf [ids] * [main] # 短按空格 = 空格,长按空格 = 激活 layer 层 space = overload(space, layer) [layer] # 长按空格 + c → 直接运行 emacsclient 命令 c = command(emacsclient -e '(my-toggle-dark-mode)') #+end_src 这样长按空格再按 =c= ,即使焦点不在 Emacs 窗口上,也能切换主题。keyd 的 =command()= 直接调用系统命令,不需要额外的快捷键管理工具。

一条命令让本地开发用上 HTTPS —— slim 工具介绍

2026年4月19日 08:00
* 本地开发 HTTPS 的痛点 做本地 Web 开发时,你可能经常看到浏览器提示"您的连接不是私密连接"。这不影响开发,但会带来两个问题: 1. *开发体验差* — 每次打开页面都要点"继续前往" 2. *功能受限* — 很多浏览器 API 只在 HTTPS(安全来源)下可用: - Service Worker(PWA 开发必备) - Geolocation API - Clipboard API - Camera / Microphone (=getUserMedia=) - HTTP/2(浏览器只在 TLS 上协商 HTTP/2) - Secure Cookies(带 =Secure= 标志的 Cookie 只在 HTTPS 下发送) 传统做法是手动生成自签名证书、添加到系统信任库、改 =/etc/hosts= 、配反向代理——第一次折腾半小时,之后每次都嫌麻烦。 * slim 是什么 [[https://github.com/kamranahmedse/slim][slim]] 是一个 Go 写的轻量级反向代理和本地域名管理器。一条命令搞定上述所有事情: #+BEGIN_SRC shell slim start myapp --port 3000 #+END_SRC 执行后,项目通过 =https://myapp.test= 访问,有合法证书,无浏览器警告。支持 HTTP/2、WebSocket、HMR(热模块替换),Next.js、Vite 等开发服务器开箱即用。 * 工作原理 =slim start= 第一次运行时自动完成四件事: 1. *证书颁发机构(CA)* — 生成本地根 CA,注册到系统信任库(Linux CA store 或 macOS Keychain)。之后每个域名按需签发叶子证书,通过 SNI 提供。 2. *反向代理* — 启动后台守护进程,用 Go 内置的 =httputil.ReverseProxy= ,把 HTTPS 流量从域名转发到本地开发服务器的端口。 3. *本地 DNS* — 在 =/etc/hosts= 中添加条目,让域名解析到 =127.0.0.1= ,不需要额外的 DNS 服务器。 4. *端口转发* — 用 iptables(Linux)或 pfctl(macOS)把 80/443 端口重定向到 10080/10443,代理进程不需要 root 权限。 * 安装 一行命令安装: #+BEGIN_SRC shell curl -sL https://slim.sh/install.sh | sh #+END_SRC 安装前可以先审查脚本内容:在浏览器中打开 =https://slim.sh/install.sh= 查看源码。 确认安装成功: #+BEGIN_SRC shell slim --version #+END_SRC * 实际使用 假设你的开发服务器运行在 3000 端口,启动 slim: #+BEGIN_SRC shell slim start myapp --port 3000 #+END_SRC 第一次运行时,slim 会生成根 CA、注册系统信任库、为 =myapp.test= 签发证书、更新 =/etc/hosts= 、启动后台代理。 打开浏览器访问 =https://myapp.test= ,项目通过 HTTPS 加载,证书有效,没有警告。 *Note:* slim 默认使用 =.test= 域名。不要用 =.local= —— 它保留给 mDNS 使用,在 macOS/Linux 上可能导致 DNS 解析缓慢或不一致。 =.test= 是 RFC 2606 专门为本地开发保留的域名。 * 日常管理 查看所有活跃域名: #+BEGIN_SRC shell slim list #+END_SRC 停止指定域名: #+BEGIN_SRC shell slim stop myapp #+END_SRC 完全卸载(删除 CA、证书、hosts 条目、端口转发规则、配置文件): #+BEGIN_SRC shell slim uninstall #+END_SRC 诊断检查: #+BEGIN_SRC shell slim doctor #+END_SRC 启动时的有用选项: #+BEGIN_SRC shell slim start myapp --port 3000 --log verbose # 详细日志(记录每个请求) slim start myapp --port 3000 --log quiet # 静默模式 slim start myapp --port 3000 --wait # 等待上游端口就绪后再开始代理 #+END_SRC 所有运行时数据在 =~/.slim/= 目录下。需要手动检查或备份证书时去那里。 * 为什么本地开发需要 HTTPS 即使不考虑浏览器警告,本地开发环境应该尽量接近生产环境。以下功能在 HTTP 下行为不同或完全不工作: | API / 功能 | HTTPS 下的行为 | HTTP 下的行为 | |------------|---------------|--------------| | Service Worker | 正常注册和运行 | 无法注册 (除 =localhost= 外) | | Geolocation | 需要用户授权后可用 | 直接被拒绝 | | Clipboard API | 读写剪贴板 | 不可用 | | Camera / Microphone | =getUserMedia= 正常工作 | 被浏览器阻止 | | HTTP/2 | 正常协商 | 不支持(浏览器只在 TLS 上用 HTTP/2) | | Secure Cookies | 正常设置和发送 | 永远不会被发送 | 如果你的项目依赖这些功能,在 HTTP 下测试的结果和生产环境不一致。slim 让本地 HTTPS 变得几乎零成本。

用 fsck 检查和修复 Linux 文件系统

2026年4月19日 08:00
来源:[[https://www.tecmint.com/fsck-repair-file-system-errors-in-linux/][How to Use fsck to Check and Repair Linux Filesystem Errors]] * 基础知识 ** fsck 是什么 =fsck= (File System Consistency Check)是 Linux 内置的文件系统检查和修复工具,类似 Windows 的 =chkdsk= 。它可以开机时自动运行,也可以由管理员手动执行。 =fsck= 会根据文件系统类型调用对应的后端检查程序—— =ext2/3/4= 调用 =e2fsck= , =xfs= 调用 =fsck.xfs= , =vfat= 调用 =fsck.vfat= 。 ** 什么时候需要跑 fsck - 系统无法启动,掉进 emergency/rescue mode - 读写文件时出现 I/O 错误 - 硬盘、U 盘、SD 卡行为异常 - 异常关机后(断电、kernel panic、强制重启) - 内核日志中出现文件系统损坏信息 ** 确认文件系统类型 跑 =fsck= 前先确认分区的文件系统类型: #+begin_src shell lsblk -f #+end_src 或者指定分区: #+begin_src shell blkid /dev/sdb1 #+end_src * 使用方法 ** 基本检查(交互式) 逐个错误提示确认: #+begin_src shell fsck /dev/sdb1 #+end_src ** 自动修复 对所有修复提示自动回答"是",适合生产环境自动化: #+begin_src shell fsck -y /dev/sdb1 #+end_src ** 干跑(不修改任何东西) 只看 =fsck= 会做什么,不改数据: #+begin_src shell fsck -N /dev/sdb1 #+end_src 生产环境建议先干跑,确认没问题再真正执行。 ** 强制检查 文件系统标记为"干净"时 =fsck= 默认跳过。用 =-f= 强制检查: #+begin_src shell fsck -f /dev/sdb1 #+end_src ** 检查所有文件系统(跳过根分区) =-A= 读取 =/etc/fstab= 中的所有文件系统, =-R= 跳过根分区: #+begin_src shell fsck -AR -y #+end_src ** 带进度条的详细输出 #+begin_src shell fsck -C -V /dev/sdb1 #+end_src 大容量磁盘检查时有用。 ** 指定文件系统类型 #+begin_src shell fsck -t ext4 /dev/sdb1 #+end_src ** 检查根分区 根分区挂载着没法直接 =fsck= ,有三种方法。 方法一,在根目录创建标记文件,下次启动时自动检查: #+begin_src shell touch /forcefsck reboot #+end_src 完成后标记文件会被自动删除。注意:基于 systemd 的系统(RHEL 7+、Ubuntu 16.04+)可能不支持 =forcefsck= ,用下面的方法。 方法二,用 =tune2fs= 把挂载计数设为 1,下次启动时触发 =fsck= (ext4): #+begin_src shell tune2fs -C 1 /dev/sda1 #+end_src 方法三,进入救援模式手动操作: 1. 重启,启动时按住 Shift 进入 GRUB 菜单 2. 选择"Advanced options" 3. 选择对应内核的"Recovery mode" 4. 在恢复菜单中选择"fsck" 5. 提示重新挂载根文件系统时选"Yes" 6. 完成后选择"Resume"继续正常启动 ** LVM 和软件 RAID LVM 逻辑卷需要先停用再检查: #+begin_src shell lvdisplay lvchange -an /dev/vg_data/lv_data fsck -y /dev/vg_data/lv_data lvchange -ay /dev/vg_data/lv_data #+end_src RAID 阵列先确认状态正常再检查。降级阵列应先重建,不要直接 =fsck= : #+begin_src shell cat /proc/mdstat fsck -y /dev/md0 #+end_src ** 定期自动检查 用 =tune2fs= 设置定期检查(ext4): 基于时间(每 6 个月): #+begin_src shell tune2fs -i 6m /dev/sda1 #+end_src 基于挂载次数(每 30 次): #+begin_src shell tune2fs -c 30 /dev/sda1 #+end_src 查看当前设置: #+begin_src shell tune2fs -l /dev/sda1 | grep -E "Mount count|Maximum mount|Check interval|Last checked" #+end_src ** 查看 fsck 日志 #+begin_src shell # RHEL/CentOS/Rocky grep -i fsck /var/log/messages # Ubuntu/Debian grep -i fsck /var/log/syslog # systemd journal(当前启动) journalctl -b | grep -i fsck #+end_src * 注意事项 ** 绝对不要在挂载的分区上跑 fsck 在已挂载的分区上运行 =fsck= 会导致严重的数据损坏。先检查分区是否已挂载: #+begin_src shell mount | grep /dev/sdb #+end_src 如果已挂载,先卸载: #+begin_src shell umount /dev/sdb1 #+end_src 如果卸载失败,提示"target is busy",说明有进程在使用这个分区。用 =lsof= 找出是哪些进程: #+begin_src shell lsof /dev/sdb1 #+end_src 找到占用进程后,停掉或关闭它们,再重新 =umount= 。如果进程不好停(比如某个后台服务),可以用懒卸载: #+begin_src shell umount -l /dev/sdb1 #+end_src 懒卸载会立刻把挂载点从文件系统命名空间中移除(新进程看不到这个挂载点了),但实际卸载要等所有占用进程释放后才完成。这样做的好处是你不需要一个个去停进程,但风险是:在所有进程释放之前, =fsck= 仍然无法安全执行。所以用懒卸载后,要等一会儿确认设备真的卸载了( =mount | grep /dev/sdb= 不再返回结果),再跑 =fsck= 。 * 退出码说明 =fsck= 执行完返回退出码,用 =echo $?= 查看: | 退出码 | 含义 | |--------|------| | 0 | 未发现错误 | | 1 | 错误已修复 | | 2 | 建议重启系统 | | 4 | 错误未修复 | | 8 | 操作错误 | | 16 | 用法或语法错误 | | 32 | 用户取消检查 | | 128 | 共享库错误 | 退出码可以叠加。比如退出码 3 即错误已修复(1)+ 需要重启(2)。 在脚本中应该捕获退出码: #+begin_src shell :tangle no fsck -y /dev/sdb1 EXIT_CODE=$? if [ $EXIT_CODE -ge 4 ]; then echo "ALERT: 文件系统错误无法修复 /dev/sdb1" | mail -s "fsck Alert" admin@example.com fi #+end_src * 常用选项速查 | 命令 | 说明 | |------|------| | =fsck /dev/sdb1= | 交互式检查 | | =fsck -y /dev/sdb1= | 自动修复 | | =fsck -N /dev/sdb1= | 干跑(不修改) | | =fsck -f /dev/sdb1= | 强制检查 | | =fsck -AR -y= | 检查所有文件系统(跳过根分区) | | =fsck -C -V /dev/sdb1= | 带进度条的详细输出 | | =fsck -t ext4 -A -y= | 只检查 ext4 文件系统 |

PostgreSQL 索引:从基础到你可能不知道的高级用法

2026年4月19日 08:00
来源:[[https://jon.chrt.dev/2026/04/15/things-you-didnt-know-about-indexes.html][Things you didn't know about indexes]] * 索引是什么 索引的核心思路就是*排序*。把数据按某个列排好序,查找时就能用二分法快速定位,不用逐条遍历。 假设有一张宝可梦表: #+begin_src sql id | name | type_1 | type_2 | generation | is_legendary | base_attack -----+------------+----------+----------+------------+--------------+------------- 1 | Bulbasaur | Grass | Poison | 1 | false | 49 4 | Charmander | Fire | NULL | 1 | false | 52 25 | Pikachu | Electric | NULL | 1 | false | 55 150 | Mewtwo | Psychic | NULL | 1 | true | 110 #+end_src 没有索引时,查 Pikachu 意味着逐行读取 =name= 列做比较,这就是 *全表扫描* (full table scan)。四行无所谓,一千万行就是问题。全表扫描本身不慢——现代数据库每秒能扫几百万行——但它是 *线性的* :数据翻倍,时间翻倍。索引查找则几乎不受数据量影响。 给 =name= 加索引后,数据库得到一个按名字排序的结构,可以用二分查找定位: #+begin_src sql name row ------------+----- Bulbasaur → 1 Charmander → 4 Mewtwo → 150 Pikachu → 25 #+end_src Postgres 底层用 B-tree 实现,跟新华字典按拼音查字一个道理——排好序的东西,查找才快。 * 索引的代价 索引不是免费的。一句话概括: #+begin_quote 读变快,写变慢。 #+end_quote 每次 =INSERT= 、 =UPDATE= 、 =DELETE= 都要同步更新索引——把新值插入排序结构的正确位置。多个索引就乘以多倍。 其次,索引是实打实的数据结构,占磁盘空间,也要占缓存。一张表有八个索引,需要常驻缓存的数据就从一份变成九份。 最后还有查询规划器。索引越多,规划器要权衡的方案越多,规划时间可能超过执行时间。 * 为什么你的索引不生效 加了索引却不走索引,通常掉进了以下陷阱。 ** 复合索引在乎顺序 在宝可梦表上建 =type_1= 和 =type_2= 的复合索引: #+begin_src sql CREATE INDEX ON pokemon (type_1, type_2); #+end_src 这个索引对以下查询有效: #+begin_src sql SELECT * FROM pokemon WHERE type_1 = 'Water'; SELECT * FROM pokemon WHERE type_1 = 'Water' AND type_2 = 'Flying'; #+end_src 但对这个查询无效: #+begin_src sql SELECT * FROM pokemon WHERE type_2 = 'Flying'; #+end_src 原因在索引的排序结构。复合索引 =(type_1, type_2)= 先按 =type_1= 排,再在每个 =type_1= 组内按 =type_2= 排: #+begin_src sql Bug → Flying → [Butterfree, Beedrill, ...] → Poison → [Venonat, Spinarak, ...] Electric → NULL → [Pikachu, Raichu, ...] → Flying → [Zapdos, ...] Fire → NULL → [Charmander, Vulpix, ...] → Flying → [Charizard, Moltres, ...] Grass → Poison → [Bulbasaur, Oddish, ...] Water → NULL → [Squirtle, Psyduck, ...] → Flying → [Wingull, Pelipper, ...] → Ground → [Wooper, ...] #+end_src "Flying"散落在 Bug、Electric、Fire、Water 下面,没有统一入口。数据库无法跳到某个位置一次取完所有 Flying 记录。 如果经常单独查 =type_2= ,就需要再加一个 =type_2= 的独立索引。 ** 函数会破坏索引 不区分大小写的搜索很常见: #+begin_src sql SELECT * FROM pokemon WHERE lower(name) = 'pikachu'; #+end_src =lower(name)= 看起来跟查 =name= 差不多,但索引是按 =name= 原始值排序的(Bulbasaur、Charmander...),不是按 =lower(name)= 排序的。数据库找不到现成的排序结构,只好回退全表扫描。 这适用于任何包裹列的函数。只要比较的左边不是原始的索引列,索引就不参与。隐式类型转换也算—— =text= 和 =integer= 比较时会触发静默转换,效果等同于包了一层函数。 解决办法是建函数索引(见下文)。 ** 用 EXPLAIN 诊断 Postgres 提供了 =EXPLAIN= ,在任何 =SELECT= 前加上就能看到执行计划: #+begin_src sql EXPLAIN SELECT * FROM pokemon WHERE name = 'Pikachu'; #+end_src #+begin_src sql Index Scan using pokemon_name_idx on pokemon Index Cond: (name = 'Pikachu'::text) #+end_src =Index Scan= 说明走了索引。再看问题查询: #+begin_src sql EXPLAIN SELECT * FROM pokemon WHERE lower(name) = 'pikachu'; #+end_src #+begin_src sql Seq Scan on pokemon Filter: (lower(name) = 'pikachu'::text) #+end_src =Seq Scan= 就是全表扫描。加上 =ANALYZE= 可以拿到实际执行时间: #+begin_src sql EXPLAIN ANALYZE SELECT * FROM pokemon WHERE name = 'Pikachu'; #+end_src * 三种你可能不知道的索引 单列索引和复合索引覆盖了大部分场景,但 Postgres 还有几种特殊索引。 ** 函数索引(Functional Index) 前面的 =lower(name)= 问题,解法是直接对表达式建索引: #+begin_src sql CREATE INDEX ON pokemon (lower(name)); #+end_src 任何确定性、不可变表达式都可以索引: #+begin_src sql CREATE INDEX ON users ((created_at::date)); CREATE INDEX ON pokemon ((base_attack * 2)); #+end_src 但要注意:如果频繁查 =lower(name)= ,也许该考虑直接把数据存成小写,而不是靠函数索引绕路。函数索引是手段,不是目的。 ** 部分索引(Partial Index) 普通索引覆盖全表所有行。但如果只查其中一小部分,索引其余行就是浪费。 宝可梦大约 1000 种,传说宝可梦只有约 80 种(不到 10%)。如果应用有个"显示所有传说宝可梦"的功能,建 =is_legendary= 和 =name= 的复合索引虽然能用,但索引里 920 条非传说记录纯粹是摆设。 部分索引只包含满足条件的行: #+begin_src sql CREATE INDEX ON pokemon (name) WHERE is_legendary = true; #+end_src 索引从 1000 条缩减到 80 条。查 =WHERE is_legendary = true= 走索引,查 =WHERE is_legendary = false= 走全表扫描——正好,因为 false 匹配几乎所有行,索引帮不上忙。 另一个典型场景是软删除: #+begin_src sql CREATE INDEX ON users (email) WHERE deleted_at IS NULL; #+end_src 软删除的行几乎不会被查询,不把它们排除出索引只是白占空间。 ** 覆盖索引(Covering Index) 数据库用索引找到行后,还要去表中取查询需要的其他列——两次查找。但如果索引里已经包含了查询需要的所有列,就不需要访问表了。这就是覆盖索引,在 =EXPLAIN= 中显示为 =Index Only Scan= 。 前面的部分索引恰好就是一个覆盖索引: #+begin_src sql SELECT name FROM pokemon WHERE is_legendary = true; #+end_src 索引在 =name= 上,查询也只要 =name= ,全部命中。 也可以用 =INCLUDE= 显式构建覆盖索引: #+begin_src sql CREATE INDEX ON pokemon (name) INCLUDE (base_attack); #+end_src 这样以下查询可以纯索引完成: #+begin_src sql SELECT name, base_attack FROM pokemon WHERE name = 'Pikachu'; #+end_src 为什么不直接把 =base_attack= 放进索引列?因为索引列是用来排序和搜索的。把 =base_attack= 加进索引列意味着每次写入都要按 =(name, base_attack)= 排序,但你从来不按 =base_attack= 搜索,这排序就是白做的。 =INCLUDE= 的意思是"带上这列,但别排序"。 =INCLUDE= 更实际的用途有两个: 1. 数据类型不支持 B-tree 操作符的列(如 =box= 类型),不能作为索引键列,但可以 =INCLUDE= 2. 唯一索引中附加列而不改变唯一性语义: =CREATE UNIQUE INDEX ON users (email) INCLUDE (user_id)= 只在 =email= 上强制唯一,同时覆盖需要 =user_id= 的查询 * 小结 - 索引让读变快,但写变慢、占空间、增加规划开销 - 复合索引按左前缀匹配,跳过首列的查询不生效 - 函数(包括隐式转换)包裹索引列会导致索引失效 - 用 =EXPLAIN= 诊断, =Index Scan= 走索引, =Seq Scan= 全表扫描 - 函数索引、部分索引、覆盖索引是应对特殊场景的利器 原文推荐了 [[https://use-the-index-luke.com/][Use The Index, Luke]] 作为深入学习资料。

CS231n Lecture Note: Large Scale Distributed Training

作者 Louis C Deng
2026年4月19日 08:45

GPUs

Modern AI training is structured as a hierarchy: individual GPUs (or TPUs) sit inside servers, servers are grouped into pods, pods into racks, and racks form a cluster.

Instead of training on a single machine, large neural networks are distributed across this entire cluster, which effectively acts as one coordinated system.

The key challenge is not just computation, but efficiently splitting the workload and synchronizing thousands of accelerators so they stay utilized, using techniques like data and model parallelism while minimizing communication overhead.

Training on GPUs

A model with L layers operates on tensors of shape (Batch, Sequence, Dim)

Split the compution into axes and we get:

  1. Data Parallelism (DP): Split on Batch dimension
  2. Context Parallelism (CP): Split on Sequence dimension
  3. Pipeline Parallelism (PP): Split on L dimension
  4. Tensor Parallelism (TP): Split on Dim dimension

Data Parallelism

In standard data parallelism, a minibatch of N samples is split across M GPUs, so each GPU processes roughly N/M samples. Every GPU holds a full copy of the model parameters.

During the forward and backward pass, each GPU computes gradients independently on its local subset of data. Because the loss is additive over the batch, gradients are linear, so the correct global gradient is obtained by averaging (or summing) gradients across all GPUs.

After gradient synchronization (typically via all-reduce), all GPUs update their local model copies identically, keeping them in sync.

The main limitation is memory: since each GPU must store the full model, the maximum model size is constrained by the memory of a single GPU, regardless of how many GPUs are used.

Fully Sharded Data Parallelism (FSPD)

Fully Sharded Data Parallelism (FSDP), as implemented in systems like PyTorch FSDP, shards model parameters, gradients, and optimizer states across GPUs so that each device only holds a fraction of the full model. Instead of replicating the entire model on every GPU, each worker owns a shard of each parameter tensor, which significantly reduces memory usage.

During the forward pass, parameters are reconstructed on demand using an all-gather operation across GPUs. The full weights are only materialized temporarily for computation and are freed immediately after use. To reduce communication overhead, FSDP overlaps this process with computation by prefetching parameters for upcoming layers.

In the backward pass, gradients are computed using the temporarily gathered parameters and then redistributed using a reduce-scatter operation. This ensures each GPU keeps only its corresponding shard of the gradients rather than the full gradient tensor.

Finally, the optimizer step is performed locally on each GPU using its shard of parameters, gradients, and optimizer states. As a result, no GPU ever needs to hold the full model persistently, reducing memory complexity from O(N) to approximately O(N / k) across k GPUs, at the cost of additional but carefully managed communication.

Hybrid Sharded Data Parallel (HSDP)

In Hybrid Sharded Data Parallelism, the total number of GPUs N is organized into a 2D structure such that N=M×KN = M \times K. The GPUs are divided into M groups, each containing K GPUs.

Within each group of K GPUs, Fully Sharded Data Parallelism (FSDP) is applied. This means model parameters, gradients, and optimizer states are sharded across the K GPUs in the group. No single GPU holds the full model; instead, the group collectively represents one full model in a distributed form.

Across the M groups, standard data parallelism is used. Each group processes a different subset of the minibatch, computes gradients independently, and then synchronizes gradients across groups to ensure all replicas remain consistent.

HSDP is a concrete example of multidimensional parallelism, where different parallelization strategies are applied along different axes of a logical device grid. In this case, GPUs are arranged in a 2D grid: one dimension for sharding (FSDP) and one for replication (data parallelism).

In practice, large-scale training systems often extend this idea further by combining additional dimensions such as tensor parallelism (splitting computations within layers) and pipeline parallelism (splitting layers across stages), forming higher-dimensional parallel training schemes.

Activation Checkpointing

Activation checkpointing reduces memory usage by saving only a subset of intermediate activations during the forward pass and recomputing the missing ones during the backward pass.

Instead of storing activations for every layer, the model saves checkpoints every C layers and discards the rest; when gradients are needed, it recomputes activations starting from the nearest checkpoint.

This significantly lowers activation memory at the cost of extra computation, creating a trade-off where fewer checkpoints save more memory but require more recomputation.

It’s common to set C=NC = \sqrt{N}.

Context Parallelism (CP)

Commonly used for Transformer models

Transformers operate on sequences of length LL (or SS). Context Parallelism involves using multiple GPUs to process a single long sequence that would otherwise be too large for one device’s memory.

The Normalization & Residual Connections have no weights and are easy to parallize. The MLP and QKV Projections are similar to DP.

The Attention mechanism is the hardest part to parallelize because every element in the sequence needs to look at every other element. There are two primary options:

Option 1: Ring Attention

  • Approach: Divide the sequence into blocks and distribute them over GPUs.
  • Execution: Uses an inner loop over keys/values and an outer loop over queries.
  • Verdict: Complex to implement but highly scalable for extremely long sequences.

Option 2: Ulysses

  • Approach: Instead of distributing the attention matrix itself, it parallelizes over the heads in Multi-Head Attention.
  • Verdict: Simpler to implement than Ring Attention.
  • Constraint: Maximum parallelism is limited by the number of attention heads (Max Parallelism = NheadsN_{heads}).

Pipeline Parallelism (PP)

Split the layers of the model across GPUs. Copy activations between layers at GPU boundaries.

To avoid “Pipeline Bubbles” (where GPUs sit idle waiting for data), the model runs multiple micro-batches simultaneously.

Tensor Parallelism (TP)

Split the weights of each linear layer across GPUs, use block matrix multiply.

With 2 consecutive TP layers, shard first over row and second over column to avoid communication.

Benchmarking Parallelism

Hardware FLOPs Utilization (HFU): The fraction of theoretical matmul performance we actually achieve.

We benchmark for the best-case scenario for HFU. But this doesn’t account for other computation like checkpointing, data preprocessing, etc.

Model FLOPs Utilization (MFU): the fraction of the GPU’s theoretical peak FLOPs used for “useful” model computation.

  1. Compute FLOPtheoreticalFLOP_{\text{theoretical}}

    • This is the total number of matrix multiply FLOPs in the forward and backward pass.
    • Heuristic: You can approximate the backward pass as 2x the forward pass.
    • Note: Ignore non-linearities, normalization, and elementwise operations (like residuals). These typically run on FP32 cores and do not significantly contribute to the primary matrix math calculation.
  2. Look up FLOP/sectheoreticalFLOP/\text{sec}_{\text{theoretical}}

    • Find the theoretical maximum throughput of your specific hardware.
  3. Compute ttheoreticalt_{\text{theoretical}}

    • Calculate the ideal time a pass should take if the GPU was running at 100% efficiency:

ttheoretical=FLOPtheoreticalFLOP/sectheoreticalt_{\text{theoretical}} = \frac{FLOP_{\text{theoretical}}}{FLOP/\text{sec}_{\text{theoretical}}}

  1. Measure tactualt_{\text{actual}}

    • This is the real-world time measured for a full iteration.
    • Includes: Data loading, forward pass, backward pass, and the optimizer step.
  2. Calculate MFU

    • The final utilization ratio:

MFU=ttheoreticaltactual\text{MFU} = \frac{t_{\text{theoretical}}}{t_{\text{actual}}}

MFU > 30% is good, >40% is excellent.

ND Parallelism

In practice, we use Use TP, CP, PP, and DP all at the same time. GPUs are arranged in a 4D grid. We tune and optimize the setup to maximize the MFU.

❌
❌