← 목록으로
회의록도 우리 회사 언어로 요약해주는 AI 만들기 — gcube에서 한국어 sLLM(Tri-7B) 서빙 + n8n 자동화
gcube · hands-on

회의록도 우리 회사 언어로 요약해주는 AI 만들기 — gcube에서 한국어 sLLM(Tri-7B) 서빙 + n8n 자동화

on this page

💡 회의록을 넣으면 담당자·할 일·기한이 정리된 액션아이템이 자동으로 나오는 파이프라인을, 자체 호스팅한 한국어 LLM으로 만들어봅니다. 외부 API 비용도 없고, 사내 회의록이 회사 밖으로 나가지도 않습니다. 이 글은 터미널 없이 Jupyter 노트북에서 위→아래로 셀만 실행하면 재현되도록 구성했습니다.


1. 왜 범용 모델이 아니라 자체 호스팅 + (선택적) 파인튜닝인가

ChatGPT나 Claude 같은 범용 모델에 회의록을 넣고 "액션아이템 뽑아줘"라고 하면 그럭저럭 나옵니다. 문제는 그다음입니다.

  • 담당자 호칭이 회사 규칙과 다르게 나옴
  • 부서명 표기가 매번 달라짐 ("마케팅팀" vs "마케팅본부")
  • 기한·형식이 제각각이라 Slack/노션에 붙여넣기 전에 사람이 한 번 더 손봐야 함
  • 그리고 무엇보다 회의록은 사내 민감 정보라 외부 API로 보내기 부담스러움

그래서 이번 편의 핵심은 두 가지입니다.

  1. 자체 호스팅: 회의록이 회사 밖으로 나가지 않도록, gcube GPU에 한국어 LLM을 직접 띄운다.
  2. (선택적) 파인튜닝: 회사 고유 포맷을 아예 모델에 학습시키면 짧은 프롬프트로도 항상 같은 형식이 나온다.

이번 글에서는 자체 호스팅 + 추론 + n8n 자동화까지를 처음부터 끝까지 재현하고, 파인튜닝은 "같은 파이프라인으로 확장 가능하다"는 지점까지 다룹니다.


2. 모델 선택: 왜 Tri-7B인가

한국어 업무 자동화가 목적이라 한국어에 강한 오픈소스 sLLM이 필요합니다. 이 글에서는 Trillion Labs의 trillionlabs/Tri-7B 를 사용합니다.

  • 한국어·영어·일본어를 함께 학습한 7.76B 모델로, 한국어 뉘앙스 처리에 강함
  • 컨텍스트 길이 32K → 긴 회의록도 통째로 넣기 좋음
  • 아키텍처가 표준 llama 계열이라, Unsloth·vLLM 같은 도구가 별도 설정 없이 그대로 동작 (뒤에서 확인함)

💡 라이선스 주의: Tri-7B는 Trillion Labs AI Model License Agreement를 따릅니다. 이 모델을 파인튜닝해 외부 배포할 경우 모델명 앞에 Tri- 접두사를 유지하고 Notice 파일을 포함해야 하며, MAU 100만 또는 연매출 1천만 달러를 초과하는 서비스는 별도 상업 라이선스가 필요합니다. 버전에 따라 Apache 2.0이 적용되는 경우도 있으니, 실제 배포 전 해당 모델 레포의 LICENSE 파일을 직접 확인하세요.


3. 실습 환경

항목
GPUgcube RTX 5090 (VRAM 32GB)
모델trillionlabs/Tri-7B
서빙vLLM (OpenAI 호환 API), 포트 8001
자동화n8n (별도 컨테이너)
작업 방식Jupyter 노트북 (터미널 불필요)

💡 포트 8001을 쓰는 이유: gcube 워크로드에 따라 8000번 포트가 기본 서비스로 이미 점유돼 있는 경우가 있습니다. 충돌을 피하기 위해 이 글에서는 8001로 통일합니다. 본인 환경에서 8000이 비어 있다면 8000을 써도 무방하며, 이 경우 아래 코드의 포트만 바꾸면 됩니다.

💡 노트북에서 진행하는 이유: 터미널이 익숙하지 않아도 셀을 순서대로 실행만 하면 되고, 각 셀의 출력이 그대로 남아 결과 확인·공유가 쉽습니다. vLLM 같은 상시 서버도 subprocess.Popen으로 백그라운드 실행하면 노트북이 멈추지 않습니다.

Jupyter Lab에서 새 노트북(Python 3 (ipykernel))을 하나 만들고, 아래 셀들을 순서대로 붙여넣어 실행하세요.


4. 노트북으로 따라 하기

셀 1 — 환경 확인

# GPU 확인
!nvidia-smi --query-gpu=name,memory.total,memory.used --format=csv
# 주요 패키지 버전 확인
import torch, transformers
print('torch       :', torch.__version__)
print('transformers:', transformers.__version__)
print('CUDA 사용 가능:', torch.cuda.is_available())
print('GPU:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A')

RTX 5090과 CUDA 사용 가능(True)이 확인되면 다음으로 넘어갑니다.


셀 2 — 모델 로드 확인 (transformers)

Tri-7B가 표준 아키텍처라 trust_remote_code 없이 로드됩니다. 최초 실행 시 모델(약 15GB)을 내려받으므로 몇 분 걸립니다.

💡 이때 노트북(=gcube 컨테이너)에서 Hugging Face로 나가는 아웃바운드 통신이 필요합니다. 다운로드가 막혀 있다면 gcube 네트워크 설정에서 외부 통신 허용 여부를 확인하세요. 한 번 받으면 캐시되어 이후에는 다시 받지 않습니다.

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

MODEL = 'trillionlabs/Tri-7B'
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16, device_map='auto')
print('OK: transformers 로드 성공')

# 다음 단계(vLLM 서빙)를 위해 GPU 메모리 비우기
del model, tok
import gc; gc.collect(); torch.cuda.empty_cache()
print('GPU 메모리 정리 완료')

OK: transformers 로드 성공이 뜨면 이 모델을 이 GPU에서 문제없이 쓸 수 있다는 뜻입니다.


셀 3 — (선택) 파인튜닝: 우리 회사 포맷 학습시키기

서빙만 해볼 거라면 이 셀은 건너뛰고 셀 4로 가세요. 베이스 모델만으로도 요약은 잘 됩니다. 다만 뒤(추론 결과)에서 보듯 날짜 계산처럼 회사·상황별 규칙이 개입하는 부분은 실행마다 흔들립니다. 이걸 고정하려면 우리 회사 회의록 포맷을 모델에 학습시키는 파인튜닝이 필요합니다.

💡 중요 — 순서: 파인튜닝(학습)과 vLLM 서빙은 같은 GPU를 두고 경쟁합니다. 둘을 동시에 올리면 메모리가 부족해 학습이 실패합니다. 그래서 파인튜닝을 먼저 끝내고(이 셀), GPU를 비운 뒤, 셀 4에서 서빙하는 순서로 진행합니다. 서빙을 이미 띄웠다면 학습 전에 pkill -f 'vllm serve'로 내려주세요.

데이터셋 준비 (각자 진행) 파인튜닝의 핵심은 데이터입니다. 우리 조직의 실제 회의록(익명화) 또는 그 포맷을 반영한 데이터를 아래 형식(ChatML/messages)의 JSONL로 준비하세요. 규모는 500~1,000건을 권장합니다. 데이터셋을 만드는 구체적 방법(실데이터 익명화, 합성 데이터 생성 등)은 각자 환경에 맞게 찾아 진행하시면 됩니다. 아래는 파인튜닝이 실제로 도는지 보여주기 위한 최소 예시입니다.

import json

# ⚠️ 예시용 최소 데이터 — 실제로는 우리 회사 회의록으로 수백 건을 준비하세요
samples = [
    {"messages":[
        {"role":"system","content":"당신은 회의록을 담당자/할 일/기한 형식의 액션아이템으로 정리하는 assistant입니다."},
        {"role":"user","content":"다음 회의록에서 액션아이템을 정리해줘:\n참석자: 김대리, 이과장\n김대리: 랜딩페이지 시안 다음주 수요일까지 필요해요.\n이과장: 개발 초안은 화요일까지 드릴게요."},
        {"role":"assistant","content":"- 담당자: 김대리 / 할 일: 랜딩페이지 시안 제작 / 기한: 다음주 수요일\n- 담당자: 이과장 / 할 일: 개발 초안 작성 / 기한: 다음주 화요일"}
    ]},
    # ... (같은 형식으로 실제 데이터를 채워 넣으세요)
]
data = samples * 20  # 예시라 복제해 60건 구성 (실제로는 다양한 실데이터 사용)

with open('/workspace/meeting_action_items.jsonl','w',encoding='utf-8') as f:
    for d in data:
        f.write(json.dumps(d, ensure_ascii=False)+'\n')
print('데이터셋 생성 완료:', len(data), '건')

학습 실행 (Unsloth QLoRA)

from unsloth import FastLanguageModel
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch

# 1) 4bit로 모델 로드
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name='trillionlabs/Tri-7B',
    max_seq_length=2048,
    load_in_4bit=True,
)

# 2) LoRA 어댑터 부착
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, use_gradient_checkpointing='unsloth',
)

# 3) 데이터셋 로드 + chat template 적용
ds = load_dataset('json', data_files='/workspace/meeting_action_items.jsonl', split='train')
ds = ds.map(lambda ex: {'text': tokenizer.apply_chat_template(ex['messages'], tokenize=False)})

# 4) 학습 (예시는 max_steps=10, 실제로는 데이터에 맞춰 epoch/steps 조정)
trainer = SFTTrainer(
    model=model, tokenizer=tokenizer, train_dataset=ds,
    args=SFTConfig(
        per_device_train_batch_size=2, gradient_accumulation_steps=2,
        max_steps=10, learning_rate=2e-4, logging_steps=1,
        output_dir='/workspace/tri7b-meeting-lora', dataset_text_field='text',
        max_seq_length=2048, report_to='none',
    ),
)
result = trainer.train()
print('학습 완료! final_loss =', round(result.training_loss, 4))

# 5) 학습된 LoRA 어댑터 저장
model.save_pretrained('/workspace/tri7b-meeting-lora')
tokenizer.save_pretrained('/workspace/tri7b-meeting-lora')
print('LoRA 어댑터 저장 완료: /workspace/tri7b-meeting-lora')

실제 실행 결과 (위 예시 데이터 기준):

데이터셋 생성 완료: 60 건
...
학습 완료! final_loss = 2.3759
LoRA 어댑터 저장 완료: /workspace/tri7b-meeting-lora

에러 없이 학습 완료가 뜨면 파인튜닝 파이프라인이 정상 동작하는 것입니다. 이후 실데이터로 규모·steps만 키우면 됩니다.

💡 Tri-7B는 model_typellama로 등록돼 있어 Unsloth가 "Fast Llama patching"으로 자동 인식합니다. 즉 기존 Llama·Qwen용 QLoRA 코드를 모델명만 바꿔 그대로 재사용할 수 있습니다.

학습 후 GPU 완전히 비우기 (서빙 전 필수 — 커널 재시작)

파인튜닝이 끝났다면, 서빙(셀 4)으로 넘어가기 전에 반드시 커널을 재시작하세요. 상단 메뉴 Kernel → Restart Kernel을 실행합니다.

💡 del + empty_cache()로는 부족한가 (실측): del modeltorch.cuda.empty_cache()를 호출해도 GPU가 완전히 비워지지 않습니다. 실제 테스트에서 약 7.4GB가 잔여 메모리로 남았고, 이 상태로 vLLM을 띄우면 다음 에러로 서빙이 실패합니다. `` ValueError: Free memory on device cuda:0 (23.57/31.36 GiB) on startup is less than desired GPU memory utilization (0.85, ...). Decrease GPU memory utilization or reduce GPU memory used by other processes. 이유는 del/empty_cache()`가 파이썬 참조만 정리할 뿐, 학습을 실행한 커널 프로세스가 붙잡은 CUDA 컨텍스트는 커널이 살아있는 한 해제되지 않기 때문입니다. 커널을 재시작해야만 GPU가 완전히 비워지고(확인: 사용량이 4 MiB로 복귀), 그 후 서빙이 정상 동작합니다.

커널을 재시작하면 앞서 실행한 변수(model, trainer 등)는 모두 사라지지만, 학습 결과인 LoRA 어댑터는 이미 /workspace/tri7b-meeting-lora에 저장돼 있으므로 그대로 서빙에 사용할 수 있습니다. 재시작 후에는 셀 1(패키지 import)만 다시 실행하고 셀 4로 넘어가면 됩니다.


셀 4 — vLLM 서버 백그라운드 기동

subprocess.Popen으로 백그라운드 실행하므로 이 셀은 즉시 끝나고, 서버는 계속 떠 있습니다.

💡 셀 3(파인튜닝)을 실행했다면: 이 셀 전에 커널을 재시작(Kernel → Restart Kernel) 했는지 확인하세요. 안 하면 학습 커널이 붙잡은 GPU 메모리 때문에 서빙이 ValueError: Free memory ... is less than desired GPU memory utilization로 실패합니다. 재시작 후 셀 1의 import만 다시 실행하고 이 셀로 오면 됩니다. (셀 3을 건너뛰고 서빙만 한다면 재시작 불필요.)

💡 무엇을 서빙하나: 셀 3 파인튜닝을 건너뛰었다면 아래처럼 베이스 모델(trillionlabs/Tri-7B) 을 그대로 서빙합니다. 파인튜닝을 했다면, vllm serve 뒤에 --enable-lora --lora-modules meeting=/workspace/tri7b-meeting-lora를 추가해 학습된 LoRA 어댑터를 함께 서빙하고, 요청 시 modelmeeting으로 지정하면 됩니다. 아래는 베이스 모델 기준입니다.

import subprocess

PORT = 8001
log = open('/workspace/vllm.log', 'w')
proc = subprocess.Popen([
    'vllm', 'serve', 'trillionlabs/Tri-7B',
    '--host', '0.0.0.0', '--port', str(PORT),
    '--max-model-len', '8192',
    '--gpu-memory-utilization', '0.85',
    '--enable-auto-tool-choice', '--tool-call-parser', 'hermes',
], stdout=log, stderr=subprocess.STDOUT)
print('vLLM 백그라운드 기동, PID =', proc.pid)

각 플래그의 의미:

  • --host 0.0.0.0 : 컨테이너 외부(=n8n 컨테이너)에서 접근할 수 있게 모든 인터페이스에 바인딩
  • --port 8001 : 서비스 포트 (8000 충돌 회피)
  • --max-model-len 8192 : 한 요청에서 처리할 최대 토큰 길이 (회의록 길이에 맞춰 조정 가능, 최대 32768)
  • --gpu-memory-utilization 0.85 : GPU 메모리의 85%까지 사용
  • --enable-auto-tool-choice --tool-call-parser hermes : OpenAI 호환 tool-calling 지원 (n8n·Open WebUI 등과의 호환을 위해 권장)

이어서, 서버가 완전히 뜰 때까지 기다립니다 (모델 적재 + CUDA 그래프 준비로 보통 1~2분).

import requests, time

BASE = 'http://localhost:8001'
for i in range(60):  # 최대 5분 대기
    try:
        r = requests.get(f'{BASE}/v1/models', timeout=3)
        if r.status_code == 200:
            print('서버 준비 완료:', r.json()['data'][0]['id'])
            break
    except Exception:
        pass
    print(f'  대기 중... ({i*5}s)')
    time.sleep(5)
else:
    print('타임아웃 - /workspace/vllm.log 를 확인하세요')

서버 준비 완료: trillionlabs/Tri-7B가 뜨면 서빙 성공입니다.


셀 5 — 추론 테스트 (회의록 → 액션아이템)

실제로 모델이 회의록을 요약하는지 확인합니다. n8n에서도 이와 완전히 동일한 방식(OpenAI 호환 /v1/chat/completions)으로 호출합니다.

import requests

meeting = '''회의 일시: 2026-07-08
참석자: 김대리(마케팅), 이과장(개발), 박팀장(기획)
박팀장: 다음 주 신제품 랜딩페이지 오픈이 목표입니다.
이과장: 개발은 목요일까지 초안 완료하겠습니다.
김대리: 카피와 배너 이미지는 제가 금요일까지 준비할게요.
박팀장: 좋습니다. QA는 금요일 오후에 다같이 진행하죠.'''

payload = {
    'model': 'trillionlabs/Tri-7B',
    'messages': [
        {'role': 'system', 'content': '당신은 회의록을 담당자/할 일/기한 형식의 액션아이템으로 정리하는 assistant입니다.'},
        {'role': 'user', 'content': '다음 회의록에서 액션아이템을 정리해줘:\n' + meeting},
    ],
    'temperature': 0.3,
    'max_tokens': 512,
}
r = requests.post('http://localhost:8001/v1/chat/completions', json=payload, timeout=60)
print(r.json()['choices'][0]['message']['content'])

실제 출력 예시 (Tri-7B, 파인튜닝 없이 프롬프트만으로):

다음은 회의록에서 추출한 액션아이템을 담당자/할 일/기한 형식으로 정리한 내용입니다:

1. 신제품 랜딩페이지 오픈
   - 담당자: 박팀장(기획)
   - 할 일: 신제품 랜딩페이지 기획 및 준비 총괄
   - 기한: 2026-07-15 (다음 주 목표)

2. 개발 초안 완료
   - 담당자: 이과장(개발)
   - 할 일: 신제품 관련 개발 초안 작성 및 완료
   - 기한: 2026-07-14 (목요일까지)

3. 카피와 배너 이미지 준비
   - 담당자: 김대리(마케팅)
   - 할 일: 신제품 홍보용 카피 문구 작성 및 배너 이미지 제작
   - 기한: 2026-07-15 (금요일까지)

4. QA(품질보증) 진행
   - 담당자: 박팀장(기획), 이과장(개발), 김대리(마케팅) (공동)
   - 할 일: 신제품 랜딩페이지 및 관련 요소들에 대한 QA 수행
   - 기한: 2026-07-15 (금요일 오후)

위 예시에서는 "다음 주 목표"를 2026-07-15로, "목요일까지"를 2026-07-14로 날짜까지 추론하고, 담당자·할 일을 정확히 배분했습니다. 담당자·할 일 배분과 형식은 베이스 모델만으로도 안정적으로 잘 나옵니다.

💡 참고 — 베이스 모델의 한계: 담당자·할 일 배분은 안정적이지만, 같은 회의록을 여러 번 돌려보면 날짜 계산은 실행마다 흔들립니다. 위 예시처럼 "목요일까지"를 정확히 07-14로 계산하는 실행도 있고, 회의 당일 날짜로 잘못 넣는 실행도 있습니다. temperature를 낮춰도 완전히 없어지지는 않습니다. 회의 요일 → 실제 날짜 변환처럼 회사·상황별 규칙이 개입하는 계산은 프롬프트만으로는 안정화가 어렵고, 아래 6번의 파인튜닝으로 고정하는 것이 확실합니다.


5. n8n에서 자동화하기

이제 이 vLLM 엔드포인트를 n8n 워크플로우에 연결합니다. 핵심 전제: n8n이 별도 컨테이너인 경우, localhost가 아니라 GPU 컨테이너의 주소로 호출해야 합니다.

5-1. GPU 컨테이너의 접속 주소 확인

노트북에서 아래를 실행해 GPU 컨테이너의 내부 IP를 확인합니다.

import socket
print('컨테이너 내부 IP:', socket.gethostbyname(socket.gethostname()))

출력된 IP(예: 10.0.10.208)가 n8n에서 vLLM을 호출할 때 쓸 주소입니다. n8n에서는 http://<이 IP>:8001 형태로 접근합니다. 컨테이너를 재시작하면 이 IP가 바뀌므로(실제로 이 글을 검증하는 동안에도 재시작 후 IP가 달라졌습니다), n8n 설정 전에 이 셀을 다시 실행해 현재 IP를 확인하세요.

💡 주의: 이 내부 IP는 컨테이너를 재시작하면 바뀔 수 있습니다. 일회성 테스트라면 IP를 직접 써도 되지만, 지속 운영한다면 gcube가 제공하는 고정 서비스 주소(있는 경우)로 대체하는 것을 권장합니다.

5-2. n8n 워크플로우 구성

n8n에서 새 워크플로우를 만들고 아래 순서로 노드를 배치합니다.

Step 1 — 트리거 회의록이 들어오는 경로에 맞는 트리거를 선택합니다.

  • 테스트 단계: Manual Trigger ("When clicking 'Execute workflow'")
  • 실제 운영: Notion에 새 페이지가 생기면 Notion Trigger, Slack 채널이면 Slack Trigger

Step 2 — HTTP Request 노드 (vLLM 호출) Manual Trigger 오른쪽 +를 눌러 HTTP Request 노드를 추가하고 다음과 같이 설정합니다.

  • Method: POST
  • URL: http://<5-1에서 확인한 IP>:8001/v1/chat/completions (예: http://10.0.10.208:8001/v1/chat/completions)
  • Send Body: 켜기
  • Body Content Type: JSON
  • JSON Body:
{
  "model": "trillionlabs/Tri-7B",
  "messages": [
    {"role": "system", "content": "당신은 회의록을 담당자/할 일/기한 형식의 액션아이템으로 정리하는 assistant입니다."},
    {"role": "user", "content": "다음 회의록에서 액션아이템을 정리해줘:\n{{ $json.meeting_text }}"}
  ],
  "temperature": 0.3,
  "max_tokens": 512
}

{{ $json.meeting_text }}는 앞 단계(트리거/추출 노드)에서 넘어온 회의록 본문 필드로 바꿔주면 됩니다. 단순 연결 테스트라면 이 부분을 고정 텍스트로 넣어도 됩니다.

Step 3 — 응답 파싱 (Edit Fields 노드) vLLM 응답은 choices[0].message.content 같은 깊은 경로에 요약이 들어 있습니다. 뒤 노드(Slack/Notion)에서 바로 쓰기 좋게, Edit Fields(Set) 노드로 이 값을 평평한 필드 하나로 꺼냅니다.

HTTP Request 오른쪽 +를 눌러 Edit Fields 노드를 추가하고:

  • Mode: Manual Mapping
  • 필드 하나 추가:
  • Name: action_items
  • Type: String
  • Value: {{ $json.choices[0].message.content }}

💡 주의: Value는 반드시 표현식(Expression) 모드로 입력해야 합니다. 입력칸을 그냥 텍스트로 두고 {{ $json... }}를 넣으면 그 문자열이 그대로 저장됩니다. 값 입력칸의 표현식 토글(= 아이콘)을 켠 뒤 넣으세요. 경로 $json.choices[0].message.content는 OpenAI 호환 응답 구조라 베이스 모델·파인튜닝 모델 모두 동일합니다.

Step 4 — 결과 전달 (Slack 또는 Notion) Edit Fields 오른쪽 +를 눌러 목적지 노드를 추가합니다. 두 노드 모두 인증 정보(크리덴셜) 입력이 먼저 필요하며, 이 부분은 본인 계정으로 직접 설정해야 합니다.

옵션 A — Slack (Slack → Send a Message)

  • Credential: Slack 계정 연결 (OAuth 또는 Bot Token)
  • Resource: Message / Operation: Send
  • Send Message To: Channel → 보낼 채널 선택
  • Message Text: {{ $json.action_items }} (표현식 모드)

옵션 B — Notion (Notion → Create a Page)

  • Credential: Notion Integration 연결
  • Resource: Database Page / Operation: Create
  • Database: 회의록 정리용 DB 선택
  • Title: 예) {{ $now.format('yyyy-MM-dd') }} 회의 액션아이템
  • 내용 속성: {{ $json.action_items }} (표현식 모드)
  • 사전 준비: ① notion.so/my-integrations에서 Integration Token 발급, ② 대상 DB 우측 ··· → Connections에서 해당 Integration 연결(공유). 이 두 가지를 해야 n8n이 DB에 접근할 수 있습니다.

이렇게 하면 전체 흐름은 다음과 같이 완성됩니다.

트리거 → HTTP Request(vLLM) → Edit Fields(action_items) → Slack 또는 Notion

5-3. 실행 및 결과 확인

우상단 Execute workflow를 클릭합니다. HTTP Request 노드가 초록색으로 완료되고, 출력 패널에서 choices[0].message.content에 담긴 액션아이템 요약을 확인할 수 있습니다.

💡 자주 걸리는 두 가지 함정 (실제로 재현하다 만난 것): 1. Method가 GET으로 나가서 "Method not allowed" 에러 — n8n HTTP Request 노드는 Method를 명시하지 않으면 기본값이 GET입니다. /v1/chat/completions는 POST만 받으므로, Method를 반드시 POST로 설정하세요. 2. {{ $json.meeting_text }}가 비어서 에러 — Manual Trigger만으로 실행하면 회의록을 넘겨주는 앞 노드가 없어 이 표현식이 빈 값이 됩니다. 연결 테스트 단계에서는 이 부분을 실제 회의록 텍스트로 바꿔 넣으세요.

💡 연결만 먼저 확인하는 팁: 추론은 10~20초 걸리니, 도달 여부만 빠르게 보려면 URL을 잠깐 .../v1/models(GET)로 바꿔 실행합니다. trillionlabs/Tri-7B가 담긴 모델 목록 JSON이 오면 연결 성공이고, timeout이나 connection refused가 뜨면 IP/포트 또는 네트워크 정책을 다시 점검해야 합니다. 확인이 끝나면 다시 POST .../v1/chat/completions로 돌려놓으세요.

이렇게 하면 회의록 입력 → n8n → 자체 호스팅 Tri-7B → 액션아이템 → Slack/노션 자동 정리까지, 외부 API 없이 무인화됩니다.


6. 예시를 넘어 실전으로 — 파인튜닝 확장

셀 3에서 파인튜닝이 실제로 도는 것까지 확인했습니다(예시 데이터로 학습 완료). 이제 이걸 실전 품질로 끌어올리는 건 결국 데이터의 문제입니다.

항목예시(셀 3)실전 권장
데이터 출처복제한 샘플 3종우리 회사 실제 회의록(익명화)
규모60건 (반복)500~1,000건 이상
max_steps10 (동작 확인용)데이터에 맞춰 epoch 단위로

💡 데이터는 반드시 "우리 회사" 것으로: 파인튜닝의 목적은 범용 요약이 아니라 우리 조직 고유의 회의 포맷·규칙(부서명 표기, 담당자 호칭, 요일→날짜 변환 등)을 재현하는 것입니다. 다른 회사 예시로 학습하면 "남의 회사 말투로 요약하는 모델"이 될 뿐입니다. 실제 회의록이 있다면 참석자명·민감정보를 익명화해 사용하고, 데이터셋을 구성·확보하는 구체적 방법은 각자 환경에 맞게 진행하세요.

학습이 끝나면 셀 4에서 LoRA 어댑터를 함께 서빙(--enable-lora --lora-modules ...)하고, 추론·n8n 요청의 model 값을 학습된 어댑터 이름으로 바꾸면 됩니다. 자동화 파이프라인(5번) 구조는 그대로 두고 모델만 교체하는 셈입니다.


7. 서버 정리

작업이 끝나면 GPU를 반환하기 위해 vLLM을 종료합니다.

proc.terminate()
print('vLLM 종료됨')

노트북 커널을 재시작했다면 proc 변수가 사라지므로, 아래처럼 종료할 수도 있습니다.

import subprocess
subprocess.run(['pkill', '-f', 'vllm serve'])
print('vLLM 프로세스 종료 요청 완료')

💡 vLLM을 종료했는데도 nvidia-smi상 GPU 메모리가 남아 있다면, 현재 커널이 CUDA 컨텍스트를 붙잡고 있는 것입니다. Kernel → Restart Kernel로 커널을 재시작하면 완전히 해제됩니다.


8. 마무리

이번 편에서는 회의록 요약이라는 실무 과제를, 회사 밖으로 데이터를 내보내지 않고 gcube GPU에 한국어 모델(Tri-7B)을 직접 띄워 해결했습니다. 그리고 n8n과 연결해 회의록 입력부터 정리·전달까지 자동화했습니다.

정리하면:

  • Tri-7B는 표준 llama 아키텍처라 transformers·Unsloth·vLLM이 별도 설정 없이 동작
  • vLLM으로 OpenAI 호환 엔드포인트를 열면 n8n·Open WebUI 등과 그대로 연동
  • 베이스 모델만으로도 한국어 요약 품질이 충분하며, 회사 포맷 고정이 필요하면 같은 파이프라인으로 파인튜닝까지 확장 가능

외부 API 비용 없이, 사내 데이터를 안전하게 다루면서 업무를 자동화하고 싶다면 gcube에서 직접 GPU를 띄워 따라 해보세요.