삽질

Discord 챗봇 "하루" 만들기 — Ollama + NovelAI 연동기

베타버전 선생님 2026. 6. 4. 23:25

개요

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

로컬에서 돌리기 위해 선택 기준은 두 가지였다.

  1. 한국어 품질
  2. 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_readyUnicodeEncodeError 발생. sys.stdout.reconfigure(encoding="utf-8") 로 해결.

GitHub push protection: CLAUDE.md에 토큰을 평문으로 적어뒀다가 push가 차단됐다. 민감 정보는 .env에만 넣고 .gitignore에 반드시 포함해야 한다.


마치며

전체 코드는 GitHub에 있다 (private). 로컬 LLM과 NovelAI를 조합하면 API 비용 없이 꽤 쓸만한 Discord 봇을 만들 수 있다. qwen3:8b의 한국어 품질이 예상보다 좋아서 캐릭터 말투도 잘 유지된다.