API로 LLM을 부른다는 게 기술적으로 무엇인가

📍 AI 공부 지도 — 25/29편
이 글은 AI의 기초부터 Meta-Harness·응용 비교까지 순서대로 읽는 29편 시리즈의 25편입니다.
📚 전체 지도 보기
📚 이 글을 읽기 전에: 22편 시리즈 + P1(Claude family)을 읽으셨다면 OK. 특히 F1 LLM·B2 프롬프트 메커니즘·B1 Agent·M3 Agent 심화가 핵심.

API로 LLM을 부른다는 게 기술적으로 무엇인가

“API로 Claude를 부른다.” “OpenAI API를 붙였다.”

이 문장을 매일 듣긴 하는데, 정작 그 안에서 진짜로 뭐가 일어나는지 물어보면 답이 애매해지는 분들이 꽤 많습니다.

SDK 한 줄 깔고 client.messages.create(...) 찍어서 답이 돌아오면 “된다”는 건 알아요. 그런데 그 한 줄이 네트워크 레벨에서 어떤 HTTP 요청이고, 왜 어떤 경우엔 타입을 치자마자 글자가 흘러나오고 어떤 경우엔 3초 기다려야 하며, 왜 “모델이 함수를 실행했다”는 말이 기술적으로 거짓말에 가까운지 — 이걸 한 번 제대로 훑고 가면 이후에 나오는 agent·streaming·tool use 얘기가 전부 다르게 읽힙니다.

이 글은 6,000~7,500자 안에서 이걸 다 훑습니다.

  1. Anthropic Messages vs OpenAI Chat Completions의 JSON 구조
  2. Streaming이 실제로는 SSE 프로토콜 위에서 도는 얘기
  3. Tool Use 라운드트립이 4단계라는 얘기 (그리고 모델은 함수를 실행하지 않는다는 얘기)
  4. 토큰 경제학 — 가격표·배치·캐싱·Opus 4.7 토크나이저 +35% 함정
  5. Rate limit이 5축(RPM/TPM/RPD/TPD/IPM)이라는 얘기
  6. SDK vs Raw HTTP, 언제 뭘 쓰나

하나씩 가보죠.


1. POST 요청 한 번이 전부다

먼저 이 문장부터 머리에 박고 시작합시다.

LLM API를 부른다 = https://api.anthropic.com/v1/messages 에 HTTPS POST를 한 번 쏘는 것.

끝입니다. 진짜로요.

Anthropic이든 OpenAI든, SDK든 Python이든 Node든 Go든, 최종적으로 네트워크 레벨에서 일어나는 건 HTTPS 위에 JSON 바디를 얹은 POST 한 발입니다. 나머지 전부(SDK, 스트리밍, tool use)는 이 POST 한 발을 어떻게 감싸고 어떻게 응답을 읽느냐의 문제일 뿐이에요.

curl로 직접 쏘면 이렇게 생겼습니다.

POST /v1/messages HTTP/1.1
Host: api.anthropic.com
x-api-key: sk-ant-...
anthropic-version: 2023-06-01
content-type: application/json

{
  "model": "claude-opus-4-7",
  "max_tokens": 1024,
  "system": "You are a helpful assistant.",
  "messages": [
    {"role": "user", "content": "LLM API가 뭐예요?"}
  ]
}

이게 전부예요. x-api-key 헤더에 키를 박고, JSON 바디를 던지면, 응답으로 JSON이 돌아옵니다.

돌아오는 응답도 생각보다 단순합니다.

{
  "id": "msg_01XYZ...",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-4-7",
  "content": [
    {"type": "text", "text": "LLM API라는 건..."}
  ],
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 18, "output_tokens": 142}
}
  • content에 답변 텍스트
  • usage에 이번 호출에서 쓴 토큰 수 (이게 과금 기준)
  • stop_reason에 왜 끝났는지 (end_turn, max_tokens, stop_sequence, tool_use 중 하나)

SDK를 쓰든 curl을 쓰든, 이 구조 위에서 움직입니다. 그래서 디버깅이 안 풀릴 때는 raw HTTP로 내려가서 보는 게 제일 빠르다는 말이 나오는 겁니다. SDK가 뭘 감추고 있는지를 벗기고 나면, 그 밑은 이 JSON 두 덩어리밖에 없어요.


2. Anthropic Messages vs OpenAI Chat — JSON 구조는 쌍둥이처럼 다르다

두 회사 다 “메시지 리스트를 던지면 답이 온다”는 기본 꼴은 같습니다. 그런데 들여다보면 작은 차이들이 계속 발목을 잡아요. 그중 가장 큰 차이 두 개만 봅시다.

차이 1 — system의 위치.

Anthropic Messages API는 systemmessages 바깥에 있습니다.

{
  "model": "claude-opus-4-7",
  "max_tokens": 1024,
  "system": "You are a senior Python engineer.",
  "messages": [
    {"role": "user", "content": "..."}
  ]
}

OpenAI Chat Completions는 systemmessages 안의 첫 원소로 들어갑니다.

{
  "model": "gpt-5",
  "messages": [
    {"role": "system", "content": "You are a senior Python engineer."},
    {"role": "user", "content": "..."}
  ]
}

같은 개념인데 위치가 다르다는 것. 이게 OpenAI 코드를 Claude로 포팅할 때 제일 자주 버그가 생기는 지점이에요. SDK가 감춰줘서 티가 안 날 뿐입니다.

차이 2 — max_tokens의 지위.

Anthropic은 max_tokens필수입니다. 빼면 400 에러가 나요. 이게 “출력 길이 상한을 항상 명시하라”는 설계 철학입니다.

OpenAI는 max_tokens(또는 max_completion_tokens)가 선택입니다. 안 쓰면 모델 최대치까지 돌려줍니다.

작은 차이 같지만 실무에선 큽니다. Claude는 토큰 예산을 미리 고정하니까 비용 예측이 쉬워요. OpenAI는 안 쓰면 풀로 때려버려서 예상 밖 고지서가 날아올 수 있습니다.

공통점은 이겁니다.

  • messages{role, content} 객체의 배열
  • roleuser, assistant, system (OpenAI는 추가로 tool, Anthropic은 tool_use/tool_result 블록으로 처리)
  • stream: true를 넣으면 Server-Sent Events로 전환
  • tools 배열에 함수 스키마를 넣으면 tool use 모드

2026년 4월 기준, Anthropic의 주력은 Opus 4.7 · Sonnet 4.6 · Haiku 4.5 3장, OpenAI는 GPT-5 · GPT-5.4가 플래그십입니다. 모델 이름만 바꾸고 나머진 거의 그대로 도는 구조예요.


3. Streaming = SSE 프로토콜 위에서 도는 얘기

ChatGPT에서 답이 한 글자씩 흘러나오는 것, 보셨죠? 그게 스트리밍이고, 네트워크 레벨에선 Server-Sent Events(SSE) 라는 아주 오래된 웹 표준 위에서 돕니다.

WebSocket이 아닙니다. HTTP/1.1 커넥션을 열어둔 채로, 서버가 data: {...}\n\n 형식의 텍스트 덩어리를 연속으로 밀어넣는 방식. 단방향 푸시라 구현도 훨씬 단순합니다.

요청에서 stream: true만 넣으면 응답이 통째로 돌아오지 않고, 이벤트 시퀀스로 쪼개져서 날아옵니다.

Anthropic의 경우, 이벤트 순서가 꽤 구조적입니다.

event: message_start
data: {"type":"message_start","message":{"id":"msg_01...","role":"assistant",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"LLM"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" API"}}

... (수십~수백 개의 delta) ...

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":142}}

event: message_stop
data: {"type":"message_stop"}

정리하면 message_startcontent_block_startcontent_block_delta × N → content_block_stopmessage_deltamessage_stop 이 한 셋입니다.

OpenAI는 좀 더 플랫합니다. choices[0].delta.content에 토큰 조각이 계속 들어오고, 끝나면 finish_reason이 박힌 마지막 청크 뒤에 data: [DONE] 한 줄이 찍히고 커넥션이 닫힙니다.

data: {"choices":[{"delta":{"content":"LLM"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":" API"},"finish_reason":null}]}

... 

data: {"choices":[{"delta":{},"finish_reason":"stop"}]}

data: [DONE]

왜 이렇게 하느냐.

LLM은 앞에서 본 대로 한 토큰씩 순차적으로 생성합니다. 답이 완성될 때까지 기다렸다가 통째로 주면, 500 토큰짜리 답변은 체감 5~10초를 멀뚱하게 기다려야 해요. 스트리밍은 첫 토큰이 나오는 순간 바로 클라이언트로 밀어넣기 때문에, TTFT(Time To First Token) 가 1초 이내로 내려옵니다. ChatGPT 같은 “타이핑되는” UX가 가능한 이유가 이겁니다.

FIG 1 · HTTP 요청 → SSE 청크 → 종료
CLIENT ── POST /v1/messages { stream:true, messages:[…] } ──▶
SERVER ◀── 200 OK, Content-Type: text/event-stream
    ◀── event: message_start
    ◀── event: content_block_start
    ◀── event: content_block_delta (TTFT ≈ 0.5~1.5s)
    ◀── event: content_block_delta × N
    ◀── event: content_block_stop
    ◀── event: message_delta (usage)
    ◀── event: message_stop
CLIENT — delta 누적 → UI 렌더 → 최종 usage 기록

SDK를 쓰면 이 SSE 파싱을 자동으로 해줘서 for chunk in response 루프로 쓰면 되지만, 디버깅할 땐 curl에 --no-buffer 붙여서 원본 청크를 직접 보는 게 가장 빠릅니다.


4. Tool Use 라운드트립 — 모델은 함수를 실행하지 않는다

여기서 오해가 제일 많이 생깁니다.

“Claude가 날씨 API를 호출했다.” “GPT가 내 DB를 조회했다.”

틀렸습니다. 모델은 함수를 실행한 적이 없습니다. 모델은 “이 함수를 이 인자로 부르라”는 JSON을 뱉었을 뿐이고, 실제 실행은 당신 코드가 한 겁니다.

이걸 정확히 이해하면 tool use 관련 버그의 90%가 풀립니다.

흐름은 이렇게 4단계입니다.

[1] 클라이언트 → 모델: tools 스키마와 함께 요청을 보낸다.

{
  "model": "claude-opus-4-7",
  "max_tokens": 1024,
  "tools": [{
    "name": "get_weather",
    "description": "Get current weather for a city",
    "input_schema": {
      "type": "object",
      "properties": {"city": {"type": "string"}},
      "required": ["city"]
    }
  }],
  "messages": [
    {"role": "user", "content": "서울 날씨 어때?"}
  ]
}

[2] 모델 → 클라이언트: stop_reason: "tool_use"와 함께 함수 호출 의도를 JSON으로 돌려준다.

{
  "stop_reason": "tool_use",
  "content": [
    {"type": "text", "text": "날씨를 확인해 드릴게요."},
    {"type": "tool_use", "id": "toolu_01ABC",
     "name": "get_weather",
     "input": {"city": "Seoul"}}
  ]
}

OpenAI에선 이게 tool_calls 배열로 돌아옵니다. 이름만 다르고 개념은 같아요.

[3] 클라이언트가 실제로 함수를 실행한다. 여기서 모델은 관여하지 않습니다. get_weather("Seoul")을 실제 API든 내 DB든 불러서 결과를 받아오는 건 전적으로 당신 코드의 책임입니다.

[4] 클라이언트 → 모델: 실행 결과를 tool_result로 messages에 추가해서 같은 대화 컨텍스트로 다시 요청한다.

{
  "messages": [
    {"role": "user", "content": "서울 날씨 어때?"},
    {"role": "assistant", "content": [
      {"type": "text", "text": "날씨를 확인해 드릴게요."},
      {"type": "tool_use", "id": "toolu_01ABC",
       "name": "get_weather", "input": {"city": "Seoul"}}
    ]},
    {"role": "user", "content": [
      {"type": "tool_result", "tool_use_id": "toolu_01ABC",
       "content": "21°C, 맑음"}
    ]}
  ]
}

그제서야 모델은 “서울은 지금 21도에 맑네요” 같은 최종 답변을 텍스트로 내놓습니다.

FIG 2 · TOOL USE 4단계 ROUND-TRIP
[1] REQUEST   클라이언트 ──▶ 모델
    messages + tools[ {name, input_schema} ]
[2] tool_use   모델 ──▶ 클라이언트
    stop_reason:”tool_use” + { name, input } (JSON)
    ※ 모델은 실행 안 함. “불러라”만 말함.
[3] EXECUTE   클라이언트 내부
    실제 get_weather(“Seoul”) 실행 → “21°C, 맑음”
[4] tool_result   클라이언트 ──▶ 모델
    messages에 tool_result 추가 → 재요청 → 최종 답변

이 구조의 귀결이 중요합니다.

  • 모델이 위험한 쿼리(예: DROP TABLE users)를 “실행”할 수 없습니다. 실행은 당신 코드예요. 당신이 막으면 됩니다.
  • 한 턴에 tool이 여러 번 도는 것도 가능합니다. tool_use → execute → tool_result → tool_use → execute → tool_result → … agent loop가 이 위에서 굴러갑니다.
  • 그래서 agent 개발은 “모델이 똑똑한가”보다 “tool_result를 얼마나 잘 정제해서 다시 넣느냐“가 생산성을 결정합니다.

5. 토큰 경제학 — 가격표와 Opus 4.7 +35% 함정

API를 프로덕션에 붙이기 전에 반드시 한 번은 만나야 하는 게 과금 구조입니다.

2026년 4월 기준, 주요 모델 가격을 정리하면 이렇습니다. 모두 1M 토큰당 USD.

FIG 3 · 2026-04 모델별 토큰 가격 ($/1M)
모델 Input Output
Claude Opus 4.7 $5 $25
Claude Sonnet 4.6 $3 $15
Claude Haiku 4.5 $1 $5
GPT-5 $1.25 $10
GPT-5.4 $2.50 $15
GPT-4.1 nano $0.10 $0.40

세 가지 포인트.

포인트 1 — 출력이 입력보다 4~5배 비싸다.

거의 모든 모델에서 output 가격이 input의 4~5배예요. 그래서 비용 최적화의 1순위는 출력 길이 줄이기입니다. max_tokens를 타이트하게 걸고, “짧게 답해”라고 system에 박고, 필요 없는 경우 JSON 모드로 구조화 출력만 받는 식.

포인트 2 — 할인 카드 두 장이 있다.

  • Batch API: -50%. 실시간이 필요 없는 대량 작업(데이터 분류, 대량 번역 등)은 Batch로 돌리면 반값입니다. 대신 24시간 안에 결과 돌려준다는 SLA.
  • 프롬프트 캐싱: 캐시 히트 부분 최대 -90%. 같은 system prompt나 긴 문서를 반복해서 넣는 케이스에서 큰 폭 할인. Anthropic은 cache_control 블록을 명시적으로 지정하는 방식이고, OpenAI는 일정 조건에서 자동으로 걸립니다.

RAG나 긴 system prompt가 고정인 애플리케이션에서 캐싱은 거의 필수라고 봐야 해요. 안 걸고 돌리면 돈이 녹습니다.

포인트 3 — Opus 4.7의 “같은 텍스트 +35% 토큰” 함정.

Anthropic 공식 발표에 따르면 Opus 4.7은 새 토크나이저를 씁니다. 그 결과 같은 입력 텍스트가 이전 버전 대비 약 +35% 더 많은 토큰으로 카운트됩니다.

무슨 뜻이냐면, 단가가 똑같아 보여도 실질 비용이 1.35배로 오른 것과 유사하다는 겁니다. Sonnet 4.6 → Opus 4.7 갈아탈 때, 카드 단가만 보고 “$15 → $25, 1.67배”로 계산하면 안 되고, 토큰 수 증가분까지 곱해서 약 2.25배로 잡아야 맞습니다.

공식 문서에 “주의하라”고 명시된 함정이라 이건 추정이 아닙니다. 모델 전환 전에 실제 토큰 카운트를 한 번 찍어보는 게 안전합니다.


6. Rate Limit 5축 — RPM만 보면 당한다

개발자가 제일 많이 부딪히는 게 429 Too Many Requests입니다. 이때 많은 분들이 RPM(분당 요청 수)만 올리려고 하는데, 레이트 리밋은 그것보다 복잡해요.

OpenAI 공식 문서 기준으로 리밋은 5개 축 위에서 걸립니다.

  • RPM (Requests Per Minute) — 분당 요청 건수
  • TPM (Tokens Per Minute) — 분당 처리 토큰 합 (입력+출력)
  • RPD (Requests Per Day) — 일일 요청 건수
  • TPD (Tokens Per Day) — 일일 토큰 합
  • IPM (Images Per Minute) — 비전/이미지 입력 전용 (멀티모달 케이스)

이 중 어느 하나라도 히트하면 429가 떨어집니다. 그래서 “요청 빈도는 낮은데 왜 막히지?” → 답은 대개 긴 prompt로 TPM을 태워서인 경우가 많습니다.

OpenAI의 Tier 구조를 예로 들면,

  • Tier 1 (가입 직후): 수만 TPM 수준
  • Tier 2~3 (스타트업 초기, 누적 $50~$500 결제): 2~4M TPM
  • Tier 5 (누적 $1,000+ 결제, 30일 이상): 30M TPM 수준까지 개방

Tier는 누적 결제액과 계정 연령으로 자동 상승합니다. “API 많이 쓰면 알아서 풀린다”고 생각하면 됩니다.

Anthropic도 구조는 비슷하고, Usage 대시보드에서 현재 tier와 한도를 확인할 수 있습니다.

429가 났을 때 표준 대응은 지수 백오프(exponential backoff) + jitter입니다.

for attempt in range(5):
    try:
        return client.messages.create(...)
    except RateLimitError:
        wait = (2 ** attempt) + random.random()
        time.sleep(wait)  # 1s, 2s, 4s, 8s, 16s (+랜덤 jitter)

SDK 대부분이 이걸 내장해서 자동 재시도를 해줍니다. 그래서 SDK를 쓰면 어지간한 429는 소리 없이 복구되는 이유가 이거예요. Raw HTTP로 내려가면 이 로직을 직접 짜야 합니다.


7. SDK vs Raw HTTP — 언제 뭘 쓰나

결론부터 가죠.

  • 프로덕션 = SDK
  • 디버깅 = Raw HTTP

SDK가 안 감추고 있는 척하지만 사실 많이 감추고 있어요. SSE 파싱, 재시도, 타임아웃, 토큰 카운트 집계, 타입 체크(TypeScript/Pydantic) 전부 SDK가 처리합니다. 프로덕션에서 이걸 직접 구현할 이유는 거의 없습니다.

다만 뭔가 이상한 에러가 떨어지거나, SDK 버전과 API 버전이 어긋나서 새 기능이 안 먹을 때는 raw HTTP로 내려가는 게 제일 빠릅니다. curl에 -v(verbose) 붙여서 실제 헤더·바디·스테이터스를 직접 확인하고 나면, 보통 범인이 드러납니다.

SDK를 쓰든 curl을 쓰든, 아까 처음에 본 그 구조 — JSON 바디를 얹은 HTTPS POST 한 발 — 위에서 도는 건 똑같습니다. 이걸 한 번 체화해 두면, 새 API(예: Gemini, Mistral)를 처음 봐도 1~2시간이면 감을 잡을 수 있습니다.


8. 한 줄로 묶으면

  • LLM API 호출 = JSON 바디 얹은 HTTPS POST 한 발 + 응답 JSON 파싱.
  • Anthropic과 OpenAI는 system 위치와 max_tokens 필수 여부에서 갈린다.
  • Streaming은 SSE 프로토콜. 이벤트 시퀀스로 쪼개져 들어온다.
  • Tool use는 4단계 라운드트립이고, 모델은 함수를 실행하지 않는다. 실행은 당신 코드.
  • 토큰 경제학은 Output이 Input의 4~5배, Batch -50%·캐싱 -90% 할인 있음, Opus 4.7은 같은 텍스트 +35% 토큰 함정.
  • Rate limit은 RPM·TPM·RPD·TPD·IPM 5축. 429 나면 지수 백오프.
  • SDK는 프로덕션용, Raw HTTP는 디버깅용. 밑바닥은 똑같다.

이 7가지만 머리에 있으면, agent 프레임워크 코드를 읽을 때 뭐가 모델 얘기고 뭐가 포장 얘기인지가 선명해집니다.


FAQ

Q1. SDK 안 쓰고 curl로 직접 쏴도 되나요?

됩니다. 프로덕션에서도 기술적으론 가능해요. 다만 SSE 파싱, 지수 백오프, 타임아웃 관리, 스트리밍 중 커넥션 끊김 처리 같은 걸 전부 직접 짜야 해서 품이 큽니다. 학습·디버깅용으론 curl -v --no-buffer가 최고이고, 서비스 배포는 공식 SDK를 권장합니다. 어차피 SDK 내부도 같은 HTTPS POST예요.

Q2. 모델이 함수를 실행하지 않는다면 “Claude Code가 파일 편집했다” 같은 건 거짓말인가요?

아닙니다. 정확히는 Claude Code(harness)가 tool_use 블록을 받아서 실제로 파일 시스템 API를 호출한 거예요. 모델은 “이 파일에 이 내용 써라”라는 JSON만 뱉었고, 실행은 harness가 합니다. 사용자 입장에선 한 몸처럼 보이지만 기술적으론 확실하게 분리돼 있어요. 이게 agent 보안 설계의 출발점이기도 합니다 — harness가 권한을 거르면 모델이 아무리 위험한 tool_use를 뱉어도 실제로는 막히거든요.

Q3. Opus 4.7을 쓰려다가 비용이 예상보다 훨씬 나왔어요. 왜죠?

가능성이 세 개입니다. (1) 출력 토큰이 input의 5배 단가라는 걸 놓쳤거나, (2) Opus 4.7의 새 토크나이저로 같은 텍스트가 ~35% 더 많은 토큰으로 계산돼서 실질 단가가 올랐거나, (3) 프롬프트 캐싱을 안 걸어서 같은 system prompt를 매번 풀 비용으로 재전송하고 있거나. 셋 다 확인해 보시면 대개 원인이 잡힙니다.


다음 편 안내

P3에서는 이 API 호출이라는 개념이 클라우드(Anthropic·OpenAI)오픈소스(Llama·Mistral 로컬 실행) 에서 각각 어떻게 갈라지는지를 다룹니다. 같은 HTTP POST처럼 생겼지만, 뒤에서 벌어지는 일이 완전히 다르거든요.


소스 리스트

  1. Anthropic — Messages API 레퍼런스: https://docs.anthropic.com/en/api/messages
  2. Anthropic — Pricing: https://platform.claude.com/docs/en/about-claude/pricing
  3. Anthropic — Tool Use Overview: https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
  4. Anthropic — Implementing Tool Use: https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
  5. Anthropic Engineering — Advanced Tool Use: https://www.anthropic.com/engineering/advanced-tool-use
  6. OpenAI — API Reference Overview: https://developers.openai.com/api/reference/overview
  7. OpenAI — Streaming Responses Guide: https://developers.openai.com/api/docs/guides/streaming-responses
  8. OpenAI — Chat Completions Streaming Events: https://developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events
  9. OpenAI — Pricing: https://developers.openai.com/api/docs/pricing
  10. OpenAI — Rate Limits Guide: https://developers.openai.com/api/docs/guides/rate-limits

🗺 P 시리즈 현재 위치

회원가입(무료)으로 매주 월요일 아침 AI 공부 지도 뉴스레터를 받아보세요 → 회원가입하기

바이브코딩 태일러
바이브코딩 태일러
AI의 작동 원리와 비즈니스 적용을 일본어·한국어로 기록합니다. 매주 월요일 뉴스레터 발행 중.
뉴스레터 구독하기 →
JAKO