콘텐츠로 이동

LLM API 기초

분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L11-50 (트랜스포머), L11-70 (토크나이저·임베딩)

LLM API 기초 — Messages, Sampling, Structured Output, Streaming

섹션 제목: “LLM API 기초 — Messages, Sampling, Structured Output, Streaming”

LLM API는 모델에게 메시지 목록과 옵션을 보내고 다음 assistant 응답을 받는 추론 API다. 겉으로는 REST/RPC 호출처럼 보이지만, 내부에서는 입력 토큰을 처리하는 prefill, 출력 토큰을 하나씩 만드는 decode, 다음 토큰을 고르는 sampling이 이어진다.

이 문서에서 먼저 잡아야 할 핵심은 네 가지다.

  1. 사용자는 대화가 이어진다고 느끼지만, API는 보통 stateless다.
  2. 대화는 문자열 하나가 아니라 messages 배열과 role로 표현된다.
  3. 비용과 지연은 input/output token, prefill/decode, streaming, prompt caching에 의해 갈린다.
  4. 제품 안정성은 자유 텍스트를 그대로 믿지 않고 structured output, retry, rate limit, fallback으로 경계를 세울 때 올라간다.

챗봇, 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이 지연과 메모리 문제로 이어진다.

2.5 선행 기술의 한계 — Text Completion에서 Chat Completion으로

섹션 제목: “2.5 선행 기술의 한계 — Text Completion에서 Chat Completion으로”

초기 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 평면에서 뒤엉킨다. messagesrole은 이 혼선을 줄이지만, 여전히 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_tokensoutput 길이의 상한을 어디에 둘 것인가비용 폭증, 중간 끊김, 불필요하게 긴 답변
timeout/retry실패를 어떻게 멈추고 다시 시도할 것인가429 폭주, 중복 실행, 전체 기능 장애
response schema자유 텍스트를 받을지 검증 가능한 구조로 받을지JSON parse 실패, downstream type error

이제 각 단어를 순서대로 풀어보자.

4. Messages와 Role — 대화를 API 계약으로 바꾸기

섹션 제목: “4. Messages와 Role — 대화를 API 계약으로 바꾸기”

Chat completion은 “지금까지의 대화와 지시를 보고 다음 assistant 메시지를 완성하라”는 인터페이스다. 여기서 messages는 순서가 있는 배열이고, 각 원소는 최소한 rolecontent를 가진다.

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

이 구조가 비용과 품질을 동시에 만든다.

  • history가 길어질수록 input token이 늘고 prefill 비용이 커진다.
  • 너무 오래된 history를 잘라내면 모델이 맥락을 잃을 수 있다.
  • history 요약을 잘못하면 사용자의 의도나 제약이 바뀔 수 있다.
  • 반복 prefix가 크면 prompt caching으로 일부 비용과 지연을 줄일 수 있다.

반례도 중요하다. 어떤 provider나 product API는 server-side thread, conversation state, vector memory를 제공할 수 있다. 그래도 공부할 때의 기본 모델은 stateless로 두는 편이 안전하다. 그래야 비용 견적, 개인정보 보관, replay, debugging, idempotency를 앱이 책임져야 한다는 점을 놓치지 않는다.

5. Token, Context, Prefill/Decode — 비용과 지연의 뼈대

섹션 제목: “5. Token, Context, Prefill/Decode — 비용과 지연의 뼈대”

LLM API 비용은 보통 input token과 output token을 나누어 계산한다.

비용 = input_tokens × input_price + output_tokens × output_price

가격표의 실제 숫자는 provider와 모델마다 바뀌므로 공식 pricing 문서를 확인해야 한다. 그러나 구조 자체는 안정적이다.

  • input token: system, user, assistant history, tool 결과, RAG context처럼 모델이 읽는 모든 token.
  • output token: 모델이 새로 생성한 assistant 응답 token. reasoning 계열 모델에서는 보이지 않는 reasoning token이 별도 과금 또는 제한 단위로 잡힐 수 있다.
  • context window: input과 앞으로 만들 output을 포함해 모델이 한 호출에서 다룰 수 있는 최대 token 창.

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를 읽고 갱신한다.
단계주로 비례하는 것사용자가 느끼는 증상줄이는 방법의 예시
prefillinput token, 긴 context첫 토큰이 나오기 전 대기, 긴 문서 입력 지연prompt caching, context 축소, chunking, RAG
decodeoutput token답변이 끝날 때까지 오래 걸림max_tokens 제한, streaming, 모델 라우팅
KV cachecontext × batch동시 요청 증가 시 메모리 부족, tail latencycontext 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 총량을 없애지는 않는다.

6. Sampling — 다음 token을 어떻게 고를 것인가

섹션 제목: “6. Sampling — 다음 token을 어떻게 고를 것인가”

모델은 매 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&A0.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이 무엇을 검증할 수 있는가다.

7. Structured Output — 자유 텍스트를 계약으로 묶기

섹션 제목: “7. Structured Output — 자유 텍스트를 계약으로 묶기”

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 schemadownstream type error를 줄여야 함
tool/function 호출tool schema 또는 strict schema호출 인자 검증과 권한 검사가 필요
RAG 답변 + 출처citation 배열 schema출처 누락·형식 깨짐을 관측해야 함
provider가 strict를 지원하지 않음JSON mode + validator + retryschema 위반을 앱 레이어에서 잡아야 함

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를 따로 본다.

8. Streaming — 기다림을 쪼개서 보여주기

섹션 제목: “8. Streaming — 기다림을 쪼개서 보여주기”

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]

8.2 streaming이 줄이는 것과 줄이지 못하는 것

섹션 제목: “8.2 streaming이 줄이는 것과 줄이지 못하는 것”

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가 깨진 중간 상태가 노출된다.

9. Prompt Caching — 같은 prefix를 다시 계산하지 않기

섹션 제목: “9. Prompt Caching — 같은 prefix를 다시 계산하지 않기”

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가 정말 고정인지 확인한다.

10. Multimodal I/O — 텍스트 밖의 입력도 token 예산에 들어간다

섹션 제목: “10. Multimodal I/O — 텍스트 밖의 입력도 token 예산에 들어간다”

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_fingerprintbackend 변경 감지모든 provider가 제공하지 않음
logprobstoken별 확률 비교, eval 분석응답 품질 전체를 한 숫자로 설명하지 못함

회귀 테스트를 만들 때는 prompt, model version, sampling 옵션, tool 결과, retrieval 결과, 날짜 의존 데이터를 함께 고정한다. temperature만 0으로 낮추고 “왜 매번 같지 않지?”라고 보면 원인을 놓치기 쉽다.

Rate limit은 provider가 일정 시간 안에 받을 수 있는 요청량과 token량을 제한하는 정책이다. 흔한 단위는 다음과 같다.

단위병목 예시
RPMrequests per minute, 분당 요청 수짧은 질문이 아주 많이 들어오는 챗봇
TPMtokens 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 limitRPM/TPM/concurrency 한도 초과Retry-After, backoff, queue, quota 조정
400 context length exceededinput + 예상 output이 context window 초과history 압축, RAG chunk 축소, max_tokens 조정
400/403 content filterprovider safety policy에 걸림사용자 입력 정제, 명확한 거절 UX, fallback 검토
504/timeoutprovider 부하, 긴 prefill/decode, 네트워크timeout 분리, idempotency, fallback
invalid JSONstructured 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는 다시 하면 안 되는 경우가 많다.

13. Fine-tune, RAG, Prompt, Agent 선택 기준

섹션 제목: “13. Fine-tune, RAG, Prompt, Agent 선택 기준”

LLM API를 배웠다고 모든 문제를 prompt로만 풀 필요는 없다. L11과 L12 전체를 연결하면 선택 기준은 다음처럼 잡힌다.

1. 일반 작업·prototype: prompt와 structured output부터
2. 자주 갱신되는 외부 지식: RAG
3. 행동·톤·포맷·도메인 습관: fine-tune 검토
4. multi-step·외부 작업 실행: tool calling 또는 agent

좀 더 구체적인 실패 신호를 보자.

현재 방식이동을 검토할 신호다음 후보
Prompt onlyprompt에 붙이는 문서가 너무 길어지고, 최신 가격·재고·정책처럼 변하는 지식이 필요함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 중 비용과 품질을 비교한다.

14. Provider 선택은 라인업보다 계약 축으로 보기

섹션 제목: “14. Provider 선택은 라인업보다 계약 축으로 보기”

모델 이름, 가격, release date, provider 라인업은 시간이 지나면 바뀐다. 이 문서는 특정 최신 모델 순위를 외우는 대신, provider를 비교할 때 봐야 할 계약 축을 남긴다.

Provider 선택 축

API 호환성

messages, system 처리, tool schema, streaming chunk, usage 응답이 앱 추상화와 맞는가

OpenAI 호환처럼 보여도 edge case가 다를 수 있다

Context와 출력 한도

필요한 input history, RAG context, output 길이를 한 호출에 담을 수 있는가

큰 context가 항상 좋은 것은 아니며 비용과 lost-in-middle을 같이 본다

Structured output

strict schema, tool calling, JSON mode의 보장 수준과 실패 응답이 명확한가

데이터 추출과 workflow 자동화에서는 품질만큼 중요하다

비용 구조

input/output/reasoning/cache/batch/multimodal 단가를 분리해 측정할 수 있는가

가격표 숫자는 provider docs에서 최신 확인한다

Rate limit과 운영 지원

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 modeJSON 형식 보장schema 일치 보장은 아님
Structured strictschema 일치가 중요한 추출·tool 인자미지원 provider/model에서는 validation retry 필요
StreamingTTFT 개선, 긴 답변 UXpartial 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 missprompt 길이 측정, provider 라우팅, 캐싱 확인
Total latency만 길어짐첫 token은 빠른데 완료가 늦음output token 과다, decode 병목max_tokens, 답변 형식, streaming UX 조정
JSON parse 실패parse error, partial chunkstreaming 조립 실패, 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를 본다. 둘을 같은 “느림”으로 묶으면 잘못된 최적화를 고르기 쉽다.

17. 운영 시나리오 — 비용과 지연을 같이 줄이기

섹션 제목: “17. 운영 시나리오 — 비용과 지연을 같이 줄이기”

가상의 사내 문서 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 hituser별 권한 데이터는 뒤에 두고 검증 필요
output schema 축소output token, parse 실패설명형 답변에는 너무 빡빡할 수 있음
모델 라우팅쉬운 요청의 단가와 latencyrouter 오판 시 품질 회귀
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 길이를 본다.

  • 챗봇과 코드 어시스턴트의 기본 응답 생성
  • RAG 답변 생성과 citation schema
  • 이메일, 보고서, 이슈 요약
  • 데이터 추출과 분류
  • tool/function calling의 인자 생성
  • agent loop의 planner와 executor 호출
  • 이미지·음성·문서 multimodal 분석
  • eval 데이터셋 생성과 회귀 테스트

엔지니어가 LLM API를 운영할 때는 다음 질문을 계속 던진다.

  • API 추상화: provider별 messages, tool schema, streaming chunk 차이를 adapter로 흡수할 수 있는가?
  • 비용 모니터링: input/output/reasoning/cache token을 분리해 보고 있는가?
  • 구조화된 응답: downstream이 enum, number, date, citation을 검증할 수 있는가?
  • UX 지연: TTFT와 total latency를 분리해 streaming이 실제로 문제를 줄였는가?
  • 오류 처리: 429, timeout, content filter, invalid JSON을 같은 retry 정책으로 뭉개고 있지 않은가?
  • 다국어 비용: 한국어 token 효율을 target 모델 기준으로 측정했는가?
  • 보안 경계: role과 system prompt만 믿지 않고 tool 권한, validation, audit log를 두었는가?
개념 A개념 B차이점
Chat completionText completionmessages+role 기반 대화 계약 vs 단일 prompt 문자열 완성
Message historyServer memory요청에 담아 보내는 맥락 vs provider가 보관하는 상태
RolePermission모델 입력 채널 힌트 vs 실제 권한 통제
TemperatureTop-p분포 sharpness 조정 vs 누적 확률 후보 cutoff
GreedySampling가장 높은 후보 선택 vs 확률 분포에서 선택
JSON modeStructured strictJSON 형식 유도 vs schema 제약
Streaming TTFTTotal latency첫 token 도착 시간 vs 전체 완료 시간
PrefillDecodeinput prompt 병렬 처리 vs output token 순차 생성
Prompt cachingKV cache요청 간 반복 prefix 재사용 vs 한 요청/sequence의 K,V 저장
Rate limitTimeout사용량 제한 초과 vs 처리 시간이 제한 초과
FallbackRetry다른 모델/provider로 우회 vs 같은 작업 재시도
seedtemperature=0sampling 재현성 보조 vs 낮은 다양성 설정

21. 체크리스트

  • Chat completion을 messages 배열과 다음 assistant 메시지 생성 계약으로 설명할 수 있다
  • system/user/assistant/tool role의 의미와 role이 보안 권한 자체는 아니라는 점을 말할 수 있다
  • stateless API에서 대화 history를 앱이 다시 보내야 하며, 이것이 input 비용과 context 문제를 만든다는 점을 설명할 수 있다
  • input token, output token, context window, prefill, decode가 비용과 지연에 어떻게 연결되는지 말할 수 있다
  • temperature, top_p, top_k, penalty가 next-token sampling 분포를 어떻게 바꾸는지 구분할 수 있다
  • JSON mode와 structured strict schema의 차이를 설명하고, 형식 안정성과 사실성을 분리해서 볼 수 있다
  • streaming이 TTFT 체감을 줄이지만 total latency와 output 비용을 없애지는 않는다는 점을 설명할 수 있다
  • prompt caching이 같은 prefix 재사용에 의존하며, 동적 값이 prefix 중간에 들어가면 깨질 수 있음을 말할 수 있다
  • RPM, TPM, concurrency rate limit을 구분하고, 429를 backoff만으로 해결하면 안 되는 경우를 설명할 수 있다
  • provider 선택 시 최신 모델명보다 API 계약, 가격 구조, rate limit, 데이터 경계를 확인해야 한다는 점을 안다
  • API 계약: chat completion, messages, roles, Responses API, Messages API, tool schema
  • Sampling: temperature, top_p, top_k, repetition penalty, frequency penalty, presence penalty, seed, logprobs
  • Structured output: JSON mode, JSON Schema, grammar-constrained decoding, Outlines, Instructor, Guidance, xgrammar
  • Streaming: SSE, async iterator, chunk buffer, incremental parser, TTFT, total latency
  • 추론 비용: prefill, decode, KV cache, prompt caching, prefix caching, continuous batching, speculative decoding
  • 운영 안정성: rate limit, retry-after, exponential backoff, circuit breaker, fallback, idempotency, dead-letter queue
  • Provider 추상화: OpenAI-compatible API, Anthropic Messages, Google Gemini API, Bedrock, Azure OpenAI, OpenRouter, LiteLLM
  • 평가와 관측성: usage tokens, cache hit ratio, P50/P95/P99 latency, schema violation rate, refusal rate, tool success rate

기본 호출과 sampling 체크

  • 공식 docs에서 현재 사용 가능한 작은 chat 모델 하나를 고른 뒤 같은 prompt를 temperature 0, 0.7, 1.2로 각각 3번 호출해 변동성을 비교한다
  • 같은 호출에서 input_tokens와 output_tokens를 기록하고, 문자 수가 아니라 token 수가 비용 단위임을 확인한다
  • temperature를 낮춰도 완전한 결정성이 보장되지 않는지 seed나 model version 조건을 함께 기록한다

Messages와 stateless history 체크

  • 2턴 대화를 만든 뒤 두 번째 요청에 이전 assistant 응답을 넣은 경우와 빼는 경우를 비교한다
  • system 메시지를 바꿨을 때 같은 user 질문의 응답이 어떻게 달라지는지 확인한다
  • 오래된 history를 모두 넣는 경우와 요약 history만 넣는 경우의 token 수와 답변 품질을 비교한다

Structured output 체크

  • JSON mode로 `{name, age}` 추출을 시도하고, age가 문자열로 나오는지 같은 schema 위반 사례를 기록한다
  • strict JSON schema 또는 provider의 tool schema로 같은 작업을 실행해 schema violation rate를 비교한다
  • schema는 맞지만 값이 틀린 사례를 만들어 business validation이 왜 필요한지 확인한다

Streaming 체크

  • streaming on/off로 같은 prompt의 TTFT와 total latency를 따로 측정한다
  • streaming 중 partial JSON을 바로 parse하면 왜 실패하는지 chunk를 저장해 확인한다
  • UI에는 chunk를 보여주되 tool argument는 backend에서 buffer 후 검증하는 구조를 그려본다

비용·캐싱·rate limit 체크

  • 긴 system prompt와 few-shot을 고정 prefix로 두고 cache hit ratio와 TTFT 변화를 provider usage 응답에서 확인한다
  • timestamp나 user_id를 prefix 중간에 넣었을 때 cache hit이 어떻게 깨지는지 비교한다
  • 가상의 TPM 한도와 평균 input/output token으로 분당 처리량을 계산한 뒤, output 길이가 늘 때 처리량이 어떻게 줄어드는지 손계산한다
  • 이미지 1개를 입력하는 multimodal 호출을 실행하고 usage/cost 응답에서 텍스트 호출과 token 계산 방식이 어떻게 달라지는지 확인한다
  • temperature가 낮은데 응답이 달라진다 → model version, seed 지원 여부, provider backend fingerprint, tool/RAG 결과 변화를 같이 본다.
  • strict schema가 깨진다 → 해당 model/provider가 strict를 실제 지원하는지 확인하고, JSON mode + validator + retry로 우회한다.
  • streaming에서 JSON parse가 실패한다 → chunk 단위가 JSON boundary와 일치한다고 가정하지 말고 buffer를 둔다.
  • prompt caching 이득이 없다 → prefix 길이, 동적 값 위치, TTL, provider별 cache 조건을 공식 문서에서 다시 확인한다.
  • 429가 retry 후에도 반복된다 → RPM인지 TPM인지 concurrency인지 나누고, queue와 token budget을 조정한다.
  • fallback provider에서만 실패한다 → messages, tool schema, safety filter, streaming chunk 형식의 contract test를 provider별로 만든다.

선택 부록 — 운영 절차를 볼 때의 최소 순서

섹션 제목: “선택 부록 — 운영 절차를 볼 때의 최소 순서”
LLM API 장애를 절차로 좁히는 짧은 순서
  1. 사용자 증상을 TTFT, total latency, 형식 오류, 내용 오류, 429/timeout 중 하나로 분류한다.
  2. 해당 요청의 input_tokens, output_tokens, model, provider, temperature, response schema, cache hit 여부를 로그에서 확인한다.
  3. TTFT 문제면 queue, prefill 길이, cache miss를 먼저 본다.
  4. total latency 문제면 output token 수, streaming 여부, decode 병목을 먼저 본다.
  5. 형식 오류면 structured output 지원 여부, streaming buffer, validator retry를 본다.
  6. 429면 RPM, TPM, concurrency 중 무엇을 넘었는지 분리한다.
  7. provider fallback을 켤 때는 품질보다 먼저 schema/tool/streaming contract test를 통과시킨다.
  1. LLM API의 기본 계약은 messagesrole을 보내 다음 assistant 응답을 받는 chat completion 흐름이다.
  2. API는 보통 stateless이므로 대화 history를 앱이 다시 보내야 하고, 이 때문에 input token 비용과 context 관리가 중요해진다.
  3. prefill은 input 처리, decode는 output 생성이며, TTFT와 total latency를 나누어 봐야 올바른 최적화를 고를 수 있다.
  4. sampling은 다양성과 재현성을 조절하고, structured output은 형식 안정성을 높이지만 사실성을 자동 보장하지 않는다.
  5. production에서는 prompt caching, streaming, rate limit, retry, fallback, provider contract test가 모델 품질만큼 중요하다.

모델명, 가격, release date, provider 라인업은 시간에 취약하다. 아래 문서는 개념과 API 계약을 확인하기 위한 출처이며, 실제 도입 전에는 각 provider의 최신 공식 문서와 pricing/deprecation 문서를 다시 확인한다.


최종 수정: 2026-07-09