
모든 것이 정상(Green)이었지만, AI는 여전히 틀렸다.
요약
AI 에이전트의 환각이나 잘못된 의사결정은 전통적인 관측성 지표(에러 코드 등)로 포착하기 어렵습니다. 이를 해결하기 위해 OpenTelemetry 데이터를 활용하여 에이전트의 실행 과정을 결정론적인 '의미론적 리스크' 점수로 평가하는 Veritas 시스템을 소개합니다.
핵심 포인트
- 전통적 관측성은 에이전트의 논리적 오류나 환각을 감지하지 못함
- Veritas는 SigNoz와 OpenTelemetry를 활용해 리스크를 점수화함
- 머신러닝 대신 결정론적 휴리스틱을 사용하여 설명 가능성 확보
- 에이전트 실행 단계를 리플레이 타임라인으로 시각화하여 문제 지점 파악
이 작업을 끝내고 싶었던 시점으로부터 3일 전, 나는 왜 내 OTLP 로그가 나타나지 않는지 파악하기 위해 SigNoz Logs Explorer를 뚫어지게 쳐다보고 있었다. Trace(추적)는 첫 실행부터 문제없이 도착했다. 하지만 로그는 아무것도 없었다. 에러도, 경고도 없이 그저 침묵만이 흐르고 있었다. 이는 그 자체로 일종의 버그였다. 왜냐하면 "아무 일도 일어나지 않았다"는 상황은 grep으로 검색할 수 있는 대상조차 주지 않기 때문이다.
에이전트(Agent)는 여전히 틀리고 있다. 그리고 그것이 바로 표준 관측성(Observability)이 포착할 수 없는 지점이다. 왜냐하면 표준 관측성은 그것을 찾기 위해 만들어진 것이 아니기 때문이다.
격차, 그리고 우리가 스스로에게 부여한 규칙
전통적인 관측성(Observability)은 세 가지 질문에 답한다: 시스템이 작동 중인가(is it up), 빠른가(is it fast), 에러를 발생시켰는가(did it throw). AI 에이전트는 이 세 가지를 모두 통과하고도 사실을 환각(Hallucinate)하거나, 검색된 증거가 전혀 없는 상태에서 행동하거나, 첫 단계부터 잘못된 계획을 실행할 수 있다. 이 중 그 어떤 것도 500 에러를 발생시키지 않는다. 이는 하나의 "결정(Decision)"으로 나타나며, 결정에는 상태 코드(Status code)가 존재하지 않는다.
우리가 구축한 그 어떤 것보다 중요했던 설계 규칙은 다음과 같다: Veritas는 다른 블랙박스(Black box)를 채점하는 또 다른 블랙박스가 되어서는 안 된다. 만약 Veritas가 특정 실행을 왜 플래그(Flag)했는지 평이한 영어로 설명할 수 없다면, 그것은 책임성(Accountability)이 아니라 추론 과정이 결여된 단순한 제2의 의견일 뿐이다. 따라서 Veritas가 사용하는 모든 시그널(Signal)은 이미 Trace(추적) 상에 존재해야 하며, 점수의 모든 항목은 인간이 논쟁할 수 있어야 한다.
이 규칙 때문에 아래의 리스크 엔진(Risk engine)에 포함된 그 어떤 것도 머신러닝(Machine-learned)으로 구현되지 않았다. 이는 우리가 의도적으로 선택한 제약 조건이며, 이 포스트의 나머지 내용이 구축된 핵심 아이디어이다.
Veritas가 하는 일
Veritas는 셀프 호스팅된 SigNoz 인스턴스로부터 실제 OpenTelemetry 텔레메트리(Telemetry)를 읽어 들여, 모든 에이전트 실행에 대해 0~100 사이의 결정론적인 의미론적 리스크 (Semantic Risk) 점수를 계산한다. 그런 다음 **리플레이 타임라인 (Replay Timeline)**을 그려내어, 사용자가 고고학 발굴을 하듯 로그를 스크롤하는 대신 상황이 잘못된 정확한 단계를 지목할 수 있게 한다. Veritas는 텔레메트리 자체를 저장하지 않는다. SigNoz가 기록 시스템(System of record)이며, Veritas는 그 위에 구축된 얇은 읽기 계층(Read-layer)이다.
우리의 FastAPI 에이전트는 planner(플래너) → retriever(리트리버) → tool(도구) → memory(메모리) → response(응답)의 5단계를 실행하며, 각 단계는 루트 agent.run 스팬 (span) 아래에 고유한 중첩된 OTel 스팬 (span)을 가집니다. 이 스팬들은 tool.success, retrieval_score, response.confidence와 같은 도메인 속성 (attributes)을 포함합니다. 6개의 독립적인 휴리스틱 (heuristics)이 이러한 속성들을 점수로 변환하며, 각 점수는 하나의 파일에 정의된 고정된 가중치 (weight)를 가집니다:
RETRIEVAL_ZERO_DOCS = 25
TOOL_FAILURE = 30
PLANNER_ZERO_TASKS = 25
...
우리는 수기로 입력한 것이 아니라 실제 엔진을 통해 실행된 5가지 시나리오를 바탕으로 이를 검증했습니다:
| 시나리오 | 점수 | 레벨 |
|---|---|---|
| Healthy (정상) | 5 | LOW |
| ... |
해당 파일의 모든 가중치는 5의 배수입니다. 이는 이전 기획 문서에 등장했던 "예시" 점수들(48, 84)이 실제 엔진이 생성할 수 없는 숫자였음을 의미합니다. 우리는 엔진의 값을 문서에 맞추기 위해 조용히 반올림하는 대신, 실제 값을 그대로 유지했습니다. 검증된 것처럼 보이도록 비밀리에 조정된 "검증된 데모"를 잡아내는 것이 바로 이 프로젝트가 존재하는 이유입니다.
다음은 실제 도구 타임아웃 (tool-timeout)이 발생한 실행에 대한 Replay 타임라인입니다. 점수는 75점, 레벨은 HIGH입니다. 리트리버 (retriever)가 문서 하나로 얇아지고, 정책 조회 (policy lookup)가 330ms에서 타임아웃되며, 신뢰도 (confidence)가 0.41로 급락합니다. 각 기여도는 로그 라인에 파묻히는 대신 인라인 (inline)으로 표시됩니다:
더 클릭하면 플래그 (flagged)된 실행이 전체 인시던트 보고서 (incident report)로 확장됩니다. 여기에는 근본 원인 (root cause), 예상값 대비 관측값 (expected-vs-observed), 쉬운 영어로 풀이된 발생 원인 체인 (why-it-happened chain), 비즈니스 영향 (business impact) 등이 포함되며, 추가적인 모델 호출 없이 동일한 리스크 분석 (risk breakdown)에서 직접 생성됩니다:
왜 하필 SigNoz인가
트레이스(Traces), 로그(Logs), 메트릭(Metrics)이 서로 연관된 하나의 신호로서 통합되어 있다는 점 — (Prometheus + Grafana + Tempo + Loki)처럼 세 가지 도구를 덕테이프로 이어 붙인 형태가 아니라는 점 — 이 Replay Timeline을 주말 만에 구축할 수 있었던 유일한 이유였습니다. 아래의 실패 사례 중 하나를 디버깅하던 중, risk_score와 risk_level 필드가 바로 그 자리에 구조화되어 있는 가공되지 않은 Logs Explorer의 모습은 다음과 같습니다:

실제로 무엇이 고장 났는가
아무것도 남기지 않고 사라져 버린 로그들. 첫 번째 실행부터 트레이스(Traces)는 SigNoz에 기록되었습니다. 하지만 로그(Logs)는 그렇지 않았습니다. 우리는 OTLP 로그 핸들러(log handler)를 연결하고, 엔드포인트(endpoint)를 호출한 뒤, Logs Explorer를 확인했습니다. 결과는 아무것도 없었고, 에러도, 경고도 없었습니다. 우리는 어딘가에서 propagate = False 설정이 루트 로거(root logger)의 핸들러에 도달하기 전 레코드를 차단하고 있다고 의심하여, 전체 로깅 설정에서 propagate라는 단어를 grep으로 검색해 보았지만 검색 결과가 전혀 없었습니다. 가설은 틀렸고, 추적할 에러 메시지도 없었습니다. 우리가 찾아낸 해결책은 루트 로거 대신 각 veritas.agent.* 로거에 이름을 지정하여 핸들러를 직접 연결하는 것이었습니다. 이를 통해 로그가 즉시 흐르기 시작했지만, 두 핸들러가 동일한 레코드에 대해 모두 실행되면서 로그 라인이 중복되는 정직한 부작용이 발생했습니다.
여기서 한 가지 짚고 넘어갈 점은, 우리의 기억에 의존하기보다 실제 OpenTelemetry Python 문서를 확인해 본 결과입니다. 문서에 명시된 표준 설정은 로깅 핸들러(logging handler)를 루트 로거(root logger)에 한 번 연결하며, 이름이 지정된 자식 로거(named child loggers)들은 Python의 기본값인 propagate=True를 통해 자동으로 이에 도달합니다. 이는 우리가 문제를 해결했던 방식과는 정반대입니다. OTel contrib 문서는 정확히 이와 같은 증상의 훨씬 더 흔한 원인을 지적합니다. 만약 앱의 어떤 부분에서라도 OTel 로깅 통합(logging integration)이 활성화되기 전에 logging.basicConfig()를 호출한다면, 통합 설정의 포맷(format)은 조용히 적용되지 않습니다. 만약 여러분도
한 번의 실행을 넘어, 플릿(fleet) 레벨 뷰가 있습니다 — 위험 분포, 단계별 지연 시간(latency), 가장 많이 실패하는 에이전트, 그리고 실제로 가장 많이 실패하고 있는 것에 기반한 규칙 기반 추천:

현재 운영 중인 것에 대해 과장하지 않으려는 차원에서 말씀드리자면: 이 페이지의 수치들은 세 개의 에이전트를 대상으로 한 시뮬레이션된 24시간 플릿 트래픽이며, 대시보드 자체에서도 이를 명시하고 있습니다. Refund Agent 카드는 실제 열어볼 수 있는 트레이스(trace)를 기반으로 하지만, Research 및 Support Agent는 아직 그렇지 않습니다. 독자가 직접 주의사항을 발견하게 하는 것보다는 여기서 말씀드리는 편이 낫겠습니다.
과거의 나에게 하고 싶은 말
'오류 없음(No errors)'은 깨끗한 건강 진단서가 아닙니다 — 그것은 볼륨만 낮춰진 증상일 뿐이며, 스택 트레이스(stack trace)와 똑같은 의심을 받을 자격이 있고, 어쩌면 그 이상일 수도 있습니다. 우리가 자체 계획 문서를 검토하다가 우리 코드로는 절대 생성할 수 없는 점수들을 발견했을 때가 바로 그 문서를 삭제해야 할 순간이지, 진실을 덮어 가릴 순간은 아니었습니다. 그리고 깨진 임포트(imports)에 대한 이론을 추격하기 전에, 디버깅하고 있는 파일이 실제로 존재하는지 확인하는 데 5분을 써보세요.
한 줄 요약
우리는 AI 에이전트가 자율성(autonomy)을 덜 가져야 한다고 생각하지 않습니다. 우리는 그들이 프로덕션 트래픽에 누군가가 신뢰하기 전에 모든 시스템이 결국 얻게 되는 것과 같은 책임감(accountability)을 필요로 한다고 생각합니다.
Veritas는 오픈 소스입니다:https://github.com/ishantgupta30/veritas-flight-recorder
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기