拓冰建站拓冰建站
首页 / 资讯中心 / 正文

让大模型长出手脚,自己写SQL查数据库(Function Calling初探)

让大模型长出手脚自己写SQL查数据库Function Calling初探前面五篇文章我们一步一步搭起了时序大模型的调用框架。但是你仔细回想一下整个流程是不是觉得哪里有点别扭别扭在什么地方呢在于我们太像保姆了。我们要自己写代码去连数据库自己写SQL把数据捞出来自己把数据洗成文本再小心翼翼地拼进提示词里喂给大模型。大模型就像个大爷张嘴吃现成的吃完吧唧嘴给个评价就走人了。这种模式在指标少、逻辑固定的时候还能凑合。但如果指标几十个老板随口问一句“查一下三号车间昨天的平均温度和最高湿度”你就得去改代码写两句SQL。这太累了。那么有没有一种办法能让大模型自己去查数据库呢它那么聪明它学过SQL它应该能自己写查询语句啊。今天这篇我们就来硬啃一个大招Function Calling也就是工具调用。我们要让大模型长出手脚自己发号施令去查数据。一、 以前是我们求着模型干活现在让模型自己发号施令1.1 回顾一下之前的累人流程我们先复盘一下以前的流程。老板提了个需求我们要干好几步活。第一步我们要把老板的自然语言翻译成我们的代码逻辑。第二步代码逻辑去连SQLite执行写死的SQL。第三步拿到数据拼成字符串。第四步丢给大模型做分析。在这个过程中大模型只参与了第四步。前三步这种苦活累活全是我们在干。我们成了大模型和数据库之间的搬运工。如果哪天老板问的指标变了或者时间范围变了我们就得去改SQL甚至改提示词。这其实就是一种硬编码。只要需求一变代码就得跟着动。这显然不是长久之计。1.2 Function Calling是个什么神仙逻辑那Function Calling是个啥呢其实说白了就是大模型不再只动嘴了它还能动手。当然它不能直接跑到你的服务器上去敲键盘。它的工作方式是这样的我们先给它一本“工具说明书”告诉它我们现在手头有哪些工具可以用。比如我们告诉它我们有一个叫query_metrics的工具可以查数据库里的监控数据。然后我们跟它说“老板想看昨天三号车间下午两点的CPU数据。”大模型看完工具说明书它一琢磨这事儿我得用query_metrics这个工具啊。于是它就不直接给你回废话了。它会返回一个结构化的JSON给你里面写着“我要调用query_metrics参数是表名cpu_table时间昨天14点”。我们拿到这个JSON之后在代码里去真正地执行那个查数据库的函数。把查出来的数据再原封不动地塞回给大模型。这时候大模型既拿到了老板的问题又拿到了真实的数据库数据它再一总结把人话吐给老板。你看在这个流程里写SQL这种活儿变成谁干了变成大模型干了。它通过生成参数的方式告诉了我们要查什么。我们只需要把工具准备好等它差遣就行了。这就是它长出手脚的意思。二、 想让模型查数据得先给它写个“工具说明书”2.1 说明书里要写些啥大模型是个瞎子它不知道你这台机器上能干什么。所以你必须极其详细地描述你的工具。说明书一般包含这几样东西工具的名字比如query_metrics。工具的作用描述这是最最核心的大模型就是看这段描述来决定要不要用这个工具的。工具需要什么参数比如表名、时间范围。每个参数的类型和描述。这个描述一定要写得大白话一点但是又要精准。比如你写“查询数据”大模型可能不知道啥时候该用。你得写“当用户需要查询时序监控指标的具体数值时使用此工具支持按指标名和时间范围过滤”。这样它才懂。2.2 用JSON Schema把说明书结构化我们要把这个说明书拼成API接口能认识的JSON格式。这就是JSON Schema。我们来看看针对查数据库这个场景工具说明书长什么样tools[{type:function,function:{name:query_metrics,description:查询服务器监控指标的时序数据。当用户需要获取CPU、内存等具体数值或趋势时请调用此工具。,parameters:{type:object,properties:{metric_name:{type:string,description:要查询的指标名称例如CPU, Memory},start_time:{type:string,description:查询的起始时间格式为 YYYY-MM-DD HH:MM:SS},end_time:{type:string,description:查询的结束时间格式为 YYYY-MM-DD HH:MM:SS}},required:[metric_name,start_time,end_time]}}}]你看这段JSON。最外面是个列表说明我们可以注册多个工具。现在里面只有一个。function里面就是具体信息。name是给代码看的。description是给大模型看的。parameters里面定义了三个参数。我们告诉大模型调用这个工具必须得告诉我指标名、开始时间和结束时间。而且我们在required里声明了这三个参数缺一不可。如果大模型只给了指标名没给时间它就不符合规范我们的代码就可以拒绝执行。另外注意看我们特意在description里写了时间的格式是YYYY-MM-DD HH:MM:SS。这很重要。如果不写大模型可能会给出昨天下午两点这种自然语言我们的代码是解析不了的。必须把格式约束死。三、 动手写一个真正能查数据库的工具函数3.1 这个函数是给Python执行的不是给模型执行的说明书写好了接下来我们要把第四篇里写的那个查SQLite的代码稍微封装一下变成一个能接收上面那些参数的函数。大模型只会告诉我们参数真正的活还得我们自己干。importsqlite3defquery_metrics(metric_name,start_time,end_time):# 这就是我们第四篇写的查数据逻辑稍微改改参数connsqlite3.connect(monitor.db)cursorconn.cursor()sql SELECT time, metric_name, value FROM metrics WHERE metric_name ? AND time ? AND time ? ORDER BY time ASC try:cursor.execute(sql,(metric_name,start_time,end_time))resultscursor.fetchall()ifnotresults:return没有查到数据# 把查出来的元组洗成文本方便大模型阅读text_lines[]forrowinresults:linef时间:{row[0]}, 指标:{row[1]}, 数值:{row[2]}%text_lines.append(line)return\n.join(text_lines)exceptExceptionase:returnf查询数据库报错了:{e}finally:conn.close()这个函数接收metric_name、start_time、end_time。连上数据库把结果查出来拼成一段文本返回。为什么我们要把结果拼成文本返回呢因为这个结果等会儿还要塞回给大模型。大模型看不懂元组列表它只认识字符串。所以我们在工具层就把数据洗好给大模型准备好最容易消化的食物。四、 串联整个调用的闭环这一步是今天最烧脑的地方。Function Calling不是一次请求就能搞定的它是一个来回交涉的过程。最少要走三个回合。4.1 第一回合发请求带上工具说明书我们先发第一次请求。这次请求跟以前不一样我们要在payload里把tools带上。importrequestsimportjsondeffirst_round_call(user_question,api_key,tools_list):urlhttps://ai.timecho.com/v1/chat/completionsheaders{Content-Type:application/json,Authorization:fBearer{api_key}}messages[{role:user,content:user_question}]payload{model:timecho-model,messages:messages,tools:tools_list# 把工具说明书塞进去}responserequests.post(url,headersheaders,jsonpayload,timeout30)resultresponse.json()# 把大模型的回复和当前的messages历史一起返回后面还要用returnresult,messages我们拿着老板的问题带上工具说明书去问大模型。这时候大模型会怎么回呢4.2 判断模型的回复是想聊天还是想干活大模型拿到问题它脑子里会盘算这个问题我是直接回答呢还是得用工具如果它觉得不需要用工具它返回的格式跟以前一样choices[0][message][content]里有段文字。如果它觉得需要用工具它就不会回content了。它会回一个叫tool_calls的字段。我们要写代码来判断这事儿# 假设这是刚才返回的resultmessageresult[choices][0][message]# 检查有没有tool_calls字段ifmessage.get(tool_calls):print(大模型决定使用工具)# 它想干活else:print(大模型直接回答了)print(message.get(content))如果它决定用工具tool_calls里面的内容大概长这样{tool_calls:[{id:call_abc123,type:function,function:{name:query_metrics,arguments:{\metric_name\:\CPU\,\start_time\:\2023-10-25 10:00:00\,\end_time\:\2023-10-25 10:04:00\}}}]}你看它生成了我们要的参数name是工具名arguments是我们定义的那些参数的JSON字符串。它终于自己把查询条件给拼出来了。4.3 第二回合执行模型给出的参数把结果塞回去既然大模型发话了我们就得照办。我们把arguments解析出来去调我们写的那个query_metrics函数。# 解析它要调哪个函数tool_callmessage[tool_calls][0]function_nametool_call[function][name]arguments_strtool_call[function][arguments]arguments_dictjson.loads(arguments_str)print(f它要调用的工具是{function_name})print(f它给的参数是{arguments_dict})# 真正执行我们的查表函数iffunction_namequery_metrics:# 用 ** 把字典展开成关键字参数传进去tool_resultquery_metrics(**arguments_dict)print(f工具执行结果\n{tool_result}\n)else:tool_result未知的工具查到数据了但这事没完。我们必须把查到的数据再拿去喂给大模型。否则它不知道结果没法写总结报告。这里有个极其繁琐但是必须遵守的规范我们要把大模型刚才的回复、以及工具执行的结果按照特定的格式拼成历史消息再发一次请求。# 1. 先把大模型刚才的回复说要调工具的那条加到历史里messages.append(message)# 2. 构造一个工具执行结果的消息加到历史里tool_message{role:tool,tool_call_id:tool_call[id],# 必须带上刚才的id让大模型对上号content:tool_result# 把查出来的数据塞进去}messages.append(tool_message)你看messages列表现在有三条记录了。第一条是用户的提问第二条是大模型说要调工具第三条是我们告诉它工具的结果。4.4 第三回合模型根据查出来的数据给出最终的自然语言结论我们带着这三条消息再发一次请求。这次请求就不需要带tools说明书了因为它已经不需要再做决策了它只需要做总结。payload_second{model:timecho-model,messages:messages# 带着完整的历史对话}response_secondrequests.post(url,headersheaders,jsonpayload_second,timeout30)final_resultresponse_second.json()print( 最终的分析报告 )print(final_result[choices][0][message][content])这时候大模型既看到了老板的问题“查一下CPU数据”又看到了工具查出来的真实数据。它就可以胸有成竹地给出一段漂亮的分析报告了。五、 交互图文字看着有点绕我们画个图把这三步走理一理。本地查表函数时序大模型TimechoAI网关用户/代码本地查表函数时序大模型TimechoAI网关用户/代码1. 发请求(用户问题 工具说明书)转发请求2. 我要用query_metrics, 参数是...返回tool_calls指令3. 执行query_metrics(CPU, 10:00, 10:04)4. 返回真实数据文本5. 发请求(历史问题 工具结果)转发带数据的上下文6. 根据数据生成人话总结返回最终content你看这个时序图这就叫闭环。大模型不再是光说不练了。它通过生成参数指挥了我们的代码干活。我们的代码干完活把结果汇报给它。它再出面把事情圆完。这就是现在大模型应用开发最主流的架构模式叫做Agent架构的基础形态。只要理解了这个三步走后面再复杂的什么多工具联动、自主规划都是在这个基础上堆逻辑而已。六、 必踩的坑模型生成的参数格式不对怎么办Function Calling 很强但是它不完美。最让人头疼的一个坑就是大模型有时候会犯迷糊生成的参数格式不对。6.1 幻觉它给的时间格式不带引号或者拼错了比如我们在说明书里写了时间格式必须是YYYY-MM-DD HH:MM:SS。大多数情况下它会很听话地给2023-10-25 10:00:00。但是偶尔如果用户的提问很诡异它可能给个昨天下午出来。一旦它给了这种参数我们的query_metrics函数去查数据库肯定查不到东西啊。SQL里的时间匹配就会失败。怎么防呢这就需要在我们的工具执行代码里加极强的容错逻辑。try:arguments_dictjson.loads(arguments_str)# 可以在这里加正则校验如果时间格式不对直接打回重做# 这里为了简单我们只做异常捕获tool_resultquery_metrics(**arguments_dict)exceptjson.JSONDecodeError:tool_result参数JSON解析失败请检查格式。exceptTypeError:tool_result缺少必要的参数请提供完整的指标名和时间范围。exceptExceptionase:tool_resultf工具执行出错:{e}我们把执行工具的代码用try...except严严实实地包起来。如果它给的参数不对我们的函数不会崩而是会返回一段报错文字。这段报错文字我们照样塞到tool_message里发回给大模型。大模型看到“参数解析失败”它就知道自己干坏事了。它会反思一下换个格式再试一次。这就相当于它自己在做调试。6.2 它瞎编了一个我们不支持的函数名还有一种情况。如果你给它注册了两个工具一个查CPU一个查内存。结果它脑抽了返回了一个function_name: query_disk。我们代码里根本没有这个函数。这时候如果直接调肯定会报AttributeError。所以我们在调用的地方一定要写if...elif...else把函数名过滤一遍。碰见不认识的直接返回“系统不支持此工具”。iffunction_namequery_metrics:tool_resultquery_metrics(**arguments_dict)eliffunction_nameanother_tool:# tool_result another_tool(**arguments_dict)passelse:tool_resultf错误不支持名为{function_name}的工具。不要小看这些防御性代码。在真实的工作里80%的时间都在写这种兜底逻辑。大模型是不稳定的只有我们的代码够硬才能把它的输出给兜住。七、 去官方文档和示例验证可行性7.1 看看文档里关于工具调用的说明Function Calling 这个能力对大模型本身的指令跟随能力要求极高。不是随便哪个模型都能玩转的。所以你在开干之前一定要去翻一翻官方的开发文档https://ai.timecho.com/docs/在文档里搜一下tools或者function_call相关的章节。看看TimechoAI的这套接口是不是完全兼容OpenAI的那种格式。如果是兼容的那我们上面写的那些JSON结构就能直接跑。如果它有自己的私有规范比如字段名不叫tools叫plugins那你得按它的来。不过现在业界的大趋势都是往OpenAI靠拢通常来说大差不差。7.2 去示例页面体验类似效果你也可以去应用示例页面https://ai.timecho.com/realtime 看看。有些官方的示例页面会把工具调用的过程在前端打印出来。比如它会显示“正在调用查询数据库工具…”然后过一会才出结果。如果你在示例页面看到了这种标志那就说明这套逻辑官方已经调通了你放心用就行。你只要照着我们今天写的三步走逻辑把代码串起来绝对没问题。八、 总结与下期预告8.1 今天的逻辑有点绕多跑几遍代码今天这篇信息量非常大。我们从以前单纯的提示词工程跨越到了Agent的领域。大模型从只会动嘴变成了会动手指挥的工具调用者。这个三步走的逻辑发说明书 - 拿参数执行 - 发结果总结确实有点绕。如果你看一遍没看懂别灰心这很正常。这是大模型应用开发里的一道坎。最好的办法就是自己把代码敲出来多打几个print看看每一步返回的JSON到底长什么样。你只要跑通了一次看到大模型自己把参数填进去查数据库的那一瞬间你绝对会感到非常震撼。那种感觉就像是你教出来的傻子突然开窍了一样。8.2 下期我们讲讲如果模型想一次查两个表怎么办今天我们只让它查了一次数据库。但是有些复杂的问题它可能需要查两次。比如老板问“对比一下昨天和前天的CPU峰值”。大模型可能需要先调一次工具查昨天的再调一次工具查前天的。它可能会在tool_calls里一次性返回两个调用请求。那我们的代码怎么处理并行调用呢这又是一个大坑。如果搞不定这套Agent就只能干点简单的活。所以下一期我们就来死磕多工具并行调用的问题。把这个解决了我们的Agent基本就毕业了。我们下篇见。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门