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

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

GPT-4를 붙여놓은 서비스가 갑자기 엉뚱한 답을 내놓기 시작했다. 로그를 뒤져봐도 에러는 없다. HTTP 응답코드도 200이다. 근데 사용자는 화가 났다. 이 상황, 한 번쯤 겪어보셨을 것 같다. 전통적인 APM으로는 "LLM 호출에 1.2초 걸렸음"까지만 알 수 있고, 정작 중요한 "어떤 프롬프트가 들어가서 무슨 답이 나왔는가"는 아무것도 알 수 없다. LLM 시스템은 비결정론적(non-deterministic)이라 동일한 입력에도 매번 다른 결과가 나오기 때문에, 장애 재현을 위해선 당시의 정확한 입력, 모델 파라미터, 온도 값까지 모두 캡처해둬야 한다.

이 문제를 풀기 위해 등장한 것이 OpenTelemetry(OTel)의 GenAI Semantic Conventions다. CNCF 표준 관찰 가능성 프레임워크인 OTel에 LLM 전용 시맨틱 레이어를 얹어서, 트레이스·메트릭·로그를 구조화된 방식으로 수집하는 접근이다. Datadog·Grafana·AWS 같은 주요 벤더들이 네이티브 지원을 선언할 만큼 사실상 업계 표준으로 자리잡아가고 있다.

이 글을 다 읽고 나면, 기존 OpenAI 프로젝트에 트레이싱을 붙이고 Jaeger에서 RAG 파이프라인의 전체 흐름을 시각화하는 것까지 직접 해볼 수 있다. Python 기준으로 작성했으며, JavaScript/TypeScript를 쓰고 있다면 OpenLLMetry 공식 저장소에서 동일한 패턴으로 적용된다.


핵심 개념

OTel이 LLM에 다르게 접근하는 이유

일반적인 서비스 트레이싱은 결정론적이다. DB 쿼리가 느리면 느린 쿼리 로그가 있고, 함수 실행 시간은 입력이 같으면 비슷하게 나온다. 그런데 LLM은 다르다. 같은 프롬프트를 temperature=0.8로 열 번 보내면 열 개의 다른 답이 나온다. 게다가 입력 토큰 수에 따라 비용이 달라지고, 모델이 중간에 바뀌기도 한다.

이 특수성 때문에 OTel은 gen_ai.* 네임스페이스를 별도로 정의했다.

속성 설명 예시 값
gen_ai.system AI 공급자 식별 openai, anthropic
gen_ai.request.model 요청한 모델 gpt-4o
gen_ai.response.model 실제 응답한 모델 gpt-4o-2024-08-06
gen_ai.usage.input_tokens 입력 토큰 수 1234
gen_ai.usage.output_tokens 출력 토큰 수 256
gen_ai.response.finish_reasons 생성 종료 이유 ["stop"], ["length"]
gen_ai.operation.name 작업 유형 chat, embeddings

처음 이 속성 목록을 봤을 때 gen_ai.request.model과 gen_ai.response.model이 따로 있는 게 좀 의아했다. "어차피 요청한 모델이 응답하는 거 아닌가?" 싶었는데, 실제로 써보니 이 두 값이 다를 때가 생각보다 많다. API 공급자 측에서 모델 버전을 조용히 자동 업그레이드할 때다. 요청은 gpt-4o로 했는데 응답이 gpt-4o-2024-08-06으로 찍히면, 언제부터 다른 버전이 응답하고 있었는지 추적할 수 있다. 사소해 보이지만, 모델 업그레이드 이후 답변 품질이 미묘하게 달라지는 케이스를 잡을 때 꽤 유용하다.

트레이스 데이터가 흐르는 구조

LLM 호출 하나가 독립적으로 존재하지 않는다. 대부분 HTTP 요청 → 문서 검색 → LLM 호출 → 응답 포맷팅 같은 파이프라인 안에 있다. OTel은 이 전체를 하나의 Trace로 묶고, 각 단계를 Span으로 표현한다.

yaml
[사용자 요청]
    └── Trace (Root Span: HTTP 요청)
          ├── Span: RAG 문서 검색 (vector DB 쿼리)
          ├── Span: 컨텍스트 준비
          ├── Span: LLM 호출 (gen_ai.* 속성 포함)
          │     ├── Event: gen_ai.content.prompt (opt-in)
          │     └── Event: gen_ai.content.completion (opt-in)
          └── Span: 응답 포맷팅

프라이버시 설계 원칙: 프롬프트·응답 내용은 기본적으로 수집하지 않는다. OTEL_GENAI_CAPTURE_MESSAGE_CONTENT=true 환경변수로 명시적으로 활성화해야만 캡처된다. PII나 의료 정보가 트레이싱 백엔드에 그대로 흘러들어가는 사고를 막기 위한 opt-in 설계다.

한 가지 최근 변화를 짚고 넘어가면, 예전에는 프롬프트 전문을 Span 속성(attribute)에 직접 넣는 방식을 많이 썼다. 그런데 프롬프트가 수 KB씩 되다 보니 트레이싱 백엔드 성능이 눈에 띄게 나빠졌다. 그래서 지금 사양은 이를 Log-based Event로 분리하는 방향으로 진화했다. Span에는 메타데이터만, 실제 콘텐츠는 연결된 Event로 따로 보내는 방식이다.

Span 유형별 역할

LLM 시스템의 복잡성을 반영해서 Span 유형도 세분화되어 있다. 멀티 에이전트 시스템이 많아지면서 에이전트 의사결정 단계를 별도 타입으로 구분할 필요가 생겼고, gen_ai.agent.* 컨벤션이 별도 명세로 분리된 것도 이런 흐름의 일환이다. MCP(Model Context Protocol) 도구 호출 추적도 이 안에 포함되고 있다.

Span 유형 대상 작업
LLM inference chat, completion 등 추론 호출
Embeddings 텍스트 벡터화
Retrieval 벡터 DB, 문서 검색
Tool execution 에이전트가 호출하는 외부 도구
Agent steps gen_ai.agent.* — 에이전트 의사결정 단계

실전 적용

예시 1: RAG 파이프라인 엔드-투-엔드 추적

저도 처음에 RAG 파이프라인을 만들 때 가장 답답했던 게 "검색은 잘 됐는데 왜 답이 이상하지?"를 알 수 없다는 점이었다. 검색 로직 따로, LLM 호출 따로 로그를 찍어봐도 두 단계가 연결이 안 돼서 인과관계를 파악하기가 너무 어려웠다. OTel을 붙이고 나면 검색 단계와 LLM 호출 단계가 같은 트레이스 안에서 연결되니까 전체 그림이 한눈에 보인다.

먼저 SDK 초기화를 살펴보자. TracerProvider는 트레이싱 파이프라인의 진입점이고, BatchSpanProcessor는 Span을 모아서 배치로 전송해 네트워크 오버헤드를 줄이는 역할이다. OTLPSpanExporter가 실제로 OTel Collector에 Span을 보내는 컴포넌트다.

python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
 
# TracerProvider: 트레이싱 파이프라인 진입점
# BatchSpanProcessor: Span을 배치로 모아 전송 (네트워크 오버헤드 감소)
# OTLPSpanExporter: OTel Collector로 Span 전달
provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
 
# OpenAI SDK에 gen_ai.* 속성 자동 주입 활성화
OpenAIInstrumentor().instrument()
 
tracer = trace.get_tracer("rag-service", "1.0.0")
 
# 최소 시그니처 예시 — 실제 구현은 벡터 DB에 맞게 달라진다
def vector_db_search(query: str, k: int) -> list[dict]:
    """쿼리를 임베딩한 뒤 벡터 DB에서 유사 문서 k개를 반환한다"""
    ...
 
def build_prompt(query: str, docs: list[dict]) -> str:
    """검색된 문서를 컨텍스트로 조합해 프롬프트 문자열을 반환한다"""
    ...
 
def answer_question(query: str) -> str:
    with tracer.start_as_current_span("rag.pipeline") as span:
        span.set_attribute("user.query", query)
 
        # 검색 단계 — 자동 계측이 커버하지 못하므로 수동 Span 생성
        with tracer.start_as_current_span("rag.retrieval") as ret_span:
            docs = vector_db_search(query, k=5)
            ret_span.set_attribute("rag.retrieved_docs", len(docs))
            ret_span.set_attribute("rag.query_vector_dim", 1536)
 
        # LLM 호출 — OpenAIInstrumentor가 이 블록 안에서 자동으로 자식 Span을 생성
        # rag.llm_call(수동) → openai.chat(자동, gen_ai.* 속성 포함) 부모-자식 구조가 된다
        with tracer.start_as_current_span("rag.llm_call"):
            response = openai.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": build_prompt(query, docs)}],
                temperature=0.3,
            )
 
        return response.choices[0].message.content
코드 포인트 역할
OpenAIInstrumentor().instrument() OpenAI SDK 패치 — 이후 모든 chat.completions.create() 호출에 gen_ai.* 속성 자동 추가
tracer.start_as_current_span("rag.retrieval") 자동 계측이 커버하지 못하는 검색 단계를 수동으로 Span 생성
ret_span.set_attribute("rag.retrieved_docs", ...) 검색 결과 수를 Span 속성으로 기록 — 나중에 답변 품질과 교차 분석 가능
rag.llm_call(수동) + OpenAI Span(자동) 의도적인 부모-자식 중첩 구조. rag.llm_call이 비즈니스 컨텍스트를 담고, 그 안의 실제 API 호출을 자동 계측 Span이 상세하게 기록

이 구조면 Jaeger 같은 트레이스 UI에서 "검색된 문서 5개 → LLM 호출 → 토큰 1,847개 소비 → 응답 생성 완료"가 한 화면에서 보인다. "검색 결과가 부실해서 LLM이 hallucination을 일으켰다"는 인과관계도 단일 트레이스 뷰에서 파악할 수 있다.

이번엔 애플리케이션 코드 레벨에서 한 발 더 나아가서, 에이전트 프레임워크를 쓸 때 내부 동작을 어떻게 추적하는지 살펴보자.

예시 2: OpenLLMetry로 멀티 에이전트 워크플로 추적

LangChain으로 에이전트를 만들어보면 어느 순간 "얘가 지금 어디서 뭘 하고 있는 거지?"를 알 수가 없는 상황에 맞닥뜨린다. 루프를 수십 번 돌다가 컨텍스트 윈도우를 초과하는 케이스가 특히 그렇다. 에러 메시지는 있는데, 그 직전에 어떤 툴을 호출했고 어떤 판단을 했는지 파악하기가 정말 어렵다. OpenLLMetry를 쓰면 프레임워크 내부 동작이 자동으로 Span으로 분해된다.

python
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from traceloop.sdk import Traceloop
from traceloop.sdk.decorators import agent, task
 
# 초기화 한 줄로 LangChain/LlamaIndex/CrewAI 자동 계측 활성화
Traceloop.init(
    app_name="research-agent",
    api_endpoint="http://otel-collector:4318",
)
 
@agent(name="research_coordinator")
def run_research(topic: str) -> str:
    # @agent 데코레이터가 함수 전체를 하나의 Agent Span으로 감싼다
    # 내부의 LangChain LLM 호출과 툴 실행이 각각 자식 Span으로 자동 기록된다
    llm = ChatOpenAI(model="gpt-4o")
    agent_executor = AgentExecutor(
        agent=create_react_agent(llm, tools=[...]),
        tools=[...]
    )
    return agent_executor.invoke({"input": f"Research about: {topic}"})["output"]
 
@task(name="summarize_findings")
def summarize(raw_findings: str) -> str:
    # @task 데코레이터는 에이전트 루프와 구분되는 별도 Task Span을 생성한다
    # 에이전트 스텝과 후처리 단계를 트레이스에서 명확하게 분리해서 볼 수 있다
    from openai import OpenAI
    client = OpenAI()
    return client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"Summarize: {raw_findings}"}]
    ).choices[0].message.content

OpenLLMetry: Traceloop이 개발한 OTel 기반 LLM 자동 계측 라이브러리. OpenAI, Anthropic, LangChain, LlamaIndex, CrewAI 등 40개 이상의 공급자를 지원한다. 2026년 3월 ServiceNow에 인수되었지만 오픈소스(Apache 2.0)로 계속 유지된다.

@agent와 @task 데코레이터가 생성하는 Span은 각각 다른 역할을 한다. @agent는 에이전트 전체 실행 흐름을 하나의 루트 Span으로 묶고, 내부의 LLM 호출과 툴 실행을 자식 Span으로 자동 생성한다. @task는 에이전트 루프와 별개인 독립적인 처리 단계를 표시할 때 쓴다. 트레이스 UI에서 보면 "에이전트가 툴 A를 호출하다가 실패해서 툴 B로 재시도했고, 결국 몇 번째 시도에서 성공했는지"가 타임라인으로 보인다.

이번엔 애플리케이션 코드에서 벗어나서 Collector 레벨에서 할 수 있는 작업을 살펴보자.

예시 3: 토큰 비용 메트릭 집계

솔직히 OTel Collector 설정은 처음 봤을 때 좀 막막하다. 근데 이 파이프라인 없이 프로덕션을 운영하다가 "이번 달 OpenAI 청구서가 왜 이렇게 많이 나왔지?" 하는 상황을 한 번 겪고 나면 투자할 만하다는 걸 알게 된다. 기능별로 어떤 엔드포인트가 토큰을 얼마나 쓰는지 보이기 시작하면, 예상 외로 비용을 많이 쓰는 지점이 꼭 나온다.

애플리케이션 개발자라면: 이 섹션은 OTel Collector를 운영하는 인프라/DevOps 담당자 기준의 설정이다. 팀에 인프라 담당자가 있다면 이 섹션을 공유해서 함께 설정하는 것이 좋다.

gen_ai.usage.input_tokens와 gen_ai.usage.output_tokens는 Span 속성으로 기록된다. 이 값을 Prometheus 메트릭으로 변환하려면 spanmetrics connector를 거쳐야 한다. spanmetrics가 Span 속성을 읽어서 메트릭으로 변환해주는 다리 역할을 하는 컴포넌트다.

yaml
# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
 
processors:
  # PII 마스킹 — 프롬프트 캡처 활성화 시 필수
  transform:
    error_mode: ignore
    trace_statements:
      - context: span
        statements:
          - replace_pattern(attributes["gen_ai.content.prompt"], "\\b\\d{6}-\\d{7}\\b", "***")
 
  # 테일 기반 샘플링 — 오류 Span은 100%, 정상은 10%만 수집
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: sample-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
 
# spanmetrics: Span 속성 → 집계 가능한 메트릭으로 변환하는 커넥터
# gen_ai.usage.input_tokens 같은 Span 속성을 Prometheus로 내보낼 수 있게 해준다
connectors:
  spanmetrics:
    namespace: gen_ai
    dimensions:
      - name: gen_ai.system
      - name: gen_ai.request.model
      - name: span.name
    metrics_flush_interval: 15s
 
exporters:
  prometheusremotewrite:
    endpoint: "http://prometheus:9090/api/v1/write"
  otlp/jaeger:
    endpoint: "http://jaeger:4317"
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [transform, tail_sampling]
      # spanmetrics connector로도 Span 전달 — 메트릭 변환을 위해 필요
      exporters: [otlp/jaeger, spanmetrics]
    metrics:
      # spanmetrics가 생성한 메트릭도 함께 수집
      receivers: [otlp, spanmetrics]
      exporters: [prometheusremotewrite]

이 파이프라인의 핵심은 Collector가 중간에서 PII 마스킹과 샘플링을 담당한다는 점이다. 애플리케이션 코드에 이런 로직을 넣지 않아도 되고, 나중에 백엔드를 Jaeger에서 Datadog으로 바꿔도 애플리케이션 코드는 손댈 필요가 없다. spanmetrics connector 덕분에 Grafana에서 gen_ai_usage_input_tokens_total을 span.name이나 gen_ai.request.model 기준으로 슬라이싱해서 기능별 비용을 바로 볼 수 있다.


장단점 분석

실제 프로젝트에 적용하면서 겪은 제약과 그 대응을 정리하면 다음과 같다.

장점

항목 내용
벤더 독립성 gen_ai.* 표준 속성을 한 번 구현하면 Datadog, Grafana, Jaeger 등 어떤 백엔드로도 전송 가능
기존 APM 통합 기존 HTTP/DB Span과 LLM Span이 동일한 Trace 안에서 연결됨 — 서비스 전체 흐름 파악 가능
자동 계측 OpenAI, Anthropic, LangChain, LlamaIndex 등 40개 이상 공급자에 대한 자동 계측 패키지 존재
비용 추적 토큰 사용량 메트릭으로 모델별·기능별 실제 비용을 정량화 가능
표준 생태계 CNCF 프로젝트로 커뮤니티 기반이 튼튼하고 장기적 지속성 확보

단점 및 주의사항

항목 내용 대응 방안
Experimental 불안정성 대부분의 GenAI Semconv가 아직 experimental — minor 버전에서 속성명이 바뀔 수 있음 CI에서 컨벤션 버전 고정, 마이그레이션 스크립트 자동화
프라이버시·컴플라이언스 프롬프트 캡처 활성화 시 PII·기업 기밀 유출 위험 데이터 마스킹 파이프라인, 접근 제어, 보존 정책 선행 구축
스토리지·성능 오버헤드 프롬프트 전문 저장 시 Span 크기 수 KB~수십 KB Head-based / Tail-based Sampling 조합, 콘텐츠 해싱
커스텀 계측 불가피 자동 계측은 LLM SDK 호출 레벨만 커버 — RAG 청킹, 프롬프트 렌더링 등은 수동 계측 필요 tracer.start_as_current_span()으로 비즈니스 로직 단계 계측 추가
에코시스템 분열 Langfuse, Helicone, LangSmith 등이 여전히 독자 포맷 병행 OTel 표준 우선 적용 후 도구별 OTLP 어댑터 활용

Head-based Sampling: 트레이스 시작 시점에 수집 여부를 결정하는 방식. 빠르지만 나중에 에러가 발생해도 이미 버려진 트레이스는 복구 불가.
Tail-based Sampling: 트레이스 전체가 완성된 후 결과를 보고 수집 여부를 결정하는 방식. 에러 트레이스를 확실히 잡을 수 있지만 메모리 사용량이 높다.

실무에서 가장 흔한 실수

  1. 프롬프트 캡처부터 켜고 보는 경우 — OTEL_GENAI_CAPTURE_MESSAGE_CONTENT=true를 개발 환경에서 테스트하다가 프로덕션에도 그대로 배포하면 고객 데이터가 트레이싱 백엔드에 쌓인다. 캡처 활성화 전에 마스킹 파이프라인이 먼저다.

  2. 자동 계측만 믿고 커스텀 Span을 빠뜨리는 경우 — OpenAIInstrumentor가 LLM 호출은 잡아주지만, 그 앞의 문서 검색 로직이나 프롬프트 조합 과정은 잡지 못한다. RAG 파이프라인에서 "어느 단계에서 시간이 걸렸는가"를 알려면 비즈니스 로직 단계별로 수동 계측이 필요하다.

  3. 컨벤션 버전을 고정하지 않는 경우 — opentelemetry-instrumentation-openai 라이브러리를 버전 고정 없이 쓰다가 속성명이 바뀐 버전으로 자동 업그레이드되면 Grafana 대시보드의 쿼리가 일제히 깨진다. requirements.txt에 마이너 버전까지 고정해두는 것이 좋다.


마치며

처음엔 관찰 가능성 없이도 잘 돌아갔다. 개발 환경에서 직접 호출해보면 다 잘 작동하니까. 근데 프로덕션 트래픽이 붙으면서 어디서 뭐가 잘못됐는지 전혀 모르는 상황이 반복됐다. 사용자 불만 제보가 오면 "재현이 안 됩니다"라고 할 수밖에 없는 상황. 그제서야 LLM 시스템에서 관찰 가능성은 선택이 아니라는 걸 깨달았다. GenAI Semantic Conventions가 아직 experimental이라는 점이 부담스럽게 느껴질 수 있지만, 주요 벤더들이 이미 네이티브 지원을 선언한 만큼 지금 시작해도 결코 이른 게 아니다.

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

  1. 기존 OpenAI 프로젝트에 자동 계측 붙여보기 — 소요 시간: 약 5분 / 선행 조건: OpenAI API 키, Python 환경
    pip install opentelemetry-instrumentation-openai로 설치하고 OpenAIInstrumentor().instrument()를 앱 시작 지점에 추가하면 된다. 우선 콘솔 익스포터만 붙여도 gen_ai.* 속성이 터미널에 찍히는 걸 바로 확인할 수 있다.

  2. 로컬에 Jaeger + OTel Collector 띄워서 트레이스 시각화해보기 — 소요 시간: 약 20분 / 선행 조건: Docker 설치
    docker compose up으로 jaegertracing/all-in-one과 OTel Collector를 올리고 OTLP 엔드포인트(http://localhost:4317)로 트레이스를 보내보면 된다. RAG 파이프라인이 있다면 검색 단계부터 LLM 호출까지 연결된 트레이스가 어떻게 시각화되는지 눈으로 확인할 수 있다.

  3. 토큰 사용량 메트릭으로 기능별 비용 분석해보기 — 소요 시간: 약 30분 / 선행 조건: 2단계 완료, Prometheus/Grafana 환경
    예시 3의 spanmetrics connector를 붙이면 gen_ai_usage_input_tokens_total을 span.name 기준으로 집계할 수 있다. Grafana에서 기능별 일일 토큰 소비량을 슬라이싱해보면 예상 외로 비용을 많이 쓰는 엔드포인트를 발견하게 된다.


참고 자료

  • Inside the LLM Call: GenAI Observability with OpenTelemetry | OpenTelemetry 공식 블로그
  • AI Agent Observability - Evolving Standards and Best Practices | OpenTelemetry
  • Semantic conventions for generative AI systems | OpenTelemetry 공식 문서
  • Semantic conventions for generative client AI spans | OpenTelemetry
  • Semantic Conventions for GenAI agent and framework spans | OpenTelemetry
  • How OpenTelemetry Traces LLM Calls, Agent Reasoning, and MCP Tools | Greptime
  • OpenTelemetry Standardizes LLM Tracing: Implementation Guide | earezki.com
  • OpenTelemetry for LLMs: Complete SRE Guide for 2026 | OpenObserve
  • GitHub - traceloop/openllmetry
  • OpenTelemetry (OTel) for LLM Observability | Langfuse
  • Datadog LLM Observability natively supports OpenTelemetry GenAI Semantic Conventions | Datadog
  • How to Implement RAG Pipeline Tracing with OpenTelemetry | OneUptime
  • OpenTelemetry GenAI Semantic Conventions | MLflow AI Platform
  • The AI Engineer's Guide to LLM Observability with OpenTelemetry | Agenta
#OpenTelemetry#LLM-Observability#RAG#GenAI-SemanticConventions#Tracing#LangChain#OpenLLMetry#Python#Jaeger#멀티에이전트
공유하기

목차

핵심 개념OTel이 LLM에 다르게 접근하는 이유트레이스 데이터가 흐르는 구조Span 유형별 역할실전 적용예시 1: RAG 파이프라인 엔드-투-엔드 추적예시 2: OpenLLMetry로 멀티 에이전트 워크플로 추적예시 3: 토큰 비용 메트릭 집계장단점 분석장점단점 및 주의사항실무에서 가장 흔한 실수마치며참고 자료

추천 포스트

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

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

RunContext · output_type · 의존성 주입으로 런타임 오류를 작성 시점에 잡아내기 LLM 기반 기능을 Python 코드에 얹다 보면 어느 순간 묘한 불안감이 생깁니다. "이 도구 함수, LLM이 엉뚱한 타입으로 호출하면 어떻게 되지?" 저도 처음에 FastAPI ...

2026년 05월 30일읽는 데 24분
LLM 평가 프레임워크 직접 구축 vs 기성 도구: 2026 팀별 선택 기준
AI

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

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

2026년 05월 30일읽는 데 24분
멀티모달 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분