우리의 장애 대응 에이전트가 12번 중 7번이나 근본 원인을 잘못 파악했습니다. 하지만 단 한 번도 잘못된 롤백을 수행하지는 않았습니다.
요약
장애 대응 에이전트 'Agent K'의 프로덕션 테스트 결과, 근본 원인 파악에는 한계가 있었으나 안전 메커니즘을 통해 단 한 번의 잘못된 롤백도 발생하지 않았음을 보고합니다. 모델의 불확실성을 인정하고 코드 기반의 결정론적 안전 장치를 구축하는 것이 핵심입니다.
핵심 포인트
- 모델의 진단 정확도보다 시스템의 안전한 동작(롤백 결정)에 집중
- LLM의 오류를 방지하기 위한 결정론적 정책 게이트(Policy Gate) 도입
- SigNoz MCP 서버와 OTel 신호를 활용한 에이전트 조사 프로세스
- 모델의 불확실성을 상쇄하는 코드 기반의 안전 메커니즘 구축
우리는 12개의 시드(seeded) 프로덕션 장애를 대상으로 Agent K를 실행했습니다. Agent K는 12번 중 5번 정확한 근본 원인(root cause)을 찾아냈습니다. 명확하게 귀속시키기 어려운 실행 건들을 제외하면 9번 중 3번입니다. 동일한 12번의 실행 동안 Agent K는 정확한 롤백/롤백 미실시 판정을 10번 내렸으며, SigNoz 링크가 뒷받침되지 않은 주장을 단 한 건도 게시하지 않았고, 정책 게이트(policy gate)를 벗어난 동작을 단 한 번도 수행하지 않았습니다.
"정확한 진단"과 "올바른 동작" 사이의 이 간극이 바로 이 프로젝트의 핵심입니다.
컨텍스트 (Context)
Agent K는 Agents of SigNoz (WeMakeDevs × SigNoz) Track 01에 제출한 우리의 결과물입니다. 설정은 다음과 같습니다: FastAPI RAG 지원 서비스 — Postgres + pgvector에 저장된 72개의 합성 도움말 센터 문서, 로컬 sentence-transformers 임베딩(embeddings), 하나의 OpenAI 호환 클라이언트 뒤에 있는 무료 티어 모델들(기본값은 Groq이며, 환경 변수 하나로 Cerebras, Gemini, OpenRouter로 교체 가능) — OpenTelemetry로 계측되어 트레이스(traces), 메트릭(metrics), 로그(logs)를 OTLP를 통해 SigNoz로 전송합니다. 우리는 Foundry를 사용하여 SigNoz를 설치했으므로 casting.yaml/casting.yaml.lock이 커밋되어 스택을 재현할 수 있으며, 나중에 동일한 익스포터(exporter)를 SigNoz Cloud로 지정할 때 코드 변경이 필요하지 않았습니다.
알람이 발생하면 Agent K는 SigNoz MCP 서버를 통해 조사하고, 해결 가능한 SigNoz 링크를 포함하는 주장만을 게시하며, 순수 Python 코드가 허용하는 경우에만 서비스를 롤백(rollback)합니다.
우리는 공학적으로 제거하려고 노력하지 않았던 하나의 가정에서 시작했습니다: 모델은 틀릴 것이다. 따라서 정확성(correctness)은 안전 메커니즘이 될 수 없습니다. 안전은 반드시 코드여야 합니다.
앱을 망가뜨리는 네 가지 방법, 그중 두 가지는 롤백이 필요합니다
모든 것은 네 가지 시드된 실패(seeded failures)와 하나의 의도적인 비대칭성을 가진 프로세스 내 플래그 저장소(in-process flag store)에 의존합니다:
# app/flags.py
FLAG_NAMES: tuple[str, ...] = (
"prompt_regression", "retry_storm", "retrieval_latency", "db_pool_exhaustion",
...
SigNoz에는 네이티브 배포 마커 (deployment-marker) API가 없습니다 (SigNoz/signoz#6162를 추적했으나, 미출시 상태로 종료됨). 따라서 우리는 이미 존재하는 파이프라인을 통해 자체적인 deployment.marker OTel 스팬 (span)을 방출합니다. 두 가지 시나리오는 하나를 방출하고, 나머지 두 가지는 방출하지 않습니다. 이러한 존재/부재의 비대칭성 (asymmetry)이 바로 쿼리 가능한 신호 (queryable signal)입니다. 즉, 마커가 없다는 것은 롤백 (rollback)이 도움이 되지 않을 것이라는 긍정적인 증거가 됩니다.
SigNoz의 POST /ask 한 건: rag.retrieval, rag.prompt_construction 및 chat 스팬이 있으며, SQLAlchemy DB 스팬이 retrieval 하위에 중첩되어 있음.
LLM 호출이 허용되지 않는 게이트 (gate)
모두 통과해야 하는 6가지 결정론적 (deterministic) 체크 항목:
# app/policy.py — 이 모듈은 절대로 LLM을 호출해서는 안 됩니다.
ACTION_ALLOWLIST: tuple[str, ...] = ("rollback",) # 정확히 하나의 항목
SANDBOX_SERVICES = frozenset({"agent-k-rag-service"})
...
slo_breach, allowlist, cooldown, confidence, deployment_related, sandbox_scope. 모든 알 수 없는 값은 '실패(fail closed)' 처리됩니다. 우리가 번 레이트 (burn rate)를 읽을 수 없는 경고는 0.0으로 파싱되며, 이는 SLO 체크를 통과하지 못하게 하여 동작을 거부합니다.
"LLM 미사용" 부분은 우리가 스스로 지키겠다고 믿는 주석 수준의 규칙이 아닙니다. 한 테스트는 ast를 사용하여 모듈의 임포트 그래프 (import graph)를 탐색하며, app.llm이 나타나면 빌드를 실패시킵니다. 또 다른 테스트는 llm.generate를 몽키패치 (monkeypatch)하여 호출 시 에러를 발생시키도록 한 뒤 전체 평가를 실행하며, 이를 통해 grep으로는 놓칠 수 있는 간접 호출 (indirect call)까지 잡아냅니다. 두 테스트 모두 219개의 테스트 스위트 (test suite)에 포함되어 있습니다.
자체 평가(eval)가 잡아낸 버그: 다시 스며든 '모델 신뢰' 문제
이 부분은 두 번 읽을 가치가 있습니다.
deployment_related 체크는 현재 어떤 시나리오를 보고 있는지 알아야 하며, 가장 저렴한 정보원은 모델의 주장 텍스트 (claim text)였습니다. 우리의 시스템 프롬프트 (system prompt)는 모델이 답변을 시작할 때 반드시 네 가지 시나리오 이름 중 하나로 시작하도록 강제합니다. 여기까지는 괜찮았습니다. 하지만 첫 12회 실행 평가 결과 다음과 같은 내용이 출력되었습니다: 두 건의 db_pool_exhaustion 사고가 retry_storm으로 서술되었고, 각각 데이터베이스 풀 문제를 해결할 수 없는 롤백을 승인 (approving a rollback) 했습니다.
게이트(gate)가 고장 난 것이 아니었습니다. 게이트는 우리가 작성한 대로 정확히 작동하고 있었습니다. 그리고 우리가 작성한 내용은 바로 "모델이 문장 안에 retry_storm이라는 단어를 작성함으로써 롤백을 승인할 수 있다" 였습니다. 우리가 주장했던 모든 안전 속성(safety property)에는 문자열 매칭(string match) 형태의 구멍이 뚫려 있었습니다.
해결책은 검증 과정이 서술(narration)이 아닌 텔레메트리(telemetry)의 확증을 요구하도록 만드는 것이었습니다:
# app/policy.py — check 5
observed = frozenset(deployment_markers or ())
is_deployment_class = incident_type in DEPLOYMENT_CLASS_FLAGS
...
그리고 deployment_markers는 오직 도구(tool)의 결과로부터만 채워지며, 결코 주장(claim)으로부터 채워지지 않습니다:
# app/investigation.py — 증거 수집 단계 내부
if ev_type == "deployment":
inv.deployment_markers_seen.update(
...
또한 우리는 순서(ordering)가 실행을 뒷받침하도록(load-bearing) 만들었습니다. 즉, 마커 쿼리(marker query)가 수행되기 전에 중단된 조사는 결코 무엇도 승인할 수 없도록 설계했으며, 이를 주석으로 남겨두는 대신 임포트(import) 시점에 단언(assert)했습니다:
assert _DEPLOYMENT_QUERY_INDEX < MIN_EVIDENCE_ITERATIONS, (
"the deployment-marker query must fall within MIN_EVIDENCE_ITERATIONS, or the "
"policy gate can never corroborate a deployment-class claim"
...
이 수정 이후, 두 가지 잘못된 승인 사례가 모두 사라졌습니다. 에이전트는 여전히 오진을 합니다. 앞선 세 번의 실행 모두에서 retrieval_latency를 "retry_storm"이라고 불렀습니다. 하지만 이제 오진은 롤백이 아닌 _거부(denial)_를 생성합니다.
사고 보고서의 정책 결정: 5개의 체크는 통과했으나, deployment_related가 실패하여 작업이 거부되었으며, 다음 단계로 인간의 개입을 요청함.
실제로 실행되고 나서야 깨진 다섯 가지 사항
1. SigNoz의 OTLP 수신기(receivers)는 첫 실행 설정(first-run setup)을 완료하기 전까지 바인딩되지 않습니다. 새로 구성된 스택은 모든 컨테이너가 가동 중이고 curl localhost:8080이 200을 반환하여 건강해 보이지만, curl localhost:8318/v1/traces는 connection reset by peer를 반환하며, 이는 마치 익스포터(exporter)가 고장 난 것처럼 보입니다. 컬렉터(collector)는 OpAMP에 의해 관리되며 조직(org) 없이는 에이전트를 등록할 수 없으므로, 백엔드는 cannot create agent without orgId 상태로 루프를 돕니다. 직접 확인해 보세요:
curl -s http://localhost:8080/api/v1/version # "setupCompleted":false
조직을 등록(UI 또는 POST /api/v1/register)하면 수신기(receivers)가 활성화됩니다. 우리는 app/telemetry.py가 잘못되었다고 믿으며 실제 시간을 허비했습니다. 하지만 그것은 잘못되지 않았습니다.
2. 우리는 MCP 도구(tool) 이름을 임의로 만들어냈습니다. 우리는 API에 대한 정신적 모델을 바탕으로 query_traces, query_logs, query_metrics라고 작성했습니다. 하지만 signoz-mcp-server v0.9.0의 실제 인벤토리는 signoz_search_traces, signoz_search_logs, signoz_aggregate_traces였습니다. 모든 증거 쿼리가 실패하면서 모든 주장이 무효화되고 모든 조사가 에스컬레이션되었습니다. 이는 에이전트의 문제처럼 보이는 완전한 실패였습니다. 두 가지 함정이 더 뒤따랐습니다. 릴리스(releases)에 Windows 바이너리가 포함되어 있지 않았고(우리는 WSL에서 교차 컴파일했습니다), SigNoz v0.134는 SIGNOZ-API-KEY를 통해 전송된 JWT를 거부하고 Authorization: Bearer 형식을 요구합니다. 그런데 이 토큰은 30분마다 만료되는데, 이는 데모 도중에 발견하기에는 아주 즐거운(?) 사실입니다.
3. 무료 티어(free tier)는 실제적인 설계 제약 사항입니다. 20개 행의 트레이스(trace) 검색은 가설이 세워지기도 전에 8,424개의 토큰을 소모했고 HTTP 413 오류를 반환했습니다. SigNoz는 행당 가능한 모든 스팬(span) 속성을 반환하며, 이 앱의 경우 대부분이 null입니다. 따라서 우리는 데이터를 잘라내는 대신 null을 제거합니다. 신호(signal)는 동일하게 유지하면서 토큰 수를 한 자릿수만큼 줄일 수 있습니다. 그 후 모델은 쿼리 엔진(query engine) 자체의 통계를 바탕으로 진단을 시작했습니다: "db_pool_exhaustion is likely due to an extremely high number of rows scanned (1018) and bytes scanned (10434)" (db_pool_고갈은 매우 높은 행 스캔 수(1018) 및 바이트 스캔 수(10434)로 인해 발생했을 가능성이 높음). 이것은 에이전트 K가 수행한 쿼리 자체의 통계였습니다. 따라서:
_ENGINE_NOISE_KEYS = frozenset({
"meta", "rowsScanned", "bytesScanned", "durationMs", "stepIntervals",
"nextCursor", "queryName", "columnType", "aggregationIndex", "signal",
...
증거는 사고(incident) 자체를 설명해야 하며, 사고를 조사하는 행위를 설명해서는 안 됩니다.
4. 에이전트가 우리의 테스트 스위트(test suite)를 조사했습니다. 우리의 테스트는 app.main을 임포트(import)하는데, 이는 setup_telemetry()를 호출하며, 이 함수는 pytest가 생성하는 모든 스팬(span)을 실제 SigNoz로 전송합니다. 라이브 실행(live run) 중에 prompt_regression만 주입되었음에도 불구하고, retry_storm=7 대 prompt_regression=3이라는 배포 마커(deployment-marker) 카운트를 읽어 들였습니다. 즉, retry_storm 마커는 pytest의 것이었으며, 에이전트는 이를 근거로 오진을 내렸습니다. 이제 AGENT_K_DISABLE_OTLP_EXPORT를 통해 세 가지 프로바이더(provider)를 모두 빌드하면서도 내보내기(export)는 억제할 수 있습니다.
5. 싱글 페이지 애플리케이션(SPA)의 200 응답은 해결 가능한 링크가 아닙니다. 우리는 SigNoz에 존재하지 않는 경로인 /deployments로 연결되는 증거 링크를 만들었습니다. 우리의 링크 체크(link checker)는 통과했습니다. SigNoz는 SPA이므로 모든 경로에 대해 200 응답을 보내기 때문에, 상태 체크(status check)만으로는 실제 경로와 오타를 구분할 수 없습니다. 링크를 클릭한 사람은 아무것도 없는 페이지에 도달했습니다. 배포 마커는 스팬(span)이므로, 올바른 목적지는 트레이스 익스플로러(traces explorer)였어야 합니다. (우리의 기본 베이스 URL도 잘못되었습니다: Foundry가 8080에서 서비스되는 SigNoz의 이전 포트인 3301로 설정되어 있었습니다.)
롤백이 실제로 발생하는 위치
에이전트 K는 Docker 소켓을 보유하지 않습니다. 별도의 deployer 사이드카(sidecar)가 이를 보유하며, 이 사이드카는 **빈 본문(empty body)**을 받는 단 하나의 변경(mutating) 엔드포인트를 노출합니다. 호출자는 이미지, 서비스, 명령어를 지정하지 않습니다. 이미지를 지정할 수 없는 호출자는 공격자의 이미지를 배포하도록 속임을 당할 수 없습니다.
deployer:
volumes:
# 한 줄로 요약된 권한 경계(privilege boundary). 이 프로젝트의 다른 어떤 것도 이를 갖지 못합니다.
...
그 안의 규칙 하나를 배우기 위해 실패한 롤백을 한 번 거쳐야 했습니다: 명령어는 docker compose up -d --force-recreate여야 하며, 절대 restart가 아니어야 합니다. restart는 컨테이너를 현재 이미지로 다시 실행하고 아무것도 변경하지 않은 채 성공을 보고합니다. 이 경우 롤백이 작동하는 것처럼 보이지만 사고는 계속됩니다.
SigNoz의 Agent K 대시보드: 정책 판결(policy verdicts), 작업이 거부된 이유, 그리고 복구 확인(recovery-verified) 횟수를 나란히 표시합니다.
우리가 첫날 스스로에게 해주고 싶은 말
- 진단(diagnoses)만이 아니라 판결(verdicts)의 등급을 매기세요. 우리의 정확도(accuracy) 수치는 평범하지만, 판결(verdict) 수치는 좋습니다. 그리고 새벽 3시에 서비스가 재시작될지 여부를 결정하는 것은 후자입니다. 두 번의 판결 실패는 모두 안전한 방향이었습니다. 즉, 조치를 취할 수 있었음에도 조치를 거부한 경우였습니다.
- 신뢰도 점수(confidence score)가 1.0에 도달하게 두지 마세요. 초기 실행 단계에서 명백히 틀린 진단에 대해 "신뢰도 100%"가 출력되었습니다. 모델은 0.9를 제안했고 우리의 부스트(boosts)가 이를 1.0으로 고정해 버린 것입니다. 이제 우리는 재보정된 신뢰도(recalibrated confidence)를 영구적으로 0.95로 제한합니다.
- 자신의 텔레메트리(telemetry)를 포함하여 모든 입력이 공격자 형태(attacker-shaped)라고 가정하세요. 위의 5가지 실패 사례 중 2가지는 에이전트가 자기 자신에 대한 데이터, 즉 자신의 쿼리 통계와 자신의 테스트 스위트(test suite)의 스팬(spans)을 바탕으로 추론한 경우였습니다.
솔직한 요약: 모델은 시스템에서 가장 신뢰할 수 없는 구성 요소이며, 그 점을 고려하여 설계하는 것이 다른 모든 것을 더 단순하게 만들었습니다. 이 모든 것은 하나의 머신, 하나의 서비스, 그리고 우리가 직접 작성한 네 가지 실패 모드라는 우리의 설정 내에서 작동했습니다. 우리는 우리가 직접 작성하지 않은 사고를 대상으로 이 모든 것을 테스트해보지는 않았습니다.
코드: github.com/SujalXplores/Agent-K · Agents of SigNoz Track 01을 위해 구축되었습니다. AI 코딩 지원(Claude Code)을 사용하여 구축되었으며 해당 내용은 리포지토리(repo)에 공개되어 있습니다. 출시된 에이전트 자체는 무료 티어 제공업체와 로컬 임베딩(local embeddings)만으로 실행됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기