跳到主要內容

AG Day 8 結構化輸出:Pydantic 契約與 JSON Schema

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 結構化輸出的三大技術路徑

回顧大型語言模型與結構化資料整合的發展歷程,業界逐步收斂出三種主流的實作路徑,理解它們的原理與適用邊界,是架構師做出精準技術選型的必備功課:

  1. 提示詞約束搭配後處理解析(Prompt + Post-Processing):這是最通用的歷史做法。在提示詞中詳細說明 JSON 結構範例,模型輸出純文字後,開發者在應用層呼叫 json.loads(),再傳入 Pydantic.model_validate()。這種做法的最大優點是不挑模型,無論是商用閉源模型還是地端私有部署的小型開源模型都能支援;但其致命缺陷在於語法失敗率高,且當輸出文字較長時極易在半途發生截斷,導致 JSON 解析失敗。
  2. 函式/工具呼叫路徑(Tool Calling / Function Calling):各大模型供應商為了支援代理架構,對模型進行了深度對齊訓練,使其具備將輸出轉換為特定工具呼叫引數的能力。在這種路徑下,我們把 Pydantic 模型的 JSON Schema 偽裝成一個工具宣告,強制模型在最後一步呼叫該工具。由於商用模型(如 GPT-4o 系列、Claude 3.5 系列)針對工具呼叫進行了強化學習微調,其格式遵循率大幅超越純文字提示詞。
  3. 原生語法約束解碼(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 時,工程師經常在以下幾個技術細節上踩雷:

  1. Pydantic v1 舊語法殘留:網路上大量教學文章仍沿用 Pydantic v1 語法。常見的致命混淆包括:使用 @validator(v2 已改為 @field_validator)、使用 .dict()(v2 已改為 .model_dump())、使用 .parse_raw()(v2 已改為 .model_validate_json())。在 Python 3.13 環境下混用舊語法,不僅會引發大量 DeprecationWarning,甚至可能導致驗證邏輯完全不被觸發。
  2. 可選欄位(Optional)未給予預設值:在 Pydantic 中,宣告 title: Optional[str] 與宣告 title: Optional[str] = None 有著天壤之別。前者代表「該欄位是必要的,但其值可以為 null」;後者才代表「該欄位是可選的,在缺少該鍵時預設為 None」。如果遺漏了 = None,導出的 JSON Schema 就會將其列在 required 清單中,當模型在 JSON 中漏掉該欄位時便會直接拋出 Field required 錯誤。
  3. OpenAI 原生約束解碼的 Schema 子集限制:OpenAI 的 Strict Structured Outputs 模式對輸入的 JSON Schema 有非常嚴苛的限制。例如:所有物件必須明確設定 additionalProperties: false;不可包含未在 required 陣列中列出的屬性(所有可選欄位都必須以 union 型別 ["string", "null"] 呈現並放進 required);不支援遞迴參照(Recursive Schema)。若手動撰寫 Schema 而未遵守規範,API 會直接退件並回傳 400 錯誤。
  4. 權杖上限配置不足引發的 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 生態如何封裝各家模型的結構化介面。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...