議本質到手寫實現大模型工具調用)
1. 從“笨辦法”開始為什么Function Calling不是你想的那樣如果你最近在折騰大模型應用尤其是想搞點能聯網、能查天氣、能操作數據庫的“智能體”那你大概率被“Function Calling”這個詞刷屏了。各種教程、框架文檔里都在提它好像不會這個你的AI應用就缺了靈魂。但說實話我第一次看到這個詞的時候腦子里也是一團漿糊。它聽起來像是一個具體的函數調用動作又像是一種協(xié)議還像是一種開發(fā)模式。網上的文章要么講得太玄乎要么直接甩給你OpenAI API的JSON格式定義看完之后的感覺是懂了但又沒完全懂。今天我們不搞那些云里霧里的概念堆砌就從最“笨”的辦法開始親手寫一遍。我的核心觀點是Function Calling本質上是一種“約定”是大模型LLM和我們寫的程序你的代碼之間的一種“對話協(xié)議”。它的目的極其單純讓大模型這個“黑盒子”能明確地告訴我們——“嘿人類我現在需要你幫我執(zhí)行某個具體操作這是操作的名字和參數你準備好就告訴我結果。”很多人誤以為Function Calling是大模型“主動”調用了我們的代碼這是一種錯覺。大模型本身只是一段概率預測程序它沒有手不會執(zhí)行curl命令也不會寫數據庫。它唯一能做的就是根據我們的“約定”在對話的某個時刻輸出一段符合特定格式的文本。而我們作為程序的真正控制者需要去“監(jiān)聽”這段文本解析它然后才去真正地執(zhí)行函數最后把執(zhí)行結果再“喂”回給大模型讓它繼續(xù)思考。所以這個“笨辦法”就是我們先忘掉LangChain、Dify、各種Agent框架。我們就用最原始的HTTP請求和字符串處理來模擬一遍這個“約定”是如何從無到有建立起來的。當你親手用幾十行代碼把這個流程跑通你就會發(fā)現那些高級框架只不過是把這個流程包裝得更優(yōu)雅、更自動化而已其核心骨架我們今天就能把它搭出來。2. 拆解核心一次完整的Function Calling對話回合要理解Function Calling我們必須把它放到一次完整的“人-模型-工具”交互回合中去看。這個回合通常包含四個清晰的步驟我把它畫成了一個簡單的循環(huán)用戶提問 - 模型思考并請求工具 - 程序執(zhí)行工具 - 模型整合結果并回復讓我們用一個最經典的例子貫穿始終用戶問“北京今天天氣怎么樣”2.1 第一步我們如何“告訴”模型有哪些工具可用大模型不是全知全能的它不知道你的程序里有個叫get_weather的函數。所以對話開始前我們必須先“注冊”工具。在Function Calling的約定里這通常是通過在發(fā)送給模型的系統(tǒng)提示System Prompt或用戶消息中附帶一個“工具列表”來實現的。這個列表不是一個簡單的函數名數組而是一個結構化的描述。我們來看一個最“笨”的表示方法假設我們用純文本約定你可以使用的工具 1. 函數名get_weather - 描述查詢指定城市的天氣情況。 - 參數city字符串必需表示城市名稱例如“北京”。在實際的OpenAI API中這個列表會被格式化成更機器可讀的JSON Schema。但本質沒變我們是在用自然語言或結構化數據向模型描述一個“能力”的接口規(guī)范。這就像你給一個新同事一份操作手冊告訴他“如果你需要查天氣就告訴我你要調用‘get_weather’手冊并且把城市名告訴我。”2.2 第二步模型如何“請求”調用工具模型收到了用戶的問題“北京今天天氣怎么樣”以及我們提供的工具列表。它開始“思考”即進行概率計算。根據訓練數據中對“天氣”和“北京”的關聯它很可能判斷出需要調用get_weather這個工具。關鍵來了模型不會直接說“幫我執(zhí)行get_weather(‘北京’)”。在Function Calling的約定下它會輸出一段嚴格符合我們約定格式的文本。如果我們約定用JSON它可能會在回復中生成一個特殊的結構塊{ “function_call”: { “name”: “get_weather”, “arguments”: “{ \”city\”: \”北京\” }” } }請注意這里的arguments是一個字符串里面包裹著一個JSON對象。為什么是字符串因為對于模型來說它的一切輸出都是文本流。它只是在按照我們描述的格式生成一段看起來像JSON的文本。它并不“理解”JSON也不“執(zhí)行”JSON解析。這個步驟模型僅僅是在說“根據我們的約定我現在需要get_weather功能相關參數是city北京我把這個請求按格式寫好了請你指我們的程序來處理。”注意這是最容易產生誤解的地方。模型輸出的{“city”: “北京”}只是一個文本片段不是可操作的字典。我們的程序必須主動去解析這段文本將其轉化為真正的數據結構。2.3 第三步我們的程序如何“執(zhí)行”并返回結果我們的程序一直在監(jiān)聽模型的回復。當檢測到回復中包含”function_call”這個關鍵字段時就知道模型在請求調用函數了。這時我們的程序需要解析從arguments字符串中提取出city的值“北京”。分派根據name字段的值“get_weather”在我們的代碼中找到對應的真實函數。執(zhí)行調用真實的get_weather(“北京”)函數。這個函數內部可能去調用一個天氣API比如心知天氣、和風天氣等獲取到真實的天氣數據例如{“temperature”: “22°C”, “condition”: “晴”, “humidity”: “40%”}。格式化將執(zhí)行結果再次按照約定格式準備好以便發(fā)回給模型。通常這個結果也需要被包裝一下。2.4 第四步模型如何“消化”結果并給出最終回復我們將上一步得到的天氣結果連同最初的對話歷史再次發(fā)送給模型。這次發(fā)送的消息很關鍵通常包含一個“角色”為”function”的消息內容就是我們的執(zhí)行結果。例如角色: function 名稱: get_weather 內容: {“temperature”: “22°C”, “condition”: “晴”, “humidity”: “40%”}模型看到這條消息后就明白了“哦我之前請求的get_weather工具已經執(zhí)行完畢結果是北京晴22度。” 于是它就能基于這個真實、具體的數據生成面向用戶的、自然流暢的最終回復“北京今天天氣晴朗氣溫22攝氏度濕度40%是個好天氣。”至此一個完整的Function Calling回合結束。你會發(fā)現模型始終在它的文本世界里工作而“調用函數”這個物理世界的動作是由我們的程序根據約定“代理”執(zhí)行的。這就是為什么它更準確的叫法應該是“工具調用”或“函數調用請求”。3. 拋開框架手寫一個極簡Function Calling流程現在我們不用任何AI框架只用Python標準庫和requests庫來模擬這個流程。我們會創(chuàng)建一個“虛擬大模型”——一個簡單的判斷邏輯來模擬LLM的決策過程。3.1 第一步定義工具與虛擬模型首先我們定義真實的工具函數和工具描述# 真實的工具函數 def get_weather(city: str) - str: 模擬查詢天氣的API調用 # 這里應該是真實的HTTP請求我們模擬數據 weather_data { “北京”: “晴朗25°C”, “上海”: “多云28°C”, “深圳”: “陣雨30°C” } return weather_data.get(city, “未找到該城市天氣信息”) # 工具描述我們的“約定” tools_description “”” 你可以使用的工具 - get_weather(city): 查詢城市天氣。參數city是字符串如‘北京’。 “”” # 一個極其簡陋的“虛擬大模型” def virtual_llm(user_query: str, tools_desc: str) - str: 模擬LLM根據用戶輸入和工具描述決定是否調用工具。 # 簡單的關鍵詞匹配來模擬LLM的“思考” if “天氣” in user_query: # 模擬提取城市實際LLM會用更復雜的方式 city “北京” if “北京” in user_query else “上海” # 簡單演示 # 按照我們約定的格式返回工具調用請求 return f”FUNCTION_CALL: get_weather ARGS: {city}” else: return f”ANSWER: 我不知道如何回答這個問題。”這個virtual_llm函數模擬了核心環(huán)節(jié)它看到了“天氣”關鍵詞結合工具描述決定調用get_weather并以我們自定義的簡單格式FUNCTION_CALL: func_name ARGS: arg輸出了請求。3.2 第二步構建主控循環(huán)接下來我們編寫主程序它負責協(xié)調整個對話流程def main(): print(“系統(tǒng)啟動工具已就緒...”) print(f“工具列表\n{tools_description}\n”) # 模擬用戶輸入 user_query “北京今天天氣怎么樣” print(f“用戶: {user_query}”) # 第一步將用戶問題和工具描述一起交給“模型” llm_response virtual_llm(user_query, tools_description) print(f“虛擬模型回復: {llm_response}”) # 第二步解析模型回復檢查是否是工具調用請求 if llm_response.startswith(“FUNCTION_CALL:”): # 解析出函數名和參數 _, func_info llm_response.split(“FUNCTION_CALL: “) func_name, args_str func_info.split(“ ARGS: “) func_name func_name.strip() args_value args_str.strip() print(f“\n檢測到工具調用請求”) print(f“- 函數名: {func_name}”) print(f“- 參數: {args_value}”) # 第三步根據函數名映射并執(zhí)行真實的函數 if func_name “get_weather”: # 執(zhí)行真正的函數 result get_weather(args_value) print(f“- 執(zhí)行結果: {result}”) # 第四步將結果格式化成“函數響應”的格式準備反饋給模型 func_response f”FUNCTION_RESULT: {result}” print(f“\n將結果反饋給模型: {func_response}”) # 這里本應再次調用virtual_llm傳入原始問題和函數結果讓模型生成最終回答。 # 為了簡化我們直接模擬最終回復。 final_answer f”根據查詢結果{args_value}的天氣是{result}。” print(f“\n模型最終回復用戶: {final_answer}”) else: print(f“錯誤未知的函數 {func_name}”) else: # 如果不是工具調用直接輸出模型回復 print(f“模型直接回復: {llm_response.replace(‘ANSWER: ‘, ”)}”) if __name__ “__main__”: main()運行這段代碼你會看到如下輸出系統(tǒng)啟動工具已就緒... 工具列表 你可以使用的工具 - get_weather(city): 查詢城市天氣。參數city是字符串如‘北京’。 用戶: 北京今天天氣怎么樣 虛擬模型回復: FUNCTION_CALL: get_weather ARGS: 北京 檢測到工具調用請求 - 函數名: get_weather - 參數: 北京 - 執(zhí)行結果: 晴朗25°C 將結果反饋給模型: FUNCTION_RESULT: 晴朗25°C 模型最終回復用戶: 根據查詢結果北京的天氣是晴朗25°C。3.3 第三步理解這個“笨辦法”的價值雖然這個例子簡陋到可笑但它完整還原了Function Calling的核心骨架約定我們和“模型”約定了工具描述格式和調用請求格式FUNCTION_CALL:。請求“模型”按約定格式輸出請求文本。解析與執(zhí)行我們的主程序主動解析這段文本并主動調用對應的真實函數。回調將執(zhí)行結果按約定格式返回完成閉環(huán)。OpenAI的Function Calling、Google的Tool Calling乃至所有AI Agent框架如LangChain的Tools、Dify的Workflow都是在將這個流程標準化、自動化、魯棒化。它們定義了更嚴謹的JSON Schema來描述工具提供了自動解析和分派調用的機制并管理復雜的多輪對話狀態(tài)。但萬變不離其宗底層模式就是我們剛才手寫的這個循環(huán)。4. 邁向實戰(zhàn)對接真實大模型API理解了本質我們再來看看如何與真實的OpenAI API協(xié)作。這里的關鍵是使用正確的消息格式。4.1 定義符合OpenAI格式的工具OpenAI要求工具定義是一個JSON Schema列表。我們升級之前的工具描述tools_for_openai [ { “type”: “function”, “function”: { “name”: “get_weather”, “description”: “Get the current weather in a given location”, “parameters”: { “type”: “object”, “properties”: { “l(fā)ocation”: { “type”: “string”, “description”: “The city and state, e.g. San Francisco, CA”, }, “unit”: {“type”: “string”, “enum”: [“celsius”, “fahrenheit”]}, }, “required”: [“l(fā)ocation”], }, }, } ]這個結構比我們的純文本描述復雜得多但它提供了機器可讀的嚴格規(guī)范包括參數類型、是否必需、枚舉值等這讓模型能更準確地生成參數。4.2 發(fā)起包含工具定義的對話請求當我們向ChatGPT API發(fā)送請求時需要將tools參數傳入import openai response openai.chat.completions.create( model“gpt-3.5-turbo”, messages[ {“role”: “user”, “content”: “What‘s the weather like in Boston?”} ], toolstools_for_openai, # 關鍵告訴模型可用的工具 tool_choice“auto”, # 讓模型自行決定是否調用工具 )4.3 解析模型的工具調用請求模型的回復response.choices[0].message會包含一個tool_calls字段如果它決定調用工具的話。我們需要檢查這個字段message response.choices[0].message if message.tool_calls: # 遍歷所有工具調用請求理論上一次可以請求多個 for tool_call in message.tool_calls: func_name tool_call.function.name # 注意arguments 是字符串需要解析成字典 import json func_args json.loads(tool_call.function.arguments) location func_args.get(“l(fā)ocation”) print(f“模型請求調用函數: {func_name}”) print(f“參數: {func_args}”) # 接下來根據func_name執(zhí)行你的真實函數... # result get_weather(location)4.4 執(zhí)行函數并將結果返回給模型執(zhí)行完函數后我們必須將結果以特定的“function”角色消息追加到對話歷史中并再次請求模型# 假設我們已經獲取了結果 weather_result {“temperature”: “22”, “unit”: “celsius”, “condition”: “sunny”} # 將函數執(zhí)行結果作為一條新消息 messages.append(message) # 先把模型的上一條消息包含tool_calls加入歷史 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, # 必須對應之前的調用ID “content”: json.dumps(weather_result), # 結果需要是JSON字符串 }) # 再次調用API讓模型基于結果生成最終回復 second_response openai.chat.completions.create( model“gpt-3.5-turbo”, messagesmessages, ) final_answer second_response.choices[0].message.content print(f“最終回答: {final_answer}”) # 例如: “It‘s currently sunny and 22°C in Boston.”這個流程和我們手寫的“笨辦法”邏輯完全一致只是消息格式和API調用被標準化了。框架的價值就在于幫你自動化管理messages數組的拼接、tool_call_id的匹配、錯誤處理等瑣碎細節(jié)。5. 深入原理大模型是如何“學會”調用函數的你可能會有疑問大模型是怎么知道在什么時候、以什么格式來請求調用函數的它并沒有被“編程”啊。這背后的核心是指令微調。在模型訓練的最后階段研究人員會使用大量精心構造的“對話-工具調用”配對數據對模型進行微調。這些數據的格式就像我們前面演示的完整回合用戶: 北京天氣 助手: 思考我需要調用get_weather工具。調用{name: get_weather, arguments: {city: 北京}} 系統(tǒng)(模擬函數執(zhí)行): {temp: 22°C} 助手: 北京今天氣溫22攝氏度天氣晴朗。模型通過學習海量這樣的例子逐漸掌握了模式“當用戶問題涉及某個領域如天氣而我自身知識無法給出具體實時數據時我應該輸出那個特定的JSON結構來‘請求幫助’然后在收到一段結構化的數據后再將其組織成自然語言回答。”所以Function Calling能力不是魔法而是通過數據訓練出來的、一種對特定文本模式的條件反射。模型學到的是一種“溝通協(xié)議”而不是真正的代碼執(zhí)行能力。6. 避坑指南Function Calling實戰(zhàn)中的常見問題在實際項目中集成Function Calling你會遇到一些教科書里不提的坑。這里分享幾個我踩過的雷。6.1 參數格式的“幻覺”與校驗大模型在生成arguments的JSON字符串時有時會產生“幻覺”輸出格式錯誤或參數值不合理。例如要求數字它給字符串要求枚舉值它給一個不在列表里的值。避坑策略永遠不要信任模型輸出的參數。必須在你的代碼中進行嚴格的二次校驗。JSON解析容錯用try-except包裹json.loads()準備好處理格式錯誤的JSON。參數類型轉換與驗證即使Schema里定義了type: “integer”模型也可能輸出”twenty two”。你需要做類型轉換和有效性檢查。對于枚舉值檢查輸入是否在允許列表中。設置默認值與回退對于非必需參數如果模型未提供或提供錯誤應使用合理的默認值。def safe_execute_function(func_name, arguments_str): try: args_dict json.loads(arguments_str) except json.JSONDecodeError: return {“error”: “Invalid JSON format from model”} if func_name “get_weather”: location args_dict.get(“l(fā)ocation”) unit args_dict.get(“unit”, “celsius”) # 提供默認值 if not location: return {“error”: “Missing required parameter: location”} if unit not in [“celsius”, “fahrenheit”]: unit “celsius” # 糾正錯誤的枚舉值 # 然后才調用真正的API return call_real_weather_api(location, unit)6.2 工具描述的“藝術”工具的描述description和參數描述至關重要它直接指導模型的行為。描述不清會導致模型不理解何時調用或如何傳參。實操心得描述要具體、場景化不要寫“獲取數據”要寫“查詢用戶最近30天的訂單列表”。好的描述能讓模型準確判斷調用時機。參數描述要明確對于location參數描述成“城市名如‘北京’或‘San Francisco, CA’”比單純寫“地點”要好得多。使用枚舉限制選項如果參數只有幾個固定值一定要用enum列出。這能極大減少模型出錯的概率。6.3 處理模型的“拒絕”與“多輪”調用有時即使用戶問題相關模型也可能不觸發(fā)工具調用而是嘗試直接回答可能答錯。或者一次對話中可能需要連續(xù)調用多個工具。應對方案控制tool_choice參數如果你確定必須調用某個工具可以設置tool_choice{“type”: “function”, “function”: {“name”: “get_weather”}}來強制模型調用。通常設為”auto”即可。設計系統(tǒng)提示詞在系統(tǒng)消息中明確指令如“你是一個天氣助手當用戶詢問天氣時你必須使用get_weather工具來獲取最新信息。”管理對話歷史在多輪工具調用中務必完整、準確地維護messages列表確保每次請求都包含完整的上下文用戶消息、之前的工具調用、工具返回結果。這是Agent框架如LangGraph的核心價值之一。6.4 錯誤處理與用戶體驗工具執(zhí)行可能失敗如網絡超時、API限流。你不能把一個Python異常堆棧直接返回給模型。技巧設計友好的錯誤信息格式返回給模型。例如不要返回{“error”: “HTTP 500”}而是返回{“error”: “Weather service is temporarily unavailable. Please try again later.”}。模型可以基于這個信息生成面向用戶的友好提示如“抱歉天氣服務暫時不可用請稍后再試。”這比直接拋出技術錯誤要優(yōu)雅得多。7. 超越基礎從Function Calling到AI Agent理解了Function Calling你就拿到了構建AI Agent的基石。一個簡單的Agent可以看作是一個永不停歇的循環(huán)While (問題未解決): 1. 將當前狀態(tài)用戶目標、歷史、可用工具送給LLM。 2. LLM決定下一步行動思考、調用工具、或給出最終答案。 3. 如果調用工具則執(zhí)行并更新狀態(tài)。 4. 重復步驟1。Function Calling就是這個循環(huán)中“調用工具”的標準化接口。而更復雜的Agent框架如LangGraph, AutoGen則在此基礎上增加了規(guī)劃拆解復雜目標為子任務。記憶管理長期和短期對話歷史。工具路由從大量工具中自動選擇最合適的一個。多Agent協(xié)作讓多個具備不同技能的Agent共同完成任務。例如一個“旅行規(guī)劃Agent”可能內部流程是先調用“搜索航班”工具再調用“查詢酒店”工具然后調用“計算預算”工具最后調用“生成行程摘要”工具。每一次調用都是通過Function Calling協(xié)議來完成的。所以當你下次看到“AI Agent開發(fā)”時可以把它理解為基于LLM大腦和Function Calling手和腳通過一套調度邏輯Agent框架來協(xié)調完成一系列復雜任務目標的系統(tǒng)。而這一切的起點就是今天我們從最底層搞明白的這次“請求-執(zhí)行-回調”的握手。親手寫一遍那個簡陋的流程勝過讀十篇概念文章。它讓你看清了華麗外衣下的簡單骨架。接下來無論是使用LangChain快速搭建原型還是深入研究如何用Dify Workflow編排復雜的業(yè)務邏輯你都會心中有數知道每一步背后究竟在發(fā)生什么。這就是“笨辦法”的力量——它給你的是理解而不僅僅是使用。