用實戰(zhàn):讓大模型從對話到動手干活)
Agent 30 課程開發(fā)指南寫到第7課這個節(jié)點我必須放慢速度細講。前面幾課如果是在教模型“聽懂人話”從第7課開始才真正教它“動手干活”。這一步跨過去Agent才從聊天窗口里的復(fù)讀機變成一個能查數(shù)據(jù)、能寫日歷、能操作業(yè)務(wù)系統(tǒng)的執(zhí)行者。這節(jié)課的內(nèi)容適合兩類人一類是跟過前面6課、正在做課程作業(yè)的學(xué)員另一類是已經(jīng)會調(diào)大模型接口但一直沒搞明白工具調(diào)用到底怎么落地的開發(fā)者??赐昴銘?yīng)該能自己給Agent接上第一個外部工具并且知道出了問題時去哪里排查。1. 先看清楚第7課在整個課程里的位置1.1 Agent 30 課程路線回顧我做這套30天課程的初衷很簡單市面上講Agent概念的資料太多能一步步帶著動手的太少。前6課我刻意沒有碰工具調(diào)用因為基礎(chǔ)不牢工具調(diào)用就是空中樓閣。到第7課學(xué)員已經(jīng)掌握了大模型API的基本用法、提示詞寫作、上下文窗口管理、思維鏈拆解、結(jié)構(gòu)化輸出以及一個簡單的記憶模塊。我習(xí)慣把課程路線分成三段第7課剛好是第二段的第一站階段對應(yīng)課程核心目標基礎(chǔ)期第1課 API接入與基本對話能發(fā)起一次模型請求基礎(chǔ)期第2課 提示詞工程能穩(wěn)定拿到想要的回答格式基礎(chǔ)期第3課 上下文窗口管理知道消息歷史怎么組織、截斷基礎(chǔ)期第4課 思維鏈與任務(wù)拆解讓模型學(xué)會把大問題拆成小步驟基礎(chǔ)期第5課 結(jié)構(gòu)化輸出讓模型輸出可解析的JSON基礎(chǔ)期第6課 記憶與狀態(tài)讓Agent能記住之前的對話內(nèi)容工具期第7課 工具調(diào)用讓Agent具備與外部世界交互的能力工具期第8課 檢索增強與知識庫讓Agent調(diào)用大規(guī)模業(yè)務(wù)知識工具期第9課 多Agent協(xié)作讓多個Agent之間分工配合應(yīng)用期第10-30課 實戰(zhàn)項目與部署把學(xué)到的能力放進真實場景從表格能看出來第7課卡在承上啟下的位置。如果跳過這節(jié)課后面第8課要在問答機器人里接知識庫第9課要幾個Agent互相傳消息學(xué)員會完全不知道怎么下手。1.2 沒有工具的Agent本質(zhì)上只是一個格式化聊天機器很多初學(xué)者會把“會用大模型”和“能干活”畫等號這是個很大的誤區(qū)。大模型本身只能做一件事根據(jù)輸入文本預(yù)測下一段文本。你想讓它幫忙發(fā)一封郵件它可以給你寫出郵件正文但真正把郵件發(fā)出去的動作它做不了。你想讓它查詢今天庫存是多少它如果沒接入數(shù)據(jù)源只能憑記憶和概率編一個數(shù)字。工具調(diào)用的價值就是把模型從“嘴巴”變成“嘴巴加手”。模型負責(zé)理解用戶意圖、決定該調(diào)用哪個工具、生成調(diào)用參數(shù)外部程序負責(zé)真正執(zhí)行再把執(zhí)行結(jié)果喂回給模型模型基于真實結(jié)果生成最終回答。這個閉環(huán)一旦跑通Agent能做的事就遠不止聊天了。我見過太多人第一步就做錯了他們試圖在提示詞里寫“當(dāng)你需要查詢時調(diào)用查詢接口”然后等著大模型自己“神奇地”去調(diào)接口。大模型不會主動發(fā)起任何HTTP請求它能做的只是輸出一段符合格式的JSON。真正的調(diào)用動作必須由你寫的代碼完成。第7課要訓(xùn)練的正是把這個閉環(huán)里每一環(huán)都焊死的能力。2. 工具調(diào)用的底層邏輯一次“請求—決策—執(zhí)行—反饋”循環(huán)2.1 模型不是在執(zhí)行函數(shù)而是在生成調(diào)用指令理解這個底層邏輯很重要。很多人一聽到Function Calling就以為模型內(nèi)部能執(zhí)行代碼其實不是。整個過程中模型只做一件它擅長的事根據(jù)對話歷史和工具定義生成一段結(jié)構(gòu)化的文本。這段文本里包含了它“想”調(diào)用的函數(shù)名和參數(shù)。至于這個函數(shù)到底怎么跑、跑完是什么結(jié)果模型一概不知。所以你可以把工具調(diào)用理解成這樣一個場景你給一位只會出主意、不會動手的顧問配了一個秘書。顧問在紙上寫出“請去檔案室把張三的入職材料拿過來”秘書負責(zé)真的跑去檔案室把材料拿回來放在顧問桌上顧問再根據(jù)材料繼續(xù)給出判斷。大模型就是那個顧問你的程序就是秘書工具就是檔案室。這個認知能幫你避開90%的坑。比如當(dāng)模型返回了一個工具調(diào)用但參數(shù)很奇怪你應(yīng)該先檢查工具描述是否足夠清晰而不是去懷疑模型“笨”。模型只是在按它看到的說明書工作說明書寫得不清楚它自然會犯錯。2.2 一次工具調(diào)用拆解成四個階段我在課程里習(xí)慣把這個過程拆成四個階段學(xué)員照著這個理解寫代碼邏輯會順很多。階段一把用戶消息和工具定義一起發(fā)送給模型。工具定義是一份JSON里面說明有哪些函數(shù)、每個函數(shù)接收什么參數(shù)。模型讀到這里就相當(dāng)于看到了“秘書的通訊錄”。階段二模型判斷應(yīng)該調(diào)用工具時不會直接返回最終答案而是在返回消息中帶一個特殊標記標記里有函數(shù)名和參數(shù)列表。這個標記通常叫做tool_call。如果沒有需要調(diào)用工具模型就直接返回普通回答流程結(jié)束。階段三你的代碼收到tool_call自己去執(zhí)行對應(yīng)的函數(shù)。注意這一步是大模型與外部世界唯一的連接點完全掌握在你手里。你可以執(zhí)行數(shù)據(jù)查詢、寫文件、調(diào)接口但也可以決定拒絕執(zhí)行并返回一個“沒有權(quán)限”的結(jié)果。階段四把函數(shù)執(zhí)行結(jié)果作為一條新的消息追加到對話里再次請求模型。模型看到真實結(jié)果后會把它整理成自然語言回復(fù)給用戶。如果結(jié)果不夠模型可能繼續(xù)發(fā)起下一輪工具調(diào)用這時候就形成一個循環(huán)直到它認為信息足夠為止。我在課程里讓學(xué)生把上面四步畫在一張紙上再對照代碼去寫。如果你現(xiàn)在看到這里有點懵問題不大下一章我會帶你把每個階段用代碼走一遍。2.3 工具描述寫得越具體調(diào)用成功率越高工具調(diào)用的成功率很大程度上不取決于模型能力而取決于你寫的函數(shù)描述。同一個函數(shù)描述寫得好不好效果能差出一大截。舉一個反面例子。你定義一個查詢待辦事項的函數(shù)描述卻只寫了“查詢待辦”。模型接到用戶“幫我把周五的任務(wù)列出來”時它不知道該傳星期幾不知道該過濾已完成還是未完成最后只能猜而猜的參數(shù)經(jīng)常是錯的。我一般推薦工具描述按照這個模板來寫這個工具負責(zé)什么事情 在什么場景下適合使用 有哪些需要注意的參數(shù)規(guī)則。以待辦查詢?yōu)槔梢詫懗伞安樵冎付ㄈ掌诘拇k事項列表可按完成狀態(tài)過濾。當(dāng)用戶提到日程、計劃、任務(wù)時優(yōu)先使用”。參數(shù)描述也要寫清楚日期格式是“YYYY-MM-DD”狀態(tài)字段可選的取值是什么。模型是按照描述做選擇的你把描述寫到能直接“照著做”的程度調(diào)用成功率會明顯提升。3. 第7課實操給Agent接上第一個待辦查詢工具3.1 確定課程交付目標與最小閉環(huán)第7課的作業(yè)我安排得比較克制不要求學(xué)生做那種功能花哨的機器人只要求完成一個最小閉環(huán)用戶詢問某天的待辦事項Agent調(diào)用一個外部函數(shù)拿到真實數(shù)據(jù)再基于真實數(shù)據(jù)回答用戶。這個閉環(huán)雖然簡單但包含工具調(diào)用的全部關(guān)鍵環(huán)節(jié)。先準備好運行環(huán)境。我建議直接用Python語言本身不挑SDK市面上主流的大模型服務(wù)商基本都提供工具調(diào)用參數(shù)寫法大同小異。下面示例中的llmsdk是為了避免綁定某個具體服務(wù)商而寫的占位客戶端你實際用時換成自己日常使用的SDK即可結(jié)構(gòu)和字段基本都能對應(yīng)上。import json # 這是你需要跟著SDK的說明替換的客戶端初始化 from llmsdk import LLMClient client LLMClient( provider你的服務(wù)商, model當(dāng)前使用的模型, api_key你的密鑰, )3.2 第一步寫一個真正可執(zhí)行的函數(shù)工具調(diào)用的前提是這個函數(shù)必須真實存在、能跑、有副作用。我們的待辦查詢函數(shù)先從一個固定數(shù)據(jù)源里取數(shù)以后可以替換成數(shù)據(jù)庫或接口。def get_todo_items(date: str, status: str ) - str: # 模擬數(shù)據(jù)真實場景可換成數(shù)據(jù)庫查詢 todo_db { 2025-01-15: [ {id: 1, title: 寫第7課講義, status: done}, {id: 2, title: 完成工具調(diào)用demo, status: pending}, ] } items todo_db.get(date, []) if status: items [item for item in items if item[status] status] if not items: return 當(dāng)天沒有符合條件的待辦事項 lines [f- [{item[id]}] {item[title]} ({item[status]}) for item in items] return \n.join(lines)代碼非常簡單我甚至推薦你在課程演示時故意把模擬數(shù)據(jù)做得少一點這樣模型很快會碰到“查不到數(shù)據(jù)”的情況能順便觀察它如何應(yīng)對空結(jié)果。很多Agent會在拿到空結(jié)果后繼續(xù)編數(shù)據(jù)這是課程里非常好的討論點。3.3 第二步把函數(shù)“翻譯”成模型能讀懂的JSON Schema模型不知道你Python函數(shù)長什么樣它只能讀JSON格式的函數(shù)描述。這里的關(guān)鍵是把參數(shù)結(jié)構(gòu)、類型、枚舉值、是否必填都寫清楚。tools [ { type: function, function: { name: get_todo_items, description: 查詢指定日期的待辦事項列表可按完成狀態(tài)過濾。當(dāng)用戶提到日程、計劃、任務(wù)時優(yōu)先使用。, parameters: { type: object, properties: { date: { type: string, description: 日期格式為 YYYY-MM-DD, }, status: { type: string, enum: [pending, done, ], description: 按狀態(tài)過濾空字符串表示不過濾, }, }, required: [date], additionalProperties: False, }, }, } ]注意這里的additionalProperties我刻意設(shè)成了False。如果不寫這個字段模型有可能自己腦補一個并不存在的參數(shù)比如“負責(zé)人”或“優(yōu)先級”然后你的程序拿不到這個參數(shù)就會報錯。把額外屬性關(guān)掉相當(dāng)于告訴模型你只能按我規(guī)定的字段來別自由發(fā)揮。還要注意枚舉值。狀態(tài)字段我只允許“pending”、“done”和空字符串模型就不會隨機生成一個“進行中”這種模糊值。減少變量就是減少排查成本。3.4 第三步組裝對話并處理模型返回的tool_call接下來是核心循環(huán)。我們的邏輯是把消息和工具定義發(fā)給模型檢查返回結(jié)果是不是工具調(diào)用標記。如果是就執(zhí)行對應(yīng)函數(shù)拿到結(jié)果后追加到對話里然后帶著追加后的消息再次請求模型直到模型不再要求調(diào)用工具。MAX_TOOL_ROUNDS 5 def call_tool(name: str, args: dict) - str: if name get_todo_items: return get_todo_items(**args) return 未知工具 messages [ {role: user, content: 幫我看看今天有什么待辦} ] for _ in range(MAX_TOOL_ROUNDS): resp client.chat.completions.create( model你的模型名, messagesmessages, toolstools, ) msg resp.choices[0].message reason resp.choices[0].finish_reason # 模型不再要求調(diào)用工具直接輸出最終答復(fù) if reason ! tool_calls: print(最終答復(fù):, msg.content) break # 保留模型返回的消息包含tool_call標記 messages.append(msg.model_dump()) # 遍歷所有工具調(diào)用 for call in msg.tool_calls: func_name call.function.name try: args json.loads(call.function.arguments) except json.JSONDecodeError as e: print(參數(shù)解析失敗:, e) continue print(調(diào)用工具:, func_name, 參數(shù):, args) result call_tool(func_name, args) # 工具執(zhí)行結(jié)果以tool角色回填到對話里 messages.append({ role: tool, tool_call_id: call.id, content: result, })這里最容易被初學(xué)者忽略的是那一行操作把模型返回的原始消息完整追加進messages。因為這條消息里包含了tool_call的id和原始參數(shù)是工具結(jié)果回填時對得上號的關(guān)鍵。如果漏掉這一步模型看到工具結(jié)果時會不知道這個結(jié)果回答的是哪個調(diào)用表現(xiàn)就會變得很怪。另一個技巧是輪次上限。我在課程里把MAX_TOOL_ROUNDS設(shè)為5給模型足夠的“多輪工具調(diào)用”空間也避免它陷入調(diào)用工具的循環(huán)。你可以根據(jù)業(yè)務(wù)場景調(diào)整但一定不能不做限制。3.5 第四步觀察一次真實的調(diào)用鏈路如果一切正常你會在控制臺看到類似下面的輸出調(diào)用工具: get_todo_items 參數(shù): {date: 2025-01-15, status: } 最終答復(fù): 2025年1月15日共有2條待辦事項其中一項“寫第7課講義”已經(jīng)完成另一項“完成工具調(diào)用demo”還沒有完成。注意整個過程中模型并沒有自己看到待辦數(shù)據(jù)它是在拿到工具返回的內(nèi)容之后用自然語言組織出來的答案。這就引出一個課堂重點模型會不會篡改結(jié)果如果數(shù)據(jù)源里明明有3條待辦模型卻只總結(jié)了2條怎么辦解決辦法有兩個方向一是要求工具結(jié)果里帶一條總數(shù)字段二是把問題拆細讓模型把結(jié)果原樣羅列而不是自由發(fā)揮。我在第5課結(jié)構(gòu)化輸出里教的JSON模式在這里可以配合使用。4. Agent工具調(diào)用實戰(zhàn)中的高頻問題排查4.1 模型反復(fù)調(diào)用同一個工具死循環(huán)出不來這是學(xué)員問我最多的問題。現(xiàn)象是模型一直在調(diào)用get_todo_items每次都傳回相同參數(shù)最后把請求次數(shù)耗完。原因通常有三個一是工具返回的結(jié)果里沒有模型需要的確認信息它覺得“沒查到再查一次”二是工具結(jié)果格式太亂模型沒看清楚三是上面的輪次限制沒加導(dǎo)致無限循環(huán)。我的排查順序一般是先看工具返回的字符串是不是被截斷了或者是不是返回了空結(jié)果。如果是空結(jié)果要在返回內(nèi)容里寫清楚“未找到請檢查日期或改問其他日期”給模型一個明確的下一步指令。同時無論如何都要加上MAX_TOOL_ROUNDS限制這是兜底。如果模型連續(xù)調(diào)用同一個工具超過3次我會懷疑問題出在工具描述與用戶問題不匹配而不是模型“笨”。4.2 模型生成的參數(shù)不合規(guī)范導(dǎo)致解析失敗參數(shù)解析失敗的現(xiàn)象有兩種一種是模型生成了殘缺的JSON另一種是生成了沒有定義的字段。殘缺JSON大多出現(xiàn)在模型非常不確定用戶意圖的時候。此時我建議通過豐富description來引導(dǎo)比如明確說“用戶只提供了今天就用今天日期”。也可以要求模型在拿不準時先反問用戶而不是強行調(diào)用。至于多出來的字段前面我們把additionalProperties設(shè)成False已經(jīng)能擋住一部分。但總會有模型不遵守的情況所以程序里必須做嚴格的參數(shù)校驗。我習(xí)慣在call_tool里加一層白名單校驗先檢查函數(shù)名是否在允許列表里再檢查傳入的參數(shù)是否和定義匹配不匹配就返回錯誤信息給模型。讓模型看到錯誤后自行修正這是一種很實用的人機協(xié)作方式。4.3 工具結(jié)果太長把上下文窗口撐爆如果你的工具要返回一批明細數(shù)據(jù)動輒幾十上百條將它們?nèi)咳o模型不僅浪費token還會讓模型抓不住重點。我的習(xí)慣是先對工具結(jié)果做摘要只返回關(guān)鍵信息總量和若干個代表樣本。比如待辦查詢?nèi)绻槌隽?00條事項返回字符串可以寫成“共200條其中未完成130條已完成70條。最近截止的是開會、寫周報、修Bug”。這樣模型既有足夠信息回答用戶又不會迷失在細節(jié)里。如果你的場景必須讓模型看到完整數(shù)據(jù)那就需要配合第3課講的上下文窗口管理做分頁或切片處理。4.4 工具權(quán)限與安全邊界一定要提前設(shè)計工具調(diào)用把外部世界交給了Agent權(quán)限設(shè)計必須跑在功能開發(fā)前面。我在課程里給學(xué)生立了幾個規(guī)矩第一Agent能調(diào)用的函數(shù)必須有一個白名單絕不能讓它任意執(zhí)行系統(tǒng)命令或訪問敏感文件。第二寫入、刪除、轉(zhuǎn)賬、發(fā)消息這類有實際影響的動作必須加人工確認環(huán)節(jié)也就是所謂的human-in-the-loop界面里彈一個確認框再執(zhí)行。第三工具返回的數(shù)據(jù)如果包含用戶隱私要提前脫敏不許把完整的手機號、地址交給模型。危險操作安全替代做法允許Agent直接執(zhí)行Shell命令只開放白名單命令并且每次執(zhí)行要記錄日志允許Agent刪除數(shù)據(jù)庫記錄改成“標記刪除”或先進入回收站允許Agent讀取環(huán)境變量中的密鑰禁止工具函數(shù)訪問密鑰密鑰只留在客戶端允許Agent代表用戶發(fā)郵件先生成草稿由用戶手動點擊發(fā)送這些安全邊界講起來不刺激但真實項目里出事故基本都是在這些地方松懈。第7課我希望學(xué)員從第一天就養(yǎng)成“工具最小權(quán)限”的思維習(xí)慣后面做復(fù)雜項目會省掉大量麻煩。5. 從第7課走出去面試官常問的多工具與進階能力5.1 多個工具組合起來才是完整的Agent如果你只給Agent一個工具它能做的事非常有限。真正有價值的Agent通常要同時掌握查詢工具、寫入工具、搜索工具、計算工具甚至呼叫外部API。課程里我常用一個會議安排場景來演示多工具協(xié)同。用戶說“幫我約明天下午3點的會議室順便看看天氣要不要帶傘”。Agent內(nèi)部可能會這樣走先調(diào)用日歷工具查詢明天下午3點是否有空如果空了就調(diào)用會議室預(yù)訂工具去占位再調(diào)用天氣工具查明天降水概率最后把所有結(jié)果整理成一句答復(fù)。這個過程中每個工具執(zhí)行結(jié)果都會作為消息反饋給模型模型再決定下一步調(diào)用哪個工具。我讓學(xué)生特別注意工具調(diào)用的順序有些工具必須先查再寫順序錯了數(shù)據(jù)就臟了。5.2 并行工具調(diào)用與工具數(shù)量控制現(xiàn)在不少主流模型的接口都支持一次返回多個tool_call也就是說模型覺得需要同時查日歷、查天氣、查會議室列表的時候會一次性輸出三個調(diào)用。你的循環(huán)代碼需要遍歷所有tool_call再統(tǒng)一回填結(jié)果。這對性能很好但也意味著你更容易遭遇“工具爆炸”。工具數(shù)量越多模型選擇錯誤的概率就越高。我個人的經(jīng)驗是單個Agent的工具數(shù)量最好控制在10個以內(nèi)超過10個就應(yīng)該考慮拆分成多個Agent每個Agent只負責(zé)一個子域。這正好是第9課“多Agent協(xié)作”要解決的問題。另外不要把功能相似的多個工具同時掛給Agent比如已經(jīng)有了“查詢天氣”就不要再加“查詢明天天氣”合并成一個帶日期參數(shù)的工具就好。工具多而全不如少而精。5.3 課后作業(yè)與進階路線第7課作業(yè)要求每個學(xué)員把自己的待辦查詢工具擴展成“至少一個查詢類工具加一個寫入類工具”寫入類工具必須帶人工確認。做完這個作業(yè)工具調(diào)用閉環(huán)就基本吃透了。學(xué)有余力的人我建議繼續(xù)往下走三件事第一把工具調(diào)用日志記錄下來至少記錄函數(shù)名、參數(shù)、耗時、返回摘要以后排查問題能救命第二嘗試把一次查詢的結(jié)果緩存起來重復(fù)問題直接命中緩存省token也降延遲第三試著做一個HTTP請求工具讓Agent能去查一個真實的外部接口這一步會直接引出第8課的知識庫檢索。我做了這么多Agent項目最大的感受是決定一個Agent上限的往往不是模型本身有多聰明而是你給了它多少高質(zhì)量的手。第7課就是在打磨這些手。工具描述里動詞開頭、參數(shù)名用下劃線、枚舉值寫滿、返回結(jié)果精簡這幾個習(xí)慣我從第一天就要求自己養(yǎng)成現(xiàn)在寫復(fù)雜Agent也依然受用。你把這節(jié)課練熟后面進入知識庫和多Agent協(xié)作時會明顯感覺路好走很多。