PydanticAI 실전 가이드 — FastAPI 방식으로 만드는 타입 안전한 AI 에이전트
Pydantic 팀이 만든 AI 에이전트 프레임워크 PydanticAI 실전 튜토리얼. 타입 안전 Tool 주입, 구조화 출력(Structured Outputs), Pydantic Logfire 모니터링, FastAPI 통합, LangGraph·CrewAI 비교까지 프로덕션 패턴을 코드와 함께 정리한다.
PydanticAI는 FastAPI를 만든 Pydantic 팀이 2024년 말 출시한 Python AI 에이전트 프레임워크다. LangGraph·CrewAI와 달리 타입 안전성을 핵심 설계 원칙으로 삼아, 에이전트 도구 입출력부터 최종 응답까지 Pydantic 모델로 검증한다. 2026년 4월 기준 GitHub 16,000+ stars, FastAPI·SQLModel 사용자라면 진입 장벽이 거의 없다.
이 글이 필요한 사람: FastAPI로 API를 만들고 있고 AI 에이전트를 프로덕션에 붙이려는 백엔드 개발자, LangChain 복잡도에 지쳐 대안을 찾는 개발자, 에이전트 런타임 오류를 개발 시점에 잡고 싶은 팀.
PydanticAI란 무엇인가 — FastAPI 철학을 에이전트에 적용한 프레임워크
PydanticAI는 Pydantic v2의 타입 검증 엔진을 AI 에이전트에 그대로 가져온 프레임워크다. FastAPI가 HTTP 핸들러의 입력을 Pydantic 모델로 검증하듯, PydanticAI는 에이전트 Tool 함수의 인수와 반환값, 최종 응답 스키마를 Python 타입으로 강제한다.
Pydantic 팀(Samuel Colvin 등)이 직접 만든 만큼, 기존 Pydantic·FastAPI 생태계와 자연스럽게 연결된다. 모델 비종속(model-agnostic) 설계로 OpenAI GPT-5.4, Claude Sonnet 4.6, Google Gemini 모두 한 줄 교체로 전환 가능하다.
핵심 설계 원칙 3가지:
타입 우선(Type-first): 도구·출력을 Python 타입으로 선언하면 런타임 검증이 자동 적용됨
모델 비종속: model="claude-sonnet-4-6" → model="gpt-5.4" 한 줄 변경
테스트 가능성: 에이전트를 순수 Python 함수처럼 단위 테스트 가능
PydanticAI는 모델 레이어·도구 레이어·검증 레이어가 타입으로 연결된 구조다 (출처: PydanticAI 공식 문서)
설치와 첫 번째 에이전트 만들기
Python 3.10 이상 환경에서 아래 명령으로 설치한다. 사용할 모델 프로바이더에 따라 extras를 추가한다.
설치
# OpenAI 또는 Anthropic 프로바이더 선택
pip install pydantic-ai
# 사용할 모델에 따라 SDK 별도 설치
pip install openai # GPT-5.4 등
pip install anthropic # Claude Sonnet 4.6 등
pip install google-generativeai # Gemini
가장 단순한 에이전트는 아래처럼 만든다. Agent 클래스에 모델명과 시스템 프롬프트만 지정하면 동작한다.
첫 번째 에이전트 (Claude Sonnet 4.6 사용)
from pydantic_ai import Agent
agent = Agent(
model="claude-sonnet-4-6",
system_prompt="당신은 Python 전문가입니다. 간결하고 정확하게 답하세요."
)
result = agent.run_sync("PydanticAI와 LangChain의 차이를 세 줄로 요약해줘")
print(result.data)
print(f"토큰 비용: {result.usage()}")
환경변수 설정 필수 Claude 사용 시 ANTHROPIC_API_KEY, OpenAI 사용 시 OPENAI_API_KEY를 .env에 설정해야 한다. PydanticAI는 python-dotenv를 자동으로 읽지 않으므로 load_dotenv()를 명시적으로 호출하거나 OS 환경변수로 주입해야 한다.
타입 안전 도구(Tool) 주입 실전 — 런타임 에러를 개발 시점에 잡는 법
PydanticAI의 가장 강력한 기능은 타입 안전 Tool이다. @agent.tool 데코레이터로 등록된 함수의 인수·반환값은 Pydantic이 자동 검증한다. LLM이 잘못된 타입의 인수를 넘기면 에이전트가 자동으로 오류를 감지하고 재시도한다.
타입 안전 Tool 등록 예시
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
from typing import Optional
import httpx
class WeatherResult(BaseModel):
city: str
temp_celsius: float
condition: str
humidity: Optional[int] = None
agent = Agent(
model="claude-sonnet-4-6",
system_prompt="날씨 정보를 조회해 사용자에게 알려주세요.",
result_type=str # 최종 응답은 문자열
)
@agent.tool
async def get_weather(ctx: RunContext, city: str) -> WeatherResult:
"""도시명을 받아 현재 날씨를 반환합니다."""
# 실제 API 호출 (예시)
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.weatherapi.com/v1/current.json",
params={"key": ctx.deps.weather_api_key, "q": city}
)
data = resp.json()
return WeatherResult(
city=data["location"]["name"],
temp_celsius=data["current"]["temp_c"],
condition=data["current"]["condition"]["text"],
humidity=data["current"]["humidity"]
)
# LLM이 city에 int를 넘기려 하면 Pydantic이 즉시 오류를 잡음
RunContext는 에이전트 실행 컨텍스트로, 외부 API 키·DB 연결·유저 정보 등 의존성을 타입 안전하게 주입한다. FastAPI의 Depends()와 유사한 개념이다. 의존성 타입은 Agent(deps_type=MyDeps)로 선언하며, ctx.deps에서 IDE 자동완성이 동작한다.
LLM → Tool 호출 → Pydantic 검증 → 에러 시 자동 재시도 흐름 (출처: PydanticAI 공식 문서)
구조화 출력(Structured Outputs)으로 에이전트 응답을 스키마로 강제하기
에이전트의 최종 응답을 자유 텍스트 대신 Pydantic 모델로 강제할 수 있다. LLM이 스키마에서 벗어난 응답을 내면 자동으로 재시도하며, 최종적으로 타입이 보장된 Python 객체를 반환한다. CrewAI·LangChain에서 흔히 발생하는 "JSON 파싱 오류" 문제를 원천 차단하는 방식이다.
구조화 출력 — Pydantic 모델을 result_type으로 지정
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from typing import List
class TechAnalysis(BaseModel):
summary: str = Field(description="한 줄 요약")
pros: List[str] = Field(description="장점 목록 (3~5개)")
cons: List[str] = Field(description="단점 목록 (2~4개)")
recommendation: str = Field(description="누구에게 추천하는지")
confidence: float = Field(ge=0.0, le=1.0, description="분석 신뢰도 0~1")
agent = Agent(
model="claude-sonnet-4-6",
result_type=TechAnalysis, # 응답을 TechAnalysis 스키마로 강제
system_prompt="기술 도구를 분석해 구조화된 평가를 제공합니다."
)
result = agent.run_sync("PydanticAI 프레임워크를 분석해줘")
analysis: TechAnalysis = result.data # IDE에서 타입 자동완성 동작
print(f"요약: {analysis.summary}")
print(f"신뢰도: {analysis.confidence:.0%}")
Structured Outputs vs JSON Mode 차이 일반 JSON Mode는 LLM에게 JSON을 출력하도록 요청할 뿐, 스키마 준수를 보장하지 않는다. PydanticAI의 result_type은 OpenAI의 Structured Outputs API(function_call 기반 스키마 강제)나 Claude의 tool_use를 내부적으로 활용해 모델 수준에서 스키마를 강제한다. Pydantic 검증까지 이중으로 거친다.
Pydantic Logfire로 에이전트 비용·성능 실시간 모니터링
PydanticAI는 Pydantic Logfire(OpenTelemetry 기반 옵저버빌리티 플랫폼)와 네이티브 통합된다. 에이전트 실행 추적, 토큰 비용, 도구 호출 횟수, 응답 시간을 대시보드에서 실시간으로 확인할 수 있다.
Logfire 통합 설정
pip install logfire
# main.py 또는 에이전트 초기화 코드 최상단에 추가
import logfire
logfire.configure() # LOGFIRE_TOKEN 환경변수에서 자동 읽음
logfire.instrument_pydantic_ai() # PydanticAI 에이전트 전체 자동 계측
# 이후 agent.run_sync() 호출 시 자동으로 추적 시작
# Logfire 대시보드: https://logfire.pydantic.dev
Logfire 없이도 result.usage()로 토큰 사용량을 코드에서 직접 확인할 수 있다. 프로덕션에서는 Logfire를 붙이면 에이전트 체인 전체의 span 추적, 느린 Tool 호출 탐지, 비용 이상 알림 설정이 가능하다.
무료 플랜: 월 100만 로그 span. 소규모 프로덕션 모니터링에 충분하다.
LangGraph · CrewAI와 실전 비교 — 언제 PydanticAI를 선택해야 하는가
세 프레임워크는 각각 다른 use case에 최적화되어 있다. 맹목적으로 한 가지를 선택하기보다 팀의 상황에 맞게 골라야 한다.
PydanticAI를 선택해야 할 때:
FastAPI 기반 서비스에 AI 기능을 추가하는 경우
에이전트 출력의 타입 안전성이 비즈니스 요구사항인 경우 (금융, 의료, 법률 데이터 처리)
작은 팀이 빠르게 프로토타입 → 프로덕션 전환이 필요한 경우
LangChain 복잡도를 피하고 싶은 경우
PydanticAI를 피해야 할 때:
수십 개 에이전트가 복잡한 상태 그래프를 공유해야 하는 경우 → LangGraph 선택
비기술직 팀원도 에이전트 역할을 설정해야 하는 경우 → CrewAI 선택
프로덕션 적용 패턴 3가지 — FastAPI 통합, 비동기 처리, 에러 핸들링
실제 서비스에 PydanticAI를 붙일 때 자주 쓰이는 패턴 3가지를 정리한다.
패턴 1 — FastAPI 엔드포인트에 에이전트 연결
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from pydantic_ai import Agent
from dataclasses import dataclass
app = FastAPI()
@dataclass
class AppDeps:
db_session: str # 실제 DB 세션 타입으로 교체
user_id: str
agent = Agent(
model="claude-sonnet-4-6",
deps_type=AppDeps,
result_type=str
)
class QueryRequest(BaseModel):
question: str
user_id: str
@app.post("/ask")
async def ask(request: QueryRequest):
deps = AppDeps(db_session="db_conn", user_id=request.user_id)
result = await agent.run(request.question, deps=deps)
return {"answer": result.data, "tokens": result.usage().total_tokens}
패턴 2 — 스트리밍 응답 (SSE)
from fastapi.responses import StreamingResponse
import asyncio
@app.post("/ask/stream")
async def ask_stream(request: QueryRequest):
async def generate():
async with agent.run_stream(request.question) as stream:
async for chunk in stream.stream_text():
yield f"data: {chunk}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
패턴 3 — 재시도 + 에러 핸들링
from pydantic_ai import Agent
from pydantic_ai.exceptions import UnexpectedModelBehavior, ModelRetry
agent = Agent(
model="claude-sonnet-4-6",
retries=3 # 도구 실패 시 최대 3회 재시도
)
@agent.tool(retries=2) # 이 Tool만 2회 재시도
async def risky_api_call(ctx, query: str) -> str:
try:
# 외부 API 호출
result = await call_external_api(query)
return result
except TimeoutError:
raise ModelRetry("API 타임아웃. 재시도합니다.") # 에이전트가 자동 재시도
try:
result = await agent.run("질문")
except UnexpectedModelBehavior as e:
# 재시도 소진 후에도 실패 시
logger.error(f"에이전트 실패: {e}")
FastAPI 요청 → PydanticAI 에이전트 → Tool 실행 → 타입 검증 → 응답 흐름
비동기 주의사항 agent.run()은 코루틴이므로 반드시 await와 함께 사용해야 한다. Jupyter Notebook이나 스크립트처럼 이벤트 루프가 없는 환경에서는 agent.run_sync()를 쓴다. FastAPI 핸들러에서는 async def + await agent.run()이 정석이다.