개요
Discord에서 동작하는 챗봇 하루를 만들었다. 핵심 스택은 두 가지다.
- 대화: 로컬 LLM (Ollama + qwen3:8b) — Jetson Nano에서 추론
- 이미지 생성: NovelAI API (nai-diffusion-4-5-full)
클라우드 LLM API 비용 없이 운영할 수 있고, 이미지는 NovelAI Opus 플랜으로 무제한 생성된다.
전체 아키텍처
Discord 유저
│ !chat / !image
▼
Discord Bot (Python / discord.py)
├── !chat ──────► Jetson Nano (Ollama, qwen3:8b)
└── !image ──────► NovelAI Cloud API
봇 프로세스 자체는 Jetson Nano에서 systemd 서비스로 실행된다. 재부팅 시 자동 시작된다.
1. 대화 기능
LLM 선택 — qwen3:8b
로컬에서 돌리기 위해 선택 기준은 두 가지였다.
- 한국어 품질
- VRAM/RAM 제약 (Jetson Nano 7.4GB)
qwen3:8b (Q4_K_M, 5.2GB)가 이 조건을 모두 만족한다.
Thinking 비활성화
qwen3는 기본적으로 <think>...</think> 블록을 출력한다. 챗봇 응답에 필요 없으므로 비활성화해야 하는데, options 안에 넣으면 무시된다. 반드시 페이로드 최상위에 넣어야 한다.
payload = {
"model": "qwen3:8b",
"messages": messages,
"stream": False,
"think": False, # ← 여기. options 안에 넣으면 안 됨
"options": {
"num_predict": 400,
"temperature": 0.7,
},
}
유저별 대화 메모리
memory.json에 유저 ID별로 최근 20턴을 저장한다. 봇을 재시작해도 대화가 이어진다.
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(memory.get_history(user_id)) # 최근 20턴 주입
messages.append({"role": "user", "content": message})
캐릭터 프롬프트
character.py에 시스템 프롬프트를 분리했다. 말투 규칙, 성격, 예시 응답까지 명시적으로 지정해야 일관성 있는 캐릭터가 나온다.
2. 이미지 생성 기능
NovelAI의 /ai/generate-image 엔드포인트를 직접 호출한다. 공식 라이브러리 없이 aiohttp로 구현했다.
V4.5 전용 페이로드 구조
V4.5는 v4_prompt / v4_negative_prompt 구조체가 필수다. 없으면 HTTP 500이 반환된다.
"v4_prompt": {
"caption": {
"base_caption": prompt,
"char_captions": [
{"char_caption": "캐릭터 프롬프트", "centers": [{"x": 0.5, "y": 0.5}]}
]
},
"use_coords": False,
"use_order": True,
},
"v4_negative_prompt": {
"caption": {"base_caption": NEGATIVE_PROMPT, "char_captions": []},
"legacy_uc": False,
},
NAIA2.0에서 참고한 핵심 파라미터
오픈소스 자동화 툴 NAIA2.0의 소스를 분석해 누락된 파라미터들을 보완했다.
| 파라미터 | 값 | 설명 |
|---|---|---|
prefer_brownian |
True |
V4.5 전용 샘플링 품질 향상 |
skip_cfg_above_sigma |
58 |
VAR+ 기능 (고주파 디테일 개선) |
autoSmea |
True |
SMEA 자동 설정 |
cfg_rescale |
0.28 |
CFG 강도 보정 |
noise_schedule |
karras |
샘플링 스케줄러 |
legacy / legacy_uc |
False |
레거시 모드 명시 비활성화 |
인라인 파라미터 오버라이드
NAIA2.0에서 영감을 받아, 프롬프트 안에 직접 파라미터를 쓸 수 있도록 파싱 로직을 구현했다.
!image 1girl, smile, seed:12345, steps:20, resolution:832x1216
seed:, steps:, resolution:, cfg_scale:, cfg_rescale:, sampler:, scheduler: 키워드를 인식해서 해당 파라미터를 오버라이드한다. 나머지 태그만 실제 프롬프트로 전달된다.
동시 생성 방지
asyncio.Lock으로 구현했다. 생성 중에 새 요청이 오면 API를 호출하지 않고 즉시 안내 메시지를 반환한다. 이게 없으면 NovelAI에서 HTTP 429 (Concurrent generation is locked)가 발생한다.
if image_lock.locked():
await ctx.reply("지금 그림 그리는 중이야! 잠깐만 기다려줘 🎨")
return
async with image_lock:
image_bytes = await generate_image(...)
재시도 로직
502/503/504/520 서버 오류 시 최대 3회 자동 재시도한다. 재시도 간격은 2 * attempt초로 증가한다.
3. 지원 명령어
| 명령어 | 설명 |
|---|---|
!chat [내용] |
하루와 대화 (최근 20턴 컨텍스트 유지) |
!image [프롬프트] |
NAI V4.5 이미지 생성. 이미지 첨부 시 Vibe Transfer |
!image 배경|캐릭터1|캐릭터2 |
| 구분자로 캐릭터 프롬프트 분리 |
!reimagine [프롬프트] |
마지막 이미지 기반 img2img |
!upscale |
마지막 이미지 NAI 2x 업스케일 |
!reset |
대화 기록 초기화 |
4. 배포 — Jetson Nano
봇 프로세스를 systemd 서비스로 등록해 재부팅 시 자동 시작되도록 했다.
[Unit]
Description=Haru Discord Bot
After=network.target
[Service]
Type=simple
User=comedu
WorkingDirectory=/home/comedu/moe-bot
ExecStart=/usr/bin/python3 -u bot.py
Restart=always
RestartSec=10
Jetson Nano 스펙:
- RAM 7.4GB (qwen3:8b 5.2GB + 봇 ~35MB 동시 운영 여유 있음)
- Swap 11GB
- Ollama가 이미 상주해 있어 추가 설치 없음
5. 삽질 포인트 요약
qwen3 thinking 비활성화: options 안에 think: false를 넣으면 무시된다. 최상위 레벨에 넣어야 한다.
NAI V4.5 v4_prompt 구조체 필수: 이 구조체 없이 요청하면 HTTP 500. V3 방식과 다르게 V4.5는 반드시 포함해야 한다.
Windows cp949 인코딩: 봇 이름에 이모지가 포함되면 on_ready 시 UnicodeEncodeError 발생. sys.stdout.reconfigure(encoding="utf-8") 로 해결.
GitHub push protection: CLAUDE.md에 토큰을 평문으로 적어뒀다가 push가 차단됐다. 민감 정보는 .env에만 넣고 .gitignore에 반드시 포함해야 한다.
마치며
전체 코드는 GitHub에 있다 (private). 로컬 LLM과 NovelAI를 조합하면 API 비용 없이 꽤 쓸만한 Discord 봇을 만들 수 있다. qwen3:8b의 한국어 품질이 예상보다 좋아서 캐릭터 말투도 잘 유지된다.
'삽질' 카테고리의 다른 글
| NVMe 마이그레이션부터 로컬 AI 채팅까지 — 삽질 2일차 (0) | 2026.05.19 |
|---|---|
| Jetson Orin Nano 개봉기 — AI 서버 만들기 첫 걸음 (0) | 2026.05.18 |