API, RAG, 모델 호출, 응답 검증을 하나의 흐름으로 보기
이 글에서는 OpenTelemetry를 사용해 LLM 서비스의 요청 흐름을 trace로 연결하는 방법을 정리합니다.
LLM 서비스의 장애 분석은 일반 API보다 어렵습니다. 한 요청 안에 검색, 모델 호출, 응답 검증, 저장, 캐시, 외부 provider 호출이 섞여 있기 때문입니다. 사용자는 “답변이 느리다”고 말하지만, 실제 원인은 vector search일 수도 있고, 모델 호출일 수도 있고, schema validation 재시도일 수도 있습니다.
그래서 trace가 필요합니다.
분석 기준일: 2026-05-12
실습 기준 환경: FastAPI, OpenTelemetry, PostgreSQL, Redis, LLM Provider API
주요 참고자료: OpenTelemetry Docs, W3C Trace Context, Google SRE
핵심 요약
- OpenTelemetry는 traces, metrics, logs를 생성·수집·내보내기 위한 관측성 프레임워크다.
- LLM 요청은 API, retrieval, LLM call, validation, DB save를 span으로 나눠야 한다.
- trace_id는 로그, metric, eval result와 연결되어야 한다.
- prompt 원문과 문서 전문은 span attribute에 넣지 않는다.
- p95/p99 latency, error rate, token usage를 trace와 함께 봐야 한다.
1. 왜 LLM 서비스에 trace가 필요한가
LLM 요청은 여러 하위 작업으로 나뉩니다.
# 예시입니다.POST /answers→ authenticate→ rate limit check→ retrieval→ prompt build→ llm call→ schema validation→ db save→ response
전체 응답 시간이 8초라고 할 때 어디서 시간이 걸렸는지 모르면 개선할 수 없습니다.
| 병목 위치 | 가능한 원인 |
|---|---|
| retrieval | vector index, metadata filter, DB 부하 |
| prompt build | context 과다, token 계산 |
| llm call | provider 지연, rate limit, output 길이 |
| validation | schema 실패, 재시도 |
| db save | connection pool, transaction 지연 |
Trace는 이 흐름을 span 단위로 나눠 보여줍니다.
2. OpenTelemetry 기본 개념
| 개념 | 설명 |
|---|---|
| Trace | 하나의 요청 전체 흐름 |
| Span | trace 안의 개별 작업 단위 |
| Attribute | span에 붙는 key-value metadata |
| Metric | 시간에 따른 수치 데이터 |
| Log | 개별 이벤트 기록 |
| Collector | telemetry 수집·가공·전송 컴포넌트 |
LLM 서비스에서는 trace와 token usage metric을 연결하는 것이 중요합니다.
3. LLM 요청 span 설계
추천 span 구조:
# 예시입니다.answer.create├── auth.check├── rate_limit.check├── retrieval.search│ └── vector.query├── prompt.build├── llm.call├── output.validate└── answer.save
각 span에는 duration과 error 여부가 기록됩니다.
4. Trace ID 전파
외부에서 들어온 traceparent header가 있으면 이어받고, 없으면 새 trace를 만듭니다.
# 예시입니다.traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
응답에도 request_id나 trace_id를 포함하면 장애 신고를 받을 때 추적이 쉬워집니다.
// 예시 JSON 구조입니다.{ "answer": "...", "trace_id": "0af7651916cd43dd8448eb211c80319c"}
5. Span Attribute 설계
좋은 attribute는 분석에 도움이 되면서 민감정보를 포함하지 않아야 합니다.
| Attribute | 예시 | 주의 |
|---|---|---|
llm.provider | openai | OK |
llm.model | configured-model | OK |
llm.prompt_version | answer.v3 | OK |
llm.input_tokens | 3200 | OK |
llm.output_tokens | 800 | OK |
rag.top_k | 5 | OK |
rag.document_scope | official_docs | OK |
user.question | 원문 질문 | 넣지 않기 |
prompt.full | 전체 prompt | 넣지 않기 |
document.content | chunk 전문 | 넣지 않기 |
6. 민감정보와 보안
보안 주의
OpenTelemetry attribute와 log에는 prompt 원문, 문서 전문, 개인정보, API key, access token을 넣지 않아야 합니다. 관측성 데이터도 외부 시스템으로 전송될 수 있으므로 보안 등급을 별도로 관리해야 합니다.
대신 hash나 길이, category를 기록합니다.
# 예시입니다.question_hashprompt_versioncontext_token_countretrieved_chunk_countdocument_scope
7. Metrics와 Logs 연결
Trace만으로는 전체 경향을 보기 어렵습니다. Metrics와 Logs도 함께 설계해야 합니다.
| Signal | 역할 |
|---|---|
| Trace | 한 요청의 상세 흐름 |
| Metric | 전체 경향과 알림 |
| Log | 이벤트와 디버깅 정보 |
예시 metric:
# 예시입니다.llm_request_duration_msllm_provider_latency_msllm_schema_validation_fail_totalrag_retrieval_latency_msrag_empty_result_totalllm_input_tokens_totalllm_output_tokens_total
로그에는 trace_id를 반드시 포함합니다.
// 예시 JSON 구조입니다.{ "level": "ERROR", "message": "LLM schema validation failed", "trace_id": "0af...", "prompt_version": "answer.v3", "error_code": "SCHEMA_VALIDATION_FAILED"}
8. Dashboard에서 볼 것
초기 dashboard는 복잡할 필요가 없습니다.
# 예시입니다.[ ] 요청 수[ ] p50/p95/p99 latency[ ] error rate[ ] provider latency[ ] schema validation failure rate[ ] retrieval latency[ ] token usage[ ] cache hit rate[ ] eval pass rate
Google SRE의 four golden signals인 latency, traffic, errors, saturation을 LLM 서비스에 맞게 확장하면 됩니다.
9. FastAPI 예제 구조
# 예시 코드입니다.from opentelemetry import trace tracer = trace.get_tracer(__name__) # 이 선언은 예시 흐름을 보여줍니다.async def create_answer(req): with tracer.start_as_current_span("answer.create") as span: span.set_attribute("llm.prompt_version", "answer.v3") with tracer.start_as_current_span("retrieval.search") as s: chunks = await retrieve(req.question) s.set_attribute("rag.top_k", len(chunks)) with tracer.start_as_current_span("llm.call") as s: result = await call_llm(req, chunks) s.set_attribute("llm.input_tokens", result.usage.input_tokens) s.set_attribute("llm.output_tokens", result.usage.output_tokens) with tracer.start_as_current_span("output.validate"): validated = validate_output(result) return validated
이 예시는 구조를 보여주기 위한 코드입니다. 실제 서비스에서는 middleware, exporter, collector 설정이 필요합니다.
10. 실무 체크리스트
# 예시입니다.[ ] 요청 전체를 하나의 trace로 볼 수 있는가?[ ] retrieval, llm call, validation이 별도 span인가?[ ] trace_id가 API 응답과 로그에 포함되는가?[ ] span attribute에 민감정보가 없는가?[ ] token usage가 metric으로 기록되는가?[ ] schema validation failure를 추적하는가?[ ] p95/p99 latency를 dashboard에서 보는가?[ ] eval result와 prompt_version을 연결할 수 있는가?
실패 사례: 로그는 많은데 병목을 찾지 못하는 경우
LLM 서비스에서 request started, retrieval done, model done 같은 로그를 많이 남겨도 장애 분석이 어려울 때가 있습니다. 각 로그가 같은 request에 속한다는 것은 알지만, 어느 단계가 p95 latency를 밀어 올리는지, retry가 몇 번 일어났는지, validation 실패가 model 호출 전인지 후인지 한눈에 보기 어렵기 때문입니다. 로그는 사건 목록이고, trace는 사건 사이의 부모-자식 관계와 시간을 보여줍니다.
특히 RAG 요청은 병목이 매번 바뀝니다. 어떤 요청은 vector search가 느리고, 어떤 요청은 provider queue가 길고, 어떤 요청은 schema validation retry 때문에 두 번째 모델 호출이 생깁니다. 하나의 trace 안에 api.request, retrieval.search, llm.call, output.validate, answer.persist span이 있으면 문제 위치를 단계별로 분리할 수 있습니다.
구현 예시: LLM 요청 span 구조
trace llm_answer_request span api.request span auth.check span retrieval.search span llm.call span output.validate span answer.persist
각 span attribute에는 원문이 아니라 운영 가능한 요약값을 넣습니다.
llm.model = "runtime-selected-model"llm.prompt_version = "rag_answer.v4"llm.input_tokens = 4200llm.output_tokens = 620retrieval.top_k = 8retrieval.index = "document_chunks_hnsw_v2"validation.schema = "answer_with_citations.v2"
prompt 전문, 문서 전문, 사용자 개인정보를 attribute에 넣지 않는 것이 중요합니다. 대신 hash, token count, version, chunk id처럼 재현과 분석에 필요한 최소값을 남깁니다.
체크리스트 적용 결과
| 확인 항목 | 좋은 신호 | 위험 신호 |
|---|---|---|
| trace 연결 | API, retrieval, LLM, validation span이 한 trace에 있음 | 단계별 로그만 흩어져 있음 |
| 민감정보 | prompt hash와 token count만 저장 | 원문 prompt와 chunk 전문 저장 |
| 비용 분석 | token usage가 trace와 metric에 연결 | 비용 리포트와 장애 trace가 분리 |
| 재시도 | retry span이 원 호출의 child로 보임 | 재시도가 새 request처럼 보임 |
이 구조가 있으면 "LLM이 느리다"는 모호한 신고를 "retrieval p95가 증가했다" 또는 "schema validation retry가 늘었다"로 바꿀 수 있습니다.
11. Q&A
Q1. 로그만 잘 남기면 trace가 없어도 되나요?
작은 서비스에서는 로그로 시작할 수 있습니다. 하지만 요청이 여러 단계로 나뉘면 trace가 훨씬 유리합니다. 특히 LLM provider 호출과 retrieval 병목을 구분하려면 span이 필요합니다.
Q2. 모든 요청을 trace하면 비용이 크지 않나요?
샘플링을 사용할 수 있습니다. 다만 오류 요청, 긴 지연 요청, eval 실패 요청은 우선적으로 보존하는 정책이 좋습니다.
Q3. prompt 내용을 trace에 넣어도 되나요?
권장하지 않습니다. prompt와 문서 chunk에는 민감정보가 포함될 수 있습니다. hash, token count, version 등으로 대체합니다.
12. 참고자료와 불확실성
참고자료
- OpenTelemetry Docs: https://opentelemetry.io/docs/
- What is OpenTelemetry: https://opentelemetry.io/docs/what-is-opentelemetry/
- W3C Trace Context: https://www.w3.org/TR/trace-context/
- Google SRE — Monitoring Distributed Systems: https://sre.google/sre-book/monitoring-distributed-systems/
불확실성
- OpenTelemetry SDK 설정은 언어와 프레임워크에 따라 달라집니다.
- 샘플링 정책과 데이터 보관 기간은 조직의 비용·보안 정책에 따라 결정해야 합니다.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.