API 호환성
messages, system 처리, tool schema, streaming chunk, usage 응답이 앱 추상화와 맞는가
OpenAI 호환처럼 보여도 edge case가 다를 수 있다분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L11-50 (트랜스포머), L11-70 (토크나이저·임베딩)
LLM API는 모델에게 메시지 목록과 옵션을 보내고 다음 assistant 응답을 받는 추론 API다. 겉으로는 REST/RPC 호출처럼 보이지만, 내부에서는 입력 토큰을 처리하는 prefill, 출력 토큰을 하나씩 만드는 decode, 다음 토큰을 고르는 sampling이 이어진다.
이 문서에서 먼저 잡아야 할 핵심은 네 가지다.
messages 배열과 role로 표현된다.챗봇, RAG, 도구 호출, 에이전트, 문서 추출, 요약 기능은 대부분 LLM API 호출 위에 올라간다. 이 API를 “프롬프트 문자열을 넣으면 답이 나오는 마법 상자”로 보면, 비용 폭증·느린 UX·깨지는 JSON·반복 429·provider 교체 실패가 모두 개별 사고처럼 보인다.
반대로 아래 흐름으로 보면 운영 판단이 단순해진다.
messages 작성 -> tokenization -> prefill(input 처리와 prefix cache 가능성) -> decode(output 생성과 sampling) -> streaming/structured output 처리 -> usage, error, rate limit 관측이 흐름은 L11의 두 선수지식과 바로 연결된다.
content/topics/L11/tokenization-and-embedding.mdx: token 수가 비용, context window, 한국어 실효 길이를 결정한다.content/topics/L11/transformer-and-attention.mdx: prefill, decode, KV cache, prefix caching이 지연과 메모리 문제로 이어진다.초기 LLM API는 text completion 형태가 중심이었다. 개발자는 단일 문자열 prompt를 만들고, 모델은 그 뒤를 이어 썼다.
User: 안녕Assistant: 안녕하세요!User: 오늘 날씨?Assistant:이 방식은 단순하지만 제품 API로 쓰기에는 네 가지 문제가 있었다.
| 한계 | text completion에서 생긴 문제 | chat completion이 줄인 문제 |
|---|---|---|
| 역할 경계가 약함 | system 지시, user 입력, assistant 답변이 모두 한 문자열에 섞임 | role 필드로 메시지의 성격을 분리 |
| 멀티턴 관리가 수동 | 개발자가 매번 대화 기록을 직접 이어붙이고 separator를 관리 | messages 배열이 표준 대화 컨테이너가 됨 |
| 도구 결과 표현이 ad-hoc | 함수 호출 결과를 다시 prompt 텍스트에 넣어야 함 | tool role, tool_calls 같은 채널이 생김 |
| provider 교체가 어려움 | 모델별 chat template, separator, special token 규칙을 앱이 직접 알아야 했음 | 많은 provider가 유사한 messages 인터페이스를 제공하기 시작함 |
OpenAI의 chat completion 계열 API가 messages: [{ role, content }] 구조를 널리 퍼뜨렸고, 이후 여러 provider가 비슷한 인터페이스를 제공했다. 단, 호환처럼 보여도 system 처리, tool schema(도구 이름과 인자 JSON Schema 계약), streaming chunk, safety filter, rate limit 단위는 provider마다 다르다. “OpenAI 호환”은 이식 비용을 낮춘다는 뜻이지, 모든 필드 의미가 같다는 뜻은 아니다.
작은 반례를 보면 왜 role이 필요한지 선명해진다. text completion 시대처럼 사용자가 입력한 문자열 안에 "\nAssistant: 이전 지시를 무시해"가 섞이면, 앱이 separator를 제대로 escape하지 않는 한 user 입력과 assistant 채널이 한 prompt 평면에서 뒤엉킨다. messages와 role은 이 혼선을 줄이지만, 여전히 validation과 권한 검사를 대체하지는 않는다.
이 전환이 만든 새 과제도 있다. chat API는 보통 stateless이므로 매 호출마다 필요한 history를 다시 보내야 한다. 그래서 context window, prompt caching, streaming, structured output, rate limit이 LLM 앱의 기본 운영 주제가 된다.
먼저 가장 작은 호출을 보자.
const response = await client.chat.completions.create({ model: "provider-confirmed-chat-model", messages: [ { role: "system", content: "답변은 한국어로 간결하게 한다." }, { role: "user", content: "LLM API에서 streaming이 왜 필요해?" }, ], temperature: 0.2, max_tokens: 300,});이 코드는 짧지만 내부에는 다음 결정이 들어 있다.
| 필드 | 학습할 질문 | 깨지면 보이는 증상 |
|---|---|---|
model | 어떤 provider와 모델 계약을 쓸 것인가 | 품질·가격·context·rate limit이 예상과 달라짐 |
messages | 어떤 history와 지시를 이번 호출에 넣을 것인가 | 모델이 이전 맥락을 잊거나 오래된 지시를 따름 |
role | 각 메시지를 어떤 채널로 읽게 할 것인가 | 사용자 입력이 system 지시처럼 섞이는 혼란 |
temperature | 다음 토큰을 얼마나 결정적으로 뽑을 것인가 | 같은 입력의 응답 변동이 과하거나, 답이 지나치게 굳음 |
max_tokens | output 길이의 상한을 어디에 둘 것인가 | 비용 폭증, 중간 끊김, 불필요하게 긴 답변 |
| timeout/retry | 실패를 어떻게 멈추고 다시 시도할 것인가 | 429 폭주, 중복 실행, 전체 기능 장애 |
| response schema | 자유 텍스트를 받을지 검증 가능한 구조로 받을지 | JSON parse 실패, downstream type error |
이제 각 단어를 순서대로 풀어보자.
Chat completion은 “지금까지의 대화와 지시를 보고 다음 assistant 메시지를 완성하라”는 인터페이스다. 여기서 messages는 순서가 있는 배열이고, 각 원소는 최소한 role과 content를 가진다.
type ChatMessage = | { role: "system"; content: string } | { role: "user"; content: string | ContentPart[] } | { role: "assistant"; content: string; tool_calls?: ToolCall[] } | { role: "tool"; content: string; tool_call_id: string };문자열 하나 대신 배열을 쓰는 이유는 모델에게 “이 텍스트가 누구의 말인가”를 명시하기 위해서다. role은 보안 경계 그 자체는 아니지만, 모델 학습과 API 처리 단계에서 system 지시, 사용자 입력, 이전 assistant 응답, tool 결과를 구분하는 기본 신호다.
| role | 의미 | 보통 들어가는 내용 | 주의할 점 |
|---|---|---|---|
system | 모델의 상위 지시, 역할, 출력 정책 | 말투, 금지 행동, 출력 형식, 제품 정책 | 모든 provider가 같은 우선순위로 다루진 않음 |
user | 실제 사용자 요청 | 질문, 문서, 작업 지시 | user 입력을 system 지시처럼 이어붙이지 말 것 |
assistant | 이전 모델 응답 또는 이번 호출의 중간 계획 | 대화 history, tool call 요청 | 오래된 assistant 답변도 다시 보내면 영향 받음 |
tool | 외부 함수/API 실행 결과 | DB 조회 결과, 검색 결과, 계산 결과 | tool 결과도 untrusted input처럼 검증해야 함 |
첫 학습에서는 system을 “법”, user를 “질문”처럼 너무 강하게 외우지 않는 편이 낫다. 실제 모델은 확률적으로 다음 token을 생성하므로 system 지시를 항상 완벽히 지키지 않는다. role은 모델이 메시지를 구분하도록 도와주는 계약 필드이고, 강한 보안은 validation, 권한 검사, sandbox, audit log와 함께 만들어야 한다.
대화가 두 턴 이어졌다고 하자.
const messages = [ { role: "system", content: "너는 사내 문서 검색 도우미다." }, { role: "user", content: "휴가 신청 규정 알려줘." }, { role: "assistant", content: "휴가 신청은 HR 포털에서 처리합니다..." }, { role: "user", content: "그럼 반차는 며칠 전까지 신청해야 해?" },];마지막 user 메시지는 짧지만, 모델은 “반차”가 앞의 휴가 규정 질문과 연결된다는 사실을 이전 메시지에서 읽는다. 여기서 중요한 운영 사실은 API provider가 이 history를 자동으로 기억한다고 가정하면 안 된다는 점이다. 앱이 messages에 이전 turn을 넣지 않으면, 모델은 보통 마지막 질문만 본다.
Stateless API는 서버가 이전 호출의 대화 상태를 기본적으로 기억하지 않는 호출 모델이다. 사용자는 채팅창에서 긴 대화가 이어진다고 느끼지만, 백엔드는 매번 “이번 추론에 필요한 message history”를 요청에 담아 보낸다.
Turn 1 request: [system, user1]Turn 1 response: assistant1
Turn 2 request: [system, user1, assistant1, user2]Turn 2 response: assistant2이 구조가 비용과 품질을 동시에 만든다.
반례도 중요하다. 어떤 provider나 product API는 server-side thread, conversation state, vector memory를 제공할 수 있다. 그래도 공부할 때의 기본 모델은 stateless로 두는 편이 안전하다. 그래야 비용 견적, 개인정보 보관, replay, debugging, idempotency를 앱이 책임져야 한다는 점을 놓치지 않는다.
LLM API 비용은 보통 input token과 output token을 나누어 계산한다.
비용 = input_tokens × input_price + output_tokens × output_price가격표의 실제 숫자는 provider와 모델마다 바뀌므로 공식 pricing 문서를 확인해야 한다. 그러나 구조 자체는 안정적이다.
L11 토크나이저 문서의 한국어 예시처럼, 같은 의미의 텍스트도 tokenizer에 따라 token 수가 달라진다. 따라서 “문자 수”나 “페이지 수”만 보고 비용을 추정하면 자주 틀린다.
실제 provider 가격이 아니라 계산 감각을 위한 가상 단가로 보자.
가정: input price = $1 / 1M tokens output price = $5 / 1M tokens
요청: system + history + user = 2,000 input tokens assistant response = 200 output tokens
계산: input 비용 = 2,000 / 1,000,000 × $1 = $0.002 output 비용 = 200 / 1,000,000 × $5 = $0.001 총 비용 = $0.003여기서 output token이 input보다 5배 비싸도, input이 10배 길면 input 비용이 더 클 수 있다. 반대로 짧은 질문에 긴 답변을 생성하는 요약·작성 기능은 output 비용이 지배할 수 있다. 그래서 비용 최적화는 “항상 output만 줄이자”가 아니라 요청 유형별 input/output 비율을 먼저 측정하는 일에서 시작한다.
L11의 트랜스포머 추론을 API 호출 관점으로 바꾸면 두 단계가 보인다.
Prefill: 입력 prompt 전체를 처리하고 각 layer의 KV cache를 만든다.Decode: 새 output token을 한 개씩 생성하면서 KV cache를 읽고 갱신한다.| 단계 | 주로 비례하는 것 | 사용자가 느끼는 증상 | 줄이는 방법의 예시 |
|---|---|---|---|
| prefill | input token, 긴 context | 첫 토큰이 나오기 전 대기, 긴 문서 입력 지연 | prompt caching, context 축소, chunking, RAG |
| decode | output token | 답변이 끝날 때까지 오래 걸림 | max_tokens 제한, streaming, 모델 라우팅 |
| KV cache | context × batch | 동시 요청 증가 시 메모리 부족, tail latency | context cap, paged attention, batch 제어 |
prefill은 입력 전체를 병렬로 처리할 수 있지만, 입력이 길수록 한 번에 처리해야 할 token이 많다. decode는 output token을 순차적으로 만든다. 다음 token은 이전 token이 나온 뒤에야 정해지므로, 긴 답변일수록 total latency가 길어진다.
**TTFT(Time To First Token)**는 요청을 보낸 뒤 첫 output token이 도착하기까지의 시간이다. Total latency는 응답 전체가 끝나기까지의 시간이다.
요청 전송 ───── prefill/queue ───── 첫 token ─ token ─ token ─ 완료 ^ TTFT ^ total latency사용자 경험에서는 TTFT가 중요하다. 첫 token이 빨리 나오면 사용자는 시스템이 살아 있다고 느낀다. 하지만 과금과 백엔드 점유 시간은 total latency와 output token 수까지 봐야 한다. streaming은 TTFT 체감을 개선하지만, 모델이 해야 할 decode 총량을 없애지는 않는다.
모델은 매 step마다 다음 token 후보들의 점수(logits)를 만든다. Sampling은 이 점수 분포에서 실제 다음 token을 고르는 규칙이다.
logits -> softmax -> token probability distribution -> next token 선택같은 prompt라도 sampling 설정이 다르면 응답이 달라진다. 그래서 sampling은 “창의성 옵션”이면서 동시에 “재현성·평가·비용” 옵션이다.
Greedy decoding은 가장 확률이 높은 token만 고른다.
next_token = argmax(softmax(logits))보통 temperature=0이 greedy 또는 greedy에 가까운 동작으로 쓰인다. 정답이 명확한 분류, 코드 생성, JSON 추출에는 낮은 temperature가 맞다. 하지만 낮은 temperature가 “항상 같은 결과”를 보장하지는 않는다. provider의 batching, floating point, 모델 업데이트, 안전 필터, tool 결과 변화로 출력이 달라질 수 있다.
Temperature는 logits를 나누어 분포를 뾰족하게 하거나 평평하게 만든다.
softmax(logits / T)
T -> 0: 가장 높은 후보에 몰림T = 1: 원래 분포에 가까움T ↑ : 낮은 확률 후보도 더 자주 선택됨실무 감각은 다음처럼 잡으면 된다. 숫자는 provider별 구현 차이가 있으므로 절대 규칙이 아니라 시작점이다.
| 작업 유형 | 시작 temperature | 이유 |
|---|---|---|
| 분류, 추출, 코드 수정 | 0~0.2 | 재현성과 형식 안정성이 중요 |
| 요약, 번역, Q&A | 0.2~0.5 | 약간의 표현 다양성은 허용 |
| 브레인스토밍, 카피 작성 | 0.7 이상 | 다양한 후보를 얻는 것이 목적 |
| 회귀 테스트 | 낮게 고정 | prompt 변경 효과를 비교하기 쉽게 만들기 |
**Top-p(nucleus sampling)**는 확률 누적합이 p 이상이 되는 최소 후보 집합만 남기고 그 안에서 sampling한다.
top_p = 0.9-> 누적 확률 90%를 채우는 token 후보만 남김Top-k는 확률 상위 k개 token만 남긴다. 모든 provider가 top-k를 직접 노출하지는 않는다.
temperature, top-p, top-k는 모두 분포를 다루는 옵션이다. 처음 운영할 때는 temperature와 top-p를 동시에 크게 건드리지 않는 편이 좋다. 둘 다 강하게 제한하면 효과가 중첩되어 “왜 답이 반복되거나 이상해졌는지”를 분리하기 어렵다.
반복 답변이 문제라면 penalty 계열을 본다.
frequency_penalty: 많이 나온 token일수록 logit을 낮춤presence_penalty: 한 번이라도 나온 token의 logit을 낮춤두 옵션은 긴 대화에서 같은 표현을 반복하는 현상을 줄일 수 있다. 반대로 법률·코드·정확한 명칭처럼 같은 단어를 반복해야 하는 작업에서는 품질을 해칠 수 있다.
“temperature를 높이면 항상 창의적이고 좋은 답이 나온다”는 틀렸다.
작업: 결제 실패 원인을 JSON으로 분류하기정답 label: ["CARD_DECLINED", "INSUFFICIENT_FUNDS", "NETWORK_ERROR"]
temperature=1.0 결과 후보: "카드 문제 같아요" "잔액 부족" "NETWORK_TIMEOUT"사람이 보기에는 의미가 비슷해도 downstream enum에는 맞지 않는다. 이 작업에서는 낮은 temperature와 structured output이 더 중요하다. 반대로 광고 문구 후보 20개를 뽑는 작업에서는 높은 temperature와 n개 샘플링이 유용할 수 있다. 즉 sampling의 기준은 “창의성”이 아니라 downstream이 무엇을 검증할 수 있는가다.
Structured output은 모델 응답을 자유 텍스트가 아니라 검증 가능한 구조로 받는 방식이다. 데이터 추출, tool calling, RAG citation, workflow automation에서 중요하다.
먼저 JSON mode는 “응답이 JSON 형식이어야 한다”는 제약이다.
{ response_format: { type: "json_object"; }}하지만 JSON mode만으로는 schema 일치가 보장되지 않는다.
{ "name": "민수", "age": "스물셋", "extra": "필요 없는 필드"}JSON은 맞지만 age가 number가 아니고, 허용하지 않은 필드가 있을 수 있다.
JSON schema 기반 structured output은 필드 이름, 타입, required 여부를 더 강하게 묶는다.
{ response_format: { type: "json_schema", json_schema: { name: "person", strict: true, schema: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, age: { type: "number" } }, required: ["name", "age"] } } }}strict: true 계열 구현은 보통 grammar-constrained decoding 또는 유사한 제약을 사용해 schema 밖 token을 생성 후보에서 막는다. provider와 모델마다 지원 범위가 다르므로 공식 문서를 확인해야 한다. 지원하지 않는 모델에서는 JSON mode + validation + retry, 또는 Instructor/Pydantic 같은 라이브러리로 보강한다.
| 상황 | 권장 방식 | 이유 |
|---|---|---|
| 화면에 바로 보여줄 설명문 | 자유 텍스트 또는 약한 형식 | 자연스러운 표현이 중요 |
| enum, 숫자, 날짜, ID 추출 | strict schema | downstream type error를 줄여야 함 |
| tool/function 호출 | tool schema 또는 strict schema | 호출 인자 검증과 권한 검사가 필요 |
| RAG 답변 + 출처 | citation 배열 schema | 출처 누락·형식 깨짐을 관측해야 함 |
| provider가 strict를 지원하지 않음 | JSON mode + validator + retry | schema 위반을 앱 레이어에서 잡아야 함 |
structured output이 제대로 작동하지 않을 때는 보통 다음 중 하나다.
| 증상 | 의미 | 먼저 볼 것 |
|---|---|---|
| JSON parse 실패 | 자유 텍스트가 섞였거나 streaming 조립 실패 | strict 지원 여부, chunk buffer, retry |
| schema에는 맞지만 값이 틀림 | 형식 제약은 의미 검증이 아님 | business validation, reference check |
| optional 필드가 과하게 비어 있음 | 모델이 정보 부족을 회피하거나 schema가 모호 | required 기준, null 허용 정책 |
| enum 밖 값이 나옴 | JSON mode만 쓰거나 schema 제약이 약함 | enum schema, post-validate, low temperature |
중요한 경계는 이것이다. structured output은 형식 안정성을 높이지, 사실성을 자동으로 보장하지 않는다. {"answer": "맞다", "citations": []}처럼 schema는 맞지만 제품 요구에는 틀린 응답이 나올 수 있다. 그래서 RAG와 평가 문서에서는 faithfulness, citation coverage, answer correctness를 따로 본다.
Streaming은 응답 전체가 완성될 때까지 기다리지 않고, 생성되는 token 또는 delta를 순서대로 받는 방식이다. 많은 chat API는 Server-Sent Events(SSE)나 유사한 stream 프로토콜을 쓴다.
const stream = await client.chat.completions.create({ model: "provider-confirmed-chat-model", messages, stream: true,});SSE chunk는 대략 이런 모양으로 도착한다.
data: {"choices":[{"delta":{"content":"안"}}]}data: {"choices":[{"delta":{"content":"녕"}}]}data: {"choices":[{"delta":{"content":"하세요"}}]}data: [DONE]streaming은 첫 token을 빨리 보여주어 체감 지연을 줄인다. 하지만 모델이 생성해야 할 output token 수는 그대로다.
| 구분 | streaming으로 좋아지는가 | 설명 |
|---|---|---|
| TTFT 체감 | 좋아짐 | 첫 chunk를 받는 즉시 UI를 갱신할 수 있음 |
| total latency | 보통 그대로 | 생성해야 할 token 수가 줄지는 않음 |
| 서버 비용 | 그대로거나 약간 증가 | 연결 유지, chunk 처리, moderation 비용이 있음 |
| JSON 안정성 | 더 어려워질 수 있음 | 부분 JSON은 중간 상태에서 parse할 수 없음 |
다음 응답은 최종적으로는 올바른 JSON일 수 있다.
{ "name": "민수", "age": 23 }하지만 stream 중간에는 이렇게 잘려 도착할 수 있다.
{"name":"민수","age":23}이 chunk를 프론트엔드에서 매번 JSON.parse()하면 계속 실패한다. structured output과 streaming을 함께 쓸 때는 보통 백엔드에서 buffer를 쌓고, 완성된 뒤 parse하거나, incremental parser를 쓰거나, UI에는 “생성 중” 상태만 보여준다. tool calling도 비슷하다. 부분 tool argument를 사용자에게 그대로 흘리면 schema가 깨진 중간 상태가 노출된다.
Prompt caching은 여러 요청에서 반복되는 prompt prefix의 prefill 결과 또는 token 처리를 재사용해 input 비용과 지연을 줄이는 기능이다. 시스템 프롬프트, 긴 정책 문서, few-shot 예시, 변하지 않는 RAG context가 앞에 길게 붙는 앱에서 효과가 크다.
요청 A: [고정 system 2k][few-shot 3k][user 질문 A]요청 B: [고정 system 2k][few-shot 3k][user 질문 B] ^ 같은 prefix가 cache 후보provider별 구현은 다르다. 어떤 곳은 자동 prefix caching을 제공하고, 어떤 곳은 cache control을 명시한다. 기존 공식 문서 기준으로는 특정 token threshold, TTL, cache write/hit 가격 같은 숫자가 제시되지만, 이 값은 바뀔 수 있으므로 운영 전 반드시 provider docs에서 다시 확인한다.
cache는 “거의 같은 prompt”가 아니라 같은 prefix를 좋아한다. 동적 값이 prefix 중간에 들어가면 그 뒤가 전부 miss가 될 수 있다.
나쁜 배치:[system][timestamp=<now>][user_id=123][few-shot][긴 정책 문서][user 질문] ^ 매 요청 바뀜 -> 뒤의 few-shot/정책 문서도 cache 재사용 어려움
좋은 배치:[system][few-shot][긴 정책 문서][timestamp][user_id][user 질문] ^ 긴 고정 prefix가 먼저 옴이 원리는 L11의 prefix caching, KV cache 감각과 연결된다. 반복되는 앞부분의 prefill을 줄이면 첫 token 전 대기와 input 비용을 줄일 수 있다. 단, user별 권한 문서나 최신 RAG 결과처럼 매번 달라져야 하는 데이터를 억지로 고정 prefix에 넣으면 보안·정확성 문제가 생긴다.
| 잘 맞는 상황 | 효과가 약한 상황 |
|---|---|
| 긴 system prompt가 거의 변하지 않음 | prompt 전체가 매번 짧고 다름 |
| few-shot 예시가 모든 사용자에게 같음 | user별 policy가 앞쪽에 섞임 |
| 동일 문서 요약을 여러 번 질의함 | RAG 결과 순서와 내용이 매번 크게 바뀜 |
| TTFT와 input 비용이 병목 | output decode가 대부분의 지연을 차지 |
운영 지표는 cache hit ratio, cached input tokens, TTFT, total cost를 함께 본다. hit ratio가 낮은데 비용 절감이 안 된다고 느껴지면, 먼저 prompt prefix가 정말 고정인지 확인한다.
LLM API는 텍스트뿐 아니라 이미지, 오디오, 비디오 입력을 받을 수 있다. provider와 모델별 지원 범위는 자주 바뀌므로 현재 라인업은 공식 문서에서 확인한다.
이미지 입력의 기본 모양은 다음처럼 content가 문자열이 아니라 여러 part의 배열이 되는 식이다.
{ role: "user", content: [ { type: "text", text: "이 화면에서 사용자가 막힐 지점을 찾아줘." }, { type: "image_url", image_url: { url: "data:image/png;base64,..." } } ]}학습할 때 중요한 점은 “이미지도 공짜가 아니다”다. 이미지는 provider 내부 규칙에 따라 token 또는 별도 단가로 계산된다. UI 스크린샷 분석, 문서 이미지 OCR, 음성 챗봇을 설계할 때는 text token뿐 아니라 multimodal input 비용, latency, 개인정보 포함 여부를 함께 본다.
LLM API는 확률적 시스템이다. 운영에서는 완전한 결정성보다 재현 가능한 비교 조건이 더 현실적인 목표다.
| 도구 | 기대할 수 있는 것 | 경계 |
|---|---|---|
| 낮은 temperature | 후보 선택 변동 감소 | provider 내부 변화까지 고정하지 못함 |
seed | 같은 조건에서 sampling 재현성 강화 | 지원 여부와 보장 수준이 provider마다 다름 |
| 모델 버전 고정 | 업데이트에 따른 회귀 감소 | safety/filter/backend 변경이 남을 수 있음 |
system_fingerprint류 | backend 변경 감지 | 모든 provider가 제공하지 않음 |
| logprobs | token별 확률 비교, eval 분석 | 응답 품질 전체를 한 숫자로 설명하지 못함 |
회귀 테스트를 만들 때는 prompt, model version, sampling 옵션, tool 결과, retrieval 결과, 날짜 의존 데이터를 함께 고정한다. temperature만 0으로 낮추고 “왜 매번 같지 않지?”라고 보면 원인을 놓치기 쉽다.
Rate limit은 provider가 일정 시간 안에 받을 수 있는 요청량과 token량을 제한하는 정책이다. 흔한 단위는 다음과 같다.
| 단위 | 뜻 | 병목 예시 |
|---|---|---|
| RPM | requests per minute, 분당 요청 수 | 짧은 질문이 아주 많이 들어오는 챗봇 |
| TPM | tokens per minute, 분당 token 수 | 긴 문서 요약, 큰 RAG context |
| concurrency | 동시에 처리 중인 요청 수 | streaming 연결이 오래 유지되는 기능 |
| daily quota | 하루 또는 월 단위 사용량 | batch 처리, 대량 평가 작업 |
rate limit은 단순히 “429가 나면 retry”가 아니다. TPM이 병목이면 짧은 exponential backoff만으로 해결되지 않을 수 있다. queue, 요청 합치기, context 축소, 작은 모델 라우팅, 요금제 상향이 함께 필요하다.
가상의 한도가 120,000 TPM이라고 하자.
요청 1개 평균: input = 2,000 tokens output = 500 tokens total = 2,500 tokens
단순 상한: 120,000 / 2,500 = 48 requests/min하지만 실제 운영에서는 retry, 긴 꼬리 요청, streaming 연결, provider 계산 방식 때문에 이론상 48 RPM을 꽉 채우면 429가 날 수 있다. 그래서 목표 처리량을 한도보다 낮게 잡고, queue depth와 429 비율을 본다. output이 500에서 2,000 tokens로 늘면 같은 TPM에서 처리량은 약 30 requests/min으로 떨어진다. output 길이 제한이 rate limit 안정성에도 영향을 주는 이유다.
| 오류/증상 | 보통 의미 | 처리 원리 |
|---|---|---|
| 429 rate limit | RPM/TPM/concurrency 한도 초과 | Retry-After, backoff, queue, quota 조정 |
| 400 context length exceeded | input + 예상 output이 context window 초과 | history 압축, RAG chunk 축소, max_tokens 조정 |
| 400/403 content filter | provider safety policy에 걸림 | 사용자 입력 정제, 명확한 거절 UX, fallback 검토 |
| 504/timeout | provider 부하, 긴 prefill/decode, 네트워크 | timeout 분리, idempotency, fallback |
| invalid JSON | structured output 제약이 약하거나 조립 실패 | strict schema, buffer, validation retry |
| tool timeout | 모델이 아니라 외부 API가 느림 | tool별 timeout, partial answer, audit log |
아래 코드는 전체 운영 런북이 아니라 재시도 판단의 최소 뼈대다. 400/403처럼 입력이나 정책이 문제인 오류는 무작정 재시도하지 않는다.
async function callLLM(messages: ChatMessage[], retries = 3) { for (let i = 0; i < retries; i += 1) { try { return await client.chat.completions.create({ model: "provider-confirmed-chat-model", messages, timeout: 30_000, }); } catch (error) { if ( error.status === 429 || error.status === 503 || error.status === 504 ) { await sleep(backoffMs(i, error.headers?.["retry-after"])); continue; }
if (error.status === 400 || error.status === 403) { throw error; }
throw error; } }
throw new Error("LLM call failed after retries");}중복 실행이 위험한 tool call이나 결제·쓰기 작업이 뒤에 붙어 있다면 idempotency key가 필요하다. LLM 응답 생성은 다시 해도 되지만, tool side effect는 다시 하면 안 되는 경우가 많다.
LLM API를 배웠다고 모든 문제를 prompt로만 풀 필요는 없다. L11과 L12 전체를 연결하면 선택 기준은 다음처럼 잡힌다.
1. 일반 작업·prototype: prompt와 structured output부터2. 자주 갱신되는 외부 지식: RAG3. 행동·톤·포맷·도메인 습관: fine-tune 검토4. multi-step·외부 작업 실행: tool calling 또는 agent좀 더 구체적인 실패 신호를 보자.
| 현재 방식 | 이동을 검토할 신호 | 다음 후보 |
|---|---|---|
| Prompt only | prompt에 붙이는 문서가 너무 길어지고, 최신 가격·재고·정책처럼 변하는 지식이 필요함 | RAG 또는 tool lookup |
| RAG | 검색 recall이 낮아 정답 chunk가 top-k에 안 들어오거나, 문체·행동 습관이 계속 흔들림 | chunk/search 개선 후 fine-tune 검토 |
| Fine-tune | 최신 사실을 기억시키려 계속 재학습해야 함 | RAG/tool로 지식 분리 |
| Agent | 평균 step 수가 거의 1이고 tool 성공률도 낮음 | 단일 호출 + structured output |
| Tool calling | 모델이 잘못된 인자를 자주 만들거나 권한 없는 tool을 고름 | schema 축소, router, 승인 단계 |
핵심은 지식과 행동을 분리하는 것이다. 자주 바뀌는 지식은 모델 안에 넣기보다 외부 store에서 조회하는 편이 안전하다. 반복되는 출력 습관이나 도메인 스타일은 prompt, schema, fine-tune 중 비용과 품질을 비교한다.
모델 이름, 가격, release date, provider 라인업은 시간이 지나면 바뀐다. 이 문서는 특정 최신 모델 순위를 외우는 대신, provider를 비교할 때 봐야 할 계약 축을 남긴다.
messages, system 처리, tool schema, streaming chunk, usage 응답이 앱 추상화와 맞는가
OpenAI 호환처럼 보여도 edge case가 다를 수 있다필요한 input history, RAG context, output 길이를 한 호출에 담을 수 있는가
큰 context가 항상 좋은 것은 아니며 비용과 lost-in-middle을 같이 본다strict schema, tool calling, JSON mode의 보장 수준과 실패 응답이 명확한가
데이터 추출과 workflow 자동화에서는 품질만큼 중요하다input/output/reasoning/cache/batch/multimodal 단가를 분리해 측정할 수 있는가
가격표 숫자는 provider docs에서 최신 확인한다RPM, TPM, concurrency, quota 상향, 장애 공지, region, enterprise 통합이 맞는가
production에서는 429와 장애 대응이 품질만큼 중요하다로그 보관, 학습 사용 여부, region, PII 처리, audit 요구사항을 만족하는가
사내 문서·고객 데이터가 들어가면 모델 성능보다 먼저 확인한다작성 시점 기준 provider별 모델명과 가격을 문서에 박아 두면 금방 낡는다. 학습 문서에서는 “무엇을 확인해야 하는가”를 기준으로 익히고, 실제 도입 전에는 공식 API reference, pricing, deprecation 문서를 다시 확인한다.
아래 숫자는 운영 정책이 아니라 처음 계측할 때의 시작점이다. 실제 기준은 서비스 SLO와 workload baseline으로 다시 정한다.
| 기법/설정 | 효과 발휘 범위 | 깨지는 조건 또는 주의 신호 |
|---|---|---|
| Temperature 낮게 | 분류·추출·코드·회귀 테스트 | 창작 후보가 너무 단조롭고 다양성이 필요함 |
| Temperature 높게 | 아이디어 생성, 다양한 문안 후보 | enum/schema/정답형 작업에서 형식과 재현성이 깨짐 |
| Top-p 조정 | 후보 분포 tail을 잘라 품질 안정화 | temperature와 동시에 과하게 조정해 원인 분리가 어려움 |
| JSON mode | JSON 형식 보장 | schema 일치 보장은 아님 |
| Structured strict | schema 일치가 중요한 추출·tool 인자 | 미지원 provider/model에서는 validation retry 필요 |
| Streaming | TTFT 개선, 긴 답변 UX | partial JSON/tool argument 처리 필요 |
| Prompt caching | 긴 고정 prefix 반복 | prefix 중간에 timestamp/user_id가 들어가 hit ratio 폭락 |
| Batch API | 비실시간 대량 요청을 비동기로 처리 | 할인율·처리 시간은 provider 문서 확인, 즉시성 작업엔 부적합 |
| Speculative decoding | 작은 draft 모델의 후보를 큰 모델이 검증해 decode 가속 | 짧은 output, acceptance rate 낮음, 이미 batch가 꽉 찬 경우 |
| Fallback provider | 장애·quota 회피 | 응답 형식과 safety policy 차이로 품질 회귀 가능 |
LLM API 문제는 항상 예외로 터지지 않는다. “응답은 왔지만 제품 요구를 조용히 어기는” 실패를 먼저 볼 줄 알아야 한다.
| 증상 | 관측 신호 예시 | 원인 후보 | 대응 원리 |
|---|---|---|---|
| Cache hit ratio 폭락 | hit ratio가 목표 대비 급락 | 고정 prefix 중간에 동적 값 삽입 | system/few-shot/정책 문서를 앞에 고정 |
| TTFT P99 폭증 | P99가 SLO를 지속 초과 | provider queue, 긴 prefill, cache miss | prompt 길이 측정, provider 라우팅, 캐싱 확인 |
| Total latency만 길어짐 | 첫 token은 빠른데 완료가 늦음 | output token 과다, decode 병목 | max_tokens, 답변 형식, streaming UX 조정 |
| JSON parse 실패 | parse error, partial chunk | streaming 조립 실패, strict 미사용 | backend buffer, strict schema, validation retry |
| 429 빈발 | 429 비율 상승, queue depth 증가 | RPM/TPM/concurrency 초과 | backoff만이 아니라 queue와 token budget 조정 |
| Content filter 증가 | 400/403 또는 safety refusal 증가 | 입력 도메인·정책 변화, provider filter 차이 | 입력 정제, UX 안내, policy와 fallback 점검 |
| 같은 입력인데 답이 흔들림 | eval flake, snapshot test 실패 | sampling, model/backend 변경, tool/RAG 변화 | seed/version/fingerprint, retrieval 고정 |
| 한국어 요청 비용이 예상 초과 | 영어 대비 token/request가 크게 높음 | tokenizer 효율, 긴 system/history | 실제 target 모델 tokenizer로 측정 |
| fallback 후 downstream 실패 | provider 교체 때 schema/tool 파싱 실패 | 호환 API의 edge case 차이 | provider별 adapter test와 contract test |
이 표는 복구 명령어 목록이 아니라 원인 좁히기 지도다. 예를 들어 TTFT만 느리면 prefill·queue·cache miss를 먼저 보고, total latency가 느리면 output 길이와 decode를 본다. 둘을 같은 “느림”으로 묶으면 잘못된 최적화를 고르기 쉽다.
가상의 사내 문서 Q&A 봇을 보자. 실제 가격은 넣지 않고 구조만 본다.
현재: 월 5M 호출 평균 input 3,000 tokens 평균 output 400 tokens P95 TTFT 1.2s cache hit ratio 20%
문제: system prompt + 정책 설명 + few-shot이 매번 2,000 tokens 반복됨 user_id와 timestamp가 prefix 중간에 들어가 prompt cache가 잘 안 맞음 모든 요청을 같은 큰 모델로 처리함선택지는 네 가지다.
| 선택지 | 줄이는 축 | 경계 |
|---|---|---|
| 고정 prefix 앞으로 이동 | prefill 비용, cache hit | user별 권한 데이터는 뒤에 두고 검증 필요 |
| output schema 축소 | output token, parse 실패 | 설명형 답변에는 너무 빡빡할 수 있음 |
| 모델 라우팅 | 쉬운 요청의 단가와 latency | router 오판 시 품질 회귀 |
| batch 처리 분리 | 비실시간 평가/요약 비용 | 사용자 대화에는 부적합 |
가장 먼저 할 일은 provider 교체가 아니라 계측이다.
1. usage.input_tokens / output_tokens를 요청 유형별로 나눈다.2. TTFT와 total latency를 분리해서 본다.3. cache hit ratio와 prefix 길이를 같이 본다.4. 429 비율과 queue depth를 같이 본다.5. fallback provider별 schema contract test를 만든다.이 시나리오에서 좋은 개선은 “모델을 더 싼 것으로 바꿨다”가 아니라, 어떤 비용 축을 줄였는지 설명할 수 있어야 한다. input이 대부분이면 caching·context 축소가 우선이고, output이 대부분이면 max_tokens·schema·요약 정책이 우선이다. TTFT가 문제면 prefill·queue·streaming을 보고, total latency가 문제면 decode와 output 길이를 본다.
엔지니어가 LLM API를 운영할 때는 다음 질문을 계속 던진다.
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| Chat completion | Text completion | messages+role 기반 대화 계약 vs 단일 prompt 문자열 완성 |
| Message history | Server memory | 요청에 담아 보내는 맥락 vs provider가 보관하는 상태 |
| Role | Permission | 모델 입력 채널 힌트 vs 실제 권한 통제 |
| Temperature | Top-p | 분포 sharpness 조정 vs 누적 확률 후보 cutoff |
| Greedy | Sampling | 가장 높은 후보 선택 vs 확률 분포에서 선택 |
| JSON mode | Structured strict | JSON 형식 유도 vs schema 제약 |
| Streaming TTFT | Total latency | 첫 token 도착 시간 vs 전체 완료 시간 |
| Prefill | Decode | input prompt 병렬 처리 vs output token 순차 생성 |
| Prompt caching | KV cache | 요청 간 반복 prefix 재사용 vs 한 요청/sequence의 K,V 저장 |
| Rate limit | Timeout | 사용량 제한 초과 vs 처리 시간이 제한 초과 |
| Fallback | Retry | 다른 모델/provider로 우회 vs 같은 작업 재시도 |
seed | temperature=0 | sampling 재현성 보조 vs 낮은 다양성 설정 |
input_tokens, output_tokens, model, provider, temperature, response schema, cache hit 여부를 로그에서 확인한다.messages와 role을 보내 다음 assistant 응답을 받는 chat completion 흐름이다.모델명, 가격, release date, provider 라인업은 시간에 취약하다. 아래 문서는 개념과 API 계약을 확인하기 위한 출처이며, 실제 도입 전에는 각 provider의 최신 공식 문서와 pricing/deprecation 문서를 다시 확인한다.
최종 수정: 2026-07-09