:別急著學 Tool Calling,先搞懂 LLM 怎么變成程序真正能用的“接口”)
第一課我們講了一件看起來很簡單但其實決定了整個 Agent 技術棧的事情LLM 只會產生輸出它不會直接改變外部世界。你讓一個裸模型刪除 report.txt它可以回答好的report.txt 已刪除。甚至可以生成os.remove(report.txt)但只要外面沒有程序真正執行os.remove()你的文件就還好好地躺在硬盤里。所以第一課最后我們把 Model 和 Runtime 分開了Model決定應該做什么 Runtime真正把事情做掉到這里其實已經很接近 Agent 了。但如果你真的開始自己寫代碼會立刻撞上第二個問題。而且這個問題比“怎么調用 Tool”更早出現。假設用戶說幫我查一下上海今天的天氣 看看下午要不要帶傘。模型判斷完之后回答我覺得這個問題最好先查一下上海今天的天氣。作為人你當然看得懂。但現在不要站在人類的角度看這句話。站在代碼的角度看。你的程序拿到的其實只是response 我覺得這個問題最好先查一下上海今天的天氣。 然后呢程序怎么知道模型到底想干什么這就是第二課真正要解決的問題。人能聽懂的話不一定是一個好的軟件接口假設我們的 Runtime 現在有三個能力查詢天氣 查詢訂單 直接回答用戶那么模型每一次做完判斷之后程序真正關心的其實只有一件事你到底選擇了哪一個但模型偏偏特別擅長“說人話”。它可能說我需要先看看上海天氣。下一次可能說為了回答這個問題我需要獲取上海當前的氣象信息。還可能說最好先確認一下上海今天有沒有雨。換個模型也許直接變成Let me check the weather in Shanghai first.對人來說這四句話表達的是同一個意思。對程序來說這是四個完全不同的字符串。于是很多人第一次自己寫這種程序時會很自然地干一件事if 天氣 in response: get_weather()看起來居然還能跑。然后有一天模型回答我需要看看上海今天會不會下雨。沒有“天氣”兩個字。代碼失效。于是你補if 天氣 in response or 下雨 in response: get_weather()過幾天模型又回答先獲取一下當地氣象情況吧。于是你繼續補if ( 天氣 in response or 下雨 in response or 氣象 in response ): get_weather()如果再復雜一點你很快就會開始寫正則表達式。然后再給正則表達式打補丁。再給補丁打補丁。做到這里應該已經能感覺到哪里不對了。問題并不在于模型“不夠聰明”。真正的問題是我們正在拿自然語言當軟件協議用。而自然語言恰恰是一個極度不穩定的軟件協議。人類喜歡自然語言是因為我們非常擅長容忍模糊。程序恰恰相反。程序最喜歡的是字段叫什么是確定的。 類型是什么是確定的。 允許出現哪些值是確定的。 缺少字段怎么辦也是確定的。于是 Agent 工程里第一個非常自然的問題出現了能不能不要讓 Runtime 猜模型是什么意思而是讓模型直接按照程序約定的格式表達自己的決定這就是 Structured Output 真正要解決的問題。第一次關鍵轉變從“說一句話”變成“返回一個數據結構”還是剛才的問題。用戶幫我查一下上海今天的天氣。以前模型返回我認為應該先查詢上海天氣。現在我們要求模型返回{ action: get_weather, arguments: { city: Shanghai } }看起來只是換成了 JSON。但從軟件工程角度看事情已經發生了本質變化。以前 Runtime 拿到我認為應該先查詢上海天氣。它必須先理解一句自然語言。現在 Runtime 拿到decision[action]得到get_weather再讀取decision[arguments][city]得到ShanghaiRuntime 不需要理解中文。不需要 NLP。不需要關鍵詞。也不需要猜。這時候LLM 第一次從一個自然語言生成器開始變成了一個可以被軟件消費的決策接口這才是 Structured Output 真正重要的地方。它解決的從來不是“讓輸出看起來整齊一點”。它解決的是怎樣把一個概率模型的輸出接進一個確定性的軟件系統。這一點非常重要。因為后面整個 Agent 都建立在這條橋上。但這里有一個坑會輸出 JSON不等于真正有了 Structured Output很多人學到這里會覺得事情已經解決了。Prompt 里面寫一句請嚴格返回 JSON不要輸出其他內容。然后告訴模型格式 { action: ..., arguments: {} }看起來很合理。但你真正跑幾次就會發現問題來了。模型可能返回當然可以下面是結果 { action: get_weather, arguments: { city: Shanghai } }這時候json.loads(response)直接失敗。你說沒關系我再強調一次“只能返回 JSON”。然后下一次模型很聽話{ action: weather, arguments: { city: Shanghai } }這是合法 JSON。但 Runtime 根本沒有一個叫weather的 Action。Runtime 只有get_weather再下一次{ action: get_weather, arguments: { city: 100 } }還是合法 JSON。但city為什么變成數字了甚至可能{ action: get_weather, arguments: {} }JSON 依然完全合法。但最關鍵的city不見了。這時候有一個很值得記住的區別Valid JSON ≠ Valid Decision這句話最好記住。JSON 解決的只是語法。但一個軟件接口真正需要的是契約。Schema 才是這里真正重要的東西假設我們希望模型返回{ action: get_weather, arguments: { city: Shanghai } }真正需要定義的其實遠遠不只是“大括號怎么寫”。我們需要告訴模型和 Runtime最外層必須是 object。 必須存在 action。 action 必須是 string。 action 只能取幾個指定值。 必須存在 arguments。 arguments 必須是 object。 如果 action 是 get_weather 那么 arguments 里必須有 city。 city 必須是 string。這就是 Schema 在干的事情。例如我們可以表達成類似這樣的約束{ type: object, properties: { action: { type: string, enum: [ get_weather, query_order, final_answer ] }, arguments: { type: object } }, required: [ action, arguments ] }具體以后你用 JSON Schema、類型系統還是某個模型 SDK 提供的 Structured Output API并不是這一課最值得記住的事情。真正重要的是腦子里要建立一個概念Schema 是 Model 和 Runtime 之間的 Contract。Runtime 相當于在告訴模型你可以負責判斷。 但如果你希望軟件理解你的判斷 請按照這個協議說。這就是一個非常標準的軟件工程思想。只不過協議的一端不再是另一個確定性的服務。而是一個 LLM。第二個關鍵轉變我們實際上正在限制模型的“動作空間”這里有一個我認為比“學會 JSON Schema”重要得多的理解。假設我們告訴模型action 只能是 get_weather query_order final_answer這意味著什么意味著模型不能突然自己發明一個search_weather_from_google也不能發明open_browser更不能突然來一個delete_database從 Runtime 的角度我們實際上定義了Action Space { get_weather, query_order, final_answer }這個概念特別重要。因為到這里LLM 已經不再只是“根據上下文繼續生成一段文字”從 Agent Engineering 的視角我們可以開始換一種方式理解它給定當前 Context ↓ 模型判斷當前應該做什么 ↓ 從允許的 Action Space 里選擇一個 Action如果寫得更抽象一點Context / State ↓ LLM ↓ Choose Action這已經開始非常像 Agent 了。而且你會發現Schema 不只是定義數據格式。它還在定義這個 Agent 到底被允許做出哪些類型的決定。這是這一課非常重要的一個“恍然大悟”。Schema 設計本身就是 Agent 設計我們繼續往下推。最開始只有一個動作get_weather所以很簡單{ action: get_weather, arguments: { city: Shanghai } }后來系統又增加一個能力query_order它需要的參數不是city而是order_id于是{ action: query_order, arguments: { order_id: PO-10086 } }再后來增加search_web它需要query于是{ action: search_web, arguments: { query: Agent Memory } }到這里你會慢慢看到一個結構出現了Decision │ ├── action │ └── argumentsaction回答我準備做什么arguments回答完成這個動作需要什么參數這已經和函數調用非常像了。例如 Pythonget_weather(cityShanghai)和結構化 Decision{ action: get_weather, arguments: { city: Shanghai } }兩者之間幾乎已經可以一一對應。注意這個時候我們仍然沒有執行函數。但是 Model 和 Runtime 之間已經建立好了“如何描述一個函數調用”的語言。這就是為什么 Structured Output 應該放在 Tool Calling 前面講。否則你第一次看到 Tool Calling 的時候很容易誤以為模型里面有一個什么神秘的函數執行系統。其實完全沒有必要這么理解。把前面的東西一步一步推出來以后你會發現Tool Calling 馬上就要變得非常樸素了。到這里先停一下模型還是沒有調用任何東西這個邊界必須再強調一次。模型返回{ action: get_weather, arguments: { city: Shanghai } }請問上海天氣查了嗎沒有。一點都沒有。現在真實發生的事情仍然只有Context ↓ LLM ↓ Tokens ↓ Structured Data模型只是用一種程序容易理解的方式表達“我認為下一步應該查詢上海天氣。”以前它用中文表達。現在它用結構化數據表達。本質沒有變化這仍然只是 Decision。真正的天氣查詢必須等 Runtime 做get_weather(cityShanghai)才會發生。所以這里可以建立一個以后非常有用的邊界Structured Output 如何表達 Decision Tool Execution 如何執行 Action這兩件事情不要混。一旦混了你后面學 Tool Calling、MCP、Agent Loop 時會越來越亂。我們現在真的寫一個02_structured_output.py這一課仍然不要 LangChain。不要 LangGraph。也不要任何 Agent Framework。我們的項目繼續保持agent-from-zero/ │ ├── 01_llm.py └── 02_structured_output.py第一課User ↓ LLM ↓ Text這一課只增加一件東西User ↓ LLM ↓ Structured Decision ↓ Validation我們的目標非常克制不是做 Agent。只是讓模型學會輸出 Runtime 能可靠理解的 Decision。假設系統目前允許四種決定get_weather query_order ask_user final_answer為什么多了一個ask_user等一下就會知道。我們先約定一個統一格式{ action: ..., arguments: {} }例如{ action: get_weather, arguments: { city: Shanghai } }或者{ action: query_order, arguments: { order_id: PO-10086 } }程序拿到模型返回之后第一件事不是執行。而是import json def parse_decision(raw): return json.loads(raw)這是 Parser。它只解決一個問題模型返回的東西 能不能被解析成 JSON例如{ action: get_weather, arguments: { city: Shanghai } }可以。而好的我建議查詢天氣。不可以。但只有 Parser 遠遠不夠。我們還需要 Validator。比如ALLOWED_ACTIONS { get_weather, query_order, ask_user, final_answer, } def validate_decision(decision): if action not in decision: raise ValueError(missing action) if arguments not in decision: raise ValueError(missing arguments) if decision[action] not in ALLOWED_ACTIONS: raise ValueError(unknown action) if not isinstance(decision[arguments], dict): raise ValueError(arguments must be an object) return decision現在raw llm(messages) decision parse_decision(raw) decision validate_decision(decision)先不用糾結llm()具體是哪一家 API。我們現在研究的是結構。這幾行代碼其實已經把一個非常重要的東西拆出來了Model Output ↓ Parse ↓ Validate ↓ Decision以后做大型 Agent 時你會發現這幾層依然存在。只不過實現得復雜得多。Parser 和 Validator千萬不要混成一件事這個區別值得專門講一下。假設模型輸出{ action: destroy_planet, arguments: {} }這是合法 JSON 嗎當然是。所以ParserPASS但這是我們允許的 Decision 嗎不是。因為destroy_planet根本不屬于我們的 Action Space。所以ValidatorFAIL這就是為什么Valid JSON ≠ Valid Decision再比如{ action: get_weather, arguments: {} }JSON 完全正確。Action 也存在。但get_weather明明需要city。所以更嚴格的 Validator 還應該檢查if decision[action] get_weather: if city not in decision[arguments]: raise ValueError(get_weather requires city)這時候你開始進入真正的軟件接口設計了。你不是在問“模型有沒有大概理解我的意思”你是在問“這個輸出是否滿足系統契約”這兩個問題完全不是一個層次。一個特別值得做的實驗故意不給模型足夠信息現在用戶說幫我查一下天氣。注意。沒有城市。如果你的 Schema 只有get_weather final_answer模型會陷入一個很尷尬的狀態。它想調用get_weather。但參數不完整。怎么辦一個非常自然的解決辦法是增加ask_user于是 Action Space 變成get_weather query_order ask_user final_answer模型可以返回{ action: ask_user, arguments: { question: 你想查詢哪個城市的天氣 } }這個例子非常值得反復琢磨。因為這里第一次可以清楚地看到我們怎樣設計 Schema會直接決定 Agent 能采取什么行為。如果沒有ask_user模型沒有一種正式方式表達“信息不夠我需要問用戶。”增加ask_user以后這種行為突然成為了系統允許的一等 Action。也就是說Schema 并不只是描述輸出長什么樣它實際上參與定義 Agent 的能力邊界。這件事到了復雜 Agent 里會越來越明顯。再看一個企業場景你會更容易理解為什么這件事重要假設以后我們做一個企業內部 Agent。用戶說幫我查一下 PO-20260807-001 的狀態。一種設計是讓模型直接生成 SQL{ sql: SELECT * FROM orders WHERE id PO-20260807-001 }看起來很靈活。但是這種設計很快會帶來另外一個問題為什么模型一定會生成SELECT它也可能生成DELETE FROM orders ...甚至生成一段你完全沒有預料到的 SQL。于是更合理的設計通常不是讓 LLM 自由創造底層操作而是 Runtime 先定義安全的高層能力query_order模型只負責{ action: query_order, arguments: { order_id: PO-20260807-001 } }真正的 SQLSELECT ...由 Runtime 內部自己決定。這其實延續了第一課那個非常重要的思想Model 決定 What Runtime 控制 How模型可以判斷我要查詢這個訂單但數據庫應該怎么連、SQL 怎么寫、權限怎么控制、哪些字段能返回不應該默認全部交給模型自由發揮。所以 Structured Output 表面是在講輸出格式。繼續往下看你會發現它已經開始碰到權限邊界 API 設計 安全邊界 系統職責劃分這才是它真正有價值的地方。為什么我說 Structured Output 是 Model 和 Runtime 的“邊界協議”到這里我們終于可以把整個結構畫完整。第一課只有Context ↓ LLM ↓ Output第二課變成Context ↓ LLM ↓ Structured Output ↓ Parser ↓ Validator ↓ Decision再往下未來才會是Decision ↓ Runtime ↓ Tool ↓ Environment所以 Structured Output 恰好站在一個非常特殊的位置概率世界 │ LLM │ ↓ Structured Output │ ───────────┼─────────── Model / Runtime Boundary ───────────┼─────────── │ ↓ Validator │ Runtime │ Tool │ Environment 確定性世界上面是概率模型。下面是傳統軟件系統。中間必須有一個雙方都能理解的 Contract。否則下面的軟件永遠在猜模型剛才那句話到底什么意思所以如果一定要用一句話總結這一課我更喜歡這樣說Structured Output 的真正價值是把 LLM 的概率性輸出壓縮成軟件系統可以驗證和消費的確定性接口。這比“讓模型輸出 JSON”準確得多。還有一個很重要的原則Schema 不是越復雜越好當大家第一次意識到 Schema 可以控制模型輸出之后很容易開始設計這種東西{ thought: ..., reasoning: ..., analysis: ..., confidence: 0.87, intent: ..., action: ..., arguments: {}, explanation: ..., summary: ... }看上去信息特別豐富。但先問一個問題Runtime 真正需要什么如果 Runtime 最終只消費action arguments那么其他字段為什么一定要存在接口設計有一個非常樸素的原則只暴露真正需要暴露的東西。Structured Output 同樣如此。你不是在試圖把模型所有“思考”全部結構化而是在定義軟件下一步真正需要消費什么數據這兩個目標差別很大。好的 Schema 往往不是最大的 Schema。而是足夠表達決策 足夠驗證 沒有多余耦合這也會成為后面設計 Tool Schema 時非常重要的原則。到這里你其實已經摸到 Tool Calling 的本質了現在再看{ action: get_weather, arguments: { city: Shanghai } }有沒有一種非常熟悉的感覺它其實已經長得很像get_weather(cityShanghai)差的只剩下一步到底誰把前面的數據結構變成后面的真實函數執行答案當然不是模型。而是 Runtime。也就是說下一課我們只需要繼續增加一層Structured Decision ↓ Runtime ↓ 根據 action 找到函數 ↓ 傳入 arguments ↓ 真正執行例如概念上tools { get_weather: get_weather, query_order: query_order, }然后tool tools[decision[action]] result tool( **decision[arguments] )這時候才發生真正的Action而今天整整一課我們只解決Decision 應該怎樣被可靠地表達這個順序非常重要。因為一旦這層理解了下一課所謂的 Tool Calling 會突然祛魅。你會發現它并不是LLM 神奇地擁有了調用函數的能力而是Model 產生一個符合協議的 Tool Decision ↓ Runtime 解釋這個 Decision ↓ 真正執行函數而已。第二課學到這里應該形成一個新的腦內模型第一課結束時我們腦子里的 LLM 是LLM 根據 Context 生成 Output第二課結束之后應該再多一層理解LLM 可以根據 Context 生成符合特定 Contract 的 Decision于是 Agent 的結構開始慢慢長出來User ↓ Context ↓ LLM ↓ Structured Decision ↓ Parser / Validator注意。目前還不是 Agent。因為我們甚至還沒有真正執行任何 Tool。但一個非常重要的地基已經完成了Model 和 Runtime 終于有辦法可靠通信了。下一課我們只需要把 Runtime 接上去。第二課結束前我建議你真的想明白這幾個問題不用背定義。如果下面這些問題能夠不看文章自己解釋出來這一課基本就過了。模型回答我覺得應該先查詢上海天氣。為什么人類很容易理解但不適合作為 Runtime 的接口模型返回{ action: get_weather }為什么這并不意味著天氣已經被查詢為什么一個輸出可以是Valid JSON卻依然不是Valid DecisionParser 和 Validator 分別解決了什么問題當我們規定action ∈ { get_weather, query_order, ask_user, final_answer }實際上是在給 Agent 定義什么為什么讓模型返回query_order(order_id)通常比直接讓模型自由生成 SQL 更容易控制如果用戶只說幫我查天氣為什么ask_user本身也值得成為一個 Action最后也是最重要的一個Structured Output 和 Tool Calling 到底差在哪一步如果最后這個問題真正想清楚了第三課基本已經學會一半。最后把第二課壓縮成四句話第一句自然語言適合人與人交流但不是可靠的軟件協議。第二句Structured Output 不是“讓模型輸出 JSON”而是在 Model 和 Runtime 之間建立可驗證的 Contract。第三句Schema 不只是定義數據格式它還在定義模型允許選擇的 Action Space。第四句Structured Output 只負責表達 Decision真正把 Decision 變成 Action 的是 Runtime。現在再回頭看我們整個系列的進度第一課LLM ↓ Output第二課LLM ↓ Structured Decision下一課終于輪到LLM ↓ Structured Decision ↓ Runtime ↓ Tool ↓ Environment也就是下一課《Agent 原理三Tool Calling——模型根本沒有調用函數真正動手的是 Runtime》到了第三課我們第一次真正執行一個 Tool。然后把 Tool 的返回結果重新交給模型。再下一步一個真正的 Agent Loop 就會自然長出來。你會發現我們從頭到尾都沒有“發明”Agent。只是每遇到一個解決不了的工程問題就不得不增加一個組件。最后回過頭時一個 Agent 自己出現了。這也是這個系列真正想講清楚的東西。