포스트

2026-03-10 강의 정리

OT + 아이스브레이킹 + [프로젝트1] 안내

2026-03-10 강의 정리

목차

  1. 문제와 접근
  2. 환경 설정 & 라이브러리 import
  3. OpenAI Chat Completions API 기본 사용법
  4. Response 객체 구조 상세 해부
  5. 역할(Role) 구분 - system / user / assistant
  6. Persona 설정 - system 메시지 활용
  7. temperature - 창의성 조절 파라미터
  8. LangChain ChatOpenAI 소개
  9. 핵심 개념 총정리
  10. 자주 나오는 실수 / 주의사항
  11. 호출 경계를 먼저 고정하기

1. 문제와 접근

LLM 연동의 출발점은 응답을 한 번 받는 데서 끝나지 않는다. 요청에 어떤 설정을 전달했고 응답에서 무엇을 꺼내 쓰는지 구분해야, 모델이나 호출 방식을 바꿔도 문제를 추적할 수 있다. OpenAI SDK로 호출 경계를 확인한 뒤 같은 흐름을 LangChain으로 옮겨 본다.

오늘 배우는 것

1
2
3
4
5
6
7
8
9
10
11
  .env 파일로 API 키 관리
      ↓
  OpenAI 클라이언트 초기화
      ↓
  chat.completions.create() 호출
      ↓
  Response 객체에서 텍스트 추출
      ↓
  system/user/assistant 역할 이해
      ↓
  LangChain ChatOpenAI 첫 사용

핵심 질문

  • API를 왜 직접 만들지 않고 OpenAI/LangChain을 쓰나? → 이미 훈련된 초거대 모델을 API 형태로 호출해서 기능을 빌려 쓰는 것

2. 환경 설정 & 라이브러리 import

필요 패키지

1
# pip install openai langchain-openai langchain-core python-dotenv
  • openai : OpenAI API 직접 호출
  • langchain-openai : LangChain의 OpenAI 연동 래퍼
  • langchain-core : 프롬프트 템플릿, 출력 파서 등 핵심 기능
  • python-dotenv : .env 파일에서 환경변수 로드

.env 파일 만들기

1
2
# 프로젝트 루트에 .env 파일 생성 (절대 Git에 올리면 안 됨!)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx

초기화 코드

1
2
3
4
5
6
7
8
9
10
11
import os
from openai import OpenAI
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()  # .env 파일 로드 → 환경변수로 등록

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
llm    = ChatOpenAI(model="gpt-4o-mini", temperature=0)
  • load_dotenv() : .env 파일을 읽어서 os.environ에 등록
  • os.getenv(“OPENAI_API_KEY”) : 환경변수에서 API 키 읽기
  • temperature=0 : 응답의 변동성을 줄이는 설정. 같은 입력이어도 항상 동일한 응답을 보장하지는 않음

API 키 확인

1
2
print(os.getenv("OPENAI_API_KEY"))
# → sk-proj-xxxxxxxx...  (None이 나오면 .env 파일 위치/내용 확인 필요)
  • None이 나오는 경우:
    • .env 파일을 찾지 못했거나 키 이름이 OPENAI_API_KEY와 다른 경우
    • 값이 없는 OPENAI_API_KEY만 적고 =값을 생략한 경우

python-dotenv는 키와 = 주변 공백을 허용한다. OPENAI_API_KEY = sk-xxx도 읽을 수 있다. 파일을 찾지 못하면 load_dotenv(dotenv_path="실제/.env/경로")로 경로를 명시한다. (공식 파일 형식)


3. OpenAI Chat Completions API 기본 사용법

가장 기본적인 호출

1
2
3
4
5
6
7
8
9
response = client.chat.completions.create(
    model    = "gpt-4o-mini",
    messages = [
        {
            "role"   : "user",
            "content": "안녕하세요, 자기소개 부탁드립니다"
        }
    ]
)
  • client.chat.completions.create() : OpenAI Chat API 호출
  • model : 사용할 모델 이름 (gpt-4o-mini, gpt-4o 등)
  • messages : 대화 내용을 리스트 형태로 전달

3가지 다른 질문 한번에 해보기

1
2
3
4
5
6
7
8
9
questions = ['파이썬이 뭐야?', 'llm이 뭐야?', 'api가 뭐야?']
for q in questions:
    response = client.chat.completions.create(
        model    = "gpt-4o-mini",
        messages = [{"role": "user", "content": q}]
    )
    print(f"Q: {q}")
    print(f"A: {response.choices[0].message.content}")
    print()

4. Response 객체 구조 상세 해부

화면에 표시할 본문과 호출 결과를 진단할 정보는 용도가 다르다. 사용자에게는 답변 텍스트를 전달하더라도, 개발 단계에서는 종료 사유와 토큰 사용량을 함께 살펴야 잘린 응답이나 예상 밖의 입력 증가를 구분할 수 있다.

Response 전체 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  ChatCompletion
  ├── id            : 'chatcmpl-DHr7Q...'   (요청 고유 ID)
  ├── model         : 'gpt-4o-mini'
  ├── choices       : [Choice]              ← 여기서 응답 텍스트 꺼냄
  │   └── [0]       : Choice
  │       ├── finish_reason : 'stop'        (정상 종료)
  │       ├── index         : 0
  │       └── message       : ChatCompletionMessage
  │           ├── role      : 'assistant'
  │           └── content   : '안녕하세요! 저는 OpenAI...'   ← 실제 텍스트
  └── usage
      ├── prompt_tokens     : 입력 토큰 수
      ├── completion_tokens : 출력 토큰 수
      └── total_tokens      : 전체 토큰 수

응답 텍스트 추출 - 단계별로 파헤치기

1
2
3
4
response.choices           # [Choice(...)]  ← 리스트
response.choices[0]        # Choice(...)    ← 첫 번째 응답 선택
response.choices[0].message         # ChatCompletionMessage(...)
response.choices[0].message.content # '안녕하세요! 저는 OpenAI...'  ← 최종 텍스트
  • choices가 리스트인 이유:
    • n 파라미터로 여러 응답을 동시에 받을 수 있음
    • n=3 이면 choices[0], choices[1], choices[2] 로 3개 응답
    • 기본값은 n=1 → choices[0] 만 사용

finish_reason 의미

1
2
3
response.choices[0].finish_reason
# 'stop'   → 정상 종료 (모델이 자연스럽게 응답 완료)
# 'length' → max_tokens 한도에 잘려서 종료 (응답이 잘렸을 수 있음!)

5. 역할(Role) 구분 - system / user / assistant

메시지 역할 3종류

role설명언제 쓰나?
systemAI의 성격, 규칙, 제약을 사전 정의대화 시작 시 1번
user사용자의 입력 메시지매 질문마다
assistantAI의 응답 (대화 기록용)멀티턴 대화 구성 시

실제 messages 배열 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
messages = [
    {
        "role"   : "system",
        "content": "당신은 IT 상담원입니다"
    },
    {
        "role"   : "user",
        "content": "비밀번호를 잊어버렸어요"
    },
    {
        "role"   : "assistant",
        "content": "비밀번호 재설정 방법을 안내드리겠습니다"
    }
]

# 대화 내용 출력
for msg in messages:
    print(f"[{msg['role']}] {msg['content']}")
# [system] 당신은 IT 상담원입니다
# [user] 비밀번호를 잊어버렸어요
# [assistant] 비밀번호 재설정 방법을 안내드리겠습니다
  • system 메시지는 보통 messages 배열 맨 앞에 하나 넣는다
  • user → assistant → user → assistant … 순서로 대화가 이어짐

6. Persona 설정 - system 메시지 활용

기본 질문과 Persona 설정 질문 비교

Persona 없이 질문: “1+1은?” → “1+1은 2입니다.”

Persona 설정 후: system: “당신은 초등학생을 가르치는 친절한 수학선생님입니다. 항상 단계별로 설명해주세요” “1+1은?” → “좋아요! 1 더하기 1을 같이 계산해봅시다. 1단계: 숫자 1이 하나 있습니다. 2단계: 또 다른 숫자 1을 더해볼게요…”

Persona 설정 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
response = client.chat.completions.create(
    model    = "gpt-4o-mini",
    messages = [
        {
            "role"   : "system",
            "content": "당신은 초등학생을 가르치는 친절한 수학선생님입니다. "
                       "항상 단계별로 설명해주세요"
        },
        {
            "role"   : "user",
            "content": "1+1은?"
        }
    ]
)
print(response.choices[0].message.content)

다양한 Persona 예시

1
2
3
4
5
# 수의사 Persona
"당신은 10년차 베테랑 수의사입니다. 의학적 관점에서 전문적으로 답변해주세요"

# 데이터 과학자 Persona
"당신은 MLOps 리더입니다. 실무 팁 위주로 두 문장 이하로 답변해주세요"
  • Persona의 효과:
    • 말투, 전문 분야, 답변 스타일을 지정할 수 있음
    • 같은 질문이라도 전혀 다른 수준과 관점으로 답변
    • 실무에서는 “고객 서비스 담당자”, “법률 자문” 등으로 활용

7. temperature - 창의성 조절 파라미터

temperature 개념

1
2
3
  temperature = 0.0  →  변동성을 줄임 (테스트에 유용하지만 동일 응답 보장은 아님)
  temperature = 0.7  →  약간의 다양성 (일반적인 챗봇에 적합)
  temperature = 2.0  →  매우 창의적이고 다양한 답변 (예측 불가)

수학적 원리 - Softmax + Temperature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np

def calculate_softmax(logits, T):
    if T <= 0:
        raise ValueError("이 Softmax 예제에서는 T > 0이어야 합니다.")
    z = np.array(logits)
    e_z = np.exp(z / T - np.max(z / T))
    return e_z / e_z.sum()

logits      = [2.0, 1.5, 1.0, 0.5]   # 각 토큰의 점수(logit)
temperature = [0.1, 0.5, 1.0, 2.0, 10.0]

for T in temperature:
    probs = calculate_softmax(logits, T)
    print(f"T={T}: {np.round(probs, 3)}")

결과 해석

1
2
3
4
  일부 결과를 소수 셋째 자리까지 반올림:
  T=0.1  → [0.993, 0.007, 0.000, 0.000] ← 1등 토큰에 확률이 집중
  T=1.0  → [0.455, 0.276, 0.167, 0.102] ← 확률이 더 분산
  T=10.0 → [0.269, 0.256, 0.243, 0.232] ← 균등 분포에 가까워짐
  • 양수 T가 작을수록 = 확률이 높은 토큰이 선택될 가능성이 커짐 (다른 토큰의 확률이 반드시 0이 되는 것은 아님)
  • T가 클수록 = 낮은 확률 단어도 선택 가능 (다양하고 창의적인 응답)

위 수식은 T > 0인 경우를 보여준다. 출력의 0.000은 반올림한 값이다. API의 temperature=0을 이 수식에 그대로 대입하면 0으로 나누게 된다. 또한 API 응답의 재현성에는 모델과 서버 구성도 영향을 주므로, 낮은 temperature만으로 완전한 동일성을 보장할 수 없다. (OpenAI 공식 설명)

temperature별 같은 질문 결과 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
question = "인공지능의 미래에 대해 한 문장으로 말해주세요"

for temp in [0.0, 1.5]:
    for trial in range(1, 4):
        response = client.chat.completions.create(
            model='gpt-4o-mini',
            messages=[{'role': 'user', 'content': question}],
            temperature=temp,
        )
        print(f"temperature {temp}, 실행 {trial}: {response.choices[0].message.content}")

# 총 6번 호출한 응답을 비교한다.
# 낮은 temperature에서는 비슷한 응답이 나오는 경향이 있지만 동일 여부는 직접 확인한다.
# 높은 temperature도 매번 다른 응답을 보장하는 것은 아니다.

8. LangChain ChatOpenAI 소개

래퍼를 도입할지는 호출 코드가 짧아지는지만으로 결정하지 않는다. 프롬프트·파서·검색기를 같은 인터페이스로 조합할 필요가 있는지 보고, 장애가 발생했을 때 실제 모델 요청까지 확인할 수 있는 구조를 유지한다.

OpenAI 직접 호출 vs LangChain 래퍼 비교

1
2
3
4
5
6
7
8
  OpenAI 직접:
    client.chat.completions.create(model=..., messages=[...])
    → response.choices[0].message.content

  LangChain ChatOpenAI:
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    response = llm.invoke("질문")
    → response.content

LangChain이 편한 이유

  • 파이프(|) 연산자로 prompt | llm | parser 체인 구성 가능
  • 다양한 LLM(Anthropic, Google 등)을 같은 인터페이스로 교체 가능
  • 프롬프트 템플릿, 출력 파서 등 풍부한 기능 제공

첫 LangChain 호출

1
2
3
4
5
6
7
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke("LangChain을 한 문장으로 설명해주세요")

print(type(response))    # AIMessage
print(response.content)  # 'LangChain은 LLM 기반 애플리케이션 개발을 위한...'
  • llm.invoke() 반환값은 AIMessage 객체
  • .content 속성으로 텍스트 추출

9. 핵심 개념 총정리

오늘 배운 것 한눈에 보기

개념핵심 내용
.env 파일API 키를 코드에 직접 쓰지 말고 환경변수로 관리
chat.completionsOpenAI LLM API 호출 메서드
Response 구조choices[0].message.content 로 텍스트 추출
role: systemAI의 성격/역할을 사전 정의
role: user사용자 입력
role: assistantAI 응답 (멀티턴 대화에서 히스토리로 사용)
temperature낮을수록 일관성, 높을수록 창의성
LangChainllm.invoke()로 더 간결하게 LLM 호출

choices가 리스트인 이유 다시 정리

1
2
3
4
5
6
7
8
9
# n 파라미터로 여러 개 응답 가능
response = client.chat.completions.create(
    model    = "gpt-4o-mini",
    messages = [{"role": "user", "content": "안녕"}],
    n        = 3   # 3개 응답 요청
)
response.choices[0].message.content  # 첫 번째 응답
response.choices[1].message.content  # 두 번째 응답
response.choices[2].message.content  # 세 번째 응답

10. 자주 나오는 실수 / 주의사항

실수 1: API 키를 코드에 직접 작성

1
2
3
4
5
# 절대 하지 말 것!
client = OpenAI(api_key="sk-proj-xxxxxxxxxxxx")  # GitHub에 올리면 키 노출!

# 올바른 방법
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

실수 2: response.content 로 접근

1
2
3
4
5
6
7
8
9
# 틀린 코드 (AttributeError 발생)
print(response.content)

# 올바른 코드
print(response.choices[0].message.content)

# LangChain AIMessage는 .content 가능
response = llm.invoke("질문")
print(response.content)   # ← LangChain에서는 OK

실수 3: .env 파일 위치

1
2
3
4
5
6
7
# load_dotenv()는 현재 작업 디렉토리의 .env를 찾음
# 노트북 파일과 같은 폴더에 .env가 있어야 함

my_project/
├── notebook.ipynb
├── .env           ← 여기!
└── ...

실수 4: messages 형식 오류

1
2
3
4
5
# 틀린 코드 (리스트 안에 딕셔너리가 아닌 문자열)
messages = ["user: 안녕하세요"]  # TypeError!

# 올바른 코드
messages = [{"role": "user", "content": "안녕하세요"}]

[보충] OpenAI API 비용 구조

토큰 기반 과금

  • OpenAI API는 토큰(token) 단위로 과금
  • 입력(prompt) 토큰 + 출력(completion) 토큰 합산
  • gpt-4o-mini: input $0.15 / 1M tokens, output $0.6 / 1M tokens (저렴)
  • gpt-4o : input $2.5 / 1M tokens, output $10 / 1M tokens (고성능)

토큰 수 확인

1
2
3
response.usage.prompt_tokens      # 입력 토큰 수
response.usage.completion_tokens  # 출력 토큰 수
response.usage.total_tokens       # 전체 = 입력 + 출력
  • 토큰 수는 언어·문자열·토크나이저에 따라 달라지므로 실제 입력을 인코딩해 확인
  • max_tokens 파라미터로 응답 길이를 제한하면 비용 절감 가능

11. 호출 경계를 먼저 고정하기

이 단계에서 고정할 것은 특정 모델 이름보다 입력과 반환값의 형태다. API 호출을 작은 함수로 감싸고, 응답 객체를 다루는 코드는 그 안에 모으면 이후 UI나 체인을 연결할 때 변경 범위가 줄어든다.

다음 실험에서는 질문과 모델 설정을 함께 기록해 답변을 비교한다. 응답이 달라졌을 때 프롬프트 변경 때문인지 호출 조건 때문인지 구분할 수 있어야, 파라미터 조정도 의미 있는 실험이 된다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그