、提示詞與工具分發(fā)實戰(zhàn))
1. 從零手搓一個小 Agent為什么我不建議你直接上大框架這兩年 Agent 這個詞被炒得火熱打開任何一個技術(shù)社區(qū)滿屏都是智能體自主規(guī)劃工具調(diào)用這類詞。但真到動手的時候很多人第一反應是去拉一個成熟框架裝一堆依賴跑通一個 Demo然后……就沒有然后了。因為框架把該藏的細節(jié)全藏起來了你根本不知道一個 Agent 到底是怎么想的出了問題也不知道從哪查。我自己走過這條路。最開始也是拿現(xiàn)成框架搭能跑但一旦要改行為邏輯、要控制成本、要排查為什么它突然不調(diào)用工具了就抓瞎。后來我干脆花了一個周末參照一個極簡 Agent 實現(xiàn)社區(qū)里常被拿來當教學樣本的那類我這邊就叫它 pi-agent 思路自己從零寫了一個不到三百行的小 Agent。寫完那一刻才真正理解Agent 的本質(zhì)沒那么玄乎它就是一個循環(huán) 提示詞 工具分發(fā)的組合體。這篇東西就是那次折騰的完整復盤。我會把整體設(shè)計思路、核心模塊拆解、可運行的實操步驟、以及我踩過的坑全部攤開講。適合兩類人一是想真正搞懂 Agent 內(nèi)部機制、不想被框架黑盒困住的開發(fā)者二是已經(jīng)會用框架、但想自己掌控每一行邏輯的中級選手??赐昴銘撃茏约簞邮謱懗鲆粋€能跑、能調(diào)工具、能多輪對話的最小 Agent并且知道每個參數(shù)為什么這么設(shè)。2. 整體設(shè)計一個 Agent 到底由哪幾塊拼起來2.1 先想清楚Agent 和普通聊天機器人的分界線在哪很多人把能對話的大模型和Agent混為一談。區(qū)別其實就一條Agent 能根據(jù)當前狀態(tài)自主決定下一步動作并且這個動作可以作用于外部世界。普通聊天機器人是你問一句它答一句被動響應Agent 是給它一個目標它自己判斷我現(xiàn)在該查資料、該算數(shù)、還是該直接回答然后執(zhí)行拿到結(jié)果再判斷下一步。這個判斷—執(zhí)行—再判斷的過程落到代碼上就是一個循環(huán)。循環(huán)的每一輪模型輸出一個意圖程序解析這個意圖如果是調(diào)用工具就去調(diào)把結(jié)果塞回上下文再進入下一輪如果是直接回答就結(jié)束。聽起來簡單但魔鬼全在細節(jié)里意圖怎么表達、工具怎么注冊、結(jié)果怎么回填、什么時候該停。我參照 pi-agent 的思路把整個 Agent 拆成四個核心模塊對話循環(huán)Loop、提示詞模板Prompt、工具注冊表Tool Registry、消息歷史管理Memory。下面逐個說。2.2 為什么選循環(huán) 工具分發(fā)而不是一次性規(guī)劃市面上 Agent 的架構(gòu)大致分兩派一派是先規(guī)劃再執(zhí)行讓模型一次性輸出完整的多步計劃然后按計劃走另一派是邊想邊做每一輪只決定下一步。我選的是后者也就是 ReAct 那一類的思路。原因很實際。一次性規(guī)劃看起來優(yōu)雅但模型對長鏈條的預判能力其實很弱計劃到第三步往往就偏了而且一旦某步失敗整個計劃作廢重規(guī)劃成本高。邊想邊做雖然輪次多、token 消耗大一點但每一步都基于最新的真實結(jié)果做決策容錯性強得多。對于一個小 Agent 來說可控性和容錯性比優(yōu)雅重要得多。提示如果你做的任務步驟非常固定比如固定的數(shù)據(jù)清洗流水線一次性規(guī)劃反而更省 token但只要任務有不確定性邊想邊做幾乎總是更穩(wěn)。2.3 模塊之間的數(shù)據(jù)流長什么樣我用一段話來描述整個數(shù)據(jù)流你對照著理解后面代碼會輕松很多用戶輸入進入 → 拼進消息歷史 → 連同系統(tǒng)提示詞一起發(fā)給模型 → 模型返回要么是普通文本、要么是工具調(diào)用請求 → 程序判斷類型 → 如果是工具調(diào)用執(zhí)行對應函數(shù)把返回值作為一條新消息追加到歷史 → 再次發(fā)給模型 → 重復直到模型返回普通文本或達到最大輪次 → 輸出給用戶。這里有個關(guān)鍵設(shè)計點工具調(diào)用的結(jié)果必須以特定角色通常是 tool 角色回填而不是簡單拼成一段文字。因為模型需要明確區(qū)分這是我請求的工具返回和這是用戶說的話否則多輪之后它會混亂。這個細節(jié)很多手寫 Agent 的人第一次都會踩。3. 核心模塊拆解每一塊怎么寫才不出坑3.1 對話循環(huán)整個 Agent 的心臟循環(huán)的骨架大概是這樣用 Python 偽代碼示意實際語言隨意def run_agent(user_input, max_turns10): messages.append({role: user, content: user_input}) for turn in range(max_turns): response call_model(messages, toolstool_schemas) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: result execute_tool(call.name, call.arguments) messages.append({ role: tool, tool_call_id: call.id, content: str(result) }) return 達到最大輪次任務未完成這段代碼短但每一行都有講究。max_turns是必須的否則模型可能陷入調(diào)工具—不滿意—再調(diào)的死循環(huán)燒錢又燒時間。我一般設(shè) 8 到 12具體看任務復雜度。tool_call_id也不能省它是把工具返回和具體某次調(diào)用對應起來的鑰匙多工具并行調(diào)用時尤其重要。還有一個容易忽略的點每輪都要把模型的原始返回含 tool_calls追加進歷史而不是只追加文本內(nèi)容。因為下一輪模型需要看到我上一輪請求了什么才能理解工具返回的是什么。3.2 提示詞模板決定 Agent 聰明還是智障系統(tǒng)提示詞是 Agent 的人格說明書寫得好壞直接決定它會不會用工具、用得對不對。我踩過的最大坑就是提示詞寫得太客氣模型經(jīng)常該調(diào)工具的時候不調(diào)直接憑記憶瞎答。一個能用的系統(tǒng)提示詞至少要說清三件事你是誰、你有什么工具、什么時候該用工具。我常用的模板結(jié)構(gòu)是這樣的你是一個可以調(diào)用工具的助手。你可以使用以下工具 {tool_descriptions} 規(guī)則 1. 當問題涉及實時信息、精確計算或你不確定的事實時必須調(diào)用工具不要憑記憶回答。 2. 一次只調(diào)用必要的工具拿到結(jié)果后再決定下一步。 3. 如果工具返回錯誤嘗試換一種參數(shù)或換一個工具不要重復同樣的調(diào)用。 4. 當你有足夠信息回答用戶時直接給出最終答案不要再調(diào)用工具。第 1 條和第 4 條是最關(guān)鍵的。第 1 條治該調(diào)不調(diào)第 4 條治調(diào)起來沒完。我實測下來加上第 4 條之后無謂的工具調(diào)用能減少一大半。注意工具描述tool_descriptions不是隨便寫寫。模型完全靠這段文字判斷工具用途描述里必須包含這個工具做什么、參數(shù)是什么、什么時候用。寫得含糊模型就會亂調(diào)。3.3 工具注冊表讓 Agent 的手能伸出去工具注冊表的核心是一份 schema 一個函數(shù)的映射。schema 給模型看函數(shù)給程序執(zhí)行。我用一個字典來管理TOOLS { get_weather: { schema: { type: function, function: { name: get_weather, description: 查詢指定城市的當前天氣, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } }, func: lambda city: real_weather_api(city) } }這里有個設(shè)計取舍schema 和函數(shù)分開存還是綁在一起。我選綁在一起因為改工具的時候不容易漏改另一半。執(zhí)行時用TOOLS[name][func](**args)一行搞定。參數(shù)校驗千萬別省。模型給的參數(shù)經(jīng)常是字符串形式的數(shù)字、或者缺字段直接傳給函數(shù)會炸。我在execute_tool里加了一層 try/except把異常信息作為工具返回內(nèi)容回填給模型讓它自己糾正。這比程序直接崩潰友好太多。3.4 消息歷史管理上下文不是越長越好消息歷史是 Agent 的記憶但它也是成本大頭。每一輪都要把完整歷史發(fā)給模型歷史越長token 越貴而且模型注意力會被稀釋容易忘掉早期關(guān)鍵信息。我的做法是保留系統(tǒng)提示詞 最近 N 輪完整對話 更早內(nèi)容的摘要。N 一般取 6 到 10。摘要可以用模型生成也可以簡單截斷。對于小 Agent我甚至直接用一個滑動窗口超過就丟最早的實測對短任務夠用。提示工具返回的超長內(nèi)容比如一整頁網(wǎng)頁一定要截斷或摘要后再回填否則一次就能把上下文撐爆。我一般限制單條工具返回不超過 2000 字符。4. 實操從零跑通一個能查天氣和算數(shù)的小 Agent4.1 環(huán)境準備與依賴選擇我用的環(huán)境很樸素Python 3.10一個模型 API 的 SDK加一個 HTTP 請求庫。沒有用任何 Agent 框架就是為了看清每一層。模型我選的是支持 function calling 的通用對話模型因為工具調(diào)用能力是 Agent 的命脈不支持 function calling 的模型得靠提示詞硬湊 JSON穩(wěn)定性差很多。依賴清單就三樣模型 SDK、requests、python-dotenv管理密鑰。裝完大概十幾秒。密鑰放.env文件別硬編碼進代碼這個習慣從第一天就要養(yǎng)成。4.2 定義兩個工具一個查天氣一個算數(shù)為了演示工具分發(fā)我定義兩個差異明顯的工具。查天氣代表外部信息獲取算數(shù)代表精確計算——這兩類恰好是模型最容易出錯的場景也最能體現(xiàn) Agent 的價值。import math def get_weather(city: str) - str: # 實際項目里換成真實天氣 API fake_db {北京: 晴18度, 上海: 多云22度} return fake_db.get(city, f未找到{city}的天氣數(shù)據(jù)) def calculate(expression: str) - str: try: # 只允許安全表達式 allowed {k: getattr(math, k) for k in dir(math) if not k.startswith(_)} result eval(expression, {__builtins__: {}}, allowed) return f計算結(jié)果{result} except Exception as e: return f計算失敗{e}算數(shù)工具用eval有安全風險我做了兩層限制清空__builtins__只暴露 math 里的函數(shù)。生產(chǎn)環(huán)境更穩(wěn)妥的做法是用專門的表達式解析庫但演示夠用了。4.3 組裝主循環(huán)并跑通第一個任務把前面的模塊拼起來主循環(huán)大概五十行。跑一個北京天氣怎么樣順便算一下 23 乘以 47的任務你會看到 Agent 先調(diào)天氣工具再調(diào)算數(shù)工具最后匯總回答。整個過程兩到三輪token 消耗可控。這里有個實測細節(jié)兩個工具調(diào)用有時會并行返回模型一次返回多個 tool_calls有時會串行。你的循環(huán)必須兩種都能處理。我一開始只處理了單個調(diào)用結(jié)果遇到并行調(diào)用直接漏掉一個排查了半天。4.4 參數(shù)計算max_turns 和溫度怎么定max_turns我前面說 8 到 12具體怎么定我的經(jīng)驗公式是預估任務最大步數(shù) × 1.5。比如一個任務最多需要查 3 次資料、算 2 次那就是 5 步乘 1.5 取 8。留余量是因為模型偶爾會走彎路。溫度temperature對 Agent 影響很大。工具調(diào)用場景我一般設(shè) 0 到 0.3越低越穩(wěn)定模型更傾向于按規(guī)則走。設(shè)高了它會發(fā)揮創(chuàng)意該調(diào)工具的時候跟你聊天。只有做創(chuàng)意類任務時才調(diào)高。參數(shù)推薦值說明max_turns8-12任務步數(shù) × 1.5temperature0-0.3工具場景求穩(wěn)單條工具返回上限2000 字符防上下文爆炸歷史保留輪數(shù)6-10平衡成本與記憶5. 常見問題與排查技巧實錄5.1 模型死活不調(diào)用工具怎么辦這是最高頻的問題。排查順序我總結(jié)成三步先看工具描述是不是太模糊模型看不懂自然不會用再看系統(tǒng)提示詞有沒有明確必須調(diào)用的規(guī)則最后看模型本身是否支持 function calling。我遇到過描述里寫獲取信息這種含糊詞改成查詢指定城市的實時天氣返回溫度和天氣狀況之后調(diào)用率立刻上來了。5.2 工具調(diào)用陷入死循環(huán)怎么破表現(xiàn)是模型反復調(diào)同一個工具、傳同樣的參數(shù)。原因通常是工具返回了錯誤但模型沒理解或者提示詞沒告訴它拿到結(jié)果就停。解法有兩個一是max_turns兜底二是把錯誤信息寫清楚讓模型知道這條路走不通。我在工具返回里會明確寫錯誤參數(shù) city 不能為空而不是拋一個裸異常。5.3 上下文越來越長、越來越貴前面提過滑動窗口和截斷。補充一個技巧把工具返回的原始 JSON 精簡后再回填。比如天氣 API 返回一大坨我只提取溫度和天氣兩個字段拼成一句話回填。這樣既省 token模型也更容易抓重點。5.4 常見問題速查表現(xiàn)象可能原因解決方向不調(diào)用工具描述模糊/提示詞沒要求改描述、加規(guī)則死循環(huán)無 max_turns/錯誤信息不清加輪次上限、明確錯誤上下文爆炸工具返回過長截斷、摘要、精簡字段參數(shù)報錯模型給錯類型加校驗、異?;靥畲鸱撬鶈枤v史混亂檢查角色標記是否正確提示調(diào)試 Agent 最有效的手段是把每一輪的完整 messages 打印出來。你會直觀看到模型看到了什么、想了什么問題一目了然。我調(diào)試時幾乎全程開著這個日志。6. 我踩過的幾個坑和一點個人體會第一個坑是把工具結(jié)果拼成普通文本回填。早期我圖省事把工具返回直接拼進 assistant 的消息里結(jié)果模型分不清哪些是自己說的、哪些是工具給的多輪之后開始胡編。改成獨立的 tool 角色消息后問題消失。第二個坑是忘了處理并行工具調(diào)用。模型一次返回多個 tool_calls 時我最初只取了第一個導致任務信息缺失。后來改成遍歷所有調(diào)用、逐個執(zhí)行、逐個回填才穩(wěn)定下來。第三個坑是提示詞里沒寫夠了就停。模型拿到工具結(jié)果后有時會想要不再確認一下于是反復調(diào)用。加上信息足夠時直接回答這條規(guī)則后輪次明顯下降。我個人在實際操作中的體會是寫 Agent 最難的從來不是代碼而是把什么時候該做什么用自然語言給模型講清楚。代碼只是骨架提示詞才是靈魂。你花在打磨提示詞上的時間往往比寫循環(huán)本身多得多。所以別急著堆功能先把一個工具、一條規(guī)則調(diào)穩(wěn)再往上加。一個小而穩(wěn)的 Agent價值遠大于一個大而亂的。后續(xù)如果想擴展我建議按這個順序加先加工具調(diào)用失敗重試再加多輪記憶摘要最后才考慮多 Agent 協(xié)作。每一步都跑穩(wěn)了再走下一步這是我折騰下來最實在的經(jīng)驗。