
AI 관측성 (AI Observability): 로그, 프롬프트, 도구 호출(Tool Calls) 및 비용
요약
AI 시스템 운영 시 단순 응답 로그를 넘어 프롬프트, 도구 호출, 비용 등 다각적인 관측성(Observability) 확보가 필수적임을 강조합니다. 특히 숨겨진 추론 토큰으로 인한 비용 폭증을 방지하기 위해 네 가지 핵심 신호를 추적해야 합니다.
핵심 포인트
- 단순 응답 로그만으로는 숨겨진 추론 토큰 비용을 파악할 수 없음
- AI 관측성의 4대 핵심 요소: 로그, 프롬프트, 도구 호출, 비용
- 도구 호출 추적을 통해 에이전트의 동작 오류 원인 파악 가능
- 비용 신호 누락 시 예상치 못한 막대한 운영 비용 발생 위험
여기 다섯 줄짜리 함수가 있습니다. LLM을 호출하고, 답변을 로그로 남긴 뒤, 이를 반환합니다.
async function ask(question: string) {
const res = await openai.responses.create({ model: "o4-mini", input: question });
console.log("answer:", res.output_text);
...
이 코드는 컴파일됩니다. 테스트도 통과합니다. 배포도 됩니다. 하지만 아무도 알아차리기 전에 한 달에 수천 달러의 비용이 조용히 발생할 것입니다. 왜냐하면 해당 로그에는 모델이 단 40개의 토큰 답변을 생성하기 위해 8,000개의 숨겨진 추론 토큰 (reasoning tokens)을 소모했다는 사실이 전혀 나타나지 않기 때문입니다.
이것이 바로 이 글에서 다루고자 하는 격차입니다. AI 호출은 일반적인 HTTP 호출이 아닙니다. 흥미로운 상태는 응답 본문 (response body)이 아닙니다. 여러분이 보낸 메시지, 모델이 선택한 도구 (tools), 모델이 소비한 토큰 (가시적인 토큰 및 그 외의 토큰), 그리고 예산에서 빠져나가는 달러입니다. 만약 여러분의 관측성 (observability) 전략이 "우리는 답변을 로그로 남긴다"라면, 여러분은 계기판이 하나뿐이고 그마저도 고도계뿐인 비행기를 조종하고 있는 것과 같습니다.
실제로 무엇을 캡처해야 하는지에 대해 이야기해 봅시다.
중요한 네 가지 신호
모든 AI 시스템에는 계측 (instrumenting)할 가치가 있는 동일한 네 가지 차원이 있으며, 대부분의 팀은 그중 한두 가지만 추적합니다.
- 로그 (Logs) - 요청/응답 쌍, 에러, 지연 시간 (latency). 기존의 APM (Application Performance Monitoring)이 이미 다루고 있는 지루한 요소들입니다.
- 프롬프트 (Prompts) - 실제로 입력된 텍스트와 실제로 출력된 텍스트. 시스템 프롬프트 (system prompts), 도구 정의 (tool definitions), 그리고 히스토리를 포함합니다.
- 도구 호출 (Tool calls) - 모델이 어떤 도구를 선택했는지, 어떤 인자 (arguments)를 사용했는지, 무엇이 반환되었는지, 어떤 순서로 이루어졌는지, 그리고 어떤 재시도 (retries)가 있었는지에 대한 정보입니다.
- 비용 (Cost) - 입력 토큰 (input tokens), 출력 토큰 (output tokens), 캐시된 토큰 (cached tokens), 추론 토큰 (reasoning tokens), 모델, 그리고 각 모델의 백만 토큰당 가격. 이를 사용자별, 기능별, 요청별로 곱하여 산출합니다.
이 중 하나라도 놓친다면, 문제의 다른 축에 대해 눈을 감고 작업하는 것과 같습니다. 비용 신호 (cost signal)를 놓치면 재무팀으로부터 Slack 메시지를 받으며 깨어나게 됩니다. 도구 호출 (tool-call) 신호를 놓치면 에이전트가 왜 계속 잘못된 항공권을 예약했는지 알 수 없습니다. 프롬프트 (prompt) 신호를 놓치면 운영 환경 (prod)에서의 회귀 (regression)가 추측 게임이 되어버립니다. 일반 로그 (plain logs)를 놓치면 호출이 발생했다는 사실조차 알 수 없습니다.
좋은 소식은 2026년에 드디어 이 네 가지를 모두 캡처하기 위한 표준이 생겼다는 것입니다. 나쁜 소식은 대부분의 팀이 여전히 자체적으로 구축하고 있으며 필드의 절반을 놓치고 있다는 점입니다.
로그 (Logs): 무엇을 캡처해야 하는가, 그리고 왜 "200 OK"는 거짓말인가
지루한 계층부터 시작해 봅시다. 모든 LLM 호출은 최소한 다음을 포함하는 구조화된 로그 라인 (structured log line)을 가져야 합니다:
- 타임스탬프 (Timestamp), 요청 ID (request ID), 부모 트레이스 ID (parent trace ID).
- 제공자 (Provider) (
openai,anthropic,bedrock, 자체 게이트웨이), 모델 이름 (model name), 모델 버전 (model version, 있는 경우). - 엔드포인트 (Endpoint) 또는 작업 (operation) (
chat.completions,responses,messages). - 지연 시간 (Latency) - 실제 경과 시간 (wall-clock)과 스트리밍 시 첫 번째 토큰까지의 시간 (time-to-first-token) 모두 포함.
- HTTP 상태 코드 (HTTP status), 에러 클래스 (error class), 에러 본문 (error body).
- 종료 사유 (Finish reason) (
stop,length,tool_calls,content_filter).
마지막 항목이 함정입니다. API의 200 응답이 "모델이 질문에 답했다"는 것을 의미하지는 않습니다. finish_reason이 length라면 응답이 문장 중간에 잘렸음을 의미합니다. content_filter는 안전 시스템이 출력을 차단했음을 의미합니다. tool_calls는 모델이 작업을 수행하도록 요청하고 있으며 대화가 아직 끝나지 않았음을 의미합니다. 만약 모니터링 시스템이 모든 200 응답을 성공으로 간주한다면, 당신은 잘림(truncation)과 거부(refusal)를 성공으로 집계하고 있는 것입니다.
스트리밍 (streaming) 케이스는 별개의 문제입니다. 스트리밍된 응답은 HTTP 200을 반환하고, 문장의 절반만 내보낸 뒤, 연결 끊김과 함께 종료될 수 있습니다. "이 호출이 성공했는가"에 대한 확인은 헤더가 아니라 스트림의 끝에서 이루어져야 합니다. 바이트 수 (byte count)와 청크 수 (chunk count)도 함께 캡처하십시오. 40개의 청크 대신 3개의 청크로 도착한 부분적인 응답은 모델이 조기에 종료되었음을 알려주며, 이 경우 사용자는 유용한 정보를 전혀 얻지 못했음에도 불구하고 첫 번째 토큰까지의 지연 시간 (latency-to-first-token)은 매우 좋게 보일 것입니다.
첫 번째 토큰까지의 지연 시간 (Time-to-first-token)은 사용자가 체감하는 속도와 실제로 상관관계가 있는 지연 시간 수치입니다. 총 소요 시간 (Total duration)은 과금 및 용량 계획 (capacity planning)에 중요하지만, 첫 번째 토큰을 600ms 만에 보고 마지막 토큰을 8초 만에 보는 사용자는 앱이 빠르다고 느낍니다. 반면, 아무것도 나타나기 전까지 4초를 기다려야 하는 사용자는 총 소요 시간이 더 짧더라도 그렇게 느끼지 않습니다.
프롬프트 (Prompts): 대화 전체를 캡처한 후, 비식별화(Redact) 하세요
한 번의 운영 사고 (prod incident)를 겪고 나서야 배우게 되는 규칙이 있습니다. 프롬프트 관련 버그 — 잘못된 답변, 이상한 말투, 발생하지 말았어야 할 거부 반응 등 — 가 나타났을 때, 요약본만으로는 디버깅 (debug)할 수 없다는 점입니다. 모델이 본 정확한 텍스트가 필요합니다. 시스템 프롬프트 (System prompt), 히스토리의 모든 메시지, 모든 도구 정의 (tool definition), 그리고 주입한 모든 검색 결과 (retrieval result)까지 말이죠. 페이로드 (payload) 전체가 필요합니다.
이 지점에서 대부분의 자체 구축 로깅 (homegrown logging) 시스템이 한계를 드러냅니다. 팀들은 실제 텍스트를 저장하는 것이 과도하다고 느껴 prompt.length === 4720과 같이 로그를 남깁니다. 그러다 사용자가 테니스에 대해 물었는데 어시스턴트가 농구에 관한 답변을 했다고 불평하면, 여러분에게 남은 것은 아무것도 없습니다 — 그저 길이와 모델 이름뿐이죠. 버그의 원인은 다른 사용자의 세션에서 유출된 오래된 메모리 청크 (memory chunk)가 시스템 프롬프트에 섞여 들어간 것이었지만, 저장하지 않았기 때문에 이를 확인할 수 없습니다.
전체 페이로드를 저장하세요. 디스크는 저렴하지만, 여러분의 시간은 그렇지 않습니다. 다만 두 가지 주의 사항이 있습니다.
네트워크를 벗어나기 전에 개인정보 (PII)를 비식별화(Redact) 하세요. 프롬프트는 비정형 사용자 입력입니다. 이름, 이메일, 주소, 신용카드 번호, 내부 계정 ID 및 그보다 더 심각한 정보들을 포함하고 있습니다. 이를 제3자 관측성 (observability) 벤더로 전송한다면, 디버깅 도구를 GDPR 책임 소재로 바꿔버리는 셈입니다. OpenTelemetry GenAI 워킹 그룹은 이 문제에 실질적인 관심을 기울여 왔습니다. 스팬 (span)이 컬렉터 (collector)를 떠나기 전에 민감한 토큰을 제거하는 파이프라인 내 PII 비식별화 프로세서 (in-pipeline PII-redaction processor) 개념이 존재합니다. Datadog의 LLM Observability는 자체 Sensitive Data Scanner를 사용하여 이메일 및 IP에 대한 기본 스캐닝 규칙을 기본적으로 제공합니다. 직접 비식별화 단계를 구축하거나, 이미 이를 구현한 벤더를 선택하세요. 가공되지 않은 프롬프트를 맹목적으로 전송하지 마십시오.
시스템 프롬프트(System Prompt)에 버전을 부여하세요. 시스템 프롬프트를 변경한다면, 그것은 프로그램을 변경한 것과 같습니다. 이를 git으로 추적되는 아티팩트(Artifact)처럼 취급하여 버전을 할당하고, 모든 요청에 해당 요청을 생성한 버전을 기록(stamp)하십시오. 새로운 프롬프트를 A/B 테스트할 때 특정 변형(variant)의 성능이 저하된다면, deploy.sha로 데이터를 나누어 보는 것과 동일한 방식으로 prompt.version을 기준으로 메트릭(Metrics)을 슬라이싱(slice)하여 분석해야 합니다.
캡처된 프롬프트의 합리적인 형태는 다음과 같습니다:
{
"request_id": "req_01HXY...",
"trace_id": "abc123",
...
시스템 프롬프트를 해시(Hash) 값으로 저장하고 버전 관리된 레지스트리(Registry)에서 조회하십시오. 그렇게 하면 과거의 어떤 프롬프트로도 과거의 어떤 요청이든 재현(Replay)할 수 있으며, 2,000 토큰에 달하는 동일한 시스템 메시지를 하루에 만 번씩 중복 저장할 필요도 없습니다.
도구 호출(Tool calls): 대부분의 에이전트가 조용히 실패하는 지점
이것은 팀들이 가장 투자를 적게 하는 신호(Signal)이며, 에이전트 형태의 모든 작업에서 가장 중요한 신호입니다.
현대의 LLM 호출은 텍스트를 반환하는 것이 아니라, 하나의 '결정(Decision)'을 반환합니다. 텍스트를 반환할 수도 있고, search_inventory({"sku": "WIDGET-7"})를 호출하라는 요청을 반환할 수도 있습니다. 세 개의 도구 호출을 병렬로 반환할 수도 있습니다. 혹은 인자(Arguments)는 합리적으로 보이지만 귀사의 카탈로그에 존재하지 않는 SKU를 참조하는 도구 호출을 반환할 수도 있습니다. 여기서 발생하는 실패 모드(Failure modes)는 기이하고 다양하며, 외부에서 보기에는 모두
- 잘못된 도구 선택 (Wrong tool picked).
cancel_order를 호출해야 할 상황에서 모델이refund_order를 호출함. - 잘못된 형식의 인자 (Malformed arguments). 모델이 파싱할 수 없는 JSON을 반환하거나, 파싱은 되지만 스키마 (Schema)를 위반함.
- 환각된 인자 (Hallucinated arguments). 모델이 도구 정의 (Tool definition)에 없는 파라미터 (Parameter)를 만들어냄. 또는 실제 파라미터에 임의로 지어낸 값(예: 존재하지 않는 주문에 대해
"order_id": "ORD-12345"입력)을 채워 넣음. - 잘못된 순서 (Wrong order). 모델이
confirm_payment를 호출하기 전에ship_order를 호출함. - 호출 누락 (Missing call). 답변의 근거가 될 도구를 사용하지 않고 질문에 답변함.
- 무한 재시도 (Infinite retry). 도구가 에러를 반환하면 모델이 동일한 인자로 재시도하고, 에러가 다시 반환되는 과정이 루프 제한에 걸리거나 비용이 폭발할 때까지 반복됨.
이 모든 사례는 각각 해결 방법과 영향 범위 (Blast radius)가 다릅니다. 응답 텍스트만으로는 이들을 구분할 수 없습니다. 각 도구 호출 (Tool call)을 별도의 구조화된 이벤트 (Structured event)로 캡처해야 합니다.
도구 호출당 최소한으로 확보해야 하는 정보는 다음과 같습니다:
- 도구 이름 (Tool name), 도구 정의 버전 (Tool definition version).
- 전체 인자 객체 (Full arguments object).
- 부모 메시지 ID (Parent message ID) 및 이를 생성한 모델의 결정.
- 도구 실행 결과 (Tool execution result) - 모델에 반환한 실제 값.
- 실행 시간 (Execution time), 성공/실패 상태, 에러 메시지 (있을 경우).
- 턴 (Turn) 내에서의 순서 (이 호출이 병렬 호출 3개 중 1번째였는지, 아니면 직렬 체인 4번째 호출이었는지).
OpenTelemetry의 GenAI 시맨틱 컨벤션 (Semantic conventions)에서는 이것이 구조화되어 있습니다. 도구를 호출하려는 모델의 요청은 gen_ai.output.messages 내부에 { "type": "tool_call", "id": "call_abc", "name": "search_inventory", "arguments": {...} } 형태의 메시지로 나타납니다. 여러분이 다시 보낸 결과는 다음 턴의 gen_ai.input.messages에 `
이러한 구조를 갖추고 나면, 도구 호출 (Tool Call)이 사람 검토자에게 도달하기도 전에 모든 호출에 대해 저렴한 결정론적 체크 (deterministic checks)를 실행할 수 있습니다.
validate-tool-call.ts
function validateToolCall(call: ToolCall, schemas: Record<string, JSONSchema>) {
const schema = schemas[call.name];
if (!schema) return { ok: false, reason: "unknown_tool" };
...
대부분의 프로덕션 AI 실패는 깊은 의미론적 환각 (semantic hallucinations)이 아니라, 구문 (syntax) 및 라우팅 (routing) 문제입니다. 정규 표현식 (regex)과 JSON 스키마 검증기 (JSON-schema validator)는 비용이 발생하기 전에 이러한 문제의 상당 부분을 잡아냅니다. 해당 검증을 첫 번째 관문으로 취급하세요. 이 관문을 통과한 실패 사례들만이 사람이나 더 강력한 모델이 등급을 매길 평가 (evals) 대상이 됩니다.
그리고 재시도 (retries)에 관하여 — "실패 시 재시도"는 시스템 프롬프트 (system prompt)에 넣을 수 있는 가장 위험한 지침 중 하나입니다. 응답 시간이 초과되었다는 이유로 charge_card 호출을 재시도하는 에이전트는 고객에게 결제를 두 번 하게 만드는 에이전트입니다. 상태를 변경하는 모든 도구에는 멱등성 키 (Idempotency keys)가 필수적입니다. 도구 호출과 함께 멱등성 키를 로그에 기록하세요. 두 호출이 동일한 키를 가지고 있다면, 재시도 경로가 실행되었음을 알 수 있습니다.
비용: 아무도 예상치 못한 청구서
이 지점이 바로 기사 상단의 OpenAI 코드 스니펫이 여러분에게 타격을 주는 부분입니다. 여러분은 답변을 로그에 남겼습니다. 하지만 비용은 남기지 않았습니다. 그리고 현대적인 모델들은 최종 금액에 영향을 미치는 최소 네 개의 토큰 카운터 (token counters)를 가지고 있습니다:
- 입력 토큰 (Input tokens) - 당신이 보낸 프롬프트입니다. 모델의 입력 요율 (input rate)로 청구됩니다.
- 출력 토큰 (Output tokens) - 돌아온 텍스트입니다. 훨씬 더 높은 출력 요율 (output rate)로 청구됩니다.
- 캐시된 입력 토큰 (Cached input tokens) - 프롬프트 접두사 캐시 (prompt-prefix cache)에서 제공된 토큰입니다. 대폭 할인된 가격으로 청구됩니다.
- 추론 토큰 (Reasoning tokens) - o-시리즈와 같은 추론 모델 (reasoning models)이 사용하는 내부 "생각" 토큰입니다. 이 토큰들은 출력 비용 (output cost)에 포함되지만, 응답 텍스트에는 나타나지 않습니다. 사용자는 이를 절대 볼 수 없지만, 당신의 지갑은 이를 보게 됩니다.
이 수치들은 결코 작지 않습니다. 예를 들어 Anthropic의 프롬프트 캐싱 (prompt caching)은 캐시 읽기 (cache reads) 비용을 기본 입력 토큰 요율의 약 10% 수준으로 책정합니다. 반대로 캐시에 쓰는 비용 (writing to the cache)은 일반 입력 토큰보다 더 비쌉니다. 5분 캐시의 경우 기본 요율의 약 1.25배, 1시간 캐시의 경우 2배입니다. 따라서 캐싱은 하나의 "도박"입니다. 캐시 쓰기가 이득이 되려면 나중에 실제로 캐시 히트 (cache hits)가 발생해야만 합니다. 이 전략이 수익을 내려면 캐시 읽기가 캐시 쓰기보다 더 빨라야 합니다. 만약 cache_creation_input_tokens와 cache_read_input_tokens를 별도로 추적하지 않는다면, 절약하는 금액보다 캐싱에 더 많은 비용을 지출하면서도 이를 인지하지 못할 수 있습니다.
OpenAI의 Responses API에 있는 usage 객체는 동일한 구분을 약간 다르게 보고합니다. input_tokens, output_tokens, total_tokens와 더불어 input_tokens_details.cached_tokens 및 output_tokens_details.reasoning_tokens를 제공합니다. OpenAI의 캐시된 토큰은 일반 입력 가격의 50%로 청구되며, 이 할인은 자동으로 적용됩니다. 사용자가 직접 선택할 필요는 없습니다. 추론 토큰 (Reasoning tokens) 역시 다시 한번 언급하자면, 출력 비용 (output cost)에 포함됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기