
模型跑分提高,和它更适合一起工作,并不是同一件事。
一个编码 Agent 可以更快定位错误、完成更长的任务,也可能在需求有歧义时替人选定方向。它交付得更快了,人却不敢离开屏幕:每隔几分钟就要检查它是否扩大范围、改变计划,或把某个未写出的假设当成事实。
本文由《听懂 AI》第 006 期整理而成。节目主要讨论 Mun Logadan 于 2026 年 8 月 14 日发布的个人文章《Why does Opus 5 feel worse to work with?》,并补充 Anthropic 的 Opus 5 发布说明和 Hacker News 社区讨论。原文描述的是作者及同事的使用感受,不是模型对照实验;关于训练和 benchmark 的解释也被作者明确标为推测。
Mun Logadan 并没有说 Opus 5 能力倒退。相反,他认为它比 Opus 4.7、4.8 更有能力,benchmark 表现也很强。让他不舒服的是协作方式:
这会产生一种反直觉的体验:模型更能完成任务,人却需要更仔细地看守它。这里的证据只是个人观察。它能说明一种真实存在的使用问题,不能证明所有用户都会遇到,也不能据此给 Opus 5 的整体能力下结论。
Anthropic 在 2026 年 7 月 24 日发布 Opus 5 时,把它描述为更主动、更适合长时间多步骤工作的模型。官方公布了 Frontier-Bench、CursorBench、OSWorld 等结果,并列出大量早期客户反馈,其中一些特别称赞它会验证工作、发现隐患,或只在需要人类判断时把人拉回来。
这些材料证明了 Anthropic 想优化的方向,也提供了具体使用案例,但仍主要来自厂商评测和早期客户引述,并不是独立的用户体验研究。个人文章关注的又是另一种场景:任务文本没有写全,隐性业务约束很多,选错方向的代价高。
同一种“主动性”,在两类任务里可能得到相反评价:
所以争议不一定是谁对谁错。双方测量的对象不同:一个更接近“模型能否完成”,另一个更接近“人是否放心让它完成”。
“重构登录模块”看起来是一句完整需求,实际可能牵涉旧客户端兼容、审计要求、埋点协议、上线窗口和客户承诺。这些信息可能散落在代码、文档、工单和人的记忆里,不会自动进入提示词。
模型可以写出结构漂亮、测试全绿的新实现,却仍然删掉某个不能改变的旧行为。问题不一定是它不会写代码,而是它不知道自己缺少了哪些背景。
现实任务还经常没有唯一正确答案。两个方案都能运行,但预算、团队经验、发布节奏或维护责任会改变选择。Agent 如果继续执行,就相当于替项目负责人做了技术之外的取舍。
原文猜测,强调 benchmark 的训练环境可能鼓励模型在歧义面前大胆选择答案,因为一项设计良好的评测通常会提供足够信息,并保证存在可以评分的结果。模型如果反问任务设计者,反而无法得分。
这个解释有启发,但没有证据证明 Opus 5 的具体协作行为由某种 benchmark 或训练方式造成。官方发布材料也没有提供能支持这条因果链的数据。
现有证据只能说明,单独测任务成功率可能遗漏“何时需要人类输入”这项能力。真实工作既要看模型能不能解题,也要看它能否发现题面之外的关键决定。
让 Agent 每做一步都询问,同样会让自动化失去意义。更实用的做法是同时判断三个因素:

这是文章中的编辑性框架,不是对 Opus 5 或其他模型的实验结果。
边界清楚、风险低、容易撤回的操作,可以直接完成并留下记录。信息不全但后果较轻时,可以声明假设,只做一个可回退的小步骤。动作虽然明确,但涉及发布、删除、付款或数据迁移时,应先取得审批。歧义和后果都很高时,Agent 应停止并请人决定方向。
问题是否有价值,要看它能不能改变方案或风险,而不是看数量。
Hacker News 讨论后来扩展到模型的固定写作句式、冗长注释和无关改动。有人认为这些问题严重消耗注意力,也有人觉得影响有限。这些都属于社区观察,不是统一实验结果。
但它们提醒了一个容易漏掉的成本:任务完成之后,人还要花多久才能信任结果。一个模型单次成功率更高,如果每次都要清理无关修改、核对隐藏假设和恢复越界操作,整体生产力未必同步提高。
团队可以记录这些指标:
能力决定 Agent 能做多复杂的任务,协作成本决定团队愿意给它多大的行动范围。
团队不必把所有背景写成一份无限增长的规则文件。固定约束适合写进项目说明,动态取舍则需要运行时判断:
规则文件能保护已经知道的边界,审批机制负责处理还没写进规则的新情况。两者缺一不可。
如果只给模型材料齐全、答案明确的任务,就很难观察它怎样处理现实中的不完整信息。更贴近协作的 Eval 可以故意留下关键歧义:
评价时不应只数模型问了多少问题,还要看它是否发现真正会改变结果的歧义,是否区分可逆与不可逆操作,是否在偏离计划前请求授权,以及人类总共花了多少时间介入。
这套指标仍是一种编辑性建议,不是现成的行业标准。它至少把“感觉更累”转换成了可以记录和比较的协作成本。

资料说明:本文没有证明 Opus 5 比旧模型更难协作,也没有把作者的训练猜测当作事实。关于审批矩阵、协作成本和 Eval 的部分,是基于原文问题做出的编辑性整理与实践建议。

比较 AI Agent 时,人们往往先问“用了哪个模型”。但模型只是其中一部分。它能访问哪些文件和工具、怎样管理上下文、何时请求批准、如何恢复失败、把运行记录保存在哪里,这些都由模型之外的 Harness 决定。
DeepSeek Harness 的 Developer Preview 把这层基础设施单独摆到台面上,并给出两个醒目的设计目标:所有能力都可以作为插件替换;每次运行都能从同一条事件流中追溯。
本文由《听懂 AI》第 005 期整理而成。主要来源是 DeepSeek Harness 官方站点、GitHub 仓库和架构文档。2026 年 8 月 26 日发布的 Cordis 预印本补充了可逆副作用和动态依赖的理论说明。项目截至 2026 年 8 月 28 日仍处于 Developer Preview,官方明确表示会出现破坏兼容性的变更。
模型可以生成文本或工具调用意图,但它不能直接在操作系统里“自己做事”。Harness 负责把模型接进真实环境:
同一个模型放进不同 Harness,能完成的任务、消耗的 token、失败方式和安全边界都可能不同。因此,评估 Agent 不能只看模型跑分,还要看它周围这套运行系统。
DeepSeek Harness 建在 Cordis 插件系统上。官方架构文档没有保留一个不可替换的特权核心:模型适配器、工具注册表、会话日志、智能体循环、沙箱、存储、调度和网页界面都通过插件提供。
这些插件把服务、带类型的事件和依赖关系挂到共享上下文中。开发者可以在配置里替换某个提供者,而不是修改 Harness 源码。例如,换掉模型适配器、把本地文件系统改成远程沙箱,或给某类会话使用不同的工具组合。
运行时并不是简单扫描一个插件目录。它按照 Profile、Bundle、用户补丁和命令行覆盖层组成一棵有顺序的插件树。官方提供 dsh --profile web --dump-config,让开发者查看机器最终实际启动的配置,而不是只看散落在多层文件中的声明。
插件卸载不只是删除一段代码。它可能已经注册监听器、打开连接、挂载服务或启动后台任务。Cordis 要求可撤销的注册在创建时同时登记清理函数,插件卸载时再按生命周期收回这些效果。
2026 年 8 月 26 日提交的 Cordis 论文把这种机制称为“时间可组合性”:组件产生的上下文变化带有逆操作,运行时负责保存并执行。论文还讨论“空间可组合性”,即组件根据声明的依赖动态激活和停用。
这个机制能清理框架知道并登记过的副作用,却不能倒转所有现实操作。插件如果已经删除外部文件、调用第三方 API 或泄露凭据,卸载函数无法让这些事情自动消失。生命周期清晰不等于风险消失。
DeepSeek Harness 把会话视为只追加的 SessionEvent 事件流。用户输入、模型请求、模型返回、工具调用与结果、步骤开始结束等事实写入同一记录。下一轮模型历史由日志重新投影,恢复、分叉、搜索、重放、遥测和持久化也从这条事件流派生。
官方文档提出一条运行时约束:“模型可见”就必须能够从日志重建。也就是说,任何真正送进模型请求的内容都应留下对应事件,避免界面显示一套历史、模型实际收到另一套历史。

图中只展示架构关系。实际插件、事件和运行模式以当前配置及官方文档为准。
可追溯不等于能够读取供应商隐藏的内部推理。Harness 只能记录自己收到、创建或发送的内容;如果模型 API 没有返回完整 Chain of Thought,它不会凭空出现在会话日志里。日志透明的是执行链,而不是模型供应商没有暴露的内部状态。
官方当前提供四种运行模式:
这些模式的意义不只是功能多少。Minimal 可以减少 Harness 自身对评测结果的干扰;Creator 则把插件开发和组合变成产品能力。团队也可以由基础 Bundle 开始,只为特定项目增加必要插件。
对于研究者和 Agent 基础设施开发者,这种架构便于回答过去很难分开的实验问题:
插件边界让替换实验更容易,事件流则提供统一的观察依据。它们提供的是实验和组合能力,不是“换插件一定更好”的保证。
截至 2026 年 8 月 28 日,官方仓库仍明确写着 Developer Preview,并警告会有破坏兼容性的变更。安全说明更加直接:项目尚未经过安全审计,不能视为安全或生产就绪的软件。
DeepSeek Harness 可以执行模型生成的代码和命令,加载第三方插件,并访问用户允许的网络、进程、凭据和文件。错误模型输出、缺陷、配置错误、恶意输入或不可信插件都可能修改或删除文件、泄露数据,甚至损害宿主机。
官方还强调,沙箱、审批和权限控制只能降低风险,不能保证完全隔离;系统无法保护已经被明确授权访问的资源。社区关于“插件疲劳”、版本冲突和供应链攻击面的担忧因此是合理的工程问题,但 HN 评论只是社区观察,不代表项目已经出现了这些事故。
如果只是想比较模型或研究 Harness,可以从隔离实验开始:
--dump-config 结果和版本信息,方便重现实验;DeepSeek Harness 把模型之外的运行系统摆到了开发者面前,让工具、会话、沙箱、循环和存储都可以观察和替换。它能否从实验台走向稳定生态,取决于接口治理、安全审计、插件质量和长期兼容性。

资料说明:本文描述的是 2026 年 8 月 28 日可见的 Developer Preview。仓库和 API 正在快速变化,后续版本可能调整名称、模式、接口和安全边界。

AI 编程工具最直观的变化,是让“写出一批能运行的代码”变得更快。一个需求可以在几小时内长出页面、接口、数据表和测试,过去需要几天的实现工作被压缩到一个下午。
但软件交付并不在代码生成时结束。团队还要理解设计、审查影响、验证行为、迁移数据、处理故障,并在几个月后继续修改。生成速度提高后,这些工作反而更容易成为新的瓶颈。
本文由《听懂 AI》第 004 期整理而成。节目来源是 Florian Herrengt 于 2026 年 8 月 11 日发布的个人文章《AI is removing the middle class of software engineering》。原文以作者经历和判断为主,并不是就业市场或软件质量的统计研究;本文另外引入 GitHub、METR 和 DORA 的研究,核对“写得更快是否等于生产力更高”。
Herrengt 描述了一个常见场景:周一早上出现多个由 Agent 生成的巨大合并请求,功能看起来能运行,却没有人能清楚解释数据从哪里来、为什么增加某个抽象、失败后如何恢复。设计依据甚至只存在于一条来回改口的模型对话里。
他所谓的“工程师中间层消失”,不是一项职业分类研究,而是一个比喻:当实现和集成越来越容易,单纯把规格翻译成代码的价值可能下降;能够理解复杂系统、判断取舍并对结果负责的人会更重要。
原文中的“25,000 行合并请求”“一下午生成 20,000 行”等数字是叙事例子,不是行业平均值。文章对薪资分化和岗位减少的判断也是作者预测,不能当成已经发生的统计结论。
不同研究给出的答案并不一致,因为它们测量的任务完全不同。
GitHub 在一项受控实验中让 95 名专业开发者完成同一个 JavaScript HTTP 服务器任务。使用 GitHub Copilot 的一组平均用时 1 小时 11 分,未使用的一组平均用时 2 小时 41 分,前者快 55%。这是一个范围清楚、自动测试可以判断完成度的单项任务。
METR 在 2025 年研究了另一种情境:16 名熟悉大型开源项目的开发者完成自己仓库里的 246 个真实任务。允许使用当时的 AI 工具后,任务完成时间反而增加了 19%。参与者原本预计会快 24%,做完后仍主观认为自己快了 20%。
这项结果也不能推广到所有开发。它只代表 2025 年初的工具、这些开发者和成熟仓库。METR 在 2026 年公布的后续数据出现了可能的加速信号:原研究参与者子集估计快 18%,新招募开发者估计快 4%,但置信区间都包含没有提升的可能,而且不愿离开 AI 工具的开发者更容易退出实验,造成明显的选择偏差。研究团队因此决定调整实验设计。
三组结果放在一起,能得到一个更可靠的判断:AI 对明确、局部、容易验证的实现任务可能明显提速;在熟悉但复杂的长期项目中,上下文、验证和协作成本可能抵消一部分收益。不能用一个百分比概括全部软件工程。
实现变快以后,团队要处理的变更数量和批次都可能增加。每个变更仍需要回答这些问题:

图中流程是一般性的交付模型,不代表每个团队都使用相同阶段,也没有给出行业统一的速度比例。
DORA 的 2025 年研究把 AI 描述为组织能力的“放大器”:基础流程、平台和文化较强的团队更容易获得收益,原有弱点也可能被同步放大。DORA 在 2026 年的后续分析中还指出,生成阶段节省的时间经常转移到审计和验证;更高的 AI 使用与更高吞吐量、同时也与更高交付不稳定性相关。这里是关联关系,不等于 AI 单独造成了不稳定。
一个人一天提交十个 PR,看起来像生产力提高了十倍。如果三个审查者接下来花两天理解、退回和重写,工作只是从生成者转移到了团队其他成员。
代码行数、PR 数量和“完成”的任务卡都属于局部产出指标。团队真正关心的是从需求到安全上线的完整周期,以及上线后的失败率、恢复时间、维护成本和知识是否有人掌握。
这并不意味着大改动永远错误,也不意味着技术债绝对不能欠。团队必须知道自己接受了什么风险、为什么此刻值得接受,以及准备怎样偿还。
原文认为,AI 会扩大优秀工程师和较弱工程师之间的薪资差距。这是作者的判断,不是文章提供数据证明的结论。
现有证据只足以说明,AI 正在改变工程技能的相对价格。模板实现、样板代码和常规转换越来越便宜;需求澄清、系统建模、复杂度控制、测试设计、事故处理和技术取舍仍然需要大量上下文与责任承担。
初级工程师也不等于“只能写 CRUD”。原文自己举了相反例子:愿意追问、建立理解并检查假设的初级开发者,可能比已经放弃理解的资深开发者更可靠。风险不在职级,而在于是否把 AI 当作建立理解的工具,还是替代理解的借口。
团队可以从变更规模和知识所有权入手:
使用 AI 并不等于放弃工程判断。真正需要警惕的是,代码已经进入生产,而团队仍不知道它为什么存在、会影响谁,以及出错后该怎么办。

资料说明:原文关于“中间层”、薪资和就业结构的描述属于作者观点。本文补充的研究测量了不同任务和组织情境,结果不可直接互相替代,也不能用于预测单个岗位的未来。

看到一段无法阅读的加密文本,人很容易把它当成“安全的乱码”。但在大模型 API 里,这类不透明数据可能保存着模型的隐藏推理、工具返回值,甚至用户输入过的敏感信息。
2026 年 8 月提交的论文《Stealing Reasoning Traces from Proprietary LLM APIs》提出了一个反直觉的风险:研究者没有暴力破解密钥,也没有直接攻破防护最严的前沿模型,而是利用加密推理块可以跨会话、跨用户和跨模型复用的特性,把它交给同一家服务商中防护较弱的兼容模型处理。
本文由《听懂 AI》第 003 期整理而成。事实来源是 Alexander Panfilov 等 8 位作者于 2026 年 8 月 10 日提交的 arXiv 预印本及作者项目页。论文测试的是 2026 年 7 月初可用的特定 API 和模型版本,不能直接代表今天所有接口仍然存在相同行为。
推理模型在给出最终回答前,会生成较长的中间推理。服务商通常不把完整推理以明文返回,而是向客户端提供摘要,以及一段签名或加密后的不透明数据。客户端保存这段数据,并在下一轮请求时原样传回,让模型延续之前的推理状态。
论文把这种数据描述为经过认证加密的封装。它既能防止用户直接阅读,也能检测内容是否被篡改,同时让服务端不必长期保存每次会话的完整推理。
需要注意,论文作者也明确说,各服务商没有公开完整的密码学实现。因此,文章中的具体结构和密钥使用方式来自研究者的实验观察与推断,不是服务商公开的协议承诺。
论文发现的关键在于兼容范围过大。一个推理块可能被拿到另一段会话、另一个用户,甚至同一服务商的另一个模型中继续使用。
攻击者可以先从能力强、拒绝训练更严格的模型获得一个加密推理块,再把它送给较弱但兼容的模型。后者本来就需要合法解开并处理这类数据,研究者再诱导它把处理到的内容输出出来。
整个过程不需要知道加密密钥,也没有修改密文。真正失守的是“这段加密数据只能在原来的用户、会话和模型里使用”这一安全边界。

示意图只说明安全边界和防御方向,不包含论文中的具体攻击提示或供应商实现细节。
第一类是模型蒸馏。竞争者可能批量提取强模型的隐藏推理,用来训练或模仿另一个模型,绕过服务商隐藏思维过程的初衷。
第二类是敏感数据泄露。开发者公开 Agent 会话、评测轨迹或 API 日志时,常常只清理肉眼可见的文本,却保留看似无害的不透明推理块。秘密可能仍藏在里面。
第三类是拒答背后的危险内容。模型最终可能正确拒绝一个恶意请求,但隐藏推理已经处理过更具体的信息。如果推理块可以被恢复,安全的最终回答并不代表整个执行过程都没有泄露。
第四类是不可见提示注入。恶意指令可以隐藏在不透明数据中,人工审查日志时看不见,后续接手同一轨迹的 Agent 却可能读取并执行。论文把它作为概念验证和长时任务污染风险来讨论。
研究者从 GitHub 和 Hugging Face 收集了 6,708 条公开 Agent 轨迹,重建了 315,320 个推理块。完整论文给出的统计包括:
论文摘要还用另一组分类口径概括为 367 项个人身份信息和 182 项凭据。不同数字对应不同分类、去重和数据范围,不能直接相加,也不能理解成同样数量的独立受害者。
研究样本来自公开轨迹,不是对整个互联网或所有生产系统的普查。论文也使用两阶段自动分类筛掉占位符和测试数据,但这仍是一项定向研究,不是完整的泄露率调查。
论文给出的一个典型风险是会话清理:用户要求 Agent 删除仓库中的秘密,模型在隐藏推理中重新读取并复述这些值;最终可见回答只说“已经清理”,但不透明推理块仍可能保留原值。
因此,只搜索最终回答里的 API_KEY 或密码格式并不够。共享原始 API 记录前,还需要删除 signature、thinkingSignature、encrypted_content 等不透明推理字段。字段名称会随供应商和 SDK 改变,不能依赖一份永远不变的黑名单。
如果含有此类数据的会话已经进入公开 Git 仓库,删除最新文件也不代表历史提交消失。应当检查 Git 历史、缓存、制品和数据集副本,并轮换可能已经暴露的凭据。
这篇论文是 2026 年 8 月 10 日提交的 v1 预印本。实验针对 2026 年 7 月初的 Anthropic、OpenAI 和 Google API 版本,服务商可以在不公告的情况下改变内部实现。
作者无法看到隐藏推理的真实明文,因此不能逐字证明每次提取都完全正确。他们主要用 API 报告的思考 token 数量与恢复文本的 token 数量做对照,并在 120 个 Codeforces 问题上观察到较强的一致性。这是提取可信度的证据,但不是完整的明文真值验证。
论文还说明,团队在发表前已向相关模型服务商、Microsoft 和 Hugging Face 负责任披露。作者报告说,各服务商确认收到报告,此后他们已经无法用相同方法继续发动攻击。这说明供应商可能采取了缓解措施,但不能据此推断所有历史数据已经安全,也不能证明所有相邻攻击面永久消失。
论文建议服务商使用多层防御:
更严格的上下文绑定会影响合法的会话压缩、历史编辑和模型切换,因此不是简单增加一个字段就能完成。即使绑定正确,只要某个模型必须解开并处理旧推理,模型级提示攻击仍可能成为风险,所以需要纵深防御。
开发者现在可以做这些事:
密文不是废数据,也不是天然安全的秘密存储。看不懂一段内容,只说明人无法直接阅读,并不代表系统中的其他组件也无法处理它。

资料说明:本文的技术结论和数字均来自论文 v1。论文作者报告的攻击状态、供应商范围和缓解结果具有时间性,后续版本或服务商更新可能改变结论。

一个 AI Agent 真正运行起来以后,并不是每一步都需要最强模型。制定计划、处理复杂异常,可能值得调用能力最强的模型;执行工具、检查返回值、整理格式和重复查询,往往更在意速度和成本。
如果所有步骤都交给同一个昂贵模型,效果容易预测,账单和延迟却会迅速增加。反过来,如果只用便宜模型,复杂任务又可能失败。模型路由想解决的,就是如何在这两种选择之间分工。
本文由《听懂 AI》第 002 期整理而成。节目讨论的主要来源是 NVIDIA 于 2026 年 8 月 11 日发布的 Nemotron 3.5 Lightning 与 NeMo Switchyard 资料,并加入了对项目成熟度、评测边界和 Hacker News 社区争议的核对。
传统聊天产品通常把一次请求交给一个固定模型。Agent 的情况不同:它可能先规划,再调用工具,读取结果,修正计划,最后生成答案。一次任务里会出现很多性质不同的步骤。
NVIDIA 把这种架构称为“模型系统”:前沿推理模型负责规划和编排,小而快的模型承担代码检查、工具调用、安全告警监控和账单查询等高频工作。这个思路并不要求小模型取代大模型,而是让每种模型做自己更合适的事。
Nemotron 3.5 Lightning 是一个 300 亿参数的混合专家模型(MoE),但每个 token 只激活约 30 亿参数。可以把它理解成一个拥有多个专家小组的组织:总知识容量仍然较大,每次任务只叫少数专家参与,因此单次计算量接近更小的稠密模型。
NVIDIA 的技术文章称,它针对长期运行 Agent 的高频执行层设计,并使用多 token 预测、推测解码和量化等手段提高吞吐量。官方公布的结果包括:
这些都是 NVIDIA 选择的测试条件和对照模型,适合用来理解产品定位,不能直接换算成任何业务的固定收益。真实效果仍取决于任务分布、推理框架、硬件、并发量和输出长度。
NeMo Switchyard 是一个用 Rust 编写的代理和路由库。它能在不同模型与供应商之间分配请求,也负责 OpenAI Chat、OpenAI Responses 和 Anthropic Messages 等接口格式之间的转换。
仓库目前提供多种路由方式:

这张图表示一般性的模型路由结构,不代表 Switchyard 会自动识别所有任务,也不表示三类模型一定同时存在。
路由器本身也会消耗资源。真实总成本不仅包括最终模型调用,还包括路由判断、额外分类模型、缓存补齐、失败重试和升级调用。若没有统一的质量门禁,所谓“节省成本”可能只是把错误推迟到后面。
NVIDIA 公布的内部基准称,Switchyard 在保持前沿级准确率的同时,可把任务完成成本降到单独使用 Opus 4.8 的近三分之一。合作方数据中还有两组很醒目:
这些结果说明模型路由有潜力,但它们来自 NVIDIA 及合作方披露,并不是对所有任务的独立保证。尤其要注意 LangChain 的结果并非“质量完全不变”,而是用 6% 的准确率差异换取明显的成本下降。
评估路由器时,至少要同时看四项指标:正确率、端到端延迟、完整任务成本和失败后的恢复成本。只比较单个 token 价格,往往会漏掉路由判断和重试。
Hacker News 讨论中,争议最大的问题之一是提示缓存。批评者认为,同一会话不断切换模型,会让已经积累的 KV Cache 失效,抵消便宜模型省下的成本。
社区里也有人给出另一种解释:每个模型可以维护自己的缓存;重新切回某个模型时,只需要为它补上缺失的对话增量,而不是每次从头处理全部上下文。这样做仍然需要额外 prefill,而且缓存不能在结构不同的模型间直接共享,但较便宜模型承担更多生成工作后,整体仍可能节省费用。
这段讨论不能当作 Switchyard 的官方缓存承诺。它更像一个提醒:模型池大小、会话黏性、缓存策略和路由频率必须一起设计。模型选得越多,路由器越复杂,未必越划算。
截至 2026 年 8 月 28 日,Switchyard 仓库仍把整个项目标为 pre-alpha,并提醒 API 和算法在 1.0 之前可能大幅变化。各组件的成熟度也不同:libsy 标为 Beta、可试验性集成;客户端和 runner 仍是 Alpha;switchyard-server 是演示服务器,明确不建议用于生产环境。
这与新闻稿中的“部署”“企业使用”并不完全矛盾:合作方可能使用的是内部集成、特定组件或受控试验,并不等于公开仓库中的演示服务器已经具备生产条件。对普通开发团队来说,更合理的起点是离线评测或旁路实验,而不是立刻替换线上网关。
如果要验证模型路由,可以从一个很小的模型池开始:一个擅长复杂规划的模型,一个便宜快速的执行模型,再加明确的升级条件。
建议先完成下面几件事:
好的路由器不会一味选择最便宜的模型。它应当使用可验证的规则,把昂贵能力留给确实需要它的步骤。

资料说明:性能和合作方数据主要来自 NVIDIA 官方材料,本文已保留测试主体、对照对象与准确率差异。关于缓存的内容来自社区讨论,只作为工程问题线索,不作为 Switchyard 的官方保证。

第一次和大语言模型聊天,很容易产生一种错觉:屏幕另一端像是坐着一个读过无数书、什么都能聊的人。它能续写邮件,能解释概念,也能顺着语气安慰你。可一旦追问一个冷门事实,它又可能用同样笃定的口吻编出不存在的人名、论文和日期。
这两种表现并不矛盾。要理解它,先放下“电子大脑”这个比喻,把它想成一位特别擅长接话、但不会自动查证的咖啡馆店员。
本文由《听懂 AI》第 001 期访谈整理而成。该期节目从科普主题出发,并非改写某一篇原文;文末补充了 Transformer、GPT-3、语言理解争议和真实性评测的原始论文。
假设你说:“今晚下雨,出门记得带……”
人很容易想到“伞”。语言模型做的事情与此有一点相似,但规模大得多:它先把输入切成一组 token,再结合前面的上下文,为下一个 token 计算概率。选出一个之后,它把这个 token 加回上下文,继续预测下一个,直到回答结束。
token 不一定等于一个完整汉字或单词。它只是模型处理文字时使用的基本单位;具体怎样切分,取决于模型采用的分词方法。

图中的候选词和概率只是工作原理示意,不是某个真实模型的测量结果。
2017 年的论文 Attention Is All You Need 提出了 Transformer 架构。它通过注意力机制处理序列中不同位置之间的关系,后来成为大语言模型的重要技术基础。2020 年的 Language Models are Few-Shot Learners 则展示了 GPT-3 这类自回归语言模型在扩大参数和训练数据规模后,可以仅凭文字指令或少量示例完成多种任务。
所以,“预测下一个 token”听起来很朴素,却不代表模型只能做简单的句子补全。模型从大量训练文本中学到语法、文体、概念之间的关联,以及常见的推理表达方式。当这些规律共同参与一次预测时,结果就可能表现为写作、问答、翻译或代码生成。
另一个常见误解是:模型先把互联网背下来,回答时再从某个数据库里找到对应段落。
训练确实可能让模型记住部分内容,尤其是重复出现或具有独特表达的文本。但通常情况下,训练材料中的语言规律会被编码进大量参数。生成回答时,模型根据参数和当前上下文计算后续内容,不是在资料库中逐条检索。
这里需要区分基础语言模型和完整的 AI 产品。一个产品可以在模型外部接入搜索引擎、知识库、计算器或其他工具。此时你看到的答案可能同时包含模型生成和外部检索结果,但检索能力不是“预测下一个 token”天然附带的事实核验机制。
模型能正确处理“下雨”和“带伞”的关系,是否就说明它理解雨是什么?
这个问题没有一句公认的结论。Emily M. Bender 和 Alexander Koller 在 2020 年的论文 Climbing towards NLU 中强调,学习语言形式与获得由现实经验支撑的意义不是一回事。模型可以熟练处理词语之间的关系,却没有淋雨、撑伞或被冷风吹过的身体经验。
另一方面,只用“随机鹦鹉”也不足以描述今天模型表现出来的全部能力。它确实掌握了强大的语言模式处理能力,但不能据此直接推断它拥有人的意识、感受或理解方式。
模型的基本目标是生成在当前上下文中看起来合适的后续,而不是保证每句话都经过外部证据核对。如果问题含糊、训练材料不足,或者错误说法在语料中很常见,它仍可能生成连贯但不真实的答案。
2021 年的 TruthfulQA 用 817 个问题测试模型是否会复述人类常见的错误观念。在当时接受评测的模型中,最佳结果有 58% 的回答被判定为真实,而人类基线为 94%。这组数字不能代表今天任何具体产品的水平,但它说明了一个长期存在的问题:语言流畅度和事实真实性不是同一个指标。
因此,遇到下面这些内容,不要因为语气自信就直接采用:
把语言模型当成一个反应很快、知识面很广、但偶尔会硬撑的助理,通常比把它当成权威更合适。
适合交给它的工作包括改写文字、整理材料、列出备选方案、模拟提问和解释概念。涉及重要事实时,可以要求它区分“已知事实”“推测”和“不确定项”,列出可核验的来源,再由人打开原始资料确认。
提问也不需要背诵所谓的“提示词咒语”。说明读者是谁、想解决什么问题、有哪些限制,再给一个例子,往往就能明显改善结果。与此同时,不要随手提交身份证号、病历、公司机密或未公开代码;能否输入某类数据,应以所在组织的制度和所用产品的数据政策为准。
使用时记住:它很会生成答案,但“很像答案”不等于“答案是真的”。

资料说明:节目第 001 期原始 sources.json 只记录了“向不懂技术的人解释大语言模型”这一主题,没有外部 URL。以上论文由本文编辑阶段补充,用于说明相关技术背景和争议,不代表节目逐句改写这些论文。

最近看了 Matt Pocock 的一段视频:
视频只有 15 分钟,讲的却不是某个新模型或提示词技巧,而是一个更基础的问题:让 AI 参与一个已有代码库时,怎样避免每次都从头解释业务名词和历史决定?
Matt 之前的 /grill-me 会持续追问,把模糊的想法问到可以执行。它并没有失效;问题在于,单靠一轮轮问答,已经确认过的概念不会自动成为项目的一部分。下一次会话里,人仍可能要解释“独立视频”到底指什么、某个对象之间是一对一还是一对多、这个状态能否随意切换。
他现在在编码场景中改用 /grill-with-docs。它保留追问,但把共同语言和不容易看懂的决策写进仓库。这样,聊天记录不再是唯一的上下文。
视频中的例子是一项新功能:在一个管理课程和视频的应用里加入 pitch。这里的 pitch 不是代码里的通用术语,而是视频的“包装”——标题、描述和对外呈现方式;团队会先想出多个 pitch,再选择其中一些制作成视频。
人一听就能根据上下文补全很多含义,AI 却没有这种默认背景。例如:
standalone video 是不属于课程或课时的视频,还是“尚未关联 pitch 的视频”?idle、scheduled、shipped 是强制流转的状态机,还是可以手动修改的标签?这些不是措辞洁癖。它们会影响数据库关系、删除规则、变量名、文件名、界面分组和后来的人怎样理解代码。若定义只存在于某次聊天里,之后每一次让 AI 修改相关部分,都会重新产生猜测空间。
context.md/grill-with-docs 借用了领域驱动设计(DDD)中的“通用语言”思路。它会先寻找 context.md,读取其中的术语和定义;在对话中发现概念不清、用词冲突或新规则时,再要求人确认并更新这份文件。
在视频里,context.md 至少承担三件事:
它不需要写成一份覆盖全部实现的百科全书。视频里的建议更接近 DDD 的 bounded context:一个大型 monorepo 可以有 context map 和多个上下文;如果一个仓库内大家说的是同一种业务语言,一份放在根目录的 context.md 就够用。
关键不在文件名,而在约束:产品、代码和与 AI 的对话尽量用同一个词。否则,文档里叫“已投递视频”,数据库表叫 standalone_videos,界面又叫“提案视频”,AI 很难判断它们到底是不是同一个东西。

共同语言需要在每次新需求中核对和更新;它不是一次写完就不再变化的说明书。
/grill-with-docs 不会读完文档就直接生成代码。它会先把新需求同既有术语表对照,指出含义不清或冲突的地方,并通过具体场景把问题问出来。
视频的演示依次确认了:
这些回答随后写回 context.md。作者也展示了一个很现实的细节:写入后产生了 pitched standalone video、unattached standalone video 之类别扭的名称。他没有假装第一版术语一定正确,而是提醒自己在“足够清楚”时停止讨论,后续需要时再重构。
这条边界很重要。共同语言的目的不是无限讨论命名,而是让接下来的实现少一点误解。
词汇表能定义“是什么”,却不总能解释“为什么”。视频把这类信息交给 ADR(Architecture Decision Record,架构决策记录)。
ADR 适合记录那些不看背景会觉得奇怪、又难以轻易撤回的选择:它面临过什么取舍、会带来什么后果。库选型这类容易替换的决定未必值得专门写 ADR;删除策略、数据关系或会影响多个模块的业务定义,通常更值得留下理由。
这也避免 AI 看到一个非直觉的实现时,自作主张把它“优化”掉。它能先读到决策背景,再判断当前需求是否真的要求改变它。

context.md 保存“是什么”,ADR 保存“为什么这样选”。
Matt 的观察是:定义稳定后,AI 不必反复解释同一个概念,回复会更简洁;代码中的命名和规划文档也会更容易互相检索。这是他在工作流中的经验,而不是对所有模型和项目都成立的性能测试结果。
确认过的业务含义不必停在对话记录里。把它记录到仓库后,下一位开发者、下一次会话和后续生成的代码,都从同一份上下文开始。
从视频可以整理出一套小而可用的做法:
这里的重点不是复制某个斜杠命令。即使不用这两个 skill,团队也可以建立同样的习惯:把 AI 提出的关键歧义当作待确认的产品或技术问题;确认后更新共享文档,而不是只在聊天窗口里回答一次。
/grill-me 并没有被淘汰视频最后给出了一条很清楚的使用边界:有代码库时,优先用 /grill-with-docs;没有代码库的开放式任务,则继续用 /grill-me。作者还举了非工程场景的例子:有人用后者整理为母亲写悼词时的回忆,价值就在于耐心追问,而不是建立术语表。
项目刚开始时,作者仍倾向 /grill-with-docs,因为这恰好是最需要建立共同语言的阶段。差别不在于有没有足够多的代码,而在于这次对话是否要留下能被后续工作复用的领域知识。
让 AI 写代码之前,把项目里的词说清楚,看起来比直接输入需求慢一点。但当这些词会进入表名、组件名、接口和用户界面时,早一点确认往往比之后在许多文件里改名更便宜。

两个账号应各自使用独立的本地状态目录;它们可以同时工作,但不共享认证和会话。
一个人同时有个人和工作两个 OpenAI 账号时,最容易踩的坑不是登录,而是登录之后。默认情况下,Codex CLI 把认证、配置、会话和本地状态都放在同一个目录。后一次登录会让下一次启动的 CLI 使用新的身份;MCP、插件和会话历史也混在一起。
我在 macOS 上用 codex-cli 0.145.0 核对过这个行为。Codex 的配置源码把 CODEX_HOME 定义为全部本地状态的根目录:默认是 ~/.codex,设置后会改用指定目录。源码中的说明 也说明日志和 SQLite 状态会随这个目录变化。
这意味着可以把“个人”和“工作”当成两套独立的 CLI 环境,而不是在同一套配置里反复登录、退出。
先把边界说清楚。Codex CLI 还没有类似 --account work 的正式账号选择器;官方仓库中相应的功能请求仍是开放状态。该请求 本身也把现状描述为:默认只有一个本地状态目录,多账号只能换目录、换认证文件或重新登录。
所以 CODEX_HOME 的作用不是把两个账号放进一个账号列表里。它做的是把两套状态彻底分开:
这比手动替换 ~/.codex/auth.json 稳妥得多,也更容易查清一条会话究竟用了哪个身份。
下面示例用两个目录保存状态。目录名只表示用途,不会把账号名称传给 OpenAI:
mkdir -p "$HOME/.codex-profiles/personal" "$HOME/.codex-profiles/work"
# 首次使用个人环境时登录个人账号
CODEX_HOME="$HOME/.codex-profiles/personal" codex login
# 首次使用工作环境时登录工作账号
CODEX_HOME="$HOME/.codex-profiles/work" codex login
以后从相同的入口启动即可:
# 个人环境
CODEX_HOME="$HOME/.codex-profiles/personal" codex
# 工作环境
CODEX_HOME="$HOME/.codex-profiles/work" codex
登录完成后,分别检查状态:
CODEX_HOME="$HOME/.codex-profiles/personal" codex login status
CODEX_HOME="$HOME/.codex-profiles/work" codex login status
如果日常经常在两个环境间切换,可以给终端写两个别名或两个很短的启动脚本。关键不是别名的名字,而是每个入口固定指向一个目录。涉及外部操作,例如创建 PR、发消息或使用带权限的 MCP 工具时,先看当前终端来自哪个入口。
auth.json看上去最快的做法,是先在默认目录登录一次,再把 auth.json 复制到另一个目录。这个方法不可靠。

复制的认证文件可能在另一个副本刷新 refresh token 后失效;两个目录应分别登录。
Codex 使用的 OAuth refresh token 可能是一次性的:当一个副本刷新 token 后,另一个副本里的旧 token 会失效。官方仓库已有复现说明:复制认证文件后,第一次可能还能使用缓存的 access token,之后可能出现 401。问题 #15410 还明确指出,用软链接或复制文件来共享 ChatGPT 订阅认证都不是稳定方案。
每个目录各自执行一次 codex login。不要从另一套环境复制认证文件,也不要把认证文件纳入 Git、网盘同步或备份脚本。
账号隔离不是只多两个 auth.json。新目录一开始没有你原来配置过的 MCP server、插件、Skills、偏好设置或历史会话。这既是代价,也是这个办法有用的原因。
我通常会把配置分成两类:
这样做的好处是,工作账号不会意外加载个人的高权限工具,个人会话也不会写进公司的历史记录。代价是第一次使用时要分别安装或配置真正需要的工具。
要注意,本文只讨论从终端启动的 Codex CLI。桌面端、IDE 扩展和其他 GUI 进程未必会继承终端环境变量;不能因为 CLI 被隔离,就假定它们也已经切换到同一账号。它们应单独核对登录状态和凭据位置。
这个办法适合把合法且明确授权的身份分开,例如个人订阅与公司账号、两个客户提供的独立账号,或需要避免配置互相污染的测试环境。
它不应用于自动探测额度、在账号受限后自动切到下一个账号,或把多个账号的额度当作一份可轮换的资源。OpenAI 的服务条款禁止规避速率限制、使用限制和保护措施;个人账号也不应与他人共享凭据。OpenAI Terms of Use
如果目标只是让日常开发时的个人、工作上下文互不干扰,两个目录、两次独立登录和两个固定启动入口已经够用。它没有魔法,也不会扩大任何一个账号的权限或额度;它只是把本来会混在一起的本地状态分开保存。
来源与核验范围:本文基于 codex-cli 0.145.0 在 macOS 上的本地检查,以及 OpenAI 公开的 Codex 配置源码、多账号需求讨论、认证文件复制问题 和 服务条款。Codex 的行为和条款可能更新;实际配置前请以本机 codex --help 与当前条款为准。
2015 年,我做过一款很小的 iOS App,中文名叫「闪印」。它只解决一件事:把旅行、采购或工作清单整理好,预览,然后打印到纸上。
旧版用 Objective-C 和 Storyboard 开发,后来陆续支持了 iPad、iPhone Xs Max 和 iCloud。它没有复杂的账号系统,也不试图成为项目管理工具。清单建好,纸张打出来,任务就完成了。
十一年后,我重新打开这个项目,决定用 SwiftUI 把它重写一遍。新版项目叫 PrintableCheckList-SwiftUI,代码已经开源。
这次重写不是给旧界面换一层 SwiftUI。我要保留原来的用途,也要回答一个新问题:如果 AI 能帮人省掉大量录入工作,一份「可打印清单」今天应该怎么做?
PrintableCheckList 仍然可以完全手动使用。你可以创建多份清单,一次粘贴多行内容,编辑、删除或拖动排序,再生成带方框的打印预览,通过 iOS 系统打印控制器输出。
AI 是可选的快捷入口。比如输入:
生成一份带孩子去北海道旅行 7 天的冬季行李清单,需要考虑滑雪和儿童常用药。
App 会返回清单标题和项目。结果不会立刻写入数据,而是先进入编辑页;你可以改标题、删掉不需要的内容、补上个人物品,确认以后再保存。AI 也能给现有清单补充遗漏项,不必每次从头生成。
整个过程可以概括为:
输入主题
↓
按需联网搜索
↓
模型返回结构化 JSON
↓
去重、限长、清理序号
↓
用户检查和修改
↓
保存到本地 → 预览 → 打印
这里最重要的一步不是「生成」,而是生成后的确认。模型负责减少输入,用户仍然决定最后打印什么。
接入 AI 时,我很快遇到一个看似简单的问题:用户说「全球票房前十名」时,他要的是十部电影,不是「查询票房」「核对排名」之类的十个任务。
因此,内置提示词会区分两类内容:
模型必须返回固定的 JSON 结构。App 还会清理 Markdown 围栏、编号和重复内容,限制标题与项目长度。补充已有清单时,已经存在的项目也会被过滤掉。
这些处理不显眼,却决定了 AI 生成的内容能不能真正进入一个普通 App,而不是停留在聊天窗口里。
旅行行李清单通常不需要搜索,但「最新票房排行」「最近发布的产品」或「当前汇率」不同。只靠模型已有知识,很容易得到过期答案。
PrintableCheckList 提供三种搜索模式:
目前 GLM 通过 Web Search API 搜索,OpenAI 通过 Responses API 的 Web Search 搜索。搜索结果会先整理成一段带来源的材料,再交给清单生成器。结果页显示来源链接,但来源不会混进最终的清单项。
DeepSeek 和自定义 OpenAI 兼容服务仍可生成清单,只是不启用这条原生搜索路径。这样没有假设所有 /chat/completions 服务都支持同一种联网工具。
新版采用 BYOK(Bring Your Own Key)模式。用户可以选择 GLM、OpenAI、DeepSeek,或填写自己的 OpenAI 兼容服务地址和模型名称。
API Key 存在 iOS Keychain,访问级别为 WhenUnlockedThisDeviceOnly。普通配置存入 UserDefaults,但不会包含 Key。生成请求和必要的搜索请求由设备直接发给用户选择的服务商,不经过开发者服务器。
没有配置 AI 也不影响手工创建、编辑、预览和打印。我坚持保留这条边界。AI 应该缩短输入时间,不应该变成打开清单 App 的通行证。
每次编辑都会先保存到设备的 Application Support/PrintableCheckList/projects.json。没有网络时,清单的创建、修改和打印都能继续使用。
可选的 iCloud 路径使用 NSUbiquitousKeyValueStore,沿用旧版的 keyProjects。代码也保留了原来的 bundle identifier,并实现了 NSKeyedArchiver 迁移:旧 Objective-C 里的 Project 和 Item 会转换成新的 Codable Swift 模型;旧 ID 不是 UUID 时,则生成稳定的 UUID。
这部分比重新画界面麻烦得多,却是一次真正的 App 更新必须承担的责任。重写代码不应该等于让用户重新输入数据。
需要说明的是,未签名模拟器不能代替真实 iCloud 环境。仓库已经覆盖旧数据导入和同步逻辑测试,但签名真机上的 iCloud 端到端验证仍然是发布前检查项。
虽然新版加入了 AI,项目名称里的 Printable 没有变。
预览页使用 SwiftUI 显示标题、项目和空白方框;真正打印时,App 生成一段经过 HTML 转义的排版内容,再交给 UIPrintInteractionController。iPad 上还单独处理了打印弹窗的锚点,避免 popover 因缺少来源视图而崩溃。
测试中还会把默认中文旅行清单交给打印格式化器,确认它能排在一张 A4 纸内。相比「按钮能点」,这更接近 PrintableCheckList 真正要完成的事情。
新版最低支持 iOS 17,使用 SwiftUI 和 Swift Concurrency。工程文件由 XcodeGen 根据 project.yml 生成,.xcodeproj 不进入版本库。生成、构建、测试、模拟器运行和归档分别有独立脚本,日常开发不必手动维护 Xcode 工程里的文件引用。
截至 2026 年 7 月 22 日,我在 iPhone 16 Pro / iOS 18.5 模拟器上执行了完整测试:42 个测试用例中,41 个通过,1 个 Keychain 用例因为无签名模拟器缺少 entitlement 而按预期跳过。覆盖范围包括:
需要 macOS、Xcode、iOS 模拟器和 XcodeGen。克隆后运行:
git clone https://github.com/terryso/PrintableCheckList-SwiftUI.git
cd PrintableCheckList-SwiftUI
./Scripts/generate.sh
./Scripts/build.sh
执行完整测试:
./Scripts/test.sh
安装并启动模拟器版本:
./Scripts/run-simulator.sh
AI 配置不是运行项目的前提。你可以先把它当作一款普通的本地清单 App,之后再决定要不要填入自己的 API Key。
软件重写很容易让人只关注新框架、新界面和新功能。但回到 PrintableCheckList,真正不能丢的只有两件事:旧数据还在,清单还能顺利打印。
SwiftUI 让界面和状态管理简单了很多,AI 让创建清单更快,联网搜索让时效性内容有了核对来源。不过这些能力最后都服务于一个很朴素的动作:拿起一张纸,照着清单去做事。
如果你是 Swift 开发者,又想把自己 Mac 上的能力(本地文件、Shortcuts、Xcode 项目、Core Data 数据……)暴露给 Claude、ChatGPT 这类 AI 助手,那么 MCP Server 就是你要的东西。而目前主流的 MCP 教程几乎都是 Python 或 TypeScript,Swift 版本极少——这也让 “swift mcp server” 成为一个几乎无人竞争的关键词。
本文用一个能跑通的最小示例,带你从零构建一个 Swift MCP Server,并接入 Claude Desktop。
Model Context Protocol (MCP) 是 Anthropic 在 2024 年底提出的开放协议,用来标准化 “LLM 应用 ↔ 外部工具/数据源” 之间的通信。你可以把它理解成 “AI 应用的 USB-C”:
tools、resources、prompts协议本体是基于 JSON-RPC 2.0 的双向消息,通过两种传输承载:
| 传输 | 场景 | 特点 |
|---|---|---|
| stdio | 本地进程,Host 直接 spawn | 简单、零配置、无网络暴露 |
| Streamable HTTP / SSE | 远程或跨机器 | 需 Accept: application/json, text/event-stream |
对本地 Mac 工具来说,stdio 是默认选择。
多数教程默认 Python/Node,但用 Swift 有几个独特优势:
swift build -c release 产出一个静态二进制,Claude Desktop 直接 spawn,无 Python 环境依赖。Codable + enum 建模,工具 handler 天然并发安全。Package 里既能被 App target 用,也能被 MCP server target 用。一个最小可用的 Swift MCP Server 包含四层:
┌─────────────────────────────┐
│ Claude Desktop (Host) │
└──────────────┬──────────────┘
stdio │ JSON-RPC 2.0
┌──────────────▼──────────────┐
│ Transport (stdin/stdout) │ 按行读、按行写
├─────────────────────────────┤
│ JSON-RPC Dispatcher │ method → handler
├─────────────────────────────┤
│ MCP Protocol Layer │ initialize / tools/list / tools/call
├─────────────────────────────┤
│ Your Tools │ echo / read_notes / run_shortcut ...
└─────────────────────────────┘
新建一个 Swift Package:
mkdir SwiftMCPDemo && cd SwiftMCPDemo
swift package init --type executable
编辑 Package.swift(macOS 13+,用到 AsyncStream 与 Foundation 的 JSON 编解码):
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "SwiftMCPDemo",
platforms: [.macOS(.v13)],
targets: [
.executableTarget(name: "SwiftMCPDemo", path: "Sources/SwiftMCPDemo")
]
)
MCP 的每条消息都是 JSON-RPC 2.0。用 Codable 把请求 / 响应 / 错误建模一次,后面所有 handler 都复用:
import Foundation
struct RPCRequest: Decodable {
let jsonrpc: String
let id: JSONValue? // 可能是 number / string / null(通知无 id)
let method: String
let params: JSONValue?
}
struct RPCResponse: Encodable {
let jsonrpc = "2.0"
let id: JSONValue?
var result: JSONValue?
var error: RPCError?
}
struct RPCError: Encodable {
let code: Int
let message: String
var data: JSONValue?
}
/// 一个能表达任意 JSON 的枚举,避免到处写 [String: Any]
enum JSONValue: Codable {
case null
case bool(Bool)
case int(Int)
case double(Double)
case string(String)
case array([JSONValue])
case object([String: JSONValue])
init(from decoder: Decoder) throws {
let c = try decoder.singleValueContainer()
if c.decodeNil() { self = .null; return }
if let v = try? c.decode(Bool.self) { self = .bool(v); return }
if let v = try? c.decode(Int.self) { self = .int(v); return }
if let v = try? c.decode(Double.self) { self = .double(v); return }
if let v = try? c.decode(String.self) { self = .string(v); return }
if let v = try? c.decode([JSONValue].self) { self = .array(v); return }
if let v = try? c.decode([String: JSONValue].self) { self = .object(v); return }
throw DecodingError.dataCorruptedError(in: c, debugDescription: "Unsupported JSON")
}
func encode(to encoder: Encoder) throws {
var c = encoder.singleValueContainer()
switch self {
case .null: try c.encodeNil()
case .bool(let v): try c.encode(v)
case .int(let v): try c.encode(v)
case .double(let v): try c.encode(v)
case .string(let v): try c.encode(v)
case .array(let v): try c.encode(v)
case .object(let v): try c.encode(v)
}
}
}
MCP over stdio 用 换行分隔的 JSON(每条消息一行)。关键点:
actor StdioTransport {
private let stdin = FileHandle.standardInput
private let stdout = FileHandle.standardOutput
func readLines() -> AsyncStream<Data> {
AsyncStream { continuation in
Task.detached {
var buffer = Data()
while let chunk = try? self.stdin.read(upToCount: 4096), !chunk.isEmpty {
buffer.append(chunk)
while let nl = buffer.firstIndex(of: 0x0A) {
let line = buffer.subdata(in: 0..<nl)
buffer.removeSubrange(0...nl)
if !line.isEmpty { continuation.yield(line) }
}
}
continuation.finish()
}
}
}
func send(_ response: RPCResponse) throws {
var data = try JSONEncoder().encode(response)
data.append(0x0A) // '\n'
try stdout.write(contentsOf: data)
}
}
func log(_ msg: String) {
FileHandle.standardError.write(Data("[mcp] \(msg)\n".utf8))
}
定义一个 Tool 协议,让每个工具自描述 schema 并处理调用:
protocol Tool: Sendable {
var name: String { get }
var description: String { get }
var inputSchema: JSONValue { get } // JSON Schema
func call(arguments: JSONValue) async throws -> JSONValue
}
struct EchoTool: Tool {
let name = "echo"
let description = "Echo the input text back to the caller."
let inputSchema: JSONValue = .object([
"type": .string("object"),
"properties": .object([
"text": .object([
"type": .string("string"),
"description": .string("Text to echo back.")
])
]),
"required": .array([.string("text")])
])
func call(arguments: JSONValue) async throws -> JSONValue {
guard case .object(let obj) = arguments,
case .string(let text) = obj["text"] ?? .null else {
throw NSError(domain: "echo", code: 1,
userInfo: [NSLocalizedDescriptionKey: "missing `text`"])
}
// MCP tool 返回的是 content 数组
return .object([
"content": .array([
.object([
"type": .string("text"),
"text": .string(text)
])
])
])
}
}
MCP 一次会话至少要处理三个方法:initialize、tools/list、tools/call。
final class Server {
let transport = StdioTransport()
var tools: [String: any Tool] = [:]
func register(_ tool: any Tool) { tools[tool.name] = tool }
func run() async {
for await line in await transport.readLines() {
await handleLine(line)
}
}
private func handleLine(_ data: Data) async {
guard let req = try? JSONDecoder().decode(RPCRequest.self, from: data) else {
log("bad json: \(String(data: data, encoding: .utf8) ?? "?")")
return
}
var resp = RPCResponse(id: req.id)
do {
switch req.method {
case "initialize":
resp.result = .object([
"protocolVersion": .string("2025-06-18"),
"capabilities": .object([
"tools": .object([:])
]),
"serverInfo": .object([
"name": .string("swift-mcp-demo"),
"version": .string("0.1.0")
])
])
case "tools/list":
let list = tools.values.map { t in
JSONValue.object([
"name": .string(t.name),
"description": .string(t.description),
"inputSchema": t.inputSchema
])
}
resp.result = .object(["tools": .array(list)])
case "tools/call":
guard case .object(let p) = req.params ?? .null,
case .string(let name) = p["name"] ?? .null,
let tool = tools[name] else {
throw NSError(domain: "mcp", code: -32601,
userInfo: [NSLocalizedDescriptionKey: "tool not found"])
}
let args = p["arguments"] ?? .object([:])
resp.result = try await tool.call(arguments: args)
case "notifications/initialized":
return // 通知无需回复
default:
resp.error = RPCError(code: -32601, message: "method not found: \(req.method)")
}
} catch {
resp.error = RPCError(code: -32000, message: "\(error)")
}
if req.id != nil {
try? await transport.send(resp)
}
}
}
main.swift 里把它跑起来:
@main
struct App {
static func main() async {
let server = Server()
server.register(EchoTool())
log("swift-mcp-demo starting on stdio")
await server.run()
}
}
编译:
swift build -c release
# 产物路径
echo "$(pwd)/.build/release/SwiftMCPDemo"
编辑 ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"swift-demo": {
"command": "/绝对路径/SwiftMCPDemo/.build/release/SwiftMCPDemo"
}
}
}
重启 Claude Desktop。在对话框输入框左下角的 🔌 图标里应能看到 echo 工具。让 Claude 调用:
用 echo 工具回显 “Hello from Swift MCP”。
如果一切正常,Claude 会把返回内容展示回来。
print 都会破坏 JSON-RPC 帧。所有日志一律走 stderr。notifications/initialized**:Host 发来的通知没有 id,如果你也回一个响应会让客户端报协议错。判断 req.id != nil 再发送。inputSchema 里声明的 required 字段必须真的能从 arguments 里拿到,否则 Host 会跳过工具或报错。Accept: application/json, text/event-stream,否则官方 SDK 直接 406。stdio 走不通再考虑升级到 HTTP。Q:Swift MCP Server 能跨平台跑吗?
可以。核心代码只依赖 Foundation,Linux 上的 Swift 5.9+ 也能编译;要触达 macOS 专属 API(EventKit 等)时才会被平台绑定。
Q:需不需要自己实现 JSON-RPC,社区有没有现成库?
有官方 Swift SDK(modelcontextprotocol/swift-sdk)。生产项目直接用它;本文手写是为了把协议讲透。
Q:MCP Server 支持流式返回吗?
支持。工具可以在长任务里通过 notifications/progress 推进度,但要小心:客户端普遍有 30~60 秒左右的调用超时,超长任务应拆成 “创建 job → 查询结果” 两个工具。
Q:怎样调试?
最简单的办法:用 mcp-inspector(npx @modelcontextprotocol/inspector /path/to/SwiftMCPDemo)在浏览器里逐条查看请求与响应。
Q:MCP 会不会被 CLI 工具替代? 围绕 CLI vs MCP 有过一场讨论,但对于强类型、需要 schema 的 macOS 原生能力,MCP 仍然是最合适的封装。
Swift + MCP 是被严重低估的组合:一份 Swift Package 就能把 macOS 原生能力干净地暴露给任何符合 MCP 的 AI 客户端,无 Python、无网络、类型安全。这篇教程的完整代码可以直接复制运行;下一步建议:
EchoTool 换成 RunShortcutTool,用 Process 调 shortcuts run;read_notes 工具走 AppleScript / EventKit;.pkg 或 Homebrew tap,让别人一键装。如果你在做类似方向的实验,欢迎订阅本站 RSS 或看看姊妹项目 Open Agent SDK (Swift),那边把 “Agent Loop + MCP 集成” 完整跑通了。