移动端AI Agent完全指南:端侧模型、Function Calling与混合路由实战
2026/9/21 18:09:53 网站建设 项目流程

上个月我干了一件事:把公司里那套跑在 Linux 服务器上的 AI Agent 骨架砍掉一半,塞进了一台 iPhone 和一台 iPad Pro。跑通的那一刻,说实话没有想象中那么兴奋,反而有点落差——iPhone 端侧那个 7B 模型生成一段工具调用的 JSON,要花两三秒,跟服务器上几十毫秒完全两个体验。但真正用了一周之后,我改变了看法:移动端 Agent 的真正价值不在“快”和“大”,而在离你的数据够近。

这篇文章想写给两类人。一类是在做 AI Agent 开发、正考虑“能不能在移动端跑”的工程师,另一类是买了新款 iPhone/iPad、想自己动手搭一个“贴身助手”的玩家。我会把我是怎么做选型、怎么搭骨架、怎么让它调用日历和提醒事项、以及一路上踩过的坑都整理出来。

先说结论:不要把“完整 Agent”理解成“模型全在本地”。我最后落地的架构是“本地小模型 + 云端大模型 + 一套完整的工具与记忆系统”,模型的“大脑”可以切换,但工具层、记忆层和交互层全部跑在设备上。这也就是我标题里说的“几乎完整”——Agent 该有的组件都有了,不完整的地方在于复杂推理仍然需要借助云端 API 的算力。

1. 在移动端跑 Agent:先想清楚这三件事

1.1 为什么是 iPhone / iPad,而不是 Mac 或服务器

先说一个很多人的误区。看到“AI Agent 跑在 iPhone 上”,第一反应是“这能跑什么大模型?”实际上,如果不是非要在端侧跑 70B 级别的模型,iPhone 和 iPad 在 Agent 这个场景里完全可以胜任,而且有些能力是服务器给不了的。

服务器上的 Agent,通常做的是“无人值守的自动化”。比如监控数据变化、定时抓网页、自动运维、批处理文档。它和你之间的关系是“任务下发-结果返回”。但移动端 Agent 的强项是“贴身数据”:你的日历、你的提醒事项、你的定位、你的相册、你的剪贴板、你的快捷指令。这些东西天然散落在手机里,服务端要拿还真不容易。我之前在服务端想整合日历和位置信息,得走云同步、开发权限、处理各种隐私协议,折腾两周。而在 iPhone 上,一个 EventKit 权限弹窗就搞定了。

所以我的判断是:如果你的 Agent 定位是“个人助理”,不是“后台机器人”,那移动端就是最合理的载体。这也是我把项目目标从“在服务器上跑一个智能体”改成“在手机里跑一个贴身智能体”的根本原因。

1.2 “几乎完整”到底差在哪

先说完整 Agent 的组件,我一般拆成五块:

  • 模型:负责理解和生成,是 Agent 的“大脑”。
  • 工具:模型能调用的外部功能,如日历、搜索、计算器、快捷指令。
  • 记忆:短期对话上下文 + 长期事实/偏好存储。
  • 循环:也就是 Agent Loop,模型在“思考-调用工具-观察结果-再思考”之间反复迭代。
  • 入口:用户怎么和 Agent 交互,聊天界面、语音、按钮、快捷指令都算。

我在移动端把这五块全部实现了。模型分两层,小模型本地跑、大模型走云端 API;工具实现了日历、提醒事项、闹钟、剪贴板、打开网页、运行快捷指令;记忆用 SQLite 加本地 embedding 实现;循环层写了一个带最大迭代次数限制的 Swift 状态机;入口是一个 SwiftUI 的聊天窗口加几个快捷指令槽位。

不完整的地方主要在两点。一是模型能力受限,本地 7B 模型的推理质量、角色设定能力和复杂 JSON 输出稳定性都不如云端大模型,所以 Agent 遇上复杂任务时我需要切到云端。二是后台运行受限,iOS 对 App 的后台生命周期控制很严,Agent 不可能像服务器那样常驻内存、后台自动循环。这两个限制,决定了我只能叫它“几乎完整”。

1.3 选型对比:本地推理、云端 API、还是混合

动手之前我列了一个表,把自己的需求过了一遍。

方案优点缺点适合场景
纯本地推理(llama.cpp / Core ML)离线可用、隐私最好、无 API 费用模型能力弱、生成速度慢、占用内存大轻量指令、隐私敏感数据
纯云端 API模型强、速度快、工具调用稳定依赖网络、有费用、数据出设备复杂对话、网页总结、知识问答
混合路由灵活、平衡隐私与能力实现复杂、路由策略需要调优个人助理、伴你一整天的 Agent

如果你只是好奇玩一下,我建议先做纯云端 API 版本,把 Agent 的“骨架”跑通,再加上工具和记忆。等骨架验证了,再逐步把单轮简单任务切到本地模型。我一开始就贪心想一步到位,结果前三天全在跟模型格式作斗争,Agent 的核心循环根本没碰。

我的最终选型是混合路由:短指令、隐私数据、离线场景走本地 7B 量化模型;复杂推理、上下文超过窗口、需要稳定工具调用时自动切到 DeepSeek、Qwen 这类国内可直连的云端 API。这样既保住了隐私底线,又不至于被端侧模型气死。

2. 移动端 Agent 的组成结构:核心细节拆解

2.1 模型层:llama.cpp 还是 MLX,量化怎么选

把 LLM 放到 iPhone 上跑,现在主流有两条路:llama.cpp 和 MLX。

  • llama.cpp:C++ 写的推理引擎,对 Apple Silicon 的 Metal GPU 支持很成熟,iOS 工程里可以通过静态库或 Swift Package 集成。优点是生态大、量化格式 GGUF 普及、各种模型都能转,缺点是封装比较“原始”,要自己做内存管理和前后处理。
  • MLX:Apple 官方的机器学习数组框架,用起来很“Swift 原生”,对模型架构做了不少优化。但它在 iOS 上目前还不够成熟,官方示例主要集中在 macOS,在 iPhone 上跑 LLM 的社区方案大多还在实验期。

我在 iPhone 上用的是 llama.cpp,原因很简单:稳定、能跑、可复现。具体集成我用的是一个基于 llama.cpp 的 Swift 封装库,支持加载 GGUF 格式的量化模型。

模型选择我走了几个弯路,最开始拿 13B 模型试,iPhone 15 Pro 跑是能跑,但内存吃紧,App 一做大就容易被系统杀掉。后来把主力模型定在 7B 以下,并且全部用 Q4_K_M 量化。量化等级这事儿,Q8 精度好但内存占用直接翻倍,Q4 在移动端是性价比最高的档位。你要是只在 iPad 上跑,内存大,可以试试 Q6。

我实测的参考数据大致是这样(单位:token/s,设备不同有差异):

设备3B Q47B Q413B Q4
iPhone 15 Pro15~206~102~4
iPad Pro M225~3012~184~6
iPhone 128~122~4基本不可用

注意这是“文本生成”的速度,Agent 场景里模型每次只生成一小段 JSON 调用,几十 token 的程度,所以 8 token/s 也能顺畅跑起来,不必被这个数字吓到。

2.2 工具层:Function Calling 的本质是“输出 JSON”

很多人一听到 Function Calling,以为框架里有什么神奇机制能直接把函数绑给大模型。其实本质很简单:你给模型一份函数清单(名字、描述、参数 JSON Schema),模型读完用户指令后,选择要不要调用某个函数,然后输出一段符合格式的 JSON 文本。框架负责解析这段 JSON,执行真实函数,再把结果以“工具返回消息”的形式塞回对话。

所以在移动端实现工具层,核心是两件事:一是把工具描述准确塞进 prompt 或 API 的 tools 参数,二是严格解析模型输出的 JSON。

云端 API 通常原生支持 tools 参数,比如 DeepSeek 的 chat completions 接口,你把工具列表传进去,模型返回 tool_calls 字段。本地模型就麻烦一点,我用的是 llama.cpp 的 GBNF grammar 功能,在生成时把 JSON Schema 转成一份上下文无关文法,强制模型只能输出合法 JSON。这是我最推荐的做法——比“先生成文本、再解析、失败重试”稳定得多,基本能把格式错误率降到接近零。

工具列表我实现了这几个:

  • 日历查询与创建:EventKit,读取一周日程、新建事件。
  • 提醒事项:EventKit,创建带时间的提醒。
  • 闹钟:闹钟 API 权限有限,我用快捷指令中转实现。
  • 剪贴板:读和写系统剪贴板。
  • 打开 URL:用 UIApplication.open 唤起指定链接。
  • 运行快捷指令:通过 URL Scheme 触发某个 Shortcuts。

每个工具都用一个 Swift 协议统一封装:

protocol AgentTool { var name: String { get } var description: String { get } var parameters: String { get } // JSON Schema 字符串 func run(arguments: [String: Any]) async throws -> String }

工具层设计上有个小细节很多人会忽略:每个工具的描述和参数说明要写得非常啰嗦。比如“创建提醒”的 date 参数,要写明格式是 ISO 8601 且包含时区,否则模型会自由发挥成“明天下午3点”,你的解析器就疯了。

2.3 记忆层:短期上下文窗口 + 长期向量记忆

Agent 没有记忆就是人工智障,这句话在移动端一样成立。但移动端的内存和流量都不能随便霍霍,所以我的记忆方案分两层。

短期记忆就是普通的对话历史列表。云端 API 模式下,我把最近 10 轮以内的消息发给模型;本地模式下,由于上下文窗口有限,我只保留最近 6 轮,并且在超出窗口时自动裁剪。这里有个经验:不要盲目把所有历史都塞进模型,token 多不仅费钱,还会让模型注意力涣散,工具调用的准确率反而下降。

长期记忆我放在 SQLite 里。每次用户说了一句关键信息(比如“我周五下午开会”、“我喜欢窗口座位”),我会把它写入一个叫 memory_items 的表,同时生成一个 embedding 向量。查询时用两种方式召回:先按关键词过滤,再算向量余弦相似度排序,留下最相关的 5~10 条拼进系统提示。

向量计算我本来想用云端 embedding,后来发现移动端 Core ML 加载一个小型 embedding 模型就能完成,300 维,速度毫秒级,干脆全部本地化。这样长期记忆的读写完全不出设备,隐私这块也能拿出来说事。

可以把记忆理解成一本私人笔记本:短期记忆是正好摊开的这一页,长期记忆是整个柜子里的索引卡片。Agent 每次干活之前,都要翻一下索引卡片,找到最相关的几页,夹到当前页旁边,再开始推理。

2.4 入口与交互:App 壳、语音、快捷指令

移动端 Agent 的交互和网页窗口完全不一样。用户不会老老实实坐在屏幕前打字,大部分时候是“一句话触发一个动作”。所以我把入口做得比较多样。

主入口是一个 SwiftUI 聊天界面,支持文本输入和语音输入。语音用 Speech 框架做实时听写,识别结果直接进入消息列表;回复时用 AVSpeechSynthesizer 读出来。这一个组合就能覆盖大多数“边走边说”的场景。

辅助入口是快捷指令槽位。我用 URL Scheme 注册了几个触发器,比如“提醒我”、“待办速记”、“日程快查”。用户在控制中心、锁屏或者 Siri 里直接唤起这些快捷指令,不需要打开 App 就能给 Agent 发命令。这一套下来,Agent 就从“一个聊天应用”变成了“操作系统级的助手”。

需要提醒的是,语音识别框架需要在 Info.plist 里申请 NSSpeechRecognitionUsageDescription 和 NSMicrophoneUsageDescription 权限,这两个不配置,App 启动就会崩。

3. 实操过程:从零搭一个能用的移动 Agent

3.1 环境准备与工程骨架

我的开发环境是 Xcode 15、iOS 17 起步,设备是 iPhone 15 Pro 和 iPad Pro M2。没有 Mac 的话,用 Swift Playgrounds 也能写 SwiftUI,但集成 llama.cpp 静态库会比较麻烦,建议还是老老实实用 Xcode。

工程结构大概这样:

  • AgentApp:SwiftUI App 入口。
  • AgentCore:核心循环、消息模型、状态管理。
  • Tools:各种 AgentTool 实现。
  • Memory:SQLite 持久化 + embedding。
  • Engine:LLM 推理封装,本地和云端两个实现。

先说核心循环,可以看成一个简化的状态机:

final class AgentLoop: ObservableObject { @Published var messages: [ChatMessage] = [] func run(userInput: String) async throws { messages.append(.user(userInput)) var steps = 0 while steps < maxIterations { steps += 1 // 1. 组装带记忆和工具列表的请求 let systemPrompt = try await buildSystemPrompt() let request = LLMRequest(messages: systemPrompt + messages, tools: tools) // 2. 调用本地或云端模型 let llmResponse = try await engine.complete(request) // 3. 如果模型决定调用工具,就执行 if let toolCall = llmResponse.toolCalls.first { let result = try await executeTool(toolCall) messages.append(.tool(toolCall.name, result)) continue } // 4. 没有工具调用,说明模型想直接回复用户 messages.append(.assistant(llmResponse.text)) break } } }

关键点在第三、四步。Agent 可以连续调用多个工具,比如“先查日历,再创建提醒”,可能模型第一次只调日历,拿到结果后再调提醒工具,我们循环继续跑,直到它直接给用户回复或者达到最大步数。最大步数我设的是 8 步,防止模型陷入死循环。

3.2 接入云端模型与 Function Calling

先用云端模型跑通骨架,是最省力的路径。我用的是 DeepSeek 的 chat completions 接口,因为它兼容 OpenAI 格式,且在国内访问稳定。你换成 Qwen、Kimi 也基本一样,只改 baseURL 和 model 名即可。

请求体大概是这样的:

{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是运行在用户手机里的个人助手,调用工具时只输出工具调用,不要解释。"}, {"role": "user", "content": "明天上午十点提醒我交周报"} ], "tools": [ { "type": "function", "function": { "name": "create_reminder", "description": "在提醒事项中创建一条带时间的提醒", "parameters": { "type": "object", "properties": { "title": {"type": "string"}, "date_time": {"type": "string", "description": "ISO 8601 格式,带时区"} }, "required": ["title", "date_time"] } } } ] }

返回里会有一个 tool_calls 数组,里面是模型决定要调用的函数名和参数:

{ "choices": [{ "message": { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "create_reminder", "arguments": "{\"title\":\"交周报\",\"date_time\":\"2025-06-12T10:00:00+08:00\"}" } }] } }] }

注意一个坑:模型返回的 arguments 是一个 JSON 字符串,不是对象,必须先反序列化再传给工具。我踩过这个坑,第一次直接当字典用,崩了。

拿到 tool_calls 后,执行对应的 AgentTool,然后把结果返回给模型。在 message 列表里追加一条 role=tool 的消息,带上 tool_call_id,这样模型就能看到工具执行结果并继续规划。细节很多,但核心就是“多轮补充消息直到模型停止调用工具”。

3.3 端侧模型加载与推理路由

当骨架跑通后,我开始把简单的日常问题切给本地模型。端侧我集成的是 llama.cpp 的 Swift 封装,加载一个 GGUF 格式的 Qwen2.5-3B-Instruct-Q4_K_M.gguf 文件。

加载代码很直接,但有几个参数必须调:

  • context size设 2048 就够用,设太大内存会爆。
  • batch size设 512,影响首 token 速度。
  • 启用 Metal GPU,也就是 use_metal = true,A 系列芯片上的加速是很明显的。

本地模型的主要用途是三类:离线场景、隐私敏感的短文本处理、一些不需要复杂推理的简单问答。云端则负责复杂任务。路由规则我用一个评分函数:

func shouldUseLocal(_ input: String, contextCount: Int) -> Bool { if let current = Locale.current.languageCode, current != "zh" { return false } if input.count > 200 { return false } if contextCount > 6 { return false } if input.contains("总结") || input.contains("分析") || input.contains("搜索") { return false } return true }

这个规则非常粗糙,但够用。你可以把它想成一个智能闸门:简单问题放行进本地,复杂问题直接上云端。真正的通用路由需要对每个模型做基准评测,我后面打算引入一个基于成本和质量的双层路由,但首次落地,经验规则最可靠。

3.4 实现日历和提醒工具

移动端 Agent 最实用的价值,就是帮用户操作真实系统数据。日历和提醒事项是我首批接入的工具,都走 EventKit 框架。

创建提醒的代码不长,权限和事件保存是关键:

final class ReminderTool: AgentTool { let name = "create_reminder" let description = "在提醒事项中创建一条提醒" let parameters = """ { "type": "object", "properties": { "title": {"type": "string", "description": "提醒内容"}, "date_time": {"type": "string", "description": "ISO 8601 日期时间,含时区"} }, "required": ["title", "date_time"] } """ func run(arguments: [String: Any]) async throws -> String { let store = try await ReminderStore.shared() let formatter = ISO8601DateFormatter() let date = formatter.date(from: arguments["date_time"] as! String)! try await store.createReminder( title: arguments["title"] as! String, dueDate: date ) return "已创建提醒:\(arguments["title"]!) 时间:\(arguments["date_time"]!)" } }

实际运行时我发现一个体验问题:模型生成的 date_time 经常是本地时间但没有时区标记,导致 ISO8601 解析失败。我的解决办法是,在参数描述里强制要求带时区偏移,并且注入一条默认时区说明到系统提示里。系统提示里写一句:“默认时区为 GMT+8,所有时间按北京时间为准”,模型生成的质量立刻提升。

日历查询工具也是 EventKit,列未来七天的日程摘要。这里有个好用的细节:查询接口返回的事件对象里,title 和 startDate 是核心字段,但很多日程的 title 是空的,需要用 location 或 notes 兜底,否则工具返回内容会是空壳。

3.5 把 Agent 跑起来:一个完整的演示场景

写到这里,给一个真正跑通的演示场景。我在 iPad 上通过快捷指令唤起 Agent,然后说:“周五下午三点有产品评审,提前半小时提醒我。”

整个流程分五步:

  1. 语音进入,Speech 识别成文字,进入 AgentLoop。
  2. 本地路由判断:这句话单独看挺简单,但涉及“创建事件+创建提醒”两步,云端模型更稳,于是走了 DeepSeek。
  3. 模型第一次调用 calendar.create_event,参数:title=产品评审,startDate=2025-06-13T15:00:00+08:00。工具执行完返回“已创建日程”。
  4. 模型第二次调用 reminder.create_reminder,参数:title=产品评审提醒,date_time=2025-06-13T14:30:00+08:00。工具执行完返回“已创建提醒”。
  5. 模型最终输出:“已经帮你把周五下午三点的产品评审加到日历,并且设置了下午两点半的提醒。”

从说话到完成,用了大约 6 秒。比纯端侧快不少,中间有一次工具调用不是一次成功的,第一次模型把时间参数写成了“周五下午3点”这种自然语言,被我加了时区约束后重试才过。这说明工具调用的稳定性是不断调优出来的,不是一次就能磨到 100%。

4. 常见问题与排查技巧实录

4.1 模型加载慢、内存崩溃

用 7B 模型在 iPhone 上跑,最典型的现象是 App 启动后加载模型要 20 秒,而且经常在后台切换时被系统杀掉。

我排查后确认了两个原因:一是模型文件在 App Bundle 里,首次启动要复制到可写空间,慢;二是加载器默认把整份模型读进内存,加上 context 缓冲,导致内存占用飙高。

解决方法:

  • 模型首次下载后统一放 Documents 目录,后续启动直接 mmap 映射,不用整体加载。
  • 把 context size 从 4096 降到 2048,内存能省出 1GB 左右。
  • 7B 模型只保留 Q4 量化版本,iPhone 上前台运行勉强流畅,iPad Pro M2 就比较宽裕。
  • 如果频繁被杀,在 Info.plist 里的 memory warning 回调也加上,收到内存警告时清掉历史消息并释放 embedding 缓存。

内存管理这块,移动端 Agent 不比 Web 服务端,能省则省。

4.2 Function Calling 伪造参数

我在端侧 3B/7B 模型上遇到过很多次“模型编参数”:用户问“今天天气怎么样”,模型居然调用 create_reminder,参数写的是“天气提醒”。这就是小模型能力不足、对工具语义理解不够。

后来我的解法是两层:

  • 端侧模型生成前,用 GBNF grammar 约束只允许输出合法 JSON 结构,但这只能解决格式问题,不能解决“调用错误工具”的问题。
  • 工具调用后必须做参数 schema 校验,日期解析失败就返回一个错误消息给模型,让模型重新规划。如果连续两次调用同一工具都失败,就直接放弃,改用云端模型重试。

这里分享一个调参经验:工具描述里的动词要具体,比如“create_reminder”比“remind”更容易被小模型理解;参数名称用 snake_case 且语义完整,date_time 比 time 好得多。这些细节在小模型上会被无限放大。

4.3 iOS 后台生命周期的现实

服务器上的 Agent 可以 7x24 小时跑,iOS 上不行。App 进后台几十秒就会被挂起,网络请求也会被暂停。我最初想做一个“后台定时巡逻”功能,折腾了 BGTaskScheduler,发现系统给的时间窗口非常短,根本跑不了连续循环。

我的经验是别跟系统对抗。把 Agent 设计成“前台交互+快捷指令触发”的模式,需要自动化的场景用快捷指令的自动化规则(比如到达某地、时间触发)唤起 App,然后 Agent 在前台完成一系列操作。这样既绕开了后台限制,又符合移动平台的使用习惯。

4.4 API Key 安全与网络配置

云端模型一定要用 API Key。如果不做任何保护,直接把 Key 写在 App 里发布,被反编译只是时间问题。

安全做法有两种:

  • 自建一个极简网关,App 把请求发到你在 Cloudflare Worker 或轻量服务器上部署的服务,由网关注入 Key 再转发给模型厂商。
  • 如果只是自用,不打算上架 App Store,Key 放进 Keychain 也能接受,但要注意别提交到 Git。

网络层还有一个 iOS 特有的坑:App Transport Security 默认禁止所有 HTTP 明文请求。如果你的自建网关是 HTTP,必须在 Info.plist 里加 NSAppTransportSecurity 的 NSAllowsArbitraryLoads,或者配置域名白名单。我现在全部走 HTTPS,就不用碰这个坑。

4.5 常见问题速查表

问题常见原因快速解决
模型加载 20 秒首次从 Bundle 复制文件下载到 Documents,用 mmap 映射
后台切换后 App 被杀内存占用过高降 context、清缓存、减少并发
工具参数总是解析失败模型输出自然语言时间强制 ISO 8601 + 时区说明
语音识别无结果没申请权限或设备静音检查 Info.plist、确认 Siri 可用
请求云端超时网络请求时间过长设置超时重试,减少上下文长度
模型重复调用同一工具小模型陷入死循环限制最大步数、工具返回错误信息

这些坑我基本踩了一遍,写出来算是给后来人排雷。

最后再分享一个我自己的体会。在移动端跑 AI Agent,最难的从来不是“把模型塞进手机”,而是接受设备的局限,然后把有限的算力用在最贴身、最隐私、最高频的场景上。它不需要打败服务器上的大模型,也不需要在基准测试里拿高分,它只需要在你问“明天怎么安排”的时候,不出五秒钟,把真实日历里的日程和提醒摆到你面前。做到这一点的成就感,比在服务器上跑通一个 70B 模型还强。

如果你也想动手,建议从“云端 API + 3 个工具 + SQLite 记忆”的骨架开始,跑通之后再慢慢加端侧模型。不要第一步就追求大而全,移动端 Agent 的乐趣本来就在于“刚刚好”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询