AG Day 8 結構化輸出:Pydantic 契約與 JSON Schema
執行需求:CPU+API key。如果暫時沒有 API key,本篇文章提供完整的離線 mock 與 dry-run 模式,不需要外部連線也能在本機完整驗證 Pydantic 契約解析、型別校驗與自我修復流程。
引言
在前幾篇的技術累積中,我們從 AG Day 5(原文連結)的函式呼叫基礎出發,逐步掌握了如何讓模型透過參數與外部工具溝通;在 AG Day 6(原文連結)中,我們親手刻劃了基礎代理迴圈,完成了多輪對話與工具反覆調度的原型;而在 AG Day 7(原文連結)中,我們更進一步探討了逾時、指數退避重試與工具呼叫失敗時的防禦性處理。然而,當我們開始正式建構貫穿全系列的「研究助理(research-agent)」時,純文字字串的輸出很快就會成為阻礙系統邁向生產環境的最大瓶頸。
試想一個真實的研究工作情境:當研究助理閱讀了一篇長達數萬字的論文或技術白皮書後,我們期望它能從中萃取出核心技術論點、對應的置信度分數、引用來源依據,並自動提出兩至三組具有延伸探索價值的關鍵字搜尋規劃。如果模型產出的是一段夾雜 Markdown 粗體、清單符號與客套寒暄的自然語言文字,後續負責資料儲存、向量檢索或派送新任務的下游模組,就必須撰寫極端複雜且脆弱的正則表達式來解析輸出。只要模型稍微改變了輸出格式、漏掉某個關鍵欄位,或者換了一種條列語氣,整條自動化資料管線就會在執行階段崩潰停擺。
要讓 AI Agent 具備工業級的可靠度與工程確定性,我們必須在「機率型自然語言生成」與「確定型軟體系統」之間築起一道強型別契約(Type Contract)。這道契約在現代 Python 工程生態系中就是結構化輸出(Structured Outputs)。透過 Pydantic v2 定義的資料模型,我們能夠將模糊的語言轉換為精確的資料物件;而 JSON Schema 則是大型語言模型與應用程式溝通時的標準契約規格。在本文中,我們將深入探索結構化輸出的運作底層,並為 research-agent 建立一套兼具型別校驗、錯誤回饋修正與離線測試的資料合約架構。
為什麼需要結構化輸出:從文字機率到型別契約
大型語言模型從本質上來說是自迴歸機率模型,它的核心工作是在給定前文脈絡的情況下,預測下一個最具關聯性或最高機率的字元權杖(token)。這項特性賦予了模型無與倫比的語言理解與文學創作彈性,但對於重視確定性、原子性與一致性的軟體工程而言,未經約束的自由文字代表著極高的資訊熵。關聯式資料庫需要固定的綱要(Schema),RESTful API 與 RPC 需要嚴格的欄位型別,而在分散式代理系統中傳遞的狀態更不能容許半點語意模糊。
在過去早期開發中,工程師通常依賴「提示詞工程(Prompt Engineering)」來約束模型輸出。例如在系統提示詞(system prompt)中強調「請務必僅輸出 JSON 格式,絕對不要包含任何前言或結尾文字」。然而,這種基於機率對齊的約束在面對長上下文、複雜推理或跨語言情境時極易破功。模型經常會產出帶有 Markdown 程式碼圍欄(如 ```json ... ```)的文字、在陣列結尾留下無效的尾隨逗號(trailing comma)、將數字加上引號變成字串,甚至自作主張地將欄位名稱替換成同義字。這些瑕疵都會導致標準 JSON 解析器直接拋出解析失敗的例外錯誤。
型別契約的核心哲學,是把輸出規格從「請模型遵守的建議」轉變為「系統運作的硬性門檻」。透過將業務資料結構化為明確的屬性、型別邊界與驗證條件,系統得以在邊界處對模型的輸出進行無情的審查。合法的資料順利轉化為內部 Python 物件流向下一道工序,不合法的資料則立即被截獲並觸發自癒重試。這正是現代代理工程告別玩具展示、走向企業級架構的關鍵轉捩點。
Pydantic v2 的核心機制:欄位校驗與 JSON Schema 生成
在 Python 3.13 與 3.14 時代,Pydantic v2 已經全面確立為 Python 世界定義資料模型的標準規格。Pydantic v2 經歷了底層重構,將核心解析引擎交由 Rust 撰寫的 pydantic-core 負責,使得序列化與校驗效能獲得了數倍至十數倍的提升。對於 AI Agent 系統而言,Pydantic v2 的核心價值體現在兩大維度:宣告式校驗器(Declarative Validators)與全自動的 JSON Schema 生成體系。
在 Pydantic 中,所有的資料實體都繼承自 BaseModel。開發者使用標準的型別提示(Type Hints)宣告屬性,並可透過 Field() 函式為欄位注入額外的元資料(metadata)。其中,Field(description=...) 扮演了至關重要的角色:這份文字描述不僅能作為程式碼本身的說明資訊,更會在轉換為 JSON Schema 時完整保留,直接成為大型語言模型在解碼時理解欄位意圖的最直接提示詞。
更進一步,Pydantic v2 提供了強大的裝飾器:@field_validator 用於處理單一欄位的清洗與過濾(例如自動去除字串前後空白、驗證網址格式、檢查清單元素是否重複);而 @model_validator(mode="after") 則可在所有欄位基本型別解析完成後,執行跨欄位的複合邏輯驗證(例如驗證開始日期必須早於結束日期、或者當置信度低於門檻時強制要求填寫質疑理由)。這些校驗規則直接在記憶體中運作,保證了任何進入核心業務邏輯的物件絕對純淨可靠。
LLM 結構化輸出的三大技術路徑
回顧大型語言模型與結構化資料整合的發展歷程,業界逐步收斂出三種主流的實作路徑,理解它們的原理與適用邊界,是架構師做出精準技術選型的必備功課:
- 提示詞約束搭配後處理解析(Prompt + Post-Processing):這是最通用的歷史做法。在提示詞中詳細說明 JSON 結構範例,模型輸出純文字後,開發者在應用層呼叫
json.loads(),再傳入Pydantic.model_validate()。這種做法的最大優點是不挑模型,無論是商用閉源模型還是地端私有部署的小型開源模型都能支援;但其致命缺陷在於語法失敗率高,且當輸出文字較長時極易在半途發生截斷,導致 JSON 解析失敗。 - 函式/工具呼叫路徑(Tool Calling / Function Calling):各大模型供應商為了支援代理架構,對模型進行了深度對齊訓練,使其具備將輸出轉換為特定工具呼叫引數的能力。在這種路徑下,我們把 Pydantic 模型的 JSON Schema 偽裝成一個工具宣告,強制模型在最後一步呼叫該工具。由於商用模型(如 GPT-4o 系列、Claude 3.5 系列)針對工具呼叫進行了強化學習微調,其格式遵循率大幅超越純文字提示詞。
- 原生語法約束解碼(Constrained Decoding / Native Structured Outputs):這是目前最先進且確定性最高的方案。雲端供應商在模型伺服器端部署了基於正規文法(Context-Free Grammar, CFG)或有限狀態機(FSM)的權杖過濾機制。在模型逐字預測權杖的解碼階段,系統會依據傳入的 JSON Schema 動態計算當前狀態下唯一合法的下一個權杖集合,並將所有不符合 JSON 語法的權杖機率強制設為負無限大(零機率)。代表性實作為 OpenAI 的
response_format={"type": "json_schema", "strict": True}。這種路徑能保證模型產出的字串百分之百具備合法的 JSON 語法,完全根除語法錯誤。
在我們的 research-agent 專案中,架構設計策略是:優先運用原生約束解碼以取得極致的格式確定性;同時保留清晰的例外攔截與修復對話流程,以便在未來切換至開源模型或工具呼叫時,系統依然具備高度的自我防禦能力。
完整實作:為 research-agent 建立嚴格的結構化抽取管線
現在,我們動手將結構化輸出整合進貫穿專案 research-agent 中。我們的目標是打造一個專職的文獻與事實萃取管線,將非結構化的長篇技術文章提煉成強型別的資料結構,為後續寫入 SQLite 資料庫(data/knowledge.db)與向量資料庫打下堅實基礎。專案目錄結構遵循規格,所有核心模組位於 src/research_agent/ 之下。
第一步,我們在 src/research_agent/schemas.py 中建立完整的資料模型定義。我們宣告三個核心實體:單一事實論點 ResearchClaim、後續檢索規劃 SearchQueryPlan,以及作為根容器的 ResearchFactExtraction:
"""src/research_agent/schemas.py:定義研究助理核心資料契約。"""
from typing import List, Optional
from pydantic import BaseModel, Field, field_validator, model_validator
class ResearchClaim(BaseModel):
"""單一研究論點與其客觀佐證資訊。"""
statement: str = Field(
...,
description="從文獻中提煉出的具體事實陳述,要求客觀、獨立且語意完整。",
min_length=5,
max_length=500,
)
confidence: float = Field(
...,
description="該論點的置信度分數,介於 0.0 至 1.0 之間。",
ge=0.0,
le=1.0,
)
source_citation: Optional[str] = Field(
default=None,
description="該論點引用的文獻作者、機構、網址或章節出處。",
)
keywords: List[str] = Field(
default_factory=list,
description="與此論點高度相關的技術關鍵字清單,至少需包含 1 個關鍵字。",
)
@field_validator("keywords")
@classmethod
def clean_and_verify_keywords(cls, value: List[str]) -> List[str]:
"""清理關鍵字字串並確保清單不為空。"""
cleaned = [k.strip() for k in value if k.strip()]
if not cleaned:
raise ValueError("關鍵字清單不得為空,必須提供至少一個有效技術標籤。")
return cleaned
class SearchQueryPlan(BaseModel):
"""根據當前研究缺口制定的延伸檢索規劃。"""
topic: str = Field(..., description="目前探討的核心技術主題。")
primary_queries: List[str] = Field(
...,
description="建議提交至搜尋引擎的高鑑別度搜尋字串,建議 2 到 4 組。",
min_length=1,
)
rationale: str = Field(
...,
description="為什麼挑選這幾組關鍵字進行深入檢索的技術論述說明。",
)
class ResearchFactExtraction(BaseModel):
"""結構化文獻提煉根容器契約。"""
title: str = Field(..., description="針對分析目標生成的清晰主題標題。")
claims: List[ResearchClaim] = Field(
...,
description="從文獻提煉出的核心論點串列。",
min_length=1,
)
search_plan: SearchQueryPlan = Field(
...,
description="針對文獻未知或待驗證面向的下一步檢索規劃。",
)
@model_validator(mode="after")
def verify_claims_volume(self) -> "ResearchFactExtraction":
"""驗證單次萃取的論點數量上限,避免單一物件過度膨脹。"""
if len(self.claims) > 25:
raise ValueError("單次萃取論點數量超出 25 筆上限,請分批提煉以維持品質。")
return self
第二步,我們撰寫一個檢驗腳本,檢查 Pydantic 模型導出 JSON Schema 的能力,並印出其關鍵節點以驗證是否符合規範:
"""檢驗 ResearchFactExtraction 的 JSON Schema 生成細節。"""
import json
from src.research_agent.schemas import ResearchFactExtraction
# 導出標準 JSON Schema 字典
schema_dict = ResearchFactExtraction.model_json_schema()
print("Schema 根層級標題:", schema_dict.get("title"))
print("根層級必要屬性清單:", schema_dict.get("required"))
print("子型別定義庫 keys:", list(schema_dict.get("$defs", {}).keys()))
# 檢查子模型 ResearchClaim 的欄位規範
claim_props = schema_dict["$defs"]["ResearchClaim"]["properties"]
print("置信度數值限制:", claim_props["confidence"].get("minimum"), "至", claim_props["confidence"].get("maximum"))
# 輸出(範例輸出):
# Schema 根層級標題: ResearchFactExtraction
# 根層級必要屬性清單: ['title', 'claims', 'search_plan']
# 子型別定義庫 keys: ['ResearchClaim', 'SearchQueryPlan']
# 置信度數值限制: 0.0 至 1.0
可以看到,Pydantic v2 自動為我們建立了型別參照系統($defs),將嵌套結構模組化,並準確地將 ge=0.0, le=1.0 轉換為 JSON Schema 的標準語法 minimum: 0.0, maximum: 1.0。這就是我們交付給模型的「考卷格式」。
第三步,我們在 src/research_agent/llm.py 模組中建立專門的結構化抽取函式。為了遵守全系列無金鑰讀者亦能通順執行的承諾,我們實作了完備的 dry-run 降級架構。當系統偵測到未配置 OPENAI_API_KEY 或指令帶有 dry_run=True 時,會自動回傳符合契約的確定型假資料,讓整套代理管線能離線運作:
"""src/research_agent/llm.py:結構化輸出提取器與離線降級實作。"""
import os
import json
from typing import Type, TypeVar
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
def _generate_mock_extraction(schema_cls: Type[T]) -> T:
"""在缺乏 API Key 或離線模式下提供保證符合契約的假資料。"""
mock_data = {
"title": "LangGraph 狀態機架構與分散式檢查點深度分析",
"claims": [
{
"statement": "LangGraph 採用圖結構(Graph)形式化多輪代理互動,克服了線性管線的表達限制。",
"confidence": 0.95,
"source_citation": "LangGraph Architecture Whitepaper 2025",
"keywords": ["LangGraph", "StateGraph", "DirectedGraph"]
},
{
"statement": "檢查點機制(Checkpointer)透過在每個節點邊界快照狀態,賦予系統持久化與時光旅行除錯能力。",
"confidence": 0.90,
"source_citation": "LangChain Core Engineering Report",
"keywords": ["Checkpoint", "Persistence", "TimeTravel"]
}
],
"search_plan": {
"topic": "LangGraph 分散式狀態最佳化",
"primary_queries": [
"LangGraph SQLite Checkpointer 並行寫入效能",
"Agent 狀態修剪與長期記憶體快取策略"
],
"rationale": "深入評估在大規模生產環境中,頻繁狀態持久化對整體回應延遲帶來的具體負載。"
}
}
return schema_cls.model_validate(mock_data)
def extract_structured(
prompt: str,
schema_cls: Type[T],
dry_run: bool = False,
) -> T:
"""呼叫 LLM 進行嚴格結構化萃取,支援原生約束與離線 dry-run。"""
model_name = os.getenv("RESEARCH_AGENT_MODEL", "gpt-4o-mini")
api_key = os.getenv("OPENAI_API_KEY")
# 觸發離線降級
if dry_run or not api_key:
print("[提示] 啟用離線 Mock 模式,跳過遠端 API 呼叫。")
return _generate_mock_extraction(schema_cls)
from openai import OpenAI
client = OpenAI(api_key=api_key)
# 採用 OpenAI 原生支援的 beta.chat.completions.parse API
response = client.beta.chat.completions.parse(
model=model_name,
messages=[
{
"role": "system",
"content": "你是一位專門分析技術文獻的高階研究助理,請嚴格遵守契約結構進行資訊萃取。",
},
{"role": "user", "content": prompt},
],
response_format=schema_cls,
)
parsed_object = response.choices[0].message.parsed
if parsed_object is None:
raise ValueError("模型回傳之內容無法解析為合法的結構化物件。")
return parsed_object
第四步,實作資料校驗失敗時的「自癒回饋修復機制(Self-Healing Loop)」。在非嚴格模式或自建模型伺服器的情境中,模型仍可能偶爾產出型別合法但違反自訂商務校驗規則的資料(例如信心值給了負數,或者漏掉了必要清單)。此時我們捕捉 ValidationError,並將結構化的錯誤清單傳回給模型進行針對性修正:
"""實作結構化驗證失敗時的自我修正迴圈。"""
from pydantic import ValidationError
def parse_with_self_healing(
raw_json_str: str,
schema_cls: Type[T],
max_repair_attempts: int = 2,
) -> T:
"""嘗試解析 JSON 字串,並在遇到 ValidationError 時模擬修復流程。"""
current_json = raw_json_str
for attempt in range(max_repair_attempts + 1):
try:
# 嘗試使用 Pydantic 進行字串解析
return schema_cls.model_validate_json(current_json)
except ValidationError as err:
print(f"[第 {attempt + 1} 次驗證攔截] 發現契約違規細項:")
for error_detail in err.errors():
loc = " -> ".join(str(p) for p in error_detail.get("loc", []))
print(f" 位置: {loc} | 訊息: {error_detail.get('msg')} (型別: {error_detail.get('type')})")
if attempt == max_repair_attempts:
raise RuntimeError(f"已達最大修復次數上限,無法修復資料: {err}")
# 在真實情境中,此處會將 err.errors() 組裝為提示詞再次呼叫模型
print("[修復中] 構造錯誤回饋提示詞,正在進行自我修復...")
# 模擬修復:將空關鍵字替換為合法預設標籤
current_json = current_json.replace('"keywords": []', '"keywords": ["自動修復標籤"]')
第五步,將所有元件組合在一起,撰寫完整的執行腳本以示範端到端文獻提煉流程:
"""執行研究文獻端到端結構化萃取示範。"""
from src.research_agent.schemas import ResearchFactExtraction
from src.research_agent.llm import extract_structured
research_document = """
LangGraph 在 2025 年第四季發表了 1.0 正式版,標誌著 Agent 架構正式轉向圖論狀態機設計。
傳統基於線性管線的代理系統在遇到複雜的多步決策、工具互動迴圈與人機協作核準時,程式碼結構往往陷入混亂。
LangGraph 透過 StateGraph 提供了宣告式的節點(Nodes)與邊(Edges)定義,並將狀態以 TypedDict 形式集中管理。
此外,透過內建的 Checkpointer 機制,所有狀態變更均能即時被寫入外部儲存(例如 SQLite),支援在系統崩潰後原地恢復。
生產環境中的主要挑戰在於如何降低大容量狀態在序列化過程中的延遲,並在多代理通訊時保持狀態隔離。
"""
prompt = f"請研讀以下研究文獻,並按照規範萃取核心論點與後續規劃:\n{research_document}"
# 執行結構化萃取(在此透過 dry_run 確保所有環境皆能順暢體驗)
extraction_result: ResearchFactExtraction = extract_structured(
prompt=prompt,
schema_cls=ResearchFactExtraction,
dry_run=True,
)
print("=" * 20, "萃取成果展示", "=" * 20)
print(f"主題: {extraction_result.title}")
print(f"提煉論點數量: {len(extraction_result.claims)} 則")
for i, claim in enumerate(extraction_result.claims, 1):
print(f" [{i}] {claim.statement}")
print(f" - 置信度: {claim.confidence}")
print(f" - 引用來源: {claim.source_citation}")
print(f" - 標籤: {', '.join(claim.keywords)}")
print(f"延伸檢索主題: {extraction_result.search_plan.topic}")
print(f"推薦檢索關鍵字: {extraction_result.search_plan.primary_queries}")
print(f"規劃理由: {extraction_result.search_plan.rationale}")
# 輸出(範例輸出):
# ==================== 萃取成果展示 ====================
# 主題: LangGraph 狀態機架構與分散式檢查點深度分析
# 提煉論點數量: 2 則
# [1] LangGraph 採用圖結構(Graph)形式化多輪代理互動,克服了線性管線的表達限制。
# - 置信度: 0.95
# - 引用來源: LangGraph Architecture Whitepaper 2025
# - 標籤: LangGraph, StateGraph, DirectedGraph
# [2] 檢查點機制(Checkpointer)透過在每個節點邊界快照狀態,賦予系統持久化與時光旅行除錯能力。
# - 置信度: 0.9
# - 引用來源: LangChain Core Engineering Report
# - 標籤: Checkpoint, Persistence, TimeTravel
# 延伸檢索主題: LangGraph 分散式狀態最佳化
# 推薦檢索關鍵字: ['LangGraph SQLite Checkpointer 並行寫入效能', 'Agent 狀態修剪與長期記憶體快取策略']
# 規劃理由: 深入評估在大規模生產環境中,頻繁狀態持久化對整體回應延遲帶來的具體負載。
第六步,我們為 schemas.py 撰寫一組正式的單元測試 tests/test_schemas.py,利用 pytest 檢驗模型邊界條件與自訂驗證邏輯,防範未來的回歸錯誤:
"""tests/test_schemas.py:針對 Pydantic 資料契約的嚴格單元測試。"""
import pytest
from pydantic import ValidationError
from src.research_agent.schemas import ResearchClaim, SearchQueryPlan, ResearchFactExtraction
def test_research_claim_boundary_success():
"""測試在合法邊界值下的資料建立。"""
claim = ResearchClaim(
statement="這是一條長度合法的客觀技術研究陳述。",
confidence=1.0,
source_citation="2025 技術白皮書",
keywords=["AI Agent", "Pydantic"],
)
assert claim.confidence == 1.0
assert len(claim.keywords) == 2
def test_research_claim_confidence_out_of_bounds():
"""測試超出範圍的置信度數值會被精確攔截。"""
with pytest.raises(ValidationError) as exc_info:
ResearchClaim(
statement="測試語句合規。",
confidence=1.05, # 違反 le=1.0
keywords=["測試"],
)
assert "Input should be less than or equal to 1" in str(exc_info.value)
def test_research_claim_empty_keywords_rejected():
"""測試自訂 field_validator 拒絕僅包含空白的無效清單。"""
with pytest.raises(ValidationError) as exc_info:
ResearchClaim(
statement="測試語句合規。",
confidence=0.8,
keywords=[" ", ""],
)
assert "關鍵字清單不得為空" in str(exc_info.value)
讀者可以在專案根目錄下透過以下 shell 命令列指令執行測試並驗證輸出:
# 執行單元測試以確認資料契約之正確性
uv run pytest tests/test_schemas.py -v
常見錯誤與踩雷
在生產環境中整合 Pydantic v2 與大型語言模型 JSON Schema 時,工程師經常在以下幾個技術細節上踩雷:
- Pydantic v1 舊語法殘留:網路上大量教學文章仍沿用 Pydantic v1 語法。常見的致命混淆包括:使用
@validator(v2 已改為@field_validator)、使用.dict()(v2 已改為.model_dump())、使用.parse_raw()(v2 已改為.model_validate_json())。在 Python 3.13 環境下混用舊語法,不僅會引發大量 DeprecationWarning,甚至可能導致驗證邏輯完全不被觸發。 - 可選欄位(Optional)未給予預設值:在 Pydantic 中,宣告
title: Optional[str]與宣告title: Optional[str] = None有著天壤之別。前者代表「該欄位是必要的,但其值可以為 null」;後者才代表「該欄位是可選的,在缺少該鍵時預設為 None」。如果遺漏了= None,導出的 JSON Schema 就會將其列在required清單中,當模型在 JSON 中漏掉該欄位時便會直接拋出Field required錯誤。 - OpenAI 原生約束解碼的 Schema 子集限制:OpenAI 的 Strict Structured Outputs 模式對輸入的 JSON Schema 有非常嚴苛的限制。例如:所有物件必須明確設定
additionalProperties: false;不可包含未在required陣列中列出的屬性(所有可選欄位都必須以 union 型別["string", "null"]呈現並放進 required);不支援遞迴參照(Recursive Schema)。若手動撰寫 Schema 而未遵守規範,API 會直接退件並回傳 400 錯誤。 - 權杖上限配置不足引發的 JSON 截斷:當萃取大量資料(例如包含十幾條論點的巢狀結構)時,生成的 JSON 字串體積可能遠超預期。若呼叫 API 時給予的
max_tokens太小,模型生成會在結構中途被強行切斷,導致最後一行遺漏了閉合括號,觸發JSONDecodeError: Unterminated string starting at line ...。在處理結構化任務時,務必預留足夠寬裕的輸出權杖配額。
效能與實務提醒
在享受強型別帶來的結構穩定性之際,我們也必須對其帶來的效能負擔與工程代價保持清醒的認知:
第一點是提示詞權杖的體積膨脹。一個結構完整、包含五到十個巢狀欄位的 Pydantic 模型,轉換為標準 JSON Schema 後可能佔用 300 到 1,000 個權杖。在多輪對話的 Agent 架構中,若每一輪對話都重複夾帶這份巨大的綱要定義,將會對整體 API 計費產生可觀的累積效應,並拉長整體的網路傳輸時間。在實務上,建議將「專門提煉結構化資料」的節點從「多輪自由對話」的節點中解耦出來,僅在需要持久化儲存時獨立執行單輪抽取。
第二點是模型伺服器端的延遲差異。原生約束解碼技術(例如 OpenAI Strict 模式)在第一次收到一個前所未見的 JSON Schema 時,伺服器端需要對語法自動機進行編譯構建,首個請求的首次權杖時間(TTFT)可能會出現一至三秒的延遲。不過,伺服器端隨後會快取該自動機,使得後續相同 Schema 的請求速度顯著加快。相比之下,純提示詞加後處理的方案雖然啟動迅速,但若頻繁驗證失敗觸發多輪修復,端到端的時間成本反而會成倍上升。
第三點是模型容量與結構複雜度的匹配度。較小的模型(例如參數量在 8B 以下的地端開源模型)在面對三層以上的巢狀 JSON 結構時,往往難以同時兼顧語法正確性與語意邏輯。如果系統的部署環境要求相容地端輕量推論引擎,架構師應主動將過於複雜的超級結構(Mega-Schema)拆解為多個扁平、專一的小型資料模型,透過循序漸進的管線分步提煉,以降低單次推論的認知負擔。
小結
在今天的內容中,我們為 AI Agent 工程實戰補齊了一塊核心拼圖——結構化輸出。我們探討了從不可預測的自然語言字串邁向工程確定型資料契約的必要性,並確立了以 Pydantic v2 作為契約中樞的架構設計。透過宣告式欄位與自訂校驗器,我們賦予了資料邊界自主防禦的能力;藉由原生的 model_json_schema() 轉換,我們打通了 Python 程式碼與 LLM 約束解碼之間的語意橋樑。
同時,我們在 research-agent 專案中實作了完整的文獻事實萃取模型,建立了兼顧雲端原生解析與離線 dry-run 降級的雙重執行管線,並展示了驗證失敗時的自我修正機制與單元測試。今天宣告的 ResearchFactExtraction、ResearchClaim 與 SearchQueryPlan,將會成為後續章節中寫入 SQLite 資料表(data/knowledge.db)的實體骨幹。
結語
有了穩固的資料契約後,我們的研究助理已經不再只是一個會說話的聊天機器人,而是能夠穩定輸出標準資料結構的專業資訊萃取引擎。然而,隨著代理執行的次數增加、呼叫的工具變多,軟體工程中最現實的維運挑戰接踵而來:這一次萃取到底花費了多少輸入與輸出權杖?整個網路往返與推論耗費了多少毫秒?我們如何為系統建立即時的成本監控與計量機制?
明天,我們會進入「AG Day 9 觀測基礎:token、延遲與成本紀錄」,為我們的 research-agent 加上完整的觀測能力,精確追蹤每一次呼叫的 token 消耗、延遲時間與計費成本,並將執行事件結構化記錄至 SQLite 資料庫中,為後續邁向 LangGraph 圖架構打下扎實的監控根基。
延伸資源
- Pydantic v2 官方文件:
https://docs.pydantic.dev/latest/。深入研讀 BaseModel、Field 宣告與最新校驗器最佳實踐。 - OpenAI Structured Outputs 開發者指南:
https://platform.openai.com/docs/guides/structured-outputs。了解伺服器端限制解碼與嚴格模式的底層原理。 - JSON Schema 官方規範標準(Draft 2020-12):
https://json-schema.org/。掌握現代資料交換與自動驗證的跨語言規格。 - LangChain 結構化輸出模組設計:
https://python.langchain.com/docs/concepts/structured_outputs/。參考 LangChain 生態如何封裝各家模型的結構化介面。
留言
張貼留言