到狀態(tài)機管理)
面試官讓我手寫一個 Tool Use我用了 3 種方案上周面試一家做 AI 應(yīng)用的公司對方讓我手寫一個 Tool Use。面試官還特意強調(diào)“不用真的調(diào)用外部工具把 Tool Use 的調(diào)度邏輯寫出來就行。”我一聽就明白了這是要看我對 LLM Agent 這條鏈路理解到什么程度。整個面試過程比我預(yù)期要順利因為我知道手寫 Tool Use 本質(zhì)上考的不是能不能記住某個框架的 API而是三個核心問題第一你知不知道模型怎么決定調(diào)哪個工具、傳什么參數(shù)第二你拿到工具結(jié)果之后怎么繼續(xù)讓模型生成回復(fù)第三你寫的這套調(diào)度邏輯在真實環(huán)境中能不能穩(wěn)定跑起來。這篇文章把我現(xiàn)場寫的三種方案完整還原出來從最簡單的循環(huán)到帶參數(shù)校驗和狀態(tài)機管理的完整流程順便把面試官現(xiàn)場追問的問題和我的應(yīng)對思路也一并分享。1. 先拆一下 Tool Use 到底是什么不把概念先理清楚后邊手寫代碼一定跑偏。Tool Use 在 LLM 應(yīng)用里指的是讓模型在生成回復(fù)的過程中把“調(diào)用某個外部函數(shù)”作為一個輸出動作。我用一個生活例子說明。你問一個助手“明天上海天氣怎么樣”如果這個助手沒有 Tool Use它只能憑訓(xùn)練數(shù)據(jù)里的知識瞎猜或者說“我無法獲取實時信息”。有 Tool Use 之后模型會先輸出一個結(jié)構(gòu)化的“調(diào)用請求”里面包含工具名和參數(shù)。系統(tǒng)收到這個請求去真實環(huán)境里執(zhí)行比如查天氣接口、查數(shù)據(jù)庫、執(zhí)行一段代碼再把執(zhí)行結(jié)果返回給模型。模型看到結(jié)果后生成最終面向用戶的回復(fù)。整個鏈路前前后后是這樣的模型輸出 Tool Call → 系統(tǒng)執(zhí)行工具 → 結(jié)果返回給模型 → 模型生成最終回復(fù)所以手寫 Tool Use本質(zhì)上是在寫一個循環(huán)多輪調(diào)用、參數(shù)校驗、錯誤處理、終止條件全都要在這個循環(huán)里體現(xiàn)。面試官要看的通常不是你用了多復(fù)雜的框架而是你能不能把這條閉環(huán)寫清楚。在開始寫方案之前有一點必須強調(diào)手寫 Tool Use 時你寫的其實是“框架”不是一個具體業(yè)務(wù)工具。這意味著你需要自己定義內(nèi)存中的函數(shù)列表、定義調(diào)用請求的數(shù)據(jù)結(jié)構(gòu)、定義執(zhí)行與返回的流程??蚣艽畹煤眉佣嗌賯€工具都不亂框架搭得差寫兩個工具你就開始復(fù)制粘貼了。2. 方案 A最簡實現(xiàn)基于 OpenAI 標(biāo)準(zhǔn)調(diào)用格式我先給面試官寫了一個最通用、最容易解釋的方案。這個方案假設(shè)外部系統(tǒng)能拿到模型原始的 tool_calls 輸出并且執(zhí)行環(huán)境支持 OpenAI 格式這也是絕大多數(shù) LLM 應(yīng)用的標(biāo)準(zhǔn)場景。工具定義只需要兩步給每個工具寫一個 name、description、parameters然后實現(xiàn)一個 dispatch 函數(shù)。dispatch 是整個 Tool Use 框架的心臟它根據(jù)模型輸出的工具名找到對應(yīng)函數(shù)并執(zhí)行。一個精簡的 dispatch 長這樣def dispatch(tool_name: str, arguments: dict) - str: if tool_name not in TOOL_REGISTRY: return fError: unknown tool {tool_name} func TOOL_REGISTRY[tool_name][func] try: result func(**arguments) return json.dumps(result, ensure_asciiFalse) except TypeError as e: return fError: invalid arguments for {tool_name}: {e} except Exception as e: return fError: tool {tool_name} execution failed: {e}這里有幾個關(guān)鍵設(shè)計點也是我在現(xiàn)場重點跟面試官解釋的部分。第一TOOL_REGISTRY 用字典維護(hù)工具名是 key函數(shù)是 value。這樣擴(kuò)展新工具時只需要在注冊表里增加一行dispatch 本身不需要改。新增一個工具的成本極低這是 Tool Use 框架最重要的擴(kuò)展性保障。第二func(**arguments) 這種寫法依賴 Python 的參數(shù)展開。arguments 如果多了字段TypeError 會被捕獲并當(dāng)作錯誤信息返回給模型如果少了必填字段同樣會拋 TypeError。這個機制保證模型輸出不合法參數(shù)時不會讓整個進(jìn)程崩潰。第三錯誤信息一定要返回給模型而不是只打印到日志里。很多人寫 Tool Use 會把異常吞掉只 print 到終端模型根本看不到。但模型是靠這些錯誤信息自我糾錯的比如它發(fā)現(xiàn)參數(shù)不對可能會重新生成一次 tool_call。這是 Tool Use 優(yōu)雅失敗的關(guān)鍵。模型循環(huán)的偽代碼我也現(xiàn)場寫了出來messages [{role: user, content: user_query}] for step in range(MAX_STEPS): response client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolsTOOL_LIST ) msg response.choices[0].message if not msg.tool_calls: final_answer msg.content break messages.append(msg) for tc in msg.tool_calls: tool_result dispatch(tc.function.name, json.loads(tc.function.arguments)) messages.append({ role: tool, tool_call_id: tc.id, content: tool_result, }) print(final_answer)MAX_STEPS 為什么必須有因為沒有它模型可能陷入“調(diào)用工具 → 返回結(jié)果 → 再調(diào)用工具”的死循環(huán)。尤其是一個工具的執(zhí)行結(jié)果不理想時模型會產(chǎn)生連續(xù)嘗試。你可以把 MAX_STEPS 理解成一個安全閥一旦達(dá)到設(shè)定步數(shù)就強制終止把最后的內(nèi)容返回給用戶避免無限消耗 token 和時間。這個方案是最小可用實現(xiàn)當(dāng)場就能跑通適合用來給面試官展示你理解“模型輸出結(jié)構(gòu)化調(diào)用請求”這個核心機制。它的局限性在于所有判斷都依賴模型自動決定調(diào)用哪個工具沒有人工干預(yù)或二次確認(rèn)的空間。3. 方案 B動態(tài)注冊 自動參數(shù)校驗應(yīng)對大規(guī)模工具寫完方案 A面試官沒有打斷我但我知道他大概率會追問如果工具數(shù)量變多這個方案還能不能撐住所以我主動遞進(jìn)了第二套方案。方案 A 能跑但存在一個很實際的痛點當(dāng)工具數(shù)量超過 10 個時你還能靠手寫函數(shù)簽名保證參數(shù)不出錯嗎而且模型是概率輸出arguments 里的字段名只要錯一個字母工具就執(zhí)行失敗。方案 B 要解決的就是這兩個問題。先看動態(tài)工具注冊。我們把工具定義做成裝飾器業(yè)務(wù)函數(shù)只需要標(biāo)注“我是工具”系統(tǒng)自動讀取函數(shù)的簽名和 docstring生成給模型看的工具描述。import inspect TOOL_REGISTRY {} TOOL_SCHEMAS [] def tool(name: str, description: str): def decorator(func): sig inspect.signature(func) params { p_name: { type: string, description: p_name, } for p_name in sig.parameters } schema { type: function, function: { name: name, description: description, parameters: { type: object, properties: params, required: list(sig.parameters.keys()), }, }, } TOOL_REGISTRY[name] func TOOL_SCHEMAS.append(schema) return func return decorator tool(get_weather, 獲取指定城市的當(dāng)前天氣) def get_weather(city: str): Retrieve weather for city. return {city: city, weather: sunny, temperature: 26}inspect.signature 是這里最核心的函數(shù)它能在運行時拿到函數(shù)參數(shù)名自動生成 JSON Schema。這樣你寫業(yè)務(wù)函數(shù)時只需要保證類型標(biāo)注和 docstring 質(zhì)量工具接入成本幾乎為零。但動態(tài)注冊只解決“生成 schema”的問題沒有解決“參數(shù)校驗”的問題。實際上模型輸出的 arguments 可能會多出了不需要的字段缺少必填字段字段類型錯誤比如 city 傳成了數(shù)組字段值非法比如溫度傳了 -999可靠的做法是引入 pydantic。pydantic 能從 schema 直接生成模型類用它做反序列化和校驗錯誤信息非常結(jié)構(gòu)化。from pydantic import create_model, ValidationError def safe_execute(tool_name: str, arguments: dict): func TOOL_REGISTRY[tool_name] schema find_schema(tool_name) Model create_model(tool_name, **generate_fields_from_schema(schema)) try: validated Model(**arguments) return func(**validated.model_dump()) except ValidationError as e: return fValidation failed: {e.errors()}這一版相比方案 A 的進(jìn)步是工具方不需要寫任何校驗代碼維護(hù)成本從“每個工具內(nèi)部各自防御”變成了“框架統(tǒng)一處理”。對面試官來說這展示了你對生產(chǎn)級代碼的思考錯誤處理不是靠運氣而是靠機制。不過引入 pydantic 也有代價。如果你只需要 3 個工具為了校驗引入一個重型依賴就屬于過度設(shè)計。所以我會把方案 B 定位為“工具數(shù)量超過 10 個、團(tuán)隊協(xié)作開發(fā)的中間態(tài)方案”。它的收益曲線陡峭但前期學(xué)習(xí)成本也高。4. 方案 C用狀態(tài)機管理多輪調(diào)度和人工確認(rèn)寫到第三個方案我開始把 Tool Use 從“函數(shù)調(diào)用”升級到“任務(wù)狀態(tài)機”。這也是面試官最希望聽到的進(jìn)階思路因為前面的方案本質(zhì)上都是“模型說什么我做什么”沒有對任務(wù)本身的生命周期做管理。為什么要狀態(tài)機因為真實場景里Tool Use 不是單次調(diào)用而是多次調(diào)用組合完成任務(wù)。比如一個“幫我訂機票”的任務(wù)模型需要先查日期、查目的地、查價格可能還要多次確認(rèn)乘客信息。如果只是無腦循環(huán)調(diào)用一旦中途用戶改主意、工具返回異常、某個步驟需要人工確認(rèn)整個流程就會失控。狀態(tài)機的核心是有明確的“狀態(tài)”和“遷移條件”。我把它簡化成四個狀態(tài)待執(zhí)行、執(zhí)行中、等待確認(rèn)、已完成。from enum import Enum class ToolUseState(Enum): PENDING pending RUNNING running AWAITING_CONFIRMATION awaiting_confirmation COMPLETED completed class ToolUseSession: def __init__(self, session_id: str, initial_query: str): self.session_id session_id self.query initial_query self.state ToolUseState.PENDING self.tool_history [] self.result None def step(self, model_response): if self.state ToolUseState.PENDING: if model_response.tool_calls: self.state ToolUseState.RUNNING else: self.state ToolUseState.COMPLETED elif self.state ToolUseState.RUNNING: for tc in model_response.tool_calls: self.tool_history.append(tc) result dispatch(tc.function.name, json.loads(tc.function.arguments)) if result.startswith(Error): # 錯誤時回到待執(zhí)行讓模型重試 self.state ToolUseState.PENDING return result self.state ToolUseState.AWAITING_CONFIRMATION elif self.state ToolUseState.AWAITING_CONFIRMATION: if model_response confirmed: self.state ToolUseState.COMPLETED else: self.state ToolUseState.PENDING return self.state注意我在 RUNNING 狀態(tài)里如果工具執(zhí)行出錯會讓狀態(tài)回退到 PENDING而不是直接終止。這模擬了“模型看到錯誤信息后重新選擇工具或調(diào)整參數(shù)”的行為。如果模型連續(xù)多次都失敗再觸發(fā) MAX_STEPS 的兜底。這個方案最大的價值不是代碼本身多復(fù)雜而是它把“工具調(diào)用”和“任務(wù)生命周期”分開考慮。工具只是執(zhí)行者而狀態(tài)機是管理者。當(dāng)任務(wù)需要多步驟、多工具協(xié)作時這種抽象讓代碼結(jié)構(gòu)變得非常清晰。順便說一下狀態(tài)機方案也很容易擴(kuò)展出“權(quán)限確認(rèn)”能力。比如某個工具被標(biāo)記為高權(quán)限執(zhí)行前必須先讓用戶確認(rèn)。在 AWAITING_CONFIRMATION 狀態(tài)下系統(tǒng)暫停執(zhí)行把待確認(rèn)信息發(fā)給用戶用戶點了確認(rèn)之后再繼續(xù)。這在轉(zhuǎn)賬、刪除、發(fā)送消息這類高風(fēng)險操作里非常有用。我在現(xiàn)場跟面試官特意強調(diào)了這個設(shè)計方案 C 的“確認(rèn)狀態(tài)”不是簡單加一個 if而是把人工確認(rèn)變成流程中的一等公民。模型不是單方面決定一切系統(tǒng)可以隨時把控制權(quán)交回給用戶。這一手如果能在面試中講明白基本能證明你不是只會調(diào) API而是對 Agent 的運行時架構(gòu)有體系化思考。5. 三個方案的對比與適用邊界把三個方案放到一起對比結(jié)論就很清晰了。面試官通常會讓你選一個場景來說明哪個方案最合適這時候你的回答要果斷要有判斷力。方案核心思路優(yōu)點缺點適用場景A簡單循環(huán) 注冊表實現(xiàn)最快容易理解錯誤控制弱擴(kuò)展性一般原型驗證、面試演示、工具數(shù)量少B動態(tài)注冊 自動校驗工具接入成本低錯誤管理規(guī)范依賴較重需要學(xué)習(xí) pydantic中大型項目、團(tuán)隊協(xié)作C狀態(tài)機驅(qū)動流程可控支持多輪會話和確認(rèn)機制代碼量最大抽象成本高復(fù)雜 Agent 任務(wù)、生產(chǎn)級系統(tǒng)選擇建議只有一條先跑通再架構(gòu)。不要一上來就寫狀態(tài)機如果你的工具只有 3 個狀態(tài)機的抽象只會浪費時間。但如果你做的是一個需要長期維護(hù)的 Agent 平臺那方案 C 一定是值得投入的方向。這里有一個我的經(jīng)驗判斷面試時寫方案 A 加方案 C 的組合最加分。方案 A 證明你具備快速實現(xiàn)能力方案 C 證明你有架構(gòu)思維。方案 B 更適合放在項目經(jīng)驗里口頭提及因為它解決的實際痛點需要在有一定規(guī)模的代碼庫中才能體現(xiàn)。6. 手寫 Tool Use 時的四個常見坑我這些年調(diào)過不少 Agent 項目也在面試現(xiàn)場看過很多人手寫 Tool Use基本上問題都集中在這四個地方。提前說出來能幫你避免 90% 的返工。第一個坑忽略錯誤信息返回。有些人的 dispatch 把異常吞了只打印到日志模型拿不到任何反饋。結(jié)果就是模型在下一輪繼續(xù)生成同樣的錯誤調(diào)用陷入死循環(huán)。我的經(jīng)驗是錯誤信息必須變成 tool message 回給模型這就是模型自我糾錯的唯一素材。你甚至可以特意在錯誤信息里帶上正確的參數(shù)格式示例模型看到后下一輪大概率能糾正過來。第二個坑不設(shè)置最大輪數(shù)。模型一旦進(jìn)入“不斷調(diào)用工具”的狀態(tài)沒有 MAX_STEPS 兜底整個請求會一直掛著浪費 token 和響應(yīng)時間。這個參數(shù)不能少而且要根據(jù)真實調(diào)用情況調(diào)優(yōu)。比如簡單問答 5 步足夠復(fù)雜任務(wù)可能要到 20 步。我一般會在日志里記錄每個任務(wù)的真實步數(shù)定期分析后再調(diào)整上限避免設(shè)太大浪費資源也避免設(shè)太小截斷正常流程。第三個坑工具注冊表沒有命名空間隔離。當(dāng)多個業(yè)務(wù)方同時往注冊表里加工具很容易出現(xiàn)同名覆蓋。比如兩個團(tuán)隊都定義了 get_user_info后注冊的會把前面的覆蓋掉。這在單人 demo 里不會暴露一上多人協(xié)作就出問題。我在實踐中會給每個業(yè)務(wù)方加前綴比如 weather_get、user_query避免沖突。如果用的是 Python也可以把不同業(yè)務(wù)方的工具放在不同模塊下注冊時帶上模塊名。第四個坑忘記清理歷史消息。Tool Use 循環(huán)里messages 是持續(xù)增長的。如果同一個 session 內(nèi)多次任務(wù)歷史消息不清模型很快會被上下文窗口截斷。一個簡單的做法是每次新一輪任務(wù)開始時重置到初始 system prompt或者只保留最近 N 輪摘要。我之前踩過這個坑用戶在一個對話里連續(xù)問了十幾個問題后模型突然“失憶”排查了半天才發(fā)現(xiàn)是上下文塞滿了。7. 面試官追問的進(jìn)階問題怎么答很多面試官在聽完手寫方案后會繼續(xù)追問幾個更深的問題。這些問題的價值在于考察你的應(yīng)變能力和系統(tǒng)設(shè)計思維。我整理了三個最常見的追問和我的應(yīng)對方式?!叭绻ぞ叻祷亟Y(jié)果特別長上下文塞不下怎么辦”答做結(jié)果摘要或者只返回關(guān)鍵字段??梢栽?dispatch 返回前加一層 summarize 函數(shù)對大結(jié)果做截斷或提煉。極端情況下用檢索方式把結(jié)果落庫并返回一個結(jié)果 ID模型需要時再按 ID 查詢。這種“結(jié)果外置”的思路在長文檔處理里很常見?!澳阍趺幢WC工具調(diào)用的安全性”答三層控制。第一層是參數(shù)校驗防止非法輸入第二層是權(quán)限機制高敏感工具需要用戶確認(rèn)第三層是操作審計所有工具調(diào)用記錄都寫入日志方便回溯。我還會補充一點有些工具必須做“白名單”限制比如發(fā)短信接口只能發(fā)給特定號碼刪除操作必須有 delete 權(quán)限才能注冊為工具?!岸鄠€工具都可以滿足用戶請求時你怎么做路由”答靠模型的函數(shù)選擇能力但你可以通過工具描述的措辭影響選擇。比如把更常用工具的描述寫得更詳細(xì)、更明確模型會更傾向于選擇它。如果要硬路由可以在 dispatch 之前加一層規(guī)則引擎人工指定某些關(guān)鍵詞走特定工具。這個方案適合工具語義高度重疊的場景比如兩個查詢庫存的工具只是數(shù)據(jù)源不同。這三個回答不追求唯一標(biāo)準(zhǔn)答案但要有結(jié)構(gòu)、有層次、有真實場景支撐。面試官要的是看到你思考過這些問題而不是背答案。8. 一點實操心得和總結(jié)我自己踩過的最深的坑是“工具描述寫得太敷衍”。給模型看的 description不是給自己看的注釋它決定了模型能不能在眾多工具中選對。比如 get_weather 的 description 如果是“獲取天氣”模型在用戶說“明天出門要不要帶傘”時可能根本不會調(diào)用它。但如果描述成“獲取指定城市未來幾天的天氣數(shù)據(jù)用于回答出行建議、穿衣建議、是否需要攜帶雨具等問題”模型就能更好地關(guān)聯(lián)用戶意圖。這個細(xì)節(jié)比多寫幾個工具函數(shù)重要得多。另外在真實項目中我還習(xí)慣給每個工具加上 examples 字段在參數(shù) schema 里放一個示例參數(shù)值。這樣模型看到的不只是參數(shù)名還有具體的填法。比如 get_weather 的參數(shù)示例是 {city: 上海}模型的調(diào)用準(zhǔn)確率會明顯提升。這個小技巧是我在項目數(shù)據(jù)對比中發(fā)現(xiàn)的同樣的模型加了 examples 后參數(shù)錯誤率能下降不少。手寫 Tool Use 這件事看起來考的是代碼實際上考的是工程化思維錯誤處理、擴(kuò)展性、流程控制、上下文管理每一項都直接影響生產(chǎn)環(huán)境的穩(wěn)定性。把這三個方案吃透再補上這些避坑經(jīng)驗應(yīng)付面試綽綽有余更重要的是你會在實際項目里寫出真正能上線的 Agent 系統(tǒng)。