Pydantic AI로 LLM 응답을 타입 안전하게 검증하기
LLM을 프로덕션에 붙여본 분이라면 이런 상황을 한 번쯤 겪어봤을 겁니다. GPT한테 JSON으로 응답하라고 시스템 프롬프트까지 꼼꼼히 썼는데, 정작 런타임에 KeyError가 터지는 상황. 아니면 result["block_card"]가 어떤 날은 True고 어떤 날은 "true"인 상황. LLM 출력을 딕셔너리로 받아 쓰는 코드는 언제나 이런 시한폭탄을 안고 있습니다.
Pydantic AI는 LLM 응답을 Pydantic BaseModel로 강제 파싱·검증함으로써, 타입 불일치와 누락 필드를 런타임 이전에 잡아내는 Python 기반 에이전트 프레임워크입니다. 2024년 말 Pydantic 팀이 공개하고 2025년 9월 v1.0 안정 버전을 출시했으며, Thoughtworks Technology Radar 2025에 "Trial" 단계로 등재됐습니다. "FastAPI가 웹 API 개발에 가져온 DX를 GenAI 에이전트 개발에도"라는 철학이 코드 곳곳에 녹아 있어서, FastAPI를 써본 Python 백엔드 개발자라면 학습 곡선이 생각보다 완만하게 느껴질 겁니다.
이 글을 읽고 나면 LLM 연동 코드에서 mypy가 통과하고, CI에서 실제 API 비용 없이 에이전트를 테스트하며, FastAPI 엔드포인트에 추가 직렬화 코드 없이 LLM 분석을 붙이는 방법을 바로 써먹을 수 있습니다. LangChain·LangGraph와 어떤 상황에서 무엇을 선택하면 좋은지도 솔직하게 다룹니다.
목차
핵심 개념
타입 안전 출력 — output_type으로 LLM 응답을 Python 객체로
기존 에이전트 프레임워크들은 LLM 응답을 문자열이나 느슨한 딕셔너리로 돌려줍니다. Pydantic AI는 다릅니다. output_type에 Pydantic 모델을 선언하면, 그 JSON Schema가 LLM 프롬프트에 자동으로 주입되고, 응답은 Pydantic 검증을 거쳐 완전한 Python 객체로 반환됩니다.
import asyncio
from typing import Literal
from pydantic import BaseModel
from pydantic_ai import Agent
class AnalysisResult(BaseModel):
sentiment: Literal["positive", "negative", "neutral"] # str이 아닌 리터럴 타입
confidence: float # 0.0 ~ 1.0
summary: str
agent = Agent('openai:gpt-4o', output_type=AnalysisResult)
async def main():
result = await agent.run('이 리뷰를 분석해줘: 배송이 너무 늦었어요')
print(result.output.confidence) # float 보장, IDE 자동완성 작동
print(result.output.sentiment) # "positive" | "negative" | "neutral" 중 하나
asyncio.run(main())저도 처음엔 "프롬프트에 JSON Schema를 주입한다는 게 실제로 얼마나 잘 먹히나?" 싶었는데, 생각보다 훨씬 안정적입니다. LLM이 잘못된 JSON을 반환하면 검증 오류 메시지 전체를 LLM에 피드백으로 다시 전달하면서 재시도합니다. 기본값은 1회 재시도(총 2번 시도)이며, Agent(retries=3)처럼 조정할 수 있습니다. 그래도 실패하면 ValidationError가 명확하게 터집니다.
참고로 LLM 제공자마다 Structured Output 구현 방식이 다른데, Pydantic AI가 이를 알아서 추상화해줍니다. OpenAI는 response_format={"type": "json_schema"}를 쓰고, Anthropic은 tool_use 방식으로 스키마를 강제하는데, 개발자 입장에서는 output_type 하나만 선언하면 됩니다.
Structured Output — LLM이 자유 텍스트가 아닌 특정 JSON 스키마에 맞는 응답을 반환하도록 유도하는 기법. Pydantic AI는 이 과정을 자동화하고, 검증 실패 시 오류 피드백과 함께 재시도까지 처리합니다.
의존성 주입 — DB, HTTP 클라이언트를 타입 안전하게 툴에 전달
FastAPI의 Depends 패턴을 써본 분이라면 바로 이해가 될 겁니다. deps_type으로 의존성 컨테이너를 선언하면, 에이전트 툴 함수 안에서 ctx.deps를 통해 타입 안전하게 꺼내 쓸 수 있습니다. DB 커넥션, HTTP 클라이언트, 설정 값 등을 전역 상태나 환경 변수 없이 명시적으로 주입할 수 있는 구조입니다.
from dataclasses import dataclass
from httpx import AsyncClient # pip install httpx
from pydantic_ai import Agent, RunContext
# from myapp.db import DatabaseConn # 실제 프로젝트의 DB 커넥션 클래스
@dataclass
class AppDeps:
db: DatabaseConn
http_client: AsyncClient
user_id: int
agent = Agent('anthropic:claude-3-7-sonnet', deps_type=AppDeps)
@agent.tool
async def fetch_user_orders(ctx: RunContext[AppDeps]) -> list[dict]:
# ctx.deps.db, ctx.deps.user_id 모두 타입 추론 가능
return await ctx.deps.db.query(
'SELECT * FROM orders WHERE user_id = $1', ctx.deps.user_id
)RunContext[T] — 툴 함수에 전달되는 컨텍스트 객체.
T에 deps 타입을 제네릭으로 선언하면ctx.deps접근 시 IDE 자동완성과 mypy 검사가 모두 작동합니다.
이 구조의 실질적인 장점은 테스트에서 드러납니다. AppDeps에 목 객체를 넣어 실제 API 비용 없이 단위 테스트를 구성할 수 있습니다. 자세한 내용은 아래 비용 없는 테스트 섹션에서 이어집니다.
모델 어그노스틱 — 벤더 교체가 한 줄
공식 문서에 따르면 OpenAI, Anthropic, Gemini, DeepSeek, Mistral, Ollama 등 25개 이상의 LLM 제공자를 지원하며, 비즈니스 로직을 건드리지 않고 모델 문자열 하나만 바꾸면 됩니다.
# 개발 환경: 로컬 Ollama (API 비용 없음)
agent = Agent('ollama:llama3.1')
# 프로덕션: OpenAI
agent = Agent('openai:gpt-4o')
# 비용 절감 실험: DeepSeek
agent = Agent('deepseek:deepseek-chat')Model-Agnostic — 특정 LLM 벤더에 종속되지 않는 설계. 벤더 가격 변동이나 서비스 장애에 유연하게 대응할 수 있습니다.
실전 적용
예시 1: 뱅킹 고객 지원 에이전트
금융 도메인에서 LLM 출력을 프로그래밍 방식으로 소비해야 하는 대표적인 케이스입니다. 카드 차단 여부(block_card)가 True/"true"/1로 들쑥날쑥하면 안 되는 상황이죠. 저희 팀도 처음에 output_type 없이 딕셔너리로 시작했다가 나중에 마이그레이션하면서 예상보다 훨씬 많은 코드를 손봤습니다. 처음부터 Pydantic 모델을 정의해두는 게 훨씬 낫다는 걸 그때 체감했습니다.
import asyncio
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
# from myapp.db import DatabaseConn # 실제 프로젝트의 DB 커넥션 클래스
@dataclass
class SupportDeps:
customer_id: int
db: DatabaseConn
class SupportResult(BaseModel):
support_advice: str
block_card: bool
risk_level: int = Field(ge=1, le=10) # Pydantic 검증: 1~10 사이
agent = Agent(
'openai:gpt-4o',
deps_type=SupportDeps,
output_type=SupportResult,
system_prompt='당신은 은행 고객 지원 에이전트입니다. 부정 거래 위험을 평가하세요.',
)
@agent.tool
async def get_customer_balance(ctx: RunContext[SupportDeps]) -> float:
return await ctx.deps.db.get_balance(ctx.deps.customer_id)
@agent.tool
async def get_recent_transactions(ctx: RunContext[SupportDeps]) -> list[dict]:
return await ctx.deps.db.get_transactions(ctx.deps.customer_id, limit=10)
async def main():
result = await agent.run(
'카드를 잃어버렸어요, 해외에서 모르는 결제가 있어요',
deps=SupportDeps(customer_id=123, db=db_conn)
)
# 완전한 타입 안전 접근 — IDE 자동완성, mypy 통과
if result.output.block_card:
await card_service.block(customer_id=123)
print(f"위험 등급: {result.output.risk_level}/10") # int 보장
asyncio.run(main())| 포인트 | 설명 |
|---|---|
block_card: bool |
LLM이 어떤 형태로 응답해도 Python bool로 강제 변환·검증 |
risk_level: int = Field(ge=1, le=10) |
범위 검증까지 Pydantic이 담당, 범위 이탈 시 재시도 트리거 |
RunContext[SupportDeps] |
툴 함수 안에서 타입 추론 완벽히 작동, 전역 상태 불필요 |
예시 2: FastAPI 엔드포인트와의 통합
실무에서 자주 맞닥뜨리는 상황인데, FastAPI 라우터에 LLM 분석을 붙이면서 별도 직렬화 코드 없이 깔끔하게 연결할 수 있습니다. FastAPI의 response_model과 에이전트의 output_type이 동일한 Pydantic 모델을 공유하기 때문에 가능한 패턴입니다.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class ReviewRequest(BaseModel):
text: str
language: str = 'ko'
class ReviewAnalysis(BaseModel):
sentiment: str
key_issues: list[str]
recommended_action: str
priority: int = Field(ge=1, le=5)
app = FastAPI()
agent = Agent('anthropic:claude-3-7-sonnet', output_type=ReviewAnalysis)
@app.post('/analyze-review', response_model=ReviewAnalysis)
async def analyze_review(request: ReviewRequest) -> ReviewAnalysis:
result = await agent.run(
f"다음 {request.language} 리뷰를 분석해주세요: {request.text}"
)
return result.output # 이미 검증된 Pydantic 모델 그대로 반환result.output이 이미 ReviewAnalysis 타입으로 검증된 객체이기 때문에 FastAPI가 그대로 직렬화합니다. API 경계에서 별도 변환 코드가 전혀 필요 없다는 게 이 패턴의 핵심입니다.
비용 없는 테스트
솔직히 이게 Pydantic AI에서 가장 마음에 드는 기능입니다. TestModel을 쓰면 실제 LLM API를 전혀 호출하지 않고 에이전트 로직을 테스트할 수 있습니다.
TestModel은 Pydantic 모델의 각 필드 타입에 맞는 기본값을 자동 생성합니다. str은 빈 문자열(""), int는 0, bool은 False, float는 0.0 식으로요. 덕분에 LLM이 "어떤 값을 반환할지"가 아니라 "에이전트가 올바른 타입 계약을 지키는지"를 테스트하는 데 집중할 수 있습니다.
import pytest
from unittest.mock import AsyncMock
from pydantic_ai.models.test import TestModel
# pytest-asyncio 필요: pip install pytest-asyncio
# @pytest.mark.asyncio — async 테스트 함수를 pytest가 실행할 수 있게 해주는 데코레이터
@pytest.mark.asyncio
async def test_support_result_schema_contract():
mock_db = AsyncMock()
mock_db.get_balance.return_value = 50000.0
mock_db.get_transactions.return_value = [
{"amount": 9999.99, "country": "NG", "merchant": "unknown"}
]
with agent.override(model=TestModel()):
result = await agent.run(
'해외 고액 결제가 수상해요',
deps=SupportDeps(customer_id=456, db=mock_db)
)
# TestModel 반환값: block_card=False, risk_level=0, support_advice=""
assert isinstance(result.output, SupportResult) # 타입 계약 검증
assert isinstance(result.output.block_card, bool) # bool 타입 보장
# Field(ge=1, le=10) 제약 → risk_level=0은 실패하므로 실제 검증 범위도 확인 가능CI에서 LLM API 비용 걱정 없이 타입 계약 위반을 잡아낼 수 있습니다. 실제 LLM 동작 검증이 필요할 때는 pydantic-evals로 별도 평가 파이프라인을 구성하는 것도 좋은 접근입니다.
어떤 프레임워크를 선택할까?
도입부에서 LangChain·LangGraph 비교를 예고했으니 솔직하게 정리해봅니다. "무조건 Pydantic AI가 낫다"는 이야기가 아니라, 상황에 따라 선택이 달라집니다.
| 상황 | 추천 프레임워크 | 이유 |
|---|---|---|
| LLM 출력을 프로그래밍 방식으로 소비 (API 응답, DB 저장) | Pydantic AI | 타입 안전성, mypy 통합, FastAPI 시너지 |
| RAG 파이프라인, 다양한 문서 로더 필요 | LangChain | 풍부한 로더·임베딩·벡터 DB 생태계 |
| 분기·순환이 많은 복잡한 멀티에이전트 | LangGraph | 성숙한 그래프 기반 상태 제어 |
| 역할 기반 멀티에이전트 협업 (리서처·작성자·리뷰어) | CrewAI | 역할 추상화, 사람이 읽기 쉬운 구성 |
| 경량 Structured Output만 필요 | Instructor | Pydantic AI보다 가볍고, LLM 라이브러리에 직접 패치 방식 |
실무에서는 섞어 쓰는 것도 충분히 현실적인 선택입니다. 문서 청킹과 임베딩은 LangChain으로, 최종 LLM 출력 처리는 Pydantic AI로 구성하는 아키텍처도 자연스럽게 동작합니다.
pydantic-graph가 필요한 시점
Pydantic AI 내에서도 복잡한 워크플로우가 필요할 때 pydantic-graph 모듈로의 전환을 고려해볼 수 있습니다. 대략 다음 기준이 참고가 됩니다.
- 분기 조건이 2개 이상이고 각 분기마다 다른 툴을 호출해야 할 때
- 단계 사이에 상태를 저장하고 재개(resume)해야 할 때
- 특정 툴 호출에 사람의 승인(Human-in-the-Loop)이 필요할 때
다만 LangGraph에 비해 아직 성숙도가 낮은 편이라, 복잡한 그래프 워크플로우라면 LangGraph 병행 검토를 권장합니다.
pydantic-graph — 상태 머신 기반 복잡한 멀티스텝 워크플로우를 지원하는 Pydantic AI의 선택적 모듈. LangGraph의 그래프 기반 접근과 유사하지만 아직 성숙도는 낮은 편입니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 런타임 전 오류 감지 | mypy/pyright가 에이전트 로직의 타입 불일치를 개발 단계에서 잡아냄 |
| 자동 재시도·검증 | LLM이 잘못된 JSON을 반환하면 오류 피드백과 함께 재시도, 최종 실패 시 명확한 예외 발생 |
| 비용 없는 테스트 | TestModel과 deps 모킹으로 실제 API 비용 없이 단위 테스트 가능 |
| 모델 독립성 | 25개 이상 LLM 지원, 비즈니스 로직 변경 없이 제공자 전환 가능 |
| FastAPI 친화성 | 동일 팀 제작, 동일 DI 패턴, 에코시스템 통합이 자연스러움 |
| 순수 Python | 별도 DSL 없이 async/await 네이티브 지원 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Python 전용 | TypeScript/Go 팀은 사용 불가 | JS 생태계는 Mastra 등 대안 검토 |
| 작은 커뮤니티 | GitHub 스타 ~16.5K, LangChain(100K+) 대비 템플릿·예제 부족 | 공식 문서와 예제 저장소가 잘 정리되어 있어 보완 가능 |
| 관찰성 유료 의존 | 프로덕션 트레이싱은 Pydantic Logfire(유료)에 의존 | OpenTelemetry 직접 연결 설정으로 대체 가능 |
| 복잡한 멀티에이전트 | 정교한 분기·순환 워크플로우는 pydantic-graph 필요, LangGraph보다 성숙도 낮음 |
복잡한 그래프 워크플로우는 LangGraph 병행 검토 |
| 툴 호출 비효율 | 일부 시나리오에서 툴 호출 반복이 많아 토큰 비용 상승 | 툴 설계 시 호출 수 최소화, 캐싱 레이어 추가 고려 |
OpenTelemetry — 벤더 독립적인 관찰성(Observability) 표준. Pydantic Logfire의 기반으로, 직접 연결하면 Datadog·Grafana 같은 기존 인프라와 통합할 수 있습니다.
실무에서 가장 흔한 실수
-
output_type없이 에이전트를 정의하는 것 — 저희 팀도 처음에 문자열 응답으로 시작했다가 나중에 Pydantic 모델로 마이그레이션하면서 예상보다 훨씬 많은 코드를 손봤습니다.output_type은 처음부터 선언해두는 게 훨씬 낫습니다. 추가 비용은 Pydantic 모델 정의 몇 줄이 전부입니다. -
의존성을 전역 상태로 관리하는 것 —
deps_type을 쓰지 않고 글로벌 DB 커넥션을 참조하면 테스트 격리가 불가능해집니다.deps를 통한 명시적 주입 패턴이 유지보수성에 훨씬 유리합니다. -
RAG 파이프라인 전체를 Pydantic AI로 구축하려는 것 — Pydantic AI는 에이전트 레이어에 특화되어 있습니다. 다양한 문서 로더·임베딩 파이프라인이 필요한 RAG는 LangChain이 더 풍부한 생태계를 갖고 있습니다. 두 가지를 혼합해서 쓰는 것도 충분히 현실적인 선택입니다.
마치며
output_type → deps_type → TestModel 세 가지 패턴을 직접 써볼 수 있는 코드와 함께 살펴봤습니다. 단일 에이전트 수준에서는 이 조합만으로 대부분의 프로덕션 타입 안전성 요구사항을 충족할 수 있습니다. 자연스럽게 남는 질문은 하나입니다 — 이 에이전트가 프로덕션에서 정확히 무슨 일을 하고 있는지, 토큰 비용은 어디서 오는지.
Python 백엔드 위주로 작업하고 LLM 출력을 프로그래밍 방식으로 소비해야 하는 프로젝트라면 지금 바로 시작해볼 수 있습니다.
-
pip install pydantic-ai로 설치 → 기존에 딕셔너리로 처리하던 LLM 응답 하나를 Pydantic BaseModel로 교체해볼 수 있습니다.output_type파라미터 하나만 추가하면 IDE 자동완성이 즉시 달라집니다. -
deps_type도입 → DB 커넥션이나 HTTP 클라이언트를 전역 상태 대신 명시적 의존성으로 리팩터링해볼 수 있습니다.TestModel()을 활용한 단위 테스트 작성이 자연스럽게 가능해집니다. -
FastAPI 엔드포인트 하나에 연결 →
response_model과output_type을 같은 Pydantic 모델로 공유하면 직렬화 코드 없이 깔끔하게 통합되는 패턴을 바로 확인할 수 있습니다.
참고 자료
공식 문서
비교 분석
- Pydantic AI — Thoughtworks Technology Radar
- PydanticAI v1: The Type-Safe Agent Framework Rewriting the Python Agent Stack — AgentMarketCap
- Pydantic AI vs LangChain 2026: Type-Safe or Flexible — Which Wins? — Kunal Ganglani
- LangChain vs PydanticAI for building an AI Agent — Medium
- The 2026 AI Agent Framework Decision Guide — DEV Community
심화 학습
- Pydantic AI: Build Type-Safe LLM Agents in Python — Real Python
- Pydantic AI Tutorial: How I Build Type-Safe AI Agents That Actually Work in Production — DEV Community
- Building AI Agents in Python with Pydantic AI — MachineLearningMastery
- Pydantic AI and MCP: Building Production-Grade AI Applications — Medium
- What is Pydantic AI? Type-Safe Agent Framework in 2026 — FutureAGI