MoreRSS

site iconEST修改

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

Inoreader Feedly Follow Feedbin Local Reader

EST的 RSS 预览

gitweets改版,复刻微信「朋友圈」

2026-07-12 17:53:00

去年搓了个 gitweets,一个 .html 实现了「微博」,拿git历史当feed流~发推

这个周末看着 coding plan 还剩 20% 要到期,没用完,怎么办呢?想来想去,挖大坑干不完,小修小改,就拿这个 gitweets 继续填坑了

首先是让AI把界面改成模仿微信 朋友圈,啪一下,很快啊,结果让人非常印象深刻,很逼真

https://f.est.im/est

现在的AI真厉害。让我去调CSS可能这辈子都搞不出来这个效果了。

后面是我的一些唠叨,不感兴趣的可以关闭页面,或者去上面那个围观一下。

想起来独乐乐不如众乐乐,要不,支持个评论功能?

项目的初衷是 static page,要实现互动肯定得用一些API了。

最能想到的思路,走传统的 github issue 什么的,和这个 gitweets 最大的出发点冲突了:一个 git repo 包含所有数据,随时搬家,不用导出。

而且麻烦的是,post 是绑定到 commit 上的。如果你用一个 JSON 之类的来存评论,势必也会新增一个 commit,这样会污染post时间线。

突然想起来一个古老的东西,git notes,这是连 ChatGPT 和 Claude 都没想到的邪路,不过它们很快确认这个办法甚好,可行。

git notes选型定下里,建立这个数据模型让我纠结了很久。围绕 github 开展流程,让我一度误入歧途

  1. oauth 登录,不要任何scope
  2. 用来存 github notes的repo邀请登录人加入项目
  3. 浏览器通过该用户access token发起 notes append

最后想明白了,压根不应该走浏览器这一套。而是只能走后端代劳,用 Fine-Grained PAT 来和 github API 交互

其间还考虑 github 越来越拉垮,想避免 vendor lock-in,直接走git http协议。

首先想到的是 Cloudflare 那个牛逼的 zig 写的 100kb 的 wasm 可以 http 读写任意 git 仓库

git protocol engine is written in pure Zig (no libc), compiled to a ~100KB WASM binary. Support for both v1 and v2 of the git protocol. Support capabilities including ls-refs, shallow clones (deepen, deepen-since, deepen-relative), and incremental fetch with have/want negotiation.

仔细读了下文档,让AI一起调研,发现tmd这玩意仅限 Worker 内部使用,只能读写 CF 内部的 假 git,不支持读写外部任意 git http。

isomorphic-git 坑也挺多 。还是先走 github API 吧

这个 git notes 要走REST API 有查询放大 3+N 的问题,怕掉用次数爆掉,于是让AI走 GraphQL。我自己手动是搓不动 GraphQL,太难了。AI虽然是 flash 普通智商版本,也分分钟拼接好。一次成功。真猛 😭

于是 Vibe 出来了。

搓完了想起一个问题,如果有人刷评论怎么办?于是让AI搓了个 /.admin 管理页面。也是秒写好。太方便了。

明显欠缺的功能搓完之后,感觉又进入了贤者时间,索然无味了。

MiMoCode 干完活儿发通知

2026-07-11 11:53:00

AI在 coding 的时候我其实在玩别的。希望agent 每次干完活,macOS 弹个通知。

手上是 MimoCode,就让它自己写个。啪,很快写好了。结果还是折腾了好一会儿, 记录几个有意思的小坑。

首先是如果当前CLI是活动的,就不弹通知。

需要判断活动窗口。最初用 osascript

osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'

直接报错 Not authorized to send Apple events to System Events (-1743)

于是换 lsappinfo,走 macOS Launch Services API,不需要任何额外授权:

lsappinfo info -only name $(lsappinfo front)
# → "LSDisplayName"="Terminal"

还能拿 PID:lsappinfo info -only pid $(lsappinfo front)

然后如何判定活动窗口是不是 CLI?

写死个["iTerm2", "Terminal", "Alacritty", "kitty", ...] 列表

太笨了。当前hook是子进程,直接遍历 parent 进程树啊!找到终端模拟器的 PID,再跟前台 app 的 PID 比对。

但是在 tmux 里爬出来是这样的:

node → zsh → tmux → launchd(1) → init(0)

Terminal.app 的 PID 根本不在树上。因为 tmux server 启动后被 reparent 到了 launchd,跟 Terminal.app 断开了父子关系。

又想到一个办法,直接查 $TMUX_PANE 是否是当前 active pane:

tmux list-panes -F '#{pane_id} #{pane_active}' | awk '$2==1 {print $1}'

如果 $TMUX_PANE == active pane ID,说明用户就在看这个窗口,不需要通知。完全不需要知道前台是哪个 app。

非 tmux 场景才用进程树 + lsappinfo 的 PID 比对。

然后就是挑选具体哪些事件要弹通知了。

权限通知:permission.ask,但文档说 "not yet wired"。试了一下,确实没触发。尴尬。雷军!!!金凡!!!

最后的方案:注册 permission.ask 占位,如果哪天接入了就能精确捕获。目前靠 tool.execute.before 抢占并推断。工具跑完了说明权限已过或不需要。

还有个情况就是 Subagent 完成也给我哐哐弹。最初硬编码了 SUBAGENT_TYPES = ["general", "explore"];后来发现 actor.preStop / actor.postStop 的 input 里有 mode: "subagent" | "peer"。于是就做了个计数器

最后通知到时候带上 Session 名字,折腾了一圈 直接 sqlite3 ~/.local/share/mimocode/mimocode.db "SELECT title FROM session WHERE id = '$SESSION_ID'"

完整代码放在

https://github.com/est/snippets/blob/master/mimocode-hooks/notify-done.ts

复制到 ~/.config/mimocode/hooks/ 就可以试试效果

写作能力和 locate cost

2026-07-09 23:01:00

自从自个儿琢磨出 locate cost 之后便开始关注这方面问题。最近看到两篇喷 harness 问题的

第一个是 Can Bölük https://blog.can.ac/2026/02/12/the-harness-problem/ 今年2月的时候发现:

Codex uses apply_patch: It takes a string as input, which is essentially an OpenAI-flavored diff, and instead of relying on a structured schema, the harness just expects this blob to follow a strict set of rules
Claude Code (and most others) use str_replace: find the exact old text, swap in the new text. Very simple to think about. But the model must reproduce every character perfectly, including whitespace and indentation.
Cursor trained a separate neural network: a fine-tuned 70B model whose entire job is to take a draft edit and merge it into the file correctly

如果你在 Codex 用别的模型

Grok 4’s patch failure rate in my benchmark was 50.7%, GLM-4.7’s was 46.2%.

Aider’s own benchmarks show that format choice alone swung GPT-4 Turbo from 26% to 59%, but GPT-3.5 scored only 19% with the same format because it couldn’t reliably produce valid diffs.

The Diff-XYZ benchmark from JetBrains confirmed it systematically: no single edit format dominates across models and use cases. EDIT-Bench found that only one model achieves over 60% pass@1 on realistic editing tasks.

懒得看原文的我直接说结论:大家都在争论哪个模型编程更强,但很多模型都知道要改什么,失败其实发生在具体改哪里。

他做了个实验,同样的 16 个模型,只换编辑这个 tool call,改成他自己发明的 hashline,给每行内容打一个短哈希做锚点,Grok Code Fast 1 从 6.7% 直接跳到 68.3%。

Can Bölük 这个老哥非常生猛,2021年有篇博客讲他在Intel CPU 发现一个指令可以序列化/反序列化打印所有 x86 指令集。微码立功了!

他这个 hashline 也很巧思,我也是最近才琢磨明白。你仔细想就会有个疑问,为啥不直接用 行号?

有个相关的问题一直困扰我。我经常让 Gemini 去搜我博客,我博客网址都是类似 stderr-XX 其中 XX 是数字,然后 Gemini 经常把别的文章内容给我总结批判一番。我得到的结论是 LLM 不识数。

后来在学 RAG embedding vs BM25 的时候突然顿悟了,tmd 这个基于 基于语义空间的相似度匹配 有利有弊。好处是比如你检索 汽车,它能联想到 车辆,无需 FTS 那里你自己要维护一套分词近义词表。坏处是行号 223 233 它觉得很「近似」直接搞混 😂


扯远了。

然后是前几天 Armin Ronacher 的 https://lucumr.pocoo.org/2026/7/4/better-models-worse-tools/

这老哥是 Flask/Werkzeug/Jinja2作者,现在主要在撸 Pi 这个agent(值得一题的是上面的老哥在撸 oh-my-pi 这个 fork)

他发现 Opus 4.8 和 Sonnet 5 在非 Claude Code 的 harness(比如他自己的 Pi 项目)里调用嵌套 edits[] 数组时会莫名其妙地塞进一堆乱七八糟的 <antml:function_calls> 这种内部控制字符。老的模型反而没这个毛病。他推测是 A\ 家训新模型强耦合了 Claude Code。逆向发现 Claude Code 客户端对格式错误极其宽容,有一整套别名映射、Unicode 修复、静默过滤多余字段的逻辑。结果就是模型在RL中适应了格式差不多就行,反正harness 会兜底。


看完这两篇我觉得印证了我前面 locate cost 一问的所有猜想。AI 指出,真正可能的机制更朴素:

RL 训练信号里,整段重写 往往比 精确定位再小改 更容易拿到奖励。重写不会因为空白符不匹配而报错。这跟第二篇里 Armin 讲的模型在宽容的 harness 里学会了偷懒,是同一个因果链条,不需要引入纹理/结构的形而上区分也能解释。

也就是说,LLM上课答题只给答案分,不给过程分,导致背题偷懒 🤣 这个方向在研究界是有名字的,叫 process supervision / process reward model,过程奖励模型。OpenAI 那篇《Let's Verify Step by Step》基本就是在数学推理场景做这件事。

但这条路有两个真实的代价,其一是 过程标注比结果标注贵得多;其二 如果训练时的 harness 比部署时的 harness 更宽容,学出来的好过程标准本身就是错的。

所以又回到一个老生常谈的话题。各大模型厂家都在推出自己的 CLI。观察「过程」比最终结果更值钱!

我让AI去 fact check了下。果然

Claude Code

数据使用文档里明确写着两条完全独立的通道:

  1. consumer 账号里"Help improve Claude"那个 toggle 控制的是"conversation content"——如果你打开它,用于训练的数据 包括整个相关对话,连带任何内容、自定义样式或对话偏好。这是"代码/对话内容"这条线。
  2. DISABLE_TELEMETRY 一条完全独立的遥测通道,文档原话是:Claude Code 会从用户的机器连接到 Anthropic,记录延迟、可靠性、使用模式这类运营指标。这类日志不包含任何代码或文件路径。关掉这条通道要单独操作,跟训练开关是两个开关、两套机制、两份文档。

Kiro(AWS)

设置页面里直接摆着两个并列开关

  1. Content Collection For Service Improvement,关掉它就是不许训练
  2. Usage Analytics And Performance Metrics 官方描述是"这是一个单独的、用于使用遥测的设置"。

也就是说厂商自己都承认这是两套独立治理的东西——只是大多数用户可能只会想起关第一个开关。

Codex(OpenAI)

官方文档列出了它 OTel 遥测会上报的事件类型,其中包括

  • codex.tool_decision 工具调用是被批准还是拒绝,以及这个决定来自配置还是用户
  • codex.tool_result 耗时、是否成功、外加一段输出片段
  • codex.user_prompt 默认只记录长度、内容会被打码

听起来很克制,但 工具决策 + 输出片段 + 时长 这几项拼起来 就是前面说那种 过程信号,不是代码本身。精确刻画了模型在 harness 里怎么试错、被拒了多少次、跑了多久。这条 OTel 通道是靠单独的 config.toml 开关控制的,跟 ChatGPT 账号层面的训练数据开关是两件事。

Google Antigravity

方向比较模糊,它把训练相关的退出开关本身命名为 Enable Telemetry,把两件事的名字焊在一起。

Google Groups 的讨论帖里用户在问这个开关到底关不关得掉训练,官方也没给出干脆的回答。


所以接下去的推论就很简单。利用公开语料能训练出 2025年级别的sota llm。但是往后就看各家谁能拿到更多的轨迹数据了。无论靠CLI / app 装机量,还是买数据,偷数据,各显神通。其中装机量/DAU几乎正比于以后的智力天花板。所谓的 trillion tokens models 估计就是从各种日志里来的(而不是人类语料)

继续推演下去,有意思的一点是,可预见的将来,AI 的智力增长几乎全来自于coding

因为 coding 有个编译器师爷能把关,保证产出可验证!

别的什么 具身 世界模型,我觉得难了 😆

还有一个考虑的,装机量看 2C,各行业应用 2B 也很重要。比如design类的。这种“轨迹” 如何收集改进也很讲究。

但是design想了下又挺主观的。不过可以降低一些看上去很笨的地方。

甚至如果从公平的角度来说,AI厂家,除了按成本收费之外,还应该给高价值数据返钱才对。不是之前有报道说Anthropic 和 OpenAI 都签了七位数金额的 RL 环境和人类专家数据合同,预计投入还要再涨 3-5 倍。就是拿来训练“过程”的吧。


今晚娃又沉迷pad,我给他pad锁了 😆

为啥说起这个呢,我一直让娃坚持写「语音日记

但是孩子长大了,他写得越来越不耐烦了,而且我苦恼作文没有批改,所以这个日记习惯实际上成了低水平重复。

其实把复制粘贴到deepseek,提示词 “以XX年级的标准点评改进下这篇” 就能搞定,奈何我家娃太懒。

所以我一直想给娃弄一个 作文训练 app。本来想不就是AI一问一答批改么。

但是想到 locate cost 突然觉得有点难。。。。。甚至比AI coding 还难。。

代码为什么定位相对容易。 哪怕 str_replace 因为空白符不匹配而报错,它要定位的目标本身是离散、有边界的——一行代码、一个函数,有语法(AST)天然把文档切成可寻址的单元。

或者 hashline,行号就每句话一个稳定锚点,把找位置从模糊的文本匹配变成精确的 ID 查找。这招完全可以照搬到「改病句」场景。做个 diff 也容易

但写作的问题,往往根本不是一个可以圈起来的line或者span,而是句子之间关系的性质。 “这段论证缺乏内在逻辑”,“这句话和上一句衔接生硬”,“全文的语气从第二段开始飘了”——这些反馈即便你精确指出“第3段第2句”,真正需要改的可能是第1句、连接词,或者整段重组。

代码的 bug 通常局限在一个可编辑单元里,作文的"bug"经常是分布式的、关系性的。定位到具体文字之后,改哪、怎么改这一步反而更模糊,比代码多绕一层

AI说,写作其实分两个层次

  1. 可验证层
    语法错误、拼写、用词重复、被动语态滥用、句长方差、可读性指标(类似 Flesch-Kincaid 这类公式)、有没有明确主题句
    这些跟代码的 compiler/linter 是同一类东西,规则可判定
  2. 不可验证层
    论证有没有说服力、有没有原创视角、语气是否统一、是否“有意思”?
    可以叫“品味”层。只有经验丰富的人的判断。而且专业阅卷老师之间对开放式作文打分的一致性本身就不高

怎么切入呢?

后者也有一些实践,比如借鉴 AP 阅卷没,用锚定范文(anchor papers)做少样本参照。而不是让模型凭空判断“这篇好不好”

也可以做高亮 + 提问式 而非直接改。比如第 2 段第 3 句话里,“非常开心”这个词能不能换一个更具体的?比如描述一下你当时的表情或动作?

甚至可以 示例驱动:AI 给出 改前 / 改后 小对比,只改 1-2 处;然后 多轮对话,孩子自己决定要改哪里,AI 只辅助,而不是 AI 主导大改。

要么就局部训练,针对常见作文类型(记事、写景、议论),开头、过渡、结尾分别训练

这样看来,写作文直接给娃一张无限大的白纸并不好。参考 Notion 的 Block 概念,强迫孩子在输入时就把“骨架”和“血肉”分开。比如,第一步只允许输入 3 个论点(形式);第二步再针对每个论点去填充素材(质料)。通过产品机制,人为制造出“伪 AST(语法树)”。

或者阻断直接生成,只给“反向约束”。不输出完整的句子让孩子抄,扮演“刁钻的苏格拉底”。比如,当孩子写“今天我很开心”,AI 的反馈不应该是“你可以改成:今天我心花怒放”,而应该是抛出环境约束(Harness):“你当时手里拿了什么东西?你的心跳有多快?”——逼迫孩子自己去完成“从潜能到现实”的推导。

当然,做好 diff 和版本控制是基础。记录孩子打磨一句话的过程。让孩子直观看到词汇的微调是如何让语义的边界越来越清晰的。

哎,这么一拆解又有点思路了,但比预计的感觉麻烦得多啊。不过语文教育有大问题啊。明明是工程上可以细化训练的(虽然很难,过去没AI需要大量人工精力)。上课根本不讲。全靠孩子天生悟性。以上种种,今年4月才喷过一篇《语文学习和考试

作文从小学一上来就300 字 500字,其实真应该「刻意练习」的是语言 primitive。什么 铺垫、呼应、留白、对比、节奏、悬念、感官、动作、心理、对话等等,都上手了,然后再各种变化,组合。

学编程也是从少量 keywords ,赋值语句,条件,循环这样一步一步来的嘛。

过去没太好的语文教学条件,归根结底因为一个班上50个娃只有一个语文老师。

但是现在有LLM了。

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点也睡着了 😂