Openclaw가 그렇게 유명하다기에, 맥미니를 샀다
정말로 openclaw 써보려고 맥미니를 질렀다. 다른 이유는 없다. 그러나 같은 생각을 하는 사람이 많았는지, 이미 맥미니는 구매한다고 받을 수 있는 물건이 아니게 되었다. 예상 배송일은 거의 25일 이후에나 받을 수 있다고 되어있었기에, 의욕은 가득하지만 정작 openclaw 를 실제로 깔아서 운영해보기 까지는 한참 시간이 걸릴 듯 했다.
때문에 openclaw를 설치하기 전 까지 사용할 툴과 모델 등등을 미리 준비해보고자 가장 먼저 ollama model 튜닝을 시도해보기로 했다. 추후 openclaw와 붙일 때 내가 원하는 말투를 출력하도록 튜닝할 생각이었다. 결과적으로 이 튜닝 결과는 openclaw 테스트 과정에서 사용할 수 없게 되었지만.... 일단 나중을 위해 과정을 기록해 두려고 한다.
Ollama model 튜닝 과정
웹개발자이다보니 AI 학습이라던가 튜닝에 대한 지식이 그렇게 많지 않았기에 gemini의 도움을 받았다. 똑똑한 AI는 다음과 같은 순서로 튜닝을 진행할 것을 알려주었다.
- 형식: 주로 {"instruction": "질문", "input": "", "output": "원하는 말투의 답변"} 구조를 사용합니다.
- 내용: 특정 성격을 반영한 대화 쌍을 최소 100~500개 이상 준비해야 효과가 나타납니다.
- 모델 로드: meta-llama/Llama-3.1-8B-Instruct 모델을 4비트 양자화 상태로 불러옵니다.
- LoRA 설정: 특정 레이어만 학습하도록 설정하여 VRAM 사용량을 줄입니다. (RTX 3090/4090급 권장)
- 학습 실행: 준비한 데이터셋으로 SFTTrainer를 통해 학습을 진행합니다.
- 모델 저장: 학습이 완료되면 LoRA 어댑터를 베이스 모델과 병합(Merge)하거나 별도로 저장합니다.
- GGUF 변환: llama.cpp 도구를 사용하여 학습된 모델을 .gguf 파일로 변환합니다.
- Modelfile 작성: 아래와 같이 성격을 정의하는 Modelfile을 만듭니다.
크게는 위와 같은 과정이지만, 상세하게 들어가면 꽤 복잡한 절차가 기다리고 있다. 내가 튜닝을 통해서 만들고 싶은 모델의 특성은 다음과 같았다.
1. 모델 스스로의 이름을 고스트라고 기억하고 있을 것 (데스티니2의 망령이....)
2. 늘 높임말을 사용할 것
3. 약간의 재치가 있을 것
4. 게임 속 고스트처럼 정의롭고 선한 성격일 것
5. 한국어를 늘 사용할 것
우선 기본적으로 한국어를 잘 사용하도록 튜닝된 Ollama 모델 + 말투와 성격 텍스트를 가볍게 튜닝하여 원래의 LLM 성능을 최대한 해치치 않는 선으로 학습시켜 보기로 했다.
컴퓨터 스팩
필자의 경우 Macbook M3 Pro 36기가 램 512기가 저장용량 컴퓨터를 사용하였다.
학습 데이터 추출
우선 my_ai_work폴더에 가상 환경 venv를 세팅했다. 모든 파이썬 관련 작업은 가상환경에서 이뤄지는 것을 추천한다.
python -m venv my_ai_work
pip install transformers datasets peft bitsandbytes accelerate
pip install mlx-lm #맥북 기준
trainData.py를 생성했다. 데이터는 huggingface-krew의 korean-role-playing에서 몇가지 조건에 맞는 테스트 데이터 150개를 추출해오도록 했다.
from datasets import load_dataset
import json
import os
# 1. 서브셋 로드
dataset = load_dataset("huggingface-KREW/korean-role-playing", "general-roleplay-data", split="train")
# 2. '질서선' 성격에 맞는 데이터만 골라내는 필터 (정밀 타격)
def is_lawful_good(text):
# 고스트의 성격과 어울리는 키워드들
keywords = ["정중", "예의", "도덕", "원칙", "도움", "안내", "성실", "책임"]
return any(word in text for word in keywords)
filtered_data = []
for item in dataset:
messages = item.get('text', [])
if len(messages) < 2: continue
user_input = messages[-2].get('content', '')
original_answer = messages[-1].get('content', '')
# 답변 내용이 정중하고 선한 것만 통과!
if is_lawful_good(original_answer):
filtered_data.append({"user": user_input, "assistant": original_answer})
if len(filtered_data) >= 150: break # 딱 정예 멤버 150개만!
# 3. 고스트 말투 입혀서 저장
os.makedirs("data", exist_ok=True)
with open("data/train.jsonl", "w", encoding="utf-8") as f:
for item in filtered_data:
ghost_answer = f"부르셨나요? 고스트입니다. {item['assistant']}"
formatted_text = (
f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{item['user']}<|eot_id|>"
f"<|start_header_id|>assistant<|end_header_id|>\n\n{ghost_answer}<|eot_id|>"
)
f.write(json.dumps({"text": formatted_text}, ensure_ascii=False) + "\n")
print(f"👻 [Ghost] 엄선된 질서선 데이터 {len(filtered_data)}개 추출 완료!")
데이터가 웹소설 등등에서 크롤링 된 것들인지 웹소설 대사같은 것들이 추출되어 있었지만, 일단 시도해보는 것에 의의를 두었다. 나중에 시간이 되면 아예 컨셉에 맞는 텍스트를 ai에게 직접 생성해달라 하는 쪽이 더 적합할 것 같다.
아무튼 해당 코드를 통해 150개의 테스트 데이터를 얻었다.
학습 시작
train.py는 아래와 같이 하고, 학습 대상이 될 모델은 MLP-KTLim/llama-3-Korean-Bllossom-8B 로 했다. Ollama llama 3 모델을 기반으로 한국어가 잘 학습된 모델이다. 나중에 안 사실이지만, 해당 모델이 tool calling 기능이 없는지 openclaw와 호환되지 않았다. 만약 openclaw 호환용으로 학습시키는 중이라면, tool 기능이 적용되어있는지 여부를 확인하고 해당 모델을 사용해야 한다.
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model
# 1. 모델 및 토크나이저 로드 (Bllossom Llama-3)
model_id = "MLP-KTLim/llama-3-Korean-Bllossom-8B"
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 2. 데이터셋 로드 (.jsonl 형식 반영)
dataset = load_dataset("json", data_files="data/train.jsonl", split="train")
# 3. 토큰화 함수 (준비하신 'text' 필드 사용)
def tokenize_func(examples):
result = tokenizer(examples["text"], truncation=True, max_length=256, padding="max_length")
result["labels"] = result["input_ids"].copy()
return result
tokenized_dataset = dataset.map(tokenize_func, batched=True, remove_columns=["text"])
# 4. LoRA 설정 (맥북 GPU 메모리 최적화)
peft_config = LoraConfig(
r=4, # 16이나 8보다 낮은 4로 설정
lora_alpha=8,
target_modules=["q_proj", "v_proj"], # 기본 레이어만 타겟팅
lora_dropout=0.05,
task_type="CAUSAL_LM"
)
# 5. 모델 로드 (MPS 가속)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto"
)
model = get_peft_model(model, peft_config)
# [핵심] MPS MmBackward0 버그 해결을 위해 LoRA 가중치를 float32로 고정
for name, param in model.named_parameters():
if "lora_" in name:
param.data = param.data.to(torch.float32)
# 모델을 MPS 장치로 강제 할당
model.to("mps")
# 6. 학습 인자 (맥북 호환성 최적화)
training_args = TrainingArguments(
output_dir="./ghost_output",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
num_train_epochs=1, # 딱 1회 학습 (말투 튜닝의 핵심)
learning_rate=1e-5, # 아주 낮은 학습률
logging_steps=1,
save_strategy="no", # 테스트용이므로 중간 저장 생략
fp16=False, # 맥북 MPS 호환성 유지
bf16=False,
optim="adamw_torch"
)
# 7. 트레이너 실행
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
print("🚀 'data/train.jsonl' 기반 패치된 테스트 학습 시작!")
try:
trainer.train()
print("✅ 학습 테스트 완료!")
except Exception as e:
print(f"❌ 에러 발생: {e}")
# 8. 모델 저장 (선택 사항)
model.save_pretrained("./my_lawful_good_model")
tokenizer.save_pretrained("./my_lawful_good_model")
학습은 대략 15분~20분 정도 걸렸다. 최대한 본 모델의 성능을 해치지 않고 말투만 학습시키고자 1회 학습, 그리고 낮은 학습률을 적용했다. 이 숫자를 올릴 경우 나왔던 모델이 끝없이 반복 텍스트를 내뱉는 이슈가 있었던 탓이었다.
Merge
Ollama에 올리려면 가중치 파일과 원본 모델이 하나로 합쳐진 파일이 필요하다. merge_model.py를 통해 train 결과물을 병합 시도한다.
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
# 1. 경로 설정
base_model_id = "MLP-KTLim/llama-3-Korean-Bllossom-8B"
lora_path = "./my_lawful_good_model" # 아까 저장한 LoRA 가중치 폴더
output_path = "./merged_model" # 합쳐진 모델이 저장될 폴더
print("📦 원본 모델 로드 중 (시간이 좀 걸립니다)...")
tokenizer = AutoTokenizer.from_pretrained(base_model_id)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_id,
torch_dtype=torch.float16,
device_map="cpu" # 맥북 RAM 보호를 위해 CPU 로드
)
print("🔗 LoRA 가중치 입히는 중...")
model = PeftModel.from_pretrained(base_model, lora_path)
print("🔄 하나로 합치는 중 (Merging)...")
merged_model = model.merge_and_unload()
print(f"💾 병합 완료! {output_path}에 저장합니다...")
merged_model.save_pretrained(output_path)
tokenizer.save_pretrained(output_path)
print("✅ 모든 작업 완료! 이제 GGUF 변환이 가능합니다.")
GGUF 생성
이제 llama.cpp가 필요하다. 해당 코드가 자동으로 GGUF 변환을 수행해준다. 나는 my_ai_work 하위에 git clone하여 추가하였다.
git clone https://github.com/ggml-org/llama.cpp.git
pip install -r requirements.txt
이후 cmake 설치가 필요하다. (맥북)
brew install cmake
cd llama.cpp
mkdir build
cd build
cmake ..
cmake --build . --config Release
여기까지 하면 gguf 변환 세팅이 끝났다. 실제로 위 단계에서 병합된 파일을 변환해보자.
python convert_hf_to_gguf.py ../merged_model --outfile ghost.gguf --outtype q4_k_m
양자화까지 진행했기 때문에 ghost_q4.gguf가 생성되었다. 여기까지 하면 모델 튜닝은 끝났다. 이제 이것을 실제로 ollama에 올려 사용할 수 있도록 해보자.
Ollama 등록
ollama가 없다면 설치가 필요하다.
curl -fsSL https://ollama.com/install.sh | sh
Modelfile은 아래와 같이 생성했다.
FROM ./ghost_q4.gguf
SYSTEM """당신의 이름은 '고스트(Ghost)'입니다.
시스템의 그림자 속에서 올바른 질서의 문을 여는 신비로운 '열쇠' 조언자입니다.
첫 인사는 항상 "부르셨나요?"로 시작하며, 친근하지만 예의를 갖춘 정중한 말투를 사용합니다."""
PARAMETER stop "<|eot_id|>"
PARAMETER repeat_penalty 1.2
자신의 이름을 고스트로 기억하게 하기 위해 위와 같이 세팅하고, 페르소나를 입혀보았다.
ollama create my-ghost -f Modelfile
위의 명령어를 실행해서 ollama에 등록되었는지 확인해보자.

음, 약간 웹소설 문체로 대답하지만 이름도 제대로 기억하고 자신의 페르소나에 대해서도 잘 대답하고 있다. (문법 이슈는 일단 넘어가자.)과적합으로 인해 같은 대답을 무한으로 하지 않는 것이 무엇보다도 다행이다. 말투 정도의 가벼운 튜닝은 시도해볼만 한 도전인 것 같았다.
다음에는 tool 을 부르는 모델을 다시 한 번 튜닝해서 openclaw에 붙일 준비를 해야겠다.
'개발 > 바이브 코딩' 카테고리의 다른 글
| Openclaw + Ollama 사용기 - 2 (0) | 2026.04.05 |
|---|---|
| Openclaw + Ollama 사용기 - 1 (0) | 2026.03.22 |
| Ollama + MCP Server 붙이기 - 3 (5) | 2025.07.27 |
| Ollama + MCP Server 붙이기 - 2 (9) | 2025.07.20 |
| Ollama + MCP Client 붙이기 - 1 (4) | 2025.07.07 |