OpenTelemetry를 활용한 AI 에이전트 의사결정 트레이싱 (Decision Tracing) 구현
요약
AI 에이전트의 동작 원인을 파악하기 위해 단순 로그를 넘어 OpenTelemetry 기반의 의사결정 트레이싱을 구현하는 방법을 설명합니다. 모델 호출, 도구 실행, 검색 과정을 스팬(Span)으로 구조화하여 에이전트의 추론 과정을 재현할 수 있는 환경 구축을 강조합니다.
핵심 포인트
- 단순 하트비트 로깅은 에이전트의 의사결정 이유를 설명하지 못함
- 모델 호출과 도구 실행을 개별 스팬으로 취급하여 추론 과정을 캡처해야 함
- OpenTelemetry GenAI 규격을 사용하여 벤더 종속성을 방지하고 이식성 확보
- 사고 발생 시 수동 검색 대신 쿼리 가능한 속성을 통해 계획 재현 가능
당신의 에이전트가 새벽 2시에 삭제해서는 안 될 무언가를 삭제했습니다. 알람이 울렸습니다. 이제 세 가지 질문에 답해야 합니다: 무엇을 했는가, 왜 했는가, 그리고 무엇을 건드렸는가. 만약 당신이 향후 30분 동안 JSON을 grep(검색)하고 있다면, 당신은 트레이싱 (Tracing)을 갖추지 못한 것입니다. 당신은 그저 UI가 더 나쁜 로그 (Logs)를 가지고 있을 뿐입니다.
이것은 첫 번째 사고가 발생하기 전까지는 아무도 계측 (Instrument)하지 않는 부분입니다. 그러니 사고가 나기 전에 미리 계측해 봅시다.
하트비트 로깅 (Heartbeat logging) vs 의사결정 트레이싱 (Decision tracing)
대부분의 에이전트 로깅 (Agent logging)은 하트비트 (Heartbeat)를 캡처합니다. 에이전트 실행됨. 도구 호출됨. 응답 반환됨. 모든 것이 HTTP 200이며, 모든 것이 쓸모없습니다.
[02:14:07] agent.run status=200
[02:14:09] tool.call db_query status=200
[02:14:09] tool.call db_delete status=200
...
다섯 개의 초록색 줄. 답변은 제로입니다. 당신에게 필요한 단 한 가지는 왜 db_delete가 실행되었는가이며, 그것은 호출을 생성한 추론 (Reasoning) 단계와 그 단계에 입력된 컨텍스트 (Context) 안에 존재합니다. 하트비트 로깅 (Heartbeat logging)은 호출기가 울리기 전에 이 두 가지를 모두 버려버립니다.
여기 잔혹한 테스트가 있습니다. 알람에서 시작해 보세요. 에이전트가 잘못된 도구를 선택했거나, 잘못된 인자 (Args)를 전달했거나, 중요한 단계 전에 컨텍스트 (Context)가 소진된 분기 (Branch)로 즉시 점프하려고 시도해 보십시오. 만약 그 점프에 수동 검색으로 몇 분 이상이 걸린다면, 당신은 실패한 것입니다. 당신은 트레이스 (Traces)가 아닌 로그 (Logs)를 가지고 있는 것입니다.
해결책은 모든 모델 호출 (Model call), 도구 실행 (Tool execution), 그리고 검색 (Retrieval)을 각각 하나의 스팬 (Span)으로 취급하고, 추론 (Reasoning) 내용을 쿼리 가능한 속성 (Attribute)으로 부착하는 것입니다. 그러면 조사자는 추측하는 대신 계획을 재현 (Replay)할 수 있습니다.
특정 벤더 스키마가 아닌 OpenTelemetry GenAI를 기반으로 구축해야 하는 이유
더 나아가기 전에 빠르게 요약해 보겠습니다. 스팬 (Span)은 시작, 종료, 그리고 부착된 메타데이터 (Metadata)를 가진 시간 측정 단위의 작업입니다. 트레이스 (Trace)는 하나의 논리적 연산에 대한 스팬 (Span)들의 트리 (Tree)입니다. 제대로 계측 (Instrument)한다면, 에이전트 실행은 스크롤해야 하는 로그가 아니라 당신이 따라갈 수 있는 트리가 됩니다.
OpenTelemetry GenAI 시맨틱 컨벤션 (semantic conventions)은 바로 이러한 목적을 위해 벤더 중립적인 (vendor-neutral) 어휘를 제공합니다. 이 사양은 2026년 중반 기준으로 개발 (Development) 상태이며 속성(attributes)들이 여전히 실험적 (experimental)이라고 표시되어 있지만, Datadog, Honeycomb, New Relic, 그리고 주요 프레임워크들은 이미 이에 맞춰 매핑되어 있습니다. 지금 이 규격에 맞춰 구축하면, 백엔드를 교체하거나 통합하라는 지시를 받더라도 트레이스 (traces)의 이식성을 유지할 수 있습니다. 독점적인 (proprietary) 형식에 종속되면, 첫 번째 실제 장애 상황이 발생했을 때 급박하게 다시 계측 (re-instrumenting)을 수행해야 할 것입니다.
이 컨벤션은 당신이 중요하게 생각하는 작업들을 정의합니다. 에이전트 호출을 위한 invoke_agent, 도구 호출을 위한 execute_tool 등이 있습니다. gen_ai.request.model 및 토큰 수와 같은 표준 gen_ai.* 속성들도 포함됩니다. 명명 규칙 (naming) 자체가 핵심입니다. 어떤 OTLP 백엔드라도 별도의 커스텀 파싱 규칙 없이 이를 이해할 수 있습니다.
pip install opentelemetry-sdk opentelemetry-exporter-otlp
pip install opentelemetry-instrumentation-anthropic
자동 계측 (Auto-instrumentation)을 사용하면 수동으로 스팬 (span) 코드를 한 줄도 작성하기 전에, 모델 및 토큰 메타데이터가 포함된 LLM 클라이언트 스팬을 즉시 얻을 수 있습니다. 이것은 최소한의 수준 (floor)이지, 완성형 (ceiling)이 아닙니다.
호출뿐만 아니라 의사결정을 캡처하세요
자동 계측은 더 나은 구조를 가진 심박수 (heartbeat)를 제공하지만, '이유 (why)'를 제공하지는 않습니다. 그 이유를 파악하기 위해서는 에이전트 루프 (agent loop)를 직접 래핑 (wrap)하고, 추론 과정 (reasoning)과 그 출처를 도구 스팬 (tool span)의 속성으로 부착해야 합니다.
from opentelemetry import trace
tracer = trace.get_tracer("toxsec.agent")
...
agent.decision.context_source가 가장 핵심적인 (load-bearing) 속성입니다. 에이전트가 비정상적인 행동을 할 때 가장 먼저 던져야 할 질문은 '트리거가 어디서 왔는가?'입니다. 운영자의 지시인가요? 도구의 출력인가요? 아니면 목표를 조용히 재작성해버린 검색된 문서인가요? 오염된 컨텍스트 (Poisoned context)는 바로 그 필드에 숨어 있으며, 만약 이를 기록하지 않았다면 조사는 시작되기도 전에 끝나버릴 것입니다.
GenAI 사양(spec)에서 강력하게 경고하는 주의 사항이 하나 있습니다. 전체 프롬프트 본문(full prompt bodies)을 스팬 속성(span attributes)에 억지로 밀어 넣지 마십시오. 속성은 항상 인덱싱되고, 항상 내보내지며(exported), 크기 제한이 있고, 트레이스 백엔드(trace backend)로 개인정보(PII)를 유출하는 아주 좋은 경로가 됩니다. 대신 대규모 콘텐츠는 스팬 이벤트(span events)로 저장하십시오. 그러면 Collector가 데이터가 경계(perimeter)를 벗어나기 전에 이를 필터링하거나 삭제할 수 있습니다. 콘텐츠 캡처(Content capture)가 기본적으로 비활성화되어 있는 데에는 이유가 있습니다. 다음과 같이 의도적으로 선택(opt in)해야 합니다:
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false
추론 요약(Reasoning summaries), 위험 등급(risk class), 컨텍스트 소스(context source) 등은 짧고 카디널리티(cardinality)가 낮아 속성으로 사용하기에 안전합니다. 전체 메시지 콘텐츠는 이벤트(events)로 처리하거나, 비식별화(redacted)하거나, 혹은 아예 기록하지 마십시오.
에이전트 핸드오프(handoffs) 간의 상관관계 ID (Correlation IDs)
단일 에이전트는 쉬운 사례입니다. 하지만 한 에이전트가 다른 에이전트에게 작업을 위임(delegate)하는 순간 타임라인은 산산조각 납니다. 핸드오프 과정에서 유지되는 ID와 명시적인 부모-자식 스팬 링크(parent-child span links)가 없다면, 근본 원인 분석은 세 개의 시스템을 동시에 가로지르는 짜깁기식 로그 포렌식(log forensics)이 되어버릴 것입니다.
OTel은 설정을 연결해 두면 트레이스 컨텍스트(trace context)를 대신 전파(propagate)해 줍니다. 부모 에이전트의 스팬 컨텍스트(span context)가 자식에게 함께 전달되므로, 전체 위임 체인이 하나의 트레이스 트리(trace tree)에 담기게 됩니다.
from opentelemetry import context, propagate
def delegate(subagent, task, parent_reasoning):
...
수신 측에서는 해당 캐리어(carrier)를 추출하여 전파된 컨텍스트(propagated context) 내부에서 자식 스팬(child span)을 시작합니다. 이제 서브 에이전트의 도구 호출(tool calls)은 별도의 공백에 떠 있는 것이 아니라, 트레이스 트리에서 부모에 매달려 있게 됩니다. 두 번째 에이전트를 배포하기 전에 이 기능을 연결하십시오. 멀티 에이전트 캐스케이드(multi-agent cascade)가 발생한 후에 상관관계 ID를 사후에 적용(retrofitting)하는 것은 주말을 통째로 날려버리는 지름길입니다.
의사결정 로깅 계약 (The decision-logging contract)
계측(Instrumentation)은 런타임(runtime)이 보는 것을 캡처합니다. 또한 모델이 행동하기 전에 의도(intent)를 선언하도록 강제할 수도 있는데, 이는 트레이스 저장소에 구체적인 정보를 제공하며 인간 검토자(human gate)가 중단시킬 수 있는 근거를 제공합니다. 쓰기(write) 또는 삭제(delete) 도구를 보유한 모든 에이전트에 대해 이를 상시 시스템 프롬프트(system-prompt) 블록으로 삽입하십시오.
의사결정 로깅 계약 (DECISION LOGGING CONTRACT) (매 턴마다 적용)
쓰기(write), 삭제(delete), 상태 수정(modify state)을 수행하는
어떠한 도구(tool)를 호출하기 전에도,
...
해당 JSON을 stdout에 묻히지 않도록 일치하는 span의 속성(attributes)으로 직접 파이프라인을 연결하여 쿼리 가능하게 만드십시오. 이제 agent.decision.context_source 필드는 모델 자체의 선언으로부터 자동으로 채워지며, 여러분의 게이트(gate)는 차단을 위한 깔끔한 risk == destructive 조건을 갖게 됩니다.
사람들이 빠지기 쉬운 함정 (Gotchas)
Span 종류(Span kinds)는 단순한 장식이 아닙니다. 도구 실행(Tool execution)은 INTERNAL이며, 이는 여러분의 앱이 소유한 코드입니다. 추론(Inference)은 CLIENT이며, 모델이 프로세스 내부에서 실행될 때는 INTERNAL입니다. 벡터 스토어(vector store)에 대한 검색(Retrieval)은 프로세스 경계를 넘나들기 때문에 CLIENT입니다. 이를 잘못 설정하면 서비스 맵(service map)의 화살표가 거꾸로 그려지게 되며, 새벽 2시에 트레이스(trace)를 읽을 때 마치 허구 소설처럼 느껴지게 됩니다.
보관 주기(Retention)는 서서히 진행되는 사례를 망가뜨립니다. 개인정보 보호를 위해 기본적으로 짧게 설정된 보관 주기는 화요일에 발생한 사고를 설명하는 span들이 목요일이면 사라지게 만듭니다. 에이전트 공격은 'low and slow' 방식으로 진행됩니다. 월요일에 메모리를 오염시키고 금요일에 현금을 인출합니다. 의사결정 트레이스(decision traces)를 매일 밤 교체되는 디버그 노이즈가 아니라, 실제 보관 정책을 가진 보안 텔레메트리(security telemetry)로 취급하십시오.
복구 평면(Recovery plane)은 에이전트의 신원(identity)을 공유해서는 안 됩니다. 이것은 트레이싱에 관한 내용은 아니지만, 사고를 멸종 사건(extinction event)으로 만드는 결정적인 요소입니다. 만약 백업이 에이전트가 보유한 것과 동일한 자격 증명(credentials) 뒤에 있다면, 그것은 동일한 토큰을 기다리고 있는 두 번째 복사본일 뿐입니다. 복구 금고(recovery vault)를 에이전트의 폭발 반경(blast radius) 밖으로 에어갭(Air-gap) 처리하십시오.
마무리
모든 모델 호출, 도구 실행, 검색을 각각의 span으로 계측(Instrument)하십시오. 추론 과정(reasoning)과 컨텍스트 소스(context source)를 속성(attributes)으로 부착하십시오. 이식성을 유지할 수 있도록 GenAI 컨벤션(conventions)을 기반으로 구축하고, 핸드오프(handoffs) 과정에서 컨텍스트를 전파하며, 파괴적인 도구를 보유한 모든 대상에 대해 의사결정 로깅 계약(decision-logging contract)을 강제하십시오. 그렇게 한다면 "무엇을 했고, 왜 했으며, 무엇을 건드렸는가"라는 질문은 주말 내내 고고학 조사를 해야 하는 과제가 아니라, 간단한 쿼리(query)가 될 것입니다.
이 사례를 구체화하는 9초간의 운영 데이터베이스 삭제(production-database wipe) 사건과 운영자 체크리스트(operator checklist)를 포함한 전체 분석 내용은 ToxSec Substack에 작성해 두었습니다.
ToxSec는 AI 보안 취약점(vulnerabilities), 공격 체인(attack chains), 그리고 방어자가 실제로 이해해야 하는 공격 도구(offensive tools)를 다룹니다. NSA, Amazon, 그리고 국방 계약 분야(defense contracting sector)에서 실무 경험을 쌓은 AI 보안 엔지니어가 운영합니다. CISSP 인증 보유, 사이버 보안 공학(Cybersecurity Engineering) 석사 학위 소지.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기