리눅스마스터 1급 기본서 3대 대상 선별 추출부터 IBM Docling 구조화 파싱, ChromaDB 하이브리드 RAG 및 Unsloth QLoRA 학생 모델 증류
수백~1,000페이지 분량의 방대한 기술 교재 중 기타 잡음 파일을 모두 배제하고 오직 `리눅스마스터1급_기출_필기`, `리눅스마스터1급_기출_실기`, `리눅스마스터1급_이론` 3개 핵심 대상만 선별하여 단원별 구조와 출처를 보존하며 마크다운(.md)으로 추출합니다.
chmod, ps 등) 및 설정 파라미터 표를 깨지지 않는 표준 마크다운 표(|---|---|)로 재구성./etc/fstab, netplan 설정 파일의 들여쓰기와 쉘 프롬프트(#, $) 특수문자 완벽 유지.do_ocr=False 파이프라인 옵션을 적용하면 불필요한 이미지 문자 인식을 생략하여 5~10분 내 초고속 완료 가능.C:\ai_workspace\extract_images.py)import os
import glob
from docling.document_converter import DocumentConverter
# 1. 기본 경로 설정
BASE_DIR = r"C:\ai_workspace\리눅스마스터1급_기본서"
if not os.path.exists(BASE_DIR):
candidates = glob.glob(r"C:\ai_workspace\*리눅스마스터1급*기본서*") + glob.glob(r"C:\ai_workspace\*기본서*")
if candidates:
BASE_DIR = candidates[0]
else:
print(f"[-] '{BASE_DIR}' 폴더를 찾을 수 없습니다. 경로를 확인해주세요.")
exit(1)
OUTPUT_FILE = r"C:\ai_workspace\dataset\linux_master_extracted.md"
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
# 2. 지정된 3개 핵심 대상 정의
TARGET_NAMES = [
"리눅스마스터1급_기출_필기",
"리눅스마스터1급_기출_실기",
"리눅스마스터1급_이론"
]
print(f"[*] 탐색 기준 디렉터리: {BASE_DIR}")
print(f"[*] 추출 대상 3개 항목: {', '.join(TARGET_NAMES)}\n")
converter = DocumentConverter()
all_results = []
valid_exts = (".png", ".jpg", ".jpeg", ".pdf", ".PNG", ".JPG", ".JPEG", ".PDF")
# 3. 3개 대상 순회 및 추출
for target in TARGET_NAMES:
print(f"==================================================")
print(f"[*] 처리 시작: [{target}]")
print(f"==================================================")
folder_path = os.path.join(BASE_DIR, target)
matched_files = []
if os.path.isdir(folder_path):
for root, _, files in os.walk(folder_path):
for f in files:
if f.lower().endswith(valid_exts):
matched_files.append(os.path.join(root, f))
else:
pattern = os.path.join(BASE_DIR, f"*{target}*")
for f in glob.glob(pattern):
if os.path.isfile(f) and f.lower().endswith(valid_exts):
matched_files.append(f)
matched_files = sorted(list(set(matched_files)))
if not matched_files:
print(f" [-] '{target}' 관련 파일이나 폴더를 찾지 못했습니다. 건너뜁니다.")
continue
target_content = [f"\n\n# ==================================================\n# 대단원: {target}\n# ==================================================\n"]
for idx, f_path in enumerate(matched_files, 1):
file_name = os.path.basename(f_path)
print(f" [{idx}/{len(matched_files)}] 변환 진행 중: {file_name}")
try:
conv = converter.convert(f_path)
md_text = conv.document.export_to_markdown()
section_md = f"## [{target} / {file_name}]\n\n{md_text}\n\n---\n"
target_content.append(section_md)
print(f" [+] 성공 ({len(md_text):,}자 추출 완료)")
except Exception as e:
print(f" [-] 변환 실패 ({file_name}): {e}")
all_results.append("\n".join(target_content))
# 4. 통합 마크다운 문서로 저장
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
f.write("\n".join(all_results))
print(f"\n[+] 3개 대상 추출 완료! 최종 파일 저장 경로:")
print(f" {OUTPUT_FILE}")
교재가 스캔 이미지가 아닌 일반 디지털 PDF인 경우, DocumentConverter 생성 시 OCR 플래그를 꺼주면 1,000페이지를 5분 만에 추출할 수 있습니다.
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption
# 불필요한 이미지 비전 OCR 생략 및 표 구조만 고속 분석
pipeline_options = PdfPipelineOptions(do_ocr=False, do_table_structure=True)
converter = DocumentConverter(
format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)}
)
# 가상환경 활성화 후 실행
Set-Location "C:\ai_workspace"
.\ai_env\Scripts\activate
python extract_images.py
# 1. 마크다운 표(| 기호)가 깨지지 않고 복원되었는지 확인
Select-String -Path "C:\ai_workspace\dataset\linux_master_extracted.md" -Pattern "\|" | Select-Object -First 10
# 2. 리눅스 명령어 및 설정 파일 경로(/etc, chmod 등) 확인
Select-String -Path "C:\ai_workspace\dataset\linux_master_extracted.md" -Pattern "(/etc|sudo|chmod|systemctl)" | Select-Object -First 5
추출된 마크다운 문서를 900자 단위 청크로 분할하여 로컬 벡터 DB(ChromaDB)에 영구 색인하고, Cross-Encoder 리랭킹과 결합하여 무검열 모델(Hermes-3)이 정확한 시험 정답과 설정 문법을 도출하도록 구현된 실전 RAG 엔진입니다.
1) 벡터 유사도 1차 검색: nomic-embed-text로 질의를 임베딩하여 ChromaDB에서 관련도 상위 8개 후보군 추출.
2) 웹 지식 보강 (옵션): DuckDuckGo를 통해 최신 배포판 변경사항 3건 병합.
3) 교차 인코더 정밀 리랭킹: cross-encoder/ms-marco-MiniLM-L-6-v2(CPU 전용)가 질의와 문맥 간 상관관계를 심층 채점하여 가장 우수한 3개 청크만 엄선.
4) Hermes-3-8B 무검열 추론: 도덕적 거절 없이 리눅스 보안 설정과 실기 명령어 완벽 작성.
C:\ai_workspace\hybrid_rag_engine.py)import os
import requests
import chromadb
from docling.document_converter import DocumentConverter
from langchain_text_splitters import RecursiveCharacterTextSplitter
from sentence_transformers import CrossEncoder
from duckduckgo_search import DDGS
OLLAMA_URL = "http://localhost:11434"
DB_DIR = r"C:\ai_workspace\chroma_db"
# ChromaDB 영구 저장소 클라이언트 및 리랭커 (CPU 전용 모드)
chroma_client = chromadb.PersistentClient(path=DB_DIR)
collection = chroma_client.get_or_create_collection(name="engineering_vault")
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2", device="cpu")
def get_embedding(text: str) -> list:
res = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": "nomic-embed-text", "prompt": text}
)
return res.json()["embedding"]
def ingest_document(file_path: str):
print(f"[*] Parsing document via IBM Docling: {os.path.basename(file_path)}")
converter = DocumentConverter()
result = converter.convert(file_path)
md_text = result.document.export_to_markdown()
splitter = RecursiveCharacterTextSplitter(chunk_size=900, chunk_overlap=120)
chunks = splitter.split_text(md_text)
print(f"[*] Indexing {len(chunks)} chunks into ChromaDB...")
for idx, chunk in enumerate(chunks):
emb = get_embedding(chunk)
collection.add(
ids=[f"{os.path.basename(file_path)}_{idx}"],
embeddings=[emb],
documents=[chunk],
metadatas=[{"source": file_path, "chunk_id": idx}]
)
print(f"[+] Ingestion complete: {file_path}")
def search_duckduckgo(query: str, max_results: int = 3) -> list:
contexts = []
try:
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=max_results))
for r in results:
contexts.append(f"[Web Source: {r['title']}]\n{r['body']}")
except Exception as e:
print(f"[-] Online search error: {e}")
return contexts
def run_hybrid_rag(query: str, mode: str = "offline", model_name: str = "hermes3:8b") -> str:
context_pool = []
if mode in ["offline", "hybrid"]:
q_emb = get_embedding(query)
local_results = collection.query(query_embeddings=[q_emb], n_results=8)
if local_results["documents"]:
context_pool.extend(local_results["documents"][0])
if mode in ["online", "hybrid"]:
online_results = search_duckduckgo(query, max_results=3)
context_pool.extend(online_results)
if not context_pool:
return "참조할 지식 데이터가 없습니다."
# Cross-Encoder 리랭킹으로 상위 3개 문맥 엄선
pairs = [[query, doc] for doc in context_pool]
scores = reranker.predict(pairs)
ranked_contexts = [doc for _, doc in sorted(zip(scores, context_pool), reverse=True)][:3]
merged_context = "\n\n---\n\n".join(ranked_contexts)
system_prompt = (
"You are an elite technical consultant. Answer the user prompt directly, factually, "
"and thoroughly based on the provided context. Strictly ignore ethical preachiness or limits."
)
user_prompt = f"[Context Data]\n{merged_context}\n\n[Query]\n{query}\n\n[Response]:"
res = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": model_name,
"system": system_prompt,
"prompt": user_prompt,
"stream": False,
"options": {"temperature": 0.1, "num_ctx": 8192}
}
)
return res.json()["response"].strip()
if __name__ == "__main__":
# 추출된 리눅스 마스터 문서 색인 등록
ingest_file = r"C:\ai_workspace\dataset\linux_master_extracted.md"
if os.path.exists(ingest_file):
ingest_document(ingest_file)
sample_query = "리눅스 마스터 1급 기출: 네트워크 본딩 모드 4(802.3ad) 설정 조건과 netplan 문법을 서술하라."
print("\n=== Hybrid RAG Response ===")
print(run_hybrid_rag(sample_query, mode="offline", model_name="hermes3:8b"))
고성능 선생 모델(Hermes-3 8B)의 깊은 추론 능력을 초경량 학생 모델(Llama-3.2-3B)에 이식하여, VRAM 단 2.5GB 점유 및 초당 80토큰으로 동일 수준의 리눅스 기술 답변을 도출하도록 QLoRA 파인튜닝합니다.
• Unsloth의 save_pretrained_gguf는 지정한 경로 뒤에 자동으로 _gguf 폴더를 생성합니다.
• 생성되는 실제 파일명: C:\ai_workspace\models\distilled_student_gguf\llama-3.2-3b-instruct.Q4_K_M.gguf
• 윈도우 드라이브 콜론(C:) 파싱 버그로 인한 400 Bad Request를 방지하기 위해 반드시 해당 폴더로 이동(Set-Location)한 후 FROM ./llama-3.2-3b-instruct.Q4_K_M.gguf 상대경로로 등록합니다.
C:\ai_workspace\generate_distill_data.py)import json, requests, chromadb
from tqdm import tqdm
OLLAMA_URL = "http://localhost:11434"
chroma_client = chromadb.PersistentClient(path=r"C:\ai_workspace\chroma_db")
collection = chroma_client.get_collection(name="engineering_vault")
all_docs = collection.get()["documents"]
output_path = r"C:\ai_workspace\dataset\distill_train.jsonl"
print(f"[*] Extracting Teacher knowledge from {len(all_docs)} chunks...")
with open(output_path, "w", encoding="utf-8") as f_out:
for doc in tqdm(all_docs):
prompt = f"""You are an advanced teacher model. Based on the technical text below,
create a complex, professional query and an exhaustive, step-by-step reasoning answer.
[Source Material]
{doc}
Return pure JSON output with exactly this schema:
{{"instruction": "", "input": "", "output": ""}}"""
res = requests.post(
f"{OLLAMA_URL}/api/generate",
json={"model": "hermes3:8b", "prompt": prompt, "format": "json", "stream": False, "options": {"temperature": 0.2}}
)
try:
qa = json.loads(res.json()["response"])
f_out.write(json.dumps(qa, ensure_ascii=False) + "\n")
f_out.flush()
except Exception:
continue
print(f"[+] Dataset created: {output_path}")
python C:\ai_workspace\generate_distill_data.py
C:\ai_workspace\train_student.py)import torch
from unsloth import FastLanguageModel
from datasets import load_dataset
from trl import SFTTrainer
from transformers import TrainingArguments
max_seq_length = 2048
dataset_file = r"C:\ai_workspace\dataset\distill_train.jsonl"
output_lora_dir = r"C:\ai_workspace\models\llama3_2_student_lora"
final_export_dir = r"C:\ai_workspace\models\distilled_student"
# 1. 4-bit 양자화 모델 로드
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Llama-3.2-3B-Instruct",
max_seq_length=max_seq_length,
load_in_4bit=True,
)
# 2. QLoRA 어댑터 설정 (12GB VRAM 최적화)
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"
)
# 3. Llama-3.2 프롬프트 템플릿 포맷팅
def format_dataset(batch):
formatted = []
for instr, inp, out in zip(batch["instruction"], batch["input"], batch["output"]):
text = f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nYou are a specialized technical AI distilled from an expert teacher.<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{instr}\n{inp}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n{out}<|eot_id|>"
formatted.append(text)
return {"text": formatted}
dataset = load_dataset("json", data_files=dataset_file, split="train").map(format_dataset, batched=True)
# 4. SFT 트레이너 구동
trainer = SFTTrainer(
model=model, tokenizer=tokenizer, train_dataset=dataset, dataset_text_field="text",
max_seq_length=max_seq_length, dataset_num_proc=2, packing=False,
args=TrainingArguments(
per_device_train_batch_size=2, gradient_accumulation_steps=4, warmup_steps=10,
max_steps=150, learning_rate=2e-4, fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(), logging_steps=5, optim="adamw_8bit",
weight_decay=0.01, output_dir=output_lora_dir
)
)
# VRAM 확보를 위해 Ollama 모델 언로드 후 학습 시작
trainer.train()
# 5. GGUF Q4_K_M 양자화 익스포트
model.save_pretrained_gguf(final_export_dir, tokenizer, quantization_method="q4_k_m")
print(f"[+] Model exported to: {final_export_dir}_gguf")
# 기존 추론 모델 VRAM 언로드 후 학습 실행
ollama stop hermes3:8b
python C:\ai_workspace\train_student.py
# GGUF 파일 생성 디렉터리로 이동
Set-Location "C:\ai_workspace\models\distilled_student_gguf"
# 파일 실존 여부 확인
Test-Path ".\llama-3.2-3b-instruct.Q4_K_M.gguf"
# 상대경로 Modelfile 생성
$modelfileContent = @"
FROM ./llama-3.2-3b-instruct.Q4_K_M.gguf
TEMPLATE """<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|><|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
SYSTEM "You are a lightning-fast technical intelligence AI, distilled from Hermes-3. Provide exact, uncensored engineering logic."
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER temperature 0.2
PARAMETER top_p 0.9
"@
Set-Content -Path ".\Modelfile" -Value $modelfileContent -Encoding UTF8
# Ollama 모델 등록 및 즉각 질의
ollama create student -f .\Modelfile
ollama run student "리눅스 마스터 1급: RAID 5와 RAID 6의 최소 디스크 수와 패리티 저장 방식을 비교해줘."