‘Logic Span’: OpenTelemetry를 사용해 환각을 추적하기
Source: Dev.to

소개
대시보드에 “Task Failed” 상태가 표시되고 있습니다. 로그를 확인해보니 LLM 제공자로부터 200 OK 응답이 정상적으로 왔습니다. 네트워크도 빠르고 JSON도 올바르게 파싱되었지만, 에이전트는 여전히 “인보이스 요약” 작업을 수행하는 최선의 방법으로 데이터베이스 항목을 삭제하는 결정을 내렸습니다.
대부분의 개발자는 OpenTelemetry (OTel) 를 사용해 느린 데이터베이스 쿼리나 네트워크 병목 현상을 찾습니다. 하지만 2026년 현재 병목 현상은 네트워크가 아니라 추론(Reasoning) 입니다. 에이전트가 정상 궤도를 벗어나면 일반적인 로그는 단지 잡음에 불과합니다. LLM에서 “버그”를 찾으려면 논리가 계획에서 정확히 어디서 갈라졌는지를 확인해야 합니다.
Phase 1: 건축적 베팅
- The Vendor Trap – LLM 제공업체의 클라우드 플랫폼에 있는 “History” 탭에 의존하는 것. 여기서는 프롬프트와 완성 결과만 보여주지만, 해당 호출 주변의 애플리케이션 내부 상태는 표시되지 않는다.
- The Ownership Path – 모든 “Thought” 혹은 *“Reasoning Step”*을 전용 OTel Span으로 감싸는 방식. 우리는 환각 현상을 스택 트레이스처럼 다룬다. Span을 중첩함으로써 상위 *“Plan”*을 시각화하고, 오류를 일으킨 정확한 하위 *“Thought”*를 식별할 수 있다.
2단계: 구현 (속성‑무거운 Span)
우리는 단순히 출력만 기록하지 않습니다; 하이퍼파라미터도 기록합니다. 에이전트가 새벽 3시에 환각을 일으켰다면, 이는 잘못된 프롬프트 때문인가요, 아니면 결정론적 작업에 온도가 너무 높게 설정됐기 때문인가요?
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
tracer = trace.get_tracer("agent.reasoning.monitor")
def execute_agent_step(step_name, prompt_version, params):
"""
The Logic Span: Wrapping the thought, not just the network call.
"""
# Span name must be low‑cardinality; dynamic data goes into attributes.
with tracer.start_as_current_span(f"Agent Thought") as span:
# Attribute Injection: Hardening the trace with metadata
span.set_attribute("llm.temperature", params.get("temp", 0.7))
span.set_attribute("llm.top_p", params.get("top_p", 1.0))
span.set_attribute("config.prompt_version", prompt_version)
try:
# Simulated Agent Logic
response = call_llm(params)
# Record the 'Thought' as a Span Event for granularity
span.add_event(
"llm_completion_received",
{"output.length": len(response)}
)
if "error" in response:
span.set_status(Status(StatusCode.ERROR))
return None
return response
except Exception as e:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
raise
Phase 3: 시니어 보안 및 테스트 감사
저는 이 추적 전략을 전문 Site Reliability 및 Security 감사에 넣었습니다. 여러분의 텔레메트리가 자체 시스템을 다운시킬 수 있는 이유는 다음과 같습니다.
1. TSDB 카디널리티 폭발 (인프라 충돌)
- Fault – 에이전트가 정확히 어떤 생각을 했는지 추적하고 싶어 사용자 ID나 프롬프트 조각을 바로 span 이름에 넣었습니다 (예:
tracer.start_as_current_span(f"Thought: {user_prompt}")). - Audit – Prometheus나 Datadog 같은 시계열 데이터베이스는 span 이름을 기준으로 인덱싱합니다. 동적인 LLM 텍스트가 포함돼 모든 span 이름이 고유하면 고카디널리티 폭발이 발생합니다. TSDB가 메모리를 초과해 관측 스택이 충돌합니다.
- Fix – Span 이름은 저카디널리티이며 정적이어야 합니다 (예:
"Agent Thought"). 동적인 데이터는 항상 Span Attributes에 저장해야 합니다.
2. 비동기 컨텍스트 누수 (논리 버그)
- Fault – 멀티‑에이전트 스웜을 비동기로 실행하면서
execute_agent_step함수를 여러 개 동시에 시작합니다. - Audit – OpenTelemetry는 컨텍스트 변수(예: Python의
contextvars또는 Node.js의AsyncLocalStorage)에 의존해 자식 span을 부모 span에 연결합니다. 백그라운드 작업을 생성할 때 OTel 컨텍스트를 명시적으로 전달하거나 격리하지 않으면 Agent A의 사고 과정이 실수로 Agent B의 트레이스에 붙어 뒤얽힌 “Spaghetti Trace”가 됩니다. - Fix – 에이전트가 사용하는 모든 스레드 풀이나 비동기 큐에 OTel
Context객체를 수동으로 전파합니다.
3. PII 준수 위반 (보안)
- Fault – 환각 현상을 디버깅하기 위해 원시 LLM 출력을 이벤트 속성에 기록합니다.
- Audit – LLM은 이메일, 이름, API 키와 같은 PII를 자주 처리합니다.
raw_output을 OpenTelemetry에 전달하면 암호화되지 않은 PII가 제3자 로깅 벤더로 전송되어 GDPR, SOC 2 및 기본 보안 위생을 위반합니다. - Fix – 민감한 속성(SSN, 이메일, 인증 토큰)을 정규식으로 제거하는 전역 SpanProcessor를 구현해 텔레메트리 데이터가 서버에서 내보내지기 전에 스크러빙합니다.
Phase 4: Checklist (The Architect’s Standard)
- Nest Your Spans – “Action”(Tool Call)은 항상 “Thought”(Reasoning)의 자식 span이어야 합니다.
- Log Hyperparameters –
temperature,model_version,seed는 코드 자체만큼 디버깅에 중요합니다. - Redact Your Traces – 전역
SpanProcessor를 통해 PII가 관측 스택으로 흐르지 않도록 합니다. - Monitor Token Truncation –
context_token_count와is_truncated불리언을 로그에 기록합니다. 에이전트가 환각을 일으키면, 시스템이 프롬프트의 일부를 조용히 잘라냈는지 알아야 합니다.
… 정답을 LLM이 보기 전에 컨텍스트 창에서 삭제했습니다.
핵심 요약
볼 수 없는 것을 고칠 수 없습니다. LLM을 블랙 박스처럼 다루는 것을 멈추고 분산 시스템처럼 다루세요. 논리를 추적하세요. 환상을 찾아내세요.
