LangChain 챗봇2장 · 개발 환경 준비16 / 20← 이전목차다음 →MetaContents

2장. 개발 환경 준비

실습 4 — ChatOpenAI 로 묻고 응답 읽기

실습 3과 같은 질문을 LangChain 의 ChatOpenAI 로 보내고, 응답 객체 AIMessage 의 content, usage_metadata, response_metadata 를 읽습니다. 마지막으로 1장의 tiktoken 어림과 API 가 센 토큰 수를 견줍니다. 실습 3처럼 .env 의 키와 인터넷이 있어야 합니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.

1단계. 모델 만들기

Python
import os

from dotenv import find_dotenv, load_dotenv
from langchain_openai import ChatOpenAI

# 과목 폴더의 .env 에서 OPENAI_API_KEY 를 읽어 환경 변수에 넣는다
# find_dotenv(usecwd=True): 지금 폴더(data)에서 위로 올라가며 .env 를 찾는다
load_dotenv(find_dotenv(usecwd=True))
# 모델 이름도 .env 에서 바꿀 수 있게 하고, 없으면 gpt-4.1-mini 를 쓴다
MODEL = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")

# ChatOpenAI: OpenAI 의 대화 모델을 LangChain 의 꽂는 모양으로 감싼 것. 키는 환경 변수에서 읽는다
model = ChatOpenAI(model=MODEL, temperature=0)
print("모델:", model.model_name, "/ 온도:", model.temperature)
출력
모델: gpt-4.1-mini / 온도: 0.0

코드가 의미하는 것 ChatOpenAI(model=MODEL, temperature=0) 는 1장의 FakeListChatModel(...) 자리에 들어가는 한 줄입니다. 키는 실습 3처럼 환경 변수에서 스스로 읽으므로 코드에 적지 않습니다. 만들기만 하는 단계라 아직 요청은 나가지 않습니다.

2단계. 메시지 보내고 답 읽기

Python
from langchain_core.messages import HumanMessage, SystemMessage

# 1차시 실습 5와 같은 메시지 객체. 사전 대신 객체로 역할을 나타낸다
messages = [
    SystemMessage("너는 구름다리여행의 친절한 여행 비서다. 두 문장 이내로 답한다."),
    HumanMessage("제주도 2박 3일 여행의 첫날에 가 볼 곳을 하나 추천해 줘."),
]

# invoke: 1차시의 가짜 모델과 똑같이 부른다. 답은 AIMessage 객체로 온다
reply = model.invoke(messages)
print(type(reply).__name__)
print(reply.content)
출력
AIMessage
첫날에는 공항에서 가까운 용두암을 추천합니다. 바다를 보며 가볍게 걷기 좋아 도착한 날 일정으로 알맞습니다.

코드가 의미하는 것 model.invoke(messages) 는 1장 실습 5에서 가짜 모델을 부른 것과 같은 줄입니다. LangChain 이 메시지 객체를 OpenAI 가 읽는 사전 목록으로 바꿔 보내고, 받은 답을 AIMessage 로 돌려줍니다. 실습 3의 create() 와 같은 일을 하지만, 3장부터는 이 꼴로 모델을 부릅니다.

3단계. 토큰 수와 서버 정보 읽기

Python
# usage_metadata: LangChain 이 회사마다 다른 토큰 정보를 같은 이름으로 맞춰 둔 사전
usage = reply.usage_metadata
print({k: usage[k] for k in ["input_tokens", "output_tokens", "total_tokens"]})

# response_metadata: 서버가 알려 준 정보. get 은 없는 이름이면 None 을 돌려준다
meta = reply.response_metadata
print("답한 모델:", meta.get("model_name"))
print("끝난 까닭:", meta.get("finish_reason"))
출력
{'input_tokens': 60, 'output_tokens': 40, 'total_tokens': 100}
답한 모델: gpt-4.1-mini-2025-04-14
끝난 까닭: stop

코드가 의미하는 것 usage_metadata 는 회사마다 다른 토큰 칸 이름을 LangChain 이 input_tokens, output_tokens 로 맞춰 둔 것이라, 다른 회사의 모델로 바꿔도 요금을 세는 코드는 그대로 둘 수 있습니다. response_metadata 를 meta.get 으로 읽은 것은 칸 이름이 회사마다 조금씩 달라, 없는 칸이 있어도 멈추지 않게 하려는 것입니다.

4단계. tiktoken 어림과 견주기

Python
import tiktoken

# 1차시 실습 3과 같은 토큰 사전
enc = tiktoken.get_encoding("o200k_base")

# 보낸 메시지의 내용만 세어 더한 어림값
guess = sum(len(enc.encode(m.content)) for m in messages)
print("tiktoken 어림(내용만):", guess)
print("API 가 센 입력 토큰:", usage["input_tokens"])
# 차이는 API 가 메시지마다 붙이는 역할 표시 같은 토큰이다
print("차이:", usage["input_tokens"] - guess)
출력
tiktoken 어림(내용만): 50
API 가 센 입력 토큰: 60
차이: 10

코드가 의미하는 것 enc.encode 로 내 PC 에서 센 토큰과 API 가 센 토큰을 견주는 것은, 보내기 전에 요금을 어림하는 방법이 얼마나 맞는지 보려는 것입니다. API 는 메시지마다 역할 표시 같은 토큰을 더 붙이므로 내용만 센 어림이 조금 적게 나옵니다. 예상 출력의 차이 10 은 실제로 돌리면 달라질 수 있습니다.

이 실습이 뜻하는 것

이 실습은 1장에 모형 칼로 연습한 동작을 진짜 칼로 그대로 해 보는 일과 같습니다. 진짜 칼을 쥐고(1단계), 같은 동작으로 썰고(2단계), 썬 결과의 무게와 상표를 보고(3단계), 연습 때 눈대중한 무게와 견줍니다(4단계). 여기서 진짜 칼이 ChatOpenAI, 써는 동작이 invoke, 무게가 usage_metadata, 눈대중이 tiktoken 어림입니다. 비유와 다른 점은, 진짜 모델의 결과는 같은 동작에도 조금씩 달라질 수 있다는 것입니다.

단계출력뜻
1. 모델gpt-4.1-mini, 온도 0.01장의 FakeListChatModel(...) 줄 대신 이 줄이 들어갔습니다
2. 답AIMessage, 용두암 추천1장 실습 5와 같은 invoke 와 content 로 진짜 답을 읽었습니다
3. 토큰·정보입력 60, 출력 40, 모두 100 쯤, stop실습 3의 usage 와 같은 숫자를 LangChain 의 이름(input_tokens 등)으로 읽었습니다
4. 어림 견주기어림 50, API 60 쯤내용만 센 어림보다 API 의 숫자가 조금 큽니다. 역할 표시 같은 토큰이 붙기 때문입니다

한 줄로 줄이면, 1장의 가짜 모델 코드에서 모델을 만드는 줄 하나만 바꿔 진짜 답을 받은 실습입니다. 4단계의 어림 50 은 내 PC 에서 tiktoken 으로 센 값이라 언제나 같고, API 의 입력 토큰은 예상한 둥근 값입니다. 실제로 돌리면 50 보다 몇 개 많은 수가 나올 것입니다. 모델이 쓰는 답의 글과 출력 토큰 수는 실행할 때마다, 모델마다 조금씩 달라집니다. 요금을 어림할 때 tiktoken 은 "조금 적게 센다"고 보고 몇 할 더 잡아 둡니다.

코드 설명

  • ChatOpenAI(model=MODEL, temperature=0) 는 키를 받지 않습니다. OpenAI() 처럼 환경 변수 OPENAI_API_KEY 를 스스로 읽습니다.
  • model.invoke(messages) 는 SystemMessage, HumanMessage 목록을 받아 OpenAI 가 읽는 사전 목록으로 바꿔 보내고, 받은 답을 AIMessage 로 바꿔 돌려줍니다.
  • reply.usage_metadata 는 OpenAI 의 prompt_tokens, completion_tokens 를 input_tokens, output_tokens 라는 LangChain 의 이름으로 바꿔 담습니다. 다른 회사 모델을 써도 같은 이름으로 읽습니다.
  • meta.get("model_name") 처럼 get 으로 읽으면 그 이름이 없을 때 오류 대신 None 이 나옵니다. response_metadata 에 담기는 이름은 모델 회사마다 조금씩 다릅니다.
  • 4단계의 guess 는 1장 실습 5에서 보낸 토큰을 센 방법과 같습니다. 실제 API 는 여기에 메시지마다 몇 개씩 더 붙입니다.
← 이전 절2장 · 실습 3 — OpenAI 라이브러리로 첫 질문 보내기다음 절 →2장 · 실습 5 — 오류를 잡아 친절하게 알리기