把大模型真正用起来之后我发现一个很明显的分水岭会聊天的模型很多会干活的模型很少。大部分时候我们打开一个助手问它“明天天气怎么样”它只会生成一段“天气可能不太好建议带伞”的漂亮话而不是真的去查接口、拉数据、设置提醒。这个差距就是从“对话”到“行动”的距离。FunctionGemma 就是我在做端侧智能体时重点尝试的一套方案它把函数调用能力直接内建到轻量级模型里让模型不仅能理解意图还能输出结构化的函数调用指令然后由本机执行器完成实际操作。这篇文章我会从模型原理、端侧部署、代码实现和调优经验四个方面把完整链路拆开聊清楚。文章面向的读者是正在做端侧 AI 应用、智能体框架或者本地助手的开发者。哪怕你还没系统接触过函数调用只要有一点大模型推理的基础跟着后面的代码走一遍也能在本地搭出一个能查时间、记备忘录、发通知的小智能体。我会尽量说人话把那些文档里不会写的坑也一并摆出来。1. 端侧智能体到底难在哪为什么“会聊天”不等于“会行动”1.1 对话模型的天然边界先看一个最典型的场景。你问一个纯对话模型“帮我把明天上午十点的会议改成十一点。”它会怎么回大概率是“好的建议您前往日历应用手动修改。”这句回答看似礼貌实际上什么都没做。原因很简单普通语言模型的能力边界在“生成文本”它没有一个可以去操作日历的“手”。LLM 本质上是在给定上文的前提下预测下一个 token。它知道日历是什么甚至知道很多改日程的步骤但它没有权限、没有接口、也没有执行环境。想让它真正干活就必须在模型外面接一层“工具层”。这一步在云端相对容易毕竟服务器可以随便调接口、跑代码。但到了手机、路由器、车载终端、嵌入式设备上问题立刻变复杂算力有限、内存吃紧、网络不稳定、隐私要求高。你不能指望设备端拖一个几百亿参数的模型再依赖云端反馈。端侧智能体真正需要的是一个小、快、准的本地“行动派”模型而不是一个只会嘴上说说的“评论员”。1.2 函数调用把“意图”变成“指令”函数调用function calling是当前把大模型变成行动体的最主流方案。核心思路很简单模型不直接执行动作而是输出一个结构化指令比如create_event(title项目评审, time2025-06-10 10:00)真正的执行交给本地代码。这样做有四个好处可解释模型只负责“决定做什么”每一步动作都是明确的函数名和参数。可控制执行前可以做权限校验、参数校验不让模型乱来。可回滚如果某次调用产生了坏结果很容易定位到是哪个函数、什么参数出问题。可扩展新增能力只需要注册一个新函数不需要重新训练模型。还是用改日程举例用户说“把明天的会改到十一点”模型并不直接操作日历数据库它输出reschedule_event(event_idevent_123, new_time11:00)。本机代码拿到这个指令后再调用系统日历 API 完成修改。这样一来模型依然是模型动作却真正发生了。1.3 FunctionGemma 解决的三个核心问题FunctionGemma 是一类面向端侧场景的轻量级工具调用模型方案。我不把它理解成某个单独模型而是一条“训练策略 推理协议 端侧执行框架”的组合。它重点解决三个问题。第一是轻量化。参数规模压缩到端侧能扛住的范围同时通过定向训练把“工具调用”这个能力突出强化而不是像通用模型那样什么都会一点、什么都不精。第二是结构化输出稳定。纯对话模型输出 JSON 经常缺括号、多字、字段名漂移。FunctionGemma 在训练阶段使用了大量工具调用样本并配合特殊 token 标记函数调用段让输出格式的稳定性比通用模型高一个量级。第三是多轮工具状态感知。真实智能体不可能只调用一次函数。用户查完天气可能接着问“那明天呢”这时候模型必须能结合上一次工具返回结果继续推理。FunctionGemma 的训练数据里包含了多轮工具结果回填样本所以它知道什么时候该继续调、什么时候该给最终答复。2. FunctionGemma 是怎么设计出来的原理与运行机制2.1 模型架构与训练对象不去纠结底层实现细节从功能视角看FunctionGemma 的底座是一个标准的 Decoder-Only 轻量语言模型参数档位从 1B 到 4B 不等。它要做的事情是在常规对话能力之上额外学会一种“工具调用方言”。训练数据混合了三类多轮对话数据保证模型仍然能正常聊天、总结、问答。函数调用数据用户在某一轮提出请求模型在回复中插入一个特殊标记比如function_call后面跟着 JSON 形式的函数名和参数。工具结果回填数据上一轮调用了函数这一轮的输入是function_result模型需要基于这个结果生成最终回复或者继续发起下一个函数调用。为什么要把函数调用训练成一种“方言”而不是靠提示词现场学会因为端侧模型参数少泛化能力有限。如果你只是临时在系统提示词里贴几个函数定义小模型很容易学会“读题”却很难学会“写标准格式”。只有把大量同类样本灌进训练过程模型才会把“看到用户请求→判断可调函数→输出 JSON 指令”这一链路变成一种条件反射。2.2 函数描述注入与结构化输出你可能会好奇模型怎么知道有哪些函数可以调答案跟提示工程有关。推理时我们会把当前可用的函数列表转成 JSON Schema 格式拼到系统提示词里。一个典型的函数声明长这样{ name: create_memo, description: 创建一条本地备忘录适合记录临时想法和待办事项, parameters: { type: object, properties: { title: { type: string, description: 备忘录标题尽量简洁 }, content: { type: string, description: 备忘录正文内容 } }, required: [title, content] } }这段描述不是给机器看的机器其实不“理解”自然语言的全部含义它是给模型的注意力机制做引导的。描述写得越具体模型越容易在需要的时候选择这个函数而不是搞混参数。结构化输出则通过两重机制保证。第一重是训练记忆模型见多了function_call的格式自然会模仿。第二重是推理后处理如果模型输出的 JSON 不够规整代码层可以做一次容错解析比如提取方括号里的文本、修复缺失的尾括号、把单引号转成双引号。这两重机制缺一不可。2.3 一次完整调用链路的六个阶段端侧智能体不是把函数定义拼进去就算完真正跑起来之后每一步都是一段可复用的逻辑。我把完整链路拆成六个阶段构造请求把系统提示词、函数声明、对话历史、当前问题拼成一个 prompt。模型推理由 FunctionGemma 生成结果结果可能是一次文本回复也可能是一次函数调用。解析输出识别function_call标记尝试提取 JSON判断格式是否合法。校验指令检查函数名是否白名单、参数是否匹配 schema、必填字段是否齐全。执行函数调用本地函数拿到返回值转成字符串或 JSON。回填结果把工具返回值以tool角色追加到对话里再送进模型做下一轮推理直到模型给出最终回复。这个循环不是无限的。我会在工程实现里设置一个最大轮次比如 4 到 6 次。一旦超过就返回“我还没能完成这个任务请换个说法再试一次”。这样做是为了防止模型因为某个函数始终报错而无限重试浪费端侧电力。3. 实操用 FunctionGemma 在本地搭一个可行动的智能体3.1 准备运行环境与模型文件动手之前先把环境准备好。我默认你手头有一台普通开发机CPU 能跑、内存 8G 以上就可以。如果你是做部署到手机或嵌入式设备步骤类似只是需要先把模型量化成更小的文件。建议的准备项Python 3.10装好 json、re、datetime 等标准库以及一个支持端侧模型推理的引擎接口。从模型仓库拿一个量化好的 FunctionGemma 权重文件优先选 int4 或 int8 的 GGUF 格式体积小、CPU 也能跑。把权重文件放到固定目录比如models/functiongemma-2b-int4.gguf。初始化推理引擎时重点设置两个参数上下文长度和线程数。上下文长度建议先给 2048太小容易截断多轮工具结果太大内存占用会直线上升。线程数设成 CPU 核心数的一半左右避免推理时整个设备卡死。3.2 定义本地工具集第一步先把“智能体能做什么”定下来。做端侧智能体最忌讳一上来就想做全知全能先选三五个高频、确定、可验证的工具。我在这里定义一组很实用的工具获取当前时间、创建本地备忘录、发送系统通知。代码很简单import json from datetime import datetime def get_local_time() - str: 获取当前本地时间 return datetime.now().isoformat() def create_memo(title: str, content: str) - bool: 把备忘录写入本地文件 memo {title: title, content: content, created_at: datetime.now().isoformat()} with open(memos.json, a, encodingutf-8) as f: f.write(json.dumps(memo, ensure_asciiFalse) \n) return True def send_notification(title: str, message: str) - bool: 发送一条本地系统通知 # 这里对接平台通知接口演示时先返回 True print(f[通知] {title}: {message}) return True TOOL_REGISTRY { get_local_time: get_local_time, create_memo: create_memo, send_notification: send_notification, }注意一个工程细节TOOL_REGISTRY的 key 必须和函数声明里的name完全一致执行层只认这个注册表不认模型输出里的名字。这样做的好处是即使模型偶尔输出一个不存在的函数名执行层也能立刻拦截而不是傻乎乎地报错。3.3 构造函数声明与系统提示词函数声明列表作为模型可参考的“工具手册”。前面已经展示了单个函数的 JSON Schema实际使用时会拼成一个数组。系统提示词我通常这样写SYSTEM_PROMPT 你是一个运行在本地设备上的智能助手。 你可以使用以下工具完成用户请求但不要虚构不存在的函数。 当用户请求需要调用工具时请严格按照下面的格式输出 function_call{name: 函数名, arguments: {参数名: 参数值}}/function_call 如果用户请求不需要工具直接使用自然语言回复。 FUNCTION_DEFS [ { name: get_local_time, description: 获取当前本地日期和时间。当用户问现在几点、今天日期时使用。, parameters: { type: object, properties: {}, required: [] } }, { name: create_memo, description: 把一段文字记录成本地备忘录。适用于用户说记一下添加到待办写个备忘。, parameters: { type: object, properties: { title: {type: string, description: 简短标题}, content: {type: string, description: 完整内容} }, required: [title, content] } }, { name: send_notification, description: 发送本地系统通知。适用于用户要求提醒、通知。, parameters: { type: object, properties: { title: {type: string, description: 通知标题}, message: {type: string, description: 通知内容} }, required: [title, message] } } ]为什么要单独加一句“不要虚构不存在的函数”因为小模型在开放环境下容易把用户提到的任何动词都当成函数名。比如用户说“帮我发个邮件”模型如果看到没有邮件函数可能会编一个send_email出来。把约束写进系统提示词能显著减少这类幻觉。3.4 核心推理与执行循环代码下面这段循环是整个智能体的心脏。它负责把对话状态、函数声明、模型输出和本地工具串起来。def build_prompt(messages, function_defs): 把消息列表和函数声明拼成模型输入 system_text SYSTEM_PROMPT \n可用工具:\n json.dumps(function_defs, ensure_asciiFalse) prompt system_text \n for msg in messages: role msg[role] content msg[content] if role user: prompt f\n用户: {content}\n elif role tool: prompt f\nfunction_result{content}/function_result\n else: prompt f\n系统: {content}\n return prompt def parse_function_call(raw_text): 从模型输出中提取函数调用 JSON match re.search(rfunction_call(.*?)/function_call, raw_text, re.DOTALL) if not match: return None try: return json.loads(match.group(1)) except json.JSONDecodeError: return None def execute_function(func_name, func_args): 执行本地工具返回 JSON 字符串 if func_name not in TOOL_REGISTRY: return json.dumps({error: f未知函数: {func_name}}) try: result TOOL_REGISTRY[func_name](**func_args) return json.dumps({result: result}, ensure_asciiFalse) except Exception as e: return json.dumps({error: str(e)}) def run_agent(user_question, max_steps4): messages [{role: user, content: user_question}] for _ in range(max_steps): prompt build_prompt(messages, FUNCTION_DEFS) raw_output engine.generate(prompt) action parse_function_call(raw_output) if action is None: # 没有函数调用视为最终回复 return raw_output func_name action.get(name) func_args action.get(arguments, {}) tool_result execute_function(func_name, func_args) messages.append({role: tool, content: tool_result}) # 继续循环让模型基于工具结果生成下一个动作或最终回复 return 已超过最大工具调用次数任务中断注意上面代码里的engine.generate是一个抽象接口。不同推理引擎的调用方式不一样但是整体逻辑完全一致。我建议你先把这段循环写稳再替换成具体的引擎。我见过太多人一上来就去调某个推理框架的 API反而忽略了智能体本身的调度逻辑。3.5 部署到低算力设备的注意点如果目标设备不是 PC而是手机、开发板这类低算力环境有几个点要提前处理。第一量化精度要够。4bit 量化通常是性价比最高的选择模型大小能压到原来的三分之一甚至四分之一。但量化后可能会损失少量工具调用准确率所以我建议准备两套模型开发调试用高精度版正式部署用 int4 版。如果在正式版上发现某个函数频繁选错优先回查 schema 描述是否清晰而不是急着换回高精度模型。第二控制上下文长度。端侧设备的内存是硬约束一次推理能塞进去的 token 有限。多轮对话时不要无限追加历史只保留最近两到三轮。工具结果如果很长先让模型自己写一个摘要再拼回上下文而不是把全套 JSON 塞进去。第三做好功耗控制。函数调用循环如果设计成无限重试设备会长时间保持高负载。我刚才在代码里加了max_steps限制实际项目里我还会再加一条单次任务总耗时超过 10 秒就主动结束并返回到主界面。端侧智能体不能像云端服务那样无限“思考”。4. 调优与避坑把准确率从“碰运气”抬到“能交付”4.1 函数 Schema 设计原则函数声明看起来简单真正决定模型能不能调对。我总结了几条实用原则。函数名要动词开头像是get_local_time、create_memo不要用time、memo、notification这类名词否则模型区分不了“查时间”和“设置时间”。描述里要写清楚触发条件。不要只写“获取时间”要写“用户问现在几点、当前日期时使用”。端侧模型参数小对隐式语义的把握弱描述越直白选择准确率越高。参数必填项越少越好。能用默认值就用默认值。比如create_memo如果允许省略标题就可以把title设为非必填模型出错的可能性会降低一个档次。一次暴露的函数数量要控制。测试时我遇到过模型在 30 个函数里反复犹豫的情况最后把函数按场景拆成多组根据用户第一轮问题动态选择暴露哪一组准确率立刻上来。如果设备只做备忘助手那就别把“控制灯泡”的函数也塞进去。4.2 采样参数怎么调函数调用不是创作不需要模型“自由发挥”。采样参数应该往确定性方向调。温度调低是首选。0.2 以下通常比较稳0.0 则完全贪心。我建议从 0.1 起步如果发现模型经常输出重复内容再尝试小幅提高。top_p保持 0.9 到 0.95主要用来过滤尾部低概率 token。top_k设成 20 到 50 之间防止模型在函数名这种关键位置突然发散。如果你的推理引擎支持约束解码一定要用起来。约束解码可以限制模型只能按照预定义的 JSON Schema 生成 token把格式错误直接扼杀在生成阶段。这是当前提升函数调用成功率最有效的工程手段比任何提示词技巧都管用。不支持也没关系代码层做一套健壮的 JSON 解析兜底。4.3 多轮状态与上下文裁剪真正跑起来之后最容易被忽略的是多轮上下文。用户说“现在几点了”模型调了get_local_time返回2025-06-10T09:30:00。用户接着问“那一个小时后呢”这时候模型必须能看到刚才的函数结果才能计算10:30。实现上要记住两点。第一工具结果一旦被模型消费就不再需要保留原始 JSON。可以在第二轮推理前把结果压缩成一句话比如“当前时间是 2025-06-10 09:30:00”然后继续拼进上下文。这样既保留了信息又不占 token。第二不要让工具结果“污染”新一轮对话的角色。工具结果必须以独立的tool角色放入消息列表不能拼在 user 消息里。否则模型容易把工具结果误当成用户新指令导致角色混乱。我遇到过好几次模型在工具返回后突然说“好的我会记住”就是角色没分清楚。4.4 安全边界设计让模型调用本地函数等于给外部文本输入开了个执行口子安全必须前置。最基础的做法是白名单机制。TOOL_REGISTRY就是白名单所有执行入口只能从这里查函数。永远不要让模型直接执行任意 Python 代码哪怕它“发誓”只是做个加法。其次要做参数校验。函数名合法不代表参数合法。create_memo的content如果允许任意字符串那模型可能把用户输入的恶意内容写进文件。文件路径类参数更要小心绝对不能让模型传一个../../etc/passwd进来。我会在每个函数内部再做一轮自己的校验不依赖模型输出的正确性。最后是敏感操作二次确认。删除文件、发送不可撤回的通知、修改系统配置这类操作不能模型一说就执行。要先把待执行动作展示给用户确认用户点击确认后再真正调用。端侧智能体追求效率但安全底线一点不能松。5. 常见问题排查实录5.1 高频报错与对应解法我在做 FunctionGemma 落地时遇到过不少问题这里整理成一张速查表。现象可能原因解决思路模型输出普通文本而不是函数调用系统提示词没有说清输出格式或函数声明没拼进去检查 build_prompt确认 FUNCTION_DEFS 已拼到系统提示词中把触发条件写得更直白函数名拼错、多字少字模型对函数名的记忆不稳定尤其函数名过长函数名控制在 2-4 个词动词开头在提示词里重复一遍可用函数名参数类型错误比如把数字写成字符串函数 schema 里没有写清楚类型检查函数定义中的parameters必要时在描述里补类型示例同一函数被无限重复调用工具结果没有正确回填模型看不到结果就只能重试检查messages.append是否正确工具角色是否被截断工具结果存在但最终回复却没有参考它上下文被裁剪或者工具结果写在了不显眼的位置增加上下文长度把最近的工具结果放在 prompt 最后面推理速度很慢上下文太长、线程数太少、模型未量化裁剪历史调整线程数换 int4 量化版本JSON 解析经常失败模型生成时没有约束解码输出多余字符优化提示词或者在解析层使用更宽容的 JSON 提取正则表格里面的问题我全都真实踩过尤其是“工具结果没回填导致重试”那个月几乎天天碰到。后来我加了一行日志专门打印每次循环后的完整 prompt问题立刻暴露出来上下文被裁剪时把工具结果裁掉了。从那以后我再也没让上下文长度超过设定值。5.2 我的排查顺序遇到问题不要急着调模型先按这个顺序排查能省很多时间。第一步看输入是不是 prompt 拼接错了。先把每次发给引擎的完整文本打出来确认函数声明、历史消息、工具结果都在并且顺序合理。这一步能解决一半问题。第二步看输出模型到底生成了什么。把原始 token 序列或原始文本保存下来不要只看解析后的结果。很多时候模型生成的是正确的function_call但你的正则没匹配到。第三步看执行工具本身有没有报错。给每个工具写一个独立的单元测试确认get_local_time()确实返回时间、create_memo()确实写入了文件。工具有问题再智能的模型也白搭。第四步才回头调模型。这时候再考虑改温度、改 schema、加少样本示例。顺序反了会很痛苦我见过有人花一周时间调提示词最后发现是个底层 JSON 解析 bug 导致的。6. 我的几点实操体会做端侧智能体越久越觉得“从对话迈向行动”这个说法不是一句口号而是整个范式转换的本质。纯对话模型是知识引擎重心在“答得好不好”函数调用模型是行动引擎重心在“做得对不对”。FunctionGemma 这类方案的真正价值是把模型体积、工具调用能力、端侧资源约束放到同一个天平上做出一个工程上可用的折中。我自己的体会有三点值得分享。第一永远不要追求全知全能。端侧智能体最适合的场景一定是边界清晰的垂直场景。我就让它管备忘录、查时间、发通知三个函数用得非常稳定。一旦加了太多工具准确率下降的速度会超出你的预期。第二把模型看作“意图理解器”把本机代码看作“安全执行器”。模型负责判断用户想干什么剩下的事情全部由你写的代码负责。这条路比让模型直接生成代码安全得多也更容易排查问题。第三每一轮工具调用都要有日志和可复现性。我在完成函数调用后都会把当时的 prompt、模型输出、工具结果完整存一份标记为 trace。后续优化模型或调 schema 时直接回放这些 trace能快速验证改动是否有效。这个习惯帮我省下了大量回归测试的时间。最后再分享一个小技巧。刚开始调通智能体时可以故意在用户请求里塞一些模棱两可的话比如“随便记一下”“提醒我明天”观察模型是调用工具还是乱猜。只要把这类边界情况覆盖到端侧智能体才算真正从“能跑”进化到“能用”。
阅读完成 · 觉得有帮助?