AI 에이전트의 트레이스(Traces)를 읽어 테스트하기: 셀프 호스팅된 SigNoz 기반의 TraceCheck 구축
요약
AI 에이전트의 실행 과정을 검증하기 위해 OpenTelemetry 트레이스를 활용하는 TraceCheck 구축 사례를 소개합니다. 블랙박스 테스트의 한계를 넘어, SigNoz를 통해 도구 호출 순서와 토큰 사용량 등 내부 프로세스를 화이트박스 방식으로 테스트하는 방법을 다룹니다.
핵심 포인트
- 단순 출력 확인을 넘어 에이전트의 실행 과정(Process)을 검증하는 화이트박스 테스트의 중요성
- OpenTelemetry와 SigNoz를 활용하여 에이전트의 도구 호출 및 스팬(Span) 데이터를 추적
- TraceCheck를 통해 도구 호출 루프, 단계 건너뛰기 등 에이전트의 논리적 오류 탐지 가능
- 셀프 호스팅 환경 구축 시 Docker 이미지 프리풀(Pre-pull)을 통한 타임아웃 문제 해결 팁
AI 에이전트의 트레이스(Traces)를 읽어 테스트하기: 셀프 호스팅된 SigNoz 기반의 TraceCheck 구축
"주문 123번에 대한 환불 처리를 성공적으로 완료했습니다." 자신감 있고, 정중하며, 완전히 정확하게 들리는 문장입니다. 하지만 이번 실행에서 에이전트는 포기하고 그냥 실행해 버리기 직전까지 lookup_order를 연속으로 6번이나 호출했습니다. 이는 제가 논점을 증명하기 위해 의도적으로 심어둔 무한 루프였습니다. 만약 제가 마지막 문장만 채점했다면, 저는 결코 이를 알지 못했을 것입니다.
이것이 바로 제가 SigNoz 해커톤을 위해 TraceCheck를 만든 정확한 이유입니다. 즉, AI 에이전트가 단순히 그럴듯한 말을 했는지 여부가 아니라, 실제로 올바르게 행동했는지를 판단하기 위해 셀프 호스팅된 SigNoz에 저장된 OpenTelemetry 트레이스(traces)를 정답(ground truth)으로 사용하는 테스트 플랫폼입니다.
제가 계속 마주쳤던 간극
LLM 에이전트를 테스트하는 대부분의 방법은 다음과 같이 요약됩니다: 입력을 보내고, 출력을 확인한다. 이것은 블랙박스 테스트(black-box testing)이며, 심각한 사각지대를 가지고 있습니다. 에이전트는 동일한 도구(tool)를 루프 내에서 호출하거나, 수행해야 할 단계를 건너뛰거나, 조용히 토큰 예산(token budget)을 다 써버릴 수 있습니다. 그러고 나서도 여전히 읽기에 괜찮은 답변을 내놓을 수 있습니다. 실패는 최종 텍스트가 아니라 _과정(process)_에서 발생하며, 과정을 전혀 살펴보지 않는다면 결코 잡아낼 수 없습니다.
저는 OpenTelemetry에 대해 읽으면서 다음과 같은 생각을 했습니다. 만약 모든 LLM 호출과 모든 도구 호출이 스팬(span)으로 기록되고, SigNoz가 해당 트레이스(trace)를 저장한다면, 테스트 러너(test runner)가 트레이스를 다시 가져와서 실제 스팬 트리(span tree)를 확인할 수 있지 않을까? 즉, 어떤 도구가 어떤 순서로, 몇 번 실행되었는지, 토큰은 얼마나 사용되었는지, 시간이 얼마나 걸렸는지 등을 일반적인 출력 확인과 병행하여 검사할 수 있다는 것입니다. 동일한 실제 트레이스로부터 화이트박스(white-box) 및 블랙박스 어설션(assertions)을 수행하는 것. 그것이 바로 TraceCheck입니다.
SigNoz를 실행하는 것 자체가 하나의 교훈이었습니다
실제 테스트 로직을 구현하기 전에, 저는 해커톤 규칙에 따라 Foundry를 통해 셀프 호스팅된 실제 SigNoz 인스턴스에 실제 에이전트가 실제 스팬(spans)을 보내도록 만들어야 했습니다. 이것만으로도 제가 예상했던 것보다 더 많은 것을 배울 수 있었습니다.
첫 번째 시도: foundryctl cast -f casting.yaml 명령어가 거의 정확히 300초 지점에서 두 번이나 중단되었으며, 매번 가져오는 Docker 이미지의 양이 달랐습니다. 이러한 일관성(동일한 타임아웃, 다른 진행 상황)을 통해 이것이 불안정한 네트워크 문제가 아니라, pull 단계에 하드코딩된 타임아웃(hardcoded timeout)임을 알 수 있었습니다. 해결책: 제한이 없는 일반 docker pull을 사용하여 5개의 이미지(signoz/signoz, signoz/signoz-otel-collector, clickhouse/clickhouse-server, clickhouse/clickhouse-keeper, postgres:16)를 수동으로 미리 pull(pre-pull)했습니다. 이미지가 캐시된 후에는 cast가 약 20초 만에 완료되었습니다.
두 번째 교훈은 이번에는 에이전트 측면에서 얻었습니다. Gemini의 무료 티어(free tier)는 새로 생성한 키를 사용했음에도 불구하고 Quota exceeded ... limit: 0 오류를 발생시켰습니다. 알고 보니 현재 Gemini API에서
scenario: "order #123 환불"
input: "123번 주문에 대해 환불받고 싶어요. 제품이 파손되어 도착했습니다"
assertions:
...
러너(Runner)는 입력을 실제 에이전트(Agent)에게 보내고, 트레이스 ID(Trace ID)를 받아온 뒤, 트레이스가 실제로 기록될 때까지 SigNoz의 /api/v5/query_range API를 폴링(Polling)합니다 (스팬(Span)은 배치(Batch)로 내보내지기 때문에 약간의 지연이 발생합니다). 그 다음, 실제 스팬 트리(Span tree)를 모든 규칙과 대조하여 확인합니다. 즉, lookup_order가 issue_refund 이전에 발생했는지, 도구(Tool) 사용이 제한 횟수를 초과하여 반복되었는지, 총 토큰(Token) 사용량은 얼마인지, 실행 시간은 얼마나 걸렸는지 등을 확인합니다. 여기에 더해, 최종 답변을 일반적인 영어 루브릭(Rubric)에 따라 평가하는 LLM-as-judge 호출이라는 블랙박스(Black-box) 체크도 수행합니다.
이 어설션(Assertion)들이 단순히 형식적으로 통과하는 것이 아니라 실제 실패를 제대로 잡아내는지 증명하기 위해, main 브랜치에는 정상적인 에이전트를 유지하고 의도적으로 버그를 심은 세 개의 브랜치를 생성했습니다.
bug/tool-loop: 에이전트가 동일한 주문을 반복해서 재확인하도록 프롬프트(Prompt)를 변경했습니다. 실제 결과:lookup_order가 제한 횟수인 2회를 초과하여 6회 호출되었습니다. 최종 답변은 여전히 매우 정상적으로 보였지만, 이 오류는 성공적으로 포착되었습니다.bug/skips-lookup: 에이전트에게 조회(Lookup) 단계가 "선택 사항"이라고 알려주었습니다. 결과:issue_refund로 바로 건너뛰었으며, 도구 실행 순서 어설션(Tool-ordering assertion)에 의해 포착되었습니다.bug/token-blowup: 시스템 프롬프트(System prompt)에 약 4,400 토큰의 의미 없는 내용을 채워 넣었습니다. 결과: 총 사용량이 제한인 8,000 토큰을 초과하여 10,059 토큰으로 급증했습니다. 다른 모든 어설션은 통과하면서도 문제가 된 정확한 지점만을 격리하여 포착해냈습니다.
CI 파이프라인에서 가장 많은 것을 배웠습니다
실제 회귀(Regression)가 발생했을 때 실제 풀 리퀘스트(Pull Request)를 차단할 수 있도록 이를 GitHub Actions에 연결하는 과정이 가장 어려운 부분이었습니다. 또한, 이 과정에서 SigNoz의 인증(Auth)이 실제로 어떻게 작동하는지에 대해 가장 많은 것을 배울 수 있었습니다.
저의 첫 번째 접근 방식은 CI 실행을 위해 SigNoz의 API를 통해 서비스 계정(Service Account)과 API 키를 생성하는 것이었습니다. 하지만 trace-query 엔드포인트에서 계속해서 403 Forbidden 오류가 발생했습니다. 근본 원인은 새로 생성된 서비스 계정은 역할(Role)이 할당될 때까지 권한이 전혀 없기 때문이었습니다. 보통 SigNoz UI에서 수동으로 수행하는 작업인데, 스크립트로 생성된 완전히 새로운 인스턴스에서는 이를 API를 통해 재현할 수 없었습니다. 해결 방법은 예상보다 간단했습니다. 로그인된 관리자의 JWT를 베어러 토큰(Bearer Token)으로 직접 사용하는 것이었습니다. 이는 SigNoz UI 자체에서 Traces 페이지를 탐색할 때 사용하는 것과 정확히 동일한 자격 증명이기 때문입니다. 별도의 역할 할당이 필요하지 않았습니다.
인증 문제를 해결하자 두 번째의 더 교묘한 문제에 부딪혔습니다. OTel 컬렉터(Collector)의 포트가 실제 트래픽을 처리할 gRPC 서비스가 제대로 연결되기 전에 일반 TCP 연결을 수락해 버리는 것이었습니다. 즉, 단순한 "포트가 열려 있는가" 체크는 통과했지만, 실제 스팬(Span) 내보내기는 여전히 리셋되었습니다. 진짜 해결책은 전체 파이프라인 — SDK → 컬렉터(Collector) → ClickHouse → 쿼리 API — 을 통해 실제 더미 스팬(Dummy Span)을 밀어 넣고, 파이프라인이 준비되었다고 신뢰하기 전에 실제 결과가 돌아올 때까지 전체 라운드 트립(Round Trip)을 재시도하는 것이었습니다. 이는 매우 구체적인 교훈을 줍니다. 리스닝 소켓(Listening Socket)이 작동하는 서비스와 동일한 것은 아니며, 상태 확인(Health Check)은 대리 지표가 아니라 실제로 의존하는 대상을 증명해야 한다는 것입니다.
과거의 나에게 해주고 싶은 말
설치 단계에 풀(Pull) 단계가 포함된 설치 프로그램을 실행하기 전에 Docker 이미지를 미리 가져오세요(Pre-pull). 무료 티어라 하더라도 결제 정보를 특정 Gemini 프로젝트에 연결해 두세요. 에이전트 테스트의 두 측면인 트레이스(Trace)와 출력(Output)을 모두 테스트하세요. 이들은 진정으로 다른 유형의 실패를 잡아내기 때문입니다. 도구 루프(Tool-loop) 버그는 출력 채점(Output Grading)으로는 전혀 보이지 않았습니다. 그리고 CI에서 서비스가 "준비됨"을 증명할 때는 포트를 확인하는 방식이 아니라, 실제 코드가 서비스를 사용하는 방식 그대로 사용하여 증명하세요.
결과
TraceCheck의 최종 결과물은 다음과 같습니다: OpenTelemetry (OTel)로 계측된 실제 에이전트, (단순히 덧붙여진 대시보드가 아닌) 실제 신뢰할 수 있는 데이터 소스(Source of Truth) 역할을 하는 셀프 호스팅된 SigNoz 인스턴스, 4가지 화이트박스(White-box) 체크와 LLM 판사(LLM-judge) 블랙박스(Black-box) 체크를 포함하는 어설션 엔진(Assertion engine), 검증된 3가지 시드 버그(Seeded-bug) 브랜치, 매 푸시(Push)마다 새로운 SigNoz를 프로비저닝하고 잘못된 머지(Merge)를 차단하는 GitHub Action, 그리고 이 모든 과정을 지켜볼 수 있는 작은 라이브 대시보드입니다. 오픈 소스이며, 셀프 호스팅이 가능하고, 프레임워크에 구애받지 않습니다. OpenTelemetry 스팬(Span)을 방출하는 에이전트라면 무엇이든 이 방식으로 테스트할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기