端侧模型这几年我前前后后试过不少,最常被朋友问到的一句话是:“这玩意儿跑起来了,然后呢?”确实,光会聊天的大模型放在本地,充其量是个离线版问答机器,没法帮人把事办了。真正让端侧模型值钱的,是让它从“对话”走到“行动”。FunctionGemma 就是冲着这个需求来的:它在开源轻量模型的基础上,针对工具调用做了专项优化,让手机、平板、边缘盒子上的模型能输出结构化的函数调用指令,由本地代码执行后把结果再喂回模型,完成一个“听懂需求 - 调用工具 - 反馈结果”的闭环。这篇内容写给要做端侧 Agent、智能硬件方案,以及被“模型只能聊不能干”卡住的开发者,讲清楚选型逻辑、运行原理和一套可以照抄的落地流程。1. 端侧智能体不是把云端大模型搬下手机,而是换一套决策链路1.1 对话模型和智能体的差距到底在哪传统对话模型的输入是一段文本,输出是一段文本。用户说“帮我把明天上午十点的会取消”,模型生成“好的,我帮您取消明天上午十点的会议”。看起来回答得体,但实际上什么事情都没发生。人的对话里“取消会议”意味着执行一个动作,模型眼里这只是一次文本接龙。智能体要求的则是“感知 - 决策 - 行动 - 反馈”。用户的指令进入模型后,模型要把自然语言映射成一组结构化的动作描述,比如输出:{ "tool": "cancel_event", "arguments": { "event_id": "evt_20250120_1000" } }外层程序解析这段输出,调用本地的日历 API,真正把数据库里的那条日程删掉,再把执行结果返回给模型,模型据此生成一句面向用户的最终回复:“已为您取消明天上午十点的会议,原会议室也已释放。”这一转变看着就一步,难在让轻量模型稳定地学会“该调用哪个工具、该填哪些参数、什么时候不要调用工具”。普通对话小模型偶尔能生成一段漂亮的 JSON,但稳定性和参数准确性远远不够。FunctionGemma 这类针对性微调过的模型,就是把这个能力直接揉进了权重里。1.2 为什么非要在端侧做智能体有人会问,云端大模型调用工具不是已经很成熟了吗,为什么还要折腾端侧?我自己的理由主要有四个。延迟最直观。智能家居场景里,用户说“打开客厅灯”,如果指令先上传云端、经过大模型推理、再返回设备,一个来回通常 1 到 3 秒,体验非常别扭。而端侧模型推理本地完成,配合 MQTT 指令下发,可以把全链路压到 400 毫秒以内。这个数字在真实交互里是天壤之别。隐私是第二个硬理由。日程、家庭住址、健康数据、摄像头画面,这些信息一旦送到云端,就等于用户把家底交给了别人。端侧方案里,所有涉及个人隐私的数据只在本地内存里过一遍,不出设备。做企业私有化部署的同学会更有体会:很多甲方根本不允许核心业务数据出网,这是采购硬门槛。成本也不能忽视。云端大模型按 token 计费,一个频繁查询的小工具,一天几万次调用,账单是真金白银。端侧推理用的是设备自身的算力,边际成本趋近于零,而且模型离线也能跑。把高频、低难度、隐私敏感的请求留在端侧,把少数复杂请求留给云端,是最经济的分配方式。1.3 端侧智能体的典型场景和边界我实际接触过的端侧 Agent 需求,大概集中在三类。一是手机上的个人助理,用来提取验证码、创建日程、打开应用、快捷回复;二是智能家居中控,用一句话控制灯光、空调、窗帘、门锁;三是一些垂直业务助手,比如门店的点餐推荐、仓库的扫码盘点、设备的故障排查辅助。这些场景有一个共同特点:工具数量不大,每类意图都非常具体,而且响应时机敏感。反过来,如果一张嘴就让端侧模型去完成“根据用户画像制定旅行计划并自动预订酒店”这种重推理任务,2B、9B 级别的模型目前还扛不住。边界不在模型能不能输出函数调用,而在意图理解和长链路规划的稳定性上。2. FunctionGemma 凭什么能接工具:模型能力与方案选型2.1 函数调用本质上是“让模型输出受限文本”很多刚接触工具调用的朋友以为,函数调用是模型直接执行代码。不是的。模型的计算本质依旧是“根据输入生成下一个 token”。函数调用之所以可行,是因为我们把可用的每个工具,连同它的参数描述,一起拼进了上下文,然后要求模型输出一段特殊格式的文本。这段文本恰好可以被我们的代码解析成函数名和参数。这里面真正考验模型的有三件事:第一,能不能从自然语言里准确识别用户意图对应哪个工具;第二,能不能按照 schema 约束补全参数,包括类型、必填项、枚举值;第三,能不能在上下文里看到工具执行结果之后继续合理作答。普通小模型没有专门训练这些习惯,输出往往是“我知道了”之类的话,或者自创一个工具名。FunctionGemma 在训练阶段专门用“指令 - 工具调用 - 结果返回 - 最终回复”这一类对话语料做微调,让模型养成了“先调用工具、再回复用户”的默认行为。2.2 FunctionGemma 的来头与训练思路FunctionGemma 属于 Gemma 开源模型家族的衍生版本,基础底座是 Gemma 2 系列。Gemma 2 本身提供 2B、9B、27B 多个参数规格,其中 2B 和 9B 对端侧部署极其友好。FunctionGemma 在底座之上,用大量工具调用样本做了指令微调,把通用模型改造成一个更适合驱动 Agent 的“行动模型”。它的训练数据形态非常有代表性,一段样本通常长这样:用户自然语言请求,例如“帮我约一下周五下午三点和李老师开会”;模型输出一个函数调用,指向create_event并带上title、start_time、attendees等参数;外部系统返回一个执行结果,例如“事件已创建,编号 evt_xxx”;模型最后生成面向用户的自然语言回复。这种四段式数据的好处是让模型在训练时就明白:自己的输出不一定是对用户的最终回答,也可能是一个需要被外部程序执行的动作。推理阶段,我们也应该严格按照这种结构组织对话,不要自由发挥。这是 FunctionGemma 使用效果好坏的关键分水岭。2.3 为什么我选 FunctionGemma 而非通用小模型或闭源 API很多开发者第一反应是:我直接拿一个开源 7B 对话模型,在提示词里写“请输出 JSON”不就行了?实测下来,通用对话模型偶尔能碰巧生成合法 JSON,但只要用户的句式一变、语序一换,它就回到“聊天状态”,完全不理会工具描述。这类问题不是提示词写得不好,而是模型根本没有把“工具调用”作为一项核心能力训练进去。自己用 LoRA 微调一个函数调用模型也是一条路,但成本不低。要准备高质量工具调用语料、要防止灾难性遗忘、要在多个工具间保持均衡,没有两周时间很难调到可用状态。FunctionGemma 的价值在于它已经完成了这部分工作,权重直接开放,能够像普通开源模型一样量化和部署。和商用闭源模型接口比,FunctionGemma 的核心优势是数据不出端、无调用成本、断网可用。商用接口的优势则是思维链和复杂理解更强。所以我的判断很简单:简单高频的任务,交给端侧 FunctionGemma;复杂规划和需要大量知识的任务,才考虑走云端。方案离线隐私成本工具调用稳定性部署难度通用开源对话模型支持好低差,输出不稳定低自训 LoRA支持好中看训练数据质量高闭源大模型 API不支持差按量计费强低FunctionGemma支持好低好,专项微调低3. 手把手把日程助手跑在本地:端侧 Function Calling 实操3.1 第一步:把工具定义成模型看得懂的 JSON Schema我习惯把工具理解成“给模型看的能力菜单”,而不是“程序员写的 API 文档”。模型不关心底层实现,它只关心这个工具叫什么、什么时候用、参数有哪些、每个参数代表什么含义。所以工具定义的质量,直接决定了模型调用得准不准。以日程助手为例,我们从两个工具开始:查日程、建日程。建日程的 schema 长这样:{ "type": "function", "function": { "name": "create_event", "description": "在日历中创建一个新的事件条目", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "事件的标题,例如:产品评审会" }, "start_time": { "type": "string", "format": "date-time", "description": "事件的开始时间,ISO 8601 格式,例如 2025-01-20T10:00:00" }, "end_time": { "type": "string", "format": "date-time", "description": "事件的结束时间,ISO 8601 格式,如果用户未提供,则为 null" }, "attendees": { "type": "array", "items": { "type": "string" }, "description": "参会人列表,可空" } }, "required": ["title", "start_time"] } } }这里有几个写 schema 的教训。每个字段的 description 一定要写清楚“这个字段在什么情况下填什么”,模型靠 description 理解语义,而不是靠字段名猜。参数名尽量用完整英文单词,别整拼音缩写。枚举类字段一定要给 enum 候选值,否则模型能给你编出一个稀奇古怪的值。工具数量刚开始控制在三五个以内,2B 模型面对超过十个工具的选择时,准确率会明显下降。3.2 第二步:按模型训练时的格式组织对话函数调用模型对对话格式很敏感。别看都是聊天气泡,换一种排列,模型可能就认不出来了。最稳妥的做法是先用推理框架内置的 chat template,不要自己手拼。实测中我习惯的提示词结构是:一条系统指令说明“你是端侧日程助手,需要调用工具时输出 JSON,否则用自然语言回复”,接着把工具列表以 JSON 形式贴给模型看,最后是用户问题。<system> 你是一个运行在用户本地设备上的日程助手。当用户的请求需要操作日历时,你必须输出一个 JSON 对象,格式如下: {"tool": "create_event", "arguments": {"title": "...", "start_time": "..."}} 当用户请求不涉及工具时,请用自然语言直接回答。 下面是你可以使用的工具列表: [工具 schema 列表] </system> <user> 帮我约一下周五下午三点和李老师开会,预计一个小时,线上线下都行。 </user>注意,这段是我在一台普通开发机上测试时用的简化模板。如果你跑的是框架内置好的 FunctionGemma 版本,尽量把消息列表单独拆分,让框架自动完成模板拼接。因为模型在训练时记住的是特定格式,模板差一点,输出稳定性就差一截。3.3 第三步:跑通最小可用的“推理 - 执行 - 回传”循环完整的智能体不是“输入一句用户指令,输出一个 JSON”就结束了。真实场景里,模型经常需要先查一次日程,再根据结果创建新事件,最后还要把结果回复给用户。这要求我们把工具调用的每一步都作为对话历史追加进去。下面是最小循环的伪代码,也是我建议团队新同学先跑通的骨架:from functiongemma_utils import LoadModel, Tools model = LoadModel("functiongemma-2b-q4_k_m.gguf") tools = Tools.load("tools_schema.json") messages = [system_prompt(), user_message("帮我约周五下午和李老师开会")] max_turn = 3 for turn in range(max_turn): response = model.chat(messages, tools=tools) if response.is_tool_call: tool_name, args = parse_json(response.content) # 真正调用本地日历 API result = dispatch_tool(tool_name, args) # 把模型输出和工具执行结果都追加进上下文 messages.append(assistant_message(response.content)) messages.append(tool_message(result)) # 让模型基于结果继续生成,可能是最终回复,也可能继续调用下一个工具 else: print(response.content) break这段代码的核心思想是:模型输出的“行动”和“执行结果”都被当成新一轮对话的一部分。模型看到工具执行结果后,能够修正自己的下一步动作,也能生成让用户读得懂的自然语言回复。循环里一定要加最大轮次限制,防止模型陷入“调了又调”的死循环。3.4 端侧部署的硬件选型和量化配置模型能不能在目标设备上跑得动,是端侧项目绕不开的现实问题。我做过的部署组合供你参考:笔记本电脑或迷你主机,跑 9B INT4 量化,内存占用大约 5GB 到 6GB,语义理解明显更强;手机对外壳是 2B INT4,内存占用 1.2GB 到 1.5GB,能流畅处理单一工具调用;树莓派这类入门板子,也是 2B INT4 更稳妥,但首 token 可能要等上一会儿。量化档位优先选q4_k_m,这是准确率和速度的平衡点。别为了省内存去用 2bit、3bit 的量化,函数调用要求输出的 JSON 必须结构完整,过低量化会让模型频繁漏掉括号、自创 key,解析环节会把人逼疯。模型的上下文长度不用开太大。函数调用的每个工具 schema 动辄几百 token,塞太多历史会把有限的算力全耗在 prefill 阶段。我通常控制在 1024 到 2048,只保留最近两三轮对话。3.5 采坑记录:采样参数和结构化输出的组合拳同一份 prompt,采样参数不同,模型输出的稳定性天差地别。端侧模型做工具调用,我强烈建议把 temperature 调到 0.2 以下,top_p 设在 0.9 附近。温度一高,模型就开始在 JSON 输出里“发挥创意”,这是最常见的事故源头。另一个技巧是配合 grammar 约束或正则解析。llama.cpp 支持在解码阶段限制输出格式,让模型只能按预定义的 JSON 规则生成 token,这比“生成完再纠错”效率高得多。从工程角度说,宁可让模型多生成一些 padding token,也不要在解析阶段处理一堆畸形 JSON。4. 端侧跑 Agent 最容易翻车的四个环节和排查记录4.1 现象一:模型打死不输出函数调用,只会“好的”这是我把普通 Gemma 换成 FunctionGemma 之前遇到的典型问题。排查思路很简单,按顺序走一遍:先确认你加载的确实是对应模型。很多新手用 llama.cpp 加载了一个普通对话模型,然后期望它自动完成函数调用,这就相当于让一个只会说话的机器人自己长出手来,不现实。其次检查你的对话格式。我给一个同事排查时发现,他把自己拼的 user 消息直接塞在 system 后面,没有按模型官方模板来,模型完全识别不出来。最后,把工具描述里的“模糊词”换成明确的触发条件,比如“当用户提到创建日程、安排会议时,必须使用 create_event 工具”,这比空泛的“你能帮我创个日程吗”要可靠很多。4.2 现象二:参数全靠脑补,模型开始编数据更隐蔽的问题是模型会“画龙点睛”。用户说“约周五下午开会”,模型不但自己填了具体时间,还贴心地把会议室、参与人全编出来,完事还输出一段事件已创建成功的 JSON。这种“过度自信”在轻量模型里非常常见。我的处理方式分成三层。第一,在 schema 里把用户未提供的信息标记为 optional,把值设为 null。第二,在系统提示词里明确加一句“所有参数必须来自用户本次对话,禁止自行推断缺失字段”。第三,在代码层做一个参数兜底,比如用户没说时长就默认一小时,没说参会人就发一个空数组。这层兜底不是让模型猜,而是由业务规则决定。涉及重要操作的场景,我还会加一道“人机确认闸门”。模型先输出一个待执行动作,程序把动作翻译成一句自然语言让用户确认,确认后才真正调 API。这个设计在端侧特别值得推广,因为它既堵住了模型幻觉,又让用户对智能体的行为有掌控感。4.3 现象三:推理慢到没法用,聊天都一卡一卡的端侧性能优化是个系统工程。我踩过最大的坑是把十个工具的描述全量塞进 system prompt,上下文一下子多了几千 token,每次请求都要重新 prefill 一遍,速度直接崩。优化手段从这几方面入手。工具描述能短则短,schema 里的 description 写清楚就行,不要写论文。上下文做滑动窗口,只能保留最近的几轮消息,旧工具执行结果可以折叠成一句摘要。手机端跑不动 9B 时,可以改成双模型级联:用 2B 做意图识别和工具选择,等确定要调用的工具后,再把局部上下文交给 9B 生成高质量回复。这个模式在延迟和效果之间平衡得很好。4.4 现象四:工具执行完,模型却说“我不知道结果”有一次我碰到一个很诡异的现象:工具明明返回了{"status": "ok", "event_id": "evt_123"},模型生成的最终回复却是“抱歉,我暂时无法完成这个操作”。排查半天发现,工具执行结果被追加进上下文时,没有明确标注这是“工具回复”。模型把它当成了聊天记录里的普通文本,反而认为缺少关键信息。解决方法是把工具结果的 message role 单独区分,同时在系统提示词里告诉模型:“当你收到 tool 类型的消息,说明工具已经执行完成,请优先依据该消息回复用户,不要回答你不知道。”另外,工具返回内容尽量结构化,不要返回空字符串,哪怕没有数据也要用{"status": "ok", "result": null}这种占位结构喂给模型。4.5 安全闸门:端侧智能体别变成“谁都能使唤的工具人”端侧设备往往只有一个人在使用,但这也意味着它很容易被恶意输入哄骗。网页里的隐藏指令、短信里的嵌入式提示词,都可能让模型误以为自己在被系统要求执行敏感操作。所以我在工程设计里始终保留几个硬性约束:涉及删除、转账、发送消息类操作的工具,必须二次确认;所有工具调用必须经过白名单校验,新增工具要改配置而不是靠模型自己发现;模型输出里的动作不能直接拼进 shell 命令,必须走参数化接口。5. 再进一步:从单次调用到多工具协作的端侧 Agent5.1 多步调用:一次任务连续调用三个工具端侧智能体的真正价值,藏在“一个自然语言指令驱动多个工具按序执行”的能力里。举个例子,用户说“明天上午找产品团队开个会,帮我约会议室并且提醒参会人”。这个任务的最终结果其实依赖三次调用:模型先调用query_rooms,拿到明天上午空闲的会议室列表;选定一个时间窗后,调用create_event创建日程,并把会议室绑定进去;最后调用notify_participants,给参会人发通知。每一步的工具执行结果都要回传进上下文,供模型判断下一步怎么走。这就是经典的 ReAct 循环,只是搬到了端侧。对 2B 模型来说,多步调用是很大的考验,每一步都有概率出错,错误还会级联放大。经验是:优先用 9B 承担多步任务,2B 只处理“单一步骤动作”。如果把业务流程理清楚,也可以在代码层把三步写成编排好的脚本,模型只负责第一步意图解析,后面交给确定性代码执行,这样的系统反而最稳。5.2 上下文管理:别让工具结果撑爆本地内存端侧设备的 KV cache 和上下文窗口都很珍贵。工具如果返回一个几十条记录的查询结果,直接塞进上下文不仅占用空间,还会让模型后续生成变得“云里雾里”。我现在的标准做法是:工具返回数据先做规整,只保留最终需要展示的字段;列表类结果做截断,最多返回前五条,并在末尾注明“共 37 条,仅显示前 5 条,如需更多请明确指定”;所有返回的 JSON 都要求紧凑格式,去掉转义和换行。这样一来,上下文占用能减少一半以上。5.3 端云协同:把隐私留在端侧,把复杂交给云端最后说一句这段时间做项目最深的体会。端侧智能体的意义不是取代云端的“超级大脑”,而是在用户设备和云端之间划定一条合理的数据边界。高频、隐私敏感、需要即时响应的操作留在端侧;需要大模型深度思考的任务,可以把脱敏后的摘要发到云端。这条边界不只是技术决策,更是产品和合规决策。连续做了几个端侧项目后我意识到,很多用户一开始并不在乎延迟,他们更在乎的是“这个智能体到底在帮我做事,还是在我家里装了一个一直往外面传数据的盒子”。把工具调用闭环稳稳当当地落在本地,就是对这个担忧最好的回应。端侧智能体这条路,模型能力只是其中一块拼图,工具定义、交互协议、上下文管理、安全闸门,每一块都需要亲手打磨。我特别推荐大家从一个两个工具的小闭环开始,跑通后再逐步叠加多步和业务逻辑,别上来就塞二十个函数,那样翻车了都不知道该查谁。
端侧模型这几年我前前后后试过不少,最常被朋友问到的一句话是:“这玩意儿跑起来了,然后呢?”确实,光会聊天的大模型放在本地,充其量是个离线版问答机器,没法帮人把事办了。真正让端侧模型值钱的,是让它从“对话”走到“行动”。FunctionGemma 就是冲着这个需求来的:它在开源轻量模型的基础上,针对工具调用做了专项优化,让手机、平板、边缘盒子上的模型能输出结构化的函数调用指令,由本地代码执行后把结果再喂回模型,完成一个“听懂需求 - 调用工具 - 反馈结果”的闭环。这篇内容写给要做端侧 Agent、智能硬件方案,以及被“模型只能聊不能干”卡住的开发者,讲清楚选型逻辑、运行原理和一套可以照抄的落地流程。1. 端侧智能体不是把云端大模型搬下手机,而是换一套决策链路1.1 对话模型和智能体的差距到底在哪传统对话模型的输入是一段文本,输出是一段文本。用户说“帮我把明天上午十点的会取消”,模型生成“好的,我帮您取消明天上午十点的会议”。看起来回答得体,但实际上什么事情都没发生。人的对话里“取消会议”意味着执行一个动作,模型眼里这只是一次文本接龙。智能体要求的则是“感知 - 决策 - 行动 - 反馈”。用户的指令进入模型后,模型要把自然语言映射成一组结构化的动作描述,比如输出:{ "tool": "cancel_event", "arguments": { "event_id": "evt_20250120_1000" } }外层程序解析这段输出,调用本地的日历 API,真正把数据库里的那条日程删掉,再把执行结果返回给模型,模型据此生成一句面向用户的最终回复:“已为您取消明天上午十点的会议,原会议室也已释放。”这一转变看着就一步,难在让轻量模型稳定地学会“该调用哪个工具、该填哪些参数、什么时候不要调用工具”。普通对话小模型偶尔能生成一段漂亮的 JSON,但稳定性和参数准确性远远不够。FunctionGemma 这类针对性微调过的模型,就是把这个能力直接揉进了权重里。1.2 为什么非要在端侧做智能体有人会问,云端大模型调用工具不是已经很成熟了吗,为什么还要折腾端侧?我自己的理由主要有四个。延迟最直观。智能家居场景里,用户说“打开客厅灯”,如果指令先上传云端、经过大模型推理、再返回设备,一个来回通常 1 到 3 秒,体验非常别扭。而端侧模型推理本地完成,配合 MQTT 指令下发,可以把全链路压到 400 毫秒以内。这个数字在真实交互里是天壤之别。隐私是第二个硬理由。日程、家庭住址、健康数据、摄像头画面,这些信息一旦送到云端,就等于用户把家底交给了别人。端侧方案里,所有涉及个人隐私的数据只在本地内存里过一遍,不出设备。做企业私有化部署的同学会更有体会:很多甲方根本不允许核心业务数据出网,这是采购硬门槛。成本也不能忽视。云端大模型按 token 计费,一个频繁查询的小工具,一天几万次调用,账单是真金白银。端侧推理用的是设备自身的算力,边际成本趋近于零,而且模型离线也能跑。把高频、低难度、隐私敏感的请求留在端侧,把少数复杂请求留给云端,是最经济的分配方式。1.3 端侧智能体的典型场景和边界我实际接触过的端侧 Agent 需求,大概集中在三类。一是手机上的个人助理,用来提取验证码、创建日程、打开应用、快捷回复;二是智能家居中控,用一句话控制灯光、空调、窗帘、门锁;三是一些垂直业务助手,比如门店的点餐推荐、仓库的扫码盘点、设备的故障排查辅助。这些场景有一个共同特点:工具数量不大,每类意图都非常具体,而且响应时机敏感。反过来,如果一张嘴就让端侧模型去完成“根据用户画像制定旅行计划并自动预订酒店”这种重推理任务,2B、9B 级别的模型目前还扛不住。边界不在模型能不能输出函数调用,而在意图理解和长链路规划的稳定性上。2. FunctionGemma 凭什么能接工具:模型能力与方案选型2.1 函数调用本质上是“让模型输出受限文本”很多刚接触工具调用的朋友以为,函数调用是模型直接执行代码。不是的。模型的计算本质依旧是“根据输入生成下一个 token”。函数调用之所以可行,是因为我们把可用的每个工具,连同它的参数描述,一起拼进了上下文,然后要求模型输出一段特殊格式的文本。这段文本恰好可以被我们的代码解析成函数名和参数。这里面真正考验模型的有三件事:第一,能不能从自然语言里准确识别用户意图对应哪个工具;第二,能不能按照 schema 约束补全参数,包括类型、必填项、枚举值;第三,能不能在上下文里看到工具执行结果之后继续合理作答。普通小模型没有专门训练这些习惯,输出往往是“我知道了”之类的话,或者自创一个工具名。FunctionGemma 在训练阶段专门用“指令 - 工具调用 - 结果返回 - 最终回复”这一类对话语料做微调,让模型养成了“先调用工具、再回复用户”的默认行为。2.2 FunctionGemma 的来头与训练思路FunctionGemma 属于 Gemma 开源模型家族的衍生版本,基础底座是 Gemma 2 系列。Gemma 2 本身提供 2B、9B、27B 多个参数规格,其中 2B 和 9B 对端侧部署极其友好。FunctionGemma 在底座之上,用大量工具调用样本做了指令微调,把通用模型改造成一个更适合驱动 Agent 的“行动模型”。它的训练数据形态非常有代表性,一段样本通常长这样:用户自然语言请求,例如“帮我约一下周五下午三点和李老师开会”;模型输出一个函数调用,指向create_event并带上title、start_time、attendees等参数;外部系统返回一个执行结果,例如“事件已创建,编号 evt_xxx”;模型最后生成面向用户的自然语言回复。这种四段式数据的好处是让模型在训练时就明白:自己的输出不一定是对用户的最终回答,也可能是一个需要被外部程序执行的动作。推理阶段,我们也应该严格按照这种结构组织对话,不要自由发挥。这是 FunctionGemma 使用效果好坏的关键分水岭。2.3 为什么我选 FunctionGemma 而非通用小模型或闭源 API很多开发者第一反应是:我直接拿一个开源 7B 对话模型,在提示词里写“请输出 JSON”不就行了?实测下来,通用对话模型偶尔能碰巧生成合法 JSON,但只要用户的句式一变、语序一换,它就回到“聊天状态”,完全不理会工具描述。这类问题不是提示词写得不好,而是模型根本没有把“工具调用”作为一项核心能力训练进去。自己用 LoRA 微调一个函数调用模型也是一条路,但成本不低。要准备高质量工具调用语料、要防止灾难性遗忘、要在多个工具间保持均衡,没有两周时间很难调到可用状态。FunctionGemma 的价值在于它已经完成了这部分工作,权重直接开放,能够像普通开源模型一样量化和部署。和商用闭源模型接口比,FunctionGemma 的核心优势是数据不出端、无调用成本、断网可用。商用接口的优势则是思维链和复杂理解更强。所以我的判断很简单:简单高频的任务,交给端侧 FunctionGemma;复杂规划和需要大量知识的任务,才考虑走云端。方案离线隐私成本工具调用稳定性部署难度通用开源对话模型支持好低差,输出不稳定低自训 LoRA支持好中看训练数据质量高闭源大模型 API不支持差按量计费强低FunctionGemma支持好低好,专项微调低3. 手把手把日程助手跑在本地:端侧 Function Calling 实操3.1 第一步:把工具定义成模型看得懂的 JSON Schema我习惯把工具理解成“给模型看的能力菜单”,而不是“程序员写的 API 文档”。模型不关心底层实现,它只关心这个工具叫什么、什么时候用、参数有哪些、每个参数代表什么含义。所以工具定义的质量,直接决定了模型调用得准不准。以日程助手为例,我们从两个工具开始:查日程、建日程。建日程的 schema 长这样:{ "type": "function", "function": { "name": "create_event", "description": "在日历中创建一个新的事件条目", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "事件的标题,例如:产品评审会" }, "start_time": { "type": "string", "format": "date-time", "description": "事件的开始时间,ISO 8601 格式,例如 2025-01-20T10:00:00" }, "end_time": { "type": "string", "format": "date-time", "description": "事件的结束时间,ISO 8601 格式,如果用户未提供,则为 null" }, "attendees": { "type": "array", "items": { "type": "string" }, "description": "参会人列表,可空" } }, "required": ["title", "start_time"] } } }这里有几个写 schema 的教训。每个字段的 description 一定要写清楚“这个字段在什么情况下填什么”,模型靠 description 理解语义,而不是靠字段名猜。参数名尽量用完整英文单词,别整拼音缩写。枚举类字段一定要给 enum 候选值,否则模型能给你编出一个稀奇古怪的值。工具数量刚开始控制在三五个以内,2B 模型面对超过十个工具的选择时,准确率会明显下降。3.2 第二步:按模型训练时的格式组织对话函数调用模型对对话格式很敏感。别看都是聊天气泡,换一种排列,模型可能就认不出来了。最稳妥的做法是先用推理框架内置的 chat template,不要自己手拼。实测中我习惯的提示词结构是:一条系统指令说明“你是端侧日程助手,需要调用工具时输出 JSON,否则用自然语言回复”,接着把工具列表以 JSON 形式贴给模型看,最后是用户问题。<system> 你是一个运行在用户本地设备上的日程助手。当用户的请求需要操作日历时,你必须输出一个 JSON 对象,格式如下: {"tool": "create_event", "arguments": {"title": "...", "start_time": "..."}} 当用户请求不涉及工具时,请用自然语言直接回答。 下面是你可以使用的工具列表: [工具 schema 列表] </system> <user> 帮我约一下周五下午三点和李老师开会,预计一个小时,线上线下都行。 </user>注意,这段是我在一台普通开发机上测试时用的简化模板。如果你跑的是框架内置好的 FunctionGemma 版本,尽量把消息列表单独拆分,让框架自动完成模板拼接。因为模型在训练时记住的是特定格式,模板差一点,输出稳定性就差一截。3.3 第三步:跑通最小可用的“推理 - 执行 - 回传”循环完整的智能体不是“输入一句用户指令,输出一个 JSON”就结束了。真实场景里,模型经常需要先查一次日程,再根据结果创建新事件,最后还要把结果回复给用户。这要求我们把工具调用的每一步都作为对话历史追加进去。下面是最小循环的伪代码,也是我建议团队新同学先跑通的骨架:from functiongemma_utils import LoadModel, Tools model = LoadModel("functiongemma-2b-q4_k_m.gguf") tools = Tools.load("tools_schema.json") messages = [system_prompt(), user_message("帮我约周五下午和李老师开会")] max_turn = 3 for turn in range(max_turn): response = model.chat(messages, tools=tools) if response.is_tool_call: tool_name, args = parse_json(response.content) # 真正调用本地日历 API result = dispatch_tool(tool_name, args) # 把模型输出和工具执行结果都追加进上下文 messages.append(assistant_message(response.content)) messages.append(tool_message(result)) # 让模型基于结果继续生成,可能是最终回复,也可能继续调用下一个工具 else: print(response.content) break这段代码的核心思想是:模型输出的“行动”和“执行结果”都被当成新一轮对话的一部分。模型看到工具执行结果后,能够修正自己的下一步动作,也能生成让用户读得懂的自然语言回复。循环里一定要加最大轮次限制,防止模型陷入“调了又调”的死循环。3.4 端侧部署的硬件选型和量化配置模型能不能在目标设备上跑得动,是端侧项目绕不开的现实问题。我做过的部署组合供你参考:笔记本电脑或迷你主机,跑 9B INT4 量化,内存占用大约 5GB 到 6GB,语义理解明显更强;手机对外壳是 2B INT4,内存占用 1.2GB 到 1.5GB,能流畅处理单一工具调用;树莓派这类入门板子,也是 2B INT4 更稳妥,但首 token 可能要等上一会儿。量化档位优先选q4_k_m,这是准确率和速度的平衡点。别为了省内存去用 2bit、3bit 的量化,函数调用要求输出的 JSON 必须结构完整,过低量化会让模型频繁漏掉括号、自创 key,解析环节会把人逼疯。模型的上下文长度不用开太大。函数调用的每个工具 schema 动辄几百 token,塞太多历史会把有限的算力全耗在 prefill 阶段。我通常控制在 1024 到 2048,只保留最近两三轮对话。3.5 采坑记录:采样参数和结构化输出的组合拳同一份 prompt,采样参数不同,模型输出的稳定性天差地别。端侧模型做工具调用,我强烈建议把 temperature 调到 0.2 以下,top_p 设在 0.9 附近。温度一高,模型就开始在 JSON 输出里“发挥创意”,这是最常见的事故源头。另一个技巧是配合 grammar 约束或正则解析。llama.cpp 支持在解码阶段限制输出格式,让模型只能按预定义的 JSON 规则生成 token,这比“生成完再纠错”效率高得多。从工程角度说,宁可让模型多生成一些 padding token,也不要在解析阶段处理一堆畸形 JSON。4. 端侧跑 Agent 最容易翻车的四个环节和排查记录4.1 现象一:模型打死不输出函数调用,只会“好的”这是我把普通 Gemma 换成 FunctionGemma 之前遇到的典型问题。排查思路很简单,按顺序走一遍:先确认你加载的确实是对应模型。很多新手用 llama.cpp 加载了一个普通对话模型,然后期望它自动完成函数调用,这就相当于让一个只会说话的机器人自己长出手来,不现实。其次检查你的对话格式。我给一个同事排查时发现,他把自己拼的 user 消息直接塞在 system 后面,没有按模型官方模板来,模型完全识别不出来。最后,把工具描述里的“模糊词”换成明确的触发条件,比如“当用户提到创建日程、安排会议时,必须使用 create_event 工具”,这比空泛的“你能帮我创个日程吗”要可靠很多。4.2 现象二:参数全靠脑补,模型开始编数据更隐蔽的问题是模型会“画龙点睛”。用户说“约周五下午开会”,模型不但自己填了具体时间,还贴心地把会议室、参与人全编出来,完事还输出一段事件已创建成功的 JSON。这种“过度自信”在轻量模型里非常常见。我的处理方式分成三层。第一,在 schema 里把用户未提供的信息标记为 optional,把值设为 null。第二,在系统提示词里明确加一句“所有参数必须来自用户本次对话,禁止自行推断缺失字段”。第三,在代码层做一个参数兜底,比如用户没说时长就默认一小时,没说参会人就发一个空数组。这层兜底不是让模型猜,而是由业务规则决定。涉及重要操作的场景,我还会加一道“人机确认闸门”。模型先输出一个待执行动作,程序把动作翻译成一句自然语言让用户确认,确认后才真正调 API。这个设计在端侧特别值得推广,因为它既堵住了模型幻觉,又让用户对智能体的行为有掌控感。4.3 现象三:推理慢到没法用,聊天都一卡一卡的端侧性能优化是个系统工程。我踩过最大的坑是把十个工具的描述全量塞进 system prompt,上下文一下子多了几千 token,每次请求都要重新 prefill 一遍,速度直接崩。优化手段从这几方面入手。工具描述能短则短,schema 里的 description 写清楚就行,不要写论文。上下文做滑动窗口,只能保留最近的几轮消息,旧工具执行结果可以折叠成一句摘要。手机端跑不动 9B 时,可以改成双模型级联:用 2B 做意图识别和工具选择,等确定要调用的工具后,再把局部上下文交给 9B 生成高质量回复。这个模式在延迟和效果之间平衡得很好。4.4 现象四:工具执行完,模型却说“我不知道结果”有一次我碰到一个很诡异的现象:工具明明返回了{"status": "ok", "event_id": "evt_123"},模型生成的最终回复却是“抱歉,我暂时无法完成这个操作”。排查半天发现,工具执行结果被追加进上下文时,没有明确标注这是“工具回复”。模型把它当成了聊天记录里的普通文本,反而认为缺少关键信息。解决方法是把工具结果的 message role 单独区分,同时在系统提示词里告诉模型:“当你收到 tool 类型的消息,说明工具已经执行完成,请优先依据该消息回复用户,不要回答你不知道。”另外,工具返回内容尽量结构化,不要返回空字符串,哪怕没有数据也要用{"status": "ok", "result": null}这种占位结构喂给模型。4.5 安全闸门:端侧智能体别变成“谁都能使唤的工具人”端侧设备往往只有一个人在使用,但这也意味着它很容易被恶意输入哄骗。网页里的隐藏指令、短信里的嵌入式提示词,都可能让模型误以为自己在被系统要求执行敏感操作。所以我在工程设计里始终保留几个硬性约束:涉及删除、转账、发送消息类操作的工具,必须二次确认;所有工具调用必须经过白名单校验,新增工具要改配置而不是靠模型自己发现;模型输出里的动作不能直接拼进 shell 命令,必须走参数化接口。5. 再进一步:从单次调用到多工具协作的端侧 Agent5.1 多步调用:一次任务连续调用三个工具端侧智能体的真正价值,藏在“一个自然语言指令驱动多个工具按序执行”的能力里。举个例子,用户说“明天上午找产品团队开个会,帮我约会议室并且提醒参会人”。这个任务的最终结果其实依赖三次调用:模型先调用query_rooms,拿到明天上午空闲的会议室列表;选定一个时间窗后,调用create_event创建日程,并把会议室绑定进去;最后调用notify_participants,给参会人发通知。每一步的工具执行结果都要回传进上下文,供模型判断下一步怎么走。这就是经典的 ReAct 循环,只是搬到了端侧。对 2B 模型来说,多步调用是很大的考验,每一步都有概率出错,错误还会级联放大。经验是:优先用 9B 承担多步任务,2B 只处理“单一步骤动作”。如果把业务流程理清楚,也可以在代码层把三步写成编排好的脚本,模型只负责第一步意图解析,后面交给确定性代码执行,这样的系统反而最稳。5.2 上下文管理:别让工具结果撑爆本地内存端侧设备的 KV cache 和上下文窗口都很珍贵。工具如果返回一个几十条记录的查询结果,直接塞进上下文不仅占用空间,还会让模型后续生成变得“云里雾里”。我现在的标准做法是:工具返回数据先做规整,只保留最终需要展示的字段;列表类结果做截断,最多返回前五条,并在末尾注明“共 37 条,仅显示前 5 条,如需更多请明确指定”;所有返回的 JSON 都要求紧凑格式,去掉转义和换行。这样一来,上下文占用能减少一半以上。5.3 端云协同:把隐私留在端侧,把复杂交给云端最后说一句这段时间做项目最深的体会。端侧智能体的意义不是取代云端的“超级大脑”,而是在用户设备和云端之间划定一条合理的数据边界。高频、隐私敏感、需要即时响应的操作留在端侧;需要大模型深度思考的任务,可以把脱敏后的摘要发到云端。这条边界不只是技术决策,更是产品和合规决策。连续做了几个端侧项目后我意识到,很多用户一开始并不在乎延迟,他们更在乎的是“这个智能体到底在帮我做事,还是在我家里装了一个一直往外面传数据的盒子”。把工具调用闭环稳稳当当地落在本地,就是对这个担忧最好的回应。端侧智能体这条路,模型能力只是其中一块拼图,工具定义、交互协议、上下文管理、安全闸门,每一块都需要亲手打磨。我特别推荐大家从一个两个工具的小闭环开始,跑通后再逐步叠加多步和业务逻辑,别上来就塞二十个函数,那样翻车了都不知道该查谁。