删除了150行道歉

TL;DR:我的AI代理有一个246行的指令文件用于管理Linear中的问题。其中150行是变通方法:硬编码的UUID、对curl的回退、“CLI不支持X"的注释。我没有重写它们——而是构建了一个让它们变得不必要的工具。现在那150行变成了零行。 你是否曾经写过一份指令文档,它的长度本身就证明了有什么地方不对劲? 我指的不是合理的文档。我指的是那些开头说"使用工具X”,然后花80%的篇幅解释工具X什么时候不工作以及应该如何替代的文件。那些实际上是为本应构建的工具道歉清单的指令。 我有一个这样的文件。而且很令人尴尬。 150行垃圾的解剖 背景:我与一个AI代理(Claude Code)合作,它管理我在Linear中的问题。为了让代理知道如何操作,我有一个技能文件——一个代理在需要创建、列表或更新问题时会读取的指令文件。 这个文件有246行。其中约100行是合理的文档:存在哪些命令、有哪些团队、使用哪些标签。这是合理的。 其他150行是防御性垃圾。三个类别: 约30行硬编码的UUID。 我使用的CLI不支持--project。所以技能文件在XML表格中包含了17个UUID(5个团队+12个项目)。代理必须找到正确的UUID并手动构建GraphQL突变来分配项目。一个应该是--project Tokamak的操作需要记住一个36字符的UUID。 约25行对curl的回退。 CLI没有搜索功能。没有按项目过滤。创建时没有项目分配。三个基本操作,三个嵌入GraphQL查询的curl块,引号转义,以及认证头。每一个都是等待代理吃掉引号的定时炸弹。 约15行"不支持X"。 五个"CLI不支持"的警告和两个"必需"(每次列表时的–sort和–no-pager)。注意这点:我在工具使用指令中记录工具的缺陷。这就像汽车手册花三页解释雨刷只有在先敲击仪表板后才能工作。 约80行防御性上下文。 整整一节标题为"何时使用API而不是CLI"。目录→UUID映射表。选择标签的启发式方法。当CLI挂起时该怎么办的规则。这些材料存在的唯一原因是工具无能为力。 “小心台阶"的标志 当一个工具有不舒适的界面时,自然的反应是记录变通方法。你写指令。你放警告。你创建一个"常见错误"部分。文档越详细,你就越相信问题已经解决了。 但实际上没有。你放了一个"小心台阶"的标志,而不是修复台阶。 当那些指令的用户是LLM时,问题就成倍增加了。人类读到"不支持–project"会记住(多多少少)。LLM读到它,处理它,三轮对话后还是会使用--project。这不是因为它笨——而是因为它优化完成任务,而--project是分配项目的逻辑路径。禁令在信号的海洋中是噪音。 我在另一篇文章中写过这个问题:对LLM的冗长指令完全等同于放置标志。LLM忽略它们不是因为叛逆。它忽略它们是因为它的功能是找到最直接的路径,而"不要使用–project,而是在这个表格中查找UUID,然后用这个GraphQL查询做curl"不是直接路径——这是一个粗糙的修补。 解决方案不是更好的技能文件 我本可以用更好的指令重写技能文件。更清晰的。有例子的。有图表的。我本可以从246行增加到400行并覆盖每个边缘情况。 这就像扩大标志。 我所做的是构建lql——一个用Rust编写的CLI,专门设计让AI代理(或人类,但主要是代理)可以与Linear交互而不需要生存手册。 设计理念是一句话:错误的路径不应该被禁止,应该是不可能的。 换句话说:你不在文档中禁止--status——你让它工作。你不记录--project在create中不存在——你让它存在。你不维护UUID表格——你自动解析名称。你不提供curl回退——工具没有做不到的事情。你不写"必需:–sort”——你设置合理的默认值。 消失的东西 这是我删除的清单: 防御性垃圾 删除的行数 删除原因 硬编码的UUID(17个ID) ~30 lql自动解析名称 对curl + GraphQL的回退 ~25 lql原生支持search、project、relate “不支持X"的注释(5个) ~15 代理期望的一切都存在 “必需"标志(2个) ~5 合理的默认值,没有必需标志 “何时使用API vs CLI"部分 ~15 没有"vs”——lql什么都能做 上下文→UUID映射表(XML) ~20 从TOML配置自动检测 启发式和防御性规则 ~40 工具是容错的,多余了 总计 ~150 剩下的是合理的文档:存在哪些命令,有哪些团队,使用哪些标签。零变通方法。零道歉。 为什么有效(有趣的部分) 行数的减少很引人注目,但这不是重点。重点是_为什么_那些行是多余的。 旧技能文件中的每行变通方法都存在是因为底层工具是脆弱和不宽容的。脆弱是因为它在合理输入面前失败(--status而不是--state)。不宽容是因为它拒绝而不提供替代(--project不存在,自己想办法)。 当你用容错工具替换脆弱工具时,指令_自动_简化。你不必重写手册——手册自己重写,因为不再有什么需要警告的。 这是解释为什么iPhone手册有10页而打印机手册有200页的同一原理。不是苹果写更好的文档。而是iPhone不需要你解释如何装纸、对齐打印头或清洁滚筒。 容错工具生成简短文档。脆弱工具生成生存手册。 当用户是LLM时,这更加重要。每行指令都是可能被误解、遗忘或矛盾的一行。有150行变通方法的技能文件给它150个错误遵循变通方法的机会。没有变通方法的技能文件给它…零个出错的机会。 ...

2026年3月26日 · Fernando

为什么我的CLI输出不是XML(以及我如何无意中重塑了TOON)

TL;DR: 当你的主要消费者是LLM时,XML和JSON因在每个元素中重复结构而浪费token。一个紧凑的定位格式能够将使用量减少50%。结果发现这种想法早已有了名字:TOON(面向Token的对象标记法)。同样的选择压力——有限的token和重复的键值——导向了同样的解决方案。 Anthropic几乎所有东西都用XML。他们的_system prompts_被包裹在<instructions>标签中,示例在<example>标签中,工具列在<function>标签中。如果你在用Claude,就会发现自己到处都是标签。 于是当我让Claude为一个专为LLM设计的CLI输出格式时,显而易见的诱惑是:XML。既然模型以XML形式接收信息,CLI不给它返回XML难道不是更顺理成章吗? 但实际上,这并非最佳选择。 问题:强迫症般的重复 想象一下,你的CLI正在列出一个项目跟踪器中的任务。假设你有50个任务。在XML中,每个任务会是这样的: <issue> <id>PROD-587</id> <state>Backlog</state> <labels>backend</labels> <title>从NAS备份中导入会话</title> <age_days>14</age_days> </issue> --- 看起来很美观,也很自描述。每个字段都有名字,一个XML解析器可以准确识别出每个字段的含义。 现在,把它乘以50个任务。`<issue>`、`<id>`、`<state>`、`<labels>`、`<title>`、`<age_days>`这些标签将被**重复50次**。它们没有提供任何新的信息,仅仅在浪费空间。大约每个任务需要70个token,列出50个任务需要约3,500个token。 JSON稍微好一点(去掉了闭合标签),但仍重复键值: ```json { "id": "PROD-587", "state": "Backlog", "labels": ["backend"], "title": "从NAS备份中导入会话", "age_days": 14 } 每行都重复"id":、"state":等键值。每个任务约需50个token,总计2,500个token。 如果我们去掉这些重复会怎样? PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d) 25个token。没有键值、没有标签、没有大括号或引号。50个任务只需1,250个token。 请注意这一点:相比JSON减少了一半,比XML减少了三分之二。目的却完全相同。 “但LLM需要结构” 这时很多人会怀疑:“那LLM怎么知道每个字段代表什么呢?” 这是一个合理的问题,而我的回答是:就像你知道的一样。 看看这行内容: PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d) 你需要别人告诉你PROD-587是ID吗?需要解释[Backlog]是状态吗?需要说明长横杠后的内容是标题吗?不需要。你通过位置和视觉格式就可以推断出来。 LLM做的事情完全一样。它们是一种识别文本模式的机器。一个一致的、定位明确的格式——ID放在第一、状态放在括号里、标签列在标题之前——对它们而言是直观的。不需要<state>Backlog</state>这种标签说明“Backlog”是个状态。 关键在于区分两种看似相似但完全不同的操作: 阅读是LLM的工作。它有上下文,理解语义,可以推断结构。就像一个人在浏览报告时不需要每个词都用标签标注——位置和惯例已经足够。 解析是程序的工作。它没有上下文,不理解语义,需要显式的分隔符来提取字段。jq '.state'需要"state"这样的键值因为它不知道什么是状态。 XML和JSON是为“解析”而设计的。它们是机器之间交流的格式,它们不“理解”内容。而LLM是“阅读”的。显式结构对它们来说是冗余的,这种冗余却消耗了额外的token。 格式选择:什么时候用什么 我并不是说XML和JSON不好。但它们并不适合这一情境。简单来说,用表格展示: 格式 每个任务的token数 自描述性 最适合 XML ~70 完全 SOAP API、配置文件、有schema的文档 JSON ~50 完全 REST API、服务间数据交换 JSONL ~50 完全 脚本、jq工具、数据流水线 定位式格式 ~25 否 LLM、人类、紧凑信息面板 规则简单:如果消费者能够理解上下文(人类、LLM),就可以省去显式结构。如果消费者无法理解上下文(脚本、解析器),才需要明确的键值。 ...

2026年3月26日 · Fernando

对抗式编程:当你的AI助手凭空编造出API

TL;DR:你的AI可能会凭空臆造出合理但不存在的API字段。解决办法不是祈祷它正确,而是:在编写代码之前下载真实的_schema_,使用API的实际返回值作为_fixtures_,并将_fetch_与数据处理分离,以便无需网络测试代码。这就是对抗式编程:假设你的AI助手会撒谎。 你是否有过这样的经历:写了针对某个API的代码,一切都编译通过,测试也没问题,逻辑看起来合理……但连接真实API时,完全行不通? 如果你独自开发,这往往是因为你读错了文档。而如果你和AI一起编程,会是因为AI编造了那份文档。 不像“幻觉”的幻觉 我当时正在用Rust构建一个命令行工具(CLI),用于与某个GraphQL API交互。我让我的AI助手为实现一个按优先级排序的过滤器编写代码,它返回了这样一段: query { issues(orderBy: { priority: ASC }) { nodes { id title priority } } } --- 整洁、合理、完全符合预期。唯一的问题是:这个API的`orderBy`字段并不接受`priority`作为值。实际的枚举类型叫做`PaginatedOrder`,合法值包括`createdAt`和`updatedAt`,而不是`priority`。 我是如何发现的?当API返回一个毫无意义的400错误时,我花了20分钟才弄明白问题不在我的代码,而是因为我使用了一个**根本不存在的字段**。 ## 总是相似的模式 这并不是个例。在几周的开发过程中,AI一再出现类似的“幻觉”: - **不存在的过滤字段**——比如建议`state.id.or`代替实际的`state.type.in`。听起来合乎逻辑,但API使用了完全不同的模式。 - **凭空捏造的枚举**——提供的值名称看起来合理,但实际API中从未定义过。 - **完全错误的生态系统方法**——在Rust项目中,AI建议使用`fcntl.flock`来处理文件锁定。这是Python的做法,而在Rust中应该用`fs2::FileExt`。 这些错误具有一个共同点——**它们都看上去非常可信**。没有一个错误显而易见。一个新手程序员在粗读文档后可能会犯下完全相同的错误。这正是危险所在:它们不像“幻觉”,更像是对领域知识“一知半解”的代码。 ## 为什么LLM会编造API? 简单来说:LLM不知道你的API具体有哪些字段。在训练中它可能见过成千上万的GraphQL APIs,而当你要求它使用其中一个时,它就像一个有着良好直觉但没有文档的开发者:**猜测**。 而且它猜得挺好,足以让你信服。然而偏偏这个“差不多”就足以让你的项目进度完全崩盘。 这就像和一个从不看文档但总是自信满满的聪明同事一起工作。对方会无比肯定地告诉你:“是的,这个端点接受名为`priority`的字段。”你完全信了,结果运行时发现它是编造的。 ## 解决方案:对抗式编程 在经历了一周内三次类似的“幻觉”后,我采取了一种不同的策略。从“先信任,后验证”,改为**“先怀疑,先验证”**。我称之为**对抗式编程**:假设你的AI助手总是会编造一些东西。 这不是敌意,而是一种开发安全手段。 ### 1. 编码前进行Schema自省 如果你在使用GraphQL API,请在向AI提出任何需求之前,下载真实的Schema: ```bash # 下载API的完整Schema curl -s https://api.example.com/graphql \ -H "Authorization: token-here" \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { types { name fields { name type { name kind ofType { name } } } } } }"}' \ > schema.json 现在你有了真实的内容。当AI告诉你“使用orderBy: { priority: ASC }”时,你可以在Schema里查找priority字段并验证。然后将相关部分的Schema片段交给AI,明确告诉它“只使用这些字段”。猜测就此终结。 ...

2026年3月26日 · Fernando

Linear Agent 不是你需要的。你的代理早就在终端里

TL;DR: Linear 推出了一个集成的人工智能代理。听起来不错,但它并没有解决开发者在终端操作 coding agents 时的痛点。我们需要的不是另一个代理,而是一个可靠的 CLI,我们现有的代理可以直接调用。如果要重写,那就用 Rust——这就是 lql 的由来,一款专为 Linear 设计、面向代理的 CLI。 昨天,Linear 宣布了他们的人工智能代理。这是一个集成到应用中的聊天机器人,能够理解你的 roadmap,你的 issue 和你的代码。你可以在 Slack 上与它对话,在评论中@提到它,它会综合上下文,建议行动方案,甚至直接为你创建 issue。 听起来很棒。真的,很棒。 尽管如此,当我读到这个公告时,我的第一反应是:“这不是我需要的东西。” Linear 的大冒险 为了让你明白我的意思,我需要先讲讲背景。我和 Linear 的关系就像一部委内瑞拉肥皂剧一样,是一段充满了爱恨交织的故事。 第一幕:MCP 服务器。 Linear 曾经有一个 MCP 服务器,供人工智能代理与其交互使用。它的表现就像是在飓风里点打火机:技术上是能点着火,但火焰从来维持不到两秒。断断续续、缓慢,偏偏总是在关键时刻掉链子。最终我直接把它卸载了。 第二幕:GraphQL API。 于是唯有通过 GraphQL 直接和 Linear 沟通。没错,它确实能用,直到你需要在某个 issue 的描述中加入特殊字符,结果这些字符的转义问题会让你重新思考自己的整个人生。某一次,我花的时间转义一个括号比写 issue 所描述的代码还要长。 第三幕:Linear CLI。 然后 linear CLI 出现了,这是一个由社区开发的项目。brew install schpet/tap/linear 然后直接运行。一个第三方工具,朴素、不显眼,但正是我需要的工具:能够直接在终端创建、列出并更新 issue,而不需要与 GraphQL 或那个让人发疯的 MCP 作斗争,也没有弹窗干扰。 我甚至专门写了一篇文章 讲述自己如何用这个 CLI 解放了工作流。一个简单的 bash 脚本帮我在不到一分钟内创建了 49 个 issue。如果用 MCP,我可能会花上一个半小时。 进入代理 现在 Linear 推出了他们的代理。这款产品承诺:一个可以理解你的工作空间、与你的代码连接并自动化你的工作流程的集成助手。 ...

2026年3月25日 · Fernando

转化即制胜:Google如何通过改变坐标将LLMs压缩至6倍

乘法很难,加法很简单。 任何小学生都知道这一点。但他们不知道的是,对数的存在正是为了利用这种不对称性:通过将乘法转换为加法,在简单的世界中操作,然后逆转这种变换。结果正确,而努力则大大减少。 这种模式——把问题转化到一个易于解决的空间,解决问题,然后再转换回来——是整个工程学中最强大的工具之一。快速傅里叶变换(FFT)用它来处理信号,对数用它来处理乘积。而现在,Google发表了一篇论文,提出用它来压缩语言模型。 这个方案叫做 TurboQuant。它的理念优雅得值得我们详细探讨一番。 问题:压缩而不牺牲质量 现代的大语言模型(LLMs)有一个主要瓶颈,而它并不是模型本身,而是它的工作内存。每当一个模型生成文本时,它需要在内存中维护一种叫做键值缓存(KV缓存)的结构——基本上就是模型用来生成下一个词所需的“上下文”记忆。 对于一个具有长上下文的大型模型来说,其KV缓存可以占用几十GB的显存。这是一个严重的问题:GPU的显存非常昂贵、有限,而且需要用来处理其他很多事情。 一个显而易见的解决方案是量化:减少每个数字的精度。比如,代替用32位浮点数(float32)存储每个值,可以用4位甚至3位。这意味着你从一个连续的值域转化为离散的定标值。通常,这种方法效果不错。 直到你遇见了一个陷阱。 规范化常数的陷阱 要对一组数字进行量化,你需要知道它们的范围:它们的最小值和最大值。这些被称为规范化常数,如果需要还原原始值,就必须保存这些完整的常数(16位)。 而问题就在这里:,如果你用3位对每个数字进行量化,但每8个数字你就需要保存一个16位的常量,这些常量会为每个数字额外占用2位。你的“3位”压缩实际上需要5位才能实现。你几乎失去了一半的压缩效果。 这就像是搬家到一套更小的公寓,却发现搬家的纸箱占了新公寓的一半空间。 而你也不能随意增加数据块的大小以分摊这些常数,因为更大的数据块意味着近似度变差——范围加大,你会失去精度。你被困在了两个矛盾的力之间。 这个问题困扰了大家好几年。每个人都试图通过发明更好的压缩算法来解决它,比如自适应块、非均匀量化、混合方案之类的方案。但所有这些方法只是在增加复杂度,带来的效果却非常有限。 然后Google做了一件不同的事情。他们没有发明一个更好的压缩器,他们改变了坐标。 改变坐标系:从直角坐标变成极坐标 TurboQuant的核心思想是,在量化之前先对向量进行随机旋转,然后将结果转换为极坐标。 分步骤来解释。 第一步:随机旋转 想象一下,你在一个高维空间中有一个向量。它的各个分量分布得并不均匀——某些维度的值非常大,而另一些则接近于零。这种不平衡恰恰是为什么你需要为每个数据块保存规范化常数:每个块都有一个不同的范围。 如果你把这个向量乘以一个随机的旋转矩阵会怎样?从几何上看,你是在将向量旋转到一个任意的方向。向量本身保持不变(长度相同,与其他向量的关系也依然保持),但它的各个分量会重新分布。 这时,会发生一个看似魔法的数学现象:在高维空间中,随机旋转会使向量的分量变得几乎均匀。这被称为测地集中现象——在高维空间中,几乎所有分布的质量都会集中在其平均值附近。随机旋转会使这一切变得平滑。 用通俗的语言来说:旋转消除了峰值和谷底。在旋转之后,所有的数据块范围变得相似。如果所有块的范围都类似了……就不需要为每个块单独保存范围了。 第二步:转换为极坐标 经过旋转之后,TurboQuant将向量从直角坐标系(x, y, z…)转换为极坐标(一个半径和多个角度)。 为什么要这样做?因为随机旋转使得角度变得高度可预测——它们被集中在狭窄的范围内且是已知的。这些角度的量化阈值是固定的,不受数据的影响。 这意味着:角度不需要保存任何规范化常数。 之前每个数字要额外占用的1-2位开销现在完全消失。你只需要保存半径(一个数值对应整个向量)和用固定界限量化的角度。 flowchart LR subgraph antes["&nbsp;&nbsp;&nbsp;传统量化&nbsp;&nbsp;&nbsp;"] direction LR V1["&nbsp;原始向量&nbsp;"] --> B1["&nbsp;划分为数据块&nbsp;"] B1 --> N1["&nbsp;计算每块的<br/>最小值/最大值&nbsp;"] N1 --> Q1["&nbsp;量化&nbsp;<br/>3位+2位开销&nbsp;"] end subgraph despues["&nbsp;&nbsp;&nbsp;TurboQuant&nbsp;&nbsp;&nbsp;"] direction LR V2["&nbsp;原始向量&nbsp;"] --> R2["&nbsp;随机旋转&nbsp;"] R2 --> P2["&nbsp;转换到极坐标&nbsp;"] P2 --> Q2["&nbsp;量化&nbsp;<br/>3位,开销≈0&nbsp;"] end antes ~~~ despues --- 精彩之处在于,旋转是可逆的。要恢复原始向量,只需反量化,回到直角坐标系,然后应用逆旋转。结果是,KV缓存的压缩比达到了**6倍**,且没有可测量的精度损失。 ## 为什么能奏效:随机性减少不确定性 这是论文中最违背直觉的地方。**添加噪声(随机旋转)实际上减少了不确定性。** 这似乎是个悖论,但其背后有着优雅的解释。 没有旋转时,每个向量都有自己内部的分布特性。某些维度占主导地位,其他维度是噪声。你无法提前知道每个块的范围,因此必须测量并存储这些范围。 有了旋转,**你强制所有向量都服从相同的统计分布**(测地集中现象的作用)。你无需测量任何东西,因为你提前知道值的分布特性。随机性破坏了每个向量的特定信息(这些信息对你毫无用处),并用可预测的规则性取而代之(这对你非常有用)。 这就像洗牌一副牌。在洗牌前,你不知道牌的顺序——必须逐张查看。而洗好后,你能确定:它们的顺序是均匀随机的。看似无序的洗牌后,在统计层面你对这副牌了解更多了。 ## TurboQuant的成果 Google使用Gemma和Mistral模型在多个基准中测试了TurboQuant,结果如下: | 指标 | 值 | |------|----| | KV缓存压缩 | **6倍**(从32位减少到大约5位,其中量化为3位) | | 注意力计算加速(H100) | **最高达8倍**的*attention logits*计算速度提升 | | 精度 | 在LongBench、RULER、ZeroSCROLLS上无可测精度损失 | | 是否需要重新训练 | **不需要**——无需微调或校准即可运行 | | 数据依赖性 | **无**——与数据无关 | 最后一点尤为关键。大多数量化技术依赖于校准数据集来调整参数。而TurboQuant完全不需要——随机旋转直接起效,因为测地集中现象是空间的属性,而不是数据的特性。 ## 每个程序员都该知道的模式 TurboQuant是一个更大范围内模式的特例,这个模式值得一个名字:**转化即制胜**。 核心思想是:当一个问题很难解决时,先问问自己,你是否站在正确的空间中。有时候,答案并不是更聪明的算法,而是问题的另外一种表达形式。 你早已熟悉的一些例子: | 问题 | 原始空间 | 转换 | 简化空间 | |------|---------|------|--------| | 大数乘法 | 算术 | 对数 | 加法 | | 过滤信号 | 时间 | FFT | 频率 | | 解微分方程 | 时间 | 拉普拉斯变换 | 代数 | | 压缩KV缓存 | 直角坐标 | 旋转+极坐标 | 规整角度 | | 找文本模式 | 字符 | 正则表达式 → 自动机 | 状态转移 | 在每种情况下,困难并不在于问题本身,而在于**表达形式**。改变坐标,难题变得简单。 ## 你今天能应用的 即使你不在压缩LLMs,也可以采用这个模式。下次遇到一个“看似简单但难以解决”的问题时,问问自己: **1. 我在正确的空间中吗?** 如果比较两件事很难,也许应该先规范化后再比较。如果搜索效率低,也许应该建立一个索引(这是为搜索优化的替代表达方式)。 **2. 我的领域中是否有已知的变换?** FFT自1965年以来就存在了。小波变换从80年代就被引入。向量嵌入技术自2013年来到大舞台。许多“困难”的问题已经有标准的变换方案,解决它们。别急着发明新东西,先查查是否有人已经找到了正确的坐标。 **3. 我能通过添加随机性简化问题吗?** 哈希、随机投影、概率性摘要——它们之所以有效,是因为添加受控随机性可破坏不必要的复杂性。如果你的问题有着让你头疼的不规则结构,有时最好的策略是有目的地摧毁这种结构。 ## 下次碰到问题解决不了时 TurboQuant的教训并不仅仅适用于模型压缩。它告诉我们,在发明更聪明的解决方案前要三思,也许你真正需要的只是**改变视角**。 近年来,机器学习领域一直试图通过更复杂的量化算法来解决规范化常数负担的问题。更大的数据块,更多的量化级别,更复杂的启发式方法——一切都停留在直角坐标的框架内。然而改进却十分有限。 Google却另辟蹊径。他们旋转数据,改变了坐标系,问题随之消失。他们不是解决了这个问题。而是**消弭**了它。 下次,当你花费数小时试图用一堆补丁修复一个无法解决的问题时,停下来问问自己:我是和问题本身作斗争,还是和表达形式做斗争?因为如果站在错误的坐标系上,世界上最聪明的算法也救不了你。 --- **参考来源:** [TurboQuant: Redefining AI Efficiency with Extreme Compression](https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/) — Google Research Blog.

2026年3月25日 · Fernando

你的AI写了可以编译但毫无意义的代码(一个linter可以抓住它)

想象一下,你请一个人工来为你制作一个书架。最终交给你的作品看上去很漂亮:架子、螺丝,一切都井井有条。但是你把书架靠在墙上,结果它倒下了。那些螺丝只是道具,看着像螺丝,但其实是塑料做的。 这就是LLM(大语言模型)滥用类型系统时的表现。它交给你的代码可以编译、能通过测试、格式上看起来挑不出毛病。但内部本该是有意义的类型,却被填上了字符串(string)。本该有明确状态的地方,用了nil,而它的意义因读代码的人而异。本该是一个有两个案例的枚举,却被换成了一个== "claude",某天有人可能会写成错拼的== "cladue",但你要等到线上了才会发现。 模型的最爱捷径:万能的字符串 过去几个月,我一直和一个AI助手合作,开发一个中型的Swift应用。它生成的代码干净、结构清晰、命名合理。但有一个模式一再出现:模型尽量避免创建新类型。 这不是因为它懒惰,而是因为创建新类型需要做设计决策,比如:这是一个枚举(enum)吗?有多少个可能值?应该放在哪儿?由谁来导入?而String则无需任何决策。它可以放在任何地方,总是可以编译。 结果就是会生成这样的代码: func sessions(harness: String? = nil) -> [Session] { if let harness { return all.filter { $0.harness == harness } } return all } // 用法: let claudeSessions = sessions(harness: "claude") let codexSessions = sessions(harness: "codex") --- 看起来没问题,确实能运行。但这个代码有三个隐藏的问题: 1. **如果你写错了`"cladue"`,没人会提醒你。**字符串可以是任何内容,但如果是枚举类型,这种错误是可以避免的。 2. **`nil`被用来表示“全部”。** 但这不是定义在某个类型中,而是一种存在于代码注释中、甚至仅仅是你的大脑中的约定。 3. **每个按`harness`过滤的函数都重复了同样的`String? = nil`模式。** 如果你以后想添加一个第三种`harness`,就必须手动搜遍代码中所有的字符串比较。 换句话说,编译器无法帮助你,因为你剥夺了它所有的语义信息。你用一个`String`代替了领域概念。 ## 模型应该写成这样 ```swift enum HarnessID: String, Codable { case claude case codex } enum HarnessFilter { case all case specific(HarnessID) } func sessions(harness: HarnessFilter = .all) -> [Session] { switch harness { case .all: return all case .specific(let id): return all.filter { $0.harnessID == id } } } // 用法: let claudeSessions = sessions(harness: .specific(.claude)) let allSessions = sessions() // 默认是.all — 很明确 现在,"cladue"不会编译。“全部”不再是一个含糊不清的nil,而是一个显式的枚举情况。如果你添加一个第三个harness,编译器会强制你在每个switch语句中处理新情况。它将运行时错误转化为了编译时错误。这才是类型系统本该发挥的作用。 ...

2026年3月23日 · Fernando

你的计划.md 需要一个“魔鬼代言人”(Codex 自荐)

你是否曾经在晚上 11 点写了一个技术计划,确信一切无懈可击,但第二天早上发现忘记添加身份验证? 我深有同感,而且不止一次。最糟糕的不是这个疏忽——而是当你用人工智能来规划时,计划听起来是如此连贯,以至于你的大脑停止了对问题的挖掘。Claude 会生成一个带有章节、依赖项和执行顺序的文档,一切看起来都是高级工程师的作品。但问题是,没有人反驳过它。 Aseem Shrey 发布了一篇文章,精准点出了这个问题,并为此提出了一个优雅的解决方案:让第二个模型审查第一个模型的计划。而且不仅仅审查一次——循环审查,直到审查员说“通过”。 问题:没有人反对你的 AI 当你仅依靠一个模型来规划和执行时,你得到的结果确实连贯但没有争议。AI 不会反驳自己。它不会说“嘿,这个身份验证模型不完整”,或者“你的 shell 脚本中的引用有问题”。 这就像你自己写了一份文档,自我审查,然后对自己说“这可真是完美”。当然是完美的——毕竟是你自己写的。注意这个点:同行评审会存在于科学、工程和医疗领域是有原因的。并不是因为作者愚蠢,而是因为创作者通常是自己作品最差的审查者。 我以前在写我的 Jedi 代码审查委员会时讨论过这个问题。不过当时谈论的是如何审查代码。而 Aseem 的建议是先审查计划,在写出任何代码之前就发现问题。这相当于提前一阶段处理问题。 操作方法:Claude 规划,Codex 审查 这个过程基于 Claude Code 的一个“技能”实现——一个 Markdown 文件,无需额外的基础设施或外部服务。当你调用 /codex-review 时: Claude 会将计划写入一个临时文件。 计划会以只读模式发送到 Codex CLI(它可以读取你的代码库获取上下文,但不改动任何东西)。 Codex 审查后会给出结论:VERDICT: APPROVED 或 VERDICT: REVISE。 如果返回的是 REVISE,Claude 会修正计划并重新提交。关键在于,Codex 会恢复之前的会话,因此能够记住它之前的反馈,并验证问题是否真的被解决了。 审查最多迭代 5 次。在实践中,通常 3 次就足够了。 简而言之,这是两个 AI 之间的一次“拉取请求”,一个提出计划,另一个专注于找漏洞。全程无需人为干预。 3 轮迭代解决了 14 个漏洞 在 Aseem 的文章中,以一个多代理系统的控制面板为案例,循环审查从原计划中找到了 14 个问题: 第一轮(8 个问题):写入端点没有身份验证。Shell 脚本的引用存在错误。数据模式的字段冲突。嵌套数组没有限制。未处理并发问题。仅支持手动测试。 第二轮(剩余 6 个问题):非原子操作的声明。ACL 权限分配过于宽泛。未指定密钥轮换机制。状态建模不一致等问题。 第三轮:所有问题解决,计划获得通过。 ...

2026年3月23日 · Fernando

Mole:那款你不知道自己需要的 Mac 清理工具(但只有两个命令值得用)

你的 Mac 现在有多少垃圾文件,是你完全不知道的? 我不是指那些2019年烧烤派对的重复照片,也不是那个简直快变成垃圾堆的下载文件夹。我说的是构建的临时文件,比如那些你疫情后就再也没碰过的 node_modules 文件夹、你甚至不记得安装过的框架缓存文件、堆积如山的 Xcode 的 derived data(派生数据),就像床底下的灰尘一样。 我好几个月磁盘空间占用达到了85%,每次删东西都跟玩杂技一样小心翼翼。直到我发现了Mole,运行了 mo purge --dry-run,屏幕上弹出了一个数字:17GB可回收空间。整理出来了三百五十个被遗忘的构建临时文件,还在那里静静地腐烂着。 十七个G!完全没有碰到任何照片或文档。 什么是Mole(以及它不是什么) Mole 是一个适用于 macOS 的CLI工具,它声称可以“深度清理并优化你的 Mac”。它带有一些看起来像瑞士军刀一样多功能的命令菜单: mo clean # 清理缓存、日志和临时文件 mo uninstall # 完全卸载应用程序 mo optimize # 检查并“优化”系统 mo analyze # 分析磁盘使用情况 mo status # 系统健康监控 mo purge # 删除项目构建临时文件 mo installer # 找到旧的安装包 mo touchid # 配置 sudo 使用 Touch ID 八个命令。直接告诉你吧:**只有两个命令值得你花费时间**。其余的从“嗯,这个我自己就搞定了”到“绝对不可能执行”都有。 ## mo purge:低调的宝藏 如果你是开发者,并且用 Mac 已经超过六个月,那么`mo purge`会帮你发现隐藏的宝藏。简单来说,它会扫描你的项目目录,找出那些占用空间却对你完全没有帮助的构建临时文件。 都有哪些类型的临时文件?基本上都是常见的“嫌疑人”: - **node_modules**(你几个月没碰的 Node/Bun 项目) - **target/**(Rust 项目) - **build/** 和 **.build/**(Swift/Xcode 项目) - **DerivedData**(Xcode 的派生数据) - **__pycache__** 和 Python 的虚拟环境 - **.gradle** 和 **build/**(Java/Kotlin 项目) - **Pods/**(iOS 的 CocoaPods 项目) 这些文件夹的好处是:可以100%重新生成。如果你某天重新打开这个项目,运行一个`npm install`或者`cargo build`,它们会从零重新创建。但在此之前,这些文件像“赖账房客”一样,白占着你的磁盘空间不交房租。 ### 正确的操作流程 首先,一切都从**干跑模式(dry-run)**开始: ```bash mo purge --dry-run 这个命令会显示找到的内容以及可以释放的空间大小,但不会实际删除任何内容。你需要利用这个阶段检查列表,并确定它不会误删掉你正在使用的项目文件(比如你当前有几个窗口正在打开一个项目的node_modules)。 ...

2026年3月23日 · Fernando

与AI助手共事就像和《记忆碎片》的主角生活在一起

想象一下你有一个出色的工作伙伴。他能解决复杂的问题,编写清晰的代码,对你的要求一听就明白。但是,每天下班后,每次你跟他说“打开课程项目”,他都会一脸茫然地看着你,问:“什么课程?在哪里?” 每天如此,毫无例外。 这就像和电影《记忆碎片》(Memento)的主角Leonard Shelby一起生活。他无法形成新的记忆,因此只能把重要的信息纹在身上,避免遗忘。 我每天都会和Claude Code一起处理四到五个项目。几周内,每次我对他说“去课程项目”或者“打开博客”,他都会像维多利亚时代的探险家寻找尼罗河的源头一样,执行find / -name "p101"命令,用五分钟时间在硬盘上搜索我每天都会使用的目录。 这可真让我抓狂。 问题:数字版的顺行性遗忘症 你的AI助手每次启动都像打开了一张白纸。它不知道你住在哪里,不知道你有哪些项目,不知道~/courses/p101/program这个目录存在,也不知道你的博客在~/code/frr.dev,或者你的Ansible代码库叫wuwei并且位于~/code/wuwei/ansible。 每次新会话,都得从头开始。这就像Leonard早晨醒来发现自己躺在那个汽车旅馆的房间里。 最自然的反应就是每次手动告诉它路径:“在/Users/fernando/code/tokamak。”这种方式虽然有效,但就像每天早晨都要向你的朋友重新介绍自己。用不了三个星期,你可能会开始考虑是不是单独工作会更省事。 现实世界的解决方案:zoxide 在给自己“纹身”之前,先说说如何用工具解决人类的困扰。 zoxide 是一种带有记忆功能的cd命令。通俗点说,它是替代cd命令的工具,可以通过输入部分片段记住你曾去过的目录,并智能跳转到最有可能的目标。 # 不再需要这样: cd /Users/fernando/courses/p101/program # 只需这样: z p101 完成了。zoxide知道当你输入"p101"时,你是要跳转到/Users/fernando/courses/p101/program,因为这是你过去47次输入类似路径时访问过的地方。 它使用的是一种频次与最近性相结合的算法。经常访问且刚访问过的目录会被优先排列,而几个月未访问的目录会被降级。这有点像TikTok的推荐算法,只不过是用来管理你的文件系统。 安装步骤简单 brew install zoxide # 添加到你的shell配置文件(我用的是Fish): # 在 ~/.config/fish/config.fish 文件中 zoxide init fish | source 从此,每次执行cd命令都会让zoxide的数据库更新。而z <模式> 让你无需思考就能跳转到目标目录。 z tokamak # → /Users/fernando/code/tokamak z blog # → /Users/fernando/code/frr.dev z wuwei # → /Users/fernando/code/wuwei/ansible z p101 # → /Users/fernando/courses/p101/program 如果遇到模糊匹配(可能有两个以上符合条件的目录),使用zi命令会调出带有交互式选择器的_fzf_工具。 为AI助手配置zoxide:zoxide query 接下来是好消息。zoxide带有一个query命令,它不会改变目录,只是返回最可能的路径: zoxide query p101 # → /Users/fernando/courses/p101/program 对于AI助手来说,这就是黄金功能。与其在整个硬盘上执行find命令,调用zoxide query可以在毫秒级速度返回正确的路径。无需探索,无需猜测。 ...

2026年3月14日 · Fernando

使用卡尔曼滤波器减少服务器请求(或关于过度设计的愉悦)

我有一个菜单栏应用,需要知道一个数字。一个从 0 到 100 的百分比。为了得到这个数字,我必须每30秒访问一次服务器。 我们来算一笔账:30秒一次请求等于1分钟2次请求,1小时120次,一天工作的8小时里就是960次。每天为了获取一个有时候20分钟都不变的数字发送将近1000个HTTP请求…… 这根本不是监控!简直就是对服务器的骚扰。 真正的问题其实不是技术问题,而是政治问题。 当你依赖一个你无法控制的API时——它既不是公开的,也没有明确的速率限制文件,同时属于一家随时可能变更服务条款的公司——每一条不必要的请求都带着风险。这风险不仅是超时的风险,还有被切断访问权限的风险。 我用的服务端 endpoint 并没有公开的文档支持。它目前是可用的,已经运行了几个月了。但我知道,我发出的每一条请求,都会增加Anthropic某台服务器日志里的条目,假以时日,可能某天他们会决定第三方应用制造的"噪声"已经太多了,直接把我的服务切断。 所以问题的关键不是“怎样更快地轮询”,而是**“怎样尽量少地轮询,同时还不遗漏关键信息?”** 这时候,一个理性的工程师可能写一个简单的if,但一个对自嘲式过度设计充满愉悦感的工程师,可能最终选择实现一个卡尔曼滤波器。 最简单的解决方案(以及它为何失败) 最直接的想法很简单:如果这个数字没变,就别问了。 如果 当前值 == 上一个值: 增加等待时间 否则: 回到30秒轮询 这个方法表现得很差。因为这个数字只有在**你有操作**的时候(比如你发送了消息,或使用了 tokens)才会改变。但在你**没有操作**时,它也会变化——比如系统设置了5个小时的滑动窗口期限,导致过期的tokens自动失效。如果你在其他设备上也在用这项服务,数字也会在你不知情的情况下变动。 所以单靠检查数字是否变化是不够的。你需要的是**预测**它什么时候会发生变化,并以多大的信心预测。 ## 卡尔曼滤波器:五分钟速成 卡尔曼滤波器是一种用来融合两种不完美信息的工具。 想象你身处一个没有窗户的房间中特别想知道室外的气温。你有两个渠道: 1. **你的心理模型**:例如 “现在是三月的下午三点,气温应该在18℃左右”。这个估计是合理的,但并不完美——可能外面刚下过雨,或者有风。 2. **一个嘈杂的廉价温度计**:你可以走到阳台测量,但这个温度计有±3℃的波动误差。 两种渠道都不完美,但卡尔曼滤波器告诉你:**把这两种信息结合起来,并在不同时刻对更可靠的信息赋予更大的权重。** 如果你刚刚10秒前查看了温度计,你的心理模型就很可靠——完全可以信任它,不用再去测量。如果你已经一个小时没测量了,你的模型就会失去可信度——这时候你得回阳台看看。 关键是**方差**:一个用来衡量“我对当前估计值有多信任”的数值。在刚看过温度计后的瞬间,方差为零;随着时间推移,方差逐渐增大。当超过某个门槛时,滤波器会告诉你:“不可信了,去获取真实数据吧。” 在我的案例里: - **心理模型** = tokens的本地使用情况。我知道Claude Code的使用记录,所以大致可以推算出消耗的token数是多少。 - **测量数据** = Anthropic的API真实返回值。它是当前最准确的信息,但获取每一次数据都有政治代价和电力消耗。 - **方差** = 随时间增长的不确定性。如果我在浏览器或手机上使用了服务,本地模型对此一无所知——也因此预测的准确性会下降。 一个复杂的多维卡尔曼滤波器(带协方差矩阵)用来解决这个问题有点类似杀鸡用牛刀。我的方法更简单:一个单一状态(token使用量)、单一传感器(API返回值)、线性模型(成本/预算比例)。20行代码就搞定了,这是可以完美解决问题的最小实现。 翻译到我的具体需求中: - **预测**: `使用量估计 = 上次真实值 + (本地新增消耗 / 总预算) × 100` - **修正**: 每次服务器返回一个新的真实值,滤波器会将方差重置为零。 - **不确定性**: 方差开始按照时间线性增长。`σ = √(Q × 自上次校正起的秒数)`。 ## 关键点:滤波器自动决定何时请求 这里正是过度设计显得合理的地方。卡尔曼滤波器不仅仅能估计值,它还能**决定何时需要获取真实数据**。五条规则,每个“tick”执行一次: | 规则 | 触发条件 | 原因 | |-------|-----------|---------| | 窗口已重置 | `now ≥ resetsAt` | tokens过期了,之前的值失效。 | | 高不确定性 | `σ > 5%` | 对预测值信任度低,必须确认。 | | 范围边界触发 | 置信区间触碰到80%、95%或100% | 临近重要阈值,用户必须知道。 | | 邻近性触发 | `使用率`接近边界8% | 有可能跨越范围,但本地估计未能察觉(比如在其他设备使用服务时)。 | | 安全超时 | 15分钟无真实数据返回 | 预防性措施,监控软件的偏执有时是优点。 | 如果没有规则触发,卡尔曼滤波器会说:“没问题,我全都掌握”——于是应用**不会发起HTTP请求**。显示给用户的值是本地估计值。 重点来了:本地估计不需要**网络消耗、功耗或风险**。它完全基于内存中的数学运算。 ## 数字对比:前后变化 小范围内token消耗稳定的一天(中度使用,较少峰值): | 场景 | 请求/小时 | 请求/天(8小时) | |---------|:-:|:-:| | 固定30秒轮询 | 120 | 960 | | 使用贝叶斯估计 | 15-30 | 120-240 | | 估计 + 休眠 | 4-10 | 30-80 | 相比之前的960次请求,网络请求减少了**75-97%**。对于“只是偶尔请求数据”的任务来说,不算太糟吧? ...(内容会继续翻译完整)

2026年3月12日 · Fernando