将 Async Swift 桥接到 Sync C:使用四个函数从任何语言调用 Apple 的 LLM

Apple 的 Foundation Models 框架(macOS 26)提供了一个约 30 亿参数的 LLM,可以在设备端运行,免费,无需 API key。但有个问题:它只能使用 Swift。而且不是普通的 Swift——是 Swift 的 async 版本。 你的工具链是 Python 吗?没有绑定。用 Rust?也没支持。脚本语言 Shell?完全没法用。这个世界上最便宜的 LLM(确实免费)被困在两堵墙后:语言和并发模型。 显而易见的解决方案是搭建一个本地 HTTP 服务器,将模型暴露为 REST API,像 Ollama 那样。但这就像是用大炮打蚊子:一个进程在后台运行,占用一个端口,JSON 来回传递,每次问一个简单的问题都要用到 curl。如果只是给一个 commit 分类为 fix 或 feat,你不需要 HTTP,你需要的只是一个简单的 C 函数。 仅有 4 个函数的 dylib 所以我做了 libfoundationmodels:一个动态库,把 Apple 的框架编译为 .dylib,它仅导出 4 个 C 函数: int32_t fm_init(void); int32_t fm_is_available(void); int32_t fm_generate(system, prompt, output, output_len); int32_t fm_classify(system, prompt, choices, output, output_len); --- 嗯,算上 `fm_generate_json` 是 5 个函数,但核心思想未变:四个概念——初始化、检查可用性、生成自由文本,以及在选项中强制分类。全部同步。全部阻塞。输入缓冲区、输出缓冲区、返回代码。经典的 C 风格。 从 Python 调用它是这样的: ```python import ctypes fm = ctypes.CDLL("libfoundationmodels.dylib") fm.fm_init() buf = ctypes.create_string_buffer(256) fm.fm_classify( None, # 没有系统提示 b"fix: handle nil response in OAuth", # 要分类的文本 b"fix\nfeat\nrefactor\ntest\ndocs", # 选项 buf, 256 ) print(buf.value.decode()) # "fix" 就是这样。没有 requests。没有 urllib。没有 JSON。没有运行的服务器。调用一个 C 函数,它会内部唤醒你 Mac 的 Neural Engine,传入文本,并将结果写回缓冲区。典型延迟:200-800ms。 ...

2026年4月5日 · Fernando

苹果的情感分析认为'删除临时文件'是死亡威胁

NLTagger是苹果的情感分析API。它集成在iOS和macOS中,在设备本地运行,不需要服务器,只需三行代码即可使用。当你搜索"Swift情感分析"时,它是第一个出现的结果。 对于任何开发者在日常工作中会写的文本,它返回以下结果: 消息 NLTagger 实际情况 “delete the temp file” -0.8 中性指令 “ok” -0.8 中性确认 “run make test” -0.6 中性指令 “commit and push” -0.4 中性指令 “great job, thanks!” +1.0 正面(正确) “this is fucking broken” -1.0 负面(正确) 评分范围从-1.0(极其负面)到+1.0(极其正面)。根据苹果的评判,“delete the temp file"与"this is fucking broken"具有几乎相同的情感强度。而"ok”——英语中最中性的回应——得分是**-0.8**。 这些不是编造的数字。这些是在任何运行macOS 14+的Mac上都能重现的结果。 如何重现 import NaturalLanguage let tagger = NLTagger(tagSchemes: [.sentimentScore]) tagger.string = "delete the temp file" let (tag, _) = tagger.tag( at: tagger.string!.startIndex, unit: .paragraph, scheme: .sentimentScore ) print(tag?.rawValue ?? "nil") // "-0.8" 八行Swift代码。结果是确定性的:相同的文本在每次执行、每个设备上都产生相同的分数。这不是随机错误,而是系统性偏见。 ...

2026年4月5日 · Fernando

我花了 $15 百万 token 写了 'fix: typo'

昨天我用 Claude Code 写了一条 commit message。diff 是一行修改:一个注释里的拼写错误。Claude Opus 读取了 diff,思考了两秒钟,生成了 fix: correct typo in auth comment。为此,它用了大约 800 个输入 token 和 30 个输出 token,分别花费 $15 和 $75 每百万 token。总成本:不到一分钱。但是,把这个过程放大到一天 40 次 commits,250 个工作日,一家公司有 200 名开发者使用 coding agents,那么这些看似微不足道的花费会积累成数千美元的开销,付出的只是等同于贴创可贴的智力劳动。 问题并不是 Opus 太贵。问题在于 coding agents 不会区分 $0.001 的任务和 $0.10 的任务。一切都通过同样的大模型执行。生成 commit 信息、分类一个 issue、验证格式 —— 全部使用高成本的模型,就像雇用一位外科医生来贴创可贴。 数据与成本 我们用 Claude Opus 4(上一代,目前大部分生产环境还在用的版本)的价格来算一个账: 任务 输入 Token 数 输出 Token 数 成本 生成 commit 信息(小型 diff) ~800 ~30 $0.014 分类某个 issue ~500 ~50 $0.011 验证 commit 格式 ~300 ~20 $0.006 生成 standup 报告 ~2000 ~200 $0.045 这些任务都不需要一个拥有 2 万亿参数和多步推理能力的大模型。这些仅仅是带有强约束的分类和生成任务。本质上就像是根据颜色对卡片进行分类。 ...

2026年4月5日 · Fernando

你的 Mac 自带一款免费的 LLM,你可能还没用上

你每个月可能花费 20 到 200 美元来访问 LLM(大型语言模型)。Claude、GPT、Gemini,无论哪一个都在烧你的钱。而这些模型从你的脚本和开发工具调用的大多数场景都可归结如下几类: “把这个错误分类到这五个类别之一。” “给这个变量起个名字。” “告诉我这个 commit 是 fix、feat 还是 refactor。” “用两句话总结这段文字。” 与此同时,你的 Apple Silicon Mac 上还坐着一个拥有 30 亿参数的语言模型:全系统集成、无需支付费用、无需网络连接、无需 API 密钥、甚至不需要网络延迟。而你大概根本没用过。 基础模型框架(Foundation Models Framework) 自 macOS 26 (Tahoe) 起,Apple 推出了 Foundation Models Framework,允许访问 Apple Intelligence 支持的语言模型。这是一个原生的 Swift 框架,适用于 macOS 26、iOS 26 和 iPadOS 26,兼容所有支持 Apple Intelligence 的 Apple Silicon 设备。 值得注意的不仅仅是它免费——这的确很棒,更特别的是它生成 Swift 类型化输出。它不会返回需要你用正则表达式和上帝保佑才能解析的 String,而是直接输出一个结构体(struct)。 import FoundationModels @Generable struct CommitClassification { @Guide(description: "The type of change") @Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore"])) let type: String @Guide(description: "One-line summary of the change, max 72 chars") let summary: String } --- `@Generable` 宏告诉框架在编译时生成架构(schema)。语言模型利用这个架构生成结构化输出。`@Guide` 则限制了可能的值——通俗地说,你给模型铺好了轨道,它就不会出轨。 以下是用法示例: ```swift let session = LanguageModelSession(instructions: """ You are a commit message classifier. Given a git diff, classify the change and write a summary. """) let diff = "..." // 你的 git diff 数据 let result = try await session.respond( to: "Classify this diff:\n\(diff)", generating: CommitClassification.self ) print("\(result.type): \(result.summary)") // "fix: handle nil response in auth flow" 就是这样。没有 URLSession(网络请求),也没有 API 密钥,也不需要解析 JSON,不需要 try? JSONDecoder().decode(SomeType.self, from: data)。整个模型在设备内部运行,依托于你 Mac 的 Neural Engine(神经引擎),并返回 Swift 类型数据,编译器将会为它进行类型检查。 ...

2026年4月4日 · Fernando

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

在每个使用编程智能助手的开发者生命中,总有一个时刻,你盯着月结账单看,心想:“这些服务确实很好用,可我的订阅怎么比小区健身房的会员费还多?” 那个时刻就这样悄然降临了。当时我同时订阅了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

在Codex中,技能不是/命令(在Claude Code中几乎是)

TL;DR:如果你在使用Codex,command(命令) 用于控制会话或应用程序,而skill(技能) 用于教代理一种工作方法。在Claude Code中,目前的文档已经将技能视为可以用/skill-name直接调用的东西,所以这两种概念在Claude Code中更为融合。但在Codex中恰恰相反:types可以作为技能存在,但/types却可能不存在。 当你从Claude Code切换到Codex时,这种混淆非常常见。而且可以理解。 你创建了一个名为types的技能,回到终端时满怀信心地输入/types……然后Codex看着你,就像你在五金店里要了一杯拉格啤酒。 问题不是技能坏掉了。问题在于,在Codex中,技能和命令并不是一回事。 请注意,这种差异并非表面上的不同,它会彻底改变你设计工作流的方式。 一个让你秒懂的类比 想象一下,Codex就像是一架有两层结构的飞机。 第一层是驾驶舱:按钮、杠杆、指示器。这是命令的栖息地,用于改变会话、客户端或工具的状态,这是操作控制。 第二层是副驾驶手册:流程、标准、检查清单、避免陷阱的指南。这是技能的领域,用于改变代理思考和执行任务的方式。 通俗地讲: 命令是动驾驶舱里的按钮。 技能是改副驾驶脑中的手册。 如果你把手册当成按钮来用,那肯定行不通。 什么是Codex中的命令 在Codex中,命令有两种形式,千万别搞混。 第一种是CLI命令: codex login codex exec "run tests and fix failures" codex resume --last codex apply --- 这很直接明了。这些是应用程序的操作:登录、运行任务、恢复会话、应用变更。如果明天系统中没有了模型,这些命令依然有意义。 第二种是**交互式会话中的斜杠命令**: ```text /model /permissions /personality /agent /status 它们也并非是“华丽的提示语”。它们控制的是实时的会话:更改模型、调整权限、切换人物风格、设定活动线程或改变可见状态。这些就像驾驶舱中的控制按钮。 OpenAI事实上非常清晰地记录了它们的用途:一方面,有专门的斜杠命令页面用于“在交互会话中控制Codex”的说明;另一方面,另有一份独立的技能页面,将技能定义为可重用工作流的创建格式。 这也是为什么有些操作会成为命令而不是技能:因为它们需要可预测性、即时响应和稳定语义。你不希望模型“有创意地解释”/permissions的含义。你只希望它直接更改权限。就这么简单。 什么是Codex中的技能 Codex中的技能是完全不同的东西。它就是一个可重用的工作流,用来教给代理何时运用某种方法、如何思考某个任务并按照特定步骤操作。 这里还有一个细微但重要的区别:OpenAI表示,skill是一种编写格式,而**plugin(插件)**是可安装或分发的单元。换句话说,你会先将工作流设计为技能;如果需要分享或进行封装,就可以把它打包成插件。 明确的例子: $types $improve $owasp $blog 或者,如果更偏语言化: 使用types来审计这个代码仓库 使用improve来检查这个diff 你在这些例子里并不是叫Codex去“切换设置”。你是在告诉它,“当我让你执行这项任务时,请按照这套剧本来操作”。 以我的types技能为例,它不应该是个按钮。它的工作是读取项目代码、检测语言、检查模型、寻找“字符串类型化的代码”、判断一个Optional的使用是否合理并是否能正确建模域状态。这需要背景和判断能力,正是技能擅长的工作。 同理,improve作为一个技能也讲得通:检查diff并不是什么机械性的操作,反而涉及到判断、上下文和优先级的权衡。 为什么在Claude Code中“看起来像是一样的” 这里就是认知的陷阱了。 最新的Claude Code文档对这一点已经毫不避讳了。它谈到技能时,会告诉你可以通过以下方式直接调用: /skill-name 也就是说,在Claude Code中,你认为的某些属于“可重用工作流”的东西是通过斜杠命令语法来调用的。用户体验上,它将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

早上用Claude,下午用Codex:原来我需要的这个双AI助手工作流

TL;DR: 在165次Claude Code会话和27次Codex CLI会话后,我发现了一个清晰的模式:早上使用Claude来处理需要头脑风暴的互动型任务,下午让Codex完成可自动执行的具体任务。这不是哪个更好的问题,而是何时使用哪一个的问题。数据显示,两者结合远胜于单独使用任何一个。 早上九点,手握一杯咖啡,我脑中只有一个模糊的点子,准备重新构建一个模块。我不知道确切该怎么做,只是确信当前的模式并不理想。 于是我启动了Claude Code。 并不是因为它是“最好的”选择——而是因为我需要一种“自言自语”的方式,一个能理解上下文的助手。我告诉Claude:“这个服务的职责太多了,帮我拆分它。”随即,一场对话开始了。它提出一种分离方案,我讨论修改它,我让它探索另一种可能性,它在动手之前还会展示diff。这是一种协作的过程。 三个小时后,模块被成功拆分,测试通过,设计也有所改进。但这是我的Claude配额的40%。而我还列了一堆机械性任务,比如更新集成测试、清理无用的import、重整目录结构。 然后我打开了Codex。 我给它任务,设置为全自动模式,然后悠闲地去吃午饭。 数据揭示的模式 几个月来,我使用一款菜单栏应用监控自己的使用情况,这款工具记录了会话、token用量及耗时。以下是统计数据: Claude Code: 165次会话,160,893条消息,28,052次工具调用。使用模型:Opus 4.5和Opus 4.6。从缓存中读取了56亿token。 Codex CLI: 27次会话。使用模型:GPT-5.4。模式:danger-full-access,审批策略:never。 令我意外的首先是Claude Code的使用时间分布: 09:00 1 ▏ 10:00 5 ██ 11:00 10 ████▌ 12:00 14 ██████▎ 13:00 7 ███ 14:00 16 ███████▏ 15:00 21 █████████▍ 16:00 15 ██████▋ 17:00 18 ████████ 18:00 13 █████▊ 19:00 14 ██████▎ 20:00 9 ████ 21:00 10 ████▌ 22:00 8 ███▌ 我70%的Claude Code会话发生在下午,仅22%在上午。 初看这些数据,我还以为自己“清晨用Claude”的理论行不通。但深入分析每个时间段的任务类型后,一切都渐渐清晰。 上午用于探索,下午用于执行 Claude Code的上午会话少但时长较长。这些会话属于设计阶段:“这部分功能应该如何运作?”、“帮我审阅这个计划”、“能不能提供一些其他选项”。这些是一次又一次的讨论,需要反复修改观点,最终达成决定。 ...

2026年3月26日 · 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