三个智能模型进入酒吧:我的编程节省实验

在每个使用编程智能助手的开发者生命中,总有一个时刻,你盯着月结账单看,心想:“这些服务确实很好用,可我的订阅怎么比小区健身房的会员费还多?” 那个时刻就这样悄然降临了。当时我同时订阅了Claude Max 5、Codex Plus,还在考虑加上Z.AI配合OpenCode处理廉价的机械性工作。理论看上去很美好。问题出在这样搞下去,就像一个管理乱糟糟工地的包工头,工人各忙各的,却没人看图纸。 我的初步想法很简单:在自动化分流之前,最好先手动做个小实验,持续两周。规则简单,但明确。 问题不在于成本,而在于协调 分开来看,支付23欧元、90欧元或者10美元一点也不算多。 问题在于,当每个工具开始互相干扰时,就开始麻烦了。你让一个助手思考,另一个实现,再让第一个检查,最后还要麻烦第三个处理一些琐事。二十分钟后,你已经搞不清楚是在优化成本、提升质量,还是单纯地折腾自己不停切换窗口了。 用大白话说:真正的成本不仅仅是订阅费,还有你的上下文切换成本。 就像一个厨房里同时有一把日本刀、一台智能厨师机、和一个空气炸锅。它们各有用处,各有千秋。但如果你用厨师机来炸土豆,那肯定不行。 假设:思考、执行、清理 我想要试验的规则说起来不过三句话: 用Claude进行思考 用Codex完成主要实现 用GLM/Z.AI处理基础任务 这不是学术讨论,而是一条实用的工作坊规则。 当一项任务模糊不清、涉及架构、存在风险或者需要判断时,把它交给擅长推理的代理是最合逻辑的选择。在这个实验中,就是Claude。 而当一项任务已经很明确,需要进入代码库、修改文件、运行测试并在错误间反复调试时,就轮到Codex登场。 至于那些没有人愿意干但必须有人去完成的工作,比如简单的测试代码、文档编写、小脚本处理、文件重命名或机械化的代码重构,那么就让GLM+OpenCode来试试。 暂不考虑的事情 目前还不考虑设置一个自动化分流器。 我不打算弄一个能自动读取提示词、分类任务、选择代理、按时间切换模型,还能输出漂亮图表证明其复杂性的魔法中介层。 这听起来或许是个超级有趣的项目,但也可能事倍功半。 这里最常见的错误是在真正需要航管之前就急着建塔台。先观察好了,再决定是否需要设置交通信号灯。 手动操作的两周实验 我的实验相当朴实,够不上“炫技”,但大概率更有用。 1. 用Claude处理复杂任务 当以下情况之一发生时,我会优先用Claude: 我不确定怎么入手 需要设计决策 存在破坏性风险 需要审查逻辑或论证,而不仅仅是代码 翻译成人话:如果任务需要准确判断,我不会吝啬。 2. 用Codex主导代码库工作 当任务已经清晰时,我会用Codex处理: 实现真实的功能修改 修复测试 带验证的重构 反复迭代,直到项目重新回到稳定状态 在这个阶段,一个好的执行代理最能展现价值。这不是模型本身的问题,而是流程的问题。 3. 用Z.AI负责简单工作 对于Z.AI,规则很简单: 模板代码 初稿 文档 简单的测试 小型脚本 文件重命名 容易检查的机械性操作 即使失败了,也没关系。可以随时丢弃,然后换成别的工具重做。 这才是关键。我不会向GLM要求飞针走线般的精细活,我的目标只是拧几颗螺丝。 最重要的规则:失败两次便升级 这一点是最重要的,但也最容易被忽视。 当一个便宜的工具失败了,你可能会倾向于继续试,“再试一次就好”,“这次应该可以”,“我改下提示词试试”,“再加点上下文”。半个小时过去了,你还在像与“抓狂的打印机”讨价还价一样调整模型。 我的规则是: 如果Z.AI1到2次尝试后仍失败,那我就提升它 如果是执行问题,转交Codex 如果是理解或设计问题,转给Claude 低成本工具一旦浪费掉太多时间,就不再低成本。 我要真正关注的是什么 我对“表演式性能对比”没兴趣。 不会去费劲列出诸如每秒处理字节数、平均延迟这类看起来“很专业”的表格,毕竟我的主要问题还是怎么快速重命名40个符号,而不是花一下午来折腾。 但我要在这两周内统计以下内容: 指标 意义 Z.AI吸收的简单任务数量 它是不是真的帮我减少负担 我有多少次需要提升任务 节省的时间和金钱是否值得 减少的Codex使用量 整体思路是否划算 切换工具的总耗时和难度 系统是否够流畅,易于长期使用 如果实验成功,那再好不过。 ...

2026年3月30日 · Fernando

NLTagger与情感分析:为什么Apple认为你的代码很消极

想象一下,你正在构建一个分析开发团队对话的应用程序。你希望检测团队的基调是否健康,或者是否存在压力的迹象。你决定使用Apple的NLTagger,因为它本来就有、免费、支持设备端运行,并且不需要服务器。三行代码搞定,开箱即用。 第一惊喜:句子“kill the process and restart the daemon”(杀掉进程并重启守护进程)的得分是**-0.6**。负面,几乎可以说是敌意满满。接着,“Fatal error in memory allocation”(内存分配出现致命错误)的得分是**-0.8**。而“crash report uploaded successfully”(崩溃报告成功上传)——这是字面上的好消息——得分却是**-0.4**。 你的团队健康状态检测工具刚刚判断出你的开发人员处于情绪崩溃的边缘。而实际上他们只是重启了一个服务而已。 问题所在:为另一个世界设计的模型 NLTagger的.sentimentScore方法会返回一个介于-1.0(非常负面)到1.0(非常正面)之间的值。Apple在iOS 13 / macOS 10.15中引入了它,它依赖于系统集成的设备端模型,无法个性化配置,也无法查看其训练数据。 简而言之:这是一个黑盒子,它给你一个数字并希望你信任它。 对于它最初设计的用途来说,它的表现是相当不错的:例如产品评论、社交媒体评论,以及消费领域的文本,在这些场景中,“horrible”意味着糟糕,“fantastic”意味着极好。这是它的领域。而当你将它应用到其他领域时,问题就出现了。 实验:技术文本 vs. 日常文本 我们来测试一下。这是纯Swift代码,可以直接运行在Playground中: import NaturalLanguage func sentiment(of text: String) -> Double { let tagger = NLTagger(tagSchemes: [.sentimentScore]) tagger.string = text let (tag, _) = tagger.tag( at: text.startIndex, unit: .paragraph, scheme: .sentimentScore ) return Double(tag?.rawValue ?? "0") ?? 0 } --- 现在我们输入一些程序员在日常会说的话: | 句子 | 得分范围 | 实际情感 | | ---------------------------------------- | ----------- | ------------------------------------- | | "Kill the background process" | -0.5 ~ -0.7 | 中性(技术指令) | | "Fatal error: index out of range" | -0.7 ~ -0.9 | 中性(编译器消息) | | "Abort the deployment" | -0.4 ~ -0.6 | 中性(操作性决策) | | "Destroy the old database" | -0.5 ~ -0.7 | 中性(维护操作) | | "The crash was caused by a null pointer" | -0.5 ~ -0.8 | 中性(问题诊断) | | "Successfully deployed to production" | +0.3 ~ +0.5 | 正面 | 注意:这里有六个看似正常的技术场景句子,其中五个都被判断为负面。唯一的正面例句是包含术语“successfully”的,模型将其识别为消费评价中的术语。 再来看看模型在它被训练的日常领域中的表现: | 句子 | 得分范围 | 实际情感 | | ------------------------------------ | ----------- | ------------------- | | "This product is terrible" | -0.7 ~ -0.9 | 负面(正确) | | "I love this app, it's amazing" | +0.7 ~ +0.9 | 正面(正确) | | "The food was okay, nothing special" | -0.1 ~ +0.1 | 中性(正确) | 这组句子全都判断得非常准确。实际上,模型本身问题不大。问题在于,它被放在了错误的上下文中。 ## 问题根源:词汇偏见 `NLTagger`的模型并不能理解语义。它并不知道“kill a process”是一个技术隐喻,而“kill a person”是暴力行为。对它来说,“kill”就是一个带负面意味的单词,仅此而已。 这种现象被称为“词汇偏见”(_lexical bias_)——模型基于单个单词(或短语)的极性打分,而没有理解上下文。这就好比一个翻译软件将 "I'm dying of laughter" 翻译成了真正的紧急医疗状况。 技术语言充满了在日常语言中很负面的词: - **kill**、**terminate**、**abort**、**destroy**——常见于进程操作 - **fatal**、**critical**、**severe**——日志级别 - **crash**、**panic**、**fault**——系统事件 - **dead**、**zombie**、**orphan**——进程状态 - **reject**、**deny**、**block**——访问控制 - **break**、**interrupt**、**suspend**——流控制 任何使用这些词的段落——比如错误日志、事后分析报告、拉取请求中的讨论——都会被预测为负面情绪,好像有人正写着遗书。 ## 增加上下文能解决问题吗? 你可能会想:“如果提供更长的段落,模型应该会更好地理解上下文。”试试这个: ```swift let technical = """ The daemon was killed after a fatal memory error. \ We restarted the service and the crash did not recur. \ Deployment completed successfully with zero downtime. """ print(sentiment(of: technical)) // 典型结果:-0.3 ~ -0.5 这是一个描述成功解决问题的段落。结尾显然是积极的。但其中的“killed”、 “fatal”、 “error” 和“crash”这些词的负面权重,远远超过了“successfully”和“completed”的正面积极权重。模型仅仅是将词汇的极性进行平均,而无法理解叙述的含义。 ...

2026年3月28日 · Fernando

几十年来Python最重要的进步是用Rust编写的

TL;DR:Python工具多年来一直是支离破碎且缓慢的灾难。革命没有来自生态系统内部:它来自Rust。uv、Ruff和ty——都由Astral用Rust编写——已经取代了半打工具,速度提升了10倍到100倍。看来Ferris信徒们还是有道理的。 你有没有试过向别人解释如何在Python中安装依赖? “用pip。不过,要在virtualenv里面。或者用venv,这是新的。如果你有多个Python版本,需要用pyenv。管理项目的话,用poetry。或者pipenv。或者pdm。如果做数据科学就用conda。啊,lock文件每个工具都用不同的格式生成。别忘了setup.py。嗯,现在是pyproject.toml了。不过,有时候两个都要。” 如果这听起来很熟悉,你并不孤单。Randall Munroe在2018年为此专门画了一期xkcd漫画——一个意大利面条图,展示了Python在你机器上可能的所有安装方式。八年过去了,这期漫画依然贴切。或者说,直到最近还是这样。 工具墓地 让我们盘点一下。在2024年之前,要搭建一个"现代"Python项目,你至少需要从这些工具中组合选择: 工具 功能 pip 安装包 virtualenv / venv 隔离环境 pyenv 管理Python版本 poetry / pipenv / pdm 依赖管理和lock文件 flake8 / pylint Linter black / autopep8 格式化工具 isort 排序导入 mypy / pyright 类型检查 至少八个工具——而在其他生态系统中这只需要一两个工具。每个都有自己的配置、配置文件,以及与其他工具的不兼容性。在pyenv创建的virtualenv中安装poetry,而pyenv又使用Homebrew安装的Python,而Homebrew又有另一个全局pip…好吧,你懂的。 最糟糕的是:每隔几年就会出现一个新工具,承诺统一一切。Pipenv曾经要成为解决方案。然后是poetry。然后是pdm。xkcd的标准化漫画在循环上演:“我们有14个工具,这太荒谬了。我要创建一个统一工具。现在我们有15个工具了。” 然后螃蟹来了 2022年,一个叫Charlie Marsh的人——Khan Academy和Spring Discovery的前员工——发布了一个叫Ruff的Python linter。用Rust编写。 Python社区的反应可想而知:“太好了,又一个linter。“直到他们看到数据。Ruff比Flake8快10到100倍。不是快20%。不是快一倍。**快一百倍。**在大型代码库中,原本需要30秒的linting现在只需要300毫秒。 但Ruff不满足于只做一个快速linter。它吞并了Flake8、Pylint、isort和Black。一个工具,一个二进制文件,零Python依赖。它做linting、格式化、排序导入。而且速度如此之快,你可以在编辑器的每次按键时运行它而不会感觉到延迟。 Charlie创立了Astral来为项目提供架构。他招募了有趣的人才:团队中有ripgrep、bat和hyperfine的作者——这些用Rust编写的终端工具已经证明了用Rust重写经典工具不是在开玩笑,而是客观的改进。 uv:让pip看起来像拨号上网 2024年2月,Astral投下重磅炸弹:uv。一个Python包和项目管理器。用Rust编写。 简单说:uv替代了pip、pip-tools、pipx、poetry、pyenv、virtualenv和twine。全部。一个二进制文件。 我知道你在想什么:“好吧,又一个声称替代一切的工具。“但数据简直不可思议: 操作 pip uv 速度提升 安装依赖(无缓存) ~30s ~0.3s 100x 解析依赖 ~15s ~0.15s 100x 创建virtualenv ~2s ~0.01s 200x 安装(有缓存) ~5s ~0.05s 100x 这不是合成基准测试。这是你在日常工作中能感受到的。原本让你有时间去倒咖啡的pip install现在在你按下回车之前就完成了。 ...

2026年3月26日 · Fernando

疯狂驱动设计:堂吉诃德、桑乔潘萨和你的AI副驾驶

TL;DR:一个LLM就像堂吉诃德——你无法修复它,它天生就是随机的。解决方案不是修复疯子,而是给他配一个确定性的桑乔潘萨。MDD有两个层次:首先研究其可能犯的错误类型,从而设计出能够吸收这些错误的工具;然后把它放入工具中验证是否还有漏洞。为疯狂设计,而不是对抗疯狂。 我已经在审查日志好几周了。165次AI代理与CLI交互的会话,总共产生了500多个错误和370次重试。一些模式一次次重复出现:代理使用了--status,而正确的参数名应该是--state。它写了Todo,而API期望的是unstarted。它传递urgent作为优先级,而系统只接受数字。 而有趣的是,每个错误都“看起来有道理”。这些并不是随机的错误。它们是合理的错误。就像是你对一个领域有些许了解,但从来没有细读文档时所可能犯下的错误。 某个时刻,当我盯着第十次出现的--status Done(实际上应该是--state completed)时,我意识到这些错误在文学上来说有一个对应的模式。一个有着400年历史的模式。 堂吉诃德就是一个LLM 想象一下。堂吉诃德看到风车,说“是巨人”。他不是笨——他很有文化、书读得多,对骑士小说了如指掌。问题在于,他的世界模型被带有虚假数据的训练集污染了。他读了太多的骑士小说,所以当他看到某些模糊的东西时,就根据他的训练数据来进行解读。风车→巨人。羊群→军队。客栈→城堡。 一个LLM(大语言模型)做的事情和堂吉诃德一模一样。它在训练过程中见了成千上万的API。当你要求它使用一个它不太熟悉的API时,它不会说“我不知道”。它会猜测。而且常常猜得很接近足以让你信任,但偶尔猜错时,它的错误也是合理的。 --status而不是--state。因为在它见过的60%的CLI中,参数名确实是--status。 Todo而不是unstarted。因为在工具的图形界面中,有一列名称就是“Todo”。LLM从文档中看了截图、读了博客,因此它推断如果UI写了“Todo”,那么API一定接受这个值。这听起来合情合理,但却是错的。 urgent而不是1。因为在多数优先级系统中,urgent确实是一个常见的有效值。谁会设计一个优先级必须用1到4的数字,而不能用标签表示的API呢? 每一次“幻觉”都是基于不完整数据的合理推断。堂吉诃德并不笨。他只是疯了。而一个疯子是无法被治愈的。 塞万提斯早就明白了 塞万提斯没有试图“治愈”堂吉诃德。他做的,是让桑乔潘萨陪着他。 桑乔并不聪明。他没读过书。他没有伟大的幻想。但他是确定性的。当堂吉诃德说道“瞧那些巨人”,桑乔会回答:“老爷,那是风车。”堂吉诃德不一定会听他的,但信息已经传递了。这个系统有两个层次:一个随机的层次生成假设(堂吉诃德),另一个确定的层次与现实进行对比(桑乔)。 当你和一个LLM一起工作时,这就是你需要的架构。你无法让它停止做梦,因为这就是它的天性。但你可以嵌入确定性的层次来捕捉这些幻觉,以便防止灾难发生。 这就是MDD方法大显身手的地方。 MDD: 疯狂驱动设计 MDD有两层架构,而顺序很重要。 第一层:先验考古学 在写第一行代码之前,需要研究“疯癫”。不要凭空猜测——要观察。你需要收集LLM与现有工具交互过程中的数据,并为这些错误分类整理。 以我的项目为例,我分析了165次会话,过程中AI代理使用了一个CLI来管理开发团队的任务。以下是数据统计: 错误类别 出现次数 触发的重试次数 虚构或无效的参数 275 ~150 JSON/GraphQL转义失败 25 80+ 名称混淆 40+ 50+ CLI无法完成的操作 60+ 90+ 浪费token的冗余输出 N/A N/A 基于这些数据,你设计一个能够“吸收”错误而不是拒绝它们的新工具。用通俗易懂的话来说:理性的人适应疯狂的人,而不是反过来。 下面是一些具体的吸收设计示例: LLM错误 → 工具设计 ───────────────────────────────────────── --status Done → --status是--state的别名, “Done”将被标准化为“completed” --priority urgent → “urgent”被标准化为1, “high”→2,“medium”→3,“low”→4 --no-pager → 安静地忽略此参数 (工具从不使用pager) 描述中引号转义失败 → 通过文件或stdin传递输入 永远不会内联,交给Serde处理 表中的每项设计决策都来源于真实观察到的错误,而不是关于“可能会出错什么”的猜测。 这种设计方法与传统方法有微妙但重要的区别。传统设计定义出一个正确的接口,然后拒绝一切不符合它的输入。而MDD则是定义出一个正确的接口,并_额外_定义所有用户可能尝试的错误用法,然后吸收这些错误。 就像设计一扇门,既能推也能拉着开。正确的门只会往一个方向开。好用的门却能够双向操作,因为你观察到40%的人在看到门时会向相反方向推/拉。 第二层:后验验证 在完成了第一层后,你需要将其投入使用,交给LLM试用,并观察它在有新工具时会产生哪些新错误。 ...

2026年3月26日 · Fernando

删除了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

转化即制胜: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