調(diào)用:Agent從“會(huì)聊天”到“能辦事”的核心實(shí)戰(zhàn))
1. 函數(shù)調(diào)用Agent從“會(huì)聊天”到“能辦事”的關(guān)鍵一躍做Agent開發(fā)這段時(shí)間我最大的一個(gè)感受是很多人把Agent等同于“一個(gè)接了大模型API的聊天機(jī)器人”但真正讓Agent變得有用的恰恰是函數(shù)調(diào)用Function Calling這一環(huán)。你可以讓模型寫詩、寫代碼、做翻譯但只要它不能去查天氣、查數(shù)據(jù)庫、發(fā)消息、操作文件它就始終是個(gè)“紙上談兵”的顧問而不是一個(gè)能幫用戶把事辦成的助手。函數(shù)調(diào)用的本質(zhì)是讓大模型在生成文本的基礎(chǔ)上額外輸出一個(gè)結(jié)構(gòu)化的“調(diào)用意圖”——包括要調(diào)用哪個(gè)函數(shù)、傳入什么參數(shù)——然后由我們的代碼去真正執(zhí)行這個(gè)函數(shù)再把執(zhí)行結(jié)果返回給模型模型基于結(jié)果繼續(xù)回答或進(jìn)行下一步操作。這個(gè)機(jī)制解決的核心問題就是大模型本身不具備訪問外部世界的能力但通過函數(shù)調(diào)用我們可以把外部能力“接入”模型的推理循環(huán)讓模型成為指揮中樞而不是執(zhí)行終端。這篇文章適合誰看如果你正在做Agent開發(fā)或者剛接觸AI Agent、打算在自己的項(xiàng)目里給模型加上工具能力那么這份總結(jié)值得你完整讀一遍。我會(huì)從函數(shù)調(diào)用的設(shè)計(jì)思路、核心實(shí)現(xiàn)細(xì)節(jié)、完整的實(shí)操代碼到我在實(shí)際項(xiàng)目中踩過的坑和排查經(jīng)驗(yàn)一次性講清楚。文章里的代碼和方案都是我在真實(shí)項(xiàng)目里驗(yàn)證過的你幾乎可以照著抄。2. 三種主流函數(shù)調(diào)用方式對(duì)比別一上來就選錯(cuò)路子在開始寫代碼之前你需要先搞清楚一個(gè)事情函數(shù)調(diào)用并不是只有一種實(shí)現(xiàn)方式。市面上常見的方案有三大類每一類的適用場(chǎng)景和坑都不一樣。2.1 API原生函數(shù)調(diào)用最省心但綁定平臺(tái)第一種是模型服務(wù)商在API層面直接支持的原生函數(shù)調(diào)用。典型代表就是OpenAI的functions/tools參數(shù)、Anthropic的tool_use以及國內(nèi)許多廠商現(xiàn)在跟進(jìn)支持的類似接口。你只需要在請(qǐng)求里把函數(shù)的定義包括函數(shù)名、參數(shù)描述、類型以JSON Schema的形式傳進(jìn)去模型在需要調(diào)用時(shí)就會(huì)在返回內(nèi)容里多出一個(gè)tool_calls字段里面是結(jié)構(gòu)化的調(diào)用請(qǐng)求。這個(gè)方案最大的優(yōu)點(diǎn)是不需要你費(fèi)勁去“教”模型怎么輸出JSON——模型本身已經(jīng)針對(duì)這個(gè)能力做過專門訓(xùn)練輸出格式穩(wěn)定解析也容易。缺點(diǎn)也很明顯你被綁定在某一家廠商的接口風(fēng)格上。換一個(gè)模型廠商或者用一些自部署的開源模型這個(gè)能力可能就不存在或者格式不兼容。不過如果你用的就是主流閉源模型API這依然是最值得推薦的第一選擇。2.2 提示詞式調(diào)用兼容性強(qiáng)但需要“調(diào)教”第二種方式是純提示詞方案。你不依賴任何API的原生能力而是在系統(tǒng)提示詞里跟模型約定“當(dāng)需要查詢天氣時(shí)請(qǐng)輸出一個(gè)JSON格式為{tool: get_weather, params: {...}}”。然后你的代碼去解析模型輸出的文本匹配到對(duì)應(yīng)的工具并執(zhí)行。這個(gè)方案的核心優(yōu)勢(shì)是兼容性極強(qiáng)——任何能聊天的模型都能用包括那些沒開放函數(shù)調(diào)用能力的開源模型。代價(jià)則是它的穩(wěn)定性完全依賴提示詞寫得夠不夠清楚以及模型本身的理解能力。模型可能今天老老實(shí)實(shí)輸出JSON明天換個(gè)措辭就加了點(diǎn)解釋性文字你的解析器就得跟著修。所以用這個(gè)方案可以再配合下一條要說的“結(jié)構(gòu)化輸出”。2.3 結(jié)構(gòu)化輸出與手動(dòng)解析夾縫中的折中方案第三種方案介于前兩者之間利用模型API提供的JSON Mode或結(jié)構(gòu)化輸出能力強(qiáng)制模型返回合法JSON但JSON的字段含義由我們自己定義再由我們手動(dòng)解析并分發(fā)到對(duì)應(yīng)的函數(shù)。這個(gè)方案的優(yōu)點(diǎn)在于既獲得了相對(duì)穩(wěn)定的輸出格式又保留了對(duì)“調(diào)用協(xié)議”的完全控制權(quán)——比如你可以在JSON里加入業(yè)務(wù)流水號(hào)、加入多個(gè)工具的同時(shí)調(diào)用請(qǐng)求這些在原生功能里往往受限。缺點(diǎn)則是你需要自己寫更多的解析和校驗(yàn)代碼而且JSON Mode只能保證格式正確不能保證字段內(nèi)容一定合理。為了讓你更直觀地做選擇我把三種方式的比較整理成一個(gè)表格對(duì)比維度API原生函數(shù)調(diào)用提示詞式調(diào)用結(jié)構(gòu)化輸出手動(dòng)解析輸出穩(wěn)定性高低依賴模型能力中高實(shí)現(xiàn)復(fù)雜度低低但調(diào)提示詞耗時(shí)中跨模型遷移性差好中多工具并行調(diào)用多數(shù)已支持看約定格式完全可控建議適用場(chǎng)景生產(chǎn)環(huán)境、主流API快速原型、開源模型需要高度自定義協(xié)議的場(chǎng)景我個(gè)人在生產(chǎn)環(huán)境里的經(jīng)驗(yàn)是能用API原生函數(shù)調(diào)用就用原生的這是性價(jià)比最高的路徑只有當(dāng)模型沒有原生支持、或者你需要一個(gè)跨廠商的統(tǒng)一Agent底層時(shí)才去走后兩條路。3. 核心細(xì)節(jié)拆解一個(gè)高質(zhì)量函數(shù)調(diào)用方案需要摳哪些細(xì)節(jié)很多人寫函數(shù)調(diào)用就直接把函數(shù)定義往參數(shù)里一塞跑通了就算完事。但真正到了Agent項(xiàng)目里調(diào)用成功率、參數(shù)準(zhǔn)確性、異?;謴?fù)能力這些細(xì)節(jié)才是決定一個(gè)Agent“好用”還是“雞肋”的分水嶺。這部分我拆成三個(gè)小節(jié)逐一講透。3.1 工具定義你的函數(shù)簽名寫得越清楚模型就越不容易犯錯(cuò)函數(shù)調(diào)用的第一步是把你代碼里的函數(shù)“翻譯”成模型能理解的語言。以O(shè)penAI的tools參數(shù)為例一個(gè)函數(shù)定義長(zhǎng)這樣tools [ { type: function, function: { name: get_weather, description: 查詢指定城市的實(shí)時(shí)天氣情況包括溫度、天氣狀況和風(fēng)力。當(dāng)用戶詢問天氣、氣溫、是否會(huì)下雨時(shí)使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名稱例如北京、上海、廣州。必須是中文城市名。 }, unit: { type: string, enum: [celsius, fahrenheit], description: 溫度單位默認(rèn)使用攝氏度。, default: celsius } }, required: [city] } } } ]這里有幾個(gè)細(xì)節(jié)是很多教程不會(huì)強(qiáng)調(diào)的。第一個(gè)是描述要“精確到使用時(shí)機(jī)”。description不只是給模型介紹這個(gè)函數(shù)是干嘛的更重要的是告訴模型“什么時(shí)候該用它”。我見過很多人寫“查詢天氣”四個(gè)字就完了結(jié)果模型在用戶說“今天適合穿什么”的時(shí)候完全沒想過可以調(diào)天氣接口。加上“當(dāng)用戶詢問天氣、氣溫、是否會(huì)下雨時(shí)使用”這種觸發(fā)條件描述后調(diào)用率立刻上了一個(gè)臺(tái)階。第二個(gè)細(xì)節(jié)是參數(shù)描述里要寫取值范圍、格式約定、甚至默認(rèn)行為。比如city參數(shù)如果你不寫明“必須是中文城市名”模型有可能會(huì)給你輸出“Beijing”而不是“北京”你的查詢接口很可能因此報(bào)錯(cuò)。越細(xì)的約束越能減少下游解析的麻煩。第三個(gè)細(xì)節(jié)是required字段不能偷懶。必填的參數(shù)必須列出來否則模型偶爾會(huì)漏掉你其實(shí)必須要的參數(shù)。我在一個(gè)項(xiàng)目里就是因?yàn)闆]把user_id設(shè)為必填導(dǎo)致后臺(tái)一連串的鑒權(quán)錯(cuò)誤。3.2 參數(shù)解析與校驗(yàn)別信模型輸出的每個(gè)字節(jié)都是金子模型不是數(shù)據(jù)庫它的輸出有一定概率出錯(cuò)。函數(shù)調(diào)用返回的JSON里參數(shù)類型不對(duì)、字段缺失、甚至整個(gè)JSON無法解析都是常見情況。因此在真正執(zhí)行函數(shù)之前你要有一道校驗(yàn)關(guān)卡。我的做法是寫一個(gè)通用的校驗(yàn)器在分發(fā)之前做三層校驗(yàn)第一層是JSON格式校驗(yàn)判斷tool_calls里的arguments字符串能不能正常解析成字典。第二層是Schema校驗(yàn)把解析出來的參數(shù)再用定義時(shí)的那套JSON Schema格式跑一遍確認(rèn)類型合法、必填項(xiàng)都在。第三層是業(yè)務(wù)校驗(yàn)這一步是校驗(yàn)一些Schema管不了的東西比如日期格式是不是合理的、金額是不是大于0、用戶ID是不是存在。前兩層可以用現(xiàn)成的庫比如JSON Schema的Python實(shí)現(xiàn)jsonschema來做第三層就得自己在每個(gè)函數(shù)里寫。很多人覺得這太繁瑣但我實(shí)測(cè)下來加一道校驗(yàn)至少能攔截掉約5%~10%的模型錯(cuò)誤輸出在長(zhǎng)期運(yùn)行的Agent服務(wù)里這個(gè)比例足以避免大量線上事故。注意參數(shù)校驗(yàn)失敗的時(shí)候不要直接拋異常終止整個(gè)對(duì)話循環(huán)。正確的做法是構(gòu)造一條“工具執(zhí)行錯(cuò)誤”的消息返回給模型告訴它“參數(shù)不合法請(qǐng)修改后重試”。模型通常會(huì)自動(dòng)修正Agent的容錯(cuò)能力就是這么一點(diǎn)點(diǎn)建立起來的。3.3 執(zhí)行分發(fā)與結(jié)果回流把函數(shù)返回變成模型能消化的“事實(shí)”校驗(yàn)通過之后就進(jìn)入執(zhí)行分發(fā)環(huán)節(jié)。我推薦用注冊(cè)表模式來管理函數(shù)與執(zhí)行器的映射。注意這里有一個(gè)新手很容易踩的坑**模型傳遞過來的只是函數(shù)名和參數(shù)字典而不是真正的Python函數(shù)對(duì)象。**你不能直接拿字符串去eval更不應(yīng)該用動(dòng)態(tài)import這種危險(xiǎn)操作。正確的做法是維護(hù)一個(gè)“名稱到函數(shù)”的映射字典或者用裝飾器把函數(shù)注冊(cè)進(jìn)一個(gè)全局注冊(cè)表。比如TOOL_REGISTRY {} def register_tool(nameNone): def decorator(func): registry_name name or func.__name__ TOOL_REGISTRY[registry_name] func return func return decorator register_tool() def get_weather(city: str, unit: str celsius): # 實(shí)際去調(diào)用天氣API return {temperature: 18, condition: 多云, city: city}執(zhí)行完函數(shù)得到結(jié)果之后還有一個(gè)關(guān)鍵步驟結(jié)果回流。你要把執(zhí)行結(jié)果組裝成一條tool角色的消息追加到對(duì)話上下文中然后再帶著這條消息去請(qǐng)求模型讓它繼續(xù)決定是給出最終回答還是發(fā)起下一輪函數(shù)調(diào)用。這里有個(gè)小技巧返回給模型的內(nèi)容應(yīng)該是“結(jié)構(gòu)化的摘要”而不是原始API響應(yīng)的整段JSON。比如天氣接口可能返回50個(gè)字段的氣象數(shù)據(jù)但模型做后續(xù)決策只需要其中的“溫度、天氣狀況、風(fēng)力”。所以你在工具函數(shù)里就應(yīng)該把結(jié)果裁剪、濃縮只保留對(duì)后續(xù)推理有意義的字段。4. 實(shí)操全流程從零寫一個(gè)帶函數(shù)調(diào)用的Agent循環(huán)理論講再多不如直接上一份能跑的代碼。這一節(jié)我會(huì)帶你從零寫一個(gè)最小可用的Agent它支持兩個(gè)工具查天氣和給指定郵箱發(fā)提醒郵件。等這個(gè)循環(huán)跑通了你就等于掌握了函數(shù)調(diào)用的全鏈路骨架之后換工具、加工具、上框架都只是往里面添磚加瓦。4.1 環(huán)境準(zhǔn)備與模型選型這個(gè)示例我用的Python 3.10OpenAI的Python SDK模型用gpt-4o-mini。你如果用的是其他兼容OpenAI接口格式的服務(wù)商比如國內(nèi)的一些廠商或自建的網(wǎng)關(guān)代碼邏輯完全不用改只要換掉base_url和api_key就行。pip install openai寫代碼之前先把API Key配置到環(huán)境變量里。我建議你創(chuàng)建項(xiàng)目根目錄下的.env文件然后用python-dotenv加載避免把Key硬編碼進(jìn)代碼。密鑰這東西一旦提交到Git倉庫后面漏出去補(bǔ)救了基本就晚了。4.2 構(gòu)建工具定義與Agent主循環(huán)下面是完整的核心代碼這個(gè)結(jié)構(gòu)我建議你保存下來后面做任何Agent項(xiàng)目都可以在這個(gè)骨架上擴(kuò)展import os import json from openai import OpenAI # 初始化客戶端 client OpenAI( api_keyos.getenv(OPENAI_API_KEY), # base_urlhttps://你的網(wǎng)關(guān)地址, # 如果用了代理網(wǎng)關(guān)放開這行 ) # ---------- 1. 工具定義 ---------- TOOLS [ { type: function, function: { name: get_weather, description: 查詢指定城市的實(shí)時(shí)天氣。當(dāng)用戶問到天氣、氣溫、是否下雨、是否適合出行時(shí)必須調(diào)用此工具。, parameters: { type: object, properties: { city: {type: string, description: 中文城市名例如北京}, }, required: [city] } } }, { type: function, function: { name: send_reminder_email, description: 給指定郵箱發(fā)送一封提醒郵件。當(dāng)用戶要求發(fā)送郵件、提醒事項(xiàng)、通知時(shí)調(diào)用。, parameters: { type: object, properties: { to_email: {type: string, description: 收件人郵箱地址}, subject: {type: string, description: 郵件標(biāo)題}, body: {type: string, description: 郵件正文內(nèi)容} }, required: [to_email, subject, body] } } } ] # ---------- 2. 真實(shí)函數(shù)實(shí)現(xiàn)模擬 ---------- def get_weather(city: str): # 實(shí)際項(xiàng)目里這里換成真實(shí)天氣API調(diào)用 return {city: city, temperature: 16, condition: 多云轉(zhuǎn)晴, humidity: 45} def send_reminder_email(to_email: str, subject: str, body: str): # 實(shí)際項(xiàng)目里這里換成郵件服務(wù)商API print(f[郵件發(fā)送] 收件人{(lán)to_email}, 主題{subject}) return {status: success, message: 郵件已進(jìn)入發(fā)送隊(duì)列} TOOL_REGISTRY { get_weather: get_weather, send_reminder_email: send_reminder_email, } # ---------- 3. 主循環(huán)Agent的核心 ---------- def run_agent(user_input: str, max_steps: int 5): messages [{role: user, content: user_input}] for step in range(max_steps): response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsTOOLS, tool_choiceauto, ) msg response.choices[0].message # 判斷模型是否需要調(diào)用工具 if not msg.tool_calls: # 沒有工具調(diào)用說明模型已經(jīng)準(zhǔn)備好直接回答了 print(f最終回答: {msg.content}) return msg.content # 有工具調(diào)用先把a(bǔ)ssistant消息放入上下文 messages.append(msg) # 逐個(gè)處理工具調(diào)用 for tool_call in msg.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) print(f[調(diào)用工具] {fn_name}({fn_args})) # 在注冊(cè)表里查找并執(zhí)行 if fn_name in TOOL_REGISTRY: result TOOL_REGISTRY[fn_name](**fn_args) else: result {error: f未知工具: {fn_name}} # 把工具執(zhí)行結(jié)果作為tool角色消息追加進(jìn)上下文 messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) # 超過最大步數(shù)兜底 raise RuntimeError(fAgent執(zhí)行超過{max_steps}輪仍未結(jié)束) # ---------- 4. 執(zhí)行測(cè)試 ---------- if __name__ __main__: run_agent(北京今天天氣怎么樣順便幫我給 zhangsanexample.com 發(fā)個(gè)提醒郵件提醒他明天下午開會(huì)。)這個(gè)循環(huán)其實(shí)就是前面說的“五步循環(huán)”發(fā)給模型→模型決定調(diào)工具→執(zhí)行工具→結(jié)果回傳→再給模型。代碼跑起來后你會(huì)在日志里看到模型先調(diào)get_weather、再調(diào)send_reminder_email、最后根據(jù)兩個(gè)工具的返回結(jié)果生成一段完整的答案。這個(gè)時(shí)序是模型自主決策的不是你寫死的這正是Agent和傳統(tǒng)腳本最大的區(qū)別。4.3 解析為什么要設(shè)置max_steps上限一個(gè)Agent循環(huán)里如果模型不夠聰明或者工具返回的信息有誤導(dǎo)它有可能陷入“反復(fù)調(diào)工具、反復(fù)失敗”的死循環(huán)里一次對(duì)話消耗幾十次API調(diào)用。設(shè)置max_steps本質(zhì)上是給整個(gè)循環(huán)加了一個(gè)“熔斷器”。我在生產(chǎn)環(huán)境里通常不會(huì)設(shè)太高3到5步足夠覆蓋絕大多數(shù)場(chǎng)景因?yàn)橐粋€(gè)正常的任務(wù)最多也就連續(xù)調(diào)用兩三次工具。如果超過這個(gè)輪數(shù)還沒結(jié)果寧可返回“我無法完成這個(gè)任務(wù)”也不要讓用戶等半分鐘還看到一直在轉(zhuǎn)圈。4.4 多工具場(chǎng)景下的進(jìn)階從手寫分發(fā)到裝飾器注冊(cè)上面的示例里用一個(gè)字典做分發(fā)夠用但工具一多就會(huì)很凌亂。我建議趁早改成裝飾器注冊(cè)的方式把“工具定義”和“業(yè)務(wù)函數(shù)”寫在一起減少維護(hù)成本import inspect import functools def tool(nameNone): def decorator(func): registry_name name or func.__name__ TOOL_REGISTRY[registry_name] func # 自動(dòng)從函數(shù)簽名生成JSON Schema描述 TOOL_SCHEMAS.append({ type: function, function: { name: registry_name, description: func.__doc__ or , parameters: generate_schema_from_func(func), } }) return func return decorator tool() def get_weather(city: str): 查詢指定城市的實(shí)時(shí)天氣。當(dāng)用戶問到天氣、氣溫、是否下雨時(shí)使用。 ...這個(gè)方案的好處是工具定義不再單獨(dú)維護(hù)一份而是從函數(shù)簽名和docstring里自動(dòng)生成函數(shù)和Schema永遠(yuǎn)同步不會(huì)出現(xiàn)“代碼里改了參數(shù)定義忘了更新”這種低級(jí)問題。等你的Agent工具數(shù)量超過10個(gè)你會(huì)發(fā)現(xiàn)這個(gè)設(shè)計(jì)幫你節(jié)省了大量精力。5. 進(jìn)階能力擴(kuò)展函數(shù)調(diào)用怎么跟Agent框架、記憶、多Agent編排結(jié)合掌握了基礎(chǔ)循環(huán)之后你會(huì)發(fā)現(xiàn)函數(shù)調(diào)用其實(shí)只是Agent系統(tǒng)的“執(zhí)行底座”。真正讓Agent強(qiáng)大的是在這個(gè)底座之上疊加記憶、技能Skill、多Agent協(xié)作等能力。這一節(jié)我結(jié)合當(dāng)前Agent開發(fā)社區(qū)的熱門方向談?wù)勗趺窗押瘮?shù)調(diào)用往更完整的Agent架構(gòu)上延伸。5.1 從零散函數(shù)到Agent Skill給函數(shù)加“使用層”最近社區(qū)里“Agent Skill”這個(gè)概念很火包括Claude發(fā)布的Agent Skills本質(zhì)上也是在解決一個(gè)問題單個(gè)函數(shù)的能力太弱應(yīng)該把“實(shí)現(xiàn)同一目標(biāo)的一組操作”打包成一個(gè)可復(fù)用的Skill。舉個(gè)例子單純一個(gè)get_weather(city)函數(shù)是單一能力但如果Agent接到的任務(wù)是“幫用戶規(guī)劃一次周末旅行”它可能先調(diào)天氣查詢、再調(diào)酒店搜索、再調(diào)地圖規(guī)劃這就是三個(gè)函數(shù)的組合。與其讓模型每次從頭一步步試探不如提前把這些函數(shù)的調(diào)用鏈封裝成一個(gè)高階工具比如plan_trip(city, date_range)內(nèi)部去編排天氣、酒店、路線三個(gè)子調(diào)用。這個(gè)抽象層次的思想跟軟件工程里的“函數(shù)→模塊→服務(wù)”演進(jìn)路徑是一模一樣的。我建議你做Agent時(shí)不要一上來就堆50個(gè)細(xì)粒度函數(shù)那會(huì)讓模型在選擇工具時(shí)無所適從。更好的策略是先做十幾個(gè)粗粒度的Skill每個(gè)Skill內(nèi)部再封裝細(xì)粒度的函數(shù)。5.2 函數(shù)調(diào)用的結(jié)果如何寫入記憶與工作區(qū)Agent在調(diào)用函數(shù)執(zhí)行任務(wù)的過程中會(huì)產(chǎn)生大量中間結(jié)果。這些結(jié)果如果每次都只在上下文里流轉(zhuǎn)一方面浪費(fèi)Token另一方面下次對(duì)話就全丟了。這就是“Agent記憶”和“工作區(qū)”要解決的問題。我目前比較推薦的做法是函數(shù)調(diào)用的結(jié)構(gòu)化結(jié)果除了返回到對(duì)話上下文之外同時(shí)寫入到一個(gè)Agent工作區(qū)里。這個(gè)工作區(qū)可以是一個(gè)本地的JSON文件、一個(gè)向量數(shù)據(jù)庫、或者干脆就是目標(biāo)項(xiàng)目的文件目錄。比如Agent執(zhí)行完save_document工具后文件寫入了本地路徑返回給模型的是一條{status: saved, path: /data/doc_123.md}這樣的消息而不是整個(gè)文件內(nèi)容。模型知道文件已經(jīng)存到哪兒了但不會(huì)把大段文件內(nèi)容塞進(jìn)上下文這樣就同時(shí)兼顧了可追溯性和Token開銷。5.3 多Agent編排中的函數(shù)調(diào)用誰調(diào)用結(jié)果歸誰多Agent系統(tǒng)現(xiàn)在也是一個(gè)熱門話題。這里要特別注意函數(shù)調(diào)用不再只是“一個(gè)Agent調(diào)用一堆工具”而是“多個(gè)Agent各自擁有不同的工具集”。架構(gòu)上要做的是給每個(gè)Agent配置獨(dú)立的tools列表和獨(dú)立的工具注冊(cè)表不能讓Agent A調(diào)用Agent B的私有工具否則就失去了隔離的意義。還有一個(gè)容易出錯(cuò)的地方是狀態(tài)歸屬Agent A調(diào)用了寫文件工具Agent B隨后要讀這個(gè)文件如果兩個(gè)Agent跑在不同的進(jìn)程甚至不同的機(jī)器上A的寫和B的讀之間就需要一個(gè)共享的存儲(chǔ)層。我建議小規(guī)模項(xiàng)目直接用一個(gè)Redis或者數(shù)據(jù)庫表來當(dāng)共享工作區(qū)而不是讓Agent B直接去讀Agent A的內(nèi)存變量。在多Agent的編排下函數(shù)調(diào)用的結(jié)果還需要帶上“執(zhí)行者”和“時(shí)間戳”信息否則日志排查時(shí)你會(huì)瘋掉。折騰過一兩次就知道這種跨Agent的調(diào)用鏈路一旦出問題沒有元信息根本定位不到是誰調(diào)錯(cuò)了參數(shù)。6. 常見問題與排查技巧實(shí)錄這些坑我不希望你重踩一遍這一部分是全文最“貴”的內(nèi)容全部來自我實(shí)際開發(fā)Agent時(shí)被折磨過的問題。我把它們整理成速查表和分析優(yōu)先看那些跟你癥狀匹配的。6.1 模型該調(diào)用函數(shù)卻不調(diào)用癥狀用戶明確問了“北京天氣怎么樣”模型卻自己編了一段“根據(jù)我的了解北京今天晴……”的幻覺答案完全沒走工具調(diào)用。排查順序先看tools參數(shù)是否真的傳進(jìn)去了、tool_choice是否被誤設(shè)成了none。這兩個(gè)是低級(jí)錯(cuò)誤檢查完基本能排除。然后再看函數(shù)描述里有沒有寫清楚“觸發(fā)時(shí)機(jī)”如果只寫了“查詢某個(gè)城市的天氣”這種靜態(tài)描述模型確實(shí)容易把工具調(diào)用當(dāng)成可選項(xiàng)。解決辦法把描述改成帶觸發(fā)條件的動(dòng)態(tài)表述。比如“當(dāng)用戶詢問道市天氣、氣溫、是否下雨、是否適合戶外活動(dòng)時(shí)必須調(diào)用此工具來獲取實(shí)時(shí)數(shù)據(jù)不得自行編造天氣信息。”加一句“不得自行編造”對(duì)于抑制幻覺很有效。6.2 參數(shù)類型不對(duì)傳了字符串而不是數(shù)字癥狀函數(shù)定義里days參數(shù)是integer類型模型卻傳了3天這種值導(dǎo)致類型校驗(yàn)報(bào)錯(cuò)。原因模型的Token化過程對(duì)中文和數(shù)字的混合輸入很不敏感。你定義的是JSON Schema但模型是在做文本生成它不一定嚴(yán)格遵循類型約定。解決辦法除了在Schema里聲明類型還要在參數(shù)描述里寫清楚“只傳數(shù)字不要帶單位”。更穩(wěn)妥的辦法是在工具函數(shù)的入口做一次顯式類型轉(zhuǎn)換比如def set_reminder(days): days int(str(days).replace(天, ).strip()) ...這屬于防御式編程寧可代碼里多兩行也不要讓一個(gè)參數(shù)錯(cuò)誤導(dǎo)致整條鏈路崩潰。6.3 并發(fā)場(chǎng)景下函數(shù)調(diào)用狀態(tài)串了癥狀A(yù)gent服務(wù)上線后一旦同時(shí)服務(wù)多個(gè)用戶就出現(xiàn)A用戶的工具調(diào)用結(jié)果跑到了B用戶的上下文里或者函數(shù)執(zhí)行時(shí)用錯(cuò)了參數(shù)。原因絕大多數(shù)情況是消息列表被設(shè)計(jì)成了共享變量。記住一個(gè)原則Agent的對(duì)話上下文是強(qiáng)隔離的每一個(gè)用戶會(huì)話必須有自己獨(dú)立的messages列表不能有全局共享的上下文。解決方案生產(chǎn)環(huán)境里用會(huì)話ID做維度管理上下文。每一次請(qǐng)求都從會(huì)話存儲(chǔ)里讀取屬于該會(huì)話的消息列表函數(shù)執(zhí)行結(jié)果也按會(huì)話ID寫回。如果需要并發(fā)執(zhí)行多個(gè)工具調(diào)用還記得給每個(gè)工具調(diào)用的結(jié)果帶上tool_call_id確保模型能正確匹配。6.4 工具返回結(jié)果太大上下文爆炸癥狀跑了一陣之后發(fā)現(xiàn)每次請(qǐng)求的Token消耗越來越大后來一查是某個(gè)工具把一份5000行的數(shù)據(jù)全量返回給了模型。原因工具返回結(jié)果進(jìn)入了一直累積的對(duì)話上下文不會(huì)被自動(dòng)清理大結(jié)果反復(fù)出現(xiàn)Token消耗自然飆升。解決辦法三個(gè)字截、摘、引。截是只返回前N行摘是讓工具內(nèi)部先做一次摘要只把摘要返回給模型引是對(duì)于極大的數(shù)據(jù)量把數(shù)據(jù)存到數(shù)據(jù)庫/文件只返回一個(gè)可以檢索的ID或路徑。我在生產(chǎn)項(xiàng)目里這三招組合使用Token成本直接降了60%以上。6.5 安全邊界函數(shù)調(diào)用的權(quán)限必須收口癥狀有開發(fā)者在Agent里暴露了一個(gè)執(zhí)行任意Shell命令的工具結(jié)果模型在某個(gè)輸入誘導(dǎo)下執(zhí)行了一串危險(xiǎn)命令。原因沒有做權(quán)限管控。函數(shù)調(diào)用的本質(zhì)是把你系統(tǒng)里已有的能力暴露給模型而模型又會(huì)聽用戶的。用戶輸入是無限的模型的判斷不是萬無一失的所以你必須假設(shè)“用戶正在嘗試攻擊這個(gè)Agent”。解決辦法這幾點(diǎn)我建議作為Agent開發(fā)的紅線第一高危工具刪除文件、執(zhí)行命令、轉(zhuǎn)賬、發(fā)短信一律不暴露給模型改成由Agent生成“待確認(rèn)操作卡片”由用戶在前端手動(dòng)確認(rèn)后再執(zhí)行。第二所有工具函數(shù)的參數(shù)要做白名單校驗(yàn)比如文件路徑只能落在指定目錄內(nèi)。第三復(fù)雜操作增加一個(gè)“審計(jì)日志”記錄每次工具調(diào)用的完整參數(shù)和結(jié)果。這三條都做到你基本就擋住了90%的常規(guī)攻擊路徑。7. 函數(shù)調(diào)用排查速查表與最后的經(jīng)驗(yàn)總結(jié)把前面幾節(jié)的內(nèi)容壓縮成一張速查表直接在排查時(shí)對(duì)號(hào)入座癥狀常見原因優(yōu)先排查項(xiàng)模型不調(diào)用工具tools參數(shù)未傳、描述缺少觸發(fā)條件tool_choice設(shè)置、description觸發(fā)詞參數(shù)格式錯(cuò)誤類型不符、單位混淆參數(shù)描述明確類型、函數(shù)入口防御轉(zhuǎn)換調(diào)用結(jié)果不生效tool_call_id不匹配、上下文漏追加消息assistant消息和tool消息的順序、id匹配Token消耗暴漲工具返回全量數(shù)據(jù)、上下文無限累積結(jié)果截?cái)?摘要、消息列表窗口管理并發(fā)結(jié)果串線會(huì)話上下文全局共享按會(huì)話ID隔離messages列表函數(shù)報(bào)錯(cuò)中斷執(zhí)行異常未捕獲try/except包裹工具執(zhí)行、異常轉(zhuǎn)為錯(cuò)誤消息回傳模型模型越權(quán)調(diào)用高危函數(shù)權(quán)限未收斂高危操作人工確認(rèn)、參數(shù)白名單我個(gè)人在實(shí)際操作中的體會(huì)是函數(shù)調(diào)用的實(shí)現(xiàn)其實(shí)不難難的是圍繞它的整套工程化設(shè)計(jì)——容錯(cuò)、隔離、審計(jì)、成本控制這些才是Agent能否長(zhǎng)期穩(wěn)定跑下去的關(guān)鍵。很多人覺得Agent開發(fā)就是“調(diào)一個(gè)API就完事了”但真正沉淀下來的能力恰恰是在這些細(xì)節(jié)里。最后再分享一個(gè)小技巧給你的每個(gè)工具寫一句話的“使用時(shí)機(jī)說明”。這句話不是給用戶看的也不是給代碼看的而是給模型看的。模型決定調(diào)不調(diào)用工具主要依據(jù)就是工具描述和當(dāng)前對(duì)話意圖之間的匹配度。這一句描述寫得好不好直接影響工具調(diào)用率值得你花時(shí)間反復(fù)打磨。我通常是跑一批真實(shí)用戶日志看看哪些工具調(diào)用率低、哪些工具被誤調(diào)用針對(duì)性調(diào)描述調(diào)完再跑一輪一般兩三輪之后工具選擇準(zhǔn)確率就能穩(wěn)定在95%以上。這個(gè)經(jīng)驗(yàn)希望你下次做個(gè)帶工具的Agent時(shí)能幫你少走不少彎路。