news 2026/10/21 3:41:30

大模型Function Calling实战:原理、代码与最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型Function Calling实战:原理、代码与最佳实践

1. 为什么需要Function Calling:先搞清楚它解决的痛点

先说个场景。你搭了一个聊天机器人,用户问“明天杭州会下雨吗”,模型可以流利地答出一段关于杭州天气的漂亮话,但它自己根本不知道明天的天气数据。它是生成文本的模型,不是天气预报系统。它能把话编得听起来很合理,仅此而已。

在大模型真正接入业务系统之前,所有人都面临同一个困境:模型只能“说话”,不能“做事”。它不知道数据库里有哪些用户,不知道订单接口长什么样,更不知道实时的天气、股价、库存。让AI“更强大”的第一步,其实就是把模型和外部工具连接起来——而Function Calling就是目前最主流、最直接的一条通道。

在没有Function Calling的年代,我们是怎么做的?土办法大概有三种。

第一种:提示词硬编码。在system prompt里写清楚“如果需要查天气,请输出[WEATHER_QUERY]杭州 明天”,然后再用正则或字符串匹配去截取这个标记。这种方法有两个硬伤:模型不一定老老实实跟着格式走,而且一旦指令复杂一点,比如“查完天气顺便帮我看看杭州有没有适合带小孩的室内活动”,模型就不知道先触发哪一个接口了。

第二种:带历史重新调用。把对话历史全部传给后端,后端用规则判断该调什么API。这个方案把决定权从模型手里拿了回来,但规则本身的维护成本极高,而且模型在前端生成的回复经常已经偏离了“可执行”的轨道,你只能眼睁睁看着它“一本正经地胡说八道”。

第三种:事后检查结果。先让模型回复,再在后端做NLP分析去猜它是不是想要某个工具的结果。这个思路听起来很“智能”,实际上非常不可靠,因为用户的一句话里可能有多种意图,而且同一种意图的表达方式太多了,规则根本写不完。

Function Calling解决的不只是“能不能调工具”这一个点,它同时解决了三个问题:工具发现(模型知道有哪些工具可选)、参数对齐(模型把用户的话转成工具需要的结构化参数)、协议统一(模型和工具之间用一套固定格式的JSON来通信)。这篇文章会从原理到代码一步步拆解,面向两类读者:一类是刚接触大模型开发、想给AI加一点“手和脚”的新手,另一类是已经用过Function Calling但经常踩坑、想把调用链路做扎实的进阶开发者。

2. 原理拆解:Function Calling到底是“调用”还是“假装调用”

2.1 核心认知:模型不执行函数,它只生成“调用意图”

这是新手最容易误解的一步。很多人以为Function Calling是模型直接跑去执行了某个Python函数,其实根本不是这么回事。

整个过程的真相是这样的:你预先定义好一批函数,每个函数用JSON Schema描述清楚“这个函数叫什么”“需要哪些参数”“参数是什么类型”,然后把这批描述和用户的对话一起发给模型。模型读完描述之后,如果判断当前对话需要用到某个函数,它不会真的去执行,而是输出一段结构化的JSON——内容大致是“我建议调用函数get_weather,参数是{"city": "杭州", "date": "明天"}”。

真正去执行这个函数的人是你,是你的后端代码。你把这个JSON解析出来,调用真实的天气API,拿到结果,然后再把这个结果作为一条消息发回给模型,模型读了结果之后,才生成最终回复给用户。

一句话总结:Function Calling的本质是“意图识别+参数提取+格式约束”三合一的解码行为,不是执行行为。模型负责筛选和填写调用单,你负责照着调用单去干活。

理解这一点非常重要,因为几乎所有的“Function Calling不稳定”问题,源头都能追溯到这三个环节里的某一个没有做扎实。

2.2 三个关键环节:意图识别、参数对齐、格式约束

意图识别是模型判断“该不该调用函数”。这一环节的表现主要取决于你的函数描述写得好不好。比如你给了一个get_weather函数,描述是“获取天气信息”,那么当用户问“明天杭州适合穿什么”的时候,模型可能不确定这个函数是否匹配。但如果你把描述改成“获取指定城市在指定日期的天气情况,包含温度、降水概率、风力等,可用于回答穿搭建议、出行规划问题”,模型就更容易在“穿什么”和“天气查询”之间建立联系。换句话说,函数的description不是写给用户看的,是写给模型看的。

参数对齐是模型把用户的话填进参数里。用户在对话里说的是“明天”、“杭州”,模型要把这两个自然语言表达转换成{"date": "2025-06-10", "city": "杭州"}这样的结构化值。这里有个细节:模型做的是“字符串到JSON”的转换,不是“语义到数据库字段”的转换,所以如果你定义的参数名和用户习惯的表达差异太大,模型就很容易填错。比如用户习惯说“明天”,函数里却要求传一个forecast_period参数,取值只能是“today”“tomorrow”,模型就需要额外推理。尽量让参数命名贴近常识,能少踩很多坑。

格式约束是确保模型输出的JSON是合法的、可解析的。现在的API基本都支持强制JSON输出模式,但如果你用的模型版本或API不支持,就需要靠参数设置来兜底。后面我会专门讲temperature和response_format的影响,这里先记住一个结论:输出格式的稳定性是Function Calling的命脉,格式坏了,后面一切逻辑都白搭。

2.3 函数声明与JSON Schema:模型的“操作手册”

模型知道的每一个工具,都是你通过一个叫tools的参数传进去的。一个典型的函数声明长这样:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市在指定日期的天气情况,包含温度、降水概率、风力、湿度等,适用于出行、穿衣、活动规划等场景", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如杭州、上海、北京" }, "date": { "type": "string", "description": "日期,格式为YYYY-MM-DD,默认为今天" } }, "required": ["city"] } } } ]

这段代码其实就是把函数的“使用说明书”写给了模型。你越详细地描述函数是干什么的、参数是什么意思,模型就越能做出正确的调用决策。

实操中我建议在description里包含三类信息:函数的业务场景、参数的含义与合法取值、常见的触发场景举例。别怕写得多,这段描述不会被用户看到,它唯一的作用就是让模型理解得更准。

但注意一个平衡:如果你把description写成一篇300字的小作文,一些模型在长上下文中可能出现注意力分散,反而表现更差。经验值是每个函数描述控制在50~120字之间,把核心信息说清楚就够了。你甚至可以针对你使用的模型做A/B测试——同样的函数描述,按不同长度版本各跑一轮测试集,看哪个版本的调用命中率更高。

3. 从零写一个完整示例:天气查询机器人的Function Calling全流程

3.1 准备工作:选什么模型、什么SDK

市面上的主流大模型API基本都支持Function Calling,包括OpenAI系、Anthropic系、开源的Qwen、GLM等国内模型。不同家的API在接口细节上有差异,但核心逻辑高度一致:传tools、收tool_calls、回传tool消息。你先跑通任何一家的流程,再去迁移到别家,成本极低。

代码部分我这篇文章用Python来写,SDK以OpenAI风格为例,因为它是目前被模仿最多的标准接口。如果你用的是其他模型,只要把client的base_url和model替换掉,整个逻辑可以无缝平移。

from openai import OpenAI client = OpenAI( api_key="你的API密钥", base_url="你的API地址(如果使用兼容网关则填写,官方API可省略)" ) MODEL = "你的模型名称"

有一点要提醒:如果你的模型是通过某个聚合网关访问的,一定要确认网关支持传入tools参数,有些网关会悄悄丢弃未知字段,导致Function Calling完全不生效——这是我在实际项目中遇到过的真实情况,排查了很长时间。

3.2 定义真实业务函数:我们不能真的接天气API,但可以模拟一个

为了演示完整的“执行函数→把结果喂回模型”链路,我先定义一个真实的Python函数。这里的真实是说它有真实业务逻辑,会校验参数、返回结构化结果;至于数据来源,我们用mock数据代替真实API,这样任何人都能直接跑通。

import json import re from datetime import datetime, timedelta def get_weather(city: str, date: str = None): """模拟真实天气查询接口,实际项目中这里替换为第三方API调用""" if date is None: date = datetime.now().strftime("%Y-%m-%d") # 做一次简单的日期格式校验,模拟真实接口的参数校验行为 if not re.match(r"^\d{4}-\d{2}-\d{2}$", date): return {"error": "date格式不正确,应为YYYY-MM-DD"} # mock数据:实际项目中这里调用requests.get(weather_api_url, params=...) mock_db = { ("杭州", "2025-06-10"): {"temp": 28, "precip_prob": 0.2, "wind": "东南风3级", "humidity": 65, "desc": "多云转晴"}, ("上海", "2025-06-10"): {"temp": 30, "precip_prob": 0.5, "wind": "南风2级", "humidity": 70, "desc": "阵雨"}, } data = mock_db.get((city, date)) if data is None: return {"error": f"暂无{city}在{date}的天气数据"} data["city"] = city data["date"] = date return data

这个函数体现了几个真实接口的通用特征:有默认参数、有入参校验、有业务判断,最后返回的都是能直接转成JSON的字典。实际项目中你只需要把mock部分替换成requests.get的调用就可以了。

3.3 完整的对话循环:一步步解释每段代码在干什么

现在进入核心。一个标准的Function Calling对话循环可以拆成六个步骤,我把它完整写出来,再逐行解释。

def run_conversation(user_input): # 第一步:组装函数声明 tools = [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市在指定日期的天气情况,包含温度、降水概率、风力、湿度等,适用于出行、穿衣、活动规划等场景", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,如杭州、上海"}, "date": {"type": "string", "description": "日期,格式YYYY-MM-DD,缺省为今天"} }, "required": ["city"] } } } ] # 第二步:初始化消息列表。system可选,但建议写,用来规定回复风格 messages = [ {"role": "system", "content": "你是一个智能助手,可以根据用户的问题使用工具获取信息。回答时简洁友好。"}, {"role": "user", "content": user_input} ] # 第三步:发起第一次请求,把tools传给模型 response = client.chat.completions.create( model=MODEL, messages=messages, tools=tools, tool_choice="auto", # auto表示由模型决定是否调用工具 ) # 第四步:判断模型的返回里有没有tool_calls assistant_message = response.choices[0].message # 第五步:如果模型决定调用工具,就循环处理每一个调用 if assistant_message.tool_calls: # 先把模型的这个带tool_calls的消息加入messages # 注意:这里要把整个assistant_message对象加进去,不能只加content messages.append(assistant_message) # 逐条执行工具并回传结果 for tool_call in assistant_message.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) print(f"[工具调用] 模型请求调用 {fn_name},参数为:{fn_args}") # 这里可以做一个函数路由,实际项目中函数多了用字典映射更清晰 if fn_name == "get_weather": result = get_weather(**fn_args) else: result = {"error": f"未知函数 {fn_name}"} messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) # 第六步:把工具结果发回给模型,让模型生成最终回复 second_response = client.chat.completions.create( model=MODEL, messages=messages, tools=tools ) return second_response.choices[0].message.content # 模型没有调用工具,直接返回它的文本回复 return assistant_message.content if __name__ == "__main__": print(run_conversation("杭州明天天气怎么样?适合跑步吗?"))

运行这段代码,你大概会看到这样的输出:

[工具调用] 模型请求调用 get_weather,参数为:{'city': '杭州', 'date': '2025-06-10'} 杭州明天多云转晴,最高气温28℃,降水概率20%,东南风3级,比较适合跑步。

这个输出的后半段说明了Function Calling的整个价值闭环:模型没有编造天气数据,而是根据工具拿到的真实结果来回答。这就是“让AI更强大”的最直观体现——它从“一本正经胡说八道”变成了“查了再说”。

3.4 函数路由的工程化写法

当项目里的函数超过3个,最忌讳的就是在代码里写一长串if/elif来分发。我建议用一个字典来做路由,清晰得多:

def get_weather(city, date=None): pass # 上面已实现 def get_stock_price(symbol): pass # 另一个业务函数 FUNCTION_ROUTER = { "get_weather": get_weather, "get_stock_price": get_stock_price, } def execute_function_call(tool_call): fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) handler = FUNCTION_ROUTER.get(fn_name) if handler is None: return {"error": f"未注册的函数: {fn_name}"} try: return handler(**fn_args) except TypeError as e: return {"error": f"参数错误: {str(e)}"} except Exception as e: return {"error": f"执行异常: {str(e)}"}

这里我故意把try/except写在路由外层,因为实际生产环境中,工具函数可能因为网络超时、参数越界、第三方API返回异常等原因抛错。错误的兜底策略是:宁可让工具返回一个error信息的JSON,也不能让整个请求崩掉。你把error信息喂回模型,模型会向用户解释“查询失败”并给出后续建议;如果你让进程直接抛异常,用户只会得到一个冷冰冰的502。

4. 提升Function Calling质量的几个关键细节

4.1 temperature参数:不是你想象的那样

很多人习惯把temperature调高让模型“更有创造性”,但在Function Calling场景里,这往往是灾难的开始。

temperature控制的是采样随机性。当模型需要输出结构化JSON去调用函数时,随机性越高,JSON格式就越容易出现错误——比如多余的逗号、不匹配的括号、字符串没有正确转义。虽然现在部分新模型支持了原生的strict模式,会从解码层面保证JSON合法,但如果你用的老模型或第三方中间件不支持strict模式,那么temperature调的越高,函数调用出错的概率就越大。

我的经验做法是:涉及Function Calling的系统,temperature设置不超过0.3;如果场景完全不追求创造性,直接设置成0。模型回复用户的时候语气会稍微“呆板”一点,但换来的是稳定可靠。对于用户而言,一个能准确解决问题的助手,远比一个有“个性”但经常翻车的助手有价值。

4.2 多个函数同时触发:并行调用(Parallel Tool Calls)

现在的API普遍支持并行工具调用,也就是说一条消息里可能返回不止一个tool_call。比如用户问“杭州和上海明天分别适合穿什么?”,如果两个城市都注册了get_weather,模型可能一次性发出两个调用请求,而不是等一个回来再发第二个。

我的处理建议是:在函数执行层使用并发,不要串行等待。比如用concurrent.futures.ThreadPoolExecutor把多个tool_call并发执行,整体耗时能从2秒降到0.5秒左右。但要注意一个隐藏问题:如果多个工具函数之间没有依赖关系,并发是安全的;如果有依赖关系(比如先要拿到用户ID才能查订单),你就得在代码里做依赖编排,这时用LangChain的ToolNode或者自己写一个简单的DAG调度器会更合适。

另外,多个函数同时触发时,模型实际上是在同一个token序列里生成了多个独立的JSON片段,每个片段都会有一个独立的id。你回传结果的时候必须严格使用tool_call_id逐一对应,不能把所有结果都塞到同一个tool消息里。

4.3 参数级别的校验:模型的“自觉”没那么可靠

即使模型把参数填对了,也不代表参数的语义是对的。举个例子:用户说“帮我查一下北京后天的天气”,模型可能准确填出{"city": "北京", "date": "2025-06-11"},但如果是“帮我查一下我上次买的东西到哪了”,模型可能把用户ID填成0或者空字符串。

所以在执行工具函数之前,我建议加一层参数校验。校验规则可以很简单:必填参数有没有值、日期格式是否正确、数字是否在合法范围。甚至可以做逻辑校验,比如end_date必须晚于start_date。这些校验规则是写在你的函数内部的(像上面mock函数里的re.match),不要指望模型保证。

还有个更微妙的问题:模型填的日期可能依赖它自己的训练知识,而训练知识是有截止日期的。比如用户说“大后天”,模型需要根据“今天”来推算,如果模型不知道“今天”是哪一天,它就很可能算错。解决方式有两种:第一种是在system prompt里显式告知今天的日期;第二种是在每个user消息前插入一条包含当前日期的上下文消息。我实测下来第二种更稳,因为prompt里的日期信息经常被长上下文稀释。

4.4 tool_choice参数:三种模式各有用途

tool_choice参数控制的是“模型是否必须调用工具”,三个常见取值:

  • "auto":模型自己判断要不要调用工具。适合大多数场景,但偶尔会漏调用。
  • "none":强制模型不调用工具。适合闲聊兜底场景。
  • {"type": "function", "function": {"name": "get_weather"}}:强制模型调用指定函数。适合你已经确定用户就是要查天气的场景。

这里有一个值得分享的实操技巧:当你的系统有多个Agent分工时,可以用tool_choice做硬路由。比如用户说“查天气”“查股票”,你不用先跑一次模型来判断意图,而是直接根据用户输入里的匹配规则来选择强制调用的函数。这本质上是把一部分意图识别工作从模型手里拿回来,交给更可控的规则去完成。

我在一个客服系统里用过这个方案:用户的开场白里包含“订单”字样时,系统直接强制调用search_order工具,意图命中率从77%提升到91%。代价是灵活性降低,所以这个技巧要谨慎使用,适合高频且明确的操作,不适合开放域的对话。

4.5 一次更完整的开发配置示例与调用轨迹追踪

最后再给一个偏工程化的实战配置示例,适合在生产环境起步时参考。核心思路很简单:所有进出模型的请求和响应都落到日志里,方便回溯和调试。

import logging import time from openai import OpenAI logging.basicConfig(level=logging.INFO) logger = logging.getLogger("function_calling") client = OpenAI(api_key="你的API密钥", base_url="你的API地址") def call_model_with_trace(messages, tools, model=MODEL, temperature=0.1): start = time.time() response = client.chat.completions.create( model=model, messages=messages, tools=tools, temperature=temperature, ) elapsed = time.time() - start msg = response.choices[0].message logger.info("请求耗时 %.2fs", elapsed) logger.info("模型回复内容: %s", msg.content if msg.content else "") if msg.tool_calls: for tc in msg.tool_calls: logger.info("工具调用: %s(%s)", tc.function.name, tc.function.arguments) return msg

这个日志输出能让你快速回答几个核心问题:这轮对话模型是否选择了调用工具?选择了哪个工具?参数是什么?耗时多少?大量的Function Calling问题排查,第一步都是先看日志,确认模型到底有没有进入工具调用分支,再谈别的。

5. 常见问题与排查实录

这一节是我最想写的部分。网上讲Function Calling原理和基础用法的文章很多,但把“真实项目里踩过的坑”一条条整理出来的很少。这里分享六个我在实际开发中遇到过的问题,每个都附上排查思路。

5.1 模型一直不调用函数,直接给出文本回答

这是最常见的问题。表现是:用户问了一个明显需要工具才能回答的问题,模型却绕过工具,自己编了一个答案。

排查思路按顺序来:

第一,检查tools参数是否真的传到了API。对,就这么简单,很多人第一次犯的错误就是tools没传进去。有些中间件SDK会忽略未声明的参数,所以第一步应该是打印出你实际发出的请求体,确认tools在不在里面。

第二,检查函数描述是否足够清晰。前面说过,description是写给模型看的提示词。如果你的description写的是“获取天气”,模型可能认为“天气”这个话题不一定非要用工具,自己也能“答个大概”。把描述改成“获取指定城市在指定日期的真实天气数据,回答穿衣建议、出行规划类问题时必须调用此工具”,触发率会明显上升。

第三,检查temperature。温度越高,模型越倾向于自由发挥而不是遵循工具调用的约束路径。特别是遇到老模型,temperature=1.0时的工具调用率可能比temperature=0.2时低20个百分点。

第四,看纯文本大模型版本。有些模型的轻量版或蒸馏版,本身对工具调用的指令遵循能力就弱。如果你在某个刚出的小尺寸模型上反复调整描述都不生效,建议先换一个已知对Function Calling支持良好的权威模型做对照测试。这能帮你判断是模型能力问题还是你配置的问题。

5.2 模型调用了函数,但参数填得不对

典型表现:用户说“帮我查杭州明天的天气”,模型调用get_weather时传了{"city": "杭州", "date": "明天"},直接把自然语言“明天”当成date参数塞进来了。

这个问题的根因一般在函数声明的参数描述上。你需要在description里写清楚date的格式要求:“日期,格式YYYY-MM-DD,例如2025-06-10。注意必须转换为具体日期,不允许使用“明天”“后天”等自然语言。”当描述里包含了转换规则和反例时,模型的表现会好很多。

如果还不行,就在外层做参数清洗。写一个对date字段做自然语言解析的预处理函数,把“明天”“后天”“下周一”这些表达统一转成具体日期。这属于防御性编程:不要100%相信模型输出,永远留一手清洗逻辑。

5.3 函数结果正确,但模型回复反而更差了

这可能是比较费解的场景。工具返回的数据明明是对的,但模型整合出来后,要么重复了数据原文导致回答很啰嗦,要么把重点信息漏掉了。

我遇到过的一个典型情况是:工具返回了JSON结构复杂、字段很多的对象,模型面对一坨嵌套的JSON不知道如何取舍,于是干脆把整个JSON“念”给用户。解决办法是,在工具返回给模型前,先在代码层精简结果。只提取用户问题最相关的字段,比如用户问的是“适合跑步吗”,那天气数据里最相关的是降水概率和温度,风力和湿度可以不放,或者放到一个次要位置。

这个经验可以总结成一条原则:喂给模型的数据,不是越多越好,而是越“相关”越好。你把数据清洗得越接近用户问题的答案形态,模型生成最终回复的质量就越高。

5.4 工具调用进入了无限循环

用户问了一个问题,模型调用工具A,把结果喂回去后,模型又调用了工具A,或者调用了工具A+B,然后再喂回去,又调用……直到达到最大轮数。

这个问题的根源通常是模型发现工具返回的结果没有直接回答用户的问题,于是想再调用一次“问得更细一点”。比如第一次传了城市没传日期,工具返回了“日期不能为空”的错误信息,模型就再调用一次,把日期补上——这其实是正常逻辑。但有些情况下,工具返回的内容和预期差距太大,模型就会反复尝试。

我的处理方案是三层防御:

第一,在工具执行函数里做好参数默认值,能不给模型“补参数”的机会就不给。第二,在循环里加最大重试次数,比如用户侧最多允许5次工具调用,超过之后就不再让模型继续调,而是明确告诉它“工具调用次数已达上限,请基于已有信息回答”。第三,对于工具返回的error信息,在喂回模型时加上一句“如果这个错误与用户问题无直接关系,请忽略并基于已知信息回答”。这个方法能阻断大部分无意义的循环。

5.5 模型调用了一个不存在的函数

表现是模型输出的tool_call里,function.name根本不在你定义的tools列表里。理论上不应该出现,但如果你用的是开源模型、私有化部署的模型,确实可能发生,尤其是你更新了系统代码但模型还“记得”旧的工具列表——这类问题多数出在上下文的工具描述缓存或中间件缓存上。

排查思路:看日志确认模型收到的tools列表里到底有哪些函数。优先怀疑两个点:一是代码部署后有没有真正重启模型服务,二是你用的代理网关会不会在某个环节缓存了旧配置。

兜底方案是加一个“未注册函数”的拦截逻辑,就像上面路由代码里写的{"error": "未注册的函数: ..."},确保即使模型输出了非法函数名,链路也不会崩掉。

5.6 结构化输出偶尔是非法JSON

不管是模型版本旧还是temperature设置偏高,非法JSON始终是可能出现的。如果你用的API支持response_format={"type": "json_object"}或strict模式,建议在Function Calling场景里优先开启。开启后模型会在解码层约束输出为合法JSON,乱逗号、缺括号这类问题会从根本上消失。

但有些老模型或第三方网关不支持这个参数,那就只能做调用方兜底:加一个容错解析器。先尝试json.loads,失败后用正则提取JSON片段再试一次,再不行就给用户返回“暂时无法处理,请换一种说法”。这段容错代码虽然不优雅,但能显著降低线上文件的报错率。

def safe_json_parse(raw_arguments): if not raw_arguments: return {} try: return json.loads(raw_arguments) except json.JSONDecodeError: match = re.search(r"\{.*\}", raw_arguments, re.S) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return {"error": "invalid_json", "raw": raw_arguments}

6. 本地模型与Function Calling:一条值得走的路

如果你对数据敏感,想把Function Calling做到完全离线,本地部署的模型也是可以支持的。目前像Qwen、GLM、Llama等主流的开源模型系列,都原生支持Function Calling。本地部署时,工具声明与调用逻辑和API模式完全一样,只是模型推理跑在你自己的机器上。

我实际跑过的经验是:在7B~14B级别的本地模型上,函数描述的写法对结果的影响比在GPT级别模型上更大。因为小模型的指令遵循能力相对弱,它需要你非常明确地告诉它“这种情况下必须调用什么工具”。所以本地模型场景下,我通常会在描述里加“必须”“禁止”这类强约束词汇,甚至直接给出用户问题的示例,帮助模型建立映射。

除了隐私敏感场景,本地模型还有一个好处:你可以看穿模型的全部推理过程。API模式下你看不到token概率分布,只能看到最终输出;本地部署时如果你用支持采样的推理框架,能从token概率分布里看出模型是否真的“倾向于”调用某个工具,对排查问题非常有帮助。

7. 几条压箱底的经验

最后分享几个我自己在Function Calling项目里最容易忘记、但一旦记住就能省很多排查时间的经验。

第一个:像维护接口文档一样维护函数描述。函数描述不是写一次就不动的。随着你的业务变化,用户提问风格变化,函数描述也需要持续迭代。我建议给每个函数的description加上版本号,记录修改时间,方便对比不同版本的效果差异。迭代方法很简单:收集用户真实的提问日志,定期拿一批问题跑一遍,看哪些描述会导致漏调用、错调用,然后针对性修改。

第二个:在开发环境做一次“函数调用回归基准测试”。准备大概20~50条代表性的用户问题,每条标注期望调用的函数和期望参数。每次改完提示词、函数描述或路由逻辑后,跑一遍这组测试,关注“调用命中率”和“参数正确率”两个指标。这个习惯能让你在改了一版描述之后,立刻知道是变好了还是变差了,而不是靠感觉上线。

第三个:把Function Calling和普通对话分层设计。不是所有用户提问都需要走工具调用链路。可以在前面加一层意图分类:大概率需要查数据的问题直接进Function Calling流程,闲聊类问题走普通对话流程。这能省一半的模型推理成本,响应速度也会提高不少。判断方法不复杂,一个轻量模型或规则引擎做前置分类就够了。

第四个:别忘了攻击面问题。你在这个系统里向模型暴露了哪些函数,等于告诉它你有哪些能力,同时也在内部系统开了一个“外部可触达”的入口。工具函数如果涉及删除、修改等操作,最好在函数内部加一层权限校验或二次确认机制,别因为麻烦就跳过。我的原则是:只暴露必要的最小权限函数集,能只读就只读,需要写操作时多留一道审批。

我把这些经验写出来,是因为这些问题几乎每个做Function Calling的人都会遇到一遍,而且很多坑不踩一次根本想不到。希望这篇文章能把你的起步时间压缩一截,让你直接把精力放在真正有价值的业务逻辑上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 3:41:18

M3U8转MP4在线工具:从概念解析到服务端实现全攻略

做视频处理这一行,M3U8 转 MP4 的需求几乎天天有人问。一个朋友发来一条视频链接,点开能播,想在手机里保存,结果找不到任何下载按钮;或者网页上明明在播放,但就是找不到那个“视频文件”,一查链…

作者头像 李华
网站建设 2026/10/21 3:41:12

Unity摄像头实时抠像:基于Shader的色度键控方案

1. 摄像头抠像到底在解决什么问题第一次接触摄像头抠像,是在一个虚拟演播场景的预研里。当时的需求很直接:用普通USB摄像头拍人,把背景实时去掉,合成到另一个场景里。听起来像是直播行业玩烂了的东西,但真动手才发现&a…

作者头像 李华
网站建设 2026/10/21 3:41:04

Fan Control 智能风扇控制完整指南:十分钟画好第一条曲线

Fan Control 智能风扇控制完整指南:十分钟画好第一条曲线 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending…

作者头像 李华
网站建设 2026/10/21 3:39:57

国赛立体仓库PLC备赛:回原点顺序与联锁设计高频考点清单

简介:面向国赛立体仓库赛项的可编程控制器程序,基于博途集成开发环境与西门子一二零零系列控制器,系统涵盖机器人通信、信捷视觉识别定位、三轴伺服驱动等核心控制模块。压缩包共四十五个文件,包括可扩展标记语言配置、工程文件、…

作者头像 李华
网站建设 2026/10/21 3:39:49

Ubuntu上从零搭建Hadoop集群:配置详解与踩坑实录

直接上手在 Ubuntu 上搭 Hadoop 集群,光看官方文档真的容易劝退。版本对不上、Java 路径写错、SSH 免密没配好,随便哪个坑都能卡你一整天。这篇我用自己实际部署的经验,从环境准备、配置文件逐项拆解,到格式化、启动、验证&#x…

作者头像 李华
网站建设 2026/10/21 3:39:45

OpenAPI到MCP桥接器:将Spring Boot REST接口自动转为AI可调用工具

1. 先说说我为什么折腾这个桥接器大约两个月前,我在为一个内部平台做技术预研,团队计划把一个已有二十多个REST接口的业务后端,接进一个基于大模型的问答与操作助手。问题一开始就不在AI能力上,而是AI压根不知道这些接口长什么样&…

作者头像 李华