개인정보처리방침© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
AI

Pydantic AI: Python AI 에이전트에서 타입 안전한 LLM 도구 호출 구현

RunContext · output_type · 의존성 주입으로 런타임 오류를 작성 시점에 잡아내기

LLM 기반 기능을 Python 코드에 얹다 보면 어느 순간 묘한 불안감이 생깁니다. "이 도구 함수, LLM이 엉뚱한 타입으로 호출하면 어떻게 되지?" 저도 처음에 FastAPI 프로젝트에 에이전트를 붙이면서 str이 들어와야 할 자리에 int가 들어오는 바람에 한참 디버깅한 기억이 있습니다. 런타임에서야 터지는 오류들, 테스트에서는 안 잡히다가 실제 LLM 응답에서 처음 발각되는 버그들. 아마 한 번쯤은 겪어봤을 겁니다.

문제의 근원은 단순합니다. LLM은 JSON으로 인자를 넘기는데, 타입 검증이 없으면 그게 그냥 Python 함수로 들어옵니다.

python
# 타입 검증 없는 기존 방식 — 런타임에서야 오류 발각
def process_tool_call(args: dict):
    amount = args["amount"]   # LLM이 "22.5"(str)를 넘겼지만 통과
    return amount * 1.1        # TypeError: can't multiply str by float
 
# Pydantic AI 방식 — 인자 타입이 보장되고, 오류 시 LLM에게 재시도 요청
@agent.tool
def process_amount(ctx: RunContext[str], amount: float) -> float:
    return amount * 1.1        # Pydantic이 검증 완료, float 보장

Pydantic AI는 "타입 시스템을 에이전트 레이어까지 확장"하는 방식으로 이 문제를 풀어냅니다. 도구 함수의 인자, 반환값, 의존성 주입, LLM 출력 파싱까지 전 계층에 Python 타입 힌트와 Pydantic 검증이 걸리기 때문에, 오류가 런타임이 아닌 작성 시점에 잡힙니다. FastAPI가 웹 API 개발 경험을 바꿔놓은 것처럼, Pydantic AI는 AI 에이전트 개발 경험을 같은 방향으로 당기고 있습니다. 타입 오류가 런타임에서 터지기 전에 IDE와 Pydantic이 먼저 잡아내는 구체적인 방법을 지금부터 보여드리겠습니다.


핵심 개념

도구 호출이 왜 타입 문제가 되는가

잠깐 배경 설명을 드리면, "도구 호출(Tool Call 또는 Function Calling)"은 LLM이 외부 함수를 실행해 정보를 얻거나 작업을 수행하는 메커니즘입니다. 동작 방식은 이렇습니다. 우리가 함수 시그니처와 설명을 JSON 스키마 형태로 LLM에 전달하면, LLM은 대화 흐름에 맞게 적절한 함수를 선택하고 인자를 JSON으로 생성해서 넘깁니다. 그 JSON을 받아 실제 Python 함수를 실행하는 건 우리 코드의 몫입니다.

여기서 문제가 생깁니다. LLM이 생성하는 JSON이 항상 우리가 기대하는 타입과 일치하지 않을 수 있거든요. "22.5"(문자열)가 float을 기대하는 자리에 들어오거나, 숫자가 들어와야 할 곳에 null이 오기도 합니다. LangChain 같은 기존 프레임워크에서는 이 부분을 직접 방어 코드로 처리해야 했는데, Pydantic AI는 이를 프레임워크 레벨에서 해결합니다.

세 가지 핵심 메커니즘

Pydantic AI를 이해하는 데 필요한 핵심 구성요소는 세 가지입니다. 각각 역할이 명확하게 나뉘어 있어서, 한 번 익혀두면 코드 읽기가 훨씬 쉬워집니다.

① @agent.tool / @agent.tool_plain — 함수를 LLM의 도구로 등록

두 데코레이터의 차이는 단순합니다. @agent.tool은 첫 번째 인자로 RunContext를 받아 의존성을 주입받을 수 있고, @agent.tool_plain은 순수 함수로 의존성 없이 동작합니다. 함수 시그니처의 타입 힌트에서 JSON 스키마가 자동 생성되어 LLM에 전달되고, 독스트링이 도구 설명과 파라미터 설명으로 그대로 쓰입니다.

쉽게 말하면, FastAPI의 라우터 함수에서 타입 힌트로 요청 검증이 되는 것처럼, 여기서는 LLM이 넘기는 JSON 인자가 같은 방식으로 검증됩니다.

② RunContext[DepsType] — 의존성을 타입 안전하게 주입받는 컨테이너

FastAPI의 Depends랑 비슷한 역할인데, LLM 레이어까지 타입이 이어진다는 게 핵심입니다. 데이터베이스 커넥션, API 클라이언트, 설정 객체 같은 것들을 도구 함수가 직접 생성하지 않고 외부에서 받아 쓰는 구조입니다. deps_type이 잘못 선언되면 mypy/pyright가 즉시 검출해줍니다.

③ Agent[ResultType] — LLM 출력을 Pydantic 모델로 자동 검증

제네릭 타입 파라미터로 출력 타입을 선언하면, LLM 응답이 해당 Pydantic 모델로 자동 파싱·검증됩니다. 파싱 실패 시 Pydantic 오류 메시지가 피드백으로 LLM에 전달되어 재시도를 유도합니다. retries=2처럼 최대 재시도 횟수를 명시하면 무한 루프를 방어할 수 있습니다.

python
import os
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
 
MODEL = os.getenv("LLM_MODEL", "anthropic:claude-sonnet-4-20250514")
 
class WeatherResult(BaseModel):
    city: str
    temperature: float
    condition: str
 
agent = Agent(
    MODEL,
    deps_type=str,              # 주입할 의존성 타입 (여기선 API 키)
    output_type=WeatherResult,  # LLM 출력의 검증 타입
    retries=2,                  # 검증 실패 시 최대 재시도 횟수
)
 
@agent.tool_plain
def get_time() -> str:
    """현재 UTC 시각을 반환합니다."""
    from datetime import datetime, timezone
    return datetime.now(timezone.utc).isoformat()
 
@agent.tool
def get_weather(ctx: RunContext[str], city: str) -> dict:
    """특정 도시의 날씨를 조회합니다.
 
    city: 조회할 도시명 (한글 가능)
    """
    api_key = ctx.deps  # 타입 안전하게 주입된 의존성
    return {"city": city, "temperature": 22.5, "condition": "맑음"}
 
result = agent.run_sync("서울 날씨 알려줘", deps="my-api-key")
print(result.output.temperature)  # float임이 타입 시스템에서 보장됨

독스트링이 JSON 스키마의 description 필드로 자동 채워진다는 점이 실무에서 특히 유용합니다. LLM이 도구를 언제 어떻게 써야 하는지를 독스트링 하나로 제어할 수 있거든요.

자동 스키마 생성: 타입 힌트 → JSON 스키마

솔직히 이 부분이 가장 편리한 지점입니다. 별도로 JSON 스키마를 손으로 작성할 필요가 없습니다.

python
from pydantic import BaseModel, Field
from typing import Optional
 
class SearchParams(BaseModel):
    query: str = Field(description="검색어")
    max_results: int = Field(default=10, ge=1, le=100, description="최대 결과 수")
    language: Optional[str] = Field(default=None, description="언어 코드 (예: ko, en)")
 
@agent.tool
def search_docs(ctx: RunContext[str], params: SearchParams) -> list[str]:
    """문서를 검색합니다."""
    ...

주의할 게 하나 있는데, Field(ge=1, le=100) 같은 제약 조건은 두 가지 역할을 동시에 합니다. 하나는 이 제약을 JSON 스키마에 포함해서 LLM에게 "1에서 100 사이의 값을 생성하라"는 힌트를 주는 것이고, 다른 하나는 LLM이 힌트를 무시하고 범위를 벗어난 값을 생성했을 때 Pydantic이 런타임에서 잡아내어 LLM에게 오류를 피드백하는 것입니다. LLM이 항상 스키마를 완벽하게 따르지는 않으니, Pydantic의 런타임 검증이 최후 방어선 역할을 한다고 보면 됩니다.


실전 적용

예시들이 단계적으로 복잡해지는 구성입니다. 상황에 맞게 골라 참고하시면 좋을 것 같습니다.

  • 예시 1 (⭐): LLM 출력 검증만 필요한 경우 — 비정형 텍스트 구조화
  • 예시 2 (⭐⭐): DB·외부 API 의존성이 있는 경우 — 고객 지원 에이전트
  • 예시 3 (⭐⭐): FastAPI 프로젝트에 AI 스트리밍을 추가하는 경우
  • 예시 4 (⭐⭐⭐): 여러 에이전트를 조합하는 경우 — 멀티 에이전트 오케스트레이션

예시 1: 비정형 텍스트에서 구조화된 데이터 추출 ⭐

영수증, 계약서, 이메일 같은 비정형 텍스트에서 필요한 정보를 추출할 때 타입 검증의 가치가 극대화됩니다. 추출된 데이터를 DB에 바로 인서트하는 파이프라인에서 특히 유용합니다.

python
import asyncio
import os
from pydantic import BaseModel, field_validator  # Pydantic v2 문법
from pydantic_ai import Agent
from typing import Optional
 
MODEL = os.getenv("LLM_MODEL", "openai:gpt-4.1")
 
class InvoiceData(BaseModel):
    vendor: str
    amount: float
    date: str
    items: list[str]
    currency: Optional[str] = "KRW"
 
    @field_validator('amount', mode='before')  # Pydantic v2: @field_validator
    @classmethod
    def amount_must_be_positive(cls, v):
        v = float(v)
        if v <= 0:
            raise ValueError('금액은 양수여야 합니다')
        return v
 
agent = Agent(MODEL, output_type=InvoiceData, retries=2)
 
async def extract_invoice(raw_text: str) -> InvoiceData:
    result = await agent.run(f"다음 영수증에서 정보를 추출해줘:\n{raw_text}")
    return result.output  # InvoiceData 인스턴스, 타입 보장됨
 
async def main():
    raw_text = """
    거래처: (주)테크솔루션
    결제금액: 550,000원
    결제일: 2026-05-15
    항목: 클라우드 서버비용, 모니터링 도구 라이선스
    """
    invoice = await extract_invoice(raw_text)
 
    print(f"거래처: {invoice.vendor}")           # str
    print(f"금액: {invoice.amount:,.0f}원")       # float
    print(f"항목: {', '.join(invoice.items)}")    # list[str]
 
    # DB 인서트 전 타입 오류 걱정 없음 (db는 실제 DB 클라이언트)
    await db.insert("invoices", invoice.model_dump())
 
asyncio.run(main())

Pydantic v2에서는 @validator 대신 @field_validator를 사용합니다. Pydantic AI는 Pydantic v2를 기반으로 하므로, 구버전 @validator를 쓰면 PydanticUserError가 발생합니다.

요소 역할
output_type=InvoiceData LLM 출력을 InvoiceData로 파싱·검증
@field_validator 추가 비즈니스 로직 검증 (금액 양수 체크 등)
model_dump() Pydantic 모델을 dict로 변환, DB 인서트에 바로 활용

예시 2: 의존성 주입 패턴으로 고객 지원 에이전트 구성 ⭐⭐

실무에서 자주 맞닥뜨리는 상황인데, 도구 함수가 DB나 외부 API에 접근해야 할 때 어떻게 의존성을 전달하느냐가 관건입니다. RunContext를 활용하면 테스트 시 mock 교체도 깔끔합니다.

python
import os
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
 
MODEL = os.getenv("LLM_MODEL", "anthropic:claude-sonnet-4-20250514")
 
# AsyncDatabase, ExternalAPIClient는 실제 프로젝트에서 정의하는 클라이언트입니다.
# 예: asyncpg.Connection, httpx.AsyncClient 등
@dataclass
class SupportDeps:
    db: "AsyncDatabase"
    user_id: int
    api_client: "ExternalAPIClient"
 
agent = Agent(
    MODEL,
    deps_type=SupportDeps,
    system_prompt="고객 지원 에이전트입니다. 정확한 정보만 제공합니다.",
    retries=2,
)
 
@agent.tool
async def get_order_status(ctx: RunContext[SupportDeps], order_id: str) -> str:
    """주문 상태를 조회합니다.
 
    order_id: 조회할 주문 ID (예: ORD-2026-001234)
    """
    order = await ctx.deps.db.fetch_order(ctx.deps.user_id, order_id)
    if not order:
        return "해당 주문을 찾을 수 없습니다."
    return f"주문 {order_id} 상태: {order.status} (예상 도착: {order.estimated_arrival})"
 
@agent.tool
async def get_product_info(ctx: RunContext[SupportDeps], product_id: str) -> dict:
    """상품 정보를 조회합니다.
 
    product_id: 조회할 상품 ID
    """
    return await ctx.deps.api_client.fetch_product(product_id)
 
# 실제 사용
async def handle_request(user_id: int, message: str):
    deps = SupportDeps(db=real_db, user_id=user_id, api_client=real_client)
    result = await agent.run(message, deps=deps)
    return result.output
 
# 테스트 시 — mock 객체로 교체하면 됩니다
async def test_order_status():
    test_deps = SupportDeps(db=mock_db, user_id=99999, api_client=mock_client)
    result = await agent.run("주문 ORD-2026-001234 상태 알려줘", deps=test_deps)
    assert "상태" in result.output

ctx.deps의 타입이 SupportDeps로 고정되기 때문에, ctx.deps.db.fetch_order를 호출할 때 IDE가 자동완성을 제공하고 typo를 잡아줍니다. 처음엔 dataclass 대신 dict를 쓰고 싶은 생각이 들 수 있는데, mypy가 타입 안전성을 제대로 검증하려면 구조체 타입(dataclass 또는 BaseModel)을 쓰는 게 훨씬 낫습니다.

예시 3: FastAPI와 실시간 스트리밍 통합 ⭐⭐

FastAPI 프로젝트에 AI 스트리밍 엔드포인트를 추가할 때 Pydantic 모델을 그대로 공유할 수 있어 코드 중복이 거의 없습니다. FastAPI의 Depends를 써본 적이 있다면 의존성 주입 흐름이 익숙하게 느껴질 겁니다.

python
import os
from fastapi import FastAPI, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from pydantic_ai import Agent
 
MODEL = os.getenv("LLM_MODEL", "anthropic:claude-sonnet-4-20250514")
app = FastAPI()
 
class ChatRequest(BaseModel):
    message: str
    user_id: int
 
agent = Agent(MODEL, deps_type=SupportDeps, retries=2)
 
# get_db(), get_client()는 실제 프로젝트에서 정의하는 팩토리 함수입니다.
def get_deps(request: ChatRequest) -> SupportDeps:
    return SupportDeps(
        db=get_db(),
        user_id=request.user_id,
        api_client=get_client(),
    )
 
@app.post("/chat/stream")
async def chat_stream(
    request: ChatRequest,
    deps: SupportDeps = Depends(get_deps),
):
    async def generate():
        async with agent.run_stream(request.message, deps=deps) as response:
            async for chunk in response.stream_text():
                yield f"data: {chunk}\n\n"
        yield "data: [DONE]\n\n"
 
    return StreamingResponse(generate(), media_type="text/event-stream")
 
@app.post("/chat")
async def chat(
    request: ChatRequest,
    deps: SupportDeps = Depends(get_deps),
):
    result = await agent.run(request.message, deps=deps)
    return {"response": result.output}

흥미로운 점은 ChatRequest 모델을 FastAPI의 요청 검증과 Pydantic AI의 의존성 주입이 동시에 활용한다는 겁니다. 같은 Pydantic 생태계 안에 있으니 모델 정의를 중복으로 작성할 필요가 없습니다.

예시 4: 멀티 에이전트 오케스트레이션 ⭐⭐⭐

에이전트를 다른 에이전트의 도구로 등록하는 패턴입니다. 코드 리뷰 시스템을 예로 들면, 분석·보안 검토를 각각 독립 에이전트로 분리하고 코디네이터가 이를 조합합니다.

python
import os
from pydantic_ai import Agent, RunContext
 
FAST_MODEL = os.getenv("FAST_LLM_MODEL", "openai:gpt-4.1")
MAIN_MODEL = os.getenv("LLM_MODEL", "anthropic:claude-sonnet-4-20250514")
 
# 서브 에이전트들 — API 키를 deps로 전달하는 경우 str 타입 사용
code_analyzer = Agent(FAST_MODEL, deps_type=str, output_type=str)
security_reviewer = Agent(MAIN_MODEL, deps_type=str, output_type=str)
 
coordinator = Agent(
    MAIN_MODEL,
    deps_type=str,
    system_prompt="코드 리뷰를 총괄합니다. 분석, 보안 검토 결과를 종합해 최종 리뷰를 작성합니다.",
    retries=2,
)
 
@coordinator.tool
async def analyze_code(ctx: RunContext[str], code: str) -> str:
    """코드 품질과 구조를 분석합니다.
 
    code: 분석할 소스 코드
    """
    result = await code_analyzer.run(
        f"다음 코드를 분석해줘:\n{code}",
        deps=ctx.deps,  # API 키를 서브 에이전트에 그대로 전달
    )
    return result.output
 
@coordinator.tool
async def review_security(ctx: RunContext[str], code: str) -> str:
    """보안 취약점을 검토합니다.
 
    code: 검토할 소스 코드
    """
    result = await security_reviewer.run(
        f"보안 관점에서 리뷰해줘:\n{code}",
        deps=ctx.deps,
    )
    return result.output
 
async def review_code(user_code: str, api_key: str) -> str:
    result = await coordinator.run(
        f"다음 코드를 리뷰해줘:\n{user_code}",
        deps=api_key,
    )
    return result.output

주의할 게 하나 있는데, 서브 에이전트에 deps를 넘길 때 타입이 일관되어야 합니다. 여기서는 모든 에이전트가 deps_type=str(API 키)로 통일되어 있습니다. 실제 프로젝트에서 각 에이전트마다 필요한 의존성이 달라진다면, 코디네이터가 적절한 타입으로 변환해서 넘기는 방식을 쓰면 됩니다.


장단점 분석

직접 써보면서 느낀 점을 정리하면, 결정적인 차이는 두 가지라고 봅니다. 하나는 "타입 안전성이 얼마나 중요한 프로젝트인가", 다른 하나는 "기존 Python 생태계(FastAPI, Pydantic)와 얼마나 깊이 통합되어 있는가"입니다. 이 두 조건이 모두 해당된다면 Pydantic AI가 단연 최선의 선택지입니다.

장점

항목 내용
엔드-투-엔드 타입 안전성 에이전트·도구·출력 전 계층에 IDE 자동완성과 mypy/pyright 검증이 적용됨
자동 스키마 생성 타입 힌트와 독스트링만으로 LLM에 전달될 JSON 스키마가 자동 생성
내장 재시도 로직 LLM이 잘못된 인자를 반환하면 Pydantic 오류를 피드백으로 전달하고 자동 재시도. retries 파라미터로 상한 설정 가능
깔끔한 DI 패턴 RunContext로 DB·API·설정을 도구 함수에 타입 안전하게 주입, 테스트 교체 용이
모델 무관 API OpenAI·Anthropic·Gemini·Bedrock·Ollama를 동일한 인터페이스로 전환 가능
FastAPI 친화성 동일한 Pydantic 모델과 async 패턴을 공유하여 기존 FastAPI 프로젝트에 자연스럽게 통합

단점 및 주의사항

항목 내용 대응 방안
생태계 규모 LangChain 대비 약 15배 작은 커뮤니티, 사전 구축된 통합 수가 적음 직접 @agent.tool로 커스텀 통합 구현, GitHub Discussions 활용
보안·컴플라이언스 미지원 내장 RBAC, 프롬프트 인젝션 감지, 가드레일 없음 별도 보안 레이어(미들웨어, 게이트웨이) 추가 필요
공급자 고급 기능 접근 제한 최소공배수 추상화로 공급자별 고급 기능 활용이 어려울 수 있음 공급자 SDK를 직접 호출하는 @agent.tool_plain 래퍼 작성
그래프 기반 워크플로 부재 복잡한 상태 기계나 조건 분기가 많은 워크플로에 부적합 LangGraph와 병행 사용 검토

실무에서 가장 흔한 실수

  1. @validator 대신 @field_validator 사용 누락: Pydantic AI는 Pydantic v2 기반입니다. v1 스타일의 @validator를 쓰면 PydanticUserError가 발생합니다. v2에서는 @field_validator('field_name', mode='before')를 사용해야 합니다.

  2. deps_type과 실제 deps 타입 불일치: Agent(deps_type=SupportDeps)로 선언하고 agent.run(deps={"db": ...})처럼 dict를 넘기는 경우입니다. pyright가 잡아주지만 런타임에서야 발견되는 경우도 있으므로, 처음부터 dataclass나 BaseModel을 쓰는 것을 권장합니다.

  3. 독스트링 빈약하게 작성: @agent.tool 함수의 독스트링이 LLM에 전달되는 도구 설명입니다. "조회합니다" 한 줄만 적으면 LLM이 언제 이 도구를 써야 하는지 판단하기 어려워집니다. 파라미터 설명과 사용 예시를 충분히 담아주는 것이 호출 정확도를 높입니다.


마치며

Pydantic AI를 실제 프로젝트에 써보면서 느낀 건, 타입 오류가 줄었다기보다 오류가 발생하는 시점이 바뀌었다는 겁니다. 런타임에서 갑자기 터지던 LLM 응답 파싱 실패가 이제는 IDE에서 빨간 줄로, 혹은 Pydantic 재시도 피드백으로 훨씬 일찍 수면 위로 올라옵니다. 특히 FastAPI와 함께 쓸 때 Pydantic 모델을 중복 정의 없이 공유할 수 있다는 점이 생산성에 직접적으로 와닿았습니다.

지금 바로 시작해볼 수 있는 3단계:

  1. 설치 및 첫 에이전트 실행: pip install 'pydantic-ai[openai]' (또는 pydantic-ai[anthropic])로 설치한 뒤, 위 날씨 예제를 복사해 실제 API 키로 agent.run_sync()를 실행해볼 수 있습니다. 환경변수 OPENAI_API_KEY 또는 ANTHROPIC_API_KEY가 필요합니다.

  2. 기존 도구 함수 타입 강화: 이미 LLM 도구 함수를 작성했다면 @agent.tool과 RunContext를 적용해 의존성 주입 패턴으로 리팩토링해볼 수 있습니다. mypy를 함께 실행하면 타입 불일치를 바로 확인할 수 있습니다.

  3. 출력 타입 검증 추가: LLM 응답을 str로 받아 직접 파싱하는 코드가 있다면 Pydantic BaseModel을 정의하고 output_type으로 지정해볼 수 있습니다. 재시도 로직이 자동으로 붙으면서 파싱 실패 케이스가 크게 줄어듭니다.


참고 자료

  • Function Tools | Pydantic AI 공식 문서
  • Dependencies & RunContext | Pydantic AI 공식 문서
  • Output 검증 | Pydantic AI 공식 문서
  • pydantic/pydantic-ai | GitHub
  • Build Type-Safe LLM Agents in Python | Real Python
  • Type-safe LLM agents with PydanticAI | Paul Simmering
  • PydanticAI v1: The Type-Safe Agent Framework | AgentMarketCap
  • PydanticAI vs LangChain vs LangGraph: Which Wins in 2026?
  • Building Type-Safe LLM Agents With Pydantic AI | n1n.ai
  • Pydantic AI | Thoughtworks Technology Radar
  • Bulletproof Agentic Workflows with PydanticAI | MarkTechPost
#PydanticAI#Python#LLM#타입안전성#AI에이전트#FastAPI#의존성주입#Pydantic#멀티에이전트#FunctionCalling
공유하기

목차

핵심 개념도구 호출이 왜 타입 문제가 되는가세 가지 핵심 메커니즘자동 스키마 생성: 타입 힌트 → JSON 스키마실전 적용예시 1: 비정형 텍스트에서 구조화된 데이터 추출 ⭐예시 2: 의존성 주입 패턴으로 고객 지원 에이전트 구성 ⭐⭐예시 3: FastAPI와 실시간 스트리밍 통합 ⭐⭐예시 4: 멀티 에이전트 오케스트레이션 ⭐⭐⭐장단점 분석장점단점 및 주의사항실무에서 가장 흔한 실수마치며참고 자료

추천 포스트

LLM 평가 프레임워크 직접 구축 vs 기성 도구: 2026 팀별 선택 기준
AI

LLM 평가 프레임워크 직접 구축 vs 기성 도구: 2026 팀별 선택 기준

RAG·챗봇·에이전트를 프로덕션에 올리는 팀이라면 지금 이 선택이 기다리고 있습니다 AI 기능을 제품에 붙이고 나서 "이게 동작하는 건 맞는데, 얼마나 잘 동작하는지는 어떻게 알지?"라는 질문을 해본 적 있다면, 딱 맞는 시점에 닿은 겁니다. 저도 처음엔 "프롬프트 몇 개 수동으로...

2026년 05월 30일읽는 데 24분
OpenTelemetry로 LLM 트레이싱 구축하기: RAG·멀티에이전트 흐름을 gen_ai 표준으로 추적하는 법
AI

OpenTelemetry로 LLM 트레이싱 구축하기: RAG·멀티에이전트 흐름을 gen_ai 표준으로 추적하는 법

GPT-4를 붙여놓은 서비스가 갑자기 엉뚱한 답을 내놓기 시작했다. 로그를 뒤져봐도 에러는 없다. HTTP 응답코드도 200이다. 근데 사용자는 화가 났다. 이 상황, 한 번쯤 겪어보셨을 것 같다. 전통적인 APM으로는 "LLM 호출에 1.2초 걸렸음"까지만 알 수 있고, 정작 중요한 ...

2026년 05월 30일읽는 데 25분
멀티모달 RAG 파이프라인 구축: 이미지·표를 LLM이 이해하게 만드는 법
AI

멀티모달 RAG 파이프라인 구축: 이미지·표를 LLM이 이해하게 만드는 법

RAG를 처음 도입했을 때 저도 비슷한 경험을 했습니다. PDF 몇 백 개를 파싱해서 벡터 DB에 넣고 검색해보니, 텍스트로 잘 설명된 내용은 꽤 잘 뽑아왔는데, 문서 중간에 끼어있던 차트나 제품 사진, 회로 다이어그램은 완전히 무시되더군요. 그 시각 정보가 사실상 문서의 핵심인 경우가...

2026년 05월 30일읽는 데 20분
AI 에이전트 장기 메모리 비교: Mem0 vs Letta vs Zep — 세 가지 철학과 실전 선택 기준
AI

AI 에이전트 장기 메모리 비교: Mem0 vs Letta vs Zep — 세 가지 철학과 실전 선택 기준

LLM 기반 앱을 한 번이라도 만들어봤다면 반드시 이 벽에 부딪힌다. "지난 대화를 어떻게 기억하게 할까?" 컨텍스트 윈도우에 전체 대화를 욱여넣으면 되겠지 싶지만, 현실은 그렇지 않다. 토큰 비용이 폭발하고, 대화가 길어질수록 LLM의 집중력은 흐트러지며, 세션이 끊기면 모든 것이 사...

2026년 05월 30일읽는 데 29분
LangGraph Supervisor Pattern: 멀티 에이전트 시스템에서 제어를 잃지 않으려면
AI

LangGraph Supervisor Pattern: 멀티 에이전트 시스템에서 제어를 잃지 않으려면

멀티 에이전트 시스템을 처음 설계할 때 가장 많이 저지르는 실수가 있습니다. "에이전트끼리 알아서 협력하겠지"라는 막연한 기대 아래 각 에이전트를 느슨하게 연결해두는 것이죠. 저도 처음엔 그렇게 생각했는데, 결과는 언제나 같았습니다. 제어 흐름이 어디 있는지 알 수 없고, 어디서 실패했...

2026년 05월 30일읽는 데 22분
AI 에이전트 88%가 프로덕션에 실패하는 이유: 5계층 하네스 아키텍처가 그 답이다
AI

AI 에이전트 88%가 프로덕션에 실패하는 이유: 5계층 하네스 아키텍처가 그 답이다

GPT-4가 처음 나왔을 때, 저도 그랬고 주변 개발자들도 다들 비슷한 착각을 했습니다. "모델만 좋으면 되는 거 아닌가?" 프롬프트 몇 줄 붙여서 프로토타입 만들고, 그럴싸하게 돌아가는 것 같으니 프로덕션 배포. 그리고 얼마 지나지 않아 에이전트가 엉뚱한 파일을 삭제하거나, 컨텍스트를...

2026년 05월 29일읽는 데 28분