MCP是手腳,Skill是靈魂· 第二篇:給Skill穿上鎧甲

作者:努力撞蘑菇AI
日期:2026年5月26日 上午6:55
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

Pydantic 為 Skill 加上類型驗證層,防止 MCP 輸出陷阱導致數據損毀

整理版摘要

呢篇文章由一個真實事故開始:朋友嘅 Agent 因為 MCP 工具輸出類型混淆,誤刪曬成個 Notion 數據庫。作者指出 MCP 本質係讓 AI 調用外部工具拎數據,但外部世界混亂、唔可靠——字段可能缺失、類型可能亂、null 變體多、嵌套結構可以成層係 None。如果 Agent 冇對呢啲數據做類型校驗,就好似高速公路上唔扣安全帶,隨時出大事。

文章詳細拆解咗 MCP 輸出嘅五大危險模式:字段缺失、類型混淆、嵌套結構、null 變體、分頁截斷,每個都可能導致 Agent 崩潰或做錯不可逆操作。作者認為解決方案係用 Pydantic——Python 最流行嘅數據驗證庫——幫 Skill 整件「鎧甲」。透過定義明確嘅 BaseModel,可以自動驗證、轉換、兜底,令業務邏輯唔再受 KeyError 或 TypeError 威脅。

文中用五個真實踩坑場景做對比,展示 Pydantic 點樣優雅處理字段唔統一、數字字串混淆、嵌套 None、列表元素類型混雜、AI 輸出 JSON 格式唔正等問題。最後提出工程化方法:建立通用守衞層(MCPGuard)統一封裝輸出驗證,並喺 Skill 內實踐三層防禦(MCP 輸出驗證 → AI 文字解析驗證 → 業務語義驗證)。作者仲建議大家將常用 MCP 工具嘅輸出模型沉澱成 mcp_types 庫,呢個目錄會成為整個 Agent 工程最寶貴嘅資產。

  • MCP 輸出充滿類型陷阱,必須用 Pydantic 驗證保護 Skill,否則隨時導致數據損毀或系統崩潰。
  • Pydantic 定義模型,可以自動校驗字段類型、提供默認值、處理 null 變體,減少 KeyErrorTypeError
  • 對比脆弱版代碼(7 行 4 個崩潰點)同鎧甲版(零 KeyError,錯誤有據可查),顯出驗證層嘅重要。
  • 統一守衞層 MCPGuard 封裝輸出驗證,配合三層防禦(MCP 輸出、AI JSON、業務語義)確保每步都安全。
  • 建立 mcp_types 庫沉澱常用 MCP 工具嘅輸出模型,每次踩坑就加驗證,三個月後成為團隊最寶貴資產。
值得記低
筆記

GitHubIssueList 模型定義

GitHubIssue、GitHubUser、GitHubLabel、GitHubIssueList 嘅 Pydantic 模型,用嚟驗證 GitHub MCP 輸出。

筆記

MCPGuard 統一守衞層

一個通用類 MCPGuard,提供 call 方法同 guarded 裝飾器,統一驗證 MCP 工具輸出,支援 strict_mode。

筆記

safe_parse_ai_json 函數

處理 AI 輸出 JSON 格式唔正嘅問題:先提取 JSON、清除 Markdown 代碼塊、移除註釋同尾隨逗號,再用 Pydantic 驗證。

整理重點

一次驚心動魄嘅事故:Agent 清空咗成個數據庫

朋友深夜傳來噩耗:「我嘅 Agent 將我成個 Notion 數據庫清空咗。」原因好荒唐:佢寫咗個自動整理文檔嘅 Agent,Skill 指令入面有句「刪除重複內容」。Agent 調用 MCP 讀取 Notion 數據庫,拎返嚟嘅 JSON 裏面某個字段係字串 "null",而唔係真正嘅 null。佢段判斷 if item["content"] is None 跳過咗,但後續去重邏輯因為冇做類型校驗,將一批格式異常嘅條目當作重複項,批量 delete。幾百條筆記就咁消失咗。

整理重點

MCP 輸出點解特別危險?

MCP 嘅本質係讓 AI 模型調用外部工具,拎取外部數據。呢啲工具(GitHub APINotion API、數據庫查詢等)有個共同特點:輸出格式唔受你控制。例如 GitHub API 有時 assignee 係 object,有時係 null,有時係空 array——視乎呢個 Issue 有冇被分配。

更危險嘅係 AI 本身。當你嘅 Skill 叫 AI「解析MCP 輸出,AI 會做推斷:字段唔見咗就「合理填充」一個佢認為合理嘅值;格式唔啱就悄悄轉換類型;數據為空就「忽略」繼續行。呢啲「智能」行為喺正常場景好貼心,但一涉及刪除、修改、發送等不可逆操作,就係災難。

MCP 輸出五大危險模式

  • 字段缺失 → AI 推斷填充 → 用錯誤值觸發錯誤邏輯
  • 類型混淆 → "123" vs 123 → 條件判斷出錯
  • 嵌套結構 → 期望 dict 收到 list → 屬性訪問拋異常
  • null 變體 → null / None / "null" / "" → 判空邏輯失效
  • 分頁截斷 → 數據只拎第一頁 → AI 以為係全部,決策錯誤
整理重點

Pydantic 點樣幫 Skill 穿上鎧甲?

PydanticPython 生態最流行嘅數據驗證庫,核心係用類型註解定義數據結構,然後解析、驗證、轉換外部數據。

Pydantic 之前,處理 MCP 輸出嘅代碼好脆弱:直接 mcp_output["data"]["issues"],隨時 KeyError;assignee 可能係 null,labels 可能空咗,任何一個條件出事程序即崩潰。如果呢個函數已經做咗一啲有副作用嘅操作(例如發咗一半通知),你連邊啲發咗邊啲未發都搞唔清。

脆弱版 vs 鎧甲版對比 python
# 脆弱版:沒有任何防禦
def process_github_issues(mcp_output):
 issues = mcp_output["data"]["issues"] # KeyError 風險
 for issue in issues:
 assignee = issue["assignee"]["login"] # assignee 可能是 null!
 priority = int(issue["labels"][0]["name"]) # labels 可能為空!
 if priority > 5:
 send_notification(assignee, issue["title"])

# 鎧甲版:先定義模型,再驗證
from pydantic import BaseModel, Field, validator
from typing import Optional, List

class GitHubIssue(BaseModel):
 id: int
 number: int
 title: str
 state: str = "open"
 assignee: Optional[GitHubUser] = None
 labels: List[GitHubLabel] = []
 @validator('state')
 def validate_state(cls, v):
 if v not in ['open', 'closed']:
 raise ValueError(f'Invalid state: {v}')
 return v
class GitHubIssueList(BaseModel):
 issues: List[GitHubIssue] = []
 total_count: int = 0
 page: int = 1
 has_more: bool = False

def process_github_issues(mcp_output):
 try:
 issue_list = GitHubIssueList(**mcp_output.get("data", {}))
 except ValidationError as e:
 logger.error(f"MCP 輸出格式異常: {e}")
 return ProcessResult(success=False, error=str(e))
 notifications_sent = []
 for issue in issue_list.issues:
 if issue.assignee is None: # 要先判斷,IDE 會提示
 continue
 priority_labels = [l for l in issue.labels if l.name.startswith("P")]
 if not priority_labels:
 continue
 send_notification(issue.assignee.login, issue.title)
 notifications_sent.append(issue.number)
 return ProcessResult(success=True, notifications_sent=notifications_sent, total_processed=len(issue_list.issues))

對比之下,脆弱版 7 行核心邏輯有 4 個潛在崩潰點,0 個有意義嘅錯誤信息;鎧甲版雖然多咗模型定義,但業務邏輯零 KeyError 風險,錯誤有據可查,行為完全可預期。

整理重點

五大真實踩坑場景與 Pydantic 解法

場景一:字段名對唔上。唔同 MCP 工具回傳嘅字段名可以好唔同,有時係 title,有時係 name,有時係 headline。脆皮直接 result["title"] 會 KeyError。鎧甲版用 @classmethod from_flexible_input 逐個嘗試,最後兜底成「(無標題)」。

場景二:數字被當成字串傳過嚟。價錢係 "129.00" 而唔係 129,做乘法時直接 crash。PydanticDecimal 或 float 類型自動強制轉換,完全唔使寫額外邏輯。

其餘三個場景簡介

  • 場景三:嵌套結構某層係 None,例如 response["user"]["profile"]["avatar_url"]——profile 可能係 None。用 Optional 嵌套同埋 property 封裝,確保永遠有兜底值。
  • 場景四:列表入面混咗唔同類型元素,例如搜索結果有文章同廣告。用 Union[ArticleItem, AdItem] 加 Literal 鑑別字段,Pydantic 自動區分,可以過濾出純文章。
  • 場景五:AI 輸出 JSON 格式唔正,例如有 Markdown 代碼塊、註釋、尾隨逗號。用 safe_parse_ai_json 先清洗再驗證,確保唔會 SyntaxError
整理重點

構建通用守衞層與三層防禦

五個場景係單點解決方案,真正工程化係做一個通用守衞層 MCPGuard。佢封裝咗 call 方法同 guarded 裝飾器,所有 MCP 工具輸出都先經過呢層驗證,回傳統一嘅 MCPCallResult,包含 success、data、error、error_type、raw_output 同 call_duration_ms。

MCPGuard 核心實作 python
class MCPGuard:
 def __init__(self, strict_mode: bool = False):
 self.strict_mode = strict_mode
 def call(self, mcp_tool_func, output_model: Type[T], *args, **kwargs) -> "MCPCallResult[T]":
 start_time = time.time()
 try:
 raw_output = mcp_tool_func(*args, **kwargs)
 except Exception as e:
 return MCPCallResult(success=False, error=str(e), error_type="api_error", call_duration_ms=...)
 try:
 if isinstance(raw_output, dict):
 validated_data = output_model(**raw_output)
 elif isinstance(raw_output, list):
 validated_data = output_model(items=raw_output)
 else:
 validated_data = output_model.parse_raw(str(raw_output))
 return MCPCallResult(success=True, data=validated_data, call_duration_ms=...)
 except ValidationError as e:
 if self.strict_mode: raise
 return MCPCallResult(success=False, error=str(e), error_type="validation", raw_output=raw_output)
 def guarded(self, output_model: Type[T]):
 def decorator(func):
 @wraps(func)
 def wrapper(*args, **kwargs):
 return self.call(func, output_model, *args, **kwargs)
 return wrapper
 return decorator

將守衞層集成到 Skill 後,需要三層防禦:第一層 MCP 輸出驗證、第二層 AI 文本 JSON 解析驗證、第三層業務語義驗證。任何一層失敗都唔會令程序崩潰,只會 return None 同埋紀錄錯誤。

最後作者建議建立 mcp_types 庫,將常用 MCP 工具嘅輸出模型沉澱落去。每次踩咗新坑就喺對應類型文件加驗證,三個月後呢個目錄會變成整個 Agent 工程最寶貴嘅資產——因為佢記錄曬所有外部世界嘅混亂,同埋你點樣馴服佢哋嘅智慧。

開場:一次嚇到背脊發涼嘅事故

舊年某個深夜,有位朋友send message過嚟:「我個Agent將我個Notion數據庫清空咗。」

起因真係好荒謬。

佢寫咗一個自動整理文件嘅Agent,Skill指令入面有一句——「刪除重複內容」。Agent調用MCP讀取咗Notion數據庫,return咗一個JSON對象。但呢個JSON嘅格式同佢預期嘅唔一樣——有個field return嘅係字串 "null",而唔係真正嘅 null

佢嘅code入面有一行判斷:

if item["content"isNone:    delete(item)

字符串 "null" 不等於 None,所以就skip咗呢個判斷。但後面嘅去重邏輯,因為冇對數據類型做校驗,將一批格式異常嘅entry全部當成「重複項」處理,一次過call咗delete接口。

一個鐘之後,佢個Notion數據庫入面幾百條notes,全部消失曬。

呢個唔係Skill指令嘅問題,而係類型防禦嘅問題。

MCP工具嘅output,本質上係一堆「嚟自外面世界嘅數據」。外面世界係混亂、唔可信、充滿驚喜嘅。你個Agent如果冇對呢啲數據做類型校驗,就等於喺高速公路揸車,但係掉咗安全帶。

今日我哋就嚟講,點樣用Pydantic幫Skill著返件真正嘅甲。圖片


點解MCP output特別危險?

講解決方案之前,我哋先搞清楚問題嘅根源。

MCP嘅本質係:俾AI模型可以調用外部工具,攞外部數據。呢啲外部工具可能係GitHub API、Notion API、數據庫查詢、文件系統操作、Shell command……

佢哋嘅共同特點係:output格式唔受你控制。

你調用GitHub API,有時 assignee field係一個object,有時係 null,有時係一個空array——取決於呢個Issue有冇被分配,取決於GitHub呢個版本嘅API行為,甚至取決於網絡timeout導致return咗錯誤格式嘅response。

更加危險嘅係AI本身。當你個Skill叫AI「解析」某個MCP工具嘅output時,AI會做「推斷」。推斷即係話:

  • 如果field missing,AI可能會「合理地填返」一個佢認為合理嘅值
  • 如果格式唔啱,AI可能會「做格式轉換」,將字串偷偷變做數字
  • 如果數據係空,AI可能會「ignore」呢個問題,繼續行落去

呢啲「智能」行為,喺正常情況下睇落好貼心。但係講到刪除、修改、發送呢啲不可逆操作嗰陣,就係災難嘅開始。

MCP output嘅五大危險模式:

1. 字段缺失   → AI 推斷填充 → 用錯誤的值觸發了錯誤的邏輯2. 類型混淆   → "123" vs 123 → 數值比較失效,條件判斷出錯3. 嵌套結構   → 期望 dict 卻收到 list → 屬性訪問拋出異常,流程中斷4. null 變體  → null / None / "null" / "" → 判空邏輯全部失效5. 分頁截斷   → 數據只返回了第一頁 → AI 以為這就是全部,決策錯誤

而家你明白問題有幾嚴重,我哋嚟睇解決方案。


咩係Pydantic,點解佢係最佳選擇?

Pydantic係Python生態入面最流行嘅數據驗證library,冇之一。佢嘅核心思想係:用Python類型註解去定義數據結構,然後用佢嚟解析、驗證、轉換外部數據。

未有Pydantic之前,處理MCP output嘅典型code係咁:

# 脆弱版:沒有任何防禦defprocess_github_issues(mcp_output):    issues = mcp_output["data"]["issues"]          # KeyError 風險    for issue in issues:        assignee = issue["assignee"]["login"]      # assignee 可能是 null!        priority = int(issue["labels"][0]["name"]) # labels 可能為空!        if priority > 5:            send_notification(assignee, issue["title"])

段code喺正常數據下可以行到,但係:

  • 如果 data field唔存在?→ KeyError
  • 如果 assignee 是 null?→ TypeError: 'NoneType' object is not subscriptable
  • 如果 labels 係空array?→ IndexError: list index out of range
  • 如果第一個label個名唔係數字?→ ValueError: invalid literal for int()

任何一個條件觸發,program crash,Agent停咗,task失敗。如果呢個function call咗有side effect嘅操作(例如send咗一半嘅notification),你甚至唔知邊啲send咗、邊啲未send。

有咗Pydantic之後:

from pydantic import BaseModel, Field, validatorfrom typing import Optional, ListclassGitHubUser(BaseModel):    login: str    id: int    avatar_url: Optional[str] = NoneclassGitHubLabel(BaseModel):    id: int    name: str    color: str = "000000"   # 默認值兜底classGitHubIssue(BaseModel):    id: int    number: int    title: str    state: str = "open"    assignee: Optional[GitHubUser] = None# 明確標註可以為 None    labels: List[GitHubLabel] = []         # 默認空列表    @validator('state')    defvalidate_state(cls, v):        if v notin ['open''closed']:            raise ValueError(f'Invalid state: {v}')        return vclassGitHubIssueList(BaseModel):    issues: List[GitHubIssue] = []    total_count: int = 0    page: int = 1    has_more: bool = False

有咗呢個model,處理邏輯變成:

defprocess_github_issues(mcp_output):    try:        issue_list = GitHubIssueList(**mcp_output.get("data", {}))    except ValidationError as e:        logger.error(f"MCP 輸出格式異常: {e}")        return ProcessResult(success=False, error=str(e))    notifications_sent = []    for issue in issue_list.issues:        if issue.assignee isNone:  # 必須先判斷,IDE 會提示你            continue        priority_labels = [l for l in issue.labels if l.name.startswith("P")]        ifnot priority_labels:            continue        send_notification(issue.assignee.login, issue.title)        notifications_sent.append(issue.number)    return ProcessResult(        success=True,        notifications_sent=notifications_sent,        total_processed=len(issue_list.issues)    )

對比嚇:

  • 脆弱版:7行核心邏輯,4個潛在crash位,0個有意義嘅error message
  • 鎧甲版:多咗model定義,但業務邏輯入面零個KeyError風險,錯誤有據可查,行為完全可預期

五大真實踩坑場景與Pydantic解法

等我哋行過五個最常見嘅MCP output陷阱,每個都有「踩坑版」同「甲版」對比。

場景一:field名對唔上

踩坑情境:你個Skill叫AI從search result提取「文章標題」,AI return嘅field有時叫 title,有時叫 name,有時叫 headline——唔同MCP工具field名唔統一。

# 踩坑版article_title = result["title"]   # 有時候 KeyError# 鎧甲版:兼容多種字段名classArticleResult(BaseModel):    title: str    @classmethod    deffrom_flexible_input(cls, data: dict) -> "ArticleResult":        title = (            data.get("title"or            data.get("name"or            data.get("headline"or            data.get("subject"or            "(無標題)"   # 最終兜底        )        return cls(title=title)# 使用:永遠不會 KeyErrorarticle = ArticleResult.from_flexible_input(raw_mcp_output)print(article.title)

場景二:數字被當成字串傳過嚟

踩坑情境:某個MCP工具return嘅價錢係 "129.00"(字串),你嘅code拎佢嚟做數值計算,同另一個工具return嘅 129(整數)做比較時直接crash。

# 踩坑版price = api_response["price"]discount = api_response["discount"]final_price = price * (1 - discount)  # 字符串不能乘法!# 鎧甲版:Pydantic 自動做類型強制轉換from decimal import DecimalclassPriceData(BaseModel):    price: Decimal      # 自動把 "129.00" 轉成 Decimal(129.00)    discount: float     # 自動把 "0.1" 轉成 0.1    currency: str = "CNY"    @property    deffinal_price(self) -> Decimal:        return self.price * Decimal(str(1 - self.discount))price_data = PriceData(price="129.00", discount="0.1")print(price_data.final_price)   # Decimal('116.100')  ✅

場景三:嵌套結構入面某一層係None

踩坑情境:你想access response["user"]["profile"]["avatar_url"],但 profile 呢一層可能係 null——新註冊用戶未填資料。

# 踩坑版:三層訪問,任何一層為 None 就崩潰avatar = response["user"]["profile"]["avatar_url"]   # 💥# 鎧甲版:Optional 嵌套 + 屬性封裝classUserProfile(BaseModel):    avatar_url: Optional[str] = None    bio: Optional[str] = NoneclassUserData(BaseModel):    id: int    username: str    profile: Optional[UserProfile] = None   # 整個 profile 可以是 None    @property    defavatar_url(self) -> str:        if self.profile isNone:            return"https://example.com/default-avatar.png"        return self.profile.avatar_url or"https://example.com/default-avatar.png"# 使用:絕對不會崩潰user = UserData(**response["user"])show_avatar(user.avatar_url)

場景四:list入面混咗唔同類型嘅元素

踩坑情境:search result大部分係文章object,間中混咗廣告object,field結構完全唔同,iterate嗰陣撞到廣告就throw exception。

# 踩坑版for item in search_results["items"]:    process_article(item["title"], item["author"])  # 廣告沒這些字段 💥# 鎧甲版:Union 類型 + Literal 鑑別字段from typing import Union, LiteralclassArticleItem(BaseModel):    type: Literal["article"]    title: str    author: str    publish_date: strclassAdItem(BaseModel):    type: Literal["ad"]    sponsor: str    click_url: strclassSearchResults(BaseModel):    items: List[Union[ArticleItem, AdItem]]  # Pydantic 根據 type 字段自動區分    total: int = 0    @property    defarticles_only(self) -> List[ArticleItem]:        return [item for item in self.items if isinstance(item, ArticleItem)]# 使用:廣告自動過濾results = SearchResults(**raw_output)for article in results.articles_only:    process_article(article.title, article.author)

場景五:AI output嘅JSON本身格式有問題

踩坑情境:你叫AI以JSON格式return分析結果,但AI有時會加Markdown code block標記,有時JSON入面有comment,有時最後多咗個逗號——呢啲都令到 json.loads() 失敗。

# 踩坑版import jsonresult = json.loads(ai_response)   # SyntaxError 頻發# 鎧甲版:先清洗,再驗證import json, refrom typing import OptionalclassAIAnalysisResult(BaseModel):    summary: str    key_points: List[str] = []    confidence: float = 0.0    next_actions: List[str] = []    @validator('confidence')    defclamp_confidence(cls, v):        return max(0.0, min(1.0, v))   # 強制在 0~1 之間defsafe_parse_ai_json(ai_response: str) -> Optional[AIAnalysisResult]:    # 第一步:提取 JSON(處理 Markdown 代碼塊)    json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', ai_response)    if json_match:        json_str = json_match.group(1)    else:        json_match = re.search(r'\{[\s\S]*\}', ai_response)        json_str = json_match.group(0if json_match else ai_response    # 第二步:清理常見格式問題    json_str = re.sub(r'//[^\n]*\n''\n', json_str)    # 移除註釋    json_str = re.sub(r',(\s*[}\]])'r'\1', json_str)  # 移除尾隨逗號    # 第三步:解析 + 驗證    try:        raw_data = json.loads(json_str)        return AIAnalysisResult(**raw_data)    except (json.JSONDecodeError, ValidationError) as e:        logger.error(f"AI 輸出解析失敗: {e}")        returnNone

構建通用嘅「MCP output守衞層」

上面五個場景,每個都係單點解決方案。真正嘅工程化做法,係構建一個通用守衞層,令所有MCP工具嘅output都先經過呢一層。

from pydantic import BaseModel, ValidationErrorfrom typing import TypeVar, Type, Optional, Generic, Any, Dictfrom functools import wrapsimport logging, timelogger = logging.getLogger(__name__)T = TypeVar('T', bound=BaseModel)classMCPCallResult(BaseModel, Generic[T]):    """MCP 調用結果的統一包裝"""    success: bool    data: Optional[T] = None    error: Optional[str] = None    error_type: Optional[str] = None   # "validation" | "timeout" | "api_error"    raw_output: Optional[Dict[str, Any]] = None    call_duration_ms: Optional[float] = None    @property    defis_valid(self) -> bool:        return self.success and self.data isnotNoneclassMCPGuard:    """MCP 輸出守衞:所有 MCP 調用都通過這裏"""    def__init__(self, strict_mode: bool = False):        """        strict_mode=True:驗證失敗時拋出異常(測試環境)        strict_mode=False:驗證失敗時返回失敗結果(生產環境)        """        self.strict_mode = strict_mode    defcall(        self,        mcp_tool_func,        output_model: Type[T],        *args,        **kwargs    ) -> "MCPCallResult[T]":        start_time = time.time()        try:            raw_output = mcp_tool_func(*args, **kwargs)        except Exception as e:            logger.error(f"MCP 工具調用失敗: {mcp_tool_func.__name__} - {e}")            return MCPCallResult(                success=False,                error=str(e),                error_type="api_error",                call_duration_ms=(time.time() - start_time) * 1000            )        try:            if isinstance(raw_output, dict):                validated_data = output_model(**raw_output)            elif isinstance(raw_output, list):                validated_data = output_model(items=raw_output)            else:                validated_data = output_model.parse_raw(str(raw_output))            return MCPCallResult(                success=True,                data=validated_data,                call_duration_ms=(time.time() - start_time) * 1000            )        except ValidationError as e:            error_msg = f"MCP 輸出驗證失敗: {mcp_tool_func.__name__}\n{e}"            logger.warning(error_msg)            if self.strict_mode:                raise            return MCPCallResult(                success=False,                error=error_msg,                error_type="validation",                raw_output=raw_output if isinstance(raw_output, dict) elseNone,                call_duration_ms=(time.time() - start_time) * 1000            )    defguarded(self, output_model: Type[T]):        """裝飾器版本:給現有 MCP 調用函數套上守衞"""        defdecorator(func):            @wraps(func)            defwrapper(*args, **kwargs):                return self.call(func, output_model, *args, **kwargs)            return wrapper        return decorator# ===== 使用示例 =====guard = MCPGuard(strict_mode=False)# 方式一:直接調用result = guard.call(    github_mcp.list_issues,    GitHubIssueList,    repo="my-org/my-repo",    state="open")if result.is_valid:    for issue in result.data.issues:        if issue.assignee:            notify(issue.assignee.login, issue.title)else:    logger.error(f"處理失敗: {result.error}")    fallback_to_cache()# 方式二:裝飾器@guard.guarded(GitHubIssueList)defget_my_issues(repo: str, state: str = "open"):    return github_mcp.list_issues(repo=repo, state=state)

將守衞層集成到Skill入面:三層防禦

將所有嘢組合埋一齊,寫一個完整嘅「有甲嘅Skill」——呢度有三層守衞

classWeeklyReportSkill:    def__init__(self, ai_client, mcp_guard: MCPGuard):        self.ai = ai_client        self.guard = mcp_guard    defgenerate(self, repo: str, week_start: str, week_end: str):        # ✅ 第一層:MCP 輸出驗證        commits_result = self.guard.call(            github_mcp.list_commits,            CommitList,            repo=repo, since=week_start, until=week_end        )        ifnot commits_result.is_valid:            logger.error(f"獲取 commits 失敗: {commits_result.error}")            returnNone        commits = commits_result.data        if len(commits.commits) == 0:            return WeeklyReport(..., summary="本週沒有代碼提交記錄。")        # 調用 AI 生成周報        ai_response = self.ai.complete(            SKILL_PROMPT.format(commits_json=commits.model_dump_json(indent=2))        )        # ✅ 第二層:AI 文本輸出的 JSON 解析驗證        parsed = safe_parse_ai_json(ai_response)        if parsed isNone:            logger.error("AI 生成的週報格式有問題")            returnNone        # ✅ 第三層:業務邏輯的語義驗證        try:            return WeeklyReport(**parsed.dict())        except ValidationError as e:            logger.error(f"週報內容驗證失敗: {e}")            returnNone

三層守衞嘅職責劃分:

層級
守衞對象
核心問題
第一層
MCP工具原始output
外部API格式唔可控
第二層
AI生成嘅文本
AI output JSON格式混亂
第三層
業務數據語義
field值唔符合業務規則

任何一層失敗,都唔會令program crash,而係return None 同有意義嘅error log。


進階技巧:Pydantic嘅三個隱藏殺手鐧

殺手鐧一:model_validator 跨field驗證

單個field合法,但多個field組合埋一齊唔合理時,用佢:

from pydantic import BaseModel, model_validatorclassDateRange(BaseModel):    start_date: str    end_date: str    @model_validator(mode='after')    defvalidate_date_order(self):        from datetime import datetime        start = datetime.strptime(self.start_date, "%Y-%m-%d")        end = datetime.strptime(self.end_date, "%Y-%m-%d")        if end < start:            raise ValueError("結束日期不能早於開始日期")        return selfclassBudgetAllocation(BaseModel):    marketing: float    development: float    operations: float    @model_validator(mode='after')    defvalidate_total(self):        total = self.marketing + self.development + self.operations        if total > 100.001:            raise ValueError(f"預算總和({total:.1f}%)超過 100%")        return self

殺手鐧二:自訂類型實現安全URL(防SSRF)

classSafeURL(str):    """只允許 http/https,並防止 SSRF 攻擊"""    @classmethod    def__get_pydantic_core_schema__(cls, source, handler):        from pydantic_core import core_schema        return core_schema.no_info_after_validator_function(            cls.validate, core_schema.str_schema()        )    @classmethod    defvalidate(cls, v: str) -> "SafeURL":        ifnot v.startswith(('http://''https://')):            raise ValueError(f"不安全的 URL: {v}")        from urllib.parse import urlparse        host = urlparse(v).hostname        forbidden = ['localhost''127.0.0.1''0.0.0.0''169.254.169.254']        if host in forbidden:            raise ValueError(f"禁止訪問內網地址: {host}")        return cls(v)classWebhookConfig(BaseModel):    url: SafeURL   # 自動校驗 URL 安全性,防 SSRF    secret: str    events: List[str] = []

殺手鐧三:model_config 精細控制解析行為

from pydantic import BaseModel, ConfigDictclassFlexibleModel(BaseModel):    model_config = ConfigDict(        populate_by_name=True,   # 允許字段有別名        extra='ignore',          # 忽略多餘字段而不是報錯        from_attributes=True,    # 允許從 ORM 對象創建    )    name: str = Field(..., alias="full_name")    score: float    tags: List[str] = []

最後:建立你嘅「MCP類型庫」

將你常用嘅MCP工具嘅output model沉澱成一個library,係回報率最高嘅工程實踐之一:

your_project/├── mcp_types/│   ├── __init__.py│   ├── github.py        # GitHub MCP 的所有輸出模型│   ├── notion.py        # Notion MCP 的所有輸出模型│   ├── slack.py         # Slack MCP 的所有輸出模型│   └── base.py          # 通用基類和 MCPGuard├── skills/│   ├── weekly_report.py # 使用 mcp_types 的 Skill│   └── ...└── ...

每次踩咗一個新坑,就將嗰個場景嘅驗證邏輯加返入對應嘅類型file度。三個月後,你會發現呢個 mcp_types 目錄係你成個Agent工程入面最寶貴嘅資產——因為佢記錄咗所有外面世界嘅混亂,同埋你點樣馴服佢哋嘅智慧。

MCP係手腳,Skill係靈魂。但如果Skill冇保護層,靈魂隨時可能會被外面世界嘅混亂所傷。Pydantic,就係嗰件甲。


環境準備

pip install pydantic>=2.0 pydantic-core

我係專注AI Agent深度實踐嘅努力撞蘑菇AI,致力於分享真實可用嘅AI使用經驗與工作流探索,歡迎你同我一齊將AI玩得明明白白,如果內容對你有幫助,歡迎關注、點讚、轉發。

開場:一次讓人脊背發涼的事故

去年某個深夜,一位朋友發來消息:「我的Agent把我的Notion數據庫清空了。」

起因極其荒唐。

他寫了一個自動整理文檔的Agent,Skill指令裏有一句話——「刪除重複內容」。Agent調用MCP讀取了Notion數據庫,返回了一個JSON對象。但這個JSON的格式跟他預期的不一樣——某個字段返回的是字符串 "null",而不是真正的 null

他的代碼裏有一行判斷:

if item["content"isNone:    delete(item)

字符串 "null" 不等於 None,所以跳過了這個判斷。但後續的去重邏輯,因為沒有對數據類型做校驗,把一批格式異常的條目全部當作「重複項」處理,批量調用了 delete 接口。

一個小時後,他的 Notion 數據庫裏幾百條筆記,消失了。

這不是 Skill 指令的問題,這是類型防禦的問題。

MCP 工具的輸出,本質上是一堆「來自外部世界的數據」。外部世界是混亂的、不可信的、充滿驚喜的。你的 Agent 如果沒有對這些數據做類型校驗,就等於在高速公路上開車,卻把安全帶扔掉了。

今天我們就來聊,如何用 Pydantic 給 Skill 穿上一件真正的鎧甲。圖片


為什麼 MCP 輸出特別危險?

在講解決方案之前,我們先把問題根源搞清楚。

MCP 的本質是:讓 AI 模型可以調用外部工具,獲取外部數據。這些外部工具可能是 GitHub API、Notion API、數據庫查詢、文件系統操作、Shell 命令……

它們的共同特點是:輸出格式不受你控制。

你調用 GitHub API,有時候 assignee 字段是一個對象,有時候是 null,有時候是一個空數組——取決於這個 Issue 有沒有被分配,取決於 GitHub 這個版本的 API 行為,甚至取決於網絡超時導致返回了錯誤格式的響應。

更危險的是 AI 本身。當你的 Skill 讓 AI「解析」某個 MCP 工具的輸出時,AI 會進行「推斷」。推斷意味着:

  • 如果字段缺失,AI 可能會「合理填充」一個它認為合理的值
  • 如果格式不對,AI 可能會「格式轉換」,把字符串悄悄變成數字
  • 如果數據為空,AI 可能會「忽略」這個問題,繼續往下走

這些「智能」行為,在正常場景下看起來很貼心。但在涉及刪除、修改、發送等不可逆操作時,就是災難的開始。

MCP 輸出的五大危險模式:

1. 字段缺失   → AI 推斷填充 → 用錯誤的值觸發了錯誤的邏輯2. 類型混淆   → "123" vs 123 → 數值比較失效,條件判斷出錯3. 嵌套結構   → 期望 dict 卻收到 list → 屬性訪問拋出異常,流程中斷4. null 變體  → null / None / "null" / "" → 判空邏輯全部失效5. 分頁截斷   → 數據只返回了第一頁 → AI 以為這就是全部,決策錯誤

現在你理解了問題的嚴重性,我們來看解決方案。


什麼是 Pydantic,為什麼它是最佳選擇?

Pydantic 是 Python 生態裏最流行的數據驗證庫,沒有之一。它的核心思想是:用 Python 類型註解來定義數據結構,然後用它來解析、驗證、轉換外部數據。

在沒有 Pydantic 之前,處理 MCP 輸出的典型代碼長這樣:

# 脆弱版:沒有任何防禦defprocess_github_issues(mcp_output):    issues = mcp_output["data"]["issues"]          # KeyError 風險    for issue in issues:        assignee = issue["assignee"]["login"]      # assignee 可能是 null!        priority = int(issue["labels"][0]["name"]) # labels 可能為空!        if priority > 5:            send_notification(assignee, issue["title"])

這段代碼在正常數據下能跑,但:

  • 如果 data 字段不存在?→ KeyError
  • 如果 assignee 是 null?→ TypeError: 'NoneType' object is not subscriptable
  • 如果 labels 是空數組?→ IndexError: list index out of range
  • 如果第一個 label 的名字不是數字?→ ValueError: invalid literal for int()

任何一個條件觸發,程序崩潰,Agent 停止,任務失敗。如果這個函數調用了有副作用的操作(比如發了一半的通知),你甚至不知道哪些發了、哪些沒發。

有了 Pydantic 之後:

from pydantic import BaseModel, Field, validatorfrom typing import Optional, ListclassGitHubUser(BaseModel):    login: str    id: int    avatar_url: Optional[str] = NoneclassGitHubLabel(BaseModel):    id: int    name: str    color: str = "000000"   # 默認值兜底classGitHubIssue(BaseModel):    id: int    number: int    title: str    state: str = "open"    assignee: Optional[GitHubUser] = None# 明確標註可以為 None    labels: List[GitHubLabel] = []         # 默認空列表    @validator('state')    defvalidate_state(cls, v):        if v notin ['open''closed']:            raise ValueError(f'Invalid state: {v}')        return vclassGitHubIssueList(BaseModel):    issues: List[GitHubIssue] = []    total_count: int = 0    page: int = 1    has_more: bool = False

有了這個模型,處理邏輯變成了:

defprocess_github_issues(mcp_output):    try:        issue_list = GitHubIssueList(**mcp_output.get("data", {}))    except ValidationError as e:        logger.error(f"MCP 輸出格式異常: {e}")        return ProcessResult(success=False, error=str(e))    notifications_sent = []    for issue in issue_list.issues:        if issue.assignee isNone:  # 必須先判斷,IDE 會提示你            continue        priority_labels = [l for l in issue.labels if l.name.startswith("P")]        ifnot priority_labels:            continue        send_notification(issue.assignee.login, issue.title)        notifications_sent.append(issue.number)    return ProcessResult(        success=True,        notifications_sent=notifications_sent,        total_processed=len(issue_list.issues)    )

對比一下:

  • 脆弱版:7 行核心邏輯,4 個潛在崩潰點,0 個有意義的錯誤信息
  • 鎧甲版:多了模型定義,但業務邏輯裏零個 KeyError 風險,錯誤有據可查,行為完全可預期

五大真實踩坑場景與 Pydantic 解法

讓我們走過五個最常見的 MCP 輸出陷阱,每一個都有「踩坑版」和「鎧甲版」對比。

場景一:字段名對不上

踩坑情境:你的 Skill 讓 AI 從搜索結果中提取「文章標題」,AI 返回的字段有時候叫 title,有時候叫 name,有時候叫 headline——不同 MCP 工具字段命名不統一。

# 踩坑版article_title = result["title"]   # 有時候 KeyError# 鎧甲版:兼容多種字段名classArticleResult(BaseModel):    title: str    @classmethod    deffrom_flexible_input(cls, data: dict) -> "ArticleResult":        title = (            data.get("title"or            data.get("name"or            data.get("headline"or            data.get("subject"or            "(無標題)"   # 最終兜底        )        return cls(title=title)# 使用:永遠不會 KeyErrorarticle = ArticleResult.from_flexible_input(raw_mcp_output)print(article.title)

場景二:數字被當成字符串傳過來

踩坑情境:某個 MCP 工具返回的價格是 "129.00"(字符串),你的代碼拿它做數值計算,和另一個工具返回的 129(整數)做比較時直接崩潰。

# 踩坑版price = api_response["price"]discount = api_response["discount"]final_price = price * (1 - discount)  # 字符串不能乘法!# 鎧甲版:Pydantic 自動做類型強制轉換from decimal import DecimalclassPriceData(BaseModel):    price: Decimal      # 自動把 "129.00" 轉成 Decimal(129.00)    discount: float     # 自動把 "0.1" 轉成 0.1    currency: str = "CNY"    @property    deffinal_price(self) -> Decimal:        return self.price * Decimal(str(1 - self.discount))price_data = PriceData(price="129.00", discount="0.1")print(price_data.final_price)   # Decimal('116.100')  ✅

場景三:嵌套結構裏某一層是 None

踩坑情境:你想訪問 response["user"]["profile"]["avatar_url"],但 profile 這一層可能是 null——新註冊用戶還沒填資料。

# 踩坑版:三層訪問,任何一層為 None 就崩潰avatar = response["user"]["profile"]["avatar_url"]   # 💥# 鎧甲版:Optional 嵌套 + 屬性封裝classUserProfile(BaseModel):    avatar_url: Optional[str] = None    bio: Optional[str] = NoneclassUserData(BaseModel):    id: int    username: str    profile: Optional[UserProfile] = None   # 整個 profile 可以是 None    @property    defavatar_url(self) -> str:        if self.profile isNone:            return"https://example.com/default-avatar.png"        return self.profile.avatar_url or"https://example.com/default-avatar.png"# 使用:絕對不會崩潰user = UserData(**response["user"])show_avatar(user.avatar_url)

場景四:列表裏混進了不同類型的元素

踩坑情境:搜索結果裏大部分是文章對象,偶爾混進廣告對象,字段結構完全不同,迭代時遇到廣告就拋異常。

# 踩坑版for item in search_results["items"]:    process_article(item["title"], item["author"])  # 廣告沒這些字段 💥# 鎧甲版:Union 類型 + Literal 鑑別字段from typing import Union, LiteralclassArticleItem(BaseModel):    type: Literal["article"]    title: str    author: str    publish_date: strclassAdItem(BaseModel):    type: Literal["ad"]    sponsor: str    click_url: strclassSearchResults(BaseModel):    items: List[Union[ArticleItem, AdItem]]  # Pydantic 根據 type 字段自動區分    total: int = 0    @property    defarticles_only(self) -> List[ArticleItem]:        return [item for item in self.items if isinstance(item, ArticleItem)]# 使用:廣告自動過濾results = SearchResults(**raw_output)for article in results.articles_only:    process_article(article.title, article.author)

場景五:AI 輸出的 JSON 本身格式有問題

踩坑情境:你讓 AI 以 JSON 格式返回分析結果,但 AI 有時候會加上 Markdown 代碼塊標記,有時候 JSON 裏有註釋,有時候最後多了一個逗號——這些都導致 json.loads() 失敗。

# 踩坑版import jsonresult = json.loads(ai_response)   # SyntaxError 頻發# 鎧甲版:先清洗,再驗證import json, refrom typing import OptionalclassAIAnalysisResult(BaseModel):    summary: str    key_points: List[str] = []    confidence: float = 0.0    next_actions: List[str] = []    @validator('confidence')    defclamp_confidence(cls, v):        return max(0.0, min(1.0, v))   # 強制在 0~1 之間defsafe_parse_ai_json(ai_response: str) -> Optional[AIAnalysisResult]:    # 第一步:提取 JSON(處理 Markdown 代碼塊)    json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', ai_response)    if json_match:        json_str = json_match.group(1)    else:        json_match = re.search(r'\{[\s\S]*\}', ai_response)        json_str = json_match.group(0if json_match else ai_response    # 第二步:清理常見格式問題    json_str = re.sub(r'//[^\n]*\n''\n', json_str)    # 移除註釋    json_str = re.sub(r',(\s*[}\]])'r'\1', json_str)  # 移除尾隨逗號    # 第三步:解析 + 驗證    try:        raw_data = json.loads(json_str)        return AIAnalysisResult(**raw_data)    except (json.JSONDecodeError, ValidationError) as e:        logger.error(f"AI 輸出解析失敗: {e}")        returnNone

構建通用的「MCP 輸出守衞層」

上面的五個場景,每個都是單點解決方案。真正的工程化做法,是構建一個通用守衞層,讓所有 MCP 工具的輸出都先經過這一層。

from pydantic import BaseModel, ValidationErrorfrom typing import TypeVar, Type, Optional, Generic, Any, Dictfrom functools import wrapsimport logging, timelogger = logging.getLogger(__name__)T = TypeVar('T', bound=BaseModel)classMCPCallResult(BaseModel, Generic[T]):    """MCP 調用結果的統一包裝"""    success: bool    data: Optional[T] = None    error: Optional[str] = None    error_type: Optional[str] = None   # "validation" | "timeout" | "api_error"    raw_output: Optional[Dict[str, Any]] = None    call_duration_ms: Optional[float] = None    @property    defis_valid(self) -> bool:        return self.success and self.data isnotNoneclassMCPGuard:    """MCP 輸出守衞:所有 MCP 調用都通過這裏"""    def__init__(self, strict_mode: bool = False):        """        strict_mode=True:驗證失敗時拋出異常(測試環境)        strict_mode=False:驗證失敗時返回失敗結果(生產環境)        """        self.strict_mode = strict_mode    defcall(        self,        mcp_tool_func,        output_model: Type[T],        *args,        **kwargs    ) -> "MCPCallResult[T]":        start_time = time.time()        try:            raw_output = mcp_tool_func(*args, **kwargs)        except Exception as e:            logger.error(f"MCP 工具調用失敗: {mcp_tool_func.__name__} - {e}")            return MCPCallResult(                success=False,                error=str(e),                error_type="api_error",                call_duration_ms=(time.time() - start_time) * 1000            )        try:            if isinstance(raw_output, dict):                validated_data = output_model(**raw_output)            elif isinstance(raw_output, list):                validated_data = output_model(items=raw_output)            else:                validated_data = output_model.parse_raw(str(raw_output))            return MCPCallResult(                success=True,                data=validated_data,                call_duration_ms=(time.time() - start_time) * 1000            )        except ValidationError as e:            error_msg = f"MCP 輸出驗證失敗: {mcp_tool_func.__name__}\n{e}"            logger.warning(error_msg)            if self.strict_mode:                raise            return MCPCallResult(                success=False,                error=error_msg,                error_type="validation",                raw_output=raw_output if isinstance(raw_output, dict) elseNone,                call_duration_ms=(time.time() - start_time) * 1000            )    defguarded(self, output_model: Type[T]):        """裝飾器版本:給現有 MCP 調用函數套上守衞"""        defdecorator(func):            @wraps(func)            defwrapper(*args, **kwargs):                return self.call(func, output_model, *args, **kwargs)            return wrapper        return decorator# ===== 使用示例 =====guard = MCPGuard(strict_mode=False)# 方式一:直接調用result = guard.call(    github_mcp.list_issues,    GitHubIssueList,    repo="my-org/my-repo",    state="open")if result.is_valid:    for issue in result.data.issues:        if issue.assignee:            notify(issue.assignee.login, issue.title)else:    logger.error(f"處理失敗: {result.error}")    fallback_to_cache()# 方式二:裝飾器@guard.guarded(GitHubIssueList)defget_my_issues(repo: str, state: str = "open"):    return github_mcp.list_issues(repo=repo, state=state)

把守衞層集成到 Skill 裏:三層防禦

把所有東西組合起來,寫一個完整的「有鎧甲的 Skill」——這裏有三層守衞

classWeeklyReportSkill:    def__init__(self, ai_client, mcp_guard: MCPGuard):        self.ai = ai_client        self.guard = mcp_guard    defgenerate(self, repo: str, week_start: str, week_end: str):        # ✅ 第一層:MCP 輸出驗證        commits_result = self.guard.call(            github_mcp.list_commits,            CommitList,            repo=repo, since=week_start, until=week_end        )        ifnot commits_result.is_valid:            logger.error(f"獲取 commits 失敗: {commits_result.error}")            returnNone        commits = commits_result.data        if len(commits.commits) == 0:            return WeeklyReport(..., summary="本週沒有代碼提交記錄。")        # 調用 AI 生成周報        ai_response = self.ai.complete(            SKILL_PROMPT.format(commits_json=commits.model_dump_json(indent=2))        )        # ✅ 第二層:AI 文本輸出的 JSON 解析驗證        parsed = safe_parse_ai_json(ai_response)        if parsed isNone:            logger.error("AI 生成的週報格式有問題")            returnNone        # ✅ 第三層:業務邏輯的語義驗證        try:            return WeeklyReport(**parsed.dict())        except ValidationError as e:            logger.error(f"週報內容驗證失敗: {e}")            returnNone

三層守衞的職責劃分:

層級
守衞對象
核心問題
第一層
MCP 工具原始輸出
外部 API 格式不可控
第二層
AI 生成的文本
AI 輸出 JSON 格式混亂
第三層
業務數據語義
字段值不符合業務規則

任何一層失敗,都不會導致程序崩潰,而是返回 None 和有意義的錯誤日誌。


進階技巧:Pydantic 的三個隱藏殺手鐧

殺手鐧一:model_validator 跨字段驗證

單個字段合法,但多個字段組合起來不合理時,用它:

from pydantic import BaseModel, model_validatorclassDateRange(BaseModel):    start_date: str    end_date: str    @model_validator(mode='after')    defvalidate_date_order(self):        from datetime import datetime        start = datetime.strptime(self.start_date, "%Y-%m-%d")        end = datetime.strptime(self.end_date, "%Y-%m-%d")        if end < start:            raise ValueError("結束日期不能早於開始日期")        return selfclassBudgetAllocation(BaseModel):    marketing: float    development: float    operations: float    @model_validator(mode='after')    defvalidate_total(self):        total = self.marketing + self.development + self.operations        if total > 100.001:            raise ValueError(f"預算總和({total:.1f}%)超過 100%")        return self

殺手鐧二:自定義類型實現安全 URL(防 SSRF)

classSafeURL(str):    """只允許 http/https,並防止 SSRF 攻擊"""    @classmethod    def__get_pydantic_core_schema__(cls, source, handler):        from pydantic_core import core_schema        return core_schema.no_info_after_validator_function(            cls.validate, core_schema.str_schema()        )    @classmethod    defvalidate(cls, v: str) -> "SafeURL":        ifnot v.startswith(('http://''https://')):            raise ValueError(f"不安全的 URL: {v}")        from urllib.parse import urlparse        host = urlparse(v).hostname        forbidden = ['localhost''127.0.0.1''0.0.0.0''169.254.169.254']        if host in forbidden:            raise ValueError(f"禁止訪問內網地址: {host}")        return cls(v)classWebhookConfig(BaseModel):    url: SafeURL   # 自動校驗 URL 安全性,防 SSRF    secret: str    events: List[str] = []

殺手鐧三:model_config 精細控制解析行為

from pydantic import BaseModel, ConfigDictclassFlexibleModel(BaseModel):    model_config = ConfigDict(        populate_by_name=True,   # 允許字段有別名        extra='ignore',          # 忽略多餘字段而不是報錯        from_attributes=True,    # 允許從 ORM 對象創建    )    name: str = Field(..., alias="full_name")    score: float    tags: List[str] = []

最後:建立你的「MCP 類型庫」

把你常用的 MCP 工具的輸出模型沉澱成一個庫,是投入回報率最高的工程實踐之一:

your_project/├── mcp_types/│   ├── __init__.py│   ├── github.py        # GitHub MCP 的所有輸出模型│   ├── notion.py        # Notion MCP 的所有輸出模型│   ├── slack.py         # Slack MCP 的所有輸出模型│   └── base.py          # 通用基類和 MCPGuard├── skills/│   ├── weekly_report.py # 使用 mcp_types 的 Skill│   └── ...└── ...

每次踩了一個新坑,就把那個場景的驗證邏輯加進對應的類型文件裏。三個月後,你會發現這個 mcp_types 目錄是你整個 Agent 工程裏最寶貴的資產——因為它記錄了所有外部世界的混亂,以及你如何馴服它們的智慧。

MCP 是手腳,Skill 是靈魂。但如果 Skill 沒有保護層,靈魂隨時可能被外部世界的混亂所傷。Pydantic,就是那件鎧甲。


環境準備

pip install pydantic>=2.0 pydantic-core

我是專注 AI Agent深度實踐的努力撞蘑菇AI,致力於分享真實可用的 AI 使用經驗與工作流探索,歡迎你和我一起把 AI 玩明白,如果內容對你有幫助,歡迎關注、點贊、轉發。