
AI 에이전트가 스스로의 실수를 바로잡는 상상, Rewind의 실제 작동 원리
요약
AI 에이전트의 실행 실패를 디버깅하기 위해 SigNoz의 트레이스와 LangGraph의 체크포인터를 활용한 'Rewind' 시스템의 작동 원리를 설명합니다. 에이전트의 실행 과정을 리플레이하여 특정 단계의 호출을 수정하고 테스트할 수 있는 '타임머신' 기능을 구현하는 방법을 다룹니다.
핵심 포인트
- SigNoz와 LangGraph를 결합하여 에이전트 실행 리플레이 구현
- 멀티 에이전트 워크로드의 실패 지점을 격리하고 재현 가능
- 트레이스(trace) 데이터를 활용한 에이전트 디버깅 및 수정 테스트
- OpenInference를 통한 LangChain/LangGraph 내부 동작 캡처
AI 에이전트가 프로덕션(production) 환경에서 실패했을 때, 당신은 그저 실패했다는 통보를 받을 뿐입니다. 당신은 가장 중요한 단 하나의 질문에 답할 기회를 거의 얻지 못합니다: 만약 세 번째 에이전트가 다른 호출(call)을 했다면 어땠을까? 실행 과정을 다시 재생(replay)할 수도 없고, 문제를 일으킨 단계를 격리할 수도 없습니다. 전체 과정을 다시 실행하고 운에 맡기지 않고서는 수정 사항을 테스트할 수도 없습니다.
저는 SigNoz의 트레이스(traces)와 LangGraph의 체크포인터(checkpointer)가 실제로 그 질문에 답할 수 있는지 확인하기 위해, 멀티 에이전트 실행을 위한 "타임머신"인 Rewind를 해커톤 기간 동안 구축했습니다. 결론부터 말하자면, 가능합니다. 이 포스트는 솔직한 버전입니다: 리플레이(replay)가 어떻게 작동하는지, 그리고 이를 거의 망가뜨릴 뻔했던 세 가지 요소에 대해 다룹니다.

설정: 미묘하게 잘못된 수정을 내놓는 크루
관찰 대상인 워크로드(workload)는 GitHub 스타일의 이슈를 해결하는 4인 에이전트 코딩 크루(Planner → Coder → Tester → Reviewer)입니다. 이슈는 의도적으로 모호하게 설정되었습니다:
"10개 초과의 주문 라인에는 10% 할인이 적용되어야 합니다."
하지만 테스트는 정확히 10개일 때 할인이 적용될 것을 요구합니다. 성능이 낮은 모델은 명세(spec)를 문자 그대로 읽고 다음과 같이 작성합니다:
if quantity > 10: # 성능이 낮은 모델이 작성한 코드
subtotal *= 0.90
그리고 경계값 테스트(boundary test)는 매우 실제적인 어설션(assertion) 오류와 함께 실패합니다:
def test_bulk_discount_boundary():
assert cart_total([(10.0, 10)]) == 90.0
E assert 100.0 == 90.0
이것이 바로 제가 로그를 다시 읽는 것이 아니라, 트레이스(trace)로부터 재현하고 수정하고 싶은 실패 사례입니다.
모든 에이전트 단계를 스팬(span)으로 만들기
저는 두 가지 방식으로 크루에 인스트루멘테이션(instrumentation)을 적용했습니다. openinference-instrumentation-langchain은 LangChain/LangGraph의 내부 동작을 자동으로 캡처하며, 아주 작은 커스텀 컨텍스트 매니저(context manager)가 각 노드(node)를 감싸서 나중에 쿼리하고 싶은 속성(attributes)들을 스팬(span)에 정확히 담도록 합니다.
@contextmanager
def node_span(config, name, inputs):
with tracer.start_as_current_span(f"crew.{name}") as span:
...
저는 모델/토큰 필드에 대해 맞춤형(bespoke)이 아닌 OpenTelemetry의 GenAI semantic conventions (gen_ai.request.model, gen_ai.usage.*_tokens)를 사용하였고, 앱 전용 부분에는 rewind. 네임스페이스(namespace)를 사용했습니다. 스팬(Span)은 OTLP/HTTP를 통해 :4318 포트에 있는 SigNoz의 인제스터(ingester)로 내보내집니다. rewind.thread_id는 전체 실행(run)을 하나로 엮어주는 스레드(thread)입니다. 이것을 기억하세요, 이것이 핵심 비결입니다.
SigNoz에 기록되는 내용은 다음과 같습니다. 모든 플래너(planner)/코더(coder)/테스터(tester)/리뷰어(reviewer) 단계가 쿼리(query) 가능합니다:

주의사항 #1: Docker Desktop이 없을 때 SigNoz를 실행하는 방법
저는 진정으로 깔끔한 원클릭 설치 도구인 Foundry (casting.yaml → casting.yaml.lock → cast)를 사용하여 SigNoz를 배포했습니다. 하지만 제 컴퓨터에는 Docker Desktop이 없었고, 남겨진 잔해들이 문제를 일으켰습니다. docker는 끊어진 심볼릭 링크(symlink)를 가리키고 있었고, docker compose는 고장 난 CLI 플러그인이었으며, Foundry의 cast는 docker-credential-desktop executable not found 오류와 함께 종료되었습니다. 해결 순서는 다음과 같습니다:
brew install colima docker및colima start실행 — Desktop 없이 사용할 수 있는 가벼운 Docker 엔진입니다.- compose 플러그인을
~/.docker/cli-plugins/로 다시 연결(relink)합니다. - Foundry가 레지스트리(registry)에 인증할 수 있도록
~/.docker/config.json에서credsStore: "desktop"라인을 삭제합니다.
이 중 어떤 것도 튜토리얼에는 나와 있지 않습니다. 만약 Docker Desktop이 없는 깨끗한 Mac을 사용 중이라면, Colima를 사용하고 config.json을 수정하는 것이 방법입니다.
SigNoz에서 텔레메트리(telemetry)를 다시 읽어오기
제가 가장 중요하게 생각하는 부분입니다. Rewind는 자체적인 트레이스 저장소(trace store)를 유지하지 않습니다. 실행(run)의 타임라인을 렌더링하기 위해, 백엔드는 v5 쿼리 API를 통해 SigNoz에 직접 쿼리하여 자신의 스팬(span)을 가져옵니다:
POST /api/v5/query_range
{
"requestType": "raw",
...
v5에서 정확하게 처리해야 할 부분은 filter.expression 문자열 구문입니다. 이는 미니 쿼리 언어(AND, =, EXISTS)이며, SigNoz가 필드를 찾을 수 있는지 여부는 fieldContext: "attribute"와 `

스펙 가드레일 (spec guardrail)과 더 강력한 모델을 사용하여 실패한 실행을 포크 (fork)하면, 코더 (coder)가 해당 라인을 if quantity >= 10으로 다시 작성하며, 이전에 빨간색이었던 동일한 pytest가 초록색으로 변합니다. 실패한 상태와 수정된 상태가 실제 트레이스 (traces)를 기반으로 나란히 놓여 있으며, 수정에는 몇 개의 토큰이 더 소모되지만 비용 차이 (cost deltas)를 통해 이를 정직하게 확인할 수 있습니다.
주의사항 #3: 라이브 쿼리 (live query)에 의존하지 말아야 할 때를 아는 것
SigNoz v0.134에서 인증 (auth) 방식이 변경되었습니다 (로그인은 이전 가이드에 나온 위치가 아닌 /api/v2/sessions 아래에 있습니다). 저는 이 사실을 알기 전까지 시간을 허비했지만, 나중에 다음과 같이 방어할 수 있는 결론을 내렸습니다: 체크포인터 (checkpointer)가 타임라인의 진실의 원천 (source of truth)이며, SigNoz 쿼리는 보충 (enrichment) 정보입니다. 쿼리 경로가 설정되어 있다면, 타임라인은 SigNoz로부터 실제 토큰/비용 정보를 가져오고 모든 카드는 라이브 트레이스 (live trace)로 딥링크 (deep-link)됩니다. 하지만 설정되어 있지 않더라도, 앱은 여전히 체크포인터로부터 재현 및 포크를 수행합니다. 갓 부팅된 관측성 스택 (observability stack)에 대한 단 하나의 네트워크 호출에 과도하게 의존하는 데모는 무대 위에서 실패할 수밖에 없습니다. 우아하게 성능을 저하시키며 작동하도록 (Degrade gracefully) 설계하세요.
동일한 텔레메트리 (telemetry)가 대시보드와 실패율 알림 (failure-rate alert)을 구동하며, 이 둘 모두 SigNoz API를 통해 프로그래밍 방식으로 구축되었습니다. 알림은 쿼리 빌더 (Query Builder)에서 직접 가져온 실패 횟수를 임계값 (threshold)과 비교하여 그래프로 나타냅니다:

과거의 나에게 해주고 싶은 말
- 쿼리를 단 하나라도 작성하기 전에 span 속성(span attributes)을 먼저 결정하세요. 저는
rewind.thread_id가 조인 키(join key)라는 것을 결정하지 못해서 필드 이름을 두 번이나 변경했습니다. 스키마(schema)를 먼저 설계하세요. fieldContext은 선택적인 사소한 정보가 아닙니다. v5 쿼리 API(query API)에서 필드를attribute로 표시하느냐resource로 표시하느냐는 데이터와 빈 결과 집합(result set)을 가르는 차이입니다.- 대시보드 위젯에는
layout이 필요합니다.id/layout없이 위젯을 게시했더니 대시보드가 빈 "Welcome" 상태로 렌더링되었습니다. 패널들은 저장되었지만 그리드(grid) 위에 배치되지 않았던 것입니다. - 재생(replay)은 프레임워크에 맡기세요. 저는 거의 "N단계부터 재실행"하는 엔진을 직접 구현할 뻔했습니다. LangGraph의
get_state_history+update_state+invoke(None, cfg)가 이미 그 엔진 역할을 수행하고 있으며, 이것이 바로 포크(fork)가 모의(mock)가 아닌 신뢰할 수 있는 이유입니다. - 두 가지 증명이 하나보다 낫습니다. pytest는 엄격한 관문(hard gate)이며(논쟁의 여지가 없습니다), LLM-as-judge 점수는 타임라인 상에서 소프트한 품질 신호(soft quality signal)로서 함께 따라옵니다. 역할은 다르지만 둘 다 유용합니다.
결론
Rewind는 운영 환경의 에이전트 실패를 재현하고, 하나의 결정을 포크하며, 실제 통과하는 테스트로 수정 사항을 증명합니다. 이 모든 과정에서 커스텀 트레이스 저장소(trace store)나 가짜 재생(faked replay)은 전혀 필요하지 않았습니다. SigNoz의 span에는 이미 전체 I/O와 비용 정보가 담겨 있었고, LangGraph의 체크포인터(checkpointer)에는 이미 실행 가능한 상태(executable state)가 담겨 있었습니다. 작업은 단 하나의 thread id로 이 둘을 결합하는 것이었습니다. 에이전트를 구축하고 있다면, _무슨 일이 일어났는지_를 위한 OpenTelemetry 트레이스(traces)와 _다르게 다시 실행하기_를 위한 체크포인터의 조합은 반드시 훔쳐 가야 할 가치가 있습니다.
Code: github.com/siddham-jain/rewind. SigNoz 에이전트 해커톤(Agents of SigNoz hackathon)을 위해 제작되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기