Function Calling 最大的骗局:AI 根本不会调用你的函数

发布时间:2026/7/16 21:30:57
Function Calling 最大的骗局:AI 根本不会调用你的函数 这篇文章我们就把这个误会彻底拆开揉碎看看 Function Calling 这台“魔术”背后到底是谁在拉线。01、先破除一个迷信MYTH · 模型到底做没做上一篇文章发出去后我们还有一个问题没有说明“模型是怎么连上12306网站的它是不是自己写了段代码偷偷跑了一下”在这里我特别说明因为“Function Calling”函数调用这个名字本身就带有极强的误导性——听起来好像模型真的伸出一只虚拟的手“啪”地点了一下某个函数把它执行了。但残忍的真相是大模型自始至终一行代码都没有执行过它甚至连你写的那个函数长什么样都不知道它只是很会“猜”猜你接下来大概率想调用哪个工具、传什么参数然后老老实实地把这个“猜测”用一种约定好的格式写出来仅此而已。真正把工具“跑起来”的从头到尾都是你写的那部分 Python 代码。模型只是一个非常擅长猜测和总结的“内容生成器”它连自己刚才“调用”了什么都不记得——它只是生成了一段符合格式的文字。这篇文章我们就把这个误会彻底拆开揉碎看看 Function Calling 这台“魔术”背后到底是谁在拉线。02、总机接线员的比喻ANALOGY · 判断与填单我最喜欢用这个比喻给人解释 Function Calling大模型更像一个坐在总机前的接线员而不是亲自跑腿的快递员。你打电话进公司总机说“我要投诉产品质量问题”接线员不会自己去处理你的投诉他只会判断——“哦这个应该转到售后部门”然后把电话转过去。真正处理你投诉的是售后部门的人接线员只是判断该转给谁这一件事做得特别准。大模型在 Function Calling 里扮演的正是这个“接线员”角色。它的工作只有两件事1.判断用户这句话是不是需要借助外部工具才能回答如果需要该用哪个工具2.填单把需要传给这个工具的参数按照约定好的格式整理出来。至于工具本身怎么运行、会不会报错、跑出来的结果对不对——这些统统不归模型管模型两眼一抹黑它只负责“转接”和“填单”这两步。那“约定好的格式”具体长什么样这就是我们接下来要拆解的核心。03、模型是怎么“猜”出该调用哪个工具的MECHANISM · 联系单与转接单还记得上一篇里我们传给模型的那个 tools 参数吗它长这样Pythontools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气情况, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如上海 } }, required: [city], }, }, } ]这一坨 JSON本质上就是递给接线员的一张“部门联系单”。上面写着“有一个叫 get_weather 的部门它是干查天气这活儿的接电话时得问对方要一个叫 city 的信息。”模型看到用户问“上海明天下雨吗”之后会拿这句话去跟“联系单”上罗列的每个部门比对——“查天气”这个描述和用户的意图很匹配啊那就该转到 get_weather 这个部门。于是模型不会直接生成一句大白话回复而是生成一段结构化的“转接单”大概长这样不同模型的具体返回结构略有差异但思路一致Json{ tool_calls: [ { id: call_8f3a2b, function: { name: get_weather, arguments: {\city\: \上海\} } } ] }看到没有模型输出的依然只是一段文字只不过这段文字被严格规范成了 JSON 格式方便你的代码去解析。模型没有“执行”任何东西它只是很擅长按照给定的格式把“我猜你想调用 get_weather参数是上海”这件事表达出来。这就是 Function Calling 的全部秘密写清楚“菜单”给模型看模型帮你点一道菜剩下炒菜的活全是你自己的代码在干04、手写一个真正的“工具调度中心”HANDS-ON · Tool Registry 实战理解了原理我们这次不满足于只写一个工具了来点更接近真实项目的东西——搭一个能同时管理多个工具的“调度中心”。这次我们用 deepseek-v4-flash 来做示例DeepSeek 的接口是 OpenAI 兼容格式换个 base_url 和模型名就能直接跑。先设计一个工具注册表Tool Registry用装饰器的方式登记工具顺便自动把函数签名转换成模型能读懂的 JSON Schema免得每次手写一遍Pythonimport inspect import json from openai import OpenAI client OpenAI( api_keysk-xxxxxxx, base_urlhttps://api.deepseek.com, ) # ----------------------------- # 第一步搭一个工具注册表用装饰器自动登记工具 # ----------------------------- class ToolRegistry: def __init__(self): self._funcs {} # 存真正的函数本体 self._schemas [] # 存喂给模型看的联系单 def register(self, description: str): 装饰器登记一个工具并自动从函数签名推导参数 schema def decorator(func): sig inspect.signature(func) properties {} required [] for name, param in sig.parameters.items(): # 简化处理默认都当字符串参数 # 实际项目可以根据类型注解自动推导 properties[name] { type: string, description: f{name}参数, } required.append(name) self._funcs[func.__name__] func self._schemas.append( { type: function, function: { name: func.__name__, description: description, parameters: { type: object, properties: properties, required: required, }, }, } ) return func return decorator def get_schemas(self): return self._schemas def call(self, name: str, args: dict): 真正执行某个工具函数 if name not in self._funcs: return f错误不存在名为{name}的工具 try: return self._funcs[name](**args) except Exception as e: # 执行失败也要告诉模型而不是程序崩掉 return f工具执行出错{str(e)} registry ToolRegistry() # ----------------------------- # 第二步登记工具 # ----------------------------- registry.register(查询指定城市的实时天气) def get_weather(city: str) - str: fake_db { 上海: 小雨18-24℃, 北京: 晴20-28℃, } return fake_db.get(city, 暂无该城市天气数据) registry.register(查询指定股票代码的当前价格) def get_stock_price(code: str) - str: fake_db { SPCX: 139.140美元, AAPL: 317.310美元, } return fake_db.get(code, 暂无该股票数据) # ----------------------------- # 第三步Agent 主循环 # ----------------------------- def run_agent(user_input: str): messages [ { role: user, content: user_input, } ] response client.chat.completions.create( modeldeepseek-v4-flash, messagesmessages, toolsregistry.get_schemas(), ) reply response.choices[0].message # 如果模型决定调用工具 if reply.tool_calls: messages.append(reply) for call in reply.tool_calls: # 模型返回的是 JSON 字符串需要解析 try: args json.loads(call.function.arguments) except json.JSONDecodeError: # JSON 不合法时兜底 args {} result registry.call( call.function.name, args, ) messages.append( { role: tool, tool_call_id: call.id, content: str(result), } ) # 第二轮请求让模型结合工具结果回答 second_response client.chat.completions.create( modeldeepseek-v4-flash, messagesmessages, ) return second_response.choices[0].message.content # 不需要调用工具 return reply.content # ----------------------------- # 测试 # ----------------------------- if __name__ __main__: print(run_agent(帮我查下苹果(AAPL)现在多少钱一股))跑起来后模型会先在心里“转接”到 get_stock_price 这个工具参数猜出 code“AAPL”我们的调度中心真正执行完拿到 “317.310美元”再喂回给模型模型才组织出一句“苹果目前股价为317.310美元”这样的自然语言回复给你。对比第一篇的代码这次多了一个“工具注册表”——这是所有正经智能体项目里都会有的东西工具一多你不可能每次都手写一遍 JSON Schema用装饰器自动生成是业界很常见的做法LangChain 这类框架内部本质上也是干这个事。05、三个新手最容易踩的坑PITFALLS · 踩坑实录写到这里必须得跟你唠叨几个我自己踩过的坑不然你上手写第一个真实项目时大概率会懵。坑一JSON参数不一定合法 模型本质是在“预测下一个字”它不是一个严格的 JSON 生成器。偶尔它会漏掉一个引号或者多加一个逗号导致 json.loads() 直接报错崩溃。上面代码里我专门加了 try/except 兜底这不是我小心谨慎而是这个坑我是真摔过——线上跑了两周好好的某一天模型突然抽风输出了个残缺JSON整个服务直接500排查了半天才发现是这里没兜底。永远不要相信模型输出的格式100%规范这是血的教训。坑二参数类型模型不检查 比如你的函数要求传入一个整数 days: int但模型有时候会传个字符串 “three” 过来尤其是用户说“帮我查未来三天”这种口语化表达时。模型只管“猜个参数出来”至于这个参数符不符合你函数的类型要求它不负责。所以工具函数内部或者调度中心的 call 方法里最好做一层参数校验和类型转换别指望模型帮你把关。坑三工具失败别让程序崩溃 新手很容易犯的错是工具内部报错了就直接抛异常让整个程序挂掉。正确的做法是像上面代码里那样把错误信息也当成一条正常的工具返回结果塞回给模型让模型知道“哦这个工具刚才失败了”模型甚至可能会据此调整策略比如换一种方式重试或者老实告诉用户“抱歉查不到”。这种“让模型知道自己失败了”的设计其实是后面第7篇要讲的“自我纠正”能力的雏形。这一篇我们捅破了什么SUMMARY · 写在最后回顾一下今天的核心Function Calling 从来不是模型在“执行代码”而是一场设计精良的“结构化猜词游戏”——你把工具清单递给模型模型猜一个最匹配的工具和参数用 JSON 格式写出来然后你的代码负责真刀真枪地把它跑起来再把真实结果喂回去。理解了这一层你会发现智能体系统所有“智能”调用底层都不过是猜测解析执行反馈的循环但你可能也发现了一个问题如果任务复杂到需要“先查一下天气根据天气结果再决定要不要接着查机票”模型能不能在生成“转接单”之前先把自己的思考过程说出来让整个决策过程更靠谱、更可控这正是下一篇的主角——ReActReasoning Acting范式。我们会让智能体在每次“转接”之前先大大方方地把“我为什么要这么做”说出来你会发现这一个小小的改动能让整个智能体的可靠性和可调试性上一个台阶。下一篇见。我是写代码的中年人长期专注于 AI 算法、智能体、以及 AI 工程化落地。