1. 只会聊天的机器人帮不了你:行动型智能体的门槛在哪
过去一年多,我一直在折腾端侧智能体,有一个感受特别深:大多数所谓“智能体”,本质还是一个会接话的聊天机器人。你说“帮我把空调调到26度”,它能回你一句“好的,已为你设置空调温度为26度”,听起来很贴心,但空调根本没动。问题出在哪?出在模型只输出了文本,并没有真正调用任何设备接口。对话模型的核心能力是“生成下一个词”,它擅长的是把回复写得像那么回事,而不是去操作系统里的开关、电路和传感器。
这就是“对话”和“行动”之间最本质的鸿沟:对话是信息输出,行动是状态改变。一个智能体要完成行动,至少要跨过四道坎:
- 从用户指令里识别出真实意图,而不是复述字面意思;
- 从候选工具集合里选出正确的那一个,比如“开灯”对应的是灯具控制接口,而不是音乐播放接口;
- 把意图里的关键信息抽取成工具的输入参数,比如“26度”“卧室”“半小时后”;
- 执行完工具后,把结果组织成自然语言回复给用户,形成闭环。
这四个环节里,第一个和第四个靠语言模型就能完成得很好,第二个和第三个才是传统对话模型的老大难。因为函数选择本质上是“结构化决策”,参数抽取本质上是“结构化生成”,这已经不是单纯的语言建模问题了。端侧智能体的核心门槛,恰恰就在这里:你能不能在一个几B级别的轻量模型上,稳定可靠地完成函数调用。
还有一个现实问题:端侧部署意味着算力、内存、带宽都受限,你不可能把几万亿参数的大模型塞进手机或者智能家居中枢里,也不可能每次调用都先连一次云端、等一个网络往返。隐私数据不出设备、离线也能用、响应延迟控制在毫秒到秒级,这些诉求把“函数调用能力”逼到了端侧模型必须自己解决的地步。FunctionGemma 这一类模型,就是针对这个场景出现的:它不是通用聊天模型,而是在轻量级语言模型基础上,专门对齐过“如何按规范选择函数、生成参数”的能力。
我最早看到这类模型时,第一反应是“这不就是给模型加了一层函数调用的指令微调吗?”实际深入用下来才发现,事情没那么简单。轻量模型在函数调用上翻车的花样比想象中多得多:参数类型理解错、字段名记错、把函数名当成普通文本生成、甚至凭空捏造一个不存在的函数。这些坑,文章后面我会逐一展开。
2. FunctionGemma 的底牌:面向工具调用的对齐机制与两类调用格式
要理解 FunctionGemma,得先回到 2024 年开源社区发布的那一组轻量级语言模型家族。当时这个系列推出了不同参数规模的版本,主打“小尺寸也能跑出不错效果”,而且允许端侧部署。没过多久,社区在它基础上做了函数调用特化:保留原有语言能力,同时新增一个非常重要的能力——根据用户输入,从给定的函数清单中“挑选”出合适的函数,并“生成”调用所需的参数。
这跟普通对话模型的本质区别在于输出目标。对话模型的输出是接续语境的自然语言;函数调用模型的输出是结构化的函数调用指令。为了把这件事做到模型层面可依赖,FunctionGemma 采用了两个关键机制:一是专门的函数调用格式(业界常见的表述是 CT1 和 CT2 两类),二是把这些格式作为训练目标做对齐。
2.1 CT1 与 CT2:函数调用的两种表达方式
先说 CT1。CT1 的核心思路是让模型在输出自然语言回复之前,先输出一个带特殊标记的函数调用。形式上类似在文本里插入一个被尖括号或特定 token 包裹的调用块,里面包含函数名和参数。这种格式对模型来说是在“写代码”:把一次调用当作一段特殊的文本片段来生成。它的优点是自然语言与函数调用可以在同一段输出里混合,适合边对话边调用的场景;缺点是解析复杂,而且模型容易在格式边界上出错——多一个空格、少一个引号,解析器就会失败。
CT2 则是更纯粹的结构化格式。模型只输出一个 JSON 对象来表达函数调用,函数名、参数名、参数值全部以 JSON 字段的形式呈现。这种格式的好处显而易见:JSON 的解析生态成熟,几乎所有端侧运行时都支持;而且将“语言生成”和“结构生成”做了更清晰的区隔,模型被明确告知“你应该输出 JSON,而不是闲聊”。我用 CT2 格式做了大量测试后感觉,它的失败率显著低于 CT1,尤其是在参数抽取这一步,模型的稳定性好很多。
2.2 对齐训练:为什么轻量模型也能稳定选函数
如果只是把函数调用格式塞进提示词里,让普通语言模型照着格式生成,结果会很惨烈。原因在于,轻量模型在生成后续 token 时,上下文里可能有大量其他文本干扰,模型很容易“忘记”要遵守格式,或是在长函数列表面前分不清该选哪个。FunctionGemma 的对齐训练解决的就是这个问题。
在训练阶段,它使用大量由用户指令、候选函数列表、正确调用结果组成的样本,让模型反复学习“customer intent 应该映射到哪一个函数”“参数值应该从用户原文里的哪个片段抽取”这两条映射关系。训练目标覆盖了函数选择的准确率、参数抽取的完整度、格式合法率等多个维度。这个过程的本质,是把“工具使用”从一种“可以会、也可以不会”的附带能力,变成模型的核心能力。一个值得注意的点是:对齐后的模型在未见过的函数 schema 上仍然能泛化,也就是说,只要你在提示词里给出一个新函数的定义和示例,它通常能举一反三地调对,不需要为每个新工具重新微调。
实测中我发现一个规律:函数选择这一类任务,对模型的“指令遵循”能力要求很高,而轻量模型在指令遵循上恰恰是短板。FunctionGemma 通过把函数调用格式作为硬性输出约束引入训练,相当于给了模型一个“脚手架”,这比在通用对话模型上加提示词模板要可靠得多。
3. 端侧落地架构:从用户指令到工具执行的完整链路怎么搭
模型本身具备函数调用能力,只解决了一半问题。真正在端侧做一个可用的智能体,还需要把模型放进一条完整的执行链路里。我搭过不止一个端侧智能体原型,最后沉淀下来的架构可以概括为五个模块:输入解析、函数注册表、意图路由、模型推理、工具执行与反馈合成。
3.1 五个模块各司其职
输入解析模块负责接收用户的自然语言指令。在端侧场景里,输入可能来自文本,也可能来自语音识别结果。这里有个容易忽略的细节:语音识别可能引入错误词,比如把“关爱模式”听成“关怀模式”,如果直接丢给模型,函数选择会出现偏差。所以我在输入解析阶段会做一个轻量的纠错和归一化,把同义表达映射到标准说法上,这能显著提升下游调用准确率。
函数注册表是整个系统的“能力清单”。每个工具以 JSON Schema 的形式注册,包括函数名、功能描述、参数列表、参数类型、枚举值约束等。这个注册表有两个作用:第一,它作为提示词的一部分传给模型,告诉模型“你能用什么工具”;第二,它是工具执行层的校验依据,模型输出函数调用后,执行层用它来做参数合法性和安全边界的检查。函数注册表的设计直接决定了智能体的上限——注册表里没有的工具,模型再聪明也调用不了。
意图路由模块是个轻量级的过滤器。它先判断用户输入是否需要调用工具:像“今天天气怎么样”这种需要工具,像“你好呀”这种纯闲聊就不需要。路由做得好,可以避免模型在闲聊场景里也强行套一个函数调用,浪费算力还显得很呆。在资源更充裕的设备上,我会用一个小分类模型做路由;资源紧张时,用基于关键词加规则的方式也能凑合。
3.2 FunctionGemma 在链路中的位置
模型推理模块是整条链路的核心。FunctionGemma 在这个环节接收拼接好的提示词——包含系统指令、函数注册表、用户输入和输出格式要求——然后生成结构化函数调用结果。这条链路的顺序和反馈机制很关键:
用户输入 → 输入解析与纠错 → 意图路由 → 拼装提示词 → FunctionGemma 推理 → 解析函数调用 → 参数校验 → 工具执行 → 返回结果 → 反馈合成 → 用户工具执行层是连接模型和真实世界的桥梁。在端侧场景里,工具可能是设备控制接口、本地数据库查询接口、日历读写接口、消息发送接口等等。执行层不仅要执行函数,还要捕获异常、收集执行结果,并把结果格式化成模型可读的结构,便于后续反馈合成。
反馈合成模块把工具返回的原始数据(比如“当前室温25.6摄氏度”)组织成自然语言回复。这一步既可以用 FunctionGemma 继续生成,也可以用一个更简单的模板拼接,取决于设备算力和对回复自然度的要求。我一般建议在资源允许时让模型来组织回复,因为工具返回结果往往是结构化的冷数据,直接拼接会显得生硬。
3.3 为什么把函数调用单独交给一个模型
这里我想解释一个设计取舍。很多人问:既然我是拿对话模型在做智能体,为什么不直接让同一个模型既聊天气、又管空调、又查日历?理论上当然可以,但在端侧受限环境下,单一通用模型面对多种任务的指令遵循负担会变大——它会频繁地在“闲聊模式”和“工具调用模式”之间切换,切换就容易出错。把函数调用作为一个专门的输出模式,用特化模型来承担,其实是在做“职责分离”:语言理解归语言理解,工具调用归工具调用。这样调优时可以针对函数选择的准确率单独干预,而不会影响对话体验。
前面这些都是架构层面的规划。你可能会觉得有点虚,所以下一节我用代码走一遍最小可运行实现——从定义函数,到部署模型,到跑通一次完整的“说一句话→执行一个动作”闭环。
4. 最小可运行实现:从函数定义到模型调用的代码走读
纸上谈兵没有意思,我给你一个可以直接复现的最小实现。这个 Demo 我跑过很多次,只依赖 Python 环境和量化后的 FunctionGemma 模型,不需要额外硬件,一台普通开发板或带 CPU 推理能力的设备就够了。
4.1 第一步:定义函数注册表
假设我们做一个室内环境控制助手,需要三个工具:控制灯光开关、查询气象信息、设定定时器。每个工具用 JSON Schema 描述:
{ "functions": [ { "name": "control_light", "description": "控制指定区域的灯光开关状态", "parameters": { "type": "object", "properties": { "location": { "type": "string", "enum": ["living_room", "bedroom", "kitchen"], "description": "目标区域" }, "action": { "type": "string", "enum": ["turn_on", "turn_off"], "description": "开关动作" } }, "required": ["location", "action"] } }, { "name": "query_weather", "description": "查询指定城市当前天气与温度", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称" } }, "required": ["city"] } }, { "name": "set_timer", "description": "在指定时长后触发提醒", "parameters": { "type": "object", "properties": { "duration_minutes": { "type": "integer", "description": "提醒时长(分钟)" }, "label": { "type": "string", "description": "提醒内容" } }, "required": ["duration_minutes", "label"] } } ] }函数注册表的设计有几个可以展开的地方。描述字段是关键——模型依赖它理解“这个函数是干嘛的”,描述写得含糊,模型就会选择困难。比如query_weather的描述如果只写“查询天气”,那用户说“上海冷不冷”时模型可能不知道要不要带上城市参数;写清楚“查询指定城市当前天气与温度”之后,模型抽取参数时就更有依据。我在实验里对比过,描述从 5 个词扩展到 20 个词,参数抽取的准确率能提高十几个百分点。这是端侧智能体工程里性价比最高的一笔投入。
4.2 第二步:加载模型并构造提示词
接下来加载量化后的 FunctionGemma 模型。端侧部署一般选择 INT4 或 INT8 量化版本,内存占用可以压到 2GB 以内。量化版本在函数调用这种高结构化任务上的表现,和未量化版本差距没有想象中大,主要是在复杂参数抽取时偶尔会把数字搞错,我会在第 6 节细说。
模型加载后的关键动作是拼装提示词。提示词的结构大致是这样:
def build_prompt(user_query: str, function_schema: dict) -> str: system_prompt = ( "你是一个端侧智能体。你需要根据用户指令,从下列函数中选择一个合适的函数," "生成符合 JSON 格式的函数调用。不要生成函数调用之外的任何内容。\n\n" "可用函数定义如下:\n" f"{json.dumps(function_schema, ensure_ascii=False, indent=2)}\n" "输出格式:{\"name\": \"函数名\", \"arguments\": {参数键值对}}\n" ) return f"{system_prompt}\n\n用户指令:{user_query}\n\n函数调用:"构建提示词时有一个很重要的注意事项:函数列表过长会直接冲击轻量模型的注意力机制。我做过一个实验,在 2B 参数的量化模型上,函数列表从 5 个增加到 20 个,函数选择的准确率从 92% 掉到 78%。因为模型需要在上下文中准确区分 20 个函数各自的名字、描述、参数约束,这对长文本注意力是很大的考验。在后面第 6 节我会讲几种缓解方案,但这里你先记住一个原则:端侧函数注册表要精简,能 5 个搞定就不要放 20 个。
4.3 第三步:推理并解析函数调用
模型推理时,把温度参数调低很重要。我在端侧测试时发现,temperature=0到0.2之间,函数选择的确定性最好;默认的0.7会让模型在多个相似函数之间反复摇摆,甚至随机挑一个。采样参数如下:
generation_config = { "max_new_tokens": 128, "temperature": 0.1, "top_p": 0.9, "do_sample": True }注意到栈顶 token 概率极高时,其实do_sample=False(贪心解码)更稳妥。我的实测结论是:函数调用这类结构化输出,贪心解码的准确率通常不会低于采样,而且不会有意外输出。早期我用采样模式,结果模型偶尔会生成“函数调用:json…”这种带 Markdown 标记的非法输出,后来改成贪心解码才好转。
推理完成后,把输出中的 JSON 块提取出来,和函数注册表做交叉验证:
def parse_and_validate(raw_output: str, function_schema: dict): try: call_data = json.loads(raw_output.strip()) except json.JSONDecodeError: raise ValueError("模型输出不是合法 JSON") func_name = call_data.get("name") args = call_data.get("arguments", {}) matched = next((f for f in function_schema["functions"] if f["name"] == func_name), None) if matched is None: raise ValueError(f"函数 {func_name} 不在注册表中") # 校验必填参数与参数类型 required = matched["parameters"].get("required", []) for key in required: if key not in args: raise ValueError(f"缺少必填参数 {key}") return func_name, args这一步把模型输出和真实执行环境之间的“信任边界”显式地建立起来。模型返回了函数名和参数,不代表它就可以执行了,必须先过注册表的校验。我在运维这套逻辑时,遇到过几次模型把location参数抽成"卧室灯"这种自由文本,而 schema 里只允许三个枚举值,幸亏在校验层拦住了,否则工具执行层会拿一个非法参数去控制硬件,后果可大可小。
4.4 第四步:执行工具并回填结果
校验通过后,把函数调用分发到对应的执行函数:
def dispatch(func_name: str, args: dict): if func_name == "control_light": return control_light(args["location"], args["action"]) elif func_name == "query_weather": return query_weather(args["city"]) elif func_name == "set_timer": return set_timer(args["duration_minutes"], args["label"]) result = dispatch(func_name, args) print("工具执行结果:", result)完整跑通之后,你可以在设备上输入“客厅开灯”,看到模型输出{"name": "control_light", "arguments": {"location": "living_room", "action": "turn_on"}},然后执行层真正去操作灯控接口。
从这个最小实现里,你应该能感受到函数调用模型的用法和对话模型有个明显区别:你不再关心模型的回复措辞是不是自然,你关心的是它输出的函数调用是不是合法、参数是不是完整。这也是为什么我在多个项目里都坚持“把函数调用解析和校验做成独立模块”,而不是在示例代码里顺手json.loads一下就完事。
5. 现实世界比沙箱残酷:参数校验、错误恢复与结果可信度
跑通 Demo 之后,我一度觉得端侧智能体这事不算难。直到我把这套代码从“受控测试场景”搬进“带真实外设的环境”,才体验到什么叫现实世界的残酷。
5.1 参数校验:模型经常给你“合法但不合理”的输入
模型生成的参数在类型上可能是合法的,但逻辑上完全不成立。举个例子:用户在 4 月问“六一儿童节提醒我买礼物”,模型可能会生成{"duration_minutes": 43800}——这个数字按分钟换算确实对,但问题是 4 月到 6 月之间可能跨了月份,天数不是固定的。端侧设备上的日历数据是模型看不到的,它只能根据字面意思估算。
我踩过的另一个坑是枚举值越界。函数注册表里明明只允许turn_on和turn_off,模型偶尔会输出{"action": "toggle"}。这种错误在模型层面是“合理猜测”,在业务层面却是非法操作。后来我在校验层加了一层“业务约束校验”,不再只看类型,还会检查枚举、范围、依赖关系。比如set_timer的duration_minutes限定 1 到 10080(7 天),模型生成负数时就地拦截。
5.2 错误恢复:工具执行失败后,智能体要能“体面地承认失败”
工具执行层不可避免会失败:设备离线、硬件无响应、权限不足、查询超时。失败的反馈如果直接抛给用户,体验会很差;如果让模型“假装成功”,又丧失了智能体的可信度。正确的做法是:把失败状态结构化,交给模型组织回复。
我用过一种比较有效的方案,在工具执行层捕获异常后,生成一个{"status": "error", "error_code": "DEVICE_TIMEOUT", "friendly_message": "设备响应超时,请检查连接或稍后重试"}这样的结果喂回给模型,让模型基于这个上下文生成回复。这样模型会自然地接一句“我尝试控制客厅灯光,但设备没有响应,可能断开了连接”,而不是干巴巴地报错。
5.3 处理多轮对话中的上下文引用
端侧智能体一定会遇到用户追述:“等等,我说的是卧室不是客厅。”这句话里没有完整指令,如果单独丢给 FunctionGemma,模型根本不知道在说什么。解决思路是在输入解析阶段做“指代消解与上下文合并”:把上一轮解析出的意图和参数缓存起来,当检测到当前指令是修正或补充时,合并两轮信息再送进模型。
我的实现方式是维护一个轻量级的session_state,记录上一轮的函数名和参数。在用户发出新指令后,先用规则判断是否存在“修正信号”(比如“不是”“改成”“换成”),如果有,就把两轮文本拼成一条带上下文的提示词,例如“用户先要求控制客厅灯光,刚才又改口说改为卧室灯光,请重新生成函数调用”。FunctionGemma 在这种“要给新意图重新选函数”的任务上表现不错,因为它本质上是根据完整指令做决策,而不是逐句独立判断。
5.4 结果可信度:警惕“幻觉函数”
轻量模型在生成函数调用时,偶尔会输出一个注册表里根本不存在的函数名。如果模型上下文里没有严格限定“只能从给定函数中选择”,它可能脑补出control_temperature、send_email之类的东西。这在沙箱测试里不常见,但在函数数量多、上下文长度大、模型量化后精度受损的情况下会频繁出现。防范手段就是我第 4 节代码里的白名单校验:解析出的函数名不在注册表中,直接拒绝执行,并用纠错提示语引导模型重新生成。这比事后追责要好得多。
还有一个值得注意的安全问题:某些工具属于“高风险操作”,比如发送消息、删除文件、转账、修改系统设置。我在执行层加了一个确认机制:当用户指令命中高风险函数时,模型先生成调用,执行层不直接触发,而是返回“请确认是否执行”,由用户二次确认后才真正执行。这个机制的成本很低,但能避免“模型误解指令导致误操作”的最坏情况。
6. 端侧性能调优:模型选型、量化与延迟的平衡方案
函数调用模型在端侧落地,性能调优是绕不开的话题。端侧环境每一项资源都紧张,模型尺寸、推理延迟、内存占用、准确率这四者之间是互相拉扯的关系。
6.1 模型选型:2B 级别是端侧函数调用的甜点位
以我之前用过的某款 2B 量化模型为例,它在函数选择准确率、参数抽取完整度上已经能够满足日常工具调用场景。7B 级别的模型在复杂指令理解上会更强,但内存占用和推理延迟会翻倍,在主流移动设备上体验往往会变差。我的建议是:如果你的工具注册表少于 10 个函数、指令长度较短、参数结构清晰,优先选 2B 级别;如果函数数量多、参数嵌套深、指令经常带复杂条件,可以考虑 7B 级别,但要接受延迟上升的现实。
6.2 量化策略的实测对比
我做过一组量化对比实验,记录感受如下:
| 量化精度 | 内存占用 | 函数选择准确率 | 参数抽取准确率 | 备注 |
|---|---|---|---|---|
| 原始 FP16 | 高,约 4GB 级 | 基准 | 基准 | 端侧难以直接部署 |
| INT8 | 降低约一半 | 略降 | 基本持平 | 大多数端侧场景的首选 |
| INT4 | 降幅最大 | 明显下降 | 偶尔出错 | 适合内存极受限设备 |
INT4 量化在函数选择准确率上的下降幅度比直觉要大,尤其是在函数描述差异较小的场景,模型容易把control_light和control_fan搞混。如果设备内存实在紧张,我会把 INT4 版本配一个“二次确认”机制来兜底,而不是裸奔。
6.3 延迟优化三板斧
第一板斧是模型预热。端侧第一次推理往往比后续推理慢一大截,因为要加载权重、初始化 KV cache、做算子适配。把一次空推理放在设备启动后的空闲期完成,能显著改善用户首次交互的体感。
第二板斧是减少上下文长度。函数注册表全部塞进提示词里会让序列变长,推理时间几乎随 token 数线性增长。我在实践中会做“函数分组预筛选”:把函数按领域分组(环境控制、气象服务、日程管理),先在模型推理前用一个极轻量的关键词分类器选出候选分组,只把这一组的函数 schema 拼进提示词。分组之后,模型上下文大幅缩短,推理延迟和准确率双双受益。
第三板斧是控制输出长度。函数调用的输出通常不到 50 个 token,但如果你把max_new_tokens设得很大,模型有时候会“发挥”出一堆多余内容。把max_new_tokens收紧到 128,既能避免模型话痨,也能减少无谓的推理时间。
6.4 内存受限时的模型管理
端侧设备往往跑着多个应用,模型常驻内存不现实。我的做法是按需加载:设备空闲时卸载模型,检测到智能体被召唤时再加载。配合量化,加载耗时通常能控制在可接受范围内。还有一种做法是流式推理——第一个 token 出来后就开始响应用户,其余 token 边生成边解析,用户会感觉响应更快,这在函数调用场景里成果有限,因为通常需要拿到完整 JSON 才能解析执行,但对长参数输出的场景仍然有效。
性能调优这件事没有银弹,最终是在“用户体验”和“资源占用”之间不断试探。我的经验是每调一步都要回到两个问题:用户在意的到底是延迟还是准确率?这个场景能不能容忍一次失败?答案不同,调优方向就不同。
7. 从“单次调用”到“多步任务”:智能体还能怎么长进
把一个单轮的函数调用闭环跑通,只是端侧智能体的起点。真实用户需求往往不是“控制一个灯”这样的单步指令,而是“我出门后把家里的灯和空调都关掉”这种涉及多个工具、多个步骤的组合任务。
7.1 多步任务规划
单步调用的模型只负责“从输入到函数调用”的一次映射。组合任务需要模型有能力把目标拆解成若干个函数调用序列,并且确保前一步执行结果能作为后一步的输入。这个能力目前我看到的做法有两种:一种是在提示词里引入“你还可以分析上一步执行结果来决定下一步动作”的引导,让模型在一次推理中连续生成多个调用;另一种是把多步任务交给一个上层规划器,规划器拆分后逐条交给 FunctionGemma 执行。端侧场景我更偏向后者,因为上层规划器可以用规则模板加轻量分类实现,避免让模型背上太长的输出包袱。
7.2 状态记忆与个性化函数库
智能体最好能记住“上次出门时把灯都关了”这种信息。用户不说,系统也能推断出动作偏好。这需要在端侧维护一个历史行为记录,在构建提示词时把相关记忆片段插入。FunctionGemma 的上下文窗口不算大,记忆不是越多越好,我的做法是做“场景相关的记忆压缩”:只把与当前意图相关的历史片段抽象成一句话放进提示词。
个性化函数库则更进一步:允许用户动态添加自定义工具,比如“给阳台浇水”。每次新增工具时,系统自动生成新的 JSON Schema 并加入注册表。实测中,模型对新增函数的泛化能力让人满意,前提是新函数的命名和描述清晰,且添加后需要做一次校准测试——让用户输入几个典型指令,确认模型能正确触发新函数。千万不要加了工具不验证就上线,轻量模型对相似函数的混淆率通常比你预期的高。
7.3 多模型协同的一种思路
端侧不应该只有一个大模型包打天下。我在一个模拟项目里做过这样的分工:一个极小的分类模型负责意图路由,把输入分成“天气类”“设备控制类”“日程类”“闲聊类”;FunctionGemma 只在工具调用类指令上被激活。闲聊场景用另一个更轻量的对话模型处理。这套架构的好处是,每个模型都只做自己最擅长的事,资源利用率最高。代价是端侧要管理多个模型的生命周期,工程复杂度上升一个层级,但换来的是更高的整体可靠性和更低的单次调用成本。
7.4 关于未来的三个预见
第一个预见:端侧智能体会从“单模型智能体”走向“多模型协同体”。专门的函数调用模型、专门的意图路由模型、专门的语音处理模型各司其职。第二个预见:函数调用与工具生态会逐渐形成一套通用 schema 标准,设备厂商只要暴露符合标准的工具描述,智能体就能即插即用。第三个预见:端侧智能体的“信任机制”会成为核心竞争力,用户不会轻易让一个设备本地模型去执行高风险操作,必须有可解释的决策日志和可回退的操作历史。
这里我不做展望式的结尾,说点更实在的。我在实际操作中最大的体会是:函数调用类模型的可靠程度,和你给它的提示词质量高度相关。把函数描述写清楚、把输出格式锁死、把采样参数调保守、把校验层做厚,这四个动作做到位,两亿参数的模型也能撑起一个堪用的端侧智能体。反过来说,如果这四个动作偷懒,再大的模型也会在端侧场景里翻车。
这半年多我把 FunctionGemma 类模型从模型选型、量化、提示词优化到执行链路做了完整折腾,最值钱的经验只有一句:对话模型解决的是“说什么”,函数调用模型解决的是“做什么”,而端侧智能体的核心工程问题,永远是如何让“做什么”这件事变得可控、可测、可回退。想入坑的朋友,可以从一个最简单的单函数闭环开始,再逐步加函数、加场景、加记忆,这条路走一遍,你对智能体的理解会脱胎换骨。