운영 환경의 실패를 재현하기 위해 사용하는 아주 작은 LLM 요청 기록기
요약
LLM 운영 환경에서 발생하는 재현하기 어려운 실패 사례를 추적하기 위한 경량화된 요청 기록기(recorder) 구현 방법을 소개합니다. API 키와 프롬프트 보안을 유지하면서도 모델, 파라미터, 원시 응답 등 디버깅에 필수적인 정보를 캡처하는 Node.js 기반의 fetch 래퍼를 다룹니다.
핵심 포인트
- LLM 실패 재현을 위해 모델, 파라미터, 원시 응답 등 상세 데이터 캡처 필요
- 보안을 위해 Authorization 헤더와 프롬프트 내용은 비식별 처리 권장
- Node.js 환경에서 외부 의존성 없이 fetch를 래핑하여 구현 가능
- 응답 본문을 두 번 소비하지 않도록 처리하는 것이 구현의 핵심
대부분의 LLM (Large Language Model) 실패 사례는 설명하기는 쉽지만 재현하기는 놀라울 정도로 어렵습니다.
사용자가 모델이 빈 답변을 반환했다고 보고합니다. 스트림 (stream) 중간에 도구 호출 (tool call)이 사라집니다. 한 제공업체 (provider)가 다른 곳에서는 모두 작동하던 요청을 거부합니다.
그러고 나서 로그를 열어보면 다음과 같은 내용을 발견하게 됩니다:
LLM request failed: 400 Bad Request
기술적으로는 사실입니다. 하지만 운영 측면에서는 쓸모가 없습니다.
부족한 부분은 대개 정확한 요청 형태 (request shape)입니다: 모델 (model), 파라미터 (parameters), 메시지 역할 (message roles), 도구 정의 (tool definitions), 타임아웃 동작 (timeout behavior), 그리고 원시 제공업체 응답 (raw provider response)입니다.
저는 완전한 관측성 플랫폼 (observability platform)보다는 작은 무언가를 원했기에, fetch를 기반으로 한 요청 기록기를 만들었습니다. 이 기록기는 API 키를 로그에 남기지 않으면서도, 실패한 호출을 조사하거나 재현할 수 있을 만큼 충분한 정보를 저장합니다.
기록기가 캡처하는 것
각 요청에 대해 저는 다음을 원합니다:
- 고유한 요청 ID (request ID)
- 타임스탬프 (timestamp) 및 소요 시간 (duration)
- URL 및 모델 (model)
- 정제된 요청 본문 (sanitized request body)
- HTTP 상태 (HTTP status)
- 원시 응답 본문 (raw response body)
- 네트워크 또는 타임아웃 오류 (network or timeout errors)
저는 의도적으로 Authorization 헤더는 기록하지 않습니다.
프롬프트 (Prompt) 내용 또한 기본적으로 비식별 처리됩니다. 전체 페이로드 (payload) 캡처는 명시적으로 활성화해야 합니다. 왜냐하면 운영 환경의 프롬프트를 저장하는 것은 조사 중인 버그보다 훨씬 더 심각한 문제를 일으킬 수 있기 때문입니다.
기록기
이 예제는 Node.js 18 이상에서 실행되며 외부 의존성이 없습니다.
recorded-fetch.mjs를 생성합니다:
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
...
이 래퍼 (wrapper)는 응답 본문을 한 번 읽고 이를 rawResponse로 반환합니다. 이는 매우 중요한데, 일반적인 fetch 응답 본문은 두 번 소비할 수 없기 때문입니다.
LLM 요청에 사용하기
example.mjs를 생성합니다:
import { createRecordedFetch } from "./recorded-fetch.mjs";
const recordedFetch = createRecordedFetch({
...
다음 명령어로 실행합니다:
LLM_API_KEY="your-api-key" node example.mjs
이제 모든 호출은 .llm-recordings 폴더 아래에 JSON 파일을 생성합니다.
실패한 요청은 다음과 같이 보일 수 있습니다:
{
"id": "9e50d5a9-4a0d-42a2-94c9-711d36b2057d",
"startedAt": "2026-07-17T08:14:32.442Z",
...
이것은 이미 일반적인 400 에러보다 훨씬 유용합니다.
이 정보는 실패의 원인이 프롬프트(prompt), 네트워크(network), 또는 모델 출력(model output)이 아니라, 아마도 제공자(provider) 간의 호환성 차이로 인해 발생했을 가능성이 높다는 것을 알려줍니다.
요청 재현하기 (Replaying a request)
로컬(local) 또는 스테이징(staging) 환경을 위해 다음과 같이 임시로 설정할 수 있습니다:
const recordedFetch = createRecordedFetch({
captureContent: true,
});
이렇게 생성된 요청 본문(request body)은 작은 스크립트로 재현(replay)할 수 있습니다.
replay.mjs 파일을 생성합니다:
import { readFile } from "node:fs/promises";
const recordingPath = process.argv[2];
...
그 다음 실행합니다:
LLM_API_KEY="your-api-key" \
node replay.mjs .llm-recordings/example.json
저는 합성 데이터(synthetic data)나 승인된 테스트 데이터의 경우에만 전체 콘텐츠 기록(full-content recording)을 활성화합니다. 운영(production) 환경에서 전역적으로 이를 켜지는 않을 것입니다.
이 도구가 구분하는 데 도움을 준 것들
이 기록기(recorder)는 외부에서 보기에 여러 실패가 동일해 보일 때 가장 유용합니다.
다음 사항들을 분리하는 데 도움을 줍니다:
- 요청 형태 실패 (Request-shape failures)
지원되지 않는 파라미터(parameters), 유효하지 않은 도구 스키마(tool schemas), 잘못된 메시지 역할(message roles), 또는 모델별 제한 사항.
- 전송 실패 (Transport failures)
타임아웃(timeouts), 연결 재설정(connection resets), 중단된 스트림(interrupted streams), 그리고 애플리케이션에 도달하지 않는 응답들.
- 제공자 실패 (Provider failures)
구조화된 에러 응답(structured error response), 예상치 못한 콘텐츠 타입(content type), 또는 OpenAI 호환 필드(OpenAI-compatible field)에 대한 서로 다른 해석.
- 애플리케이션 실패 (Application failures)
제공자가 유효한 응답을 반환했지만, 저의 파서(parser), 상태 머신(state machine), 또는 도구 실행기(tool executor)가 이를 잘못 처리한 경우.
이러한 카테고리들은 매우 다른 해결책으로 이어집니다. 이 네 가지 모두를 단순히 재시도(retrying)하는 것은 디버깅 전략이 아닙니다.
운영 환경 고려 사항 (Production considerations)
이 기록기는 의도적으로 작게 설계되었으므로, 이를 사용하는 애플리케이션에 여러 결정 사항을 남겨둡니다.
운영 환경에서 이와 같은 기능을 실행하기 전에, 저는 다음과 같은 사항들을 추가할 것입니다:
- 녹화 데이터의 보관 기간 (retention period)
- 모든 성공적인 요청을 기록하는 대신 샘플링 (sampling) 적용
- 저장 시 암호화 (encryption at rest)
- 요청 및 응답 본문 (request and response bodies)의 크기 제한
- 사용자 및 도구 데이터에 대한 더 엄격한 비식별화 (redaction)
- 녹화 디렉토리에 대한 접근 제어 (access controls)
- 애플리케이션 트레이스 (trace) 또는 작업 ID (operation ID)와의 상관관계 (correlation) 연결
또한, 저는 성공적인 호출보다 실패한 호출을 더 공격적으로 기록할 것입니다. 모든 응답을 영구적으로 보관하는 것은 개인정보 보호 문제를 야기하는 비용이 많이 드는 방식입니다.
이 래퍼 (wrapper)를 작게 유지하는 이유
더 풍부한 트레이스 (traces), 토큰 사용량 (token usage), 지연 시간 분포 (latency distributions), 그리고 모델 수준의 메트릭 (metrics)을 수집할 수 있는 훌륭한 관측성 (observability) 제품들이 있습니다.
그럼에도 저는 HTTP 경계 (boundary) 근처에 아주 작은 기록기를 두는 것을 선호합니다.
이는 데이터가 SDK 추상화 (abstractions), 응답 파서 (response parsers), 재시도 정책 (retry policies), 또는 에이전트 프레임워크 (agent frameworks)를 통과하기 전에 제가 검사할 수 있는, 특정 제공자(provider)에 종속되지 않은 결과물 (artifact)을 제공하기 때문입니다.
TokenBay에서 진행 중인 작업을 포함하여, 여러 OpenAI 호환 엔드포인트 (endpoints)를 테스트할 때, 그 가공되지 않은 경계 (raw boundary)는 호환성 문제를 가장 쉽게 확인할 수 있는 지점이 되는 경우가 많습니다.
목표는 모든 것을 로그로 남기는 것이 아닙니다.
목표는 다음번 운영 환경의 실패를, 단 하나의 에러 메시지만으로 디버깅할 필요가 없을 만큼 충분히 재현 가능하게 만드는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기