AI Sparkup

최신 AI 쉽게 깊게 따라잡기⚡

작은 LLM을 똑똑하게 만드는 법, Docker의 RAG 실전 가이드

Claude에게 물어봤더니 모른다고 하더군요. Gemini도 마찬가지였습니다. 제가 개발 중인 Golang 라이브러리 Nova에 대한 질문이었는데, 두 AI 모두 그럴듯한 답변을 내놓긴 했지만 완전히 틀린 코드였죠. 그런데 30억 개 파라미터짜리 작은 모델이 이 문제를 해결했습니다.

사진 출처: Docker 공식 블로그

Docker의 Principal Solutions Architect인 Philippe이 작은 LLM(Large Language Model)을 실용적으로 활용하는 방법을 공유했습니다. RAG(Retrieval Augmented Generation) 기법을 활용하면 0.5~7B 파라미터 규모의 작은 모델도 특정 프로젝트에서 Claude나 Gemini를 뛰어넘는 성능을 낼 수 있다는 내용입니다. Docker Model Runner와 Agentic Compose를 활용한 구체적인 구현 방법과 코드가 포함되어 있습니다.

출처: Making (Very) Small LLMs Smarter – Docker 공식 블로그

Claude도 Gemini도 모르는 문제

Philippe은 자신이 개발 중인 Golang 라이브러리 Nova에 대해 도움이 필요했습니다. “Nova Chat 에이전트에서 스트림 컴플리션을 사용하는 코드를 보여줘”라고 물었죠. Claude와 Gemini 모두 Nova를 몰랐고, 실망시키고 싶지 않았는지 전혀 관계없는 코드를 제안했습니다.

“소스 코드를 전부 넣어주면 되지 않나?”라고 생각할 수 있습니다. 하지만 Philippe의 상황은 달랐어요. 인터넷 접속이 제한된 환경이거나 기밀 프로젝트라면 Claude나 Gemini를 쓸 수 없습니다. 로컬에서 돌아가는 작은 LLM이 필요했죠.

작은 모델의 두 가지 필수 룰

Philippe은 Qwen2.5-Coder-3B를 선택했습니다. 30억 개 파라미터로 코드 생성에 최적화된 모델이에요. Docker Model Runner로 간단히 설치할 수 있습니다:

docker model pull hf.co/Qwen/Qwen2.5-Coder-3B-Instruct-GGUF:Q4_K_M

하지만 이 작은 모델에 Nova 라이브러리 문서를 전부 넣는 건 역효과를 냅니다. Philippe은 수많은 실험 끝에 두 가지 핵심 원칙을 발견했습니다:

1번 룰: 작은 모델일수록 컨텍스트는 적게
모델에 제공하는 정보가 많을수록 집중력이 떨어집니다. 마치 시험 볼 때 교과서 전체를 펼쳐놓는 것보다 요약 노트 한 장이 더 효과적인 것과 비슷해요.

2번 룰: 대화 히스토리도 최소화
대화가 길어질수록 컨텍스트가 계속 쌓이고, 모델 성능이 저하됩니다. Philippe은 대화 히스토리를 최근 2개 메시지로 제한했습니다.

RAG로 필요한 정보만 골라내기

그렇다면 어떻게 “필요한 정보만” 제공할까요? 여기서 RAG가 등장합니다.

Philippe의 접근법은 이렇습니다:

  1. Nova 코드 스니펫 파일을 섹션별로 나눔 (Chunk)
  2. 각 섹션을 Embedding 모델(ai/embeddinggemma)로 벡터로 변환
  3. 이 벡터들을 메모리 기반 벡터 데이터베이스에 저장
  4. 사용자가 질문하면 질문도 벡터로 변환
  5. 코사인 유사도로 가장 관련 있는 2-3개 스니펫만 찾아냄
  6. 찾아낸 스니펫 + 시스템 지시사항 + 질문을 조합해서 LLM에 전달
사진 출처: Docker 공식 블로그

핵심은 전체 문서가 아니라 정확히 필요한 2-3개 섹션만 모델에게 주는 겁니다. Philippe은 유사도 임계값을 0.45로, 최대 유사 결과를 3개로 설정했어요. 이 값들은 모델 성능에 따라 조정할 수 있습니다.

Docker Agentic Compose로 구현하기

실제 구현은 Docker Agentic Compose와 LangchainJS로 이뤄졌습니다. 설정 파일에서 주목할 부분:

environment:
  HISTORY_MESSAGES: 2
  MAX_SIMILARITIES: 3
  COSINE_LIMIT: 0.45

models:
  chat-model:
    model: hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m

  embedding-model:
    model: ai/embeddinggemma:latest

대화 모델과 임베딩 모델을 분리해서 정의하고, Docker Compose가 없는 모델을 자동으로 다운로드합니다. 시스템 지시사항도 설정 파일에서 관리할 수 있어요.

JavaScript 코드는 시작 시 벡터 데이터베이스를 생성하고, 대화 루프에서 유사도 검색 → 프롬프트 구성 → 스트리밍 응답 → 히스토리 업데이트 순서로 진행됩니다.

결과: Claude가 못한 걸 30억 모델이 해냈다

“Nova Chat 에이전트에서 스트림 컴플리션을 사용하는 코드 스니펫이 필요해”라고 물었더니, 작은 모델이 완벽한 코드를 생성했습니다:

// Nova Chat 에이전트 설정
agent, err := chat.NewAgent(
    ctx,
    agents.Config{
        EngineURL: "http://localhost:12434/engines/llama.cpp/v1",
        SystemInstructions: "You are Bob, a helpful AI assistant.",
        KeepConversationHistory: true,
    },
    models.Config{
        Name: "ai/qwen2.5:1.5B-F16",
        Temperature: models.Float64(0.8),
    },
)

// 스트림 컴플리션 생성
result, err := agent.GenerateStreamCompletion(
    []messages.Message{
        {Role: roles.User, Content: "Who is James T Kirk?"},
    },
    func(chunk string, finishReason string) error {
        if chunk != "" {
            fmt.Print(chunk)
        }
        return nil
    },
)

에러 핸들링, 주석, Nova 라이브러리의 정확한 API 사용법까지 모두 포함된 코드였습니다. Claude가 할 수 없었던 걸 해낸 거죠.

유사도 설정의 중요성

흥미로운 발견이 하나 더 있었습니다. “Nova RAG 에이전트 스니펫이 필요해”라고 물었을 때는 관련 결과를 찾지 못했어요. 질문에 “vector store”라는 키워드가 없었기 때문입니다.

해결책은 두 가지였습니다:

  • 유사도 임계값을 낮추거나 최대 결과 개수를 늘림
  • 코드 스니펫에 KEYWORDS 메타데이터 추가

Philippe은 각 스니펫 제목 아래 KEYWORDS: ... 줄을 추가했고, 검색 정확도가 크게 개선됐습니다.

작은 모델, 큰 가능성

30억 파라미터 모델로 Claude를 뛰어넘는다는 건 과장처럼 들릴 수 있습니다. 하지만 특정 도메인에서는 충분히 가능합니다. 작은 모델 + RAG 조합의 장점은 명확해요:

  • 비용: API 호출 비용 제로
  • 속도: 로컬에서 즉시 실행
  • 프라이버시: 민감한 코드가 외부로 나가지 않음
  • 오프라인: 인터넷 없이도 작동

물론 한계도 있습니다. 임베딩 모델의 정확도, 청크 분할 전략, 컨텍스트 크기 제약 등을 신중히 고려해야 해요. 하지만 Philippe의 실험이 보여주듯, 이런 제약을 이해하고 창의적으로 접근하면 작은 모델로도 실용적인 솔루션을 만들 수 있습니다.

특히 여러 개의 특화된 작은 에이전트를 조합하면 더 흥미로운 결과를 만들어낼 수 있다고 Philippe은 말합니다. 이건 다음 글의 주제가 될 예정이라고 하네요.

참고자료:


AI Sparkup 구독하기

최신 게시물 요약과 더 심층적인 정보를 이메일로 받아 보세요! (무료)

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다