OpenTelemetry와 SigNoz를 활용한 AI 에이전트의 행동 드리프트 (Behavioral Drift) 감지
요약
AI 에이전트의 동작 변화(Behavioral Drift)를 감지하기 위한 DriftWatch 솔루션을 소개합니다. OpenTelemetry와 SigNoz를 활용하여 도구 호출, 소요 시간, 모델 정보, 토큰 사용량 및 비용을 추적하고 시각화합니다.
핵심 포인트
- OpenTelemetry를 통한 AI 에이전트 실행 프로세스의 정밀한 추적
- 도구 호출(tool calls) 및 모델별 토큰 사용량/비용 모니터링
- SigNoz와 연동하여 에이전트의 동작 드리프트 현상 가시화
- Node SDK 계측을 통한 추가 코드 없이 OTLP 데이터 전송 가능
최근에 https://camperjobs.ng 사이트를 디자인하기 위해 AI와 impeccable이라는 기술을 사용했는데, 결과물이 매우 마음에 들었습니다. 하지만 그 이후에 동일한 기술을 사용하여 다른 애플리케이션을 재디자인하려고 시도했을 때, 이전 사이트와 같은 느낌을 구현할 수 없었습니다. 이미 알고 계시듯 기술은 그것과 함께 사용되는 모델만큼만 효과적이지만, 저에게는 이를 알 수 있는 방법이 없었습니다.
도구가 탄생하게 된 계기
기술이 이전처럼 작동하도록 만들려다 마주한 좌절감, 무엇이 변했는지, 어떤 모델이 사용되었는지, 그리고 이전 결과에 도달하기 전 모델과 기술의 사고 과정(thought process)은 무엇이었는지에 대한 의문들.
이러한 문제에 대한 해결책을 고민하던 중, X(구 트위터)에서 한 트윗을 발견했고 [image below], 저만 이런 문제에 직면한 것이 아니라는 사실을 깨달았습니다. 같은 날 SigNoz 해커톤에 관한 이메일을 받았고, 그것이 제가 필요로 했던 모든 신호였습니다.
이 모든 과정이 저를 DriftWatch를 만들게끔 이끌었습니다.
해결책
AI 에이전트의 핵심, DriftWatch를 소개합니다.
그렇다면 DriftWatch란 무엇일까요? AI 모델이나 에이전트를 사용하거나 이를 통해 구축할 때, 우리는 그것들이 백그라운드에서 무엇을 하고 있는지에 대해 대부분 눈이 멀어 있습니다.
- 어떤 도구(tools)가 호출되고 있는지
- 소요 시간은 얼마나 되는지
- 어떤 모델이 사용되었는지
- 우리가 의도한 동작에서 언제 드리프트 (drift)가 발생했는지
- 얼마나 많은 토큰 (tokens)이 사용되었는지
- USD로 비용이 얼마나 발생했는지
DriftWatch는 모든 프로세스, 도구 호출 (tool calls), 지속 시간 (duration), 드리프트 (drifts)뿐만 아니라 사용된 모델 및 기술을 관찰함으로써 이러한 모든 문제를 해결합니다. 또한 SigNoz에서 전달되고 분석되는 OpenTelemetry 레코드를 제공하여, 모든 프로세스의 무엇을(whats), 왜(whys), 언제(whens)에 대한 깊은 통찰력을 제공합니다.
DriftWatch의 작동 방식
DriftWatch는 도구 호출(tool calls)을 래핑(wrap)하며, OpenTelemetry 요소를 사용하여 시작부터 응답까지의 모든 프로세스를 추적합니다. 구체적으로는, 모든 에이전트 실행(agent run)은 하나의 부모 agent.run 스팬(span)을 가지며, 모든 도구 호출은 각각의 자식 스팬(child span)을 가집니다. 또한 세 가지 커스텀 메트릭(custom metrics)이 이와 함께 기록됩니다: agent.tool.calls (카운터(counter), 도구 및 결과별 레이블링), agent.tool.duration (히스토그램(histogram)), 그리고 agent.tokens (카운터(counter), 모델/제공자/유형별 레이블링). 이 중 어느 것도 아직 SigNoz 전용은 아닙니다. 이는 단지 프로세스 시작 시점에 한 번 계측(instrumented)된 OpenTelemetry Node SDK일 뿐입니다:
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: telemetryConfig.serviceName,
...
OTEL_EXPORTER_OTLP_ENDPOINT를 셀프 호스팅되는 SigNoz 콜렉터(SigNoz의 자체 docker compose up을 로컬에서 실행 중이라면 http://localhost:4318)로 지정하면, 위에서 언급한 모든 스팬(span)과 메트릭(metric)이 추가적인 글루 코드(glue code) 없이 OTLP/HTTP를 통해 SigNoz로 전달됩니다. 이 부분이 진정으로 "공짜"인 부분입니다. SigNoz의 콜렉터는 OTLP를 네이티브로 지원하므로, 별도의 변환 계층(translation layer)을 작성할 필요가 없습니다.
이를 통해 _가시성(visibility)_을 확보할 수 있습니다. 하지만 제가 실제로 원했던 부분, 즉 에이전트의 행동이 조용히 변할 때 이를 알아차리는 기능은 SigNoz가 기본적으로 제공하지 않는 기능이 필요했습니다. 바로 두 개의 시간 창(time windows)을 비교하여 그 차이가 무엇을 _의미하는지_를 묻는 작업입니다. 따라서 DriftWatch는 SigNoz의 /api/v4/query_range 빌더 API를 직접 쿼리하여 두 개의 1시간 단위 시간 창(기준점(baseline) vs 현재(current))을 가져온 뒤 이를 비교(diff)합니다:
const requestBody = {
start: startTimeMs,
end: endTimeMs,
...
한 번의 요청으로 세 가지 빌더 쿼리(builder queries)—도구 호출 횟수(도구 구성 및 에러율 확인용), p95 도구 지연 시간(latency), 그리고 총 토큰 사용량(total token spend)—를 두 윈도우(window)에 대해 병렬로 팬아웃(fan out)합니다. SIGNOZ-API-KEY 헤더는 SigNoz 자체 UI(Settings → API Keys)에서 가져오며, 빌더 쿼리의 형태(shape)를 잡는 데는 몇 번의 실패한 요청이 필요했습니다. v4 API의 aggregateAttribute/dataType 필드는 대시보드만 봐서는 명확하지 않기 때문입니다. 결국 SigNoz UI에서 패널을 수동으로 만들면서 네트워크 탭(network tab)을 관찰하여 정확한 페이로드(payload)를 역공학(reverse-engineering)한 뒤, 그 형태를 코드에 복사하여 완성했습니다.
[IMG:1]
두 윈도우가 모두 일반 숫자(도구 구성 비율, 에러율, p95 지연 시간, 토큰 사용량)로 반환되면, DriftWatch는 이 값들을 사용자가 설정한 모델에 전달하여 드리프트(drift) 여부를 판단하도록 요청합니다. 이때 generateObject가 아닌 일반 generateText 호출과 엄격한 JSON 전용 시스템 프롬프트(system prompt)를 사용하며, 모델이 답변을 마크다운(markdown)으로 감싸는 경우 최대 몇 번까지 재시도합니다:
You are an SRE copilot that classifies whether an AI agent's behavior
has drifted enough to warrant a human alert. Reply with a SINGLE raw
JSON object: {"drift": bool, "severity": "none"|"low"|"medium"|"high",
...
이 판결 결과와 원본 기준값(baseline)/현재값(current) 수치는 /drift 응답으로 나타나며, 이는 DriftWatch 콘솔이 사람이 읽기 쉬운 카드 형태로 렌더링하는 것과 동일한 JSON입니다. 여기에는 심각도(severity), 변화가 발생한 특정 지표(metrics), 그리고 한 문장으로 된 권장 사항이 포함됩니다.
[IMG:2]
SigNoz에서 확인하기
전체 루프는 운영 트래픽(production traffic)에 영향을 주지 않고도 엔드 투 엔드(end-to-end)로 테스트할 수 있습니다:
# 로컬에 SigNoz 실행
git clone https://github.com/SigNoz/signoz && cd signoz/deploy/docker && docker compose up -d
...
pnpm seed 40 명령은 에이전트를 통해 40개의 혼합된 요청을 발생시키며, 이는 베이스라인(baseline)과 현재(current) 윈도우 모두를 실제 스팬(span)으로 채우기에 충분합니다. 직후에 localhost:8080에서 SigNoz를 열면 agent.run 트레이스(trace)가 쌓이는 것을 볼 수 있으며, 아무 트레이스나 클릭하여 각 도구 호출(tool call)이 고유한 지속 시간(duration)과 상태(status)를 가진 자식 스팬(child span)으로 나타나는 것을 확인할 수 있습니다. 솔직히 이 트레이스 뷰(trace view)가 디버깅에 가장 유용한 부분입니다. 드리프트 판정기(drift judge)가 "search_docs share 40% → 75%"라고 표시할 때, 단순히 그 말을 믿을 필요 없이 해당 윈도우의 실제 트레이스로 클릭해 들어가 어떤 도구 호출이 지배적이었는지 직접 확인할 수 있기 때문입니다.
[Screenshot: a single
agent.runtrace expanded, showing child spans for each tool call with duration andgen_ai.*attributes]
또한 저는 SigNoz를 완전히 건너뛰고 피스처(fixture) 윈도우를 사용하는 drift:dry-run 모드도 구축했습니다. 이는 데이터가 채워진 SigNoz 인스턴스 없이도 판정기(judge) 로직 자체를 데모하는 데 유용하며, 제가 위의 프롬프트를 반복해서 개선할 때 매번 실제 트래픽을 다시 실행하며 토큰을 낭비하지 않기 위해 사용한 방식입니다.
Autopilot: 루프 닫기 (closing the loop)
드리프트 감지가 작동하기 시작하면, 당연히 다음 질문은 "좋아, 이제 어떻게 하지?"가 됩니다. 그래서 DriftWatch에는 드리프트 판결을 행동으로 전환하는 두 번째 선택적 루프가 있습니다. 정책 규칙(policy rules)에 따라 에이전트를 일시 중지하거나, 알려진 양호한 상태(known-good state)로 롤백하거나, 혹은 단순히 알림(Slack/Telegram/webhook)을 보내는 방식입니다. 파괴적인 작업의 경우 인간의 승인 단계(human approval step)를 거치도록 제한합니다. 이는 별도의 데이터 소스가 아니라 동일한 SigNoz 기반 수치 위에 구축된 얇은 계층(thin layer)입니다. 이 포스트는 특히 SigNoz 통합에 초점을 맞추고 있으므로 상세히 다루지는 않겠지만, 이것이 바로 "에이전트의 드리프트를 감지했다"라는 사실을 "방치된 상태에서도 계속 실행할 수 있는 것"으로 바꿔주는 핵심 요소입니다.
시사점 (Takeaways)
잘된 점: OpenTelemetry의 Fastify/HTTP 자동 계측 (auto-instrumentation) 덕분에, 단 하나의 커스텀 스팬 (custom span)을 작성하기도 전에 sdk.start()가 실행되는 즉시 기본 요청 트레이싱 (request tracing)을 무료로 얻을 수 있었습니다. Exporter를 SigNoz의 Collector로 올바르게 지정하고 나니, 그 위에 agent.run/도구 (tool) 스팬과 세 가지 커스텀 메트릭 (custom metrics)을 계층적으로 쌓는 과정은 매우 간단했습니다.
어려웠던 점: HTTP나 DB 스팬과 달리 "AI 에이전트 행동 (AI agent behavior)"에 대한 기존의 시맨틱 컨벤션 (semantic convention)이 없었기 때문에, 직접 속성 이름(agent.task_id, agent.skills_used, gen_ai.usage.*)을 만들어내고 이를 일관되게 유지해야 했습니다. 코드에서 (UI가 아닌) SigNoz v4 쿼리 빌더 (query builder) 페이로드 (payload) 형식을 정확하게 맞추는 데는 상당한 시행착오가 필요했습니다. 잘못된 compositeQuery로 인한 에러 메시지는 어떤 필드가 잘못되었는지 항상 구체적으로 알려주지는 않기 때문입니다. 또한, 드리프트 판정을 위해 모델이 순수 JSON (bare JSON)을 안정적으로 반환하게 만드는 것이 예상보다 덜 안정적이었습니다. 수정 프롬프트를 포함한 재시도 (retry-with-correction-prompt) 방식은 주말 프로젝트용으로는 충분히 잘 작동하는 임시방편 (hack)이었습니다.
결론 (Conclusion)
DriftWatch는 SigNoz를 "트레이스가 죽으러 가는 곳"에서 AI 에이전트를 위한 실제 피드백 루프 (feedback loop)로 탈바꿈시킵니다. OpenTelemetry로 한 번 계측하고, SigNoz의 빌더 API (builder API)를 통해 두 개의 시간 창 (time windows)을 쿼리한 다음, 모델이 변화가 감지되었을 때 알려주도록 하세요. 코드: https://github.com/codewithveek/drift-watch — 라이브 인스턴스 (live instance): https://driftwatch.veek.me/console/.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기