코드에서 모델 호출하기

지난 장에서는 메시지 목록을 작성했습니다. 이 장에서는 그 목록을 Python에서 실제 모델로 보내고 답변을 다시 받습니다. 그 왕복은 이 모델들을 사용하여 구축하는 가장 작은 완전한 루프이며, 작성할 거의 모든 기능이 그 위에 앉아 있습니다.
프롬프트를 입력하면, 코드가 어딘가로 보내고, 텍스트가 돌아옵니다. 모델이 프로그램에 살고 있는 것처럼 느껴집니다. 그렇지 않으며, 실제로 어디서 실행되는지 보면 비용, 속도, 그리고 만날 특이한 점들을 설명합니다.
그러면 물리적으로 무슨 일이 일어나고 있는지 상상해 봅시다. 모델은 공급자의 서버에 앉아 있는 엄청난 양의 매개변수 집합이며, 절대 볼 수 없는 하드웨어에 있습니다. 호출할 때, 코드는 메시지를 담은 일반적인 웹 요청을 인터넷을 통해 보냅니다.
공급자는 자신의 머신에서 예측 루프를 실행하고 생성된 토큰을 다시 보냅니다. 모델 호출은 다른 누군가의 컴퓨터에서 잠깐의 시간을 빌리는 것입니다. 이 프레이밍은 많은 것을 설명합니다: 지연은 모델이 자신의 끝에서 한 번에 하나의 토큰을 생성하기 때문이고, 비용은 그들이 그 계산을 청구하기 때문이며, 전체는 네트워크 호출이며 그것이 의미하는 모든 것입니다.
예제는 공식 OpenAI 라이브러리를 사용합니다. 다른 공급자는 세부 사항에서 다르지만, 형태는 거의 모든 곳에서 동일합니다: 메시지 목록을 보내고, 메시지를 받습니다. 그 동일성은 같은 코드가 호스팅하거나 다른 곳에서 임차하는 오픈 모델을 가리킬 수 있다는 보험이며, 종종 기본 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": "당신은 간결한 어시스턴트입니다. 한 문장으로 답하세요."},
{"role": "user", "content": "하늘은 왜 파란가요?"},
],
)두 가지가 필요합니다: model, 실행하려는 모델의 이름, 그리고 messages, 지난 장의 역할 태그가 지정된 메시지 목록. 나머지는 모두 기본값입니다.
모델 이름이 단일 상수에 있음을 주목하세요. 더 나은, 더 저렴한 모델이 몇 개월마다 나오는 분야에서는 의도적입니다. **모델 이름**을 한 곳에 유지하여 모델 교환이 한 줄 변경인지 확인하세요. 이것은 실제 변동성 규칙입니다: 변동성 있는 사양을 찾을 수 있는 곳에 유지하고, 코드 전체에 분산시키지 마세요.
model과 messages만 필요합니다. 모델 이름은 단일 상수에 속합니다. 몇 개월마다 더 저렴하거나 더 나은 것이 나타나기 때문에, 파일 전체에 하드코딩하면 교환이 사냥이 됩니다. 저는 모델이 이름이 바뀌었는데 9개 파일에 붙여넣은 후 느리게 배웠습니다. 응답 읽기
회신이 어떤 구조로 감싸져 돌아옵니다. 원하는 텍스트는 하나의 경로에 있지만, 나머지 구조는 알 가치가 있으며, 무엇이 일어났는지 말해줍니다.
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" 절단을 트리거하므로, 여유를 남겨두세요. 추출과 분류를 위해, 매번 낮은 온도를 찾으세요. 스트리밍
기본적으로 전체 답변이 생성를 완료할 때까지 기다린 다음, 한 번에 도착합니다. 모델은 한 번에 하나씩 토큰을 생성하므로, 긴 회신은 아무것도 하지 않고 바라보는 긴 대기를 의미합니다.
**스트리밍**은 각 토큰을 생성하는 순간 즉시 당신에게 보내므로, 텍스트는 즉시 나타나고 채팅 앱이 회신을 입력하는 것을 보여주는 방식으로 실시간으로 채워집니다.
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는 **상태 비저장**입니다: 각 요청은 완전히 독립적입니다. 공급자의 서버는 이전 호출을 기억하지 않습니다. 정확히 보낸 메시지에 대해 예측을 실행하고, 결과를 반환하며, 다음 시간을 위해 교환에 대해 아무것도 유지하지 않습니다.
그래서 대화는 어디에든 모델이나 API에 저장되지 않습니다. 코드에 살아 있습니다. 2번 차례가 1번 차례에서 무슨 일이 일어났는지 알기를 원하면, 메시지의 실행 목록을 직접 유지하고 매번 전체를 다시 보냅니다. 주고받기 채팅은 매 호출마다 계속 커지는 기록을 다시 보내는 것으로 생성하는 환상입니다.
그것도 왜 긴 대화가 시간이 지나면서 메시지당 더 많은 비용을 드는지입니다: 각 호출은 모든 이전 차례를 입력 토큰으로 다시 보내므로, 프롬프트는 사용자의 새 질문이 짧을 때도 계속 커집니다.
history = [
{"role": "system", "content": "당신은 친근한 여행 어시스턴트입니다. 답변을 짧게 유지하세요."},
]
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("저는 리스본에서 주말이 있습니다. 뭘 봐야 하나요?")
chat("그 중 아이들에게 가장 좋은 것은?") # 기록이 1번 차례를 전달하기 때문에만 작동합니다두 번째 질문은 첫 번째 질문과 그 답변이 여전히 history에 있고 함께 보내지기 때문에만 의미가 있습니다. 당신은 메모리입니다. 이 한 가지 아이디어, 매 독립 호출 이전에 전체 컨텍스트를 어셈블하고 다시 보낸다는 것이 대화의 기본이며, 나중에 이것은 도구 사용, 검색, 에이전트의 기본이기도 합니다. 그들은 모두 각 독립 호출 이전에 메시지 목록에 무엇을 들어갈지 결정하는 변형입니다.
뭔가 잘못되었을 때
모델 호출은 다른 누군가의 서버로의 네트워크 호출이므로, 네트워크 호출이 실패하는 모든 방법으로 실패합니다. 처음부터 이를 위해 설계하세요.
- 오류와 속도 제한. 공급자는 윈도우에 보낼 수 있는 요청 수를 제한합니다. 상한선을 치면 호출이 속도 제한 오류로 실패합니다. 표준 응답은 기다리고 재시도하는 것이고, 각 실패 후 대기를 길게 합니다 (이것을 **백오프**라고 부릅니다) 그래서 바쁜 서비스를 망치지 않습니다.
- 타임아웃. 호출이 매달릴 수 있습니다. 타임아웃을 설정하여 하나의 느린 요청이 앱을 동결하지 않도록 하세요.
- 키는 서버에 남아 있습니다. 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 "죄송합니다, 뭔가 잘못되었습니다. 다시 시도하세요."
