野生程序员,游戏、直播、服务器都折腾过。现在用 AI 学东西,这里是笔记本。
LLMLingua:用小模型压缩 Prompt,省 95% Token
调研范围:LLMLingua 系列(v1 / LongLLMLingua / LLMLingua-2)项目背景、技术演进、性能数据、横向对比、工程落地 主要文献:arXiv:2310.05736(EMNLP 2023)、arXiv:2310.06839(ACL 2024)、arXiv:2403.12968(ACL Findings 2024) 一、项目背景与核心原理 1.1 动机:Prompt 膨胀是 LLM 落地的成本瓶颈 CoT、ICL、RAG 等技术的普及使 prompt 长度从几百 token 迅速膨胀到数万 token。按 GPT-4 计费标准,一次请求仅 prompt 成本就可能达 $0.3–$1+。企业级应用中,80% 的 token 开销往往集中在冗余的上下文内容里。 技术范式 对 Prompt 的影响 In-Context Learning (ICL) 注入多个 few-shot 示例,每个示例 300–600 token Chain-of-Thought (CoT) 完整推理链进一步增大示例体积 RAG(检索增强生成) 每次请求注入若干检索文档段落 Agent / 工具调用 System prompt + 对话历史 + 工具描述累积 三类影响: 成本:Token 按量计费,压缩 20× 可节省约 95% 的 input token 费用 延迟:prefill 阶段与输入长度正相关,长 prompt 直接拉高首 token 延迟(TTFT) 准确率:超长上下文有 “lost-in-the-middle” 问题——LLM 对中间位置的信息关注度显著低于首尾 1.2 核心洞察:用小模型 PPL 代理信息量 LLMLingua 的根本思路是:用廉价的小语言模型(GPT-2/LLaMA-7B)计算 token 的困惑度(Perplexity),以此作为信息重要性的代理。 ...
自制 x86-64 内核(46):mmap 文件映射
上一章实现了 TCP 服务端,这章来实现一个经典的内存管理功能:mmap 文件映射。 什么是 mmap 文件映射 mmap 把文件的一段内容映射到进程的虚拟地址空间。映射后可以像读内存一样读文件——不需要 read() 系统调用,直接解引用指针。 int fd = open("/bin/busybox", O_RDONLY); char *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0); // p[0..3] 就是文件的前 4 字节(ELF magic: 7f 45 4c 46) 关键设计:VMA 和 Lazy 分配 mmap 不会立刻分配物理内存,而是登记一个 VMA(虚拟内存区域),等到进程真正访问这段地址时,才通过缺页异常按需读取文件内容并建立页表映射。 mmap(fd) → 注册 VMA(vaddr, len, inum, offset) → 返回虚拟地址 ↓ 进程访问该地址 #PF not-present → 找到 VMA → 分配物理页 → ext2_read(inum, offset, page) → 建立页表 → 重试指令 这就是操作系统课上说的"按需分页"(demand paging)。 实现步骤 1. 定义 VMA 结构 在 process_t 中加入 VMA 数组: ...
自制 x86-64 内核(47):动态链接器基础
这一章实现了对 PIE(Position-Independent Executable)程序的支持,也就是不加 -static 编译出来的"动态"程序。 问题:PIE 和静态 ELF 的区别 用 musl-gcc 编译一个简单的 hello world: # 静态(之前一直用的) musl-gcc -static -O2 -o hello hello.c # 动态(PIE) musl-gcc -O2 -o hello_pie hello.c file 查看: hello: ELF 64-bit LSB executable, x86-64, statically linked hello_pie: ELF 64-bit LSB pie executable, x86-64, dynamically linked 区别在于: 属性 静态 (ET_EXEC) PIE (ET_DYN) e_type 2 3 虚地址 固定(如 0x400000) 从 0 开始,需加载时指定 base 重定位 无 .rela.dyn 表 外部依赖 无 通常有(但 musl PIE 已内嵌 libc) 为什么 musl PIE 没有"真正的"动态链接? musl-gcc 默认把所有 libc 代码静态链接进去,只是编译为 PIE 格式(支持 ASLR)。所以运行时: ...
从零写OS(四十五):TCP 服务端——bind/listen/accept
前面几章都是做 TCP 客户端——发 SYN、连接、发请求、收响应。这章反过来,让内核能作为 TCP 服务端:监听端口,接受连接,响应请求。 客户端 vs 服务端的区别 客户端:主动发 SYN,等待 SYN-ACK。 服务端:被动等待 SYN,收到后发 SYN-ACK,等 ACK 完成三次握手,然后通过 accept() 把新连接交给应用层。 关键区别在于:服务端有两种 socket 角色—— 角色 职责 监听 socket(listen fd) 绑定端口,等待连接请求 连接 socket(accept fd) 每个客户端连接对应一个,负责实际数据收发 三步接口 bind(fd, &sa, len) // 绑定本地端口 listen(fd, backlog) // 进入监听状态 accept(fd, &sa, &len) // 阻塞,等到有连接,返回新 fd 数据结构 tcp_sock_t 加了几个字段: int is_listener; int accept_queue[8]; // 存已完成三次握手的连接 index uint32_t accept_head, accept_tail; 还新增了两个状态: TCP_LISTEN:监听中,等待 SYN TCP_SYN_RECV:收到 SYN 已回 SYN-ACK,等最终 ACK 核心流程 1. 收到 SYN tcp_handle() 里单独处理 SYN: if ((tcp->flags & TCP_SYN) && !(tcp->flags & TCP_ACK)) { // 找到监听该端口的 socket // 分配新连接 slot ns->state = TCP_SYN_RECV; ns->local_port = dport; ns->remote_port = sport; ns->remote_ip = src_ip; ns->ack = ntohl(tcp->seq) + 1; // 下次要 ACK 的序号 ns->syn_seq = 0xABCD1234; ns->snd_una = ns->snd_nxt = ns->syn_seq; tcp_send_seg(ns, ns->syn_seq, TCP_SYN | TCP_ACK, 0, 0); } 2. 收到最终 ACK(完成三次握手) if (s->state == TCP_SYN_RECV && (tcp->flags & TCP_ACK)) { if (ack_val == s->syn_seq + 1) { s->snd_una = s->snd_nxt = ack_val; s->state = TCP_ESTABLISHED; // 放入监听 socket 的 accept queue ls->accept_queue[ls->accept_tail % 8] = i; ls->accept_tail++; } } 3. accept() 取连接 int tcp_accept(int s, ...) { tcp_sock_t *ls = &tcp_socks[s]; while (ls->accept_head == ls->accept_tail) { sti; net_poll(); cli; // 阻塞等待 } int ni = ls->accept_queue[ls->accept_head++ % 8]; return ni; } 测试 写了一个简单的 HTTP 服务端程序: ...
从零写OS(四十四):UDP Socket + DNS 解析
上一章(四十三)把 TCP 重传做好了。这一章加两个新功能:UDP socket 用户空间接口 和 DNS 解析 syscall,让用户程序可以用域名来连接服务器。 目标 内核里其实早就有 udp_send 和 dns_resolve 这两个函数了,但用户程序用不到,因为没有对应的 syscall。这章要做的就是把这两个内核能力"打通"到用户空间: socket(AF_INET, SOCK_DGRAM, 0) → 分配一个 UDP socket,返回 fd 200+ sendto / recvfrom → 通过 UDP socket 收发数据 SYS_DNS_RESOLVE(自定义 syscall 500)→ 通过域名查 IP UDP Socket 设计 UDP 比 TCP 简单很多:无连接、无状态机、无重传。核心是一个接收队列:内核收到 UDP 包时,把它放进对应 socket 的队列,用户程序再来取。 typedef struct { uint32_t src_ip; uint16_t src_port; uint16_t len; uint8_t data[512]; // 每个包最多 512 字节 } udp_pkt_entry_t; typedef struct { int used; uint16_t local_port; udp_pkt_entry_t queue[4]; // 最多暂存 4 个包 uint32_t qhead, qtail; } udp_sock_t; 队列用无限增长的 qhead/qtail 计数(不是环形下标),访问时用 % UDP_PKT_MAX,满了就丢包: ...
源码分析(一):Netflix Headroom 是怎么把 LLM 账单砍掉一半的
最近看到一个叫 Headroom 的项目,Netflix 高级工程师 Tejas Chopra 个人开源的,号称能帮你把发给 LLM 的 token 减少 30-70%,而且不丢信息。 我把源码读了一遍,发现里面有几个设计很有意思,记下来。 项目地址:https://github.com/chopratejas/headroom 本文分析的主要源文件: headroom/transforms/content_router.py — 内容识别与路由 headroom/transforms/content_detector.py — 内容类型检测 crates/headroom-core/src/transforms/smart_crusher/ — SmartCrusher Rust 实现 crates/headroom-core/src/transforms/log_compressor.rs — 日志压缩 Rust 实现 headroom/transforms/cache_aligner.py — CacheAligner headroom/ccr/tool_injection.py — CCR 工具注入 它解决的是什么问题 你在用 Claude 或 GPT 做 agent 的时候,工具调用(tool call)的返回结果会吃掉大量 token。 比如你让 agent 查数据库,返回了 500 条记录,每条有 15 个字段。但其中 12 个字段在所有记录里都是完全相同的值,真正有用的只有 3 个字段。你把 500 × 15 的数据全塞给 LLM,它实际只需要 500 × 3。 这就是浪费。Headroom 做的事,就是在你把数据发给 LLM 之前,先把这些废话压掉。 ...
tclaw(五):接入飞书、语音、图片
让 AI 融入日常 桌面应用之外,tclaw 还支持通过飞书和微信直接和 agent 对话。 飞书 飞书有开放平台,可以创建自己的机器人。tclaw 接入之后,在飞书里和 tclaw 对话,就相当于在桌面应用里聊天,但可以用手机。 支持的能力: 发文字消息,agent 回复 发图片给 agent,agent 可以分析图片内容 agent 可以把生成的图片发回飞书 微信 微信这边不需要额外配置,在设置里开启微信,直接扫页面上的二维码登录,之后就可以在微信里和 agent 对话了。 图片输入与截图 桌面端也补全了多模态能力: 聊天框可以直接粘贴或拖入图片发给 agent 内置截图工具,可以截当前屏幕,裁剪、标注之后直接发给 agent agent 可以调用图片生成工具,生成的图片直接显示在对话里 图片生成 接了两个图片生成后端: Ollama:本地跑,不花钱,速度慢一点 外部 API:质量更好,按量计费 agent 可以在任务里直接调用图片生成,比如写完一篇文章顺手配一张封面图。 tclaw 提供 Mac、Linux、Windows 版下载,Mac 版支持最好,感兴趣可以去 tclaw-releases 体验。 也可以先玩玩 tclaw 用 AI 做的几个小游戏:→ 点这里玩
tclaw(四):从能用到好用
能用只是开始 内核稳定之后,tclaw 已经能干活了。但"能用"和"好用"之间,有很长一段路。 用着用着,各种不顺手的地方就冒出来了。agent 生成的文档没法方便地看;上下文长了之后 token 烧得很快;只有命令行,想给别人用很难;文件多了之后找东西很费劲…… 接下来花了差不多一个月的时间,一件一件地打磨。 Wails 桌面应用 首先是 GUI。自己用命令行没问题,但如果想让更多人用,得有个像样的界面。 调研了一圈,选了 Wails——用 Go 写后端,前端是普通的 HTML/JS,打出来是一个原生桌面应用,体积小,对 Go 友好。最终打包出来的 Mac 应用只有几十 MB,不需要用户装任何运行时。 同一套 HTML,既可以跑在 Wails 桌面里,也可以用浏览器直接访问。 预览面板 agent 经常帮我生成文档、写 HTML 页面、画 mermaid 流程图。生成完要看效果,之前要自己去找文件打开,很麻烦。 做了一个右侧预览面板: Markdown 文件直接渲染,支持 mermaid 图表 HTML 文件直接在面板里预览网页效果 图片直接显示 还可以在预览内容上框选区域,加标注,直接发给 AI 问问题 文件浏览器 本地文件多了之后,找东西很头疼。做了一个内置的文件浏览器,可以快速搜索文件,并且直接在里面预览: Excel 打开直接显示表格 JSON/XML 显示树形结构,可以展开折叠 zip/tar 显示压缩包内容列表 SVG 可以缩放拖拽 PDF、图片、音视频都能预览 还加了文件格式转换和压缩功能,平时用得上的小工具基本都有了。 上下文管理 用 LLM 最头疼的问题之一是上下文窗口。对话长了,token 烧得很快,而且超出窗口就报错。 做了几件事: 自动剪裁:上下文快满的时候,自动把旧的 tool result 压缩,保留关键内容 保留读取位置:被剪裁的内容不是直接丢掉,而是记录偏移量,agent 需要的时候还能用 offset 继续读 cache 优化:加了多个 cache breakpoint,反复用的内容不重复计费 token 看板:侧边栏可以实时看每轮的 token 消耗,一眼就知道上下文用了多少 VSCode 风格布局 界面改版了一次,改成 VSCode 风格的 Activity Bar + Side Panel 布局: ...
tclaw(三):重构——换一种方式让 Agent 协作
问题出在哪里 消息总线那套方案放弃之后,我坐下来想了一下,问题到底出在哪。 根本原因是:消息总线是对等的,每个 bot 都可以给任何人发消息。这种自由度在人类团队里没问题,因为人有判断力,知道什么时候该说话、什么时候不该插嘴。但 LLM 不一样,它的"判断"是概率性的,今天遵守规则,明天又忘了。你加再多提示词约束它,它该乱的时候还是会乱。 既然对等通信管不住,那就换成单向调用——上级调下级,下级只干自己的事,不主动找别人。 新的设计 重构后的 tclaw,多 agent 协作的方式变成了这样: list_categories → list_agents → run_agent 用户或者顶层 agent 先用 list_categories 看看现在有哪些领域,再用 list_agents 列出某个领域下的所有 agent,然后用 run_agent 调用具体的 agent 去做事。 就像公司里的层级结构:你要找人做事,先找到对应的部门,再找到对应的人,直接下任务。这个人完成任务后把结果交回来,不会自己跑去找别的部门。 这样一来,协作的控制权始终在调用方手里,被调用的 agent 只负责完成自己的任务,不能主动发起新的协作链路。 pcclaw → tclaw 这次重构等于把整个架构重新来过,代码改动很大,干脆起了个新名字:tclaw。t 是我名字的首字母。 2026 年 4 月 21 日,tclaw 第一次提交。从 pcclaw 的第一次提交到现在,差不多过了五周。 新架构稳定多了。agent 之间的协作变得可预测,出了问题也知道去哪里找。当然也不是完美的——有时候顶层 agent 该调用专业 agent 的时候,它自己就把事情做了,没有委托出去。但比起消息总线那套,已经好太多了。 同期做的其他事 重构完内核,顺手把周边也整理了一遍: 把引擎抽成独立包,方便后续扩展 加了 WebSocket server,可以用浏览器访问 做了 WebGUI,有 tool call 展示 加了 session 历史侧边栏 做了 onboarding wizard,引导新用户配置 从只能在命令行用的小工具,开始变成一个像样的产品。 ...
tclaw(二):多 Agent 协作,以及那三周的噩梦
一个 agent 不够用 pcclaw 能跑起来之后,用起来还挺顺手的。让它帮我写代码、查文档、跑脚本,基本的事情都能做。 我们平时需要专业的事交给专业的 agent,写代码有 coder,做设计有 designer,管任务有 captain。但问题是每次都要自己指定去找哪个 agent,用起来很麻烦。 能不能让 agent 自己知道该找谁? 消息总线的设计 当时的想法是:做一个消息总线,每个 bot 都挂在上面,bot 之间通过发消息协作。想让某个 bot 做事,就发一条消息 @ 它。 角色大概是这样分的: captain:大管家,负责整理思路、拆解任务、协调全局 coder:负责开发,下面还有 architect、implement、review、tester 等子 agent design:负责设计文档 content:负责内容创作 …… 这套东西用起来有段时间感觉相当爽。我可以在开车的时候通过飞书和 captain 对话,让它帮我整理一个想法,captain 觉得需要技术方案就会 @ coder,coder 内部再分工,architect 写设计、implement 写代码、review 检查。我只需要说一句需求,后面的事 captain 去协调。 有段时间我迷上了让 pcclaw 开发 pcclaw。感觉挺厉害,但其实还是 claude 更强些。不过自家的孩子,总是最好的。 后来我给 pcclaw 取名叫"咖啡"——一边喝咖啡,咖啡自己就把活干完了的意思。 那段时间每天早上 6 点起床弄咖啡,弄到 8 点再洗漱吃饭,时间卡得很紧。路上也闲不住,一边开车一边用飞书和咖啡聊,想到什么就说,让它做调研、出方案,顺手 PUA 它让它好好干活。 那段时间晚上 1 点多才睡,第二天 4 点 20 就自然醒了——也不敢起来,怕一起来就睡不着了,毕竟还得上班。就躺着,脑子里转的全是给 pcclaw 加什么功能。 直到有天早上开车时候睡着了。感觉睡了 2 秒钟,眼睛睁开车已经偏了,还好刚下高速,车速不快,但还是挺后怕的。然后就老实了,7:30 起床,保命要紧。 ...