| Aspect | Prompt Engineering | RAG | Fine-Tuning |
|---|---|---|---|
| New knowledge | โ Limited | โ Best | โ Good (if in training data) |
| Style/tone control | โ ๏ธ Limited | โ No | โ Best |
| Cost to operate | Low | Medium (API + storage) | Low (same base model cost) |
| One-time cost | $0 | $ (embedding) | $$$ (training) |
| Iteration speed | Fast (edit prompt) | Medium (update docs) | Slow (retrain) |
{"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is 2+2?"}, {"role": "assistant", "content": "4"}]}
{"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the capital of France?"}, {"role": "assistant", "content": "Paris"}]}
import json
def create_training_data(inputs, outputs):
training_data = []
for user_input, assistant_output in zip(inputs, outputs):
entry = {
"messages": [
{"role": "system", "content": "You are a customer service agent for Acme Corp. Be polite and helpful."},
{"role": "user", "content": user_input},
{"role": "assistant", "content": assistant_output}
]
}
training_data.append(entry)
# Save to JSONL
with open("training_data.jsonl", "w") as f:
for entry in training_data:
f.write(json.dumps(entry) + "\n")
Instead of updating all model weights, LoRA adds small trainable matrices to selected layers. Much more efficient than full fine-tuning.
Quantizes the base model to 4-bit, then applies LoRA. Allows fine-tuning on a single consumer GPU.
| Method | Memory Required | Training Speed | Quality | Best for |
|---|---|---|---|---|
| Full Fine-Tuning | >80GB GPU (2-4xA100) | Slowest | Best | Production models with resources |
| LoRA | ~20-40GB (1xA100) | Moderate | ~90% of full | Most practical for fine-tuning |
| QLoRA | ~8-16GB (1xRTX 4090/3090) | Slower due to quantization | ~85-90% | Consumer GPUs, personal projects |
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
import torch
# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# Load base model with quantization
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto"
)
# LoRA config
lora_config = LoraConfig(
r=8, # rank
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# 1. Prepare and validate data
!openai tools fine_tunes.prepare_data -f training_data.jsonl
# 2. Upload file
openai.File.create(
file=open("training_data.jsonl"),
purpose="fine-tune"
)
# 3. Create fine-tuning job
openai.FineTuningJob.create(
training_file="file-abc123",
model="gpt-3.5-turbo"
)
# 4. Monitor progress
openai.FineTuningJob.retrieve("ftjob-abc123")
# 5. Use fine-tuned model
response = openai.ChatCompletion.create(
model="ft:gpt-3.5-turbo:my-org:custom-id",
messages=[{"role": "user", "content": "Hello!"}]
)
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-3-8b-bnb-4bit",
max_seq_length=2048,
dtype=None,
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
)
import json
from openai import OpenAI
def evaluate_model(base_model, fine_tuned_model, test_cases):
"""Compare base model vs fine-tuned model on test cases."""
client = OpenAI()
results = {"base": [], "fine_tuned": []}
for test in test_cases:
# Test base model
base_response = client.chat.completions.create(
model=base_model,
messages=[{"role": "user", "content": test["input"]}]
)
base_answer = base_response.choices[0].message.content
# Test fine-tuned model
ft_response = client.chat.completions.create(
model=fine_tuned_model,
messages=[{"role": "user", "content": test["input"]}]
)
ft_answer = ft_response.choices[0].message.content
# Compare (use LLM as judge or human review)
comparison = llm_judge(test["expected"], base_answer, ft_answer)
results["base"].append(comparison["base_score"])
results["fine_tuned"].append(comparison["ft_score"])
return {
"base_avg": sum(results["base"]) / len(results["base"]),
"ft_avg": sum(results["fine_tuned"]) / len(results["fine_tuned"])
}
If you have 10,000 queries/day with 1,000 input tokens and 200 output tokens:
You have 100 customer support conversations. How would you convert them into fine-tuning data? What would you include in the system message?
You have a single RTX 3090 (24GB VRAM) and want to fine-tune Llama 3 8B. Which method do you use? Why not full fine-tuning?
You spend $200/month on GPT-4 API calls. Fine-tuning GPT-3.5 costs $100 and reduces token usage by 30% (shorter prompts). Monthly API cost after fine-tuning? Payback period?