학습 자료

파이썬에서 모델을 부른다 — 그리고 왜 이 클라이언트인가


Article

앞 시간에 터미널에서 모델과 대화했다. 이제 코드에서 부른다.

오늘 여섯 시간 중 지금
  1. ① 모델을 얹는다끝남
  2. ② 파이썬에서 부른다지금 여기
  3. ③ 검색기와 붙인다다음
  4. ④~⑥ 고치고 잰다오후

1. 도구를 고르는 것이 오늘의 결정이다

Ollama 를 파이썬에서 부르는 방법이 여러 개인데, 그중 하나만 다음 주에 값을 한다.

세 가지 방법
방법생김새다음 주에
ollama 파이썬 패키지ollama.chat(...)상용 API 로 못 바꾼다. 코드를 다시 쓴다
requests 로 직접requests.post("...api/chat")직접 다 만들어야 한다
openai 패키지client.chat.completions.create(...)주소·열쇠·모델 세 줄만 바꾼다
Ollama 가 OpenAI 와 같은 모양의 창구를 따로 열어둔다. 그래서 셋째가 된다.

패키지 이름이 openai 라 헷갈리는데 OpenAI 회사 것을 쓰는 게 아니다. 요청을 주고받는 형식이 업계 표준처럼 굳은 것이고, Ollama 도 그 형식을 흉내 낸 창구를 열어둔다. 주소만 내 컴퓨터로 돌려놓으면 같은 코드가 로컬 모델에게 간다.

python -m pip install openai

2. 가장 짧은 호출

18_first_call.py
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",   # 내 컴퓨터의 Ollama
    api_key="ollama",                       # 로컬은 검사 안 한다. 아무 글자나
)

res = client.chat.completions.create(
    model="qwen2.5:3b",
    messages=[
        {"role": "user", "content": "파이썬에서 리스트와 튜플의 차이를 한 문장으로"},
    ],
)

print(res.choices[0].message.content)
터미널
  • 리스트는 변경 가능하고 튜플은 불변( immutable)한 파이썬의 데이터 구조입니다.
  • [6.0초]
세 줄만 보면 된다

나중에 바뀔 곳이 정확히 이 셋이다

base_url
어디로 보낼지
localhost:11434/v1
api_key
누구인지
로컬은 아무 값
model
무슨 모델
qwen2.5:3b

messages 가 목록인 이유는 원래 대화 전체를 넣는 자리이기 때문이다. 모델은 기억이 없어서, 이어서 대화하려면 지난 말을 매번 다시 넣어줘야 한다.

role 세 가지
role누구의 말쓰임
system지시하는 사람「근거 안에서만 답해라」 같은 규칙. 내일 주제
user사용자질문
assistant모델모델이 앞서 한 답. 대화를 이어갈 때 넣는다

3. 응답 안에 뭐가 들어 있나

19_response.py
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

res = client.chat.completions.create(
    model="qwen2.5:3b",
    messages=[{"role": "user", "content": "파이썬에서 리스트와 튜플의 차이를 한 문장으로"}],
)

print("model  :", res.model)
print("finish :", res.choices[0].finish_reason)
print("usage  :", res.usage.prompt_tokens, "/", res.usage.completion_tokens)
터미널
  • model : qwen2.5:3b
  • finish : stop
  • usage : 46 / 26
이 셋을 지금 알아둔다
왜 중요한가
finish_reason왜 멈췄나. stop 은 스스로 끝냄length잘린 것이다
prompt_tokens입력 토큰 수앞 시간에 본 그 프리필 부담
completion_tokens출력 토큰 수다음 주에 이게 곧 돈이다

finish_reasonlength 면 길이 제한에 걸려 문장 중간에서 끊긴 것이다. 그런데 에러가 아니라 정상 응답으로 오고, 화면에는 그럴듯하게 나오고 뒷부분만 없다. 내일 JSON 을 받을 때 이게 사고가 된다.

4. 글자가 오는 대로 받는다 — 스트리밍

터미널에서는 글자가 하나씩 찍혔는데 위 코드는 다 끝나야 나온다. 답이 길면 몇십 초를 빈 화면으로 기다린다.

20_stream.py
from openai import OpenAI
import time

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

t = time.perf_counter()
first = None

stream = client.chat.completions.create(
    model="qwen2.5:3b",
    messages=[{"role": "user", "content": "겹쳐서 자르는 이유를 두 문장으로"}],
    stream=True,                    # 이 한 줄이 전부다
)

for chunk in stream:
    piece = chunk.choices[0].delta.content or ""     # 없으면 빈 문자열
    if piece and first is None:
        first = time.perf_counter() - t
    print(piece, end="", flush=True)                 # 줄바꿈 없이 이어 찍는다

print()
print(f"첫 글자 {first:.1f}초 / 전체 {time.perf_counter() - t:.1f}초")
터미널
  • 두 가지 서로 겹치는 이미지를 자를 경우, 이를 통해 원하는 부분만 분리하여 작업
  • 효율을 높이는 것이 가능합니다. 동시에 각각의 작업은 진행되므로 전체 처리 시간을
  • 단축시킬 수 있습니다.
  • 첫 글자 0.2초 / 전체 0.7초

flush=True 를 빼면 스트리밍이 안 보인다. 파이썬이 글자를 모아뒀다 한꺼번에 내보내기 때문이다.

전체 시간이 줄어드는 것은 아니다. 첫 글자가 빨리 오니까 빠르게 느껴질 뿐이다. 앞 시간에 봤듯 조각을 많이 넣으면 첫 글자까지 20초가 넘는데, 그동안 빈 화면이면 멈춘 줄 안다.

5. 🔴 그런데 답이 이상하다

위 출력을 다시 읽어보자. 질문은 「겹쳐서 자르는 이유」였다. 4강에서 배운 청킹 이야기다.

그런데 모델은 「두 가지 서로 겹치는 이미지를 자를 경우」로 시작해서 이미지 편집 이야기를 했다. 완전히 다른 분야로 알아들었다.

모델이 멍청해서가 아니다. 모델은 우리 문서를 모른다. 「겹쳐서 자른다」만 던지면 세상의 온갖 자르기 중에서 아무거나 고른다. 그리고 모르겠다고 하지 않는다 — 위 답변은 문법도 맞고 말도 되지만 우리 질문과 아무 상관이 없다.

우리한테는 264편이 있다. 9,005조각으로 잘라서 검색까지 되게 만들어뒀다. 그 조각을 같이 넣어주면 된다.

6. 없는 모델을 부르면

에러를 한 번 내보고 간다. 오타는 반드시 난다.

client.chat.completions.create(
    model="qwen2.5:3b-없는이름",
    messages=[{"role": "user", "content": "안녕"}],
)
터미널
  • BadRequestError
  • Error code: 400 - {'error': {'message': 'invalid model name', ...}}

400 은 「네가 잘못 보냈다」는 뜻이다. 모델 이름을 ollama list 에 나온 그대로 적었는지 확인한다. 번호는 지금 익혀두면 다음 주가 편하다 — 401 은 열쇠가 틀린 것이라 로컬에서는 안 나고 다음 주에 나며, 429 는 너무 자주 보낸 것이고, 500 대는 저쪽 문제라 이것만 다시 시도할 값어치가 있다.

이번 편에 나온 것

정리
쓴 것하는 일
pip install openai클라이언트를 깐다
OpenAI(base_url=..., api_key=...)어디로 보낼지 정한다
client.chat.completions.create(...)물어본다
messages=[{role, content}]대화 전체를 넣는 자리
res.choices[0].message.content답 글자를 꺼낸다
res.choices[0].finish_reasonstop 이면 정상, length잘렸다
res.usage.completion_tokens출력 토큰. 다음 주엔 이게 돈이다
stream=True글자가 오는 대로 받는다
chunk.choices[0].delta.content조각 하나의 글자
print(..., end="", flush=True)모아두지 말고 바로 찍는다

다음 편에서 검색해둔 조각을 프롬프트에 같이 넣는다.

Share
  • 파이썬
  • AI
  • LLM
  • Ollama
  • OpenAI