
실시간 음성 프롬프트에서 안전한 캔버스 초안까지: SigNoZ를 통한 Sidechalk의 엔드 투 엔드 관찰
요약
Sidechalk 프로젝트에서 SigNoZ를 활용하여 AI 에이전트의 실시간 음성 프롬프트 및 캔버스 조작 과정을 엔드 투 엔드 관찰하는 방법을 다룹니다. WebRTC 사이드밴드와 모델 응답 간의 계측 간극을 메우고, 에이전트의 전체 실행 경로를 모니터링하는 기술적 도전 과제를 설명합니다.
핵심 포인트
- SigNoZ를 통한 AI 에이전트의 토큰, 지속 시간, 비용 및 폴백 이벤트 관찰
- WebRTC 사이드밴드와 모델 응답 간의 텔레메트리 간극 해결 필요성
- 초안 우선(draft first) 원칙을 통한 AI 제안의 안전한 캔버스 적용
- 에이전트의 결정론적 역연산을 통한 실행 취소(undo) 기능 구현
프로젝트: Sidechalk on GitHub
해커톤: WeMakeDevs Agents of SigNoZ
트랙: 트랙 01 — AI & 에이전트 관찰성 (Observability)
비디오:
마지막 라이브 데모에서, 저는 Sidechalk에게 경사면 도표를 검사하고, 누락된 힘에 대해 추론하며, 초안으로 노란색 마찰력 화살표를 하나 추가해 달라고 요청했습니다. 애플리케이션은 권한이 부여된 캔버스 (canvas)를 확인하였고, 처음 두 개의 답변은 대화 형식으로 유지했으며, 세 번째 지시 이후에만 가시적인 제안을 생성했습니다. 학습자는 이를 검사, 수락 또는 거부할 수 있었으며, 수락된 변경 사항이라도 여전히 실행 취소(undo)가 가능했습니다.
SigNoZ 커맨드 센터는 동일한 실행 과정을 포착했습니다. 여기에는 gpt-realtime-2.1 및 gpt-5.6-terra 모두에 대한 토큰 시리즈 (token series), 구조화된 제공자 (structured-provider) 지속 시간, 전체 에이전트 턴 (agent-turn) 지속 시간, 추정 모델 비용, 하나의 폴백 (fallback) 이벤트, 그리고 백그라운드 워커 (background-worker) 활동이 표시되었습니다. 두 번째로 프로비저닝된 대시보드는 제안 검증, 결정, 안전 차단 (safety blocks), 그리고 권한 있는 실시간 전달 (authoritative realtime delivery)을 다룹니다.
이 과정에 도달하면서 실제 사각지대를 발견했습니다. 이전 실행에서는 학습자 대상 워크플로우 (workflow)는 성공했지만, 대부분의 AI 패널은 비어 있었고 워커 텔레메트리 (worker telemetry)만 나타났습니다. 문제는 SigNoZ나 메트릭 내보내기 (metric-export) 지연이 아니었습니다. 저는 타이핑된 어시스턴트 경로를 계측 (instrumented)했지만, 실시간 WebRTC 사이드밴드 (sideband)는 다른 라이프사이클 (lifecycle)을 따르고 있었습니다.
그 간극을 메우기 위해 저는 "모델이 응답했는가?"라는 질문보다 더 어려운 질문에 답해야 했습니다: 학습자가 실제로 사용한 완전한 에이전트 경로를 관찰할 수 있는가?
Sidechalk가 안전하게 만들고자 하는 것
Sidechalk는 협업형 리빙 노트북 (living notebook)입니다. 학습자는 Excalidraw 캔버스에 그림을 그리고, 문서에서 작업하며, 미디어를 가져오고, 어시스턴트에게 말하거나 타이핑할 수 있습니다. 어시스턴트는 권한이 부여된 Sidechalk 페이지만을 검사할 수 있으며 화살표, 손글씨, 원, 움직임, 주석, 인용, 교체 또는 복구 가능한 삭제를 제안할 수 있습니다.
이 시스템의 핵심 불변량(invariant)은 **초안 우선(draft first)**입니다. 모델의 출력은 표준 Yjs 문서를 직접 수정할 수 없습니다. AI의 표시(marks)는 학습자가 명시적으로 수락할 때까지 시각적으로 구분되어 유지되며, 수락된 작업은 실행 취소(undo)를 위한 결정론적 역연산(deterministic inverse)을 가집니다.
데모를 위해 저는 미완성된 자유물체도(free-body diagram)를 사용합니다. 경사면 위의 블록에 mg, N, 그리고 경사면 아래 방향의 속도 화살표는 있지만 마찰력은 없는 상태입니다. 대화는 의도적으로 세 단계로 나뉩니다:
- “보드에서 무엇이 보이나요? 아직 아무것도 변경하지 마세요.”
- “속도 화살표는 블록이 경사면을 따라 내려가고 있음을 보여줍니다. 마찰력은 어느 방향으로 작용해야 하나요? 아직 그리지 마세요.”
- “경사면 위쪽으로 노란색 화살표 하나를 추가하고 그 옆에
friction이라고 쓰세요. 다른 것은 바꾸지 말고 초안 상태로 유지하세요.”
처음 두 단계는 표면 작업(surface operation)에 대한 권한을 부여하지 않은 상태에서 새로운 시각적 맥락, 대화 기억, 그리고 물리적 추론 능력을 테스트합니다. 오직 세 번째 단계만이 초안 작성을 승인합니다.
저는 또 다른 실제적인 한계에 부딪힌 후 이 시나리오에 도달했습니다. 가져온 PNG 파일은 하나의 평면화된 캔버스 요소(flattened canvas element)입니다. 에이전트에게 해당 이미지에 포함된 화살표 하나를 삭제하라고 요청하는 것은 Excalidraw 벡터 요소를 삭제하는 것과 같지 않습니다. 저는 이러한 불일치를 숨기는 대신, 인터페이스 계약(surface contract)이 정직하게 수행할 수 있는 유용한 가산적 수정(additive correction) 작업으로 과제를 변경했습니다.
하나의 편집 단계는 하나 이상의 모델 시스템을 가로지릅니다
이 시스템은 웹상의 Next.js 및 Excalidraw, 권한 있는 API를 위한 Fastify, 인증 및 영구 상태를 위한 Supabase, 협업을 위한 Yjs/Hocuspocus, 분산 작업을 위한 Redis/BullMQ, 그리고 제약된 Python STEM 검증기(verifier)로 구성된 TypeScript 모노레포(monorepo)입니다. OpenAI가 실시간 음성 및 멀티모달 제안을 처리하며, Google 및 Anthropic을 위한 프로바이더 어댑터(provider adapters)도 존재합니다.
Next.js / Excalidraw
|
v
...
Node API, realtime, 그리고 worker 서비스들은 트레이스(traces), 메트릭(metrics), 그리고 선택된 로그(logs)를 내보냅니다. Python verifier는 트레이스를 내보내며, Next.js 서비스는 @vercel/otel을 사용합니다. 자동 계측(Automatic instrumentation)은 HTTP, fetch, 그리고 지원되는 라이브러리의 활동을 캡처했지만, 제가 중요하게 생각했던 제품 경계(product boundaries)를 명시하지는 못했습니다. 그래서 저는 각 신뢰 경계(trust boundary)에 수동 스팬(manual spans)을 추가했습니다.
| 경계 | 스팬 (Spans) | 답변하는 내용 |
|---|---|---|
| Learner turn | sidechalk.ai.turn | 입력된 텍스트 또는 음성/제안 턴(turn)이 완료되었는가? |
| ... |
이러한 수동 계측은 프레임워크 계측만으로는 설명할 수 없는 비즈니스 운영 트레이싱을 위한 SigNoZ 가이드를 따릅니다. 또한 각 실패에 대해 provider, validator, persistence, human decision, 또는 realtime publication과 같은 유용한 경계를 부여합니다.
사각지대: 텍스트 입력 턴과 실시간 음성은 서로 다른 시스템이었다
기존의 텍스트 기반 /turns 라우트는 이미 전체 워크플로우를 감싸고 있었으며 커스텀 메트릭을 기록하고 있었습니다. 이 짧은 발췌본은 관련 없는 검증 및 스트리밍 코드는 생략하면서 실제 스팬 및 메트릭 이름을 유지합니다:
return withSpan("sidechalk.ai.turn", {
"sidechalk.channel": "typed",
"sidechalk.ai.mode": mode,
...
실시간 음성은 하나의 HTTP 요청 안에 머물지 않습니다. 브라우저는 WebRTC를 통해 오디오를 전송하고, 서버 측 사이드밴드(sideband)는 provider 이벤트를 수신하며, 브라우저는 현재 권한이 있는 페이지를 캡처합니다. 또한 초안(draft)이 렌더링되고 확인되는 동안 도구 호출(tool calls)이 일시 중지될 수도 있습니다. 기존 구현은 이러한 이벤트들을 올바르게 저장했지만, recordAiTurn()을 호출하지 않았고 그에 상응하는 root/provider 스팬을 생성하지도 않았습니다.
수정 전 대시보드 스크린샷은 이 격차를 명확하게 보여주었습니다:

이것은 임시적인 접두사(pre-fix) 상태였습니다. 워커 텔레메트리(Worker telemetry)를 통해 SigNoZ에 도달할 수 있음이 증명되었고, 문제는 실시간 음성 경로(live-voice path)의 공백으로 좁혀졌습니다.
계측(instrumentation)되지 않은 경로를 기다린다고 해서 해결될 문제는 아니었습니다.
비동기 실시간 트레이스(Realtime trace) 수정하기
해결책은 response.create를 트리거하는 엔드포인트(endpoint) 주변에 타이머를 추가하는 것이 아니었습니다. 해당 요청은 비동기 프로바이더 라이프사이클(asynchronous provider lifecycle)이 끝나기 전에 종료되기 때문입니다. 저는 사이드밴드 이벤트(sideband events) 전반에 걸쳐 턴 스팬(turn span)을 유지하고, 각 실시간(Realtime) 응답에 대해 자식 프로바이더 스팬(child provider span)을 생성해야 했습니다. 다음 발췌문은 축약되었으나, 라이프사이클과 속성(attributes)은 실제 실행 중인 구현에서 가져온 것입니다:
if (!session.activeTurn) {
session.activeTurn = {
startedAtMs: performance.now(),
...
response.done이 도착하면, Sidechalk는 수치적인 사용량 메타데이터(usage metadata)만 추출하고, 프로바이더 스팬을 종료하며, 도구 호출(tool call)을 통해 계속 진행하거나 학습자 턴(learner turn)을 종료합니다:
const calls = completedFunctionCalls(event);
const usage = realtimeResponseUsage(event);
...
표면 제안(surface-proposal) 도구는 해당 음성 턴(voice turn) 아래에 중첩되어 있습니다. 이제 이 구조화된 모델 호출은 프로바이더/모델 속성(provider/model attributes), 토큰 사용량(token usage), 체크인된 가격표에 모델이 존재할 경우의 예상 비용(estimated cost), 제안 검증(proposal validation) 및 영속성(persistence)을 기록합니다. 취소된 음성, 프로바이더 오류, 소켓 실패(socket failure)는 미완성된 트레이스(traces)가 유출되는 대신 스팬을 명시적으로 종료합니다.
이것은 설치 가이드에서는 얻을 수 없었던 교훈이었습니다: 엔드포인트를 계측하지 말고, 그것을 시작하는 라이프사이클을 계측하십시오.
다음번의 새로운 음성 제안은 이전에 누락되었던 신호들을 생성해냈습니다:


이것은 빈 대시보드가 아닙니다. 동일한 1시간의 시간 범위 내에 fallback (폴백), cost (비용), worker (워커), provider-duration (제공자 소요 시간), token (토큰), 그리고 complete agent-turn (완전한 에이전트 턴) 신호들이 포함되어 있습니다. 별도의 metric-histogram (메트릭 히스토그램) P95 패널은 이 캡처 화면에서 여전히 No Data라고 표시되지만, trace (트레이스)에서 파생된 provider (제공자) 및 agent-turn (에이전트 턴) P95 패널에는 데이터가 채워져 있습니다. 더 깔끔하지만 오해를 불러일으킬 수 있는 스크린샷을 제시하기보다는, 이 상태를 그대로 보여주기로 했습니다.
신뢰를 중심으로 구축된 메트릭 및 대시보드
Node SDK는 OTLP/HTTP를 통해 traces (트레이스), metrics (메트릭), 그리고 선택적으로 상관관계가 있는 logs (로그)를 내보냅니다. Metrics (메트릭)는 15초 주기의 reader (리더)를 사용합니다. 제가 기록하는 항목은 다음과 같습니다:
- AI turn (AI 턴) 볼륨 및 end-to-end latency (엔드 투 엔드 지연 시간)
- provider invocation (제공자 호출) 소요 시간 및 fallback (폴백)
- input/output (입력/출력) tokens (토큰) 및 추정 모델 비용
- proposal (제안) 작업, validation (검증), decision (결정) 및 undo (실행 취소) 결과
- verifier (검증기) 및 citation (인용) 결과
- queue (큐) 대기 및 worker (워커) 활동
- canonical-safety (표준 안전성) invariant (불변량) 위반
두 개의 멱등하게 프로비저닝된 (idempotently provisioned) 대시보드는 각각 8개의 패널을 포함합니다:
- Sidechalk AI Command Center는 volume (볼륨), P95 latency (P95 지연 시간), provider duration (제공자 소요 시간), fallback (폴백), tokens (토큰), estimated cost (추정 비용), worker activity (워커 활동), 그리고 complete agent-turn duration (완전한 에이전트 턴 소요 시간)을 다룹니다.
- Sidechalk Trust and Proposal Safety는 proposal operations/outcomes (제안 작업/결과), verifier (검증기) 및 citation (인용) 활동, validation (검증), invariant blocks (불변량 차단), authoritative realtime delivery (권위 있는 실시간 전달), 그리고 errors (오류)를 다룹니다.
활성화된 4개의 규칙이 provider reliability (제공자 신뢰성), learner-facing latency (학습자 대상 지연 시간), model budget (모델 예산), 그리고 canonical safety (표준 안전성)를 감시합니다. 테스트된 로컬 webhook (웹훅)이 alert envelopes (경고 엔벨로프)를 수락합니다. 저는 이것이 프로덕션 환경에서 작동하고 복구되는 canary (카나리)라고 주장하지 않습니다. 이 글은 제가 검증한 로컬 스택을 설명합니다.
“No Data”는 세 가지 다른 의미를 갖습니다
이 프로젝트는 표면적으로 유사해 보이는 세 가지의 빈 상태(empty states)를 생성했습니다:
- Expected absence (예상된 부재): 폴백 (fallback)이나 불변성 위반 (invariant violation)이 발생하지 않았습니다. 비어 있는 상태는 정상적인 증거입니다.
- Missing instrumentation (계측 누락): 수정 전 실행(pre-fix run) 시, 워커 (worker) 데이터는 도착했지만 음성 AI 경로는 비어 있는 상태로 유지되었습니다. 애플리케이션 경로는 사이드밴드 라이프사이클 (sideband lifecycle)을 계측 (instrument)하기 전까지는 보이지 않았습니다.
- Broken query infrastructure (쿼리 인프라 오류): 제가 생성한 ClickHouse 설정이
histogramQuantile을 올바르게 로드하지 못해 P95 쿼리가 실패했습니다.
세 번째 케이스는 이례적으로 구체적이었습니다. 생성된 실행 가능한 함수 (executable-function) 설정이 잘못된 파일 이름 패턴을 참조했고, UDF의 quantile 파라미터가 잘못된 타입을 사용했습니다. 저는 로컬에서 생성된 파일들을 수정하고, ClickHouse만 재시작한 뒤 함수를 직접 확인했습니다:
docker exec signoz-telemetrystore-clickhouse-0-0 \
clickhouse-client --query \
"select name from system.functions where name = 'histogramQuantile'"
이러한 구분은 중요합니다. 패널을 보기 좋게 만들기 위해 폴백 (fallback) 이벤트를 인위적으로 만들어내어서는 안 되지만, 계측 (instrumentation)되지 않은 음성 경로를 정상이라고 판단해서도 안 됩니다.
로컬 경로 재현하기
저는 Windows 11, Docker Desktop, WSL2, Node 24, 그리고 Foundry v0.2.14를 사용했습니다. Foundry는 체크인된 casting.yaml로부터 셀프 호스팅 (self-hosted) SigNoZ 스택을 생성했습니다:
foundryctl gauge -f casting.yaml
foundryctl cast -f casting.yaml
표준 컨테이너 서비스는 SigNoZ UI 포트 8080과 MCP 포트 8000을 사용합니다. 제 컴퓨터에서는 해당 포트들이 이미 사용 중이었기 때문에, 현재 호스트 매핑은 UI의 경우 18080, MCP의 경우 18000입니다. OTLP/HTTP는 4318로 유지되며, Sidechalk 검증기 (verifier)는 8002를 사용합니다. 제 리매핑 (remap)을 맹목적으로 복사하지 말고, 본인의 포트 점유 상태를 확인하세요.
OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
OTEL_ENVIRONMENT=development
...
리포지토리 루트에서:
pnpm ports:preflight
docker compose up -d redis verifier
pnpm dev
...
verifier는 예상되는 대시보드 이름, Query Builder 버전, 패널 ID 및 개수, 활성화된 알림 라우팅 (alert routing), 웹훅 채널 (webhook-channel) 식별자, 그리고 세 가지 대표적인 텔레메트리 (telemetry) 쿼리를 확인합니다. sidechalk.ai.turn.duration.bucket이 마지막으로 확인된 시점이 4시간 전일 때 의도적으로 실패하도록 설정되었습니다. 이 실패는 유용했습니다. 프로비저닝 (provisioning)은 존재했지만, 새로운 트래픽이 해당 경로를 증명하지 못했기 때문입니다.
새로운 에이전트 턴 (agent turn) 이후, 이 필터는 다음과 같이 워크플로 (workflow)를 찾아냅니다:
service.name = 'sidechalk-api' AND name = 'sidechalk.ai.turn'
쓰기 권한 없는 장애 조사 (Incident investigation)
Sidechalk에는 공식 SigNoZ MCP server를 기반으로 하는 Incident Copilot이 포함되어 있습니다. MCP server는 메트릭 (metrics), 트레이스 (traces), 로그 (logs), 알림 (alerts), 대시보드 (dashboards) 및 서비스 (services)를 노출할 수 있습니다. Sidechalk는 의도적으로 읽기 작업만을 허용 목록 (allowlist)에 포함합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기