方案與工程實踐)
1. 從一道面試題說起為什么“手寫 Tool Use”成了高頻考點“手寫一個 Tool Use”——這六個字放在三五年前多數(shù)前端或后端崗位的面試里根本不會出現(xiàn)。但從大模型應(yīng)用開發(fā)崗位大量釋放開始它幾乎成了繞不開的一道坎。我最近幫幾個朋友做模擬面試十個人里有七個被問到類似的問題形式各不相同有的讓你白板寫偽代碼有的讓你在共享編輯器里跑通一個最小閉環(huán)還有的干脆扔給你一段殘缺的調(diào)度邏輯讓你補全。先說清楚 Tool Use 到底是什么。你可以把它理解成大模型和外部世界之間的一根“數(shù)據(jù)線”。模型本身只會輸出文本它不知道今天天氣、查不了數(shù)據(jù)庫、發(fā)不了郵件。Tool Use 就是讓模型在生成回復(fù)的過程中主動“喊一聲”——我需要調(diào)用某個函數(shù)參數(shù)是這些——然后由我們的程序去執(zhí)行這個函數(shù)再把結(jié)果塞回給模型讓它繼續(xù)生成最終答案。整個過程像是一個不會用工具的實習(xí)生旁邊坐著一個會跑腿的助理實習(xí)生說“幫我查一下庫存”助理跑一趟回來告訴他結(jié)果他再接著寫報告。面試官考這個考的不是你會不會調(diào)某個現(xiàn)成的 SDK?,F(xiàn)成的東西誰都會抄文檔。他真正想看的是三件事第一你懂不懂模型輸出和函數(shù)調(diào)用之間的協(xié)議長什么樣第二你能不能處理多輪調(diào)用、參數(shù)校驗、異常兜底這些臟活第三你在沒有框架加持的情況下能不能從零搭出一個能跑的調(diào)度循環(huán)。這三件事恰好對應(yīng)了三種不同層次的方案。我前后用過三種寫法來應(yīng)對這類題目從最裸的手動解析到半自動的注冊表調(diào)度再到基于結(jié)構(gòu)化輸出的強約束方案。三種方案沒有絕對的優(yōu)劣關(guān)鍵看面試官想考察什么、你手頭的模型能力如何、以及你對“可控性”和“開發(fā)效率”的取舍。下面我把這三種方案逐一拆開把每一步的意圖、參數(shù)、坑點都講透你照著抄作業(yè)就能在面試里穩(wěn)住。2. 方案一手動解析文本協(xié)議最裸但最能體現(xiàn)理解深度2.1 核心思路與適用場景這個方案的核心就一句話讓模型按約定格式輸出一段特殊文本我們用字符串匹配把它摳出來執(zhí)行完再把結(jié)果拼回去。聽起來很土但它恰恰是最能證明你“懂原理”的寫法。面試官如果問“如果不用任何框架你怎么實現(xiàn) Tool Use”你把這套講清楚基本就穩(wěn)了一半。適用場景很明確模型本身沒有原生函數(shù)調(diào)用能力或者你用的是自部署的開源模型輸出格式完全由提示詞控制。這種方案的可控性極高因為解析邏輯全在你手里出問題也好排查。缺點是模型容易“跑偏”格式稍微不對就得靠正則兜底穩(wěn)定性依賴提示詞質(zhì)量。2.2 協(xié)議設(shè)計怎么讓模型“說人話”又“說機器話”協(xié)議設(shè)計是這個方案的地基。我一般用類似下面這種結(jié)構(gòu)讓模型在需要調(diào)用工具時輸出一個固定標記包裹的 JSONtool_call {name: get_weather, arguments: {city: 杭州}} /tool_call為什么用 XML 風(fēng)格的標簽而不是直接輸出 JSON因為純 JSON 容易和模型正?;貜?fù)里的內(nèi)容混淆加個標簽邊界正則匹配時誤傷概率大幅降低。這個細節(jié)很多人不注意實測下來標簽包裹的解析成功率比裸 JSON 高出一大截。提示詞里要寫清楚三件事可用工具列表、每個工具的參數(shù)說明、以及“只有在確實需要外部信息時才調(diào)用”的約束。工具列表我習(xí)慣用 JSON Schema 的簡化版描述讓模型知道參數(shù)類型和是否必填。這里有個經(jīng)驗參數(shù)描述里一定要寫例子比如city: 城市名稱例如杭州模型對例子的敏感度遠高于對類型聲明的敏感度。2.3 解析與執(zhí)行正則、異常與多輪循環(huán)解析部分我通常寫一個函數(shù)用正則把tool_call.../tool_call之間的內(nèi)容摳出來然后json.loads解析。這里必須包一層 try-except因為模型偶爾會輸出帶注釋的 JSON 或者單引號直接解析會炸。解析失敗時的兜底策略是把原始輸出當作普通回復(fù)返回給用戶同時記錄一條日志而不是直接拋異常中斷對話。執(zhí)行環(huán)節(jié)要注意參數(shù)校驗。模型給的參數(shù)不一定符合預(yù)期比如該傳整數(shù)的地方傳了字符串該傳數(shù)組的地方傳了單個值。我一般寫一個輕量的校驗層按工具定義檢查必填項和類型不通過就構(gòu)造一條錯誤信息塞回給模型讓它自己修正。這個“讓模型自我修正”的機制特別重要面試時主動提出來是加分項。多輪循環(huán)是這套方案的靈魂。完整流程是用戶提問 → 模型輸出可能含 tool_call→ 解析 → 執(zhí)行 → 把執(zhí)行結(jié)果作為一條新消息追加到對話歷史 → 再次調(diào)用模型 → 直到模型輸出不含 tool_call 的最終回復(fù)。循環(huán)要有最大輪數(shù)限制我一般設(shè) 5 輪防止模型陷入死循環(huán)反復(fù)調(diào)用同一個工具。def run_agent(user_input, max_turns5): messages [{role: user, content: user_input}] for _ in range(max_turns): reply call_model(messages) tool_call parse_tool_call(reply) if not tool_call: return reply result execute_tool(tool_call) messages.append({role: assistant, content: reply}) messages.append({role: tool, content: str(result)}) return 達到最大輪數(shù)限制注意把工具執(zhí)行結(jié)果追加為tool角色消息時不同模型的角色命名不一樣有的用function有的用tool有的干脆用user。這個必須對著你實際用的模型文檔來寫錯了模型會“看不懂”結(jié)果。2.4 實操心得與常見坑第一個坑是模型“假裝”調(diào)用工具。它可能輸出一段看起來像 tool_call 的文本但其實是它在解釋這個工具怎么用。解決辦法是在提示詞里強調(diào)“只有真正需要執(zhí)行時才輸出標簽”同時在解析后加一層判斷如果工具名不在注冊列表里就當作普通文本處理。第二個坑是參數(shù)里的中文和特殊字符。正則匹配時如果用了貪婪模式遇到嵌套內(nèi)容會匹配過頭。我一般用非貪婪的.*?配合re.DOTALL確保跨行也能正確匹配。第三個坑是對話歷史膨脹。多輪調(diào)用后消息列表會越來越長token 消耗飆升。我的做法是只保留最近 N 輪的工具調(diào)用記錄更早的用摘要替代。這個優(yōu)化在面試里提一句面試官會覺得你考慮過生產(chǎn)環(huán)境的問題。3. 方案二注冊表加裝飾器把調(diào)度做成可擴展的框架3.1 為什么要從“手動”升級到“注冊表”方案一能跑但每加一個工具就要改解析邏輯和提示詞工具一多就亂。面試官如果追問“你有二十個工具怎么辦”方案一就露怯了。這時候方案二登場用一個注冊表把所有工具集中管理用裝飾器自動收集工具定義調(diào)度器根據(jù)模型返回的工具名去注冊表里找對應(yīng)函數(shù)執(zhí)行。這個方案的核心價值在于“可擴展性”。新增工具只需要寫一個函數(shù)加一個裝飾器提示詞里的工具列表和執(zhí)行時的查找邏輯都自動更新。這在真實項目里是剛需也是面試官想看到的工程思維。3.2 工具注冊表的設(shè)計與實現(xiàn)注冊表本質(zhì)上就是一個字典鍵是工具名值包含函數(shù)引用、參數(shù) schema 和描述。我用裝飾器在函數(shù)定義時自動注冊TOOL_REGISTRY {} def tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] { function: func, description: description, parameters: parameters, } return func return decorator tool( nameget_weather, description查詢指定城市的當前天氣, parameters{ type: object, properties: {city: {type: string, description: 城市名}}, required: [city], }, ) def get_weather(city): return f{city}今天晴氣溫 22 度這樣設(shè)計的好處是工具定義和實現(xiàn)綁在一起不會出現(xiàn)“改了函數(shù)忘了改 schema”的情況。生成提示詞時遍歷注冊表把每個工具的 name、description、parameters 拼成一段文本塞進系統(tǒng)提示。執(zhí)行時用工具名去TOOL_REGISTRY里查查不到就返回錯誤信息。3.3 調(diào)度循環(huán)與參數(shù)綁定調(diào)度循環(huán)和方案一類似區(qū)別在于執(zhí)行環(huán)節(jié)。方案二是根據(jù)工具名動態(tài)查找函數(shù)然后用**arguments把參數(shù)字典展開傳進去。這里要注意參數(shù)名必須和函數(shù)簽名一致否則會報unexpected keyword argument。我一般在校驗層加一步用inspect.signature檢查函數(shù)接受的參數(shù)名和模型給的參數(shù)做比對不匹配就提前攔截。import inspect def execute_tool(tool_call): name tool_call[name] args tool_call[arguments] if name not in TOOL_REGISTRY: return {error: f未知工具: {name}} func TOOL_REGISTRY[name][function] valid_params inspect.signature(func).parameters filtered {k: v for k, v in args.items() if k in valid_params} try: return {result: func(**filtered)} except Exception as e: return {error: str(e)}這段代碼里有個細節(jié)值得說我用filtered過濾掉了模型多給的參數(shù)而不是直接報錯。因為模型有時候會“自作聰明”加一些額外字段直接報錯會讓對話中斷過濾掉反而更穩(wěn)。當然必填參數(shù)缺失還是要報錯的這個交給前面的校驗層。3.4 工具描述的質(zhì)量決定調(diào)用準確率這個方案里工具能不能被正確調(diào)用八成取決于描述寫得好不好。我踩過的坑是兩個工具功能相近描述又都寫得很籠統(tǒng)模型就經(jīng)常調(diào)錯。解決辦法是在描述里明確寫出“什么時候用這個、什么時候不用”甚至把反例寫進去。比如查詢訂單和查詢物流這兩個工具描述里我會寫“查詢訂單狀態(tài)用這個如果用戶問的是包裹到哪了用查詢物流那個。”這種“排他性描述”能顯著降低誤調(diào)用率。面試時如果你能主動提到這一點說明你真的調(diào)過模型不是紙上談兵。提示工具數(shù)量超過十個以后提示詞會變得很長模型對靠后工具的關(guān)注度會下降。我的經(jīng)驗是把最常用的工具放在列表最前面同時在系統(tǒng)提示里用一句話概括“你有以下工具可用”而不是把全部 schema 都塞進去。4. 方案三結(jié)構(gòu)化輸出加原生函數(shù)調(diào)用最穩(wěn)但依賴模型能力4.1 結(jié)構(gòu)化輸出解決了什么根本問題方案一和方案二都有一個共同的軟肋解析靠字符串匹配模型輸出格式一抖解析就可能失敗。方案三換了個思路——不讓模型“自由發(fā)揮”輸出文本而是用模型原生的結(jié)構(gòu)化輸出能力直接返回一個符合 schema 的對象。這樣解析環(huán)節(jié)幾乎不會出錯因為格式由模型側(cè)保證?,F(xiàn)在主流的大模型 API 大多支持某種形式的函數(shù)調(diào)用或結(jié)構(gòu)化輸出。有的是在請求里傳tools參數(shù)模型返回一個專門的tool_calls字段有的是用 JSON mode 強制輸出合法 JSON。不管哪種核心都是把“格式約束”從提示詞層面下沉到模型層面。4.2 原生函數(shù)調(diào)用的請求與響應(yīng)結(jié)構(gòu)以常見的函數(shù)調(diào)用接口為例請求時把工具定義放在tools字段里每個工具用 JSON Schema 描述。模型如果決定調(diào)用工具響應(yīng)里會有一個tool_calls數(shù)組每個元素包含id、function.name和function.arguments。注意arguments通常是一個 JSON 字符串需要再解析一次。response client.chat.completions.create( modelyour-model, messagesmessages, toolstool_schemas, tool_choiceauto, ) msg response.choices[0].message if msg.tool_calls: for call in msg.tool_calls: name call.function.name args json.loads(call.function.arguments) result execute_tool(name, args) messages.append({ role: tool, tool_call_id: call.id, content: str(result), })這里的關(guān)鍵是tool_call_id。多工具并行調(diào)用時模型可能一次返回多個 tool_call每個都有自己的 id。執(zhí)行完把結(jié)果回傳時必須帶上對應(yīng)的 id模型才能把結(jié)果和調(diào)用對應(yīng)起來。這個 id 機制是方案三獨有的面試時能講清楚說明你真的用過原生接口。4.3 并行調(diào)用與結(jié)果回傳的順序問題原生函數(shù)調(diào)用支持一次返回多個工具調(diào)用這是方案一和方案二很難優(yōu)雅實現(xiàn)的。比如用戶問“北京和上海今天天氣怎么樣”模型可以一次返回兩個get_weather調(diào)用。執(zhí)行時你可以串行也可以并行但回傳結(jié)果時必須每個結(jié)果單獨一條消息且都帶上各自的tool_call_id。我踩過的坑是把多個結(jié)果合并成一條消息回傳模型會“懵”因為它期待的是每個 id 對應(yīng)一條結(jié)果。正確做法是每個結(jié)果一條tool角色消息順序無所謂但 id 必須對得上。這個細節(jié)在面試里如果被問到“并行調(diào)用怎么處理”答對了就是亮點。4.4 三種方案的橫向?qū)Ρ扰c選型建議對比維度方案一 手動解析方案二 注冊表調(diào)度方案三 原生函數(shù)調(diào)用實現(xiàn)難度低中低依賴 API解析穩(wěn)定性低中高可擴展性差好好對模型要求無無需支持函數(shù)調(diào)用并行調(diào)用支持需自己實現(xiàn)需自己實現(xiàn)原生支持調(diào)試難度低全在手里中高黑盒適合面試展示原理理解工程能力實戰(zhàn)經(jīng)驗選型建議很直接如果面試官想看你懂不懂底層協(xié)議用方案一如果想看你有沒有工程化思維用方案二如果崗位偏應(yīng)用落地、模型也支持用方案三。最穩(wěn)的回答是三種都講然后說明你在什么場景下會選哪種這比只講一種顯得全面得多。5. 面試現(xiàn)場怎么講把三種方案串成一個故事5.1 從需求出發(fā)而不是從代碼出發(fā)很多人一上來就開始寫代碼面試官還沒搞清楚你要解決什么問題你就已經(jīng)陷進細節(jié)了。我的做法是先花一分鐘把需求講清楚模型需要調(diào)用外部工具我們要解決的是“模型怎么表達調(diào)用意圖”和“我們怎么執(zhí)行并回傳結(jié)果”這兩個問題。然后說“我準備了三種方案從簡單到復(fù)雜分別對應(yīng)不同的約束條件”這樣面試官會覺得你有全局觀。講的時候要有遞進感。先說方案一強調(diào)它最能體現(xiàn)對協(xié)議的理解然后說“但方案一擴展性差所以有了方案二”自然過渡到注冊表最后說“如果模型原生支持函數(shù)調(diào)用方案三最省事也最穩(wěn)”。這個敘事線本身就是一次方案選型的演示比干巴巴地羅列代碼強太多。5.2 主動暴露權(quán)衡而不是只講優(yōu)點面試官最怕聽到“這個方案完美”。每種方案都有代價主動說出來反而加分。比如方案一解析不穩(wěn)定方案二需要維護注冊表方案三依賴模型能力且調(diào)試困難。我會在講每種方案時都帶一句“它的代價是……”然后說“所以我在實際項目里會根據(jù)模型能力和團隊情況來選”。這種“權(quán)衡思維”是資深工程師和初級工程師的分水嶺。初級工程師只會說“我用某某框架”資深工程師會說“我為什么不用某某框架”。面試官考手寫 Tool Use本質(zhì)上考的就是后者。5.3 現(xiàn)場編碼時的節(jié)奏控制如果面試要求現(xiàn)場寫代碼千萬別一上來就寫完整實現(xiàn)。我的節(jié)奏是先寫核心數(shù)據(jù)結(jié)構(gòu)工具定義和注冊表再寫解析函數(shù)再寫調(diào)度循環(huán)最后補異常處理。每寫一段停下來解釋一句“這一步是為了解決什么問題”。這樣即使時間不夠沒寫完面試官也能看出你的思路是完整的。代碼風(fēng)格上變量名要清晰別用a、b、tmp這種。函數(shù)盡量短一個函數(shù)只做一件事。注釋不用多但關(guān)鍵判斷處要寫一句為什么。這些細節(jié)在面試里都是隱形的加分項。6. 生產(chǎn)環(huán)境里那些面試不會問但你必須知道的坑6.1 工具執(zhí)行超時與熔斷面試里寫的調(diào)度循環(huán)通常假設(shè)工具秒回但真實環(huán)境里工具可能超時、可能報錯、可能返回超大結(jié)果。我一般給每個工具執(zhí)行加超時控制超過閾值就返回“工具執(zhí)行超時”讓模型基于這個信息繼續(xù)。同時要有熔斷機制同一個工具連續(xù)失敗三次就暫時把它從可用列表里摘掉避免模型反復(fù)調(diào)用一個壞掉的工具。6.2 結(jié)果截斷與 token 控制工具返回的結(jié)果可能非常長比如查數(shù)據(jù)庫返回幾百行。直接塞回給模型會撐爆上下文。我的做法是給每個工具的結(jié)果設(shè)一個長度上限超過就截斷并加一句“結(jié)果已截斷如需更多請細化查詢條件”。這個處理在面試里提一句面試官會知道你考慮過生產(chǎn)環(huán)境的 token 成本。6.3 安全邊界別讓模型為所欲為工具調(diào)用最大的風(fēng)險是模型調(diào)用了不該調(diào)用的工具或者傳了危險的參數(shù)。比如一個刪除數(shù)據(jù)的工具模型可能因為理解偏差就調(diào)了。我的做法是給工具分級只讀工具可以直接調(diào)寫操作工具需要二次確認危險工具直接不暴露給模型。這個安全分層的思路在任何涉及 Tool Use 的崗位面試里都是加分項。注意永遠不要假設(shè)模型會“理智”地使用工具。提示詞里的約束只是軟約束真正的安全邊界必須寫在代碼里而不是寫在提示詞里。6.4 日志與可觀測性生產(chǎn)環(huán)境里出問題第一件事就是看日志。我一般記錄每次工具調(diào)用的工具名、參數(shù)、執(zhí)行耗時、返回狀態(tài)、結(jié)果摘要。這些日志在排查“模型為什么調(diào)錯了工具”時特別有用。面試時如果被問到“怎么調(diào)試”把這套日志方案講出來比說“打 print”專業(yè)得多。7. 我個人的一點體會三種方案我都在真實項目里用過也都在面試里講過。如果只能選一種深入講我會選方案二因為它最能體現(xiàn)“工程化”的能力既不像方案一那么裸也不像方案三那么依賴外部條件。但真正讓我在面試里拿到 offer 的不是某一種方案的代碼寫得多漂亮而是我能把三種方案的取舍講清楚并且能說出每種方案在什么場景下會出什么問題、我怎么解決。手寫 Tool Use 這件事代碼本身不難難的是背后的判斷力。模型能力在變框架在變但“理解協(xié)議、管理調(diào)度、控制邊界”這三件事不會變。把這三點吃透不管面試官怎么問你都能接得住。最后分享一個小技巧面試前自己動手把三種方案各寫一遍跑通一個最小閉環(huán)哪怕只是調(diào)用一個查天氣的假函數(shù)。跑通過和只看過講出來的感覺完全不一樣。