2장. 개발 환경 준비
실습 4 — ChatOpenAI 로 묻고 응답 읽기
실습 3과 같은 질문을 LangChain 의 ChatOpenAI 로 보내고, 응답 객체 AIMessage 의 content, usage_metadata, response_metadata 를 읽습니다. 마지막으로 1장의 tiktoken 어림과 API 가 센 토큰 수를 견줍니다. 실습 3처럼 .env 의 키와 인터넷이 있어야 합니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.
1단계. 모델 만들기
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단계. 메시지 보내고 답 읽기
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단계. 토큰 수와 서버 정보 읽기
# 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 어림과 견주기
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.0 | 1장의 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 는 여기에 메시지마다 몇 개씩 더 붙입니다.