이미지 로딩 중...

Qwen 2.5 모델 완벽 활용 가이드 - 슬라이드 1/13
A

AI Generated

2025. 11. 16. · 4 Views

Qwen 2.5 모델 완벽 활용 가이드

Alibaba의 최신 오픈소스 LLM인 Qwen 2.5 모델을 실무에 활용하는 방법을 다룹니다. 모델 설치부터 추론, 파인튜닝까지 단계별로 배워보세요.


목차

  1. Qwen 2.5 모델 설치 및 기본 설정
  2. 텍스트 생성 및 기본 추론
  3. 스트리밍 응답 구현하기
  4. 배치 처리로 여러 프롬프트 동시 생성
  5. 모델 양자화로 메모리 절약하기
  6. 프롬프트 캐싱으로 추론 속도 향상
  7. LoRA로 효율적인 파인튜닝 수행하기
  8. 함수 호출(Function Calling) 구현하기
  9. RAG(검색 증강 생성)로 외부 지식 활용하기
  10. 멀티모달 처리로 이미지 이해하기
  11. vLLM으로 프로덕션 배포하기
  12. 프롬프트 엔지니어링 Best Practices

1. Qwen 2.5 모델 설치 및 기본 설정

시작하며

여러분이 LLM 프로젝트를 시작할 때 이런 고민을 하신 적 있나요? "GPT API는 비용이 부담스럽고, 오픈소스 모델은 성능이 떨어질까봐 걱정이야." 특히 한국어 처리가 중요한 프로젝트라면 더욱 고민이 깊어집니다.

바로 이런 상황에서 Qwen 2.5가 주목받고 있습니다. Alibaba에서 공개한 이 모델은 다국어 지원이 뛰어나고, 특히 한국어 성능이 우수합니다.

무엇보다 완전 오픈소스로 공개되어 로컬 환경에서 자유롭게 사용할 수 있죠. 이번 카드에서는 Qwen 2.5를 여러분의 개발 환경에 설치하고 기본 설정하는 방법을 알아보겠습니다.

한 번만 제대로 세팅해두면 다양한 AI 프로젝트에 활용할 수 있습니다.

개요

간단히 말해서, Qwen 2.5는 Hugging Face Transformers 라이브러리를 통해 쉽게 설치하고 사용할 수 있는 최신 LLM입니다. 모델 크기는 0.5B부터 72B까지 다양하게 제공되어, 여러분의 하드웨어 환경에 맞게 선택할 수 있습니다.

예를 들어, GPU 메모리가 8GB 정도라면 7B 모델을, 더 큰 환경이라면 14B나 32B 모델을 선택할 수 있죠. 기존에는 LLM을 사용하려면 복잡한 설정과 의존성 관리가 필요했다면, 이제는 transformers 라이브러리 하나로 간단하게 시작할 수 있습니다.

Qwen 2.5의 핵심 특징은 빠른 추론 속도, 긴 컨텍스트 지원(최대 128K 토큰), 그리고 코드 생성 능력입니다. 이러한 특징들이 실무에서 챗봇, 문서 요약, 코드 어시스턴트 등 다양한 용도로 활용될 수 있게 해줍니다.

코드 예제

# Qwen 2.5 모델 설치 및 기본 설정
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

# 모델 이름 지정 - 7B 모델 사용
model_name = "Qwen/Qwen2.5-7B-Instruct"

# 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# 모델 로드 - GPU 사용 가능 시 자동으로 GPU에 로드
device = "cuda" if torch.cuda.is_available() else "cpu"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,  # 메모리 절약을 위해 float16 사용
    device_map="auto",  # 자동으로 디바이스 할당
    trust_remote_code=True
)

print(f"모델이 {device}에 성공적으로 로드되었습니다!")

설명

이것이 하는 일: 이 코드는 Qwen 2.5 모델을 로컬 환경에 다운로드하고 메모리에 로드하여 사용할 준비를 완료합니다. 첫 번째로, transformers 라이브러리에서 필요한 클래스들을 import합니다.

AutoModelForCausalLM은 텍스트 생성 모델을 자동으로 로드해주고, AutoTokenizer는 텍스트를 모델이 이해할 수 있는 토큰으로 변환해줍니다. 이렇게 Auto 클래스를 사용하면 모델별 세부사항을 몰라도 쉽게 사용할 수 있죠.

그 다음으로, model_name을 지정하고 토크나이저와 모델을 차례로 로드합니다. torch_dtype=torch.float16으로 설정하면 모델 가중치를 16비트로 저장하여 메모리 사용량을 절반으로 줄일 수 있습니다.

device_map="auto"는 모델을 여러 GPU에 자동으로 분산하거나, GPU 메모리가 부족하면 CPU 메모리를 활용하도록 해줍니다. trust_remote_code=True 옵션은 Qwen 모델이 사용하는 커스텀 코드를 신뢰한다는 의미입니다.

Qwen 2.5는 일부 최적화된 커스텀 코드를 포함하고 있어 이 옵션이 필요합니다. 마지막으로, GPU 사용 가능 여부를 확인하여 자동으로 최적의 디바이스에 모델을 로드합니다.

이렇게 하면 GPU가 있는 환경에서는 빠른 추론이, 없는 환경에서도 CPU로 동작이 가능합니다. 여러분이 이 코드를 실행하면 첫 실행 시에만 모델 파일(약 14GB)이 다운로드되고, 이후에는 캐시된 모델을 빠르게 로드할 수 있습니다.

실무에서는 한 번 설치해두면 여러 프로젝트에서 재사용할 수 있어 개발 효율이 크게 향상됩니다.

실전 팁

💡 GPU 메모리가 부족하다면 더 작은 모델(3B 또는 1.5B)을 선택하거나, load_in_8bit=True 옵션으로 8비트 양자화를 활용하세요. 성능은 약간 떨어지지만 메모리를 크게 절약할 수 있습니다.

💡 모델 다운로드 시간을 줄이려면 HF_HOME 환경변수를 설정하여 빠른 SSD에 캐시 경로를 지정하세요. 특히 여러 모델을 테스트할 때 유용합니다.

💡 프로덕션 환경에서는 trust_remote_code=True를 사용하기 전에 코드를 검토하세요. 보안상 신뢰할 수 있는 모델만 사용하는 것이 안전합니다.

💡 첫 실행 시 "RuntimeError: CUDA out of memory" 에러가 발생하면 torch.cuda.empty_cache()로 GPU 메모리를 정리하거나, 배치 크기를 줄여보세요.

💡 모델 로드 시간을 측정하려면 time 모듈을 사용하여 로드 전후 시간을 기록하세요. 이를 통해 최적의 설정을 찾을 수 있습니다.


2. 텍스트 생성 및 기본 추론

시작하며

여러분이 챗봇을 개발하거나 자동 응답 시스템을 만들 때 이런 고민을 하신 적 있나요? "API 호출마다 비용이 발생하는데, 테스트할 때마다 돈이 나가네." 개발 단계에서는 수백 번의 테스트가 필요한데 비용이 부담스러울 수밖에 없습니다.

로컬에서 LLM을 실행하면 이런 비용 걱정 없이 무제한으로 테스트할 수 있습니다. 또한 인터넷 연결 없이도 작동하고, 민감한 데이터를 외부로 보내지 않아 보안도 강화됩니다.

이번 카드에서는 Qwen 2.5로 텍스트를 생성하는 기본 추론 방법을 배워보겠습니다. 한 번 익혀두면 질의응답, 번역, 요약 등 다양한 작업에 활용할 수 있습니다.

개요

간단히 말해서, 텍스트 생성은 여러분이 입력한 프롬프트를 바탕으로 모델이 자연스러운 문장을 완성하는 과정입니다. Qwen 2.5는 Instruct 버전의 경우 대화형 프롬프트 형식을 지원합니다.

예를 들어, "다음 코드를 설명해줘" 같은 명령을 직접 입력하면 모델이 이해하고 적절한 답변을 생성해줍니다. 기존에는 복잡한 프롬프트 엔지니어링이 필요했다면, 이제는 chat 템플릿을 사용하여 간단하게 대화형 인터페이스를 구현할 수 있습니다.

핵심 매개변수는 max_new_tokens(생성할 최대 토큰 수), temperature(창의성 정도), top_p(샘플링 범위)입니다. 이들을 조정하면 생성되는 텍스트의 길이와 품질을 제어할 수 있어, 용도에 맞는 최적의 결과를 얻을 수 있습니다.

코드 예제

# Qwen 2.5로 텍스트 생성하기
from transformers import AutoModelForCausalLM, AutoTokenizer

# 이전에 로드한 모델과 토크나이저 사용
# 대화 형식으로 프롬프트 구성
messages = [
    {"role": "system", "content": "당신은 친절한 AI 어시스턴트입니다."},
    {"role": "user", "content": "Python에서 리스트 컴프리헨션을 설명해줘"}
]

# 채팅 템플릿 적용하여 텍스트로 변환
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

# 텍스트를 토큰으로 변환하고 모델 디바이스로 이동
inputs = tokenizer([text], return_tensors="pt").to(model.device)

# 텍스트 생성 실행
outputs = model.generate(
    **inputs,
    max_new_tokens=512,  # 최대 512개 토큰 생성
    temperature=0.7,  # 적절한 창의성
    top_p=0.8,  # 상위 80% 확률 내에서 샘플링
    do_sample=True  # 샘플링 활성화
)

# 생성된 텍스트 디코딩
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)

설명

이것이 하는 일: 이 코드는 사용자의 질문을 받아 Qwen 2.5 모델이 이해할 수 있는 형식으로 변환한 뒤, 적절한 답변을 생성하여 반환합니다. 첫 번째로, messages 리스트에 대화 내용을 구조화합니다.

system 역할은 모델의 페르소나를 정의하고, user 역할은 실제 질문을 담습니다. 이렇게 구조화하면 모델이 맥락을 더 잘 이해하고 일관된 답변을 생성할 수 있습니다.

OpenAI API와 유사한 형식이어서 기존 코드를 쉽게 마이그레이션할 수 있죠. 그 다음으로, apply_chat_template 메서드가 이 구조화된 대화를 모델이 학습한 프롬프트 형식으로 변환합니다.

내부적으로 특수 토큰들이 추가되어 "<|im_start|>system\n당신은..." 같은 형식이 됩니다. add_generation_prompt=True는 모델이 답변을 시작할 수 있도록 프롬프트 끝에 어시스턴트 역할 마커를 추가합니다.

세 번째로, 변환된 텍스트를 토크나이저로 인코딩하여 숫자 ID 시퀀스로 만들고, 이를 모델이 있는 디바이스(GPU 또는 CPU)로 이동시킵니다. return_tensors="pt"는 PyTorch 텐서 형식으로 반환하라는 의미입니다.

generate 메서드가 실제 텍스트 생성을 수행합니다. max_new_tokens은 새로 생성할 토큰 수를 제한하여 너무 긴 응답을 방지합니다.

temperature는 0에 가까울수록 결정적이고, 1에 가까울수록 창의적인 응답을 생성합니다. top_p는 누적 확률 상위 80% 토큰만 고려하여 너무 이상한 단어가 선택되는 것을 방지합니다.

마지막으로, 생성된 토큰 ID를 다시 텍스트로 디코딩합니다. skip_special_tokens=True로 <|im_start|> 같은 특수 토큰을 제거하여 깔끔한 텍스트만 얻습니다.

여러분이 이 코드를 사용하면 질의응답, 코드 설명, 번역, 요약 등 다양한 NLP 작업을 로컬에서 처리할 수 있습니다. API 비용 걱정 없이 무제한으로 테스트하고, 응답 시간도 네트워크 지연이 없어 빠릅니다.

실전 팁

💡 정확한 답변이 필요하면 temperature=0.10.3, 창의적인 글쓰기는 0.70.9를 사용하세요. 코드 생성은 낮은 값이, 스토리텔링은 높은 값이 적합합니다.

💡 흔한 실수: do_sample=False로 설정하면 temperature와 top_p가 무시됩니다. 샘플링을 원하면 반드시 do_sample=True를 설정하세요.

💡 메모리 최적화: 긴 대화를 처리할 때는 with torch.no_grad()로 감싸서 gradient 계산을 비활성화하면 메모리를 절약할 수 있습니다.

💡 생성 속도 향상: use_cache=True(기본값)로 설정하면 이전 토큰의 계산 결과를 재사용하여 생성 속도가 크게 향상됩니다.

💡 답변이 중간에 끊기면 max_new_tokens를 늘리거나, eos_token_id를 확인하여 모델이 종료 토큰을 올바르게 생성하는지 확인하세요.


3. 스트리밍 응답 구현하기

시작하며

여러분이 ChatGPT를 사용할 때 단어가 하나씩 나타나는 것을 보신 적 있죠? 이런 스트리밍 방식은 사용자 경험을 크게 향상시킵니다.

전체 응답을 기다리는 대신 즉시 결과를 확인할 수 있으니까요. 하지만 일반적인 generate() 메서드는 모든 생성이 완료될 때까지 기다려야 합니다.

긴 응답의 경우 사용자가 수십 초를 기다려야 하는 불편함이 있죠. 이번 카드에서는 TextIteratorStreamer를 사용하여 ChatGPT처럼 실시간으로 텍스트를 출력하는 방법을 배워보겠습니다.

웹 애플리케이션이나 CLI 도구에서 즉시 활용할 수 있습니다.

개요

간단히 말해서, 스트리밍은 텍스트 생성이 완료되기 전에 중간 결과를 실시간으로 받아볼 수 있게 해주는 기법입니다. Transformers 라이브러리는 TextIteratorStreamer 클래스를 제공하여 이를 간단하게 구현할 수 있습니다.

예를 들어, 챗봇 UI에서 사용자가 질문하자마자 답변이 타이핑되듯 나타나게 할 수 있습니다. 기존에는 전체 응답이 생성될 때까지 기다렸다가 한 번에 표시했다면, 이제는 토큰 하나하나가 생성되는 즉시 화면에 출력할 수 있습니다.

핵심은 멀티스레딩입니다. 모델 생성은 별도 스레드에서 실행하고, 메인 스레드는 streamer에서 토큰을 받아 출력합니다.

이렇게 하면 UI가 블로킹되지 않고 반응성을 유지할 수 있습니다.

코드 예제

# 스트리밍 방식으로 텍스트 생성하기
from transformers import TextIteratorStreamer
from threading import Thread

# 메시지 준비
messages = [
    {"role": "system", "content": "당신은 친절한 AI 어시스턴트입니다."},
    {"role": "user", "content": "FastAPI로 REST API 만드는 방법을 단계별로 설명해줘"}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

# 스트리머 생성 - skip_prompt로 입력 프롬프트는 제외
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

# 생성 파라미터 설정
generation_kwargs = dict(
    inputs,
    streamer=streamer,
    max_new_tokens=1024,
    temperature=0.7,
    do_sample=True
)

# 별도 스레드에서 생성 시작
thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()

# 스트리밍으로 토큰을 받아 실시간 출력
print("AI: ", end="", flush=True)
for new_text in streamer:
    print(new_text, end="", flush=True)

thread.join()  # 생성 완료 대기
print()  # 줄바꿈

설명

이것이 하는 일: 이 코드는 모델이 텍스트를 생성하는 동시에 생성된 부분을 즉시 화면에 출력하여 사용자가 기다리는 시간을 체감적으로 줄여줍니다. 첫 번째로, 기본 메시지와 입력 준비 과정은 이전과 동일합니다.

차이점은 TextIteratorStreamer 객체를 생성한다는 것입니다. skip_prompt=True는 입력 프롬프트를 출력에서 제외하고 새로 생성된 부분만 받겠다는 의미입니다.

skip_special_tokens=True로 특수 토큰도 제거합니다. 그 다음으로, generation_kwargs 딕셔너리에 모든 생성 파라미터를 모읍니다.

핵심은 streamer=streamer를 전달하는 것입니다. 이렇게 하면 generate() 메서드가 토큰을 생성할 때마다 streamer에 전달합니다.

이 딕셔너리를 사용하는 이유는 Thread의 kwargs로 깔끔하게 전달하기 위함입니다. 세 번째로, Thread를 생성하여 model.generate를 별도 스레드에서 실행합니다.

왜 별도 스레드가 필요할까요? generate()는 블로킹 함수여서 완료될 때까지 다음 코드가 실행되지 않습니다.

별도 스레드에서 실행하면 메인 스레드는 streamer에서 토큰을 받아 출력하는 작업을 동시에 수행할 수 있습니다. for 루프에서 streamer를 이터레이트하면 새 토큰이 생성될 때마다 yield됩니다.

flush=True는 버퍼를 즉시 비워 화면에 바로 표시되게 합니다. end=""로 줄바꿈 없이 연속으로 출력하면 마치 타이핑하는 것처럼 보입니다.

마지막으로 thread.join()으로 생성 스레드가 완료될 때까지 기다립니다. 이렇게 하지 않으면 프로그램이 생성이 끝나기 전에 종료될 수 있습니다.

여러분이 이 코드를 사용하면 웹 애플리케이션에서 Server-Sent Events(SSE)나 WebSocket과 결합하여 실시간 챗봇을 구현할 수 있습니다. 사용자는 즉각적인 피드백을 받아 훨씬 나은 경험을 하게 됩니다.

실전 팁

💡 FastAPI와 결합할 때는 StreamingResponse를 사용하여 streamer를 직접 반환할 수 있습니다. async def endpoint에서 yield로 스트리밍하면 됩니다.

💡 흔한 실수: thread.join()을 빼먹으면 프로그램이 종료되면서 생성이 중단될 수 있습니다. 반드시 join()으로 완료를 기다리세요.

💡 에러 처리: 생성 중 예외가 발생하면 streamer가 무한 대기할 수 있습니다. try-except로 스레드 내부 에러를 잡고 streamer.end()를 호출하세요.

💡 성능 팁: timeout 파라미터로 streamer의 대기 시간을 제한하여 무한 대기를 방지할 수 있습니다. 예: TextIteratorStreamer(tokenizer, timeout=60)

💡 UI 개선: 토큰 단위가 아닌 단어나 문장 단위로 출력하고 싶다면 버퍼에 모았다가 공백이나 마침표에서 flush하는 로직을 추가하세요.


4. 배치 처리로 여러 프롬프트 동시 생성

시작하며

여러분이 수백 개의 제품 설명을 자동 생성하거나, 대량의 이메일에 자동 답변을 작성해야 하는 상황을 상상해보세요. 하나씩 순차적으로 처리하면 엄청난 시간이 걸립니다.

실제로 프로덕션 환경에서는 대량의 데이터를 효율적으로 처리하는 것이 중요합니다. GPU는 병렬 처리에 최적화되어 있어, 여러 입력을 동시에 처리하면 처리량이 크게 향상됩니다.

이번 카드에서는 배치 처리를 통해 여러 프롬프트를 동시에 생성하는 방법을 배워보겠습니다. 처리 시간을 몇 배나 단축할 수 있는 필수 기법입니다.

개요

간단히 말해서, 배치 처리는 여러 입력을 하나의 그룹으로 묶어 한 번에 모델에 전달하는 기법입니다. GPU는 SIMD(Single Instruction Multiple Data) 아키텍처로 설계되어 동일한 연산을 여러 데이터에 동시에 수행하는 데 매우 효율적입니다.

예를 들어, 10개의 프롬프트를 순차 처리하면 10초 걸릴 작업이 배치로 처리하면 3초 만에 끝날 수 있습니다. 기존에는 for 루프로 하나씩 처리했다면, 이제는 padding을 추가하여 모든 입력을 같은 길이로 맞춘 뒤 한 번에 처리할 수 있습니다.

핵심은 padding과 attention_mask입니다. 입력 길이가 다르면 짧은 입력에 패딩을 추가하고, attention_mask로 모델이 패딩 부분을 무시하도록 해야 합니다.

이렇게 하면 결과의 품질을 유지하면서도 속도를 크게 높일 수 있습니다.

코드 예제

# 배치 처리로 여러 프롬프트 동시 생성
import torch

# 여러 프롬프트 준비
prompts = [
    "Python 데코레이터를 간단히 설명해줘",
    "JavaScript의 클로저란 무엇인가요?",
    "Docker와 가상머신의 차이는?",
    "REST API와 GraphQL을 비교해줘"
]

# 각 프롬프트를 대화 형식으로 변환
texts = [
    tokenizer.apply_chat_template(
        [{"role": "user", "content": prompt}],
        tokenize=False,
        add_generation_prompt=True
    ) for prompt in prompts
]

# 배치 토크나이징 - padding 자동 추가
inputs = tokenizer(
    texts,
    return_tensors="pt",
    padding=True,  # 가장 긴 시퀀스에 맞춰 패딩
    truncation=True,  # 최대 길이 초과 시 자르기
    max_length=2048
).to(model.device)

# 배치 생성 실행
with torch.no_grad():  # 메모리 절약
    outputs = model.generate(
        **inputs,
        max_new_tokens=256,
        temperature=0.7,
        do_sample=True,
        pad_token_id=tokenizer.pad_token_id  # 패딩 토큰 ID 지정
    )

# 각 결과 디코딩 및 출력
for i, output in enumerate(outputs):
    response = tokenizer.decode(output, skip_special_tokens=True)
    print(f"Q{i+1}: {prompts[i]}")
    print(f"A{i+1}: {response}\n")

설명

이것이 하는 일: 이 코드는 여러 개의 서로 다른 질문을 GPU에서 병렬로 처리하여 전체 처리 시간을 크게 단축시킵니다. 첫 번째로, 처리할 여러 프롬프트를 리스트로 준비합니다.

리스트 컴프리헨션으로 각 프롬프트를 apply_chat_template으로 변환합니다. 이 단계에서는 아직 텍스트 상태이며, 토크나이징은 다음 단계에서 배치로 수행합니다.

그 다음으로, tokenizer()에 텍스트 리스트를 한 번에 전달합니다. padding=True가 핵심인데, 이는 배치 내에서 가장 긴 시퀀스를 찾아 나머지를 그 길이에 맞춥니다.

예를 들어, 길이가 [50, 80, 60, 70] 토큰인 4개 입력이 있다면 모두 80으로 맞춰집니다. truncation=True는 max_length를 초과하는 입력을 자르고, max_length=2048은 최대 길이를 지정합니다.

세 번째로, with torch.no_grad() 컨텍스트를 사용합니다. 추론 시에는 gradient 계산이 필요 없으므로 이렇게 하면 메모리 사용량이 크게 줄어듭니다.

특히 배치 크기가 클수록 효과가 큽니다. pad_token_id를 명시적으로 지정하여 모델이 패딩을 올바르게 처리하도록 합니다.

generate() 메서드는 입력 텐서의 첫 번째 차원(배치 차원)을 인식하고 자동으로 배치 처리를 수행합니다. 내부적으로 모든 입력에 대해 동시에 forward pass를 실행하고, 각각 독립적으로 토큰을 생성합니다.

마지막으로, outputs는 배치 크기만큼의 생성 결과를 담고 있습니다. enumerate로 각 결과를 순회하며 디코딩하고 출력합니다.

각 결과는 독립적이므로 순서와 관계없이 처리할 수 있습니다. 여러분이 이 코드를 사용하면 대량의 텍스트 생성 작업을 효율적으로 처리할 수 있습니다.

GPU 활용률이 높아져 같은 시간에 더 많은 작업을 완료할 수 있고, 서버 비용도 절감됩니다.

실전 팁

💡 배치 크기는 GPU 메모리에 따라 조절하세요. 8GB GPU라면 48개, 24GB라면 1632개가 적당합니다. CUDA OOM 에러가 나면 배치 크기를 줄이세요.

💡 흔한 실수: pad_token_id를 지정하지 않으면 경고가 발생하고 결과가 이상할 수 있습니다. 반드시 tokenizer.pad_token_id를 전달하세요.

💡 성능 측정: time 모듈로 순차 처리와 배치 처리 시간을 비교해보세요. 보통 2~4배 빠른 것을 확인할 수 있습니다.

💡 동적 배치: 입력 길이가 비슷한 것끼리 그룹핑하면 패딩이 줄어 더 효율적입니다. sorted()로 길이별로 정렬한 뒤 배치를 구성하세요.

💡 프로덕션 팁: 대량 처리 시 DataLoader를 사용하면 자동으로 배치를 생성하고 멀티프로세싱으로 데이터 로딩을 병렬화할 수 있습니다.


5. 모델 양자화로 메모리 절약하기

시작하며

여러분이 강력한 LLM을 사용하고 싶지만 GPU 메모리가 부족해서 포기한 적 있나요? 7B 모델도 float16으로는 14GB나 필요해서 일반적인 GPU로는 버거울 수 있습니다.

양자화는 이런 메모리 문제를 해결하는 강력한 기법입니다. 모델 가중치를 더 적은 비트로 표현하여 메모리를 절반 이상 줄이면서도 성능은 거의 유지할 수 있습니다.

이번 카드에서는 bitsandbytes 라이브러리를 사용하여 8비트 및 4비트 양자화를 적용하는 방법을 배워보겠습니다. 제한된 하드웨어에서도 큰 모델을 실행할 수 있게 됩니다.

개요

간단히 말해서, 양자화는 32비트나 16비트로 저장된 모델 가중치를 8비트 또는 4비트로 변환하여 메모리 사용량을 줄이는 기술입니다. bitsandbytes는 NVIDIA에서 개발한 라이브러리로 고품질 양자화를 지원합니다.

예를 들어, 8비트 양자화를 사용하면 14GB 모델을 7GB로 줄일 수 있고, 4비트는 3.5GB까지 가능합니다. 기존에는 고성능 GPU가 필수였다면, 이제는 일반 게이밍 GPU나 심지어 CPU에서도 큰 모델을 실행할 수 있습니다.

핵심 기법은 NF4(4-bit NormalFloat)와 double quantization입니다. NF4는 신경망 가중치의 분포에 최적화된 양자화 방식이고, double quantization은 양자화 파라미터 자체도 양자화하여 메모리를 더욱 절약합니다.

이들을 결합하면 성능 저하를 최소화하면서 극적인 메모리 절감을 달성할 수 있습니다.

코드 예제

# 4비트 양자화로 모델 로드하기
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

# 4비트 양자화 설정
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,  # 4비트 양자화 활성화
    bnb_4bit_quant_type="nf4",  # NormalFloat 4비트 사용
    bnb_4bit_compute_dtype=torch.float16,  # 연산은 float16으로
    bnb_4bit_use_double_quant=True,  # 이중 양자화로 추가 절약
)

# 양자화된 모델 로드
model_name = "Qwen/Qwen2.5-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,  # 양자화 설정 전달
    device_map="auto",
    trust_remote_code=True
)

# 메모리 사용량 확인
print(f"모델 메모리: {model.get_memory_footprint() / 1e9:.2f} GB")

# 일반적인 추론 가능
messages = [{"role": "user", "content": "양자화의 장점을 설명해줘"}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

outputs = model.generate(**inputs, max_new_tokens=256)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

설명

이것이 하는 일: 이 코드는 7B 모델을 14GB 대신 3.5GB 정도의 메모리로 로드하여 제한된 GPU에서도 실행 가능하게 만듭니다. 첫 번째로, BitsAndBytesConfig 객체를 생성하여 양자화 방식을 세밀하게 제어합니다.

load_in_4bit=True가 4비트 양자화를 활성화하는 핵심 옵션입니다. 8비트를 원하면 load_in_8bit=True를 사용하면 됩니다.

그 다음으로, bnb_4bit_quant_type="nf4"는 NormalFloat 4비트 방식을 지정합니다. 일반적인 INT4 대신 NF4를 사용하면 신경망 가중치의 정규분포 특성을 활용하여 더 나은 품질을 얻을 수 있습니다.

연구에 따르면 NF4는 INT4보다 훨씬 적은 정확도 손실을 보입니다. bnb_4bit_compute_dtype=torch.float16은 중요한 설정입니다.

가중치는 4비트로 저장되지만 실제 연산은 float16으로 수행합니다. 이렇게 하면 수치 안정성을 유지하면서도 메모리는 절약됩니다.

연산 시 4비트 가중치를 임시로 float16으로 변환했다가 다시 4비트로 저장하는 방식입니다. bnb_4bit_use_double_quant=True는 추가적인 최적화입니다.

양자화 과정에서 생성되는 스케일 팩터들도 양자화하여 메모리를 더욱 절약합니다. 이를 통해 추가로 0.5GB 정도 절감할 수 있습니다.

quantization_config를 from_pretrained에 전달하면 모델 로드 시 자동으로 양자화가 적용됩니다. device_map="auto"와 함께 사용하면 모델 레이어를 여러 GPU에 분산하거나 CPU 메모리도 활용할 수 있습니다.

여러분이 이 코드를 사용하면 고사양 GPU 없이도 대형 LLM을 실행할 수 있습니다. RTX 3060 같은 12GB GPU로도 7B 모델을 여유롭게 돌릴 수 있고, 추론 속도도 큰 차이가 없습니다.

실전 팁

💡 bitsandbytes 설치: pip install bitsandbytes accelerate를 먼저 실행하세요. CUDA 버전과 호환되는지 확인이 필요합니다.

💡 품질 비교: 4비트는 보통 원본의 95-98% 성능을 유지합니다. 중요한 작업이라면 먼저 테스트하여 품질을 확인하세요.

💡 속도 트레이드오프: 양자화된 모델은 로드는 빠르지만 추론은 약간 느릴 수 있습니다. 메모리가 충분하면 float16이 더 빠릅니다.

💡 파인튜닝: QLoRA 기법으로 양자화된 모델도 파인튜닝 가능합니다. peft 라이브러리와 함께 사용하면 LoRA 어댑터를 학습할 수 있습니다.

💡 CPU 오프로딩: llm_int8_enable_fp32_cpu_offload=True 옵션으로 일부 레이어를 CPU로 오프로드하여 더 큰 모델도 실행할 수 있습니다.


6. 프롬프트 캐싱으로 추론 속도 향상

시작하며

여러분이 긴 시스템 프롬프트를 사용하는 챗봇을 개발한다고 생각해보세요. 매번 사용자가 질문할 때마다 동일한 긴 시스템 프롬프트를 처음부터 다시 처리해야 한다면 비효율적이겠죠?

실제로 대화형 애플리케이션에서는 시스템 프롬프트나 컨텍스트가 반복되는 경우가 많습니다. 이런 반복을 매번 새로 계산하면 시간과 자원이 낭비됩니다.

이번 카드에서는 KV 캐시를 활용하여 반복되는 프롬프트를 재사용하고 추론 속도를 크게 향상시키는 방법을 배워보겠습니다. 특히 대화형 서비스에서 큰 효과를 볼 수 있습니다.

개요

간단히 말해서, KV 캐시(Key-Value Cache)는 이전에 계산한 어텐션 값들을 저장했다가 재사용하는 메커니즘입니다. Transformer 모델은 각 토큰을 생성할 때 이전 모든 토큰의 키(Key)와 값(Value)을 참조합니다.

예를 들어, 100번째 토큰을 생성할 때 1~99번째 토큰의 KV를 다시 계산하는 대신 캐시에서 가져오면 연산량이 크게 줄어듭니다. 기존에는 매 생성마다 전체 시퀀스를 처음부터 재처리했다면, 이제는 새 토큰에 대한 계산만 수행하면 됩니다.

핵심은 past_key_values를 generate() 메서드에 전달하는 것입니다. 이를 통해 이전 대화의 KV 캐시를 재사용할 수 있고, 특히 긴 시스템 프롬프트를 사용할 때 첫 번째 응답 이후 속도가 몇 배나 빨라집니다.

코드 예제

# KV 캐시를 활용한 효율적인 다회차 대화
import torch

# 긴 시스템 프롬프트 (실무에서는 더 길 수 있음)
system_prompt = """당신은 전문적인 프로그래밍 튜터입니다.
학생들에게 개념을 명확하게 설명하고, 실용적인 예제를 제공하며,
베스트 프랙티스를 강조해야 합니다. 코드 예제는 주석을 포함하고,
초보자도 이해할 수 있도록 단계별로 설명하세요."""

# 첫 번째 대화 - 캐시 생성
messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": "리스트 컴프리헨션이 뭔가요?"}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

# return_dict_in_generate로 캐시를 함께 반환
outputs = model.generate(
    **inputs,
    max_new_tokens=256,
    return_dict_in_generate=True,  # 딕셔너리로 반환
    use_cache=True  # 캐시 사용 활성화 (기본값)
)

response1 = tokenizer.decode(outputs.sequences[0], skip_special_tokens=True)
print(f"응답 1: {response1}\n")

# 두 번째 대화 - 캐시 재사용
messages.append({"role": "assistant", "content": response1})
messages.append({"role": "user", "content": "예제 코드를 보여줘"})

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

# 이전 캐시를 전달하여 재사용
outputs2 = model.generate(
    **inputs,
    max_new_tokens=256,
    past_key_values=outputs.past_key_values,  # 캐시 재사용
    return_dict_in_generate=True
)

response2 = tokenizer.decode(outputs2.sequences[0], skip_special_tokens=True)
print(f"응답 2: {response2}")

설명

이것이 하는 일: 이 코드는 다회차 대화에서 시스템 프롬프트와 이전 대화 내용을 매번 재계산하지 않고 캐시를 재사용하여 응답 속도를 2-3배 향상시킵니다. 첫 번째로, 긴 시스템 프롬프트를 포함한 첫 대화를 설정합니다.

실무에서는 시스템 프롬프트가 수천 토큰에 달할 수 있는데, 이를 매번 처리하면 큰 오버헤드가 됩니다. 여기서는 간단한 예시지만 원리는 동일합니다.

그 다음으로, return_dict_in_generate=True를 설정하여 생성 결과를 딕셔너리로 받습니다. 이 딕셔너리에는 sequences(생성된 토큰)뿐만 아니라 past_key_values(KV 캐시)도 포함됩니다.

use_cache=True는 기본값이지만 명시적으로 설정하여 의도를 분명히 합니다. outputs.past_key_values에는 모델의 모든 레이어에 대한 Key와 Value 텐서가 저장되어 있습니다.

Qwen 2.5-7B의 경우 28개 레이어 각각에 대한 KV가 있고, 각각은 (batch_size, num_heads, sequence_length, head_dim) 형태의 텐서입니다. 이것이 메모리를 차지하지만 재계산 비용보다 훨씬 저렴합니다.

세 번째로, 두 번째 대화를 준비합니다. messages에 이전 응답과 새 질문을 추가하여 전체 대화 히스토리를 구성합니다.

이것을 다시 토크나이징하여 입력을 만듭니다. 핵심은 generate()에 past_key_values를 전달하는 것입니다.

모델은 이 캐시를 보고 "아, 이 부분은 이미 계산했네"라고 인식하고 새로운 토큰에 대해서만 계산을 수행합니다. 시스템 프롬프트가 1000토큰이라면 1000번의 어텐션 계산을 건너뛸 수 있는 것이죠.

여러분이 이 코드를 사용하면 챗봇 서비스에서 응답 속도를 크게 개선할 수 있습니다. 특히 긴 컨텍스트나 시스템 프롬프트를 사용하는 경우 첫 응답 이후 대화가 훨씬 빨라져 사용자 경험이 향상됩니다.

실전 팁

💡 메모리 관리: KV 캐시는 상당한 메모리를 사용합니다. 대화가 너무 길어지면 del outputs.past_key_values로 캐시를 정리하세요.

💡 세션 관리: 실무에서는 각 사용자의 캐시를 세션별로 관리해야 합니다. Redis나 메모리 캐시에 저장하여 여러 요청에 걸쳐 재사용하세요.

💡 성능 측정: time.time()으로 캐시 사용 전후의 생성 시간을 측정해보세요. 시스템 프롬프트가 길수록 효과가 큽니다.

💡 주의사항: past_key_values는 배치 처리와 함께 사용할 때 복잡해집니다. 각 배치 항목마다 별도 캐시가 필요하므로 단일 대화에 적합합니다.

💡 Static KV Cache: Qwen 2.5는 static cache도 지원합니다. 미리 캐시 크기를 할당하여 동적 메모리 할당 오버헤드를 줄일 수 있습니다.


7. LoRA로 효율적인 파인튜닝 수행하기

시작하며

여러분이 Qwen 2.5를 특정 도메인에 맞게 커스터마이징하고 싶다면 어떻게 해야 할까요? 전체 모델을 파인튜닝하려면 엄청난 GPU 메모리와 시간이 필요합니다.

7B 모델의 전체 파인튜닝은 80GB GPU가 필요할 수 있죠. 실제 프로덕션 환경에서는 효율적인 파인튜닝이 필수입니다.

제한된 리소스로 빠르게 모델을 커스터마이징하고, 여러 버전을 실험할 수 있어야 합니다. 이번 카드에서는 LoRA(Low-Rank Adaptation)를 사용하여 소수의 파라미터만 학습하면서도 좋은 성능을 얻는 방법을 배워보겠습니다.

일반 GPU로도 충분히 파인튜닝할 수 있습니다.

개요

간단히 말해서, LoRA는 전체 모델을 학습하는 대신 작은 어댑터 레이어만 추가하여 학습하는 효율적인 파인튜닝 기법입니다. 핵심 아이디어는 가중치 업데이트를 저차원(low-rank) 행렬로 근사하는 것입니다.

예를 들어, 7B 모델 전체를 학습하는 대신 1-2%에 해당하는 파라미터만 학습하여 99%의 성능을 달성할 수 있습니다. 기존에는 전체 모델을 복사하여 학습했다면, 이제는 작은 어댑터만 저장하면 됩니다.

베이스 모델은 하나만 두고 여러 LoRA 어댑터를 교체하며 사용할 수 있죠. LoRA의 핵심 파라미터는 rank(어댑터 크기)와 alpha(학습률 스케일링)입니다.

rank가 클수록 표현력이 좋지만 메모리가 늘어나고, alpha는 학습 강도를 조절합니다. 이들을 적절히 설정하면 1% 파라미터로도 전체 파인튜닝에 준하는 성능을 낼 수 있습니다.

코드 예제

# LoRA로 Qwen 2.5 파인튜닝하기
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import TrainingArguments, Trainer

# 4비트 양자화된 모델 준비 (이전 카드 참조)
# model과 tokenizer가 이미 로드되었다고 가정

# LoRA 설정
lora_config = LoraConfig(
    r=16,  # LoRA rank - 낮을수록 파라미터 적음
    lora_alpha=32,  # 스케일링 팩터 - 보통 rank의 2배
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # 어텐션 레이어에 적용
    lora_dropout=0.05,  # 과적합 방지
    bias="none",  # bias는 학습하지 않음
    task_type="CAUSAL_LM"  # 인과적 언어 모델링
)

# 양자화 모델을 학습 가능하게 준비
model = prepare_model_for_kbit_training(model)

# LoRA 어댑터 추가
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # 학습 가능한 파라미터 확인

# 학습 설정
training_args = TrainingArguments(
    output_dir="./qwen-lora",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,  # 효과적인 배치 크기 16
    learning_rate=2e-4,
    fp16=True,  # 혼합 정밀도 학습
    logging_steps=10,
    save_strategy="epoch"
)

# 데이터셋 준비 (여기서는 예시)
# train_dataset = your_dataset

# trainer = Trainer(
#     model=model,
#     args=training_args,
#     train_dataset=train_dataset,
# )
# trainer.train()

# 학습 후 어댑터 저장
# model.save_pretrained("./qwen-lora-adapter")

설명

이것이 하는 일: 이 코드는 7B 모델을 일반 GPU에서 파인튜닝할 수 있도록 LoRA 어댑터를 설정하고 학습 준비를 완료합니다. 첫 번째로, LoraConfig로 LoRA의 동작을 정의합니다.

r=16은 어댑터의 rank로, 이 값이 작을수록 파라미터가 줄어듭니다. 연구에 따르면 8~64 사이가 적절하며, 간단한 작업은 8, 복잡한 작업은 64를 사용합니다.

lora_alpha=32는 학습률 스케일링으로 보통 rank의 2배를 사용합니다. 그 다음으로, target_modules는 어느 레이어에 LoRA를 적용할지 지정합니다.

Transformer의 어텐션 메커니즘에서 가장 중요한 Query, Key, Value, Output projection에 적용하는 것이 일반적입니다. 모든 linear layer에 적용하면 성능이 더 좋지만 메모리가 늘어납니다.

lora_dropout은 과적합을 방지하기 위한 드롭아웃 비율입니다. prepare_model_for_kbit_training은 양자화된 모델을 학습 가능하게 만듭니다.

일부 레이어를 float32로 변환하고 gradient checkpointing을 활성화하여 메모리를 절약합니다. 이것이 QLoRA(Quantized LoRA)의 핵심입니다.

get_peft_model이 실제로 LoRA 어댑터를 모델에 삽입합니다. 내부적으로 지정된 레이어에 저차원 행렬 쌍(A, B)을 추가하고, 원래 가중치 W는 고정한 채 A와 B만 학습합니다.

최종 출력은 W + BA가 됩니다. print_trainable_parameters()를 호출하면 "trainable params: 70M || all params: 7B || trainable%: 1.0%" 같은 출력을 볼 수 있습니다.

TrainingArguments는 학습 하이퍼파라미터를 설정합니다. gradient_accumulation_steps=4는 4번의 forward pass 후 한 번 업데이트하여 큰 배치 효과를 내면서 메모리는 절약합니다.

fp16=True는 혼합 정밀도 학습으로 속도를 높이고 메모리를 줄입니다. 여러분이 이 코드를 사용하면 맞춤형 챗봇, 도메인 특화 모델, 스타일 변환 등 다양한 커스터마이징을 효율적으로 수행할 수 있습니다.

어댑터만 저장하면 되므로 여러 버전을 관리하기도 쉽습니다.

실전 팁

💡 rank 선택: 간단한 분류나 스타일 변환은 r=8, 복잡한 지식 주입은 r=32~64를 사용하세요. 작은 값부터 시작하여 점진적으로 늘려보세요.

💡 target_modules 확장: model.named_modules()로 모든 레이어 이름을 확인하고, MLP 레이어(gate_proj, up_proj, down_proj)도 추가하면 성능이 향상됩니다.

💡 데이터셋 형식: 학습 데이터는 {"text": "..."} 형식이나 ChatML 형식으로 준비하세요. datasets 라이브러리로 쉽게 로드할 수 있습니다.

💡 학습 모니터링: wandb나 tensorboard를 통합하여 loss, learning rate 등을 실시간으로 모니터링하세요. TrainingArguments에 report_to="wandb" 추가하면 됩니다.

💡 추론 시 로드: 학습 후 from_pretrained(base_model)로 베이스 모델을 로드하고 PeftModel.from_pretrained(model, "adapter_path")로 어댑터를 합치세요.


8. 함수 호출(Function Calling) 구현하기

시작하며

여러분이 AI 어시스턴트를 만들 때 이런 요구사항을 받은 적 있나요? "날씨를 물어보면 API를 호출해서 실제 날씨를 알려줘야 해." 단순히 텍스트를 생성하는 것을 넘어 외부 도구를 사용해야 하는 상황입니다.

LLM이 아무리 똑똑해도 실시간 정보나 계산은 직접 할 수 없습니다. 데이터베이스 조회, API 호출, 수학 계산 같은 작업은 외부 함수가 필요하죠.

이번 카드에서는 Qwen 2.5에서 Function Calling을 구현하여 모델이 적절한 함수를 선택하고 파라미터를 추출하는 방법을 배워보겠습니다. 실제로 동작하는 AI 에이전트의 핵심 기술입니다.

개요

간단히 말해서, Function Calling은 LLM이 사용자 요청을 분석하여 어떤 함수를 호출해야 하는지 결정하고 필요한 파라미터를 JSON으로 반환하는 기능입니다. Qwen 2.5는 시스템 프롬프트에 함수 정의를 포함시키는 방식으로 Function Calling을 지원합니다.

예를 들어, "서울 날씨 알려줘"라는 질문에 get_weather(city="서울") 함수를 호출하도록 모델이 JSON을 생성합니다. 기존에는 정규표현식이나 복잡한 파싱이 필요했다면, 이제는 모델이 자연어를 구조화된 함수 호출로 변환해줍니다.

핵심은 함수 스키마를 OpenAI 형식으로 정의하고 시스템 프롬프트에 포함시키는 것입니다. 모델은 이 스키마를 보고 언제, 어떻게 함수를 호출할지 학습합니다.

잘 설계된 스키마는 정확한 함수 호출을 보장하고 에러를 줄여줍니다.

코드 예제

# Function Calling 구현
import json

# 사용 가능한 함수 정의 (OpenAI 형식)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "주어진 도시의 현재 날씨 정보를 조회합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "도시 이름 (예: 서울, 부산)"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "온도 단위"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# 함수 정의를 시스템 프롬프트에 포함
system_msg = f"당신은 유용한 어시스턴트입니다. 다음 함수들을 사용할 수 있습니다:\n{json.dumps(tools, ensure_ascii=False, indent=2)}"

messages = [
    {"role": "system", "content": system_msg},
    {"role": "user", "content": "서울의 날씨를 섭씨로 알려줘"}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.1)  # 낮은 temp로 정확성 향상
response = tokenizer.decode(outputs[0], skip_special_tokens=True)

# 응답에서 함수 호출 파싱
if "get_weather" in response:
    # 실제로는 JSON 파싱이 필요
    print("모델이 함수 호출을 요청했습니다:")
    print(response)

    # 여기서 실제 함수 실행
    # result = get_weather(city="서울", unit="celsius")
    # 결과를 다시 모델에 전달하여 최종 답변 생성

설명

이것이 하는 일: 이 코드는 사용자 요청을 분석하여 적절한 함수와 파라미터를 JSON 형식으로 추출하고, 실제 함수를 호출할 수 있도록 준비합니다. 첫 번째로, tools 리스트에 사용 가능한 함수들을 OpenAI 형식으로 정의합니다.

각 함수는 name(함수 이름), description(언제 사용하는지), parameters(받는 인자들)를 포함합니다. description이 매우 중요한데, 모델이 이를 보고 언제 이 함수를 호출할지 판단하기 때문입니다.

parameters는 JSON Schema 형식으로 작성됩니다. properties에 각 파라미터의 타입과 설명을 명시하고, required에 필수 파라미터를 나열합니다.

enum으로 허용되는 값을 제한할 수도 있습니다. 이렇게 상세히 정의하면 모델이 올바른 형식으로 파라미터를 생성합니다.

그 다음으로, 이 함수 정의들을 JSON 문자열로 변환하여 시스템 프롬프트에 포함시킵니다. ensure_ascii=False로 한글이 깨지지 않게 하고, indent=2로 가독성을 높입니다.

모델은 이 프롬프트를 보고 "아, 내가 이 함수들을 사용할 수 있구나"라고 인식합니다. temperature=0.1로 낮게 설정하는 것이 중요합니다.

함수 호출은 정확성이 중요하므로 창의성보다는 결정적인 출력이 필요합니다. 높은 temperature는 잘못된 파라미터나 존재하지 않는 함수를 생성할 수 있습니다.

모델의 응답을 파싱하여 함수 호출 정보를 추출합니다. 실제 구현에서는 정규표현식이나 JSON 파서를 사용하여 함수 이름과 파라미터를 추출합니다.

그 다음 실제 Python 함수를 호출하고, 결과를 다시 메시지에 추가하여 모델에게 전달하면 모델이 결과를 자연어로 요약해줍니다. 여러분이 이 코드를 사용하면 날씨 조회, 데이터베이스 검색, 계산기, 이메일 전송 등 다양한 도구를 LLM과 연결할 수 있습니다.

이것이 바로 AI 에이전트의 핵심이며, 단순 챗봇을 넘어 실제로 일을 수행하는 어시스턴트를 만들 수 있게 해줍니다.

실전 팁

💡 함수 설명 작성 팁: description은 구체적이고 명확하게 작성하세요. "날씨 조회"보다 "주어진 도시의 현재 날씨 정보를 조회합니다"가 훨씬 좋습니다.

💡 에러 처리: 모델이 잘못된 JSON을 생성할 수 있습니다. try-except로 파싱 에러를 잡고, 검증 로직으로 파라미터를 확인하세요.

💡 멀티 턴 대화: 함수 결과를 messages에 tool 역할로 추가하고 다시 모델을 호출하면 결과를 해석한 답변을 받을 수 있습니다.

💡 여러 함수 호출: 복잡한 요청은 여러 함수를 순차적으로 호출해야 할 수 있습니다. ReAct 패턴으로 사고-행동-관찰 루프를 구현하세요.

💡 프롬프트 최적화: 함수가 많으면 토큰이 많이 소모됩니다. 사용자 요청과 관련된 함수만 필터링하여 전달하면 효율적입니다.


9. RAG(검색 증강 생성)로 외부 지식 활용하기

시작하며

여러분이 회사 내부 문서를 기반으로 질의응답 시스템을 만든다고 생각해보세요. Qwen 2.5는 아무리 똑똑해도 여러분 회사의 내부 규정이나 제품 매뉴얼은 모릅니다.

학습 데이터에 없는 정보는 답할 수 없죠. 환각(hallucination)도 큰 문제입니다.

모델이 자신 없는 내용에 대해 그럴듯하지만 틀린 답변을 만들어낼 수 있습니다. 특히 전문적이거나 최신 정보가 필요한 경우 더욱 그렇죠.

이번 카드에서는 RAG(Retrieval-Augmented Generation)를 구현하여 외부 문서에서 관련 정보를 검색하고 이를 바탕으로 답변하는 방법을 배워보겠습니다. 정확성과 신뢰성이 크게 향상됩니다.

개요

간단히 말해서, RAG는 사용자 질문에 관련된 문서를 먼저 검색한 뒤, 검색된 내용을 컨텍스트로 포함하여 모델이 답변을 생성하도록 하는 기법입니다. 핵심 과정은 3단계입니다: 문서를 임베딩하여 벡터 DB에 저장, 질문을 임베딩하여 유사한 문서 검색, 검색된 문서와 질문을 함께 모델에 전달.

예를 들어, "우리 회사 휴가 규정은?" 질문에 인사 규정 문서를 검색하여 제공합니다. 기존에는 모델이 기억에만 의존했다면, 이제는 필요한 정보를 실시간으로 찾아 활용할 수 있습니다.

마치 오픈북 시험처럼 말이죠. 필수 구성 요소는 임베딩 모델(텍스트를 벡터로 변환)과 벡터 데이터베이스(FAISS, Chroma 등)입니다.

임베딩 모델로 의미적 유사도를 계산하고, 벡터 DB로 빠르게 검색합니다. 이 조합이 정확하고 빠른 RAG 시스템을 만듭니다.

코드 예제

# RAG 시스템 구현 (FAISS + 임베딩)
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

# 임베딩 모델 로드 - 한국어 지원 모델
embedder = SentenceTransformer('jhgan/ko-sroberta-multitask')

# 지식 베이스 (실제로는 회사 문서, DB 등)
documents = [
    "우리 회사는 연차 휴가를 연간 15일 제공합니다. 입사 1년 후부터 사용 가능합니다.",
    "재택근무는 주 2회까지 가능하며, 사전에 팀장 승인이 필요합니다.",
    "점심시간은 12시부터 1시까지이며, 사내 구내식당을 무료로 이용할 수 있습니다.",
    "회사 노트북은 보안 정책에 따라 개인 용도로 사용할 수 없습니다."
]

# 문서를 임베딩으로 변환
doc_embeddings = embedder.encode(documents, convert_to_numpy=True)

# FAISS 인덱스 생성 및 문서 추가
dimension = doc_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)  # L2 거리 기반 검색
index.add(doc_embeddings.astype('float32'))

# 사용자 질문
query = "휴가는 며칠이야?"
query_embedding = embedder.encode([query], convert_to_numpy=True).astype('float32')

# 가장 유사한 문서 2개 검색
k = 2
distances, indices = index.search(query_embedding, k)

# 검색된 문서를 컨텍스트로 사용
retrieved_docs = [documents[i] for i in indices[0]]
context = "\n".join(retrieved_docs)

# RAG 프롬프트 구성
messages = [
    {"role": "system", "content": "당신은 회사 규정을 안내하는 어시스턴트입니다. 제공된 문서를 바탕으로만 답변하세요."},
    {"role": "user", "content": f"다음 정보를 참고하여 질문에 답하세요:\n\n{context}\n\n질문: {query}"}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

outputs = model.generate(**inputs, max_new_tokens=256)
answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f"답변: {answer}")

설명

이것이 하는 일: 이 코드는 사용자 질문과 의미적으로 유사한 문서를 찾아내고, 이를 근거로 정확하고 신뢰할 수 있는 답변을 생성합니다. 첫 번째로, SentenceTransformer로 임베딩 모델을 로드합니다.

'jhgan/ko-sroberta-multitask'는 한국어에 특화된 모델로, 문장의 의미를 768차원 벡터로 변환합니다. 의미가 유사한 문장은 벡터 공간에서 가까이 위치하게 됩니다.

영어라면 'all-MiniLM-L6-v2' 같은 모델을 사용할 수 있습니다. 그 다음으로, 지식 베이스의 모든 문서를 임베딩으로 변환합니다.

encode() 메서드가 텍스트 리스트를 받아 numpy 배열로 반환합니다. 실무에서는 수천~수백만 개의 문서를 처리할 수 있으며, 배치로 효율적으로 임베딩할 수 있습니다.

FAISS 인덱스를 생성하여 임베딩을 저장합니다. IndexFlatL2는 L2 거리(유클리드 거리)를 사용하는 가장 간단한 인덱스입니다.

문서가 많다면 IndexIVFFlat 같은 근사 검색 인덱스를 사용하여 속도를 높일 수 있습니다. add()로 모든 문서 임베딩을 인덱스에 추가하면 검색 준비가 완료됩니다.

사용자 질문도 동일한 임베딩 모델로 벡터화합니다. index.search()로 질문 벡터와 가장 가까운 k개 문서를 찾습니다.

distances는 거리(낮을수록 유사), indices는 문서 인덱스를 반환합니다. 이렇게 검색된 문서가 질문과 가장 관련성이 높은 정보입니다.

검색된 문서들을 하나의 context 문자열로 결합하여 프롬프트에 포함시킵니다. 시스템 프롬프트에 "제공된 문서를 바탕으로만 답변하세요"를 명시하여 모델이 환각을 하지 않도록 유도합니다.

이렇게 하면 모델은 주어진 컨텍스트 내에서 답변을 찾으려 노력합니다. 여러분이 이 코드를 사용하면 고객 지원 챗봇, 사내 지식 검색, 법률/의료 상담 등 정확성이 중요한 애플리케이션을 구축할 수 있습니다.

모델의 창의성과 외부 지식의 정확성을 결합한 강력한 시스템입니다.

실전 팁

💡 청킹 전략: 긴 문서는 512토큰 이하로 나누세요. 문장 경계에서 자르고, 약간의 오버랩(50토큰)을 두면 컨텍스트 손실을 줄입니다.

💡 재순위화(Reranking): 검색된 문서를 Cross-Encoder로 다시 점수 매기면 정확도가 크게 향상됩니다. sentence-transformers의 CrossEncoder를 사용하세요.

💡 하이브리드 검색: 키워드 검색(BM25)과 벡터 검색을 결합하면 더 좋은 결과를 얻습니다. 각각의 강점을 활용할 수 있죠.

💡 메타데이터 필터링: 날짜, 카테고리, 권한 등 메타데이터로 먼저 필터링한 뒤 벡터 검색하면 관련성이 높아집니다.

💡 답변 출처 표시: 검색된 문서의 인덱스나 제목을 답변과 함께 반환하여 사용자가 출처를 확인할 수 있게 하세요.


10. 멀티모달 처리로 이미지 이해하기

시작하며

여러분이 사진을 업로드하면 내용을 설명해주는 서비스를 만든다면 어떻게 해야 할까요? Qwen 2.5 기본 모델은 텍스트만 처리하지만, Qwen 2.5-VL 같은 비전-언어 모델은 이미지와 텍스트를 함께 이해할 수 있습니다.

실제로 많은 애플리케이션이 멀티모달 기능을 필요로 합니다. 의료 영상 분석, 제품 이미지 기반 검색, 문서 OCR 등 이미지와 텍스트가 결합된 작업이 점점 늘어나고 있죠.

이번 카드에서는 Qwen 2.5-VL 모델을 사용하여 이미지를 이해하고 설명하는 방법을 배워보겠습니다. 텍스트 전용 모델의 한계를 뛰어넘는 강력한 기능입니다.

개요

간단히 말해서, Qwen 2.5-VL은 이미지와 텍스트를 동시에 입력받아 이미지에 대한 질문에 답하거나 설명을 생성할 수 있는 비전-언어 모델입니다. 내부적으로 이미지 인코더와 텍스트 모델이 결합되어 있습니다.

예를 들어, 사진을 업로드하고 "이 이미지에 무엇이 있나요?"라고 물으면 모델이 사진을 분석하여 답변합니다. 기존에는 이미지 분류, OCR, 캡셔닝을 별도 모델로 처리했다면, 이제는 하나의 모델로 다양한 비전-언어 작업을 수행할 수 있습니다.

핵심은 이미지와 텍스트를 결합한 프롬프트입니다. 특수 토큰으로 이미지 위치를 표시하고, processor가 이미지를 전처리하여 모델이 이해할 수 있는 형태로 변환합니다.

이 방식으로 자연어와 비전 정보를 통합적으로 처리합니다.

코드 예제

# Qwen 2.5-VL로 이미지 이해하기
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
from PIL import Image
import torch

# VL 모델 로드 (비전-언어 통합 모델)
model_name = "Qwen/Qwen2-VL-7B-Instruct"
model = Qwen2VLForConditionalGeneration.from_pretrained(
    model_name,
    torch_dtype=torch.float16,
    device_map="auto",
    trust_remote_code=True
)

# VL 전용 프로세서 (이미지 + 텍스트 처리)
processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)

# 이미지 로드
image = Image.open("example.jpg")  # 여러분의 이미지 경로

# 비전-언어 프롬프트 구성
messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": image},  # 이미지 첨부
            {"type": "text", "text": "이 이미지에 무엇이 보이나요? 자세히 설명해주세요."}
        ]
    }
]

# 프로세서로 입력 준비
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(
    text=[text],
    images=[image],
    return_tensors="pt",
    padding=True
).to(model.device)

# 생성 실행
outputs = model.generate(
    **inputs,
    max_new_tokens=512,
    temperature=0.7
)

# 결과 디코딩
response = processor.batch_decode(outputs, skip_special_tokens=True)[0]
print(f"모델 응답: {response}")

설명

이것이 하는 일: 이 코드는 이미지를 모델이 이해할 수 있는 형태로 변환하고, 이미지와 관련된 질문에 대해 맥락을 이해한 답변을 생성합니다. 첫 번째로, Qwen2VLForConditionalGeneration으로 비전-언어 모델을 로드합니다.

이 모델은 기본 Qwen 2.5에 이미지 인코더(보통 ViT 계열)가 추가된 구조입니다. 이미지 인코더가 이미지를 임베딩으로 변환하면, 이를 텍스트 임베딩과 결합하여 처리합니다.

그 다음으로, AutoProcessor를 로드합니다. 일반 토크나이저와 달리 processor는 이미지 전처리(리사이징, 정규화 등)와 텍스트 토크나이징을 모두 수행합니다.

이미지와 텍스트의 형식을 모델이 기대하는 형태로 통일시키는 역할을 합니다. PIL Image로 이미지를 로드합니다.

JPEG, PNG 등 다양한 형식을 지원하며, URL에서 직접 로드하거나 numpy 배열을 변환할 수도 있습니다. 실무에서는 사용자 업로드, 카메라 입력, API에서 받은 이미지 등 다양한 소스를 처리합니다.

messages에서 content를 리스트로 구성하는 것이 핵심입니다. 각 요소가 type으로 이미지인지 텍스트인지 구분됩니다.

{"type": "image", "image": image}는 이미지 객체를 첨부하고, {"type": "text", ...}는 질문을 담습니다. 이 구조로 여러 이미지와 텍스트를 섞어서 전달할 수 있습니다.

processor() 호출 시 text와 images를 모두 전달합니다. 내부적으로 이미지는 텐서로 변환되고, 텍스트는 토큰화되며, 특수 토큰으로 이미지 위치가 표시됩니다.

최종적으로 모델은 이미지 임베딩과 텍스트 임베딩이 결합된 시퀀스를 받아 처리합니다. 여러분이 이 코드를 사용하면 이미지 캡셔닝, Visual Q&A, 문서 이해, 제품 설명 자동화 등 다양한 멀티모달 애플리케이션을 구축할 수 있습니다.

텍스트만으로는 불가능했던 풍부한 사용자 경험을 제공할 수 있습니다.

실전 팁

💡 이미지 해상도: 너무 큰 이미지는 자동으로 리사이징되지만, 미리 적절한 크기(예: 1024x1024)로 조정하면 메모리와 속도를 최적화할 수 있습니다.

💡 여러 이미지 처리: content에 여러 이미지를 추가할 수 있습니다. "첫 번째와 두 번째 이미지의 차이점은?" 같은 비교 질문도 가능합니다.

💡 OCR 활용: 문서 이미지에 "이 문서의 텍스트를 추출해줘"라고 하면 강력한 OCR로 동작합니다. 표나 복잡한 레이아웃도 잘 처리합니다.

💡 메모리 관리: VL 모델은 일반 모델보다 메모리를 더 사용합니다. 양자화(4비트)를 적극 활용하여 메모리를 절약하세요.

💡 프롬프트 엔지니어링: "자세히", "단계별로", "표 형식으로" 같은 지시를 추가하면 원하는 형식의 답변을 얻을 수 있습니다.


11. vLLM으로 프로덕션 배포하기

시작하며

여러분이 개발한 LLM 애플리케이션을 실제 서비스로 배포할 때 이런 문제에 직면합니다. "개발 환경에서는 잘 되는데 실제 사용자가 늘어나니 응답이 너무 느려." 동시에 여러 요청을 처리해야 하는데 일반 generate()는 한 번에 하나씩만 처리합니다.

프로덕션 환경에서는 처리량(throughput), 지연시간(latency), 자원 활용률이 매우 중요합니다. 같은 하드웨어로 더 많은 사용자를 서비스하려면 최적화가 필수죠.

이번 카드에서는 vLLM을 사용하여 Qwen 2.5를 고성능 추론 서버로 배포하는 방법을 배워보겠습니다. PagedAttention과 continuous batching으로 처리량을 몇 배나 높일 수 있습니다.

개요

간단히 말해서, vLLM은 LLM 추론을 위해 특별히 설계된 고성능 서빙 엔진으로, 메모리 효율성과 처리량을 극대화합니다. 핵심 기술은 PagedAttention입니다.

KV 캐시를 페이지 단위로 관리하여 메모리 단편화를 줄이고, 동적으로 할당/해제하여 더 많은 요청을 동시에 처리할 수 있습니다. 예를 들어, 일반 방식으로 10명을 서비스하던 GPU로 30명을 서비스할 수 있습니다.

기존에는 요청을 순차적으로 처리하거나 정적 배치로 묶었다면, 이제는 continuous batching으로 요청이 완료되는 즉시 새 요청을 추가하여 GPU를 항상 최대한 활용합니다. vLLM의 장점은 OpenAI API 호환 서버도 제공한다는 것입니다.

기존 OpenAI API를 사용하던 코드를 거의 수정 없이 로컬 vLLM 서버로 전환할 수 있어, 마이그레이션이 쉽고 비용을 크게 절감할 수 있습니다.

코드 예제

# vLLM으로 고성능 추론 서버 구축
from vllm import LLM, SamplingParams

# vLLM 엔진 초기화
llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    tensor_parallel_size=1,  # GPU 개수
    gpu_memory_utilization=0.9,  # GPU 메모리 90% 활용
    dtype="half",  # float16 사용
    trust_remote_code=True
)

# 샘플링 파라미터 설정
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.8,
    max_tokens=512,
    repetition_penalty=1.1  # 반복 방지
)

# 여러 프롬프트를 배치로 처리
prompts = [
    "Python의 장점을 3가지 설명해줘",
    "FastAPI와 Flask의 차이는?",
    "Docker를 사용하는 이유는?",
    "RESTful API 설계 원칙은?"
]

# 고성능 배치 생성 - continuous batching 자동 적용
outputs = llm.generate(prompts, sampling_params)

# 결과 출력
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"프롬프트: {prompt}")
    print(f"응답: {generated_text}\n")
    print(f"토큰 수: {len(output.outputs[0].token_ids)}")
    print("-" * 80)

# OpenAI API 호환 서버로 실행하려면:
# python -m vllm.entrypoints.openai.api_server \
#   --model Qwen/Qwen2.5-7B-Instruct \
#   --trust-remote-code \
#   --dtype half

설명

이것이 하는 일: 이 코드는 vLLM 엔진을 초기화하고 여러 요청을 최적화된 방식으로 동시에 처리하여 처리량을 몇 배 향상시킵니다. 첫 번째로, LLM 객체를 생성하여 vLLM 엔진을 초기화합니다.

tensor_parallel_size는 사용할 GPU 개수로, 여러 GPU에 모델을 분산할 수 있습니다. gpu_memory_utilization=0.9는 GPU 메모리의 90%를 KV 캐시용으로 사용하겠다는 의미입니다.

높을수록 더 많은 요청을 동시에 처리하지만, 너무 높으면 OOM 위험이 있습니다. 그 다음으로, SamplingParams로 텍스트 생성 파라미터를 정의합니다.

일반 generate()와 비슷하지만, repetition_penalty 같은 추가 옵션으로 품질을 더욱 제어할 수 있습니다. 이 파라미터는 모든 요청에 공통으로 적용되지만, 요청별로 다르게 설정할 수도 있습니다.

prompts 리스트에 여러 요청을 준비합니다. vLLM의 강력한 점은 이들이 동시에 처리된다는 것입니다.

일반 transformers는 배치 내 모든 시퀀스가 같은 길이로 생성될 때까지 기다려야 하지만, vLLM은 continuous batching으로 하나가 끝나면 즉시 새 요청을 추가합니다. llm.generate() 호출 시 내부적으로 PagedAttention이 동작합니다.

KV 캐시를 블록(페이지) 단위로 관리하여, 각 시퀀스가 필요한 만큼만 메모리를 차지합니다. 긴 시퀀스와 짧은 시퀀스가 섞여도 메모리를 효율적으로 사용할 수 있어, 전통적인 방식보다 2-3배 많은 요청을 처리할 수 있습니다.

outputs는 각 프롬프트에 대한 결과를 포함합니다. 각 output 객체는 생성된 텍스트, 토큰 ID, 로그 확률 등 상세 정보를 담고 있어 모니터링이나 디버깅에 유용합니다.

여러분이 이 코드를 사용하면 실제 프로덕션 환경에서 수천 명의 동시 사용자를 서비스할 수 있습니다. OpenAI API 서버 모드로 실행하면 기존 클라이언트 코드를 그대로 사용하면서 비용만 절감하는 것도 가능합니다.

실전 팁

💡 메모리 튜닝: --gpu-memory-utilization을 0.85부터 시작해 점진적으로 올리세요. 너무 높으면 불안정하고, 너무 낮으면 성능 손실이 있습니다.

💡 모니터링: vLLM은 Prometheus 메트릭을 제공합니다. Grafana로 처리량, 지연시간, 메모리 사용량을 실시간 모니터링하세요.

💡 스케일링: 트래픽이 많다면 여러 vLLM 인스턴스를 실행하고 로드밸런서(nginx, HAProxy)로 분산하세요. 각 GPU마다 별도 프로세스가 일반적입니다.

💡 양자화 결합: vLLM도 AWQ, GPTQ 같은 양자화를 지원합니다. --quantization awq 옵션으로 메모리를 더욱 절약하고 처리량을 높이세요.

💡 토큰 제한: max_model_len으로 최대 시퀀스 길이를 제한하면 메모리를 예측 가능하게 관리할 수 있습니다. 기본값은 모델의 최대 길이입니다.


12. 프롬프트 엔지니어링 Best Practices

시작하며

여러분이 Qwen 2.5를 사용할 때 같은 질문을 해도 어떻게 물어보느냐에 따라 답변 품질이 크게 달라지는 것을 경험하셨을 겁니다. "코드 짜줘"보다 "Python으로 이메일 검증 함수를 작성해줘.

정규표현식을 사용하고 주석을 포함해줘"가 훨씬 나은 결과를 냅니다. 프롬프트는 LLM과의 인터페이스입니다.

같은 모델이라도 프롬프트를 잘 작성하면 성능을 몇 배나 끌어올릴 수 있죠. 특히 비용과 성능이 중요한 프로덕션 환경에서는 프롬프트 최적화가 필수입니다.

이번 카드에서는 Qwen 2.5로부터 최고의 결과를 얻기 위한 프롬프트 엔지니어링 기법들을 배워보겠습니다. 실무에서 바로 적용할 수 있는 검증된 패턴들입니다.

개요

간단히 말해서, 프롬프트 엔지니어링은 모델이 원하는 방식으로 답변하도록 명확하고 구조화된 지시를 작성하는 기술입니다. 효과적인 프롬프트의 핵심 요소는 명확성(구체적인 지시), 컨텍스트(충분한 배경 정보), 구조(단계별 안내), 예시(few-shot learning)입니다.

예를 들어, "요약해줘"보다 "다음 기사를 3개 bullet point로 요약해줘. 각 포인트는 한 문장으로"가 훨씬 좋습니다.

기존에는 시행착오로 프롬프트를 작성했다면, 이제는 검증된 패턴과 기법을 활용하여 체계적으로 접근할 수 있습니다. 주요 기법으로는 Chain-of-Thought(단계별 사고 유도), Few-Shot Learning(예시 제공), Role Prompting(역할 부여), Output Formatting(출력 형식 지정)이 있습니다.

이들을 적절히 조합하면 복잡한 작업도 높은 정확도로 수행할 수 있습니다.

코드 예제

# 프롬프트 엔지니어링 예제 - Chain-of-Thought와 Few-Shot 결합
from transformers import AutoTokenizer, AutoModelForCausalLM

# 나쁜 프롬프트 예시
bad_prompt = "53 * 47 계산해줘"

# 좋은 프롬프트 예시 - Chain-of-Thought + Few-Shot
good_prompt = """당신은 정확한 수학 계산을 수행하는 전문가입니다.
단계별로 계산 과정을 보여주세요.

예시 1:
질문: 23 * 15를 계산해줘
단계 1: 23 * 10 = 230
단계 2: 23 * 5 = 115
단계 3: 230 + 115 = 345
답: 345

예시 2:
질문: 84 / 4를 계산해줘
단계 1: 80 / 4 = 20
단계 2: 4 / 4 = 1
단계 3: 20 + 1 = 21
답: 21

이제 당신 차례입니다:
질문: 53 * 47을 계산해줘"""

# 출력 형식 지정 프롬프트
structured_prompt = """다음 제품 리뷰를 분석하고 JSON 형식으로 결과를 반환하세요.

리뷰: "배송은 빨랐는데 제품 품질이 기대 이하였습니다. 가격 대비 아쉽네요."

다음 JSON 형식으로 응답하세요:
{
  "sentiment": "긍정/부정/중립",
  "aspects": {
    "배송": "긍정/부정/중립",
    "품질": "긍정/부정/중립",
    "가격": "긍정/부정/중립"
  },
  "summary": "한 문장 요약"
}"""

# Role Prompting 예시
role_prompt = """당신은 10년 경력의 시니어 Python 개발자입니다.
초급 개발자에게 설명하듯이, 명확하고 실용적인 조언을 제공하세요.

질문: 언제 리스트 대신 튜플을 사용해야 하나요?"""

# 모델에 프롬프트 전달 (이전 카드의 model, tokenizer 사용)
messages = [{"role": "user", "content": good_prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

outputs = model.generate(**inputs, max_new_tokens=512, temperature=0.3)  # 낮은 temp로 정확성 향상
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)

설명

이것이 하는 일: 이 코드는 다양한 프롬프트 엔지니어링 기법을 보여주며, 각 기법이 모델 성능에 어떻게 영향을 미치는지 비교할 수 있게 합니다. 첫 번째로, bad_prompt는 너무 간단하여 모델이 어떤 형식으로 답변할지, 과정을 보여줄지 등을 알 수 없습니다.

이런 프롬프트는 일관성 없는 결과를 낳고, 복잡한 작업에서는 오류가 발생하기 쉽습니다. 그 다음으로, good_prompt는 여러 기법을 결합합니다.

먼저 역할을 명확히 합니다("정확한 수학 계산을 수행하는 전문가"). 그런 다음 Few-Shot Learning으로 두 개의 예시를 제공하여 모델이 원하는 형식을 학습하게 합니다.

"단계별로 계산 과정을 보여주세요"는 Chain-of-Thought를 유도하여 중간 추론 과정을 생성하게 합니다. Few-Shot Learning의 핵심은 예시의 품질과 관련성입니다.

예시는 실제 작업과 유사해야 하고, 다양한 경우를 커버해야 합니다. 2-5개의 예시가 일반적이며, 너무 많으면 토큰 낭비이고 너무 적으면 패턴을 학습하기 어렵습니다.

structured_prompt는 출력 형식을 명시적으로 지정합니다. JSON 스키마를 보여주면 모델이 그 구조를 따르려고 노력합니다.

이는 파싱 오류를 줄이고 downstream 처리를 쉽게 만듭니다. "다음 JSON 형식으로 응답하세요"는 명확한 지시로 모델의 행동을 제약합니다.

role_prompt는 페르소나를 부여합니다. "10년 경력의 시니어 개발자"는 모델이 전문적이고 경험 기반의 답변을 생성하도록 유도합니다.

"초급 개발자에게 설명하듯이"는 설명 수준을 조정합니다. 이런 역할 지정은 톤, 스타일, 깊이를 제어하는 강력한 방법입니다.

temperature=0.3으로 낮게 설정하는 것도 중요합니다. 정확성이 중요한 작업(계산, 분류, 정보 추출)은 낮은 temperature가, 창의성이 중요한 작업(스토리, 브레인스토밍)은 높은 temperature가 적합합니다.

여러분이 이 기법들을 사용하면 같은 모델로도 훨씬 나은 결과를 얻을 수


#Qwen#LLM#Transformers#FineTuning#ModelOptimization#AI

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.