코드에서 모델 호출하기


지난 장에서는 메시지 목록을 만들었습니다. 이 장에서는 그 목록을 Python으로 실제 모델에 보내서 답을 받아옵니다. 이 왕복 과정이 이 모델들로 무언가를 만드는 가장 작은 완전한 순환 구조이며, 여러분이 작성할 거의 모든 기능이 이 위에 세워집니다.
프롬프트를 입력하면 코드가 어딘가로 보내고, 텍스트가 돌아옵니다. 마치 모델이 여러분의 프로그램 안에 있는 것처럼 느껴지지만 실제로는 그렇지 않습니다. 모델이 실제로 어디에서 실행되는지 알면 비용, 속도, 그리고 여러분이 겪게 될 여러 특이한 현상들이 설명됩니다.
실제로 물리적으로 무슨 일이 벌어지는지 그려봅시다. 모델은 프로바이더의 서버, 여러분이 절대 볼 수 없는 하드웨어 위에 놓인 거대한 파라미터 집합입니다. 모델을 호출하면 여러분의 코드는 메시지를 담은 평범한 웹 요청을 인터넷을 통해 보냅니다.
프로바이더는 자신들의 머신에서 예측 루프를 실행하고 생성된 토큰을 여러분에게 돌려보냅니다. 모델 호출이란 남의 컴퓨터를 잠깐 빌려 쓰는 것입니다. 이렇게 생각하면 많은 것이 이해됩니다. 지연 시간은 모델이 상대편에서 토큰을 하나씩 생성하는 데 걸리는 시간이고, 비용은 그 연산에 대해 프로바이더가 부과하는 요금이며, 이 전체가 네트워크 호출이라는 사실이 함축하는 모든 것을 그대로 안고 갑니다.
예제는 공식 OpenAI 라이브러리를 사용합니다. 다른 프로바이더는 세부사항이 다르지만, 형태는 거의 어디서나 같습니다. 메시지 목록을 보내면 메시지가 하나 돌아옵니다. 이런 동일한 형태 덕분에 특정 프로바이더에 종속되지 않을 수 있습니다. 같은 코드로 여러분이 직접 호스팅하거나 다른 곳에서 대여한 오픈 모델을 가리키게 할 수 있고, 대개는 base URL만 바꾸면 됩니다(오픈 모델과 클로즈드 모델에서 이 선택을 다룹니다).
요청
pip install openai로 라이브러리를 설치하고, 클라이언트를 만들고, 호출하세요. 클라이언트는 환경 변수에서 API 키를 읽어오므로 키가 코드에 등장하지 않습니다.
from openai import OpenAI
client = OpenAI() # 환경 변수에서 OPENAI_API_KEY를 읽어옵니다
MODEL = "gpt-4o-mini" # 모델을 바꿀 때 고치는 단 한 줄
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a concise assistant. Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
)필수 항목은 두 가지입니다. 실행하고 싶은 모델의 이름인 model, 그리고 지난 장에서 다룬 역할이 붙은 메시지 목록인 messages입니다. 나머지는 모두 기본값이 있습니다.
모델 이름이 하나의 상수에 담겨 있는 것을 눈여겨보세요. 몇 달에 한 번씩 더 좋고 더 저렴한 모델이 등장하는 분야이기 때문에 이렇게 해두는 것입니다. **모델 이름**을 한 곳에만 두면 모델을 바꿀 때 한 줄만 고치면 됩니다. 이것이 변화에 대응하는 규칙을 실제로 적용한 예입니다. 자주 바뀌는 구체적인 값들은 코드 곳곳에 흩어놓지 말고 찾기 쉬운 한 곳에 모아두세요.
model과 messages 두 가지뿐입니다. 모델 이름은 하나의 상수에 담아두세요. 더 저렴하거나 더 좋은 모델이 몇 달에 한 번씩 나오는데, 그럴 때마다 코드를 뒤지고 싶지는 않을 테니까요. 저도 이걸 힘들게 배웠습니다. 모델 이름이 바뀌었을 때 아홉 개 파일에 그대로 박혀 있는 걸 발견하고서요. 응답 읽기
응답은 어떤 구조로 감싸여서 돌아옵니다. 원하는 텍스트는 하나의 경로로 들어가 있지만, 나머지 구조도 알아둘 만합니다. 무슨 일이 있었는지 알려주기 때문입니다.
choice = response.choices[0]
print(choice.message.content) # 응답 텍스트
print(choice.finish_reason) # "stop" -> 모델이 스스로 끝냈다는 뜻
print(response.usage) # Usage(prompt_tokens=24, completion_tokens=18, total_tokens=42)여기서 중요한 것은 세 가지입니다. message.content가 텍스트이며, choices[0] 안에 들어 있는 것은 API가 여러 개의 선택지를 돌려줄 수 있기 때문입니다. 다만 여러분은 거의 늘 하나만 요청하고 첫 번째를 읽습니다.
finish_reason은 모델이 왜 멈췄는지 알려줍니다. "stop"은 생각을 다 끝냈다는 뜻이고, "length"는 토큰 한도에 걸려서 답이 중간에 끊겼다는 뜻입니다. 이 필드를 확인하는 것이 눈으로 훑어보는 대신 코드로 잘린 응답을 감지하는 방법입니다.
그리고 usage는 이 호출이 소모한 토큰 수를 보내는 프롬프트와 생성된 응답으로 나눠서 보여줍니다. 이 usage 객체가 바로 항목별로 나뉜 청구서입니다. 모든 호출은 토큰 단위로 정확히 얼마가 들었는지 알려주며, 이것이 여러분이 만들어가면서 지켜봐야 할 숫자입니다.
response.choices[0].message.content에 있지만, 옆에 있는 두 필드도 중요합니다. finish_reason은 왜 멈췄는지 알려줍니다. "stop"은 완료, "length"는 토큰 한도에 걸려 잘렸다는 뜻입니다. 그리고 response.usage는 토큰으로 표시된 청구서로, 프롬프트와 완성분이 나뉘어 있어서 호출 비용을 추측할 필요가 없습니다. 알아둘 만한 파라미터
model과 messages 외에도 자주 등장하는 선택적 설정 두 가지가 있습니다.
temperature는 LLM은 어떻게 동작하는가에서 다룬 무작위성 다이얼을 구체화한 것입니다. 낮은 값(0에 가까운 값)은 모델을 상위 후보에 가깝게 유지시켜서 집중되고 반복 가능한 결과를 냅니다. 추출이나 분류 작업에 원하는 특성입니다. 높은 값(0.7에서 1 사이)은 모델이 확률이 낮은 토큰까지 시도하게 해서 더 다채롭고 창의적이지만 방향을 벗어나기도 쉬운 결과를 냅니다. 여러분이 조정하는 것은 모델이 얼마나 대담하게 샘플링하느냐일 뿐, 그 이상은 아닙니다.
max_tokens는 응답이 늘어날 수 있는 최대 토큰 수를 제한합니다. 답이 끝없이 길어지는 것과 청구서가 끝없이 늘어나는 것을 막아줍니다. **max_tokens**를 너무 낮게 잡으면 모델이 중간에 끊깁니다. 이것이 바로 앞 절에서 본 "length" 종료 이유이므로, 여유를 넉넉히 남겨두세요.
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.2, # 집중되고 일관된 답변
max_tokens=300, # 응답 길이 제한
)temperature는 모델이 얼마나 대담하게 샘플링하느냐를 조정합니다. 낮으면 집중되고 반복 가능한 답, 높으면 다채롭고 창의적인 답이 나옵니다. max_tokens는 청구서를 보호하려고 응답 길이를 제한하는데, 너무 낮게 잡으면 "length" 잘림이 발생하니 여유를 남겨두세요. 추출이나 분류 작업에는 언제나 낮은 temperature를 쓰세요. 스트리밍
기본적으로는 전체 답변이 생성을 마칠 때까지 기다렸다가 한꺼번에 받습니다. 모델이 토큰을 하나씩 생성하기 때문에 긴 답변은 아무것도 없는 화면을 바라보며 오래 기다리는 것을 의미합니다.
**스트리밍**은 각 토큰이 생성되는 즉시 여러분에게 보내주므로, 텍스트가 곧바로 나타나서 실시간으로 채워집니다. 채팅 앱에서 답변이 타이핑되듯 나타나는 것과 같은 방식입니다.
stream = client.chat.completions.create(
model=MODEL,
messages=messages,
stream=True,
)
for chunk in stream:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)전체 생성 시간은 어느 쪽이든 동일합니다. 모델이 더 빨라지는 것이 아닙니다. 스트리밍은 단지 토큰을 언제 보게 되느냐만 바꿀 뿐이며, 끝날 때까지 붙잡아두지 않고 생성되는 즉시 보여줍니다.
이를 다루는 방법은 청크를 순회하면서 텍스트의 각 delta를 출력하는 것입니다. 대가로 코드가 조금 늘어나지만 대기 경험은 훨씨 좋아집니다. 스트리밍은 토큰을 언제 보는지를 바꿀 뿐, 얼마나 빨리 도착하는지를 바꾸지는 않습니다.
delta를 출력함으로써 토큰을 더 일찍 볼 수 있을 뿐입니다. 모든 호출은 독립적입니다
이것이 이 장에서 가장 중요한 동작이며, 컨텍스트 윈도우 강의에서 곧바로 이어지는 내용입니다. API는 **무상태(stateless)**입니다. 각 요청은 완전히 독립적으로 존재합니다. 프로바이더의 서버는 여러분의 이전 호출을 기억하지 못합니다. 여러분이 보낸 메시지만을 가지고 정확히 예측을 수행하고, 결과를 반환한 뒤에는 다음번을 위해 이 교환에 대해 아무것도 남기지 않습니다.
그러니 대화는 모델이나 API 어디에도 저장되지 않습니다. 대화는 여러분의 코드 안에 존재합니다. 두 번째 턴이 첫 번째 턴에서 무슨 일이 있었는지 알아야 한다면, 여러분이 직접 누적되는 메시지 목록을 유지하고 그 전체를 다시 보내야 합니다. 주고받는 채팅이란 매번 점점 커지는 이력을 다시 보냄으로써 여러분이 만들어내는 환상입니다.
이것이 또한 긴 대화가 시간이 지날수록 메시지당 비용이 더 커지는 이유입니다. 매 호출마다 이전의 모든 턴을 입력 토큰으로 다시 보내므로, 사용자의 새 질문이 짧아도 프롬프트는 계속 커집니다.
history = [
{"role": "system", "content": "You are a friendly travel assistant. Keep answers short."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# 이력 전체가 매번 올라갑니다. 서버는 아무것도 기억하지 않습니다
response = client.chat.completions.create(model=MODEL, messages=history)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply}) # 다음 턴을 위해 응답을 보관
return reply
chat("I have a weekend in Lisbon. What should I see?")
chat("Which of those is best for kids?") # history가 첫 번째 턴을 함께 실어 나르기 때문에 가능합니다두 번째 질문이 말이 되는 것은 오직 첫 번째 질문과 그 답이 여전히 history에 남아 함께 전송되기 때문입니다. 여러분이 바로 그 기억입니다. 매번 독립적인 호출을 하기 전에 전체 컨텍스트를 조립하고 다시 보낸다는 이 하나의 개념이 대화의 밑바탕이 되며, 나중에는 도구 사용, 검색, 에이전트의 밑바탕도 됩니다. 이들은 모두 각각의 독립적인 호출 앞에 그 메시지 목록에 무엇을 담을지 결정하는 문제를 변형한 것일 뿐입니다.
문제가 생겼을 때
모델 호출은 남의 서버로 보내는 네트워크 호출이므로, 네트워크 호출이 실패하는 모든 방식으로 실패할 수 있습니다. 처음부터 이를 염두에 두고 설계하세요.
- 오류와 요청 제한(rate limit). 프로바이더는 일정 시간 안에 보낼 수 있는 요청 수를 제한합니다. 이 한도에 걸리면 호출은 rate-limit 오류로 실패합니다. 표준적인 대응은 기다렸다가 재시도하는 것이며, 실패할 때마다 대기 시간을 늘려가는 방식을 **백오프(backoff)**라고 부릅니다. 이렇게 해야 바쁜 서비스를 계속 두드리지 않게 됩니다.
- 타임아웃. 호출이 멈춰버릴 수 있습니다. 하나의 느린 요청이 앱 전체를 멈춰 세우지 않도록 타임아웃을 설정하세요.
- 키는 서버에만 두세요. API 키는 여러분의 돈을 쓰는 비밀번호입니다. API 키를 절대 브라우저 코드에 넣지 마세요. 누구나 개발자 도구를 열어 읽을 수 있습니다. 브라우저는 여러분의 백엔드와 대화하고, 그 키를 쥐고 있는 백엔드가 모델과 대화합니다.
- 어떤 답도 틀릴 수 있다고 가정하세요. 성공한 호출조차 환각이나 형식이 깨진 출력을 돌려줄 수 있으므로, 다음 장들은 그 결과를 여러분의 코드가 믿을 수 있는 것으로 만드는 방법을 다룹니다.
def ask_model(messages):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
timeout=20, # 20초 뒤 포기
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Sorry, something went wrong. Please try again."
