MoreRSS

site iconEST修改

EST = Extrospect, Sein & Tao ,后端工程师。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

EST的 RSS 预览

grep vs sqlite 谁更适合微信聊天记录?

2026-07-09 11:02:00

一个爆火的讨论

云风 @cloudwu 2026-06-29
微信的开发人员根本就不懂该怎么储存数据。这种聊天软件,文本和媒体文件分开存,文本根本就不应该保存在什么数据库(sqlite)里, 一个对话一个文本文件追加就可以了。需要搜索的时候 grep 一下性能完全符合需求。一个对话能有多少文本?一秒一个字 24 小时不间断,一年也就 30M 个字。

网上的争论都是猜测,我呢,决定让 opus 跑一局。

首先让AI去搜微信聊录表结构

微信(Android)聊天记录存储在加密 SQLite 数据库 EnMicroMsg.db 中,使用 SQLCipher(AES-256-CBC,PBKDF2 256000 轮派生密钥)。核心 message 表:

CREATE TABLE message (
    msgId      INTEGER PRIMARY KEY,  -- 本地自增 ID
    msgSvrId   INTEGER,              -- 服务器消息 ID
    type       INTEGER,              -- 1=文字, 3=图片, 34=语音, 43=视频
    isSend     INTEGER,              -- 0=接收, 1=发送
    createTime INTEGER,              -- Unix 时间戳
    talker     TEXT,                 -- wxid 或群 chatroom ID
    content    TEXT,                 -- 消息正文
    imgPath    TEXT                  -- 附件路径
);

测试设计

  • 数据量:50 万条模拟消息(模拟中度用户 ~2 年)
  • 搜索关键词微信支付服务器数据库会议周末
  • 环境:macOS Apple Silicon, Python 3.14, ripgrep 15.1, DuckDB 1.5.4, Polars 1.42
  • 每项测试 3 轮取最小值

参赛选手

分类 方案 思路
传统文本 grep (BSD) 最朴素的逐字节匹配
SIMD文本 ripgrep AVX2/NEON 并行 + 多线程
零拷贝 mmap 直接搜索 OS page cache + Python bytes.find
压缩文本 zstd 流式解压搜索 省空间,边解压边搜
索引 倒排索引 (2-gram) 搜索引擎思路,内存索引
索引 Bloom Filter 分块 概率型预过滤
RDBMS SQLite LIKE 微信的实际方案(去掉加密)
RDBMS SQLite mmap 模式 mmap I/O 加速
RDBMS FTS SQLite FTS5 (trigram) 全文搜索引擎
列式DB DuckDB contains() OLAP 列式扫描
列式DB DuckDB FTS DuckDB 的全文搜索扩展
列式文件 Parquet(zstd) + DuckDB 列式文件直接查询
DataFrame Polars lazy scan Rust实现的极速 DataFrame
DataFrame Polars in-memory 全量载入内存
并行文本 ripgrep 多文件并行 分块文件 + rg 多线程

测试结果

关键词搜索延迟(ms, 3轮最小值)

# 方案 微信支付 服务器 数据库 会议 周末 平均
1 SQLite FTS5 (trigram) 0.65 0.46 0.37 ❌² ❌² 0.31¹
2 Polars lazy scan 3.51 2.56 2.43 2.49 2.47 2.69
3 倒排索引 (2-gram) 2.71 3.26 2.78 3.24 2.37 2.87
4 Polars in-memory 2.67 3.61 2.98 3.21 3.21 3.13
5 DuckDB contains() 3.72 3.23 3.18 3.91 4.41 3.69
6 Parquet + DuckDB 4.63 4.37 4.55 4.88 4.84 4.65
7 ripgrep 多文件并行 10.65 10.26 9.48 10.32 9.05 9.95
8 DuckDB FTS (BM25) 12.53 11.77 12.85 11.82 12.78 12.35³
9 ripgrep (SIMD, 单文件) 13.24 13.77 13.40 14.14 13.10 13.53
10 mmap 直接搜索 17.00 22.62 22.86 31.36 30.52 24.87
11 Bloom Filter + 扫描 25.34 25.25 25.06 26.68 27.01 25.87
12 SQLite mmap LIKE 37.45 37.45 37.92 37.83 37.81 37.69
13 SQLite LIKE 43.13 41.40 41.50 41.51 41.46 41.80
14 grep (BSD) 139.10 136.86 141.42 122.54 124.16 132.82
15 zstd 流式解压搜索 161.32 164.22 164.56 170.23 170.73 166.21

¹ FTS5 只对 ≥3字符 的关键词有效,取3个有效关键词平均
² trigram tokenizer 无法匹配 2 字符的中文词
³ DuckDB FTS 默认 tokenizer 不支持中文,返回 0 结果(延迟仍可参考)

视觉化排名

 1.                                                            █   0.31ms SQLite FTS5
 2.                                                            █   2.69ms Polars lazy
 3.                                                            █   2.87ms 倒排索引
 4.                                                            █   3.13ms Polars in-mem
 5.                                                            █   3.69ms DuckDB contains
 6.                                                           ██   4.65ms Parquet+DuckDB
 7.                                                         ████   9.95ms rg多文件并行
 8.                                                       ██████  12.35ms DuckDB FTS
 9.                                                       ██████  13.53ms ripgrep (SIMD
10.                                                 ████████████  24.87ms mmap 直接搜索
11.                                                 ████████████  25.87ms Bloom+扫描
12.                                           ██████████████████  37.69ms SQLite mmap LIKE
13.                                         ████████████████████  41.80ms SQLite LIKE
14. ████████████████████████████████████████████████████████████ 132.82ms grep (BSD
15. ████████████████████████████████████████████████████████████ 166.21ms zstd流式解压

复合条件查询(指定用户 + 时间范围 + 关键词"会议")

方案 延迟 (ms) 倍率(vs grep)
SQLite indexed 2.80 76x
DuckDB 4.57 47x
Polars in-memory 7.02 30x
ripgrep pipe 20.47 10x
grep pipe 213.90 1x

存储大小

格式 大小 vs TSV 说明
Parquet (zstd) 8.3 MB 0.18x 列式 + 字典编码 + 压缩
zstd 压缩 TSV 12.7 MB 0.27x 纯压缩
DuckDB + FTS 26.0 MB 0.55x 含全文索引
TSV 纯文本 47.0 MB 1.00x 基线
SQLite 69.2 MB 1.47x B-tree 开销
SQLite + FTS5 116.8 MB 2.49x trigram 索引翻倍

各方案深度分析

Tier 1: 亚毫秒级(< 1ms)

SQLite FTS5 (trigram)
- 原理:对 content 字段的每个 3 字符子串建倒排索引
- 优点:查询极快(0.3-0.6ms),无需额外依赖
- 缺点:索引体积翻倍(+68MB);trigram 无法匹配 ≤2 字符的关键词
- 适用:搜索词通常 ≥3 字符的场景

Tier 2: 低毫秒级(2-5ms)

Polars (lazy/in-memory)
- 原理:Rust 实现的 DataFrame 引擎,列式内存布局 + SIMD 字符串匹配
- 优点:Parquet 文件仅 8.3MB(最小!),查询 2-3ms,复合查询也快(7ms)
- 缺点:需要加载到内存;Python 库依赖
- 杀手锏:8MB 的 Parquet 文件 + 3ms 搜索延迟,这是存储效率和速度的最佳平衡点

倒排索引 (2-gram, 内存)
- 原理:搜索引擎最经典的思路,对所有 2-gram 建 posting list
- 优点:构建仅 0.78s,查询 2.9ms,支持任意长度关键词
- 缺点:纯内存(需要序列化/加载),索引构建需要全量遍历
- 适用:append-only 数据可以增量更新索引

DuckDB contains()
- 原理:列式存储,content 列连续存放,CPU cache 友好 + SIMD 扫描
- 优点:无需专门索引即可 3.7ms;复合查询也仅 4.6ms;文件仅 26MB
- 缺点:需要 DuckDB 运行时
- 杀手锏:不建任何索引,纯靠列式布局就比 SQLite LIKE 快 11 倍

Parquet 文件 + DuckDB 零拷贝查询
- 原理:Parquet 本身就是列式格式,DuckDB 可以直接查询不需导入
- 优点:文件仅 8.3MB,不需要数据库进程,查询 4.6ms
- 缺点:每次查询需要启动 DuckDB 连接
- 杀手锏:一个 8MB 的文件就是完整的"数据库",任何语言都能读

Tier 3: 10ms 级

ripgrep 多文件并行
- 原理:把消息分块成多个文件,ripgrep 的 work-stealing 线程池并行搜索
- 优点:比单文件 ripgrep 快 ~35%(10ms vs 13.5ms)
- 缺点:文件管理复杂
- 适用:数据天然按时间分文件存储的场景

ripgrep (SIMD, 单文件)
- 原理:AVX2/NEON 每周期处理 16-32 字节,多线程(对单文件仍用单线程)
- 优点:零配置,即装即用
- 缺点:对单文件只能单线程

Tier 4: 失败/不推荐的"邪路"

Bloom Filter 分块预过滤
- 问题:中文常用 2-gram 只有 ~690 种,每个块都包含所有 n-gram,过滤率为 0
- 结论:对高频 n-gram 的数据集完全无效,白费构建时间

zstd 流式解压搜索
- 问题:Python 解压+搜索 166ms,比不压缩的 grep 还慢
- 结论:CPU 密集的解压抵消了 I/O 节省。如果数据在 SSD 上,不如直接读原文
- 可能有用的场景:数据在网络存储/HDD 上,I/O 是瓶颈时

DuckDB FTS (BM25)
- 问题:默认 tokenizer(类似 ICU word boundary)不支持中文
- 结论:需要自定义 tokenizer 或等 DuckDB 支持 trigram/CJK

mmap 直接搜索
- 表现:24.87ms,比 ripgrep 慢 2 倍
- 原因:Python 的 mmap.find() 是朴素搜索,没有 SIMD 优化
- 如果用 C/Rust 实现的 mmap + SIMD,预期接近 ripgrep 水平

推荐方案排名

如果从零设计微信聊天存储

优先级 方案 搜索延迟 存储 理由
🥇 Parquet + Polars/DuckDB 3-5ms 8 MB 存储最小、搜索极快、格式通用、append 友好
🥈 DuckDB 数据库 3.7ms 26 MB 单文件数据库、列式、SQL 查询、FTS 索引可选
🥉 SQLite + FTS5 0.3ms 117 MB 最快搜索(有索引)、但索引太大
4 ripgrep + 分块 TSV 10ms 47 MB 最简单、无依赖、人类可读

如果要"一行命令搜聊天记录"

# 方案 A#: ripgrep 搜纯文本 (~10ms)
rg "微信支付" messages.tsv

## 方案 B: DuckDB 直接查 Parquet (~5ms)
duckdb -c "SELECT * FROM read_parquet('messages.parquet') WHERE contains(content, '微信支付')"

## 方案 C: DuckDB 复合查询 (~5ms)  
duckdb -c "SELECT * FROM read_parquet('messages.parquet')
           WHERE talker='wxid_xxx' AND createTime > 1672531200
           AND contains(content, '会议')"

最终结论

"用 grep 代替 SQLite" — 部分正确,但格局太小

  1. ripgrep 比 SQLite LIKE 快 3 倍(13ms vs 42ms)— "grep 派"的论据成立
  2. 但 DuckDB/Polars 比 ripgrep 又快 4 倍(3ms vs 13ms)— 列式才是正道
  3. Parquet 比纯文本还小 5.6 倍(8MB vs 47MB)— 省空间还更快
  4. 复合查询(按人+时间+关键词)SQLite 仍然最强(2.8ms)

真正的启示

聊天记录是 append-only 数据 → 最适合列式存储 (Parquet/DuckDB)

  • 不需要 RDBMS 的事务/锁/B-tree 开销
  • 列式布局让 content 列连续存放,CPU cache 极度友好
  • zstd 字典压缩对重复模式多的聊天文本压缩率惊人(82%)
  • 追加写入只需要 append 新的 row group

微信搜索慢的根因:不是 SQLite vs grep 的问题,而是 SQLCipher 加密的 PBKDF2 256000 轮 + 行式存储对全文扫描不友好。如果用 Parquet + 硬件 AES,可以同时做到加密和极速搜索。

脑洞:终极方案

messages.parquet (8MB, zstd压缩, 硬件AES加密)
  → DuckDB/Polars 直接查询 (3-5ms)
  → 可选: 内存倒排索引 for < 1ms 搜索
  → 追加: 新消息 append 到 staging 文件, 定期 merge
  → 导出: parquet 是开放格式, 任何语言/工具可读

复现

pip install duckdb polars zstandard cryptography
python3 benchmark.py 500000     # 基础测试 (SQLite/grep)  
python3 benchmark_v2.py 500000  # 扩展测试 (DuckDB/Polars/mmap/...)

源码放在 https://github.com/est/snippets/tree/master/grep_vs_sqlite

模拟聊天记录有500,000行 一个 50MB 的 .tsv 太大了。自己让AI根据文章和最上面表结构生成一份吧。

后记

被Polars的性能震惊了。说实话如果换我肯定不会选sqlite。聊天记录又不会改。ACID的 OLTP 明显浪费

[AI] curl -NT. 导致100% CPU原因

2026-07-08 16:05:00

有AI就是好使,搁以前自己得盯半天也看不明白。。。

问题确认

curl -NT. 在连接一个持续推送数据的 streaming 服务器时,会产生一个 紧密的 pause/unpause 循环,导致 100% CPU。

根因分析

-T.(注意是点号,不是减号)将 stdin 设为非阻塞模式,然后依赖 EAGAIN + CURL_READFUNC_PAUSE 机制来避免阻塞。核心循环发生在三个回调之间:

第一步 — 上传尝试失败src/tool_cb_rea.c):

  {
    rc = read(per->infd, buffer, sz * nmemb);
    if(rc < 0) {
      if(errno == EAGAIN) {
        errno = 0;
        config->readbusy = TRUE;
        return CURL_READFUNC_PAUSE;
      }
      /* since size_t is unsigned we cannot return negative values fine */
      rc = 0;
    }
  }

stdin 没数据 → read() 返回 EAGAIN → 设 readbusy=TRUE → 暂停上传。

第二步 — 下载数据到达立刻解除暂停src/tool_cb_wrt.c):

  if(config->readbusy) {
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
  }

streaming 服务器持续发送数据 → tool_write_cb 被不断调用 → 每次都无等待地立刻解除上传暂停

第三步 — 进度回调只有 1ms 退避src/tool_cb_rea.c):

  if(config->readbusy) {
    if(ulprev == ulnow) {
#ifndef _WIN32
      waitfd(1, per->infd);
#else
      /* sleep */
      curlx_wait_ms(1);
#endif
    }

    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
  }

tool_readbusy_cb 有 1ms 的 waitfd 退避,但被 tool_write_cb 的无等待解除抢先了。

完整的死循环(每次 curl_multi_perform 内)

lib/transfer.cCurl_sendrecv 中,处理顺序是先下载后上传

  if(k->keepon & KEEP_RECV) {
    result = sendrecv_dl(data, k);
    if(result || data->req.done)
      goto out;
  }

  /* If we still have writing to do, we check if we have a writable socket. */
  if(Curl_req_want_send(data)) {
    result = sendrecv_ul(data);
    if(result)
      goto out;
  }

所以单次迭代内:
1. sendrecv_dl → 读到 streaming 数据 → tool_write_cb → 看到 readbusy=TRUE立刻解除暂停(零等待)
2. sendrecv_ultool_read_cb → stdin EAGAIN又暂停readbusy=TRUE
3. 回到 curl_multi_poll → 下载 socket 有数据 → 立刻返回(不会 sleep)
4. 重复 1-3

lib/easy.c 的主循环:

  while(!done && !mresult) {
    int still_running = 0;

    mresult = curl_multi_poll(multi, NULL, 0, 1000, NULL);

    if(!mresult)
      mresult = curl_multi_perform(multi, &still_running);

curl_multi_poll 的 1000ms 超时形同虚设——因为下载 socket 始终可读,poll() 立刻返回。

结果:每秒跑上千次这个循环,每次包含多个系统调用(pollreadwrite),CPU 直接打满。

问题本质

stdin 没有被加入 curl_multi_poll 的 pollset。只有网络 socket 参与了 poll。所以 curl 无法高效地"同时等待 stdin 和网络数据",只能靠 tool_write_cb 里的盲目 unpause 来弥补,在 streaming 场景下退化成忙等。

用户侧解决方案

如果你不需要从 stdin 上传数据(只是想看 streaming 响应),不要用 -T.

# 正确的方式:只看 streaming 输出
curl -N https://streaming-server/events

# 或者重定向 stdin 为空
curl -NT. https://streaming-server/events < /dev/null

< /dev/null 会让 read() 返回 0(EOF)而不是 EAGAIN,不会进入 pause/unpause 循环。

如果确实需要双向 streaming(边上传 stdin 边接收),目前没有完美的用户侧 workaround。可以考虑:

# 用管道加一层缓冲,减轻空转(效果有限)
cat | curl -NT. https://streaming-server/events

源码级修复思路

如果要在源码层面修复,最直接的方案是tool_write_cb 中去掉盲目 unpause,改为只在 tool_readbusy_cb 中带退避地 unpause,并增大退避时间:

// tool_cb_wrt.c — 去掉这段,或加条件限制
// 不再从 write callback 无脑解除上传暂停
if(config->readbusy) {
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);  // 删除或改为有条件触发
}
// tool_cb_rea.c — 增大退避时间
if(config->readbusy) {
    if(ulprev == ulnow) {
        waitfd(100, per->infd);  // 从 1ms 提高到 100ms
    }
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
}

更彻底的方案是把 stdin fd 加入 curl_multi_poll 的 extra fds,但这需要改 curl_multi_poll 的调用方式,涉及面更大。

或许「数羊」真是个有效的入睡法

2026-07-06 15:51:00

无聊刷博客,《AI如何导致和修复了我的失眠问题》 这老哥真猛,通过统计规律发现自己 失眠 的罪魁祸首。(不剧透。感兴趣点开阅读)

我自己也写过 快速入睡的技巧和原理 以及 强行入睡方法 v2.0 其实我都忘记这个 2.0 方法了。都不知道自己当时怎么想到这个办法的,原来自己写的东西也能常看常新(老登健忘症😂),所以还是要多写,多记录

本文章的讨论都是基于这个 2.0方法的,接着看之前请务必点开 2.0 那个链接,不长,一会儿就看完。

然后我就无聊让AI 评价一下这个 2.0 方法是不是真的,然后AI说真有学者在搞类似的,关键词 :

  • 睡前“认知打断 / imagery distraction
  • cognitive shuffle
  • serial diverse imagining (SDI)

然后我去搜了下

TikTok和Instagram上爆火的“认知洗牌法”,火到连医生都开始推荐。选一个随机的单词(比如 cake,蛋糕),专注于这个词的第一个字母(C),然后列出一串以这个字母开头的词:cat(猫)、carrot(胡萝卜)、calendar(日历)等等,一边列举,一边在脑中想象这些词的画面。当你准备好了,就转到下一个字母(A),重复上述过程,继续进行下去(K、E),直到你睡着或者想换一个新词为止

嗯,和我的方法居然殊途同归,只是更加麻烦,需要调用大脑的语言区。

但是自媒体这个标题让我产生了兴趣 《别再数羊了》,我恰好周末刷了 西藏那曲拉姆 的视频 藏族人家里几十上百头牦牛,如何识别是不是自己家的?

然后突然发现一个被大多数人忽略的惊人的事实:放牧人的白天是极度无聊和空虚的,以至于他/她们能辨识自己每一头羊的特征、性别,甚至给每头羊起个名字。视频说牦牛和人一样,每一头都有独一无二的毛色、长相、形态等。

脸上有点黑?叫小黑;黑白相间的?叫花脸。按脾气起名字,暴躁哥,温顺妹;还有谁和谁喜欢一起吃草,小牛的母牛妈妈是谁等等。

所以这两件事就串起来了。「数羊」这事儿一定是牧区的人发明出来的,比如英国乡下,但是城里人哪里知道这些细节啊。

牧民晚上躺床上,没事干,说不定就给自己牛羊编故事造剧情啊。而且关键词是「数」,你不能陷入一个逻辑推理细节,必须不停地轮换,把羊变成高频切换的具象个体(有名字、有外形、有性格),才能保证大脑疲惫然后入睡。

AI总结:

对于古代或乡村的牧羊人来说,夜晚盘点羊群是一天结束时最让人安心的闭环。他们脑海中的羊,确实是毛发质感、体态特征各异的具象实体。
现代城市人剥离了这种生活语境,把一幅丰富的田园3D渲染图,降维成了枯燥的Excel递增表格,自然也就失去了助眠的神奇功效。

我觉得AI真有点东西的。清点生产资料也是被忽略的一环。如果我睡前都能盘点自己该做的事儿都做了,羊吃饱了,明天会更好,没啥落下的,那我肯定也睡得安稳啊。

但是现代人难就难在很多事是跨很多天的,入睡是非常不情愿的。如果有精力很多人甚至愿意熬夜。

都怪爱迪生,本来以为发明电灯泡给人类漫长的黑夜带来光明,没想到人类却用这玩意来加班和难眠!!😤 😤 😤 如果太阳下山就睡觉,就算失眠4小时你到23点也睡着了 😂

唯物主义「天命」论

2026-07-02 16:07:00

看到一篇雄文《明末士大夫为什么毫无气节纷纷变节投降满清?》大受启发,想看原文的可以点开链接,下面是精简和摘录:

因为程朱理学在理论存面存在漏洞,被鞑子无意之间利用了,事实上大部分的鞑子统治者在这方面,也都是知其然而不知所以然。
不否认明末也有很多了不起的仁人志士,但如果你对中国历史有些疑惑,觉得似乎宋朝之后中国就有点不一样了,那恭喜你,你的直觉是对的。由于元代留下的遗毒过甚,明朝没能拨乱反正,元明清三代,所有的皇帝和“儒家士大夫”,都是失礼而不自知之人。元明清三代的所谓“礼法”,放在先秦两汉的大学者们面前,诸子百家不管哪家,都一眼就能看出来全是假礼。如果墨子这个儒家最大的反对者看见了,估计更是嘲笑孔子能笑的棺材板都压不住。这些“假礼”,就是元明清三代那些僵化的等级秩序、规规矩矩。出于汉人的直觉,厌恶这些是再正常不过了。
一、建用皇极
宋以前儒学,与宋明理学不是同一种思想。先秦至汉唐儒学的最高原则是天命与大中之道,而不是君主本身。朱熹重新解释《尚书·洪范》"皇极",从九畴排第五提高到最高优先级, 把它解释为 君主是天下的最高标准,天下围绕君主建立秩序。,从而改变了儒家的政治哲学。
二 · 定于一尊
元朝恢复科举时规定:四书、五经必须按照朱熹注释考试,使其成为与功名利禄直接挂钩的唯一标准。于是理学不再只是一个前朝有争议的一个"逆党"学说,而成为整个帝国唯一的意识形态。明清完全继承了这一制度。
三、诸夏之亡
《论语》中孔子其实始终强调,华夏共同体高于个人君臣关系。例如孔子称赞管仲,就是因为即使“不忠”,改事新君,只要能够保卫华夏,也仍然值得肯定。但程朱理学更加重视君臣名分、上下秩序、皇权连续性,于是出现一种新的逻辑:即使皇帝是异族,也不能没有皇帝。1908年孔令贻把德国皇帝威廉二世肖像迎进了孔府
四、天命之礼
孔子的礼,本质上来源于 天命。所以:礼约束君主,君主不能创造礼。而理学实践中却逐渐变成皇帝成为礼法的最终解释者。于是礼不再约束权力,而成为权力工具。
五、凡心之仁
基督教的本质是爱与诫二元一体的罪文化,那么发源自中国的东亚文化,本质就是仁与礼二元一体的耻文化。
基督教中所谓的爱,叫做“Agape”。这是一个专有词,它有多重要呢?欧洲所有国家,不管哪种语言,它的拼写方式都是一致的,一字不易。“Agape”的源头是神,它是一种具有普世性的博爱。而“仁”和“Agape”的区别在于,中国的“仁”,其源头是凡人,是“己所不欲勿施于人”,它是一种推己及人的有差等之爱。
华夷之辩是“礼”的边界,“礼”是对“仁”的约束。面对民族危机时,个人、家庭利益、官职利益、君臣秩序都会压倒共同体利益。因此许多人最终选择保身、顺从、投降,而不是抗清。
(注:这里其实用 “异端” 和 “有经人” 对比更加强烈)
六、知行合一
孔子、董仲舒的礼法,理论源头是天帝,实践中确实也按天帝至上来执行。而程朱理学的礼法,理论源头是天理,但是实践中,理学的礼法源头压根不是天理!在以前,经筵都是大儒给皇帝讲课,到了乾隆那,变成皇帝给大儒讲课了。
先秦两汉礼法的源头是神?因为礼法的源头就不能是一个具体的人!礼法是用来栓人的保守性,如果礼法的源头是人,那么栓着你的绳子就牵在那个人手上,你就是那个人的奴才,礼法就成了赤裸裸的等级压迫。
如果天帝无法约束皇权,那么天帝也不能保护皇权。天帝不能保护皇权,皇帝就只能自己保护自己,于是朱元璋废除了丞相制度。所谓明清飞速膨胀的君权,其实和南北朝盛产的疯子皇帝有异曲同工之妙。跟着龙椅遗传的精神病,本质是因为坐在龙椅上的人没有安全感。没有人相信天命,连皇帝本人都不相信自己真正“受命于天”。而对于儒生大臣,后人说张居正是“常务副皇帝”。
七、失礼之国
(作者的一些感想,比较杂,不引述了)

看完之后真过瘾。本来想着今天的键政就到这了。

特别是第二点,最近几年我逐渐从对“科举”的好感,降低了。学生时代总有那么一些“考功名”的亲切感,但是现实世界还是觉得“军功合伙人”更优。

但是有一个更大的疑惑,转念一想不对劲。于是补充一点我自己的观点。

皇帝这个岗位,从秦到唐,都是一把手承包了世俗君权,和神权的双重责任。皇帝在赵政之前实际上是两个岗位,大祭司负责给「帝」传话,王中王称皇负责行政。

宋以后皇帝把道德秩序这一块外包给儒生了,自己关起门做皇家经营了,剩下的全是算计。

说得直白点汉唐的皇帝还勉强要点b脸,遇到难堪的事,还得想办法给手下和民间一点说法。

宋以后就是无情的 打工 - 服从 叙事。

我是真的越来越看不起大怂国。我把内心抱怨说给AI,AI指出

宋代皇帝其实仍然非常受士大夫制约

我反驳:

例如宋代皇帝其实仍然非常受士大夫制约,需要合理分赃才能一起搜刮老百姓。造成有史以来遍地造反运动。

AI当时就不乐意了。把教科书和网上常见吹捧宋朝的资料抬出来了。比如说宋朝其实造反的规模和烈度没那么多

但是我想说,你把大半个中国都丢干净了,西夏 辽金的汉人躺棺材里了,当然北境无人“造反”了。因为别人都被占领了。

秦汉 隋唐造反不就是关中和河北人打架吗?这一毛病从姬发那一辈儿就没停过。天子这一岗位说的直白点就是给大家当调停人,pax sinica 。你宋说白了就一个节度使,偏安江淮一隅,还这么多造反的。呸!

AI 被这个角度刁钻的回答干懵逼了。说你这个框架,衡量皇朝优劣的标准不是"有没有起义",而是 能不能维持整个华夏共同体的秩序。那么很多评价都会变。最后还嘴硬一句,宋朝其实内部治理得很好啦,最终是被蒙古人迫不得已干趴下的。

我当时就火了。对蒙古你好意思讲“战争”?实际上周 秦 汉 唐 的草原治理能力,也是 “天命” 的支柱啊。周武王牧誓,手里拿着的就是牦牛尾巴!不是象征汉人农耕的的锄头!纵观宋、金两朝,对草原的经略就是完全失败的。垃圾!

因为刚刚前一阵子看到 《金朝对草原的减丁,为何遏制不了蒙古的崛起》 这里 cue 一下

宋辽金真是一群乡镇企业家暴发户械斗。烂得要命。唉。你仔细想一下宋吹,那些证据,多少是近代人牵强附会的?宋朝人自己觉得骄傲吗?给好评的,都是后世明清没当上官的文人吧?

天命最大的意义在哪里?给人指明前行的道路,给人以希望。即便黑暗中世纪教会和君权也是这么分工的。挫宋做到了啥?苟且罢了。

有人说,大宋“杯酒释兵权”终结了五代十国,功劳巨大。但从“天命”的角度讲,国之大事,在祀与戎。你老赵家没能给一个民族找到希望,也干不过架,你这个王权就没有存在的根基。说得直白一点,东亚这篇土地,从周天子那一辈人开始就是武装殖民模式。你不殖民,有的是蒙古人 女真人殖你的民。

其实一开始那个文章的框架来讨论一个具体的事就很有力度。如何评价北宋赵光义毁掉太原?

具体的事迹大家可以问下AI。正如文章里所说的,丢掉了 华夷之辩 这个“天命”。那别人河北幽州人全体投夷你也怨不了谁。你之后靖康之变都是报应。

网上对 “内亚” 的说法一直有巨大争议,阿姨那边一直说“武德注入”,实际上征服,殖民 和扩张这些说法太粗暴。但是如果天子不提供秩序,那么你也别怨替代秩序的出现;无德,丢天命,天命归别人。似乎就是这么简单的道理

政治空间是没有真空的

谁的组织度更高,谁的天命就更强。

联系到欧洲发家,启蒙运动 文艺复兴,是在抛弃君权 神权 这个思维定势下做到的。更厉害。

不过替代品似乎是——资本?一个侧面就是牛津剑桥主打神学专业,改成政治经济学

资本的扩张我觉得按照脉络去捋,是蒸汽机,发展纺织业,全球贸易。 归根结底是煤铁革命,但是仔细想,实际上是把战争的边界改成向几百亿年的太阳能存款挖出来消耗了。

农牧时代是拿当季的太阳能来pk。谁能提供摩擦最小的当季太阳能分配,谁就在古代“有德”,有“天命”

现代社会一样的。全球变暖,污染,绿色能源等,一直到社会公平正义。

天命天命,天就是天上射下来的能量,命就是草木人间生命。

哈哈哈这个解释如何?是不是很唯物。

天(能源)与命(生命负载)之间,能否实现最高效的转换与分配?

生命本身就是一个“负熵的过程”。生命存在的意义,就是把“天”射下来的能量,通过光合作用、通过食物链,转化为有序的社会结构、文明形态和思想结晶。

如果转换效率高、分配摩擦小: 生命、社会繁荣,这就叫“天命有归”。

如果中间商抽成太多,腐败、内卷、战略自残(如毁太原): 能量(天)射下来了,却无法高效转化为“命(繁荣)”,能量在内部耗散了,这就是天命将尽。

大宋这种对外又送又怂,把“皇帝”这个singleton强行改成“兄弟之国”, 对内三冗,取缔军功兑现,换成“理学”考试,用首都的局部繁荣掩盖整体的失败。就是丢 天命 的典型。

古代的战争:争夺的是对“天光(土地)”的占有权。

现代的危机:焦虑的是“天光(太阳能存款)”快烧完了,我们该如何重新设计“命(人类社会)”的分配效率。

政治的本质就是能量管理工程。

天命 在过去,看重国体和人君,现代看政治制度科技政策。资本这个玩意,纵然有那么多毛病,但是很好的执行了“天命”

我的 Vibe Coding 最佳实践——ADR文档

2026-06-26 09:40:00

工作和业余也用AI写代码,大大小小项目都经历了。从 rules, skills, spec, agent 到 harness 都玩过了

从AI嘴里发现一条比较稳的套路——ADR文档

rule, skills, spec 这些东西最大的问题就是瞎jb指挥。ADR 的好处是记录why,以及决策演变历史。

贴一段我整理的 ADR 文档就明白了:

---
title: 如何使用 ADR
id: ADR-001
date: 2026-06-26 09:01:21
status: accepted
---

ADR

ADR(Architecture Decision Record,架构决策记录)的核心目标很简单:记录为什么做出了某个重要技术决策,而不是记录系统长什么样。

目前比较常见的是 MADR、Nygard ADR 两种风格,但组织方式都大同小异。

一个团队通常会按下面几个层次组织。

1. 一个 ADR 对应一个决策

不要一个 ADR 写整个系统设计。

好的粒度例如:

ADR-001 使用 PostgreSQL 作为主数据库
ADR-002 API 使用 REST 而不是 GraphQL
ADR-003 服务间通信采用 gRPC
ADR-004 用户认证采用 OAuth2 + JWT
ADR-005 使用事件驱动 Outbox Pattern

而不是

系统架构设计.md

因为几年以后,很难知道某个结论为什么来的。

2. 编号保持永久

一般都会固定编号。

adr/

0001-use-postgresql.md
0002-use-rest-api.md
0003-use-grpc.md
0004-use-oauth2.md

编号一旦存在,就不要修改。

即使后来废弃:

0003-use-grpc.md
Status: Superseded by ADR-0018

这样引用不会失效。

3. Status 非常重要

一般都有状态。

  • Proposed
  • Accepted
  • Deprecated ❌
  • Superseded 🔄
  • Rejected ⛔️

例如:

Status: Accepted
Date: 2026-06-19 01:02:03

如果后来换了:

Status: Superseded
Superseded by: ADR-0018

而新的 ADR:

ADR-0018
Supersedes: ADR-0003

形成完整历史。

4. 一个 ADR 的典型模板

---
title: ADR-008 使用 PostgreSQL
id: ADR-008
date: 2026-06-18 12:32:46
status: accepted
---


## Context

目前需要:

- ACID
- JSON 查询
- 成熟生态

候选:

- PostgreSQL
- MySQL
- MongoDB

## Decision

选择 PostgreSQL。

## Consequences

优点:

- SQL 功能完整
- JSONB 支持优秀
- 社区成熟

缺点:

- 运维复杂度略高
- 分库分表方案需要额外设计

可以再加:

Alternatives

Decision Drivers

Trade-offs

References

5. 按领域组织,而不是按时间(可选)

在一个目录,用文件名体现领域:

008.backend.use-grpc.md
010.security.use-oauth.md
012.frontend.react-query.md

这样编号保持连续,查找也方便。

6. ADR 之间允许引用

例如:

ADR-0015

Context

依赖:
- ADR-0002
- ADR-0008

Decision

由于 ADR-0008 已经确定 PostgreSQL,
因此 Outbox Pattern 可以直接利用事务。

形成决策网络,而不是孤立文档。

7. ADR 只记录"为什么"

这是很多团队最容易犯的错误。

不要写:

Controller
Service
Repository

这是设计文档。

ADR 应该写:

为什么不用 MongoDB?

为什么不用 GraphQL?

为什么采用 Saga?

为什么拆成多个 Service?

为什么 Event Sourcing 被放弃?

重点永远是 Why,而不是 What

8. 和设计文档分开

过去很多团队会这样组织:

文档 回答的问题
RFC / Proposal 未来准备怎么做?
ADR 为什么这样做?
Architecture Doc 系统如何组织?
Design Doc 某个功能如何实现?
Runbook 如何运维?

流程是 RFC → ADR → Design Doc → Code。

RFC 用于讨论方案,达成决策后沉淀为 ADR;随后具体实现细节写入设计文档,最终落实到代码。这样既保留了决策依据,又避免 ADR 演变成冗长的设计说明。

在AI 时代,更简洁,易维护的方式是:

  • ADR 形成决策历史;
  • DESIGN.md (小项目也可以直接放 README.md) 反应当前设计,大量引用 ADR 而不是重复。
  • 迭代排期(spec,phase文档等)引用ADR作为缘由

AI编写的项目,到后期,泥潭就是大量的docs。ADR 的好处是不用修订,全面引用+supersed。保证决策链清晰,低上下文成本

MacOS 快速插入当前时间

2026-06-20 12:06:22

第一步:创建快捷指令

打开 Shortcuts

点击右上角 + 新建快捷指令。

添加动作 1:日期

搜 添加 日期(Current Date) 动作,默认为当前时间

添加动作 2:格式化日期

添加 格式化日期,日期格式 自定义,填 yyyy-MM-dd HH:mm:ss

添加动作 3:Applescript

on run {input, parameters}
  -- 稍微延长一点延迟,确保触发快捷键的手指已经离开键盘
  delay 0.1
  -- display dialog "Current date"

  -- 将 Shortcuts 传入的 list 转换为字符串
  set ts to item 1 of input as string

  tell application "System Events"
    -- 释放可能被系统残留挂起的修饰键状态
    --  键盘区数字的 Key Code 分布是乱序的
    set keyCodeMap to {29, 18, 19, 20, 21, 23, 22, 26, 28, 25}

    key up command
    key up option
    key up control
    key up shift


    repeat with i from 1 to length of ts
      set c to character i of ts
      set charID to id of c
      if c is ":" then
        -- 分号,加 shift 变成冒号
        key code 41 using {shift down}
      else if c is "-" then
        -- 减号,不需要 shift
        key code 27
      else if c is space then
        key code 49
      else if charID ≥ 48 and charID ≤ 57 then
        -- ASCII 码范围过滤 转换算出 1 到 10 的索引
        set targetIndex to charID - 47

        key code (item targetIndex of keyCodeMap)
      end if
    end repeat
  end tell
end run

第二步:设为快速操作

点快捷指令右上角

勾 Use as Quick Action(用作快速操作)

选 任何应用程序

第三步:绑定快捷键

新版macOS可以直接绑定。

之前的:系统设置 → 键盘 → 键盘快捷键 → 服务(或“快速操作”)


我习惯的方式是右手 Cmd+Opt+T 。

以前觉得 applescript 慢,但是现在反而发现需要 delay 0.1 否则会触发 Cmd+Opt 的连招

本来AI给的版本是 keystroke 指令,容易误触 modifier keys,所以改成 key code。

还以为 AI 写错了,没想到 mac 的 0-9 数字键code 居然不是连续的。

不过这JB玩意不稳定,一会儿授权失效了,需要去 设置 - 隐私 - 辅助功能 里删除 Shortcuts 再添加。。