6장. RAG 의 원리와 로컬 모델로 옮기기
실습 1 — 모델이 모르는 질문 해 보기
3장부터 쓴 Qwen/Qwen2.5-0.5B-Instruct 에 가상 쇼핑몰의 정책을 묻고, 문서 없이 얼마나 답하는지 봅니다. 질문은 shop_policy_qa.jsonl 의 30개(문서에 있는 24개, 없는 6개)이고, 쇼핑몰과 정책은 모두 연습용으로 지어낸 것입니다. GPU 가 있으면 GPU 로, 없으면 CPU 로 돕니다.
1단계. 모델 불러오기
import json
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
name = "Qwen/Qwen2.5-0.5B-Instruct"
# GPU 가 있으면 fp16 으로 GPU 에, 없으면 float32 로 CPU 에 올린다
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForCausalLM.from_pretrained(name, dtype=dtype).to(device)
def chat(messages, max_new_tokens=100):
"""메시지 목록을 받아 모델의 답(글)을 돌려준다. do_sample=False 라 다시 돌려도 같은 답이다."""
enc = tok.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True).to(device)
with torch.no_grad():
out = model.generate(**enc, max_new_tokens=max_new_tokens, do_sample=False)
# 프롬프트 뒤에 새로 만든 토큰만 글로 바꾼다
return tok.decode(out[0][enc["input_ids"].shape[1]:], skip_special_tokens=True).strip()
print("장치:", device)장치: cuda2단계. 문서 없이 물어보기
# 질문 30개를 읽는다. 근거조항이 0 이면 문서에 없는 질문이다
qa = [json.loads(line) for line in open("shop_policy_qa.jsonl", encoding="utf-8")]
print("질문 수:", len(qa), "· 문서에 없는 질문:", sum(r["근거조항"] == 0 for r in qa))
NO_DOC = [{"role": "system", "content": "당신은 쇼핑몰 상담원입니다. 짧게 답하세요."}]
# 문서에 있는 질문 둘, 없는 질문 하나를 골라 본다
for q in ["휴대폰으로 결제하면 한도가 있나요?", "골드 등급이 되려면 얼마나 사야 해요?", "새벽 배송도 해 주나요?"]:
answer = chat(NO_DOC + [{"role": "user", "content": q}])
print("질문:", q)
print("답 :", answer.replace("\n", " ")[:110])질문 수: 30 · 문서에 없는 질문: 6
질문: 휴대폰으로 결제하면 한도가 있나요?
답 : 결제 금액에 대한 한도는 상점마다 다르지만, 일반적으로 10만원 이상의 결제를 원하는 경우가 많습니다. 이 금액을 기준으로 상점에서 할인이나 특별한 혜택을 제공할 수 있습니다.
질문: 골드 등급이 되려면 얼마나 사야 해요?
답 : 골드 등급을 구할 때는 몇 가지 요소가 중요합니다: 1. 가격: 골드의 가격에 따라 가치가 달라집니다. 2. 브랜드와 제조사: 브랜드와 제조사를 고려해야 합니다. 3. 판매량: 현재 판매량과
질문: 새벽 배송도 해 주나요?
답 : 네, 새벽 배송을 지원합니다. 저희는 고객님의 주문에 따라 최선을 다해 배송을 진행하겠습니다. 언제든지 도움이 필요하시면 말씀해주세요!3단계. 30개 채점하기
from collections import Counter
REFUSE = ["찾지 못", "찾을 수 없", "나와 있지 않", "제공되지 않"]
def grade(key, answer):
"""본문 "LLM 이 모르는 것"의 채점 함수. 결과: 정답, 오답, 거절, 지어냄, 모름 맞음"""
refused = any(w in answer for w in REFUSE)
if key == "":
return "모름 맞음" if refused else "지어냄"
if any(k in answer for k in key.split("|")):
return "정답"
return "거절" if refused else "오답"
results = [grade(r["정답열쇠"], chat(NO_DOC + [{"role": "user", "content": r["질문"]}])) for r in qa]
# 문서에 있는 질문(24개)과 없는 질문(6개)을 따로 센다
print("문서에 있는 질문:", Counter(x for x, r in zip(results, qa) if r["근거조항"]))
print("문서에 없는 질문:", Counter(x for x, r in zip(results, qa) if not r["근거조항"]))문서에 있는 질문: Counter({'오답': 22, '정답': 2})
문서에 없는 질문: Counter({'지어냄': 6})이 실습이 뜻하는 것
이 실습은 우리 가게 안내서를 한 번도 본 적 없는 새 아르바이트생에게 손님 질문을 던져 보는 것과 같습니다. 아르바이트생을 불러 자리에 앉히고(1단계), 질문 세 개를 먼저 던져 어떻게 답하는지 듣고(2단계), 질문 30개를 모두 물어 정답 낱말이 들어 있는지로 채점합니다(3단계). 여기서 아르바이트생이 Qwen2.5-0.5B-Instruct 모델, 안내서가 shop_policy.txt 정책 문서, 채점표가 grade() 함수입니다. 비유와 다른 점은, 사람은 모르면 머뭇거리기라도 하지만 모델은 모르는 것도 망설임 없이 그럴듯하게 답한다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 모델 불러오기 | 장치: cuda | 모델이 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 올라갔습니다. GPU 가 없으면 cpu 로 나오고 더 느리게 돕니다 |
| 2. 질문 수 | 질문 수: 30 · 문서에 없는 질문: 6 | 시험 문제는 30개이고, 그 가운데 6개는 정책 문서에도 답이 없는 질문입니다 |
| 2. 휴대폰 결제 | "일반적으로 10만원 이상…" | 문서의 답은 "한 달에 50만원까지"인데, 모델은 흐릿한 일반론을 지어냈습니다 |
| 2. 골드 등급 | "골드의 가격에 따라…" | 쇼핑몰의 회원 등급을 금(골드)을 사는 이야기로 잘못 알아들었습니다 |
| 2. 새벽 배송 | "네, 새벽 배송을 지원합니다" | 문서에 없는 서비스를 있다고 자신 있게 답했습니다. 이렇게 사실이 아닌 것을 지어내는 것이 환각입니다 |
| 3. 문서에 있는 질문 | '오답': 22, '정답': 2 | 답이 문서에 있는 24문제 가운데 2개만 맞혔습니다. 그 2개도 일반론에 열쇠 낱말이 우연히 섞인 것입니다 |
| 3. 문서에 없는 질문 | '지어냄': 6 | 답이 없는 6문제에 한 번도 모른다고 하지 않고 모두 지어냈습니다 |
한 줄로 줄이면, 모델은 우리 쇼핑몰 정책을 모르고, 모를 때도 모른다고 하지 않는다는 것을 숫자(24문제 중 정답 2개, 없는 6문제 모두 지어냄)로 확인한 실습입니다. 이 점수가 뒤 실습의 출발선입니다. 실습 4에서 같은 30문제를 문서를 붙여 다시 풀고 견줍니다. 채점은 열쇠 낱말이 들어 있는지만 보므로, 정답 2개가 "알고 맞혔다"는 뜻은 아닙니다.
코드 설명
apply_chat_template(..., return_dict=True)는 3장처럼 메시지 목록을 모델이 학습한 대화 꼴의 토큰으로 바꿉니다.chat()은 이 장 내내 "메시지를 넣으면 답 글이 나오는" 함수로 씁니다.- 문서 없이 물으니 휴대폰 결제 한도는 "일반적으로 10만원 이상"처럼 흐리게 지어내고, 골드 등급은 금(골드)을 사는 이야기로 알아듣고, 문서에 없는 새벽 배송은 "네, 새벽 배송을 지원합니다"라고 자신 있게 답합니다. 모두 문서와 다른 환각입니다.
- 문서에 있는 24개 가운데 정답은 2개, 오답 22개이고, 문서에 없는 6개는 모두 지어냈습니다. 정답 2개도 "반품은 일반적으로 불가능합니다", "대부분 10% 이하"처럼 일반론에 열쇠 낱말이 우연히 들어간 것입니다.
- 정답 열쇠는 "50만원"처럼 짧은 낱말이라, 운 좋게 겹치는 낱말을 말하면 맞힌 것으로 셉니다. 숫자가 몇 개 맞았다고 모델이 정책을 아는 것은 아닙니다.