我经常被问到这样一个问题:在 Mac 上做本地 AI,到底该用 Python 还是 Swift?过去两年这个答案几乎一边倒——HuggingFace 生态、transformers、各种推理脚本,全是 Python 的天下。但过去一年里,情况起了明显变化。Apple 官方正在把 Swift AI 工具链逐段补齐,从端侧模型到 MLX 本地 Agent,Swift 开发者终于不用先绕道 Python 才能把模型跑起来。这篇文章就聊聊我观察到的、也实际动手验证过的那条主线:官方组件到底补了什么、MLX 在 Agent 场景里怎么用、以及我自己在 Swift + MLX 这条路上趟出来的经验。适合正在做端侧 AI、想在 Apple 设备上跑私有模型、或者已经会用 Swift 但刚准备进入 AI 领域的读者。
1. Apple 为什么要补齐 Swift AI 工具链:先说清楚这个“补”字的分量
1.1 Swift 在 AI 圈的处境,以及端侧需求为什么绕不开 Swift
如果你过去两年混在 AI 应用开发圈,会有一个直观感受:Swift 几乎被排除在主流对话之外。模型训练、微调、推理脚本,大家默认都用 Python;连很多人跑本地大模型,都只是打开一个 Python 脚本调用 MLX 的 Python 包。Swift 更多是在 iOS/macOS 应用层做 UI、做逻辑,真正碰模型推理的人很少。
这很可惜,因为端侧 AI 恰恰是 Swift 的主场。Apple 设备上跑本地模型,你不可能开一个 Python 解释器常驻后台,也不可能把整条推理链路都用 Objective-C 重写一次。一个原生 Swift App 想要在用户手机上离线跑大模型,它天然就需要一整套能从模型文件一路走到 UI 渲染的 Swift 工具链。过去这套链路是断的:模型准备、量化、加载、推理、蒸馏、工具调用,每一段都要自己去拼接。
我也见过很多团队用折衷方案:Swift 写业务层,Python 写推理服务,两边通过本地 HTTP 或进程通信对接。这种架构能跑,但首次启动慢、资源占用高、调试链路过长,而且根本不适合做真正离线优先的 Agent。Apple 显然看到了这个裂缝。
1.2 Swift for TensorFlow 的教训:官方入局不是第一次,这次思路变了
说到 Apple 官方做 AI 工具链,很多人会想到早年的 Swift for TensorFlow。那是一次很有雄心的尝试,把 TensorFlow 的计算图直接接进 Swift 编译器,想做一门“原生 AI 语言”。后来大家都知道了,S4TF 在 2021 年前后逐步停摆,核心成员离场,官方精力转向了 MLX。
S4TF 的弯路给了我一个很重要的判断依据:Apple 这次做 Swift AI 工具链,不再追求语言层级的深度绑定,而是老老实实把“模型推理”这件事做成一个可用的运行时层。MLX 就是那个运行时。它不取代编译器的角色,而是作为一个数组计算框架存在,和 NumPy、PyTorch 的定位类似,但专门为 Apple 芯片的 Unity Memory 优化。Swift 开发者可以直接用 SPM 拉包、加载 HuggingFace 上的模型、做生成和工具调用,链路短了很多。
这个思路的变化很关键。以前官方想做的是“用 Swift 替代 Python 训练模型”,现在官方做的是“让 Swift 用户能直接消费现成的端侧模型”。前者要重建生态,后者是把生态接进来。后者虽然听起来没那么宏大,但对实际做产品的人来说,价值要直接得多。
1.3 “工具链”的完整拼图:模型、运行时、标注和 Agent 三环
我理解的“Swift AI 工具链”,至少由三个环节拼起来:
- 模型侧:从 PyTorch 权重到 Core ML / MLX 可加载格式,这是一整套转换、量化、封装工具。
- 运行时侧:Core ML 负责系统级低延迟推理,MLX 负责更灵活的研究和本地生成。
- 应用侧:LLM 编排、Tool Calling、Agent 循环、并发管理,最后落在 Swift 原生代码上。
三个环节缺一环都难受。以前缺第二环和第三环,所以大家只能 Python 跑 MLX、Swift 画界面,中间用 socket 通信。现在第二环有 MLX Swift API,第三环有越来越多的官方示例工程,整个链路终于可以从.mlx或.safetensors文件一路写到.app里。
我会在下一节先拆端侧模型,把 Core ML 和 Foundation Models 这块讲清楚,然后再单独讲 MLX 和 Agent。
2. 端侧模型是怎么跑起来的:Core ML、Foundation Models 与 Swift 的接缝
2.1 从 PyTorch 到 Core ML 的转换路径,动手怎么选
端侧模型落地的第一条路是 Core ML。Apple 官方提供了 coremltools 这套 Python 工具,可以把 PyTorch 或 TensorFlow 模型转成.mlmodel/.mlpackage格式。转换本身不难,难点在于算子和动态形状。
我自己的经验是,凡是涉及注意力结构、自定义算子、控制流的模型,直接转 Core ML 经常会碰到不支持的 op。遇到这种情况,要么切割子图,要么用 Core ML 的 Flexible Shape 重新标注,要么干脆把输入张量固定成静态 shape——但这又会让 Agent 场景里的动态长度变得很别扭。
如果你只是想快速验证一个模型能不能在 Apple 设备上跑,我的建议是先用coremltools.convert跑一遍,注意把minimum_deployment_target设成你的目标系统版本,比如 macOS 14.0 或 iOS 17.0。爆奇怪的 op 时,优先看看有没有 torch 算子可以替换。你也不需要一次转一个超大模型,可以先用 3B 级别的模型验证链路,再决定要不要往上加。
2.2 模型量化与端侧运行的基本参数
端侧跑模型,内存和算力都是硬约束。以 3B 参数的模型为例,FP16 大概要 6GB 权重内存,对手机不现实,对很多 Mac 机型也不友好。降下来主要靠量化。
- 8bit 量化:MX 系列芯片上能显著减内存,但延迟改善幅度有限。
- 4bit 量化:这个是我最常用的,3B 模型权重可以压到 2GB 上下。
- Core ML 还支持调色板压缩和稀疏化,有些场景能压得更狠,但要看模型结构是否支持。
量化后的精度损失,对 Agent 的工具调用影响是需要重点测试的。小模型 4bit 之后,如果温度拉高,可能出现 JSON 格式错乱、参数名拼错、工具名称幻觉等问题。所以我在跑 Agent 时不会把温度设到 0.9 以上,通常 0.6 到 0.8 之间,不然解析工具调用会让人调到头秃。
这里也顺便放一张对比表,方便大家看清 Core ML 和 MLX 在端侧链路里的定位差异。
| 维度 | Core ML | MLX |
|---|---|---|
| 定位 | 生产部署、系统级推理 | 研究、本地生成、快速原型 |
| 计算图 | 静态优化为主 | Lazy 动态图,按需执行 |
| 硬件调度 | CPU/ANE/GPU 自动管线 | 优先 GPU/CPU 统一内存 |
| 模型格式 | mlmodel/mlpackage,需转换 | 直接加载 safetensors/MLX 权重 |
| 上手成本 | 转换链略重 | 社区权重多,Swift 拉包即用 |
2.3 官方 Foundation Models 在端侧扮演的角色
很多人会问:苹果不是已经做了 Foundation Models 吗?那是不是普通开发者也能直接调用?
我理解官方 Foundation Models 的思路是系统级整合。它更多服务于 Siri、系统写作工具、摘要等内置能力,是 Apple Intelligence 的底座组件。作为第三方开发者,你能看到的是系统封装好的 API,而不是“把某个 3B 权重拿来随便跑 prompt”那种自由度。
所以如果你想做的是自己的 Agent、自己的私有知识库、自己的工具调用流程,MLX 反而是更实际的选择。它不是替代 Core ML 或 Foundation Models,而是给了开发者一块真正可控的“自留地”。这一点接下来单独展开。
3. MLX:这才是本地 Agent 真正能用起来的运行时
3.1 统一内存:为什么 MLX 在 Apple Silicon 上有天然优势
MLX 与 NumPy、PyTorch 这类传统框架最大的不同,在于它天然围绕 Apple Silicon 的 Unified Memory 设计。传统电脑上,GPU 显存和 CPU 内存是分离的,模型权重放显存、数据经过 PCIe 拷贝,这些开销在小模型上不明显,但跑 7B 级别的大模型时就会成为瓶颈。
在 Apple Silicon 上,CPU 和 GPU 访问的是同一块物理内存。MLX 直接把数组分配在统一内存空间里,模型权重、中间激活、KV Cache 都可以让 CPU 和 GPU 无拷贝共享。这意味着你写 Swift 代码时,把 MLXArray 当普通张量处理,来回访问数据的心理负担小很多。
我实际跑的体感是:一个 3B 4bit 量化的模型,在 M 系列芯片上加载到生成第一个 token 的时间往往在一两秒内。这个速度对于端侧 Agent 的“思考-调用工具-再思考”循环来说,是完全可以接受的。
3.2 MLX 的几个关键设计:懒加载、模型共享与 mmap 加载
MLX 不只是“快”,它在工程上还解决了好几个本地推理的老问题。
- Lazy 求值:你构造计算图时不会立即执行,而是等真正需要结果时才跑。这让框架有机会合并算子、延迟内存分配,对长序列生成尤其友好。
- 多设备支持:MLXArray 可以显式放到不同 device 上,比如 GPU 或 CPU,代码里用
.gpu()/.cpu()就能迁移。 - 模型文件 mmap:部分模型权重可以直接 mmap 到内存,按页加载,而不是一次性把几个 GB 全部读进内存。这对我这种内存不算大的 Mac 尤其重要,启动快,还能留给系统更多缓存空间。
我最早开始用 MLX 的 Swift 包时,一个很大的感受是:它不像一个套壳 API,而是真正为本地模型设计过的运行时。你不需要手工管理 CUDA 显存,不需要关心虚拟内存换页,大部分时候把权重塞进统一内存就够了。
3.3 MLX 与 Core ML 的分工,别把两者搞混
我接触的开发者里,经常有人把 MLX 理解成“Core ML 的替代品”。其实分工不太一样:
- Core ML 更像生产部署的最终形态,适合你已经定下来的模型、固定输入输出、要放进 App 里给普通用户用。
- MLX 更像研究/快速迭代层,它和你调模型、调 prompt、调工具调用逻辑的阶段匹配。
也就是说,你可以在 MLX 里把整个 Agent 跑通,验证好功能,再考虑是否有必要把部分模型转成 Core ML 做分发。对大多数个人项目和中小团队来说,直接在 MLX 上跑 Agent 已经是够用的方案,多一步转 Core ML,往往只是为了安装包体积和系统集成。
下一节我会直接把 Swift + MLX 本地 Agent 的代码拆开,从模型加载一路写到工具调用。
4. 手写一个 Swift + MLX 的本地 Agent:从加载模型到工具调用
4.1 环境准备:MLX Swift 包、模型下载与缓存
我用的依赖是官方维护的mlx-swift,里面分了好几个子包,包括MLX、MLXLLM、MLXLMCommon。在 Package.swift 里加上对应依赖就能跑。
// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "LocalAgentDemo", platforms: [.macOS(.v14)], dependencies: [ .package(url: "https://github.com/ml-explore/mlx-swift.git", from: "0.19.0") ], targets: [ .executableTarget( name: "LocalAgentDemo", dependencies: [ .product(name: "MLX", package: "mlx-swift"), .product(name: "MLXLLM", package: "mlx-swift"), .product(name: "MLXLMCommon", package: "mlx-swift") ] ) ] )模型推荐优先去 HuggingFace 搜mlx-community开头的权重,比如mlx-community/Llama-3.2-3B-Instruct-4bit。这些权重已经转成 MLX 可加载结构,省掉了自己从 safetensors 转换的步骤。第一次加载会把模型拉进缓存,之后都是直接读取。
import MLX import MLXLLM import MLXLMCommon let config = ModelConfiguration.llama3_2_3B let container = try await LLMModelFactory.shared.loadContainer(configuration: config)loadContainer会把模型权重、tokenizer、配置全部打包成一个对象。你可以把它理解成本地推理的原子里程碑式入口。
4.2 生成器循环:参数、采样、停止
拿到容器之后,生成一段文本其实非常直接:
let messages: [[String: String]] = [ ["role": "user", "content": "用三句话解释一下端侧模型的价值"] ] let parameters = GenerateParameters(temperature: 0.7, topK: 40) let result = try await container.generate(messages: messages, parameters: parameters) print(result)这里的关键参数我会单独说:
temperature控制随机性。Agent 场景我推荐 0.6 到 0.8,太高容易让工具调用输出 JSON 变形。topK限制采样候选。小模型上topK=40比较平衡。maxTokens一定要设上限,不设的话长输出可能直接把内存占满。Agent 工具调用场景我通常给 512 到 1024。
生成器底层是流式产出 token,container.generate这个便捷 API 适合快速验证。如果你想做流式 UI,可以走回调版本,把每个 token 逐步推给界面。
4.3 让 Agent 调用工具:tool parsing 与结果回填
本地 Agent 的关键不在生成文本,而在“模型决定调用工具、解析参数、执行工具、把结果喂回模型”这个循环。
以一个天气查询工具为例。模型需要先知道存在一个get_weather(city: String)工具,这个描述直接放在 system prompt 里:
You can call these tools: get_weather(city: string) Reply in JSON: {"tool": "get_weather", "params": {"city": "北京"}}模型的第一轮输出往往是一段 JSON。你需要解析它:
struct ToolCall: Codable { let tool: String let params: [String: String] } let decoded = try JSONDecoder().decode(ToolCall.self, from: output.data(using: .utf8)!)拿到工具名和参数之后,在 Swift 里执行真实逻辑,比如查本地最近天气缓存或调用系统 API。然后把结果作为新的 user message 回填,让模型基于工具结果继续回答。
这一步看起来简单,但实测里最容易翻车的是 JSON 解析。模型可能输出多余文字、漏掉右括号、或者把参数名从city变成City。我自己的做法是:
- 在 prompt 里给一个标准 JSON 示例。
- 解析失败时不要崩溃,把原始输出返回给模型,请它重新输出严格 JSON。
- 对数值参数用
Double/Int严格解码,别默认 String。
4.4 Swift 并发安全:多任务调用模型为什么必须加锁
Agent 一旦接上真实工具,就不可避免要用到 Swift Concurrency。比如你可能同时发起几个异步任务,其中一个要用 LLM 做摘要,另一个要调用模型做工具决策。这时候一个很容易踩的坑就来了:MLXLMCommon的容器内部是有可变状态的,多个Task并发调用同一个容器,轻则输出错乱,重则直接崩溃。
我建议把所有模型调用收敛到一个actor里,让模型访问变成串行:
actor InferenceActor { private let container: LLMModelContainer init(container: LLMModelContainer) { self.container = container } func run(prompt: String) async throws -> String { let messages = [["role": "user", "content": prompt]] return try await container.generate(messages: messages, parameters: defaultParams) } }这样外面随意派发任务,最终到模型层的请求都是有序的。如果你确实需要并行,多实例化一组容器、然后做任务分组,比单容器并发调用要安全得多。
5. 实测避坑:模型格式、性能调优和并发问题这几关怎么过
5.1 模型转换优先级:MLX 社区权重优先,别先扎进 Core ML
很多新手拿到一个模型的第一反应是:先转 Core ML。我的建议恰恰相反:先找mlx-community现成权重,先用 MLX 跑通 Agent,再考虑 Core ML 的事情。
理由很简单:你在 Agent 阶段调模型 prompt、调工具调用格式、调停止策略,这些迭代可能一天几十次。如果每次都要先转一遍 Core ML,时间成本会把热情拖没。MLX 是研究到生产之间最短路径,先跑起来,比一开始就“做正确格式”重要得多。
我自己踩过的坑是,想当然地拿一个 safetensors 原始权重丢给 MLX 加载,结果报错一大堆,后来才发现需要先用mlx_lm.convert转成 MLX 格式,尤其是量化权重和tokenizer_config需要联动处理。这个问题在mlx-community权重上不存在,所以请直接搜别人转好的。
5.2 内存峰值、温度参数与输出长度的平衡
端侧跑本地 Agent,最常见的问题是“跑着跑着内存满了”。3B 4bit 模型本身 2GB 左右,但生成时要算中间激活和 KV Cache,长度一长,峰值还能再涨一两 GB。16GB 内存的 M 系列机器跑 3B 是够的,但要同时开很多大型 App 就不保险。
我总结了一套比较省心的配置:
- 模型用 4bit 量化权重。
maxTokens设 512,工具决策足够了。- 温度设置 0.7,稳定性和多样性平衡。
- 每次调用前先释放上一轮的大数组,用
autoreleasepool或等效的 Swift 作用域管理。 - 如果要做长对话,定期裁剪历史消息,不要让 prompt 无限膨胀。
按这套配置,3B 模型的响应延迟在 M 系列芯片上通常可控,交互感上和云端小模型差距不大。
5.3 并发调用与断线重连的实用处理
最后说一个比较隐蔽的坑:本地 Agent 也分“工具层并发”和“模型层并发”。工具层可以放开并发,比如同时查天气、查日历、查文件,它们互不干扰。但模型层必须小心。
我在项目里一般是这样分工:
| 场景 | 并发策略 |
|---|---|
| 多个工具同时执行 | 自由的 TaskGroup,没问题 |
| 模型生成结果 | 收敛到单 actor 串行 |
| 模型生成期间 App 临时切后台 | 暂停请求,保留容器,尽量不释放模型 |
| 工具返回结果后再次调用模型 | 重新进入 actor,等待前一轮完成 |
这个分工解决了我印象最深的崩溃问题:最开始我直接在 SwiftUI 里开了几个Task同时调用模型,不到十分钟必崩一次。把所有模型调用收进 actor 之后,运行一整天也没再出过问题。
5.4 一个小技巧:把 Agent 的推理日志单独打出来
最后分享一个调试技巧。Agent 和普通文本生成不同,模型输出里包含工具调用意图、系统回填、最终回答,几段内容混在一起,很容易把人绕晕。
我会把每一轮循环的输入输出单独打成结构化日志,包括:
- 当前 prompt 长度和 token 数。
- 模型原始输出内容。
- 解析得到的 JSON 结构。
- 工具执行结果摘要。
- 回填后的新 prompt 摘要。
这样每次模型抽风,你都能快速判断是 prompt 问题、解析问题还是工具执行问题。我的经验是,Agent 真正难调的地方,往往不在模型,而在“模型和工具之间的那层胶水代码”。把日志做细,比反复改 prompt 有效得多。
Apple 官方把 Swift AI 工具链补齐这件事,让我最在意的不是某个模型多强,而是 Swift 开发者终于有了一个能闭环的路径:端侧模型跑得起来,MLX 本地 Agent 写得顺,并发问题也有官方 API 可依。按我现在的习惯,拿到一个新模型,第一件事就是看有没有 mlx-community 权重,而不是先纠结 Core ML 分发;跑通 Agent 之后,再考虑哪一层要转成更底层的系统部署格式。这条路已经比我两年前体感的顺畅太多了。