6장. RAG 의 원리와 로컬 모델로 옮기기
외부 API 로 만드는 RAG
집에 주방이 없으면 음식을 배달시킵니다. 메뉴를 고르고 주소를 알려 주면 바깥 식당이 만들어 보내 줍니다. 편하지만 주문할 때마다 돈이 들고, 내 주소와 주문 내용이 식당에 전해집니다. 외부 API 로 LLM 을 쓰는 것이 이 배달입니다.
1번 내 PC 에서 검색과 프롬프트 만들기를 하고, 2번 API 키와 함께 요청을 인터넷으로 보냅니다. 3번 공급사의 서버가 큰 모델로 답을 만들고, 4번 그 답이 돌아옵니다. 2번에서 질문과 문서 조각이 모두 내 PC 밖으로 나간다는 것을 기억해 둡니다. 여기서 배달 식당이 API 서버, 주문서가 요청입니다.
외부 API 의 좋은 점과 아쉬운 점
| 견줄 것 | 내용 |
|---|---|
| 좋은 점 | 내 PC 에 GPU 가 없어도 큰 모델을 쓴다. 설치가 간단하다 |
| 비용 | 보낸 토큰과 받은 토큰 수만큼 돈을 낸다. 요금은 공급사의 요금표를 본다 |
| 개인정보 | 질문과 문서가 공급사 서버로 간다. 사내 규정상 보내면 안 되는 문서가 있다 |
| 끊김 | 인터넷이나 공급사 서버가 멈추면 같이 멈춘다 |
| 바뀜 | 공급사가 모델을 바꾸거나 없애면 같은 질문에 다른 답이 나올 수 있다 |
코드의 생김새
아래는 OpenAI(ChatGPT 를 만든 회사)의 파이썬 라이브러리 openai 로 외부 API 를 부르는 코드입니다. 실습 2의 조항 나누기, 실습 3의 키워드 검색, 「근거를 붙인 프롬프트」의 build_messages 를 한 파일에 모았습니다. 그래서 이 파일 하나만 복사해 실행해도 됩니다.
API 키는 코드에 적지 않고, PowerShell 에서 환경 변수로 넣어 둡니다. 창을 닫아도 남게 하려면 아래 ".env 파일에 키를 두고 쓰기"처럼 파일에 적어 둘 수도 있습니다. 코드의 load_dotenv() 는 .env 파일이 없으면 아무 일도 하지 않으므로, 어느 방법으로 넣어도 같은 코드가 돌아갑니다.
pip install openai python-dotenv scikit-learn # API 호출, .env 읽기, 키워드 검색에 쓰는 패키지
$env:OPENAI_API_KEY = "발급받은_인증키" # 이 창에서만 살아 있는 환경 변수로 키를 넣는다진짜 키가 있어야 돌아가므로 교안을 만든 PC 에서는 OpenAI 서버로 보내지 못했습니다. 이 PC 에서 실행하지 않은 안내 코드입니다. 다만 같은 코드를 주소만 Ollama 로 바꿔(아래 "Ollama 로 미리 시험하기") 끝까지 돌아가는 것은 확인했습니다. shop_policy.txt 는 실습 자료의 data 폴더에 있습니다. 이 파일과 같은 폴더에 복사하거나 DATA_DIR 을 그 폴더로 바꿉니다.
# 6장 「외부 API 로 만드는 RAG」: 검색 + 프롬프트 + 외부 API 를 한 파일에
# 필요한 것: pip install openai python-dotenv scikit-learn
# 이 파일과 같은 폴더(또는 DATA_DIR)에 shop_policy.txt
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# shop_policy.txt 가 있는 폴더. 교안의 data 폴더에 있다면 그 경로로 바꾼다
DATA_DIR = Path(__file__).parent
# 1. 문서를 조항 단위로 나눈다 (실습 2와 같다)
text = (DATA_DIR / "shop_policy.txt").read_text(encoding="utf-8")
chunks = [c.strip() for c in text.split("\n\n")[1:]] # 맨 앞의 제목 줄은 뺀다
# 2. 키워드 검색을 만든다 (실습 3과 같다)
vec = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3))
X = vec.fit_transform(chunks)
def search(question, k=3):
"""질문과 비슷한 조항 k 개의 (번호, 점수)를 돌려준다"""
scores = cosine_similarity(vec.transform([question]), X)[0]
top = scores.argsort()[::-1][:k]
return [(int(i), round(float(scores[i]), 3)) for i in top]
# 3. 근거를 붙인 프롬프트를 만든다 (「근거를 붙인 프롬프트」 절과 같다)
SYSTEM = ("당신은 쇼핑몰 상담원입니다. 아래 [문서]에 적힌 내용만 근거로 짧게 답하세요. "
"문서에 답이 없으면 '문서에서 찾지 못했습니다.'라고만 답하세요.")
def build_messages(question, found):
context = "\n".join(found)
user = f"[문서]\n{context}\n\n[질문]\n{question}"
return [{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}]
# 4. 외부 API 에 묻는다
load_dotenv() # .env 가 있으면 OPENAI_API_KEY 를 읽는다
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) # 키는 환경 변수에서. 코드에 적지 않는다
MODEL = os.environ.get("OPENAI_MODEL", "gpt-6-luna") # 쓸 모델 이름. 환경 변수로 바꿀 수 있다
question = "휴대폰으로 결제하면 한도가 있나요?"
found = [chunks[i] for i, _ in search(question)] # 질문과 가까운 조항 셋
print("찾은 조항:", [c[:c.index(")") + 1] for c in found])
resp = client.chat.completions.create(
model=MODEL, # 어느 모델에게 물을지
messages=build_messages(question, found), # 시스템 지시 + [문서] + [질문]
# temperature 는 주지 않는다. GPT 추론 모델은 0 을 받지 않는다(아래 설명)
)
print(resp.choices[0].message.content) # 모델의 답
print(resp.usage) # 보낸 토큰과 받은 토큰 수아래는 예상 출력(실행하지 않고 유추한 예시)입니다. 찾은 조항 줄은 이 PC 에서 실제로 나온 것이고, 답의 문장과 토큰 수는 모델에 따라 다릅니다.
찾은 조항: ['제14조(결제 수단)', '제15조(무이자 할부)', '제3조(출고 마감)']
휴대폰 결제는 한 달에 50만원까지 할 수 있습니다.
CompletionUsage(completion_tokens=20, prompt_tokens=250, total_tokens=270, …)| 코드 | 쉬운 뜻 |
|---|---|
DATA_DIR = Path(__file__).parent | __file__ 은 지금 실행하는 파일의 경로다. 그 파일이 있는 폴더에서 shop_policy.txt 를 찾는다 |
OpenAI(api_key=...) | OpenAI 서버에 요청을 보낼 손님 창구(client)를 만든다. base_url 을 주지 않으면 OpenAI 의 주소로 간다 |
os.environ.get("OPENAI_MODEL", "gpt-6-luna") | 환경 변수 OPENAI_MODEL 이 있으면 그 이름을, 없으면 gpt-6-luna 를 쓴다. 코드를 고치지 않고 모델을 바꿀 수 있다 |
client.chat.completions.create(...) | 메시지 목록을 보내고 답을 받는다. chat completion 은 "대화를 이어 써 달라"는 요청이라는 뜻이다 |
resp.choices[0].message.content | 돌아온 답 후보 가운데 첫째의 글. 보통 후보는 하나다 |
resp.usage | 이번 요청에서 보낸 토큰(prompt_tokens)과 받은 토큰(completion_tokens)의 수. 요금이 이 수로 정해진다 |
프롬프트에 붙인 문서 조각도 보낸 토큰에 들어갑니다. 조각을 많이 붙일수록 비용이 늡니다. search(question) 의 k 를 3에서 10으로 늘리면 보낸 토큰도 그만큼 커집니다.
temperature 를 넣지 않는 까닭
실습 5, 6의 Ollama 코드는 temperature=0 을 주어 다시 물어도 같은 답이 나오게 했습니다. 그런데 같은 줄을 gpt-6-luna 에 보내면 아래 오류가 납니다. 수강생이 이 코드를 진짜 키로 실행해 받은 오류입니다.
openai.BadRequestError: Error code: 400 - Unsupported value: 'temperature' does not support 0 with this model. Only the default (1) value is supported."이 모델은 temperature 를 0 으로 줄 수 없고, 기본값 1 만 쓸 수 있다"는 뜻입니다. 요즘 GPT 모델은 답하기 전에 속으로 먼저 따져 보는 추론(reasoning) 모델이라, 온도를 사용자가 바꾸지 못하게 막아 두었습니다. 그래서 완성본에서는 temperature 를 아예 넣지 않았습니다.
| 견줄 것 | Ollama 의 Qwen(실습 5, 6) | GPT 추론 모델(gpt-6-luna 등) |
|---|---|---|
temperature=0 | 된다. 가장 그럴듯한 낱말만 고른다 | 400 오류. 기본값 1 만 된다 |
| 다시 물으면 | 거의 같은 답이 나온다 | 문장이 조금씩 달라질 수 있다 |
| 답의 내용 | 붙인 조항에서 온다 | 붙인 조항에서 오므로 크게 흔들리지 않는다 |
400 은 HTTP 상태 코드로 "요청이 잘못됐다(Bad Request)"는 뜻입니다. 키가 틀렸을 때는 401, 모델 이름이 없을 때는 404 가 옵니다. 400 이 왔다는 것은 키와 모델 이름은 맞았고 인자 하나가 문제였다는 신호입니다.
모델 이름 고르기
model= 에는 공급사가 정한 모델 이름(model ID)을 글자 그대로 적습니다. GPT 는 Generative Pre-trained Transformer(미리 학습한, 글을 만들어 내는 트랜스포머)의 머리글자로, OpenAI 모델 이름의 앞머리입니다. 아래는 2026년 11월에 OpenAI 의 공식 모델 문서에서 확인한 이름입니다. 넷 모두 이 코드가 쓰는 chat.completions.create 로 부를 수 있습니다.
model= 에 적는 이름 | 공식 문서의 설명 | 이 예시에 |
|---|---|---|
gpt-6-luna | 가장 효율적인 모델. 많은 요청을 빠르고 싸게 처리하는 단순한 일에 맞는다 | 권장. 근거가 프롬프트에 붙어 있어 큰 모델이 아니어도 된다 |
gpt-6.1-terra | 빠르고 효율적인 중간 모델. 자연스러운 글쓰기에 강하다 | 답의 문장을 더 다듬고 싶을 때 |
gpt-6.1-sol | 가장 큰 모델에 가까운 성능을 더 낮은 값으로 | 이 예시에는 과하다 |
gpt-6-astra | 가장 뛰어나고 가장 비싼 모델. 어렵고 복잡한 일에 쓴다 | 이 예시에는 과하다 |
RAG 는 답의 근거를 검색으로 찾아 프롬프트에 붙여 주므로, 모델이 스스로 많이 알 필요가 적습니다. 그래서 작고 싼 모델부터 써 보고, 답이 모자랄 때만 큰 모델로 올립니다. luna, terra, sol, astra 는 라틴어로 달, 땅, 해, 별이라는 낱말입니다. 이름만 보고 크기를 짐작하지 말고 공식 문서의 설명으로 고릅니다.
모델 이름과 값은 자주 바뀝니다. 위 표의 이름이 없어졌거나 내 계정에서 쓸 수 없으면 NotFoundError(model_not_found)가 납니다. 그럴 때는 내 키로 부를 수 있는 모델 목록을 직접 확인합니다. 이것도 진짜 키가 필요해 실행하지 않은 안내 코드입니다.
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI() # api_key 를 주지 않으면 OPENAI_API_KEY 를 스스로 찾는다
print(sorted(m.id for m in client.models.list())) # 내 키로 쓸 수 있는 모델 이름 목록목록에서 고른 이름은 코드를 고치지 않고 환경 변수로 바꿔 넣을 수 있습니다.
$env:OPENAI_MODEL = "gpt-6.1-terra" # 다음 실행부터 이 모델에게 묻는다(이 창에서만)| 알아 둘 것 | 내용 |
|---|---|
| ChatGPT 구독과 API 는 따로다 | ChatGPT 유료 구독이 있어도 API 요금은 따로 낸다. 키는 platform.openai.com 에서 만들고 결제 수단도 따로 등록한다 |
| 요금 | 모델마다 100만 토큰당 값이 다르고 자주 바뀐다. 쓰기 전에 공식 요금표를 본다 |
| 도구 호출 | 9장의 도구 호출(function calling)을 OpenAI 모델로 하려면, 모델에 따라 Chat Completions 대신 Responses API 를 써야 한다고 공식 문서에 적혀 있다 |
| 공식 문서 | developers.openai.com/api/docs/models |
Ollama 로 미리 시험하기
키를 만들기 전에 코드가 맞게 짜였는지 보고 싶으면, 같은 코드를 Ollama 로 돌려 봅니다. openai 라이브러리는 OPENAI_BASE_URL 환경 변수가 있으면 그 주소로 요청을 보냅니다. 코드는 한 글자도 고치지 않습니다. 이 시험을 한 PowerShell 창은 진짜 키로 돌리기 전에 닫습니다(아래 "진짜 키로 돌릴 때는 새 PowerShell 창에서").
$env:OPENAI_BASE_URL = "http://localhost:11434/v1" # 요청을 OpenAI 대신 내 PC 의 Ollama 로 보낸다
$env:OPENAI_API_KEY = "ollama" # Ollama 는 키를 보지 않는다. 칸만 채운다
$env:OPENAI_MODEL = "qwen2.5:3b" # Ollama 에 받아 둔 모델 이름
python 06_외부API로만드는RAG.py # 위 코드를 저장한 파일을 실행한다교안을 만든 PC 에서 이렇게 돌린 결과는 아래와 같습니다. 이것은 실제로 실행한 출력입니다(토큰 줄은 길어서 앞부분만 적었습니다).
찾은 조항: ['제14조(결제 수단)', '제15조(무이자 할부)', '제3조(출고 마감)']
휴대폰으로 결제할 때 한 달에 50만원까지 이용할 수 있습니다.
CompletionUsage(completion_tokens=24, prompt_tokens=240, total_tokens=264, …)temperature 를 넣지 않았으므로 같은 명령을 한 번 더 돌리면 "한 달에"가 "한달에"로 바뀌는 것처럼 문장이 조금씩 달라졌습니다. 답의 내용(한 달 50만원)은 두 번 모두 같았습니다.
Ollama 는 6장 「Ollama 로 로컬 모델 띄우기」에서 설치합니다.
진짜 키로 돌릴 때는 새 PowerShell 창에서
Ollama 로 시험한 창에서 바로 진짜 키로 돌리면 오류가 나거나, 오류 없이 엉뚱한 곳에 묻게 됩니다. 시험할 때 넣은 환경 변수 셋이 그 창에 그대로 남아 있기 때문입니다. .env 에 진짜 키를 적어도 소용이 없습니다. load_dotenv() 는 이미 있는 환경 변수를 덮어쓰지 않으므로(아래 ".env 파일에 키를 두고 쓰기"), 창에 남은 시험용 값이 이깁니다.
| 남은 환경 변수 | 진짜 키로 돌렸을 때 |
|---|---|
OPENAI_BASE_URL | 요청이 OpenAI 가 아니라 계속 Ollama 로 간다. 오류가 나지 않아 알아채기 어렵다 |
OPENAI_API_KEY = "ollama" | 주소만 지웠다면 OpenAI 가 가짜 키를 거절해 401 오류가 난다 |
OPENAI_MODEL = "qwen2.5:3b" | OpenAI 에는 그 이름의 모델이 없어 404 오류가 난다 |
그래서 진짜 키로 돌릴 때는 PowerShell 창을 새로 열어 실행합니다. $env: 로 넣은 값은 그 창에만 있으므로, 새 창에는 셋 모두 없습니다. 이 방법이 가장 확실합니다. 새 창에서는 가상환경도 다시 켜야 합니다.
cd C:\llm-course # cd = change directory. 코드가 있는 폴더로 간다(내 폴더 이름으로 바꾼다)
.venv\Scripts\Activate.ps1 # 가상환경을 다시 켠다. 줄 앞에 (.venv) 가 붙으면 켜진 것이다
Get-ChildItem Env:OPENAI* # 남은 OPENAI 환경 변수를 본다. 아무것도 나오지 않아야 한다
python 06_외부API로만드는RAG.py # 이제 .env 의 진짜 키로 OpenAI 에 묻는다VS Code 의 터미널이라면 터미널 창 오른쪽 위의 휴지통 아이콘(터미널 끄기)을 누른 뒤 새 터미널을 엽니다. 창을 새로 열기 어렵다면 아래처럼 남은 값을 하나씩 지워도 됩니다.
시험이 끝나면 원래대로 돌리기
창을 새로 열지 않을 때는 넣었던 주소부터 지웁니다. 이 값이 남아 있으면 진짜 키를 넣어도 요청이 Ollama 로 갑니다. Remove-Item 은 지우기에 성공하면 아무것도 찍지 않으므로, Test-Path 로 정말 지워졌는지 확인합니다.
Remove-Item Env:OPENAI_BASE_URL # Ollama 로 돌려 두었던 주소를 지운다. 성공하면 아무것도 찍지 않는다
Test-Path Env:OPENAI_BASE_URL # 그 환경 변수가 남아 있으면 True, 없으면 False수강생의 PC(가상환경 .venv 를 켠 PowerShell)에서 실행한 결과입니다.
(.venv) PS C:\llm-course> Remove-Item Env:OPENAI_BASE_URL
(.venv) PS C:\llm-course> Test-Path Env:OPENAI_BASE_URL
False첫 줄 뒤에 아무 글자도 없는 것이 성공한 모습이고, False 는 "그 환경 변수가 이제 없다"는 뜻입니다. 이제 요청은 원래 주소인 OpenAI 로 갑니다.
| 명령과 옵션 | 원래 말 | 뜻 |
|---|---|---|
Remove-Item | remove(지우다) + item(항목) | 파일, 폴더, 환경 변수 같은 항목을 지운다 |
Env: | environment(환경) | PowerShell 이 환경 변수를 모아 둔 곳. 폴더처럼 Env:이름 으로 가리킨다 |
Test-Path | test(시험하다) + path(경로) | 그 경로에 항목이 있는지 보고 True 나 False 를 돌려준다 |
$env:OPENAI_BASE_URL | 값을 바로 찍는다. 지워졌으면 빈 줄만 나온다 | |
Get-ChildItem Env:OPENAI* | get(가져오다) + child item(안에 든 항목) | 이름이 OPENAI 로 시작하는 환경 변수를 모두 늘어놓는다 |
시험할 때 함께 넣은 키와 모델 이름도 지웁니다. 키를 ollama 로 둔 채 OpenAI 로 보내면 401 오류가 납니다. 키를 .env 에 적어 두었다면 창의 키는 지우기만 하면 됩니다. 그러면 load_dotenv() 가 .env 의 진짜 키를 넣습니다.
Remove-Item Env:OPENAI_API_KEY # 시험용 'ollama' 키를 지운다. 진짜 키는 .env 에서 읽힌다
Remove-Item Env:OPENAI_MODEL # 지우면 코드의 기본값 gpt-6-luna 를 쓴다
Get-ChildItem Env:OPENAI* # 남은 OPENAI 환경 변수를 본다. 아무것도 나오지 않아야 한다.env 를 쓰지 않는다면 키를 지우는 대신 $env:OPENAI_API_KEY = "발급받은_인증키" 로 진짜 키를 다시 넣습니다.
알아 둘 것이 셋 있습니다.
- 이미 지운 것을 한 번 더 지우면
Cannot find path 'Env:\OPENAI_BASE_URL' because it does not exist.라는 빨간 오류가 납니다. "이미 없다"는 뜻이라 그대로 두어도 됩니다. - 하나라도 빠뜨리면 위 표의 오류가 납니다. 헷갈리면 지우는 대신 창을 새로 여는 편이 빠릅니다.
.env파일에OPENAI_BASE_URL을 적어 두었다면Remove-Item으로는 지워지지 않습니다. 그 줄은.env파일에서 지웁니다.
.env 파일에 키를 두고 쓰기
위의 $env:OPENAI_API_KEY = … 는 그 PowerShell 창에서만 살아 있습니다. 창을 닫거나 VS Code 를 다시 켜면 사라져서 매번 다시 넣어야 합니다. 그래서 키를 .env 파일에 적어 두고, 코드가 시작할 때 읽어 오게 하는 방법을 많이 씁니다.
현관 비밀번호를 떠올려 보세요. 문에 비밀번호를 써 붙이지 않고, 집 안 서랍의 수첩에 적어 둡니다. 집에 들어온 사람만 수첩을 볼 수 있고, 집 사진을 남에게 보낼 때 서랍은 찍지 않습니다. 여기서 수첩이 .env 파일, 서랍에서 꺼내 읽는 일이 load_dotenv(), 집 사진을 보낼 때 서랍을 빼는 일이 .gitignore 입니다. 비유와 다른 점은, .env 파일은 잠기지 않는 보통 글자 파일이라 PC 를 함께 쓰는 사람은 열어 볼 수 있다는 것입니다.
| 견줄 것 | PowerShell 의 $env: | .env 파일 |
|---|---|---|
| 값을 두는 곳 | 그 PowerShell 창의 기억 | 코드 옆의 글자 파일 |
| 살아 있는 동안 | 창을 닫으면 사라진다 | 지우기 전까지 남는다 |
| 코드에서 할 일 | 없다. os.environ 으로 바로 읽는다 | 맨 앞에서 load_dotenv() 를 한 번 부른다 |
| 조심할 것 | 거의 없다 | 깃허브에 올라가지 않게 .gitignore 에 적는다 |
이름의 .env 는 env(environment, 환경)에 점을 붙인 것입니다. Linux 와 macOS 에서는 점으로 시작하는 파일이 숨김 파일이 되어, 설정 파일 이름에 흔히 씁니다. 이 파일을 읽어 주는 패키지가 python-dotenv 이고, 코드에서는 dotenv 라는 이름으로 불러옵니다. 교안을 만든 PC 는 python-dotenv 1.2.4 입니다.
1단계. 패키지 설치
pip install python-dotenv # .env 파일을 읽어 환경 변수에 넣어 주는 패키지2단계. .env 파일 만들기
코드 파일과 같은 폴더에 이름이 정확히 .env 인 파일을 만들고 아래처럼 적습니다. VS Code 의 탐색기에서 "새 파일"을 누르고 .env 라고 적으면 됩니다. 메모장으로 만들면 .env.txt 로 저장되기 쉬우니, 저장할 때 파일 형식을 "모든 파일"로 고릅니다.
# .env 파일. 한 줄에 "이름=값" 하나. = 양쪽에 빈칸을 두지 않는다
OPENAI_API_KEY=발급받은_인증키따옴표는 없어도 됩니다. # 으로 시작하는 줄은 설명이라 읽지 않습니다.
3단계. 깃에서 빼고, 예시 파일 남기기
깃(Git)으로 코드를 관리한다면 같은 폴더의 .gitignore 파일에 .env 를 한 줄 적습니다. 그러면 git add 를 해도 .env 는 올라가지 않습니다. 대신 키 자리를 비워 둔 .env.example 을 올려, 다른 사람이 어떤 이름이 필요한지 알게 합니다. 14장의 배포 폴더도 이 방법을 씁니다.
# .gitignore 에 적는 줄
.env
# .env.example (깃에 올리는 견본. 진짜 키를 적지 않는다)
OPENAI_API_KEY=발급받은_인증키4단계. 코드에서 읽기
코드 맨 앞에서 load_dotenv() 를 부르면, 그 뒤로는 PowerShell 에서 넣었을 때와 똑같이 os.environ 으로 읽습니다. 아래는 위의 API 코드에 두 줄을 더한 것이고, 마찬가지로 이 PC 에서 실행하지 않은 안내 코드입니다.
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv() # 코드 파일 옆(없으면 위쪽 폴더)의 .env 를 읽는다
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) # 그 뒤로는 PowerShell 에서 넣었을 때와 같다openai 라이브러리는 api_key 를 주지 않으면 OPENAI_API_KEY 환경 변수를 스스로 찾습니다. 그래서 load_dotenv() 뒤에는 OpenAI() 만 써도 됩니다. 다만 어디서 키를 읽는지 눈에 보이게 하려고 교안은 api_key= 를 적었습니다.
진짜 키 없이 동작 확인하기
진짜 키가 없어도 load_dotenv() 가 어떻게 움직이는지는 볼 수 있습니다. 아래 코드는 임시 폴더에 가짜 값이 든 .env 를 만들어 읽어 보고, 끝나면 폴더와 환경 변수를 모두 지웁니다.
import os
import tempfile
from pathlib import Path
from dotenv import load_dotenv, dotenv_values
with tempfile.TemporaryDirectory() as folder: # 끝나면 저절로 지워지는 임시 폴더
env_file = Path(folder) / ".env"
# 연습용 .env 파일을 만든다. 값은 가짜다
env_file.write_text("DEMO_API_KEY=시험용_가짜_키\nDEMO_MODEL=쓸_모델_이름\n", encoding="utf-8")
print("파일 속 값:", dict(dotenv_values(env_file))) # 환경 변수에 넣지 않고 내용만 읽는다
print("읽기 전:", os.environ.get("DEMO_API_KEY", "(없음)"))
load_dotenv(env_file) # 파일의 값을 환경 변수에 넣는다
print("읽은 뒤:", os.environ.get("DEMO_API_KEY", "(없음)"))
os.environ["DEMO_API_KEY"] = "창에서_넣은_값" # PowerShell 에서 미리 넣은 경우를 흉내 낸다
load_dotenv(env_file) # 이미 있는 값은 덮어쓰지 않는다
print("이미 있으면:", os.environ["DEMO_API_KEY"])
load_dotenv(env_file, override=True) # override=True 면 파일의 값으로 덮어쓴다
print("override=True:", os.environ["DEMO_API_KEY"])
for name in ["DEMO_API_KEY", "DEMO_MODEL"]: # 예시가 넣은 환경 변수를 지운다
os.environ.pop(name, None)파일 속 값: {'DEMO_API_KEY': '시험용_가짜_키', 'DEMO_MODEL': '쓸_모델_이름'}
읽기 전: (없음)
읽은 뒤: 시험용_가짜_키
이미 있으면: 창에서_넣은_값
override=True: 시험용_가짜_키| 코드 | 쉬운 뜻 |
|---|---|
from dotenv import load_dotenv, dotenv_values | 패키지 이름은 python-dotenv 이지만 불러올 때는 dotenv 로 쓴다 |
dotenv_values(파일) | .env 의 내용을 사전으로 읽기만 한다. 환경 변수는 건드리지 않는다 |
load_dotenv(파일) | .env 의 값을 환경 변수에 넣는다. 파일을 주지 않으면 코드 파일이 있는 폴더에서 위쪽 폴더로 올라가며 .env 를 찾는다 |
override=True | override 는 "덮어쓰다"라는 뜻이다. 기본값 False 에서는 이미 있는 환경 변수가 이긴다 |
os.environ.pop(name, None) | 예시가 넣은 환경 변수를 지운다. 없으면 오류 대신 그냥 넘어간다 |
출력의 넷째 줄이 눈여겨볼 곳입니다. PowerShell 에서 이미 넣은 값이 있으면 load_dotenv() 는 그 값을 그대로 두고, .env 의 값은 쓰지 않습니다. 그래서 .env 의 키를 고쳤는데도 예전 키로 요청이 가면, 창에 남은 $env:OPENAI_API_KEY 를 먼저 의심합니다. 창을 새로 열거나 override=True 를 주면 됩니다.
주의할 점
- API 키가 든 파일을 깃허브에 올리면 남이 내 돈으로 API 를 씁니다. 키는 환경 변수나
.env파일(깃에서 빼 둔)에 둡니다. - 사내 문서를 보내기 전에 회사의 보안 규정과 공급사의 데이터 처리 약관(입력을 학습에 쓰는지, 얼마나 보관하는지)을 확인합니다.