Service LLM API로 나만의 모델 학습시키기
AI 챗봇을 서비스에 붙이려는 팀이 흔히 마주치는 고민이 있습니다. GPT-5나 Claude Opus 같은 최상위 모델을 붙이면 답변 품질은 확실히 좋은데, 사용자가 늘어날수록 API 호출 비용이 트래픽에 정비례해서 불어납니다. 반대로 처음부터 작은 모델을 직접 학습시키려면 양질의 학습 데이터를 모으는 것부터가 큰 일입니다.
이 둘 사이의 절충안으로 업계에서 자주 쓰는 방법이 있습니다. 실력 좋은 모델에게 문제를 풀게 하고 그 답변을 교재 삼아 작고 저렴한 모델을 가르치는 방식입니다. 학계에서는 이걸 지식 증류(knowledge distillation)라고 부르는데, 이름은 낯설어도 원리는 단순합니다. 이 글에서는 이 방식을 실제로 어떻게 구현하는지, 그리고 왜 비용 면에서 유리한지를 정리합니다.
1. 선생 모델과 학생 모델이라는 아이디어
원리는 이렇습니다. 실력이 뛰어난 선생 모델(GPT, Claude, Gemini 같은 대형 모델)에게 우리가 다루려는 질문들을 왕창 풀게 합니다. 그 질문과 답변 쌍을 교재로 만들어서 작고 가벼운 학생 모델에게 그대로 가르칩니다.
학생 모델이 선생 모델만큼 똑똑해질 필요는 없습니다. 학생 모델은 우리가 실제로 다루는 좁은 범위의 질문에만 잘 답하면 됩니다. 상담 챗봇이라면 상담 관련 질문에, 코드 리뷰 도우미라면 그 회사의 코드 스타일에 맞는 리뷰만 잘하면 충분합니다. 세상 모든 질문에 답할 필요가 없는 만큼, 모델 크기도 훨씬 작아도 됩니다.
작은 모델은 실행 비용이 낮고 응답 속도도 빠릅니다. 문제는 "작고 저렴하면서도 우리 목적에는 잘 맞는" 학습 데이터를 어디서 구하느냐인데, 여기서 선생 모델의 답변이 그 역할을 합니다.
2. Service LLM API로 여러 모델의 답변 모으기
Tencent Cloud에서 제공하는 Service LLM API들이 있습니다.. 토큰 하나로 GPT, Gemini, Claude, Grok, DeepSeek, GLM, Kimi, MiniMax 같은 주요 LLM을 한 게이트웨이에서 호출할 수 있습니다. 모델마다 계정을 따로 만들고 API 키를 따로 관리하는 대신, 토큰 하나로 원하는 모델을 골라 부르는 구조입니다. 여기서는 VOD라는 제품을 예시로 합니다.
호출 방식은 세 가지 프로토콜 중 하나를 고르면 됩니다.
| 경로 | 형식 | 주요 대상 모델 |
|---|---|---|
/v1/chat/completions | OpenAI Completions | GPT, Gemini, Kimi, GLM, DeepSeek 등 |
/v1/messages | Anthropic Messages | Claude, MiniMax |
/v1/responses | OpenAI Responses (공식 권장) | GPT |
먼저 콘솔에서 토큰을 발급받습니다. 발급 자체는 vod.intl.tencentcloudapi.com 엔드포인트에 CreateAigcApiToken을 호출하는 방식이고 이 요청에는 SecretId/SecretKey 기반의 TC3-HMAC-SHA256 서명이 필요합니다. 일단 토큰을 받고 나면 실제 모델 호출은 훨씬 간단합니다. 발급받은 토큰을 Authorization: Bearer 헤더(또는 x-api-key)에 담아 넘기기만 하면 됩니다.
학습 데이터를 모으는 스크립트는 이런 식으로 짭니다. 우리가 다루려는 질문 목록을 준비해두고 선생 모델에 하나씩 던져서 답변을 JSONL 파일로 쌓습니다.
import json
import requests
WAND_TOKEN = "<발급받은 토큰>"
BASE_URL = "https://mmu.vod-qcloud.com/v1"
def ask_teacher(question: str, model: str = "gpt-5.1") -> str:
response = requests.post(
f"{BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {WAND_TOKEN}"},
json={
"model": model,
"messages": [
{"role": "system", "content": "당신은 우리 서비스의 상담 전문가입니다."},
{"role": "user", "content": question}
],
"temperature": 0.3
},
timeout=30
)
response.raise_for_status
return response.json["choices"][0]["message"]["content"]
questions = load_questions("questions.txt") # 우리가 다루려는 질문 목록
with open("training_data.jsonl", "w", encoding="utf-8") as f:
for question in questions:
answer = ask_teacher(question)
f.write(json.dumps({"question": question, "answer": answer}, ensure_ascii=False) + "\n")여기서 한 가지는 분명히 해둘 필요가 있습니다. 이 Service LLM API 자체는 모델을 학습시켜주는 기능이 없습니다. 이 API가 하는 일은 선생 모델의 답변을 모으는 단계까지입니다. 그렇게 모은 데이터로 작은 모델을 실제로 파인튜닝하는 작업은 별도의 학습 파이프라인(오픈소스 프레임워크든, 다른 학습 인프라든)에서 진행해야 합니다. 이 API가 대체해주는 건 "여러 선생 모델과 각각 따로 계약하고 연동하는 번거로움"이지, 학습 과정 자체가 아닙니다.
기본 사용량 제한은 분당 30회, 분당 30만 토큰(30 RPM / 300K TPM)입니다. 질문이 수천 개 단위로 많다면 이 제한에 맞춰 요청 속도를 조절해야 합니다. 초과하면 HTTP 429가 돌아옵니다.
하나 더, 실무에서 챙겨야 할 부분이 있습니다. 모델 제공사마다 자사 모델의 출력물을 학습 데이터로 재사용하는 것을 두고 약관이 다릅니다. 데이터를 모으기 전에 사용하려는 모델의 이용약관을 한 번 확인해두는 게 안전합니다.
3. 왜 비용 면에서 유리한가
숫자로 감을 잡아보면 이렇습니다. 상담 챗봇에 하루 10만 건의 질문이 들어온다고 가정해봅니다. 매 요청마다 최상위 모델을 직접 호출하면 이 비용은 트래픽이 늘어나는 만큼 그대로 커집니다. 반면 처음에 대표 질문 1,000~2,000개 정도로 선생 모델을 호출해 학습 데이터를 만들고 그걸로 학생 모델을 한 번 학습시켜두면, 이후 10만 건이든 100만 건이든 실제 운영 비용은 작은 모델을 돌리는 비용으로 고정됩니다. 선생 모델 호출은 데이터를 만드는 초기 단계에 한 번 몰리고 운영 단계에서는 거의 발생하지 않습니다.
Tencent Cloud를 통한 Service LLM API 쪽 비용 구조도 이 흐름과 잘 맞습니다. 모델별로 별도 계약이나 최소 사용량 약정 없이 실제로 호출한 만큼만 과금되는 사용량 기반 방식입니다. 그래서 처음에 "어느 모델의 답변이 우리 데이터에 가장 잘 맞는지" 여러 모델을 조금씩 테스트해보는 단계에서도 큰 부담이 없습니다. 토큰 하나로 GPT와 Claude, DeepSeek을 같은 질문에 돌려보고 결과를 비교한 다음, 가장 적합한 모델만 골라 본격적으로 데이터를 뽑으면 됩니다.
4. 실제로 해보기: 상담 모델 직접 학습시켜보기
말로만 설명하면 진짜 되는지 알 수 없으니, 위 방법을 노트북 한 대에서 GPU 없이 그대로 실행해봤습니다. 가상의 스트리밍 서비스 "SignalPlay"를 하나 만들고(실존하지 않는 데모용 서비스입니다), 요금제와 정책을 정해둔 다음 따라가 보겠습니다.
[SignalPlay 요금제]
- 베이직: 월 4,900원, SD 화질, 광고 있음, 동시접속 1대
- 스탠다드: 월 9,900원, HD 화질, 광고 없음, 동시접속 2대, 오프라인 다운로드 가능
- 프리미엄: 월 14,900원, 4K 화질, 광고 없음, 동시접속 4대, 오프라인 다운로드 가능
- 그 외: 7일 무료체험, 해지 시 환불 불가, 결제는 신용카드/카카오페이만
4-1. 선생 모델로 학습 데이터 만들기
위 코드로 gpt-5.1에 상담 질문 18개를 던져서 받은 답변입니다. 그대로 옮기면 이렇습니다.
{"question": "스탠다드 요금제는 한 달에 얼마예요?", "answer": "스탠다드 요금제는 한 달에 9,900원입니다."}
{"question": "동시접속 4대까지 되는 요금제는 뭐예요?", "answer": "동시접속 4대까지 가능한 요금제는 **프리미엄 요금제**입니다. \n- 월 14,900원 \n- 4K 화질 \n- 광고 없음 \n- 동시접속 4대 \n- 오프라인 다운로드 가능"}
{"question": "고객센터는 주말에도 운영하나요?", "answer": "고객센터는 **평일 09:00~18:00만 운영**하며, **주말과 공휴일에는 휴무**입니다."}4-2. 학생 모델은 5억 개 파라미터짜리 초소형 모델
학생 모델로는 Qwen2.5-0.5B-Instruct라는 오픈소스 모델을 로컬에 받았습니다. 요즘 이름이 알려진 모델들이 보통 수백억에서 수조 개 파라미터인 것과 비교하면 이 모델은 그 1000분의 1 수준입니다.
학습시키기 전에 먼저 이 모델에게 SignalPlay를 물어봤습니다.
Q: 스탠다드 요금제는 한 달에 얼마예요? A (학습 전): 스탠다드 요금제는 각 서비스마다 다를 수 있지만, 일반적으로 SignalPlay는 1000원이상의 요금을 제공합니다. 그러나 이는 최대한 낮아지는 요금으로 추정됩니다.
Q: 무료체험 기간이 며칠이에요? A (학습 전): 죄송합니다, 저는 인공지능으로서 무료 체험 기간을 제공할 수 없습니다. 하지만 SignalPlay의 웹사이트나 앱에서 직접 체험 가능성을 확인하실 수 있습니다.
당연히 전혀 모릅니다. SignalPlay는 이 세상에 없는 가상의 서비스니까요.
4-3. LoRA로 가볍게 파인튜닝
모델 전체를 다시 학습시키려면 4억 9천만 개 파라미터를 전부 건드려야 합니다. 데이터 18개로 그걸 다 학습시키면 금방 망가집니다. 그래서 LoRA라는 기법을 씁니다. 원본 가중치는 그대로 얼려두고 몇몇 층 옆에 작은 행렬 쌍을 새로 붙여서 그 작은 행렬만 학습시키는 방식입니다. 본체는 그대로 두고 작은 보정판만 새로 만드는 셈입니다.
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters
# trainable params: 540,672 || all params: 494,573,440 || trainable%: 0.1093target_modules가 "이 보정판을 어디에 붙일지"를 정합니다. 여기서는 어텐션 연산의 질문(query)과 값(value) 부분(q_proj, v_proj)에만 붙였습니다. r=8은 그 보정판의 크기(랭크)로, 작을수록 학습할 파라미터가 줄어듭니다. 실제로 학습되는 파라미터는 540,672개로, 전체 4억 9,457만 개 중 0.1093%뿐입니다.
다음으로 데이터를 모델이 학습할 수 있는 형태로 바꿔야 합니다. 질문과 답변을 하나의 대화로 이어 붙이되, 손실(loss)은 답변 부분에서만 계산되도록 질문 부분을 가려줍니다.
def build_example(question: str, answer: str):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question},
{"role": "assistant", "content": answer},
]
full_text = tokenizer.apply_chat_template(messages, tokenize=False)
prompt_messages = messages[:-1]
prompt_text = tokenizer.apply_chat_template(prompt_messages, tokenize=False, add_generation_prompt=True)
full_ids = tokenizer(full_text, return_tensors="pt")["input_ids"][0]
prompt_len = len(tokenizer(prompt_text, return_tensors="pt")["input_ids"][0])
labels = full_ids.clone
labels[:prompt_len] = -100 # 질문 부분은 채점 대상에서 제외
return full_ids, labels-100으로 채운 부분은 파이토치가 loss 계산에서 그냥 건너뜁니다. 이 처리를 안 하면 모델이 "고객의 질문을 예측하는 연습"까지 같이 하게 되는데 그건 우리가 원하는 게 아닙니다. 우리가 가르치고 싶은 건 오직 "이 질문에 이렇게 답하라"는 부분입니다.
이제 데이터 18개를 준비된 형태로 만들고 학습 루프를 돕니다.
examples = [build_example(r["question"], r["answer"]) for r in rows]
optimizer = torch.optim.AdamW(model.parameters, lr=2e-4)
model.train
for epoch in range(EPOCHS):
epoch_loss = 0.0
for input_ids, labels in examples:
outputs = model(input_ids=input_ids.unsqueeze(0), labels=labels.unsqueeze(0))
loss = outputs.loss
optimizer.zero_grad
loss.backward
optimizer.step
epoch_loss += loss.item한 에폭마다 18개 데이터를 전부 한 번씩 보여주고 그걸 10번 반복했습니다. 매번 모델이 낸 답과 실제 정답의 차이(loss)를 계산해서 그 차이를 줄이는 방향으로 LoRA 행렬만 아주 조금씩 조정합니다. 10 에폭 동안 loss가 이렇게 떨어졌습니다.
| 에폭 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| loss | 1.47 | 1.21 | 0.99 | 0.80 | 0.61 | 0.46 | 0.36 | 0.28 | 0.22 | 0.20 |
첫 스텝 loss 0.73에서 마지막 스텝 0.13까지, 학습이 실제로 일어나고 있다는 걸 숫자로 확인할 수 있습니다. 데이터가 18개뿐이라 GPU 없이 CPU만으로도 전체 학습이 금방 끝났습니다.
여기서 한 가지 짚어야 할 점이 있습니다. loss가 이렇게 꾸준히, 거의 일직선으로 떨어지는 건 데이터가 딱 18개뿐이라 모델이 그 18개를 통째로 외워버렸기 때문입니다. 정식 학습이라면 검증용 데이터를 따로 떼어놓고 그쪽 loss가 다시 올라가는 시점(과적합)을 지켜봐야 하지만, 이번 데모는 "학습이 실제로 반영되는지"를 보여주는 목적이라 그 과정은 생략했습니다.
4-4. 학습 후 같은 질문을 다시 던져보기
같은 모델에 같은 질문을 다시 물었습니다.
| 질문 | 학습 전 | 학습 후 |
|---|---|---|
| 스탠다드 요금제는 한 달에 얼마예요? | "일반적으로 1000원 이상의 요금을..." | "스탠다드 요금제는 한 달에 9,900원입니다." |
| 무료체험 기간이 며칠이에요? | "무료 체험 기간을 제공할 수 없습니다" | "SignalPlay 신규 가입 시 무료체험 기간은 7일입니다." |
| 동시접속 4대까지 되는 요금제는 뭐예요? | "실제 요금제를 제공할 수 없습니다" | "동시접속 4대까지 가능한 요금제는 프리미엄 요금제입니다." |
18개 질문-답변 쌍만으로도 없던 지식이 그대로 들어간 게 눈에 보입니다.
4-5. 정직하게 짚어야 할 한계
학습 데이터에 없던 질문도 던져봤습니다.
Q: SD 화질로 보는 요금제는 얼마짜리예요? A (학습 후): SD화질 요금제는 월 9,900원입니다.
틀렸습니다. SD 화질은 베이직(4,900원)이고 9,900원은 HD인 스탠다드 요금제입니다. 학습 데이터에 있던 문장 패턴은 잘 재현하지만 "SD = 베이직"이라는 연결까지는 18개의 예시만으로 배우지 못한 겁니다.
이게 바로 이런 소형 모델을 실전에 붙일 때 폴백 구조가 필요한 이유입니다. 학습 데이터를 수백~수천 개로 늘리고 사람이 검수하는 과정을 거치면 이런 오답은 크게 줄어들지만 완전히 없어지지는 않습니다. 그래서 앞서 말한 것처럼, 학생 모델이 자신 없어 하는 질문은 선생 모델로 넘기는 구조를 같이 두는 게 안전합니다.
마치며
정리하면 이렇습니다. Tencent Cloud Service LLM API는 토큰 하나로 여러 상위 모델의 답변을 모을 수 있게 해주는 창구입니다. 그렇게 모은 답변을 교재 삼아 우리 목적에 맞는 작은 모델을 학습시키는 게 이번 글에서 다룬 방법입니다. 실제로 18개의 질문과 답변만으로도 5억 파라미터짜리 모델이 없던 지식을 답할 수 있게 되는 걸 직접 확인했고, 이 정도 데이터로는 학습 범위를 벗어난 질문에서 틀릴 수 있다는 한계도 함께 봤습니다. 모델 하나하나와 따로 계약하고 연동하는 수고를 줄여주고 여러 모델을 가볍게 테스트해볼 수 있게 해주니, 이 방식의 진입 장벽도 크게 낮아집니다.
모든 서비스에 이 방식이 정답인 건 아닙니다. 다루는 질문의 범위가 좁고 반복적일수록, 그리고 트래픽이 클수록 이득이 커집니다. 반대로 질문의 폭이 넓고 예측하기 어려운 서비스라면 처음부터 대형 모델을 그대로 쓰는 편이 나을 수 있습니다. 하지만 특정 도메인에 집중된 서비스를 운영 중이라면 한 번쯤 시도해볼 만한 비용 절감 방법입니다.