030 Function Calling让大模型调用外部函数与数据库一次让我熬夜到凌晨三点的调试上个月在给一个智能客服系统做集成需求很简单用户问“帮我查一下订单OD20240315的状态”大模型需要去MySQL里查这条记录然后返回给用户。我天真地以为把SQL查询写成函数注册给GPT-4就完事了。结果呢模型返回了一串JSON里面赫然写着“SELECT * FROM orders WHERE order_id ‘OD20240315’”——它把SQL语句当成了回答内容而不是去执行函数。更离谱的是有一次它居然自己编造了一个订单状态“已发货”而数据库里那条记录明明是“待支付”。这就是典型的Function Calling翻车现场。模型要么不会调用函数要么调用了但无视返回结果要么更糟——它开始“脑补”数据。Function Calling到底在解决什么问题大模型本质上是一个文本生成器它不知道你数据库里有什么不知道当前时间更没法主动去调用API。Function Calling就是给模型装上一套“工具接口”让它知道遇到某些问题时别自己瞎编去调用我给你的函数。但这里有个关键认知——模型不会主动执行任何代码。它只是根据你的函数描述生成一个结构化的调用请求。真正执行函数的是你的应用程序执行完再把结果塞回给模型让它基于真实数据生成回答。函数定义别让模型猜你的参数先看一个我踩过坑的例子。最初我这样定义函数functions[{name:query_order,description:查询订单状态,parameters:{type:object,properties:{order_id:{type:string,description:订单号}}}}]看起来没问题对吧但实际测试时用户说“查一下我的订单”模型直接调用了query_order但order_id参数传了个空字符串。为什么因为description太模糊了模型不知道“订单号”长什么样。后来我改成这样# 这里踩过坑description要写清楚格式和示例别让模型猜order_id:{type:string,description:订单号格式为OD后跟8位数字例如OD20240315}加上示例格式后模型提取参数的准确率从60%飙升到95%。函数定义的description是给模型看的prompt写得越精确模型越不会犯错。参数校验别相信模型传过来的任何值你以为模型会乖乖传正确的参数太天真了。有一次模型把“OD20240315”传成了“OD2024-03-15”因为用户问的是“2024年3月15日的订单”。模型自作主张做了格式转换。所以你的函数入口必须做参数校验defquery_order(order_id:str):# 别这样写直接拿order_id去查数据库# 要这样写importreifnotre.match(r^OD\d{8}$,order_id):return{error:订单号格式不正确请提供OD后跟8位数字的订单号}# 校验通过后再查库这里有个经验校验失败时返回明确的错误信息而不是抛异常。因为异常会导致整个调用链中断而返回错误信息可以让模型重新理解问题甚至主动向用户索要正确的参数。多轮对话中的状态管理Function Calling最容易被忽视的是多轮对话场景。用户说“查一下我的订单”模型调用了query_order但参数里没有user_id。这时候怎么办我的做法是分两步第一轮模型发现缺少参数返回一个“需要补充信息”的标记而不是强行调用函数。# 函数定义里加一个必填参数校验required:[order_id]如果模型检测到参数不全它应该生成一条自然语言回复“请问您的订单号是多少”而不是硬着头皮调用。但这里有个坑有些模型会“假装”调用函数传一个默认值进去。我遇到过模型把order_id传成“unknown”的情况。解决方案是在函数里加一个白名单校验所有不在预期格式内的参数直接拒绝。数据库查询别让模型直接写SQL有些教程教你把“执行任意SQL”注册成函数让模型自己写SQL去查。这简直是灾难。我见过模型生成的SQL里出现“SELECT * FROM users WHERE password ‘xxx’”——它把用户输入的密码直接拼进了查询条件。永远不要让模型直接操作数据库。正确的做法是封装成业务函数# 别这样写让模型生成SQLdefexecute_sql(sql:str):# 危险模型可能生成DROP TABLE# 要这样写封装业务逻辑defget_order_status(order_id:str):# 这里只查订单状态模型无法做其他操作sqlSELECT status FROM orders WHERE order_id %s# 执行查询...每个函数只做一件事权限最小化。查询订单的函数不能修改订单查询用户信息的函数不能看到密码字段。函数返回值的处理模型会选择性失明这是最让我头疼的问题。函数返回了正确的数据但模型在生成回答时要么忽略部分字段要么自己“润色”数据。比如函数返回{order_id:OD20240315,status:待支付,amount:299.00,create_time:2024-03-15 14:30:00}模型生成的回答“您的订单OD20240315已支付成功金额299元。”——它把“待支付”改成了“已支付成功”。解决方案是在系统prompt里加一条硬约束当你收到函数返回结果时必须严格使用返回数据中的字段值不得修改、推断或补充任何信息。如果返回数据中不包含用户询问的信息请如实告知。但即使这样某些模型还是会“创造性发挥”。我的终极方案是在函数返回数据里加一个校验字段。defquery_order(order_id:str):resultdb_query(order_id)# 加一个签名让模型无法篡改result[_signature]hashlib.md5(str(result).encode()).hexdigest()[:8]returnresult然后在系统prompt里要求模型在回答时包含这个签名前端再做校验。虽然麻烦但对付那些“爱编故事”的模型很有效。超时与重试生产环境的必修课Function Calling在生产环境会遇到各种网络问题。模型调用函数后你的服务要去查数据库如果数据库响应慢模型那边可能已经超时了。我遇到过的情况函数执行了5秒但模型等待函数返回的默认超时是10秒。看起来没问题但用户在这5秒内又发了一条消息“还在吗”导致上下文混乱。解决方案是给函数调用加一个明确的超时控制# 函数执行超时设置为8秒给模型留2秒缓冲function_call_timeout8# 秒如果函数执行超时不要返回空结果而是返回一个明确的超时标记return{status:timeout,message:查询超时请稍后重试}这样模型至少知道发生了什么而不是傻等或者自己编一个结果。个人经验三个必须遵守的铁律做了半年Function Calling集成踩了无数坑总结三条最实用的经验第一永远假设模型会传错参数。每个函数入口都要做参数校验校验失败返回友好错误让模型有机会纠正。不要相信模型会“理解”你的意图。第二函数返回数据要“喂到嘴边”。不要返回ID让模型自己去关联直接把所有需要的信息都返回。比如查询订单时顺便把用户姓名、商品名称都查出来一起返回。模型处理结构化数据的能力远不如直接给它文本。第三日志里记录每一次函数调用。包括模型传入了什么参数、函数返回了什么结果、模型最终生成了什么回答。这是排查问题的唯一手段。我见过太多人出了问题只能靠猜就是因为没有日志。最后说一句Function Calling不是银弹。它解决的是“模型如何获取外部数据”的问题但解决不了“模型如何正确使用数据”的问题。后者需要你在prompt工程、参数校验、结果验证上持续下功夫。别指望注册几个函数就能让模型变成你的业务专家——它只是个会调用工具的话痨你得教会它什么时候该用工具什么时候该闭嘴。