FastAPI + OpenAI 兼容协议 + DeepSeek 实战:大模型 Function Calling 工具调用全拆解
FastAPI OpenAI 兼容协议 DeepSeek 实战大模型 Function Calling 工具调用全拆解功能概览功能核心内容天气查询工具声明tools、单轮发起、解析tool_calls打印函数名参数不执行学历查询多工具声明、get_xueli外部调用 Redis 缓存、工具结果回灌第二轮合成调用链路总览用户问题 → messages tools 发给模型 → 模型返回 tool_calls函数名 JSON 参数 → 本地执行对应函数天气/学历 → 把 函数结果 以 roletool 追加回 messages → 再次请求模型 → 模型生成最终自然语言回答一、环境准备OpenAI 依赖下载与配置单列工具调用依赖 OpenAI 官方 SDK配置单独拎出来讲不与业务代码混在一起。1.1 依赖下载pipinstallopenai代码顶部都是from openai import OpenAI装好这个包即可。redis与requests是学历查询里缓存和调用外部 API 用到的按需安装pip install redis requests。1.2 密钥配置环境变量raw_keyos.getenv(DASHSCOPE_API_KEY)api_keyraw_key.strip()代码通过DASHSCOPE_API_KEY读取阿里云百炼的 API Key需提前在系统或.env中导出该变量。1.3 base_url 兼容端点clientOpenAI(api_keyapi_key,base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1,)关键是base_url必须带/compatible-mode/v1后缀——百炼把 OpenAI 协议做成了兼容模式漏写这一截会直接 404。二、知识点讲解2.1 什么是 Function Calling模型本身不能直接查天气、查数据库。我们告诉模型你有这些函数可用模型在觉得需要时返回我想调用某个函数、参数是这样真正的函数由我们本地执行再把结果交还给模型整理成话。这就是模型决策 本地执行的混合模式。2.2 tools 工具声明结构每个工具是一个 JSON-Schematype: functionfunction.name函数名function.description给模型看的功能描述function.parameters入参的 JSON Schema。description写得好不好直接决定模型会不会在该调用时调用。2.3 tool_calls 是什么模型认为需要调用工具时返回的消息里message.tool_calls不为None里面是列表每项含function.name函数名、function.argumentsJSON 字符串参数、id该次调用的标识回灌结果时必须带上。2.4 单轮识别 vs 完整闭环单轮拿到tool_calls就结束仅解析函数名/参数闭环执行函数 → 把结果以roletool追加 → 再请求一次模型由模型产出最终回答。少了回灌这步用户永远看不到自然语言结果。三、代码逻辑拆解3.1 客户端初始化与模型选择两个功能的顶部完全一致raw_keyos.getenv(DASHSCOPE_API_KEY)api_keyraw_key.strip()clientOpenAI(api_keyapi_key,base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1,)第 1 行从环境变量取密钥原始值第 2 行.strip()去掉首尾空白避免复制粘贴带换行导致鉴权失败第 3–5 行构造 OpenAI 客户端base_url指向百炼兼容端点。模型名不在客户端里写而是在每次create时指定见 3.3。3.2 第一个功能天气查询3.2.1 工具声明与本地函数tools[{type:function,function:{name:get_current_weather,description:当你想查询指定城市的天气时非常有用。,parameters:{type:object,properties:{location:{type:string,description:城市或县区比如北京市、杭州市、余杭区等。,}},required:[location],},},},]defget_current_weather(arguments):weather_conditions[晴天,多云,雨天]random_weatherrandom.choice(weather_conditions)locationarguments[location]returnf{location}今天是{random_weather}。name函数标识必须与本地真实函数同名模型返回时原样带回description模型的使用说明书决定何时触发parameters.properties.location入参 schemarequired声明该参数必填模型被要求必须产出它get_current_weather是本地实现从参数里取location随机返回一个天气演示用真实场景替换为气象 API。3.2.2 发起请求与单轮解析defget_response(messages):completionclient.chat.completions.create(modeldeepseek-v4-flash-0731,messagesmessages,toolstools,)returncompletion user_message[{role:user,content:你是谁}]messages.extend(user_message)completionget_response(messages)messages.append(completion.choices[0].message)ifcompletion.choices[0].message.tool_callsisNone:print(f无需调用工具直接回复{completion.choices[0].message.content})else:print(需要调用工具:)tool_callscompletion.choices[0].message.tool_callsforiintool_calls:f_namei.function.name f_argi.function.argumentsprint(f调用工具是{f_name}参数{f_arg})modeldeepseek-v4-flash-0731具体模型名写在每次请求里toolstools把工具清单一并提交模型才会返回tool_callsmessages.extend把用户问题追加进对话列表模块级messages []completion.choices[0].message模型原始消息先整体append进messages保证多轮上下文连续if ... tool_calls is None判断是否触发工具——None说明模型直接回答了否则进入工具分支工具分支里i.function.name取函数名、i.function.arguments取参数字符串是字符串不是字典要用json.loads解析注意这一段到这里只print没真正执行函数、也没回灌所以它只是识别演示。3.3 第二个功能学历查询3.3.1 Redis 客户端与外部学历查询工具rRedis(host127.0.0.1,port6379,db11,decode_responsesTrue)defget_xueli(arguments):vcodearguments[vcode]keyfboos:llm:academic_credential_verification:{vcode}redis_vreifr.get(key)ifredis_vreifisNone:API_KEYMY_KEY_le0KRXAsCh6cNphDEURqJCs02jt3x1BASE_URLhttps://www.apimy.cn/api/xxw/bgcxparams{key:API_KEY,vcode:arguments[vcode]}headers{Content-Type:application/json}responserequests.get(BASE_URL,paramsparams,timeout30)response.raise_for_status()dataresponse.json()r.set(key,json.dumps(data,ensure_asciiFalse))returnjson.dumps(data,ensure_asciiFalse)else:returnredis_vreif第 1–4 行Redis 客户端db11与多轮对话的db10区分开decode_responsesTrue让取出的 value 直接是字符串key用学历验证码拼键同一验证码只查一次if redis_vreif is None缓存未命中才真打外部 API命中直接返回省额度requests.get(..., timeout30)timeout必带外部接口抽风时不会把连接挂死r.set(key, json.dumps(data))把结果以 JSON 字符串写回 Redis返回时json.dumps(data)与缓存写入格式保持一致工具结果对模型而言都是字符串即可。3.3.2 多工具声明tools[{type:function,function:{name:get_current_weather,description:当你想查询指定城市的天气时非常有用。,parameters:{type:object,properties:{location:{type:string,description:城市或县区比如北京市、杭州市、余杭区等。,}},required:[location],},},},{type:function,function:{name:get_xueli,description:当你想查询学历或验证学历时非常有用。,parameters:{type:object,properties:{vcode:{type:string,description:学历验证码,}},required:[vcode],},},},]在天气工具之外还声明了get_xueli工具结构一致参数换成了vcode学历验证码required: [vcode]。两个工具并列放在同一个tools列表里模型可自由选其一或都用。3.3.3 完整闭环执行工具 结果回灌user_message[{role:user,content:帮我查下一下学历验证码是:你自己的学历吗}]messages.extend(user_message)completionget_response(messages)messages.append(completion.choices[0].message)ifcompletion.choices[0].message.tool_callsisNone:print(f无需调用工具直接回复{completion.choices[0].message.content})else:print(需要调用工具:)tool_callscompletion.choices[0].message.tool_calls fun{get_current_weather:get_current_weather,get_xueli:get_xueli}foriintool_calls:f_namei.function.name f_argi.function.argumentsprint(f调用工具是{f_name}参数{f_arg})tool_resultfun[f_name](json.loads(f_arg))print(f工具返回结果是{tool_result})# 把工具结果追加消息再次请求模型生成自然文本messages.append({role:tool,tool_call_id:i.id,content:tool_result})final_respget_response(messages)print(模型整理后的文本回答,final_resp.choices[0].message.content)fun {...}函数名到本地函数对象的映射表模型返回函数名后据此分发执行fun[f_name](json.loads(f_arg))用json.loads把参数字符串还原成字典后调用messages.append({role: tool, ...})这是闭环的关键——role必须是tooltool_call_id填模型给的i.idcontent填工具返回值get_response(messages)再次发起此时模型已拿到工具结果会生成最终自然语言回答。源码注释也点明把工具结果追加消息再次请求模型生成自然文本就是回灌 二次请求。