構(gòu)化輸出實戰(zhàn):JSON Schema、Function Calling與容錯解析)
1. 聊聊結(jié)構(gòu)化輸出以及為什么你得重視它先按住鍵盤我問你一個問題當(dāng)你調(diào)用一個主流大模型讓它從一段產(chǎn)品評論里抽取“商品名稱、評價星級、問題描述”三個字段時模型返回的是什么大概率是一段帶了解釋的自然語言比如“我覺得這款耳機音質(zhì)很好但降噪一般我給四顆星”——聽起來沒問題可到了你的業(yè)務(wù)代碼里這段內(nèi)容沒法直接入庫沒法做統(tǒng)計分析更沒法觸發(fā)后續(xù)自動化處理。你需要的是類似下面這樣的東西{ product_name: 某耳機, rating: 4, issue: 降噪效果一般 }“LLM的結(jié)構(gòu)化輸出”這個命題正是在解決這類問題。它指的是通過一套可控的方法讓大模型輸出的內(nèi)容嚴(yán)格遵循預(yù)定義的格式通常是JSON、XML或表格而不是一句隨性的自然語言。說白了就是把一個“愛自由發(fā)揮的文科生”訓(xùn)成一個“按規(guī)范填表的質(zhì)檢員”。這個能力幾乎是一切LLM工程應(yīng)用的基石。你在做一個信息抽取、知識庫問答、Agent工具調(diào)用、報表生成、業(yè)務(wù)流程自動化不管表面多花哨最終落點都是“讓模型輸出能被程序直接吃掉的數(shù)據(jù)”。我見過太多項目演示階段跑得很好一上線就崩根因常常不是模型能力不夠而是輸出格式不穩(wěn)定下游解析邏輯脆得跟紙一樣。這篇文章會把這層?xùn)|西拆開結(jié)構(gòu)化輸出的幾種實現(xiàn)思路、怎么選型、完整的實操流程、以及那些你在文檔里很難查到的坑和恢復(fù)策略。適合正在做大模型應(yīng)用開發(fā)的人也適合剛?cè)腴T、想搞明白“為什么我加了JSON三個字母它還是給我回一篇小作文”的開發(fā)者。2. 結(jié)構(gòu)化輸出的主流實現(xiàn)路子各有各的脾氣2.1 提示詞約束最省事也最靠運氣最早期的做法就是你在System Prompt里拼命強調(diào)“請你只輸出JSON不要包含任何其他內(nèi)容”。有些模型聽話有些模型不聽話還有些模型在你請求里加了“必須”之后會把JSON包在一個Markdown代碼塊里或者在里面夾帶注釋。請從以下用戶評論中提取信息只輸出JSON格式不要輸出任何其他文字。 評論耳機音質(zhì)不錯但降噪一般我給四顆星。這類做法不是不能用它適合兩種場合第一模型夠強理解力到位第二任務(wù)簡單到模型出錯的可能性很小。但它最大的問題在于不可控你沒辦法從根本上阻斷模型輸出非JSON內(nèi)容。只要模型在生成過程中的某一步“跑偏”整段結(jié)果就報廢了。由于成本最低我現(xiàn)在仍然會在部分一次性、非關(guān)鍵路徑的任務(wù)里用它但凡是進(jìn)入生產(chǎn)環(huán)境、需要反復(fù)執(zhí)行的任務(wù)我不會把穩(wěn)定性寄托在提示詞上。2.2 Function Calling把輸出框在函數(shù)簽名里Function Calling有的文檔叫工具調(diào)用是模型廠商提供的一種能力你在請求里聲明一批函數(shù)的名稱、參數(shù)描述模型在生成時會先判斷該調(diào)用哪個函數(shù)然后返回一個結(jié)構(gòu)化的參數(shù)對象。舉個例子你要做一個天氣查詢工具{ name: get_weather, description: 查詢指定城市的天氣情況, parameters: { type: object, properties: { city: {type: string, description: 城市名稱如北京、上海}, date: {type: string, description: 日期格式為YYYY-MM-DD} }, required: [city, date] } }模型輸出會變成類似這樣{city: 北京, date: 2025-06-01}它比純提示詞強在模型廠商在訓(xùn)練階段就針對“按函數(shù)簽名輸出參數(shù)”做了對齊所以格式偏離的概率低得多。但要注意Function Calling本質(zhì)上是給“工具調(diào)用”設(shè)計的如果你只是想穩(wěn)定輸出一個業(yè)務(wù)數(shù)據(jù)結(jié)構(gòu)硬套函數(shù)定義會顯得很別扭——你造了一個并不存在的工具只為讓模型學(xué)會填表多少有點繞路。2.3 JSON Schema / 響應(yīng)格式約束生產(chǎn)環(huán)境里最常用的方案后來主流服務(wù)商陸續(xù)支持了“響應(yīng)格式限制”大多是基于JSON Schema的。你在請求里給出一份詳細(xì)的Schema服務(wù)商會在解碼階段確保輸出符合這個結(jié)構(gòu)。比如{ type: object, properties: { summary: { type: string, description: 一句話總結(jié)評論內(nèi)容 }, rating: { type: integer, minimum: 1, maximum: 5 }, issue: { type: string, enum: [音質(zhì), 降噪, 續(xù)航, 其他] } }, required: [summary, rating, issue] }如果我用一個支持這種約束的SDK調(diào)用模型最終拿到的結(jié)果大概率就是一個嚴(yán)格合法的JSON對象。不滿足Schema條件的token會在解碼過程中被拒絕這比事后解析要可靠得多。目前各家實現(xiàn)細(xì)節(jié)不同有的偏寬松只保證“是合法JSON”有的支持嚴(yán)格校驗?zāi)J綆缀跄茏龅桨俜种俜?。選型時優(yōu)先選帶“strict”或“schema”模式的接口哪怕稍微慢一點換來的是下游代碼省心很多。2.4 受限解碼 / 語法級約束沒有接口限制時的硬核路子如果沒有廠商提供的結(jié)構(gòu)化輸出接口或者你在跑本地開源模型那就需要另一種思路在生成時直接干預(yù)采樣過程把每一步可能輸出的token限制在“符合語法”的集合里。這類方案的代表是Outlines、Guidance等開源庫。它們會在生成前解析你的Schema或自定義語法甚至是一段模板構(gòu)建一個有限狀態(tài)機。模型每生成一個token庫會先過濾掉那些會讓結(jié)果偏離格式的候選token然后才做概率采樣。import outlines schema { type: object, properties: { summary: {type: string}, rating: {type: integer, minimum: 1, maximum: 5} }, required: [summary, rating] } generator outlines.generate.json(model, schema) result generator(請分析評論音質(zhì)不錯降噪一般四顆星)它的原理不復(fù)雜語言模型生成到“rating”的值位置時本來有10個、20個候選token但受限解碼會把范圍縮小到0-9這幾個數(shù)字。這樣一來輸出在誕生那一刻就是合法的根本不會出現(xiàn)“跑偏”的可能。代價是生成速度會受影響因為每次都要查詢狀態(tài)機、過濾token。但在本地部署場景里這個代價是值得的因為它換來的確定性極強。如果你用了大模型但一直沒有解決格式崩壞的問題試試這條路效果會讓你意外。3. 實操過程用結(jié)構(gòu)化輸出做一個穩(wěn)定的信息抽取接口3.1 場景定義與方案選型我以“用戶評論信息抽取”為例完整走一遍結(jié)構(gòu)化輸出的實現(xiàn)。目標(biāo)是從一批用戶評論中抽取出評論摘要一句話評分等級1-5的整數(shù)主要吐槽點限定了音質(zhì)、降噪、續(xù)航、舒適度、其他五個選項是否推薦購買布爾值需求明確后我直接選JSON Schema方案。為什么不用Function Calling因為我不需要觸發(fā)任何外部動作只是想拿到一份結(jié)構(gòu)化的業(yè)務(wù)數(shù)據(jù)。為什么不用受限解碼因為當(dāng)前項目用的是線上服務(wù)沒有直接改采樣過程的入口所以走服務(wù)商提供的Schema更實際。如果你在本地環(huán)境或者用開源模型完全可以切到受限解碼如果你們公司的內(nèi)部平臺只開放了Function Calling那就把它當(dāng)成一種“虛擬函數(shù)”來用只要結(jié)果穩(wěn)手段是可以遷移的。3.2 設(shè)計一份合理的JSON Schema很多人在這里容易翻車Schema寫得過于簡單模型即使“合規(guī)”也容易輸出垃圾內(nèi)容。比如你把issue字段設(shè)計成一個字符串不加枚舉模型可能輸出“這款耳機音質(zhì)雖然不錯但降噪有點拉胯”——這串內(nèi)容作為枚舉值完全沒法映射到業(yè)務(wù)分類上。我實踐下來的Schema設(shè)計原則有三條第一能用枚舉限定的字段絕不放開成自由文本。自由文本在解碼層面只能做類型校驗做不了內(nèi)容校驗枚舉則直接把輸出空間鎖死。第二字段描述寫清楚。很多人覺得Schema里的description是寫給人看的無所謂。但服務(wù)商的實現(xiàn)通常會把description作為Prompt的一部分喂給模型一句話描述寫得含糊模型對字段的理解就差一截。第三required字段不要漏。如果有一個字段在大多數(shù)任務(wù)里都該出現(xiàn)那就把它放進(jìn)required里防止模型“覺得沒必要填”就直接省掉?;谶@三點我把Schema定成{ type: object, properties: { summary: { type: string, description: 用不超過20個字概括這條評論的核心內(nèi)容 }, rating: { type: integer, description: 用戶對產(chǎn)品的整體評分1表示很差5表示很好, minimum: 1, maximum: 5 }, issue: { type: string, description: 用戶主要提及的問題類別只能從枚舉值中選擇, enum: [音質(zhì), 降噪, 續(xù)航, 舒適度, 其他] }, recommend: { type: boolean, description: 根據(jù)評論情感判斷用戶是否推薦購買 } }, required: [summary, rating, issue, recommend] }3.3 編寫Prompt與調(diào)用代碼這里有一個很多教程不會講的細(xì)節(jié)即使有了SchemaSystem Prompt也不能寫得太敷衍。Schema約束的是格式Prompt約束的是語義理解兩者配合才能同時解決“格式錯”和“理解偏”兩個問題。你是一個用戶評論分析助手。請閱讀用戶評論提取關(guān)鍵信息并按照給定的JSON Schema返回結(jié)果。 注意 1. 必須嚴(yán)格遵循Schema不要額外添加字段。 2. summary要簡潔不要復(fù)述整個評論。 3. issue只能從枚舉值里選不要自創(chuàng)分類。 4. 如果評論與產(chǎn)品無關(guān)將recommend設(shè)為false并在summary中說明。 用戶評論耳機戴著挺舒服續(xù)航也夠用但降噪效果一般坐地鐵還是能聽到噪音。價格性價比還行總體滿意。調(diào)用代碼import json from some_llm_sdk import Client, ResponseFormat client Client(api_keyyour-key-here) response client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: 你是一個用戶評論分析助手。請閱讀用戶評論提取關(guān)鍵信息并按照給定的JSON Schema返回結(jié)果。}, {role: user, content: 用戶評論耳機戴著挺舒服續(xù)航也夠用但降噪效果一般坐地鐵還是能聽到噪音。價格性價比還行總體滿意。} ], response_formatResponseFormat.json_schema(schemaschema) ) result json.loads(response.content) print(result)輸出{ summary: 佩戴舒適續(xù)航好但降噪一般性價比尚可, rating: 4, issue: 降噪, recommend: true }整個過程里最容易被忽略的其實是response_format參數(shù)。以前我圖省事只在Prompt里寫“請返回JSON”然后自己在代碼里做容錯。后來換成顯式聲明Schema后格式錯誤率從大約30%降到了個位數(shù)這是個質(zhì)的區(qū)別。3.4 后處理解析、校驗、重試的鐵三角哪怕結(jié)構(gòu)化輸出已經(jīng)很可靠也不能裸奔。任何模型服務(wù)都有極低概率抽風(fēng)網(wǎng)絡(luò)抖動返回空字符串、服務(wù)端更新導(dǎo)致行為微變、你的Schema寫得不嚴(yán)謹(jǐn)導(dǎo)致邊界情況。所以我在調(diào)用后面永遠(yuǎn)掛著一個三層后處理def safe_parse(response_text, schema): text response_text.strip() # 第一層嘗試直接解析 try: data json.loads(text) return validate(data, schema) except json.JSONDecodeError: # 第二層嘗試截取代碼塊或大括號范圍 text extract_json_block(text) data json.loads(text) return validate(data, schema) except Exception: # 第三層帶著錯誤信息重試一次 return retry_with_error_feedback(text)第一層是理想狀態(tài)第二層是常見意外兜底第三層則把錯誤信息拼回消息歷史讓模型自己“看到問題后修正”比重新空跑一次的召回率高得多。這里我多提一句重試不是無限重試設(shè)置最多2到3次就夠了。生產(chǎn)環(huán)境中每一輪重試都是錢和時間超過閾值寧可走“人工兜底”或“默認(rèn)值填充”也別讓接口卡死。4. 繞不開的坑常見格式錯誤、恢復(fù)策略與防錯設(shè)計4.1 我踩過的五種典型錯誤模式就算使用了結(jié)構(gòu)化輸出方案你還是可能遇到劣化輸入。常見錯誤我也整理成了一張表錯誤類型典型表現(xiàn)常見觸發(fā)原因恢復(fù)思路混入解釋文案JSON前面多了一段“好的這是結(jié)果”Prompt約束沒生效模型“話癆”模式正則定位第一個JSON起始符強行截斷JSON截斷輸出到一半達(dá)到token上限上下文太長或Schema太大增加輸出token上限或拆分任務(wù)字段名不一致要求輸出rating它給你stars描述不清、跨語言混用在Schema description里明確字段含義類型錯誤要求integer它輸出4模型從文本里提取了帶引號的值標(biāo)注類型說明后處理統(tǒng)一轉(zhuǎn)類型空值濫用該輸出空數(shù)組它輸出nullSchema沒有專門定義數(shù)組字段明確字段類型為array并在Prompt里強調(diào)“無結(jié)果時返回[]”每一種我都實際遇到過。尤其是字段名不一致這個最容易忽略很多模型內(nèi)部偏好用更自然的英文單詞你Schema里寫的字段名如果太生僻比如prod_score它生成的JSON里就可能改名成product_score。所以Schema的字段命名要么貼近常識要么在description里反復(fù)點名。4.2 第一道防線用正則和容錯解析兜底當(dāng)模型沒走結(jié)構(gòu)化接口、或者返回格式被包裝過了我一般用一個“抓取修復(fù)”的組合套路。抓取的核心思路不全文做JSON解析而是先定位最外層的大括號或中括號范圍把可能的前綴、后綴雜物全部丟掉。最簡單的方法是找第一個{或[然后從末尾往前找匹配的閉合符。像下面這段怪東西當(dāng)然可以以下是你需要的JSON json { rating: 4, issue: 降噪 }希望對你有所幫助第一步去掉Markdown代碼塊標(biāo)記第二步去掉外圍自然語言第三步才做json.loads。順序不能反一旦先把整段文本丟進(jìn)json.loads拋出的異常反而會打斷你本來可以做容錯的流程。 還有一種情況是JSON里混入了尾逗號比如rating: 4,}。有些修復(fù)庫比如json_repair這類工具能幫你修正如果修復(fù)效果不穩(wěn)定更穩(wěn)的做法是截取合法前綴重新解析。 ### 4.3 第二道防線帶錯誤信息的重試機制 重試不是簡單重跑一遍。瞎重試等于花同樣的成本再賭一次運氣。輕量做法是把上一次的錯誤信息拼到下一次請求里 python messages [ {role: system, content: system_prompt}, {role: user, content: origin_input} ] if last_error: messages.append({ role: user, content: f你上次返回的內(nèi)容存在以下問題{last_error}。請重新生成確保完全符合給定的JSON格式。 })這樣做的效果比重新空跑高很多因為模型拿到了“哪里錯了”的反饋會主動修正。實測下來這種帶錯誤反饋的重試一次成功率達(dá)到90%以上。在Agent場景里這個方法經(jīng)常配合日志回放讓模型“看到環(huán)境反饋再行動”本質(zhì)是同一招。4.4 第三道防線業(yè)務(wù)側(cè)的兜底與降級不管前面的機制多完美總會有極端情況。生產(chǎn)系統(tǒng)必須做好“模型輸出不可用時業(yè)務(wù)不至于掛”的設(shè)計。我的做法是分三層降級第一層默認(rèn)值填充。比如rating解析失敗時默認(rèn)給0并在日志里標(biāo)記為異常數(shù)據(jù)。第二層人工審核隊列。凡是結(jié)構(gòu)化解析失敗的原始文本全部進(jìn)一個“待人工處理”的列表寧可讓人看一眼也不要讓臟數(shù)據(jù)入庫。第三層緩存和兜底答案。同一個輸入重復(fù)多次失敗直接返回一個事先寫好的靜態(tài)結(jié)果避免每次都燒成本。這三個習(xí)慣聽著不復(fù)雜但絕大多數(shù)線上事故都出在“沒做兜底”這一個小細(xì)節(jié)上。結(jié)構(gòu)化輸出只能降低概率業(yè)務(wù)設(shè)計才能控制風(fēng)險。5. 關(guān)于Schema設(shè)計與調(diào)參我的幾條實踐心得5.1 溫度別調(diào)太高但也不建議調(diào)到0很多人一聽“結(jié)構(gòu)化輸出不穩(wěn)定”第一反應(yīng)是把temperature調(diào)到0。方向沒錯但不絕對。過低的溫度會讓模型輸出變得機械在字段語義理解上反而可能變得死板尤其是涉及主觀判斷的字段比如“推薦是否購買”。我一般把temperature設(shè)在0.1到0.3之間。這個區(qū)間既能把隨機性壓住又保留了模型對語義的判斷余量。還有一點如果服務(wù)支持seed參數(shù)配合固定seed可以進(jìn)一步提升可復(fù)現(xiàn)性排障的時候更舒服。5.2 Schema字段數(shù)量不是越多越好我見過一個同事為了讓模型輸出盡量信息豐富一口氣定義了四十多個字段。結(jié)果就是模型經(jīng)常把不相關(guān)的內(nèi)容填進(jìn)某些字段或者為湊滿字段開始“編”。結(jié)構(gòu)化輸出的復(fù)雜度與穩(wěn)定性是反比關(guān)系字段越多約束越多模型犯錯的概率也越高。實用做法是把必需字段控制在十個以內(nèi)。太復(fù)雜的提取需求拆成多個小任務(wù)調(diào)用每個任務(wù)只負(fù)責(zé)一小塊。有人會覺得這樣調(diào)用次數(shù)多、成本高但你把重試成本加進(jìn)去算一筆賬反而更劃算。5.3 把“格式約束”和“語義質(zhì)量”分開治理這句話值得刻在電腦邊上結(jié)構(gòu)化輸出解決的是“形狀對不對”不等于“內(nèi)容對不對”。你拿到的JSON完全合規(guī)但summary可能寫得很差rating可能理解錯了。所以校驗邏輯要分兩層格式層用Schema語義層用業(yè)務(wù)規(guī)則甚至傳統(tǒng)模型評分。比如recommend字段是布爾值但模型基于一條負(fù)面評論給出true這時候格式毫無問題語義已經(jīng)錯了。這類問題不能靠加Schema求解只能通過few-shot示例、更多上下文控制、以及針對性的后置校驗來解決。我在生產(chǎn)里常用一個輕量做法在Schema之外再加一個斷言列表針對業(yè)務(wù)規(guī)則做硬性校驗不滿足就歸入“需要重試”的類別。5.4 留意輸出token上限與Schema長度的關(guān)系大模型輸出token有上限Schema本身也會占用一定的上下文和輸出預(yù)算。當(dāng)Schema很大、說明很詳細(xì)時留給實際內(nèi)容的token反而變少。尤其在做長文本抽取時摘要字段還沒填完就截斷了。解決方式要么壓縮description長度要么最大化輸出預(yù)算要么把任務(wù)拆得更細(xì)。我的經(jīng)驗是Schema里的description盡量精簡到一兩句話太長的描述不僅浪費token還可能影響模型注意力的分配。它看一段又臭又長的字段說明時理解效果反而更差。做了這些年LLM應(yīng)用我的體感是結(jié)構(gòu)化輸出是少數(shù)“前期稍微多花點心思后期能省掉一大堆麻煩”的環(huán)節(jié)。它不性感不像多輪對話和Agent那么抓眼球但絕大多數(shù)經(jīng)得起生產(chǎn)考驗的LLM項目都在這個不起眼的環(huán)節(jié)上下了笨功夫。下次你的項目再遇到模型輸出不穩(wěn)定先別急著換更大參數(shù)的模型回到你這邊的格式約束和Schema上看看很多答案就藏在那里。