
스스로를 관찰하고 치유하는 AI 에이전트를 구축했습니다 - 그 방법은 다음과 같습니다
요약
AI 에이전트의 복잡한 실행 과정에서 발생하는 조용한 장애를 해결하기 위해, 스스로를 관찰하고 치유하는 'Sentinel' 시스템 구축 방법을 소개합니다. SigNoz의 OpenTelemetry를 활용하여 에이전트의 텔레메트리를 모니터링하고, 문제 발생 시 인간의 개입 없이 자동으로 복구하는 아키텍처를 다룹니다.
핵심 포인트
- AI 에이전트의 복잡한 호출 체인 내 장애를 식별하기 위한 관측성(Observability)의 중요성
- SigNoz와 OpenTelemetry를 활용한 에이전트 단계별 상세 트레이싱 구현
- 모니터링 에이전트가 워커 에이전트의 문제를 감지하고 자동 치유하는 루프 구축
- 로그 기반 디버깅을 넘어선 스팬(Span) 단위의 정밀한 데이터 분석
저는 모든 것을 중단시키는 장애보다 더 무서운 프로덕션 장애는 조용히 발생하는 장애라는 것을 알 정도로 오랫동안 DevOps를 수행해 왔습니다. 서서히 증가하는 지연 시간(Latency). 점점 높아지는 에러율(Error rates). 적절한 계측(instrumentation)을 하지 않아 대시보드는 초록색을 유지하고 있지만 치솟는 비용 같은 것들 말이죠.
AI 에이전트는 이를 더 악화시킵니다. 이들은 LLM 호출을 체이닝하고, 도구(tools)를 호출하며, 데이터베이스를 쿼리하고, 스스로 결정을 내립니다. 그 체인 중 어딘가에서 문제가 발생하면 명확한 스택 트레이스(stack trace)가 나타나지 않습니다. 장애는 호출 시퀀스의 어딘가에 존재하며, 만약 계측을 해두지 않았다면 당신은 눈을 가린 채 비행하는 것과 같습니다.
Agents of SigNoz 해커톤을 위해 저는 Sentinel을 구축했습니다. 이는 하나의 AI 에이전트가 SigNoz에 의해 완전히 관찰되고, 두 번째 에이전트가 해당 텔레메트리(telemetry)를 모니터링하다가 문제가 발생하면 첫 번째 에이전트를 자동으로 치유하는 시스템입니다. 인간의 개입(human in the loop)은 없습니다.
제가 이것을 어떻게 구축했는지, 그리고 무엇을 배웠는지 정확히 설명하겠습니다.
제가 해결하려 했던 문제
단순한 AI 에이전트를 예로 들어보겠습니다. 질문을 받고, 컨텍스트를 검색하고, LLM을 호출하여 답변을 반환합니다. 잘 작동할 때는 아주 좋습니다. 하지만 고장 나면 실제로 무엇을 볼 수 있을까요?
적절한 관측성(observability)이 없다면: 로그에 500 에러가 찍힙니다. 아마 타임아웃(timeout)일 수도 있습니다. 어떤 단계에서 실패했는지, 각 단계에 시간이 얼마나 걸렸는지, 토큰(tokens)을 얼마나 소모했는지, 혹은 실패가 도구(tool)에서 발생했는지 LLM에서 발생했는지에 대한 정보는 전혀 없습니다.
SigNoz의 OpenTelemetry 트레이스(traces)가 있다면: 폭포수(waterfall) 형태의 스팬(span)으로서 모든 단계를 볼 수 있습니다. 도구 호출에 20ms가 걸렸습니다. LLM 호출에 2.5초가 걸렸습니다. 토큰 수는 입력 850, 출력 180이었습니다. 세 번째 재시도(retry)는 다른 모델을 사용했습니다. 모든 정보가 그곳에 있습니다.

이것이 관측성 부분입니다. 하지만 저는 더 나아가고 싶었습니다. 문제를 볼 수 있게 되었다면, 시스템이 스스로를 고칠 수 있을까요?
아키텍처 (The Architecture)
두 개의 에이전트. 하나의 관측 플랫폼. 하나의 치유 루프(healing loop).
사용자 질문 (User question)
↓
WORKER AGENT (FastAPI :8001)
...
단순히 "애플리케이션에 계측(instrument)을 수행하고 대시보드를 만드는 것"과 차별화되는 점은 Sentinel 자체가 계측(instrumented)되어 있다는 것입니다. SigNoz에서 여러분은 worker-agent와 sentinel-agent라는 두 개의 서비스를 보게 됩니다. 치유자(healer)가 관찰되는 과정을 지켜볼 수 있습니다. 조사(investigation), 진단(diagnosis), 치유 동작(healing action) — 이 모든 것이 스팬(spans)으로 나타납니다.
Foundry를 사용한 SigNoz 설정
저는 이전에 Foundry를 사용해 본 적이 없었습니다. 알고 보니 이것이 현재 SigNoz를 배포하는 올바른 방법이었습니다.
Foundry를 사용하기 전에는 저장소(repo)를 클론하고, Docker Compose 파일을 수동으로 수정하며, 어떤 머신에서는 작동하고 다른 머신에서는 작동하지 않는 Bash 스크립트를 실행해야 했습니다. 재현성(reproducibility)이 중요한 프로젝트에서는 그리 좋은 방식이 아니었습니다.
Foundry를 사용하면 저는 단 12줄의 YAML을 작성했습니다:
apiVersion: v1alpha1
kind: Installation
metadata:
...
그다음:
foundryctl gauge -f casting.yaml # 환경 검증 (validates your environment)
foundryctl cast -f casting.yaml # 모든 요소 배포 (deploys everything)
두 개의 명령어. 2분. SigNoz가 localhost:8080에서 6개의 컨테이너와 함께 실행됩니다 — 저장용 ClickHouse, 4317 포트에서 스팬(spans)을 수신하는 OTel Collector, 웹 UI, 그리고 8000 포트의 MCP 서버입니다.
mcp 아래의 enabled: true 설정이 바로 여러분의 텔레메트리(telemetry)를 AI 에이전트에게 노출하는 부분입니다. 단 한 줄로 SigNoz는 Sentinel이 쿼리(query)할 수 있는 데이터 소스(data source)로 변합니다.

Foundry는 또한 정확히 해결된 구성(resolved configuration)을 고정하는 잠금 파일(lock file)인 casting.yaml.lock을 생성합니다. 제 저장소를 클론하는 누구라도 foundryctl cast를 실행하면 동일한 스택(stack)을 얻게 됩니다. 심사위원이 여러분의 배포를 다시 실행해 보는 해커톤(hackathon)에서는 이는 선택이 아닌 필수입니다.
Worker 계측 (Instrumenting the Worker)
자동 계측(Auto-instrumentation)은 코드 수정 없이 HTTP 레이어를 처리합니다:
FastAPIInstrumentor.instrument_app(app)
이제 /ask로 들어오는 모든 요청은 자동으로 루트 스팬 (root span)을 생성합니다. HTTP 메서드, URL, 상태 코드(status code) 등 모든 것이 제가 아무것도 작성하지 않아도 캡처됩니다.
흥미로운 부분은 자동 계측 (auto-instrumentation)이 볼 수 없는 비즈니스 로직을 위한 수동 스팬 (manual spans)입니다:
with tracer.start_as_current_span("agent.llm.call") as span:
span.set_attribute("gen_ai.system", "groq")
span.set_attribute("gen_ai.request.model", model)
...
이 gen_ai.* 이름들은 OpenTelemetry GenAI 시맨틱 컨벤션 (semantic conventions)을 따릅니다. SigNoz는 이를 기본적으로 읽어 들입니다. 별도의 커스텀 설정 없이도 토큰 수 (token counts)가 트레이스 (traces)에 나타납니다. 처음부터 이름을 올바르게 지정하면 도구들이 알아서 작동합니다.
초기에 제가 실수했던 한 가지는 예외 (exception)를 발생시키면 스팬 (span)이 자동으로 ERROR로 표시될 것이라고 가정한 것이었습니다. 그렇지 않습니다. 직접 설정해야 합니다:
if _state["flaky_api"] and random.random() < 0.4:
span.set_status(trace.StatusCode.ERROR, "LLM timeout (injected)")
span.set_attribute("error.type", "injected_timeout")
...
해당 set_status 호출이 없으면, 500 에러가 반환되더라도 SigNoz에서는 스팬이 OK로 표시됩니다. 에러 스팬을 카운트하는 경고 규칙 (alert rule)은 절대 실행되지 않습니다. 제가 실제로 트레이스 속성 (trace attributes)을 살펴보고 있었기 때문에 이 문제를 발견할 수 있었습니다. 이것이 바로 깊이 있는 계측 (instrumentation)이 필요한 또 다른 이유입니다.
트레이스가 실제로 보여주는 것
트래픽을 일부 보낸 후, SigNoz의 워터폴 (waterfall) 차트입니다:
GET /ask 2.52s
agent.request 2.52s
agent.tool.search 0.02ms
...
전체 2.52초 중 LLM 호출이 2.5초를 차지합니다. 도구 호출 (tool call)은 20ms입니다. 지연 시간 (latency)을 줄이고 싶다면 도구가 아니라 LLM에 집중해야 합니다. 이는 즉각적으로 실행 가능한 정보입니다. 이 트레이스가 없다면 여러분은 추측만 하고 있었을 것입니다.
Worker agent GET /ask trace waterfall with agent.llm.call spans

자가 치유 루프 (Self-Healing Loop) 구축하기
Worker에는 런타임에 동작을 변경할 수 있는 /control 엔드포인트가 있습니다:
@app.post("/control")
def control(action: dict):
action_type = action.get("type", "")
...
Sentinel은 치유를 적용하기 위해 이 엔드포인트를 호출합니다. 상태 변경은 즉각적이며, 재시작이 필요하지 않습니다.
SigNoz 알람이 발생하면, Sentinel은 웹훅(webhook) POST를 수신하고 다음을 실행합니다:
diagnosis = investigate(alert_name, payload) # LLM 호출
action = PLAYBOOK.get(alert_name, {"type": "log_only"}) # 결정론적 조회 (deterministic lookup)
requests.post(f"{WORKER_URL}/control", json=action) # 치유 (heal)
플레이북(playbook)은 의도적으로 단순하게 설계되었습니다:
PLAYBOOK = {
"HighErrorRateWorker": {"type": "reset"},
"HighLatencyWorker": {"type": "switch_model", "model": "llama-3.1-8b-instant"},
...
LLM은 진단(diagnose)을 수행합니다. 플레이북은 행동(act)합니다. LLM이 치유 동작을 스스로 만들어내는 것은 신뢰하지 않습니다. 실제 시스템에서 그런 방식은 예측 불가능할 수 있기 때문입니다. LLM은 사람이 읽을 수 있는 진단을 제공하고, 코드는 결정론적인(deterministic) 응답을 제공합니다. 이는 의도적인 설계 결정이었습니다.
Docker 네트워킹 문제
SigNoz 알람 웹훅이 Sentinel을 가리키도록 설정했을 때 계속해서 연결 거부(connection refused) 오류가 발생했습니다. localhost:8080의 SigNoz와 localhost:8002의 Sentinel은 서로 연결되어야 하는 것 아닌가요? 맞습니다, 그래야 하죠.
하지만 틀렸습니다. SigNoz는 Docker 컨테이너 내부에서 실행됩니다. 컨테이너 내부에서 localhost는 사용자의 Mac이 아니라 컨테이너 자신을 의미합니다. 이를 알아내는 데 생각보다 오랜 시간이 걸렸습니다.
해결책은 호스트 머신을 가리키는 Docker의 특수 DNS 이름입니다:
host.docker.internal
어떤 Docker 컨테이너 내부에서든, 이 이름은 호스트 머신의 IP로 해석됩니다. 알림 채널 URL을 이 주소를 사용하도록 업데이트하자마자 테스트 알림이 즉시 전송되었습니다.

대시보드
Sentinel Operations 대시보드에는 SigNoz Query Builder로 구축된 4개의 패널이 있습니다:
- Request Rate (요청률) — 시간에 따른 분당 요청 수
- p99 Latency (p99 지연 시간) — 밀리초(ms) 단위의 99번째 백분위수 지연 시간
- Error Rate (오류율) — 실패한 요청의 비율
- LLM Calls (LLM 호출) — 분당 agent.llm.call 스팬(span) 횟수

실패를 주입하면 오류율 (Error Rate) 패널이 눈에 띄게 상승합니다. Sentinel이 치유하면 다시 0으로 떨어집니다. 그 시각적 효과는 그 어떤 설명보다 설득력이 있습니다.
실제로 작동하는 모습 관찰하기
전체 데모 시퀀스:
# 1. 실패 주입
curl -X POST localhost:8001/control \
-H "Content-Type: application/json" \
...
Sentinel의 응답:
{
"alert": "HighErrorRateWorker",
"diagnosis": "High error rates indicate LLM API failures...",
...
오류율이 0으로 떨어집니다. 시스템이 치유되었습니다. 실패 주입부터 복구까지 30초 미만이 소요되었습니다. 초기 주입 이후에 인간이 손댄 것은 아무것도 없습니다.

깨달음을 얻은 부분
Traces 탐색기를 열고 sentinel-agent로 필터링했을 때, Sentinel의 조사가 전체 트레이스 (trace)로 나타나는 것을 확인했습니다. 치유 과정의 모든 단계를 커버하는 9개의 스팬 (span)이 있었습니다. 조사는 3.7초가 걸렸습니다. 실제 치유에는 20ms가 소요되었습니다. 병목 현상은 해결 방법 자체가 아니라 LLM 진단이었습니다.
이것은 제가 예상하지 못했던 디테일입니다. 치유 도구가 작동하는 것을 지켜볼 수 있습니다. 진단에 시간이 얼마나 걸리는지, 성공했는지, 어떤 조치가 적용되었는지 확인할 수 있습니다. 만약 Sentinel이 무언가를 치유하는 데 실패하면, 그것은 ERROR 스팬 (span)으로 나타나며, 이는 그 자체로 또 다른 알람을 트리거할 수도 있습니다.
치유 도구는 관찰됩니다. 이는 운영 도구 (operational tooling)에 대해 생각하는 방식을 바꿉니다.
SigNoz services tab showing worker-agent and sentinel-agent

내가 다르게 했을 부분
도구 호출 (tool call)은 모킹 (mocked)되어 있으며, 정해진 검색 결과 (canned search result)를 반환합니다. 실제 버전이라면 실제 벡터 데이터베이스 (vector database)에 접근할 것이며, 바로 그 지점에서 가장 흥미로운 트레이스 (traces)를 확인할 수 있을 것입니다. 검색 실패 (retrieval failures)와 캐시 미스 (cache misses)는 에이전트 성능 저하 (agent degradation)의 거대한 원인이지만, 이를 제대로 계측 (instruments)하는 사람은 거의 없습니다.
또한 Sentinel에 쿨다운 기간 (cooldown periods)을 추가하겠습니다. 현재는 경고 (alert)가 계속 발생하면 Sentinel도 계속 응답합니다. 프로덕션 환경에서는 중복 제거 (deduplication)가 필요합니다. 만약 동일한 경고가 2분 안에 세 번 발생한다면, 세 번의 동시 치유 시도 (healing attempts)를 원하지는 않을 것입니다.
그리고 OTel Collector에서의 테일 샘플링 (tail sampling)입니다. 실제 규모 (real scale)에서는 모든 트레이스를 저장하고 싶지 않을 것입니다. 대신 모든 에러, 모든 느린 요청, 그리고 아마도 정상 요청의 1% 정도를 저장하고 싶을 것입니다. 이 아키텍처는 코드 변경 없이 이를 지원하며, 이는 Collector 설정 (configuration)의 문제입니다.
내가 실제로 배운 것
Foundry는 현재 SigNoz를 배포하는 올바른 방법입니다. 락 파일 (lock file) 방식은 여러분의 배포가 git에 넣을 수 있고, 팀원들과 공유할 수 있으며, 심사위원들이 정확하게 재현할 수 있는 버전 관리된 아티팩트 (versioned artifact)임을 의미합니다. 여러분의 애플리케이션 코드 (application code)를 다루는 것과 동일한 방식으로 관측성 인프라 (observability infrastructure)를 코드로서 취급(as code)하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기