6.9. AI RAG 응답 생성

54분 33초부터 1시간 20분 50초까지 보시면 됩니다.

6.9.1. 미리보기

전형적인 RAG 에서 다뤘던 RAG 로직을 장고 프로젝트에 적용합니다.

  1. 먼저 커스텀 장고 관리명령 make_vector_store 를 구현하여, load, split, embed, store 단계를 한 번에 수행하여 VectorStore를 생성합니다.

  2. 생성된 VectoreStore를 장고 서버에서 로딩하여, 빽다방 메뉴 정보를 RAG로서 활용하여 답변을 생성합니다.

08-rag.png

RAG 응답 수신 화면

6.9.2. VectorStore 생성명령 make_vector_store 구현

6.9.2.1. 지식파일 준비

먼저 빽다방.txt 지식 파일 원본을 chat/assets/빽다방.txt 경로에 복사합니다. make_vector_store 명령을 실행할 때 원본 지식 파일의 경로를 지정합니다.

6.9.2.2. 의존 라이브러리

requirements.txt 파일에 없다면 추가하고 라이브러리 설치
openai
langchain
langchain-core
scikit-learn
numpy
uv pip install -r requirements.txt

6.9.2.3. 커스텀 settings 설정 추가

mysite/settings.py 파일에 추가
# 암시적으로 OPENAI_API_KEY 환경변수를 사용하지 않고, 명시적으로 OPENAI_API_KEY 설정을 참조하겠습니다.
# 설정을 추가하여 LLM마다 다른 API 키를 명시적으로 지정할 수도 있습니다.
OPENAI_API_KEY = env.str("OPENAI_API_KEY", default=None)

# RAG 임베딩 모델도 명시적으로 설정/주입하겠습니다.
# 구동환경에 따라 환경변수를 통해 손쉽게 임베딩 모델을 변경할 수 있습니다.
RAG_EMBEDDING_MODEL = env.str("RAG_EMBEDDING_MODEL", default="text-embedding-3-small")

# 임베딩 파일 경로도 명시적으로 설정/주입하겠습니다.
VECTOR_STORE_PATH = env.str(
    "VECTOR_STORE_PATH",
    default=(BASE_DIR / "vector_store.pickle"),
)

환경변수 값 참조는 settings 에서만 수행하고, 코드에서는 settings 만 참조토록 합니다.

  1. 모든 환경변수 참조를 settings 에 모아두면 설정값 관리가 단순해집니다. 환경변수 이름이나 디폴트 값을 변경해야할 때 여러 파일을 수정할 필요없이 settings 파일만 수정하면 됩니다.

  2. 애플리케이션 코드에서 의미있는 설정 이름을 참조하므로,코드의 의도가 명확해지고 IDE 자동완성 기능을 통해 설정값을 쉽게 참조할 수 있습니다.

  3. 애플리케이션 코드에서 환경변수를 직접 참조하지 않으므로 보안이 강화됩니다.

  4. 환경변수 값에 대한 값 변환을 settings 에서 일원화하여 처리할 수 있습니다.

  5. 테스트 코드에서 settings 값을 쉽게 변경/주입할 수 있어, 환경변수를 직접 조작하는 것보다 테스트 작성과 관리가 용이합니다.

6.9.2.4. 이전에 구현한 RAG 모듈

전형적인 RAG 에서 구현한 RAG 모듈을 재사용합니다. 아래 코드를 복사해주세요.

settings 에서 다음 2가지 설정을 참조합니다.

  • OPENAI_API_KEY

  • RAG_EMBEDDING_MODEL

6.9.2.5. 커스텀 장고 명령 make_vector_store 구현

장고에서는 python manage.py ... 명령을 통해 다양한 장고 명령을 실행할 수 있습니다.

각 장고 명령은 장고앱 폴더 안에 management/commands/ 폴더 안에 구현합니다. 별도로 등록과정은 없으며 해당 장고 앱이 settings.INSTALLED_APPS 에 등록되어 있으면 그 장고앱의 장고 명령을 실행할 수 있습니다.

명령 이름은 파일명입니다. chat/management/commands/make_vector_store.py 경로에 명령을 구현했다면 python manage.py make_vector_store 명령으로 실행할 수 있으며, 명령 옵션도 명령 파일에 정의하여 지원할 수 있습니다.

모든 장고 명령은 BaseCommand 클래스를 상속받아 구현합니다.

  • 원본 지식 파일의 경로를 받도록 옵션을 추가했습니다.

  • 명령이 수행되면 handle 메서드가 호출됩니다. handle 메서드에서 원본 지식 파일을 읽어들여 chat.rag 모듈을 통해 벡터 스토어를 생성하고, settings.VECTOR_STORE_PATH 경로에 저장합니다.

이 과정은 지식 파일이 변경되었을 때마다 수행되어야하며, 생성된 벡터 스토어 파일은 장고 서버에 배포하고 장고 서버를 재시작시켜 다시 로딩해야 합니다.

chat/management/commands/make_vector_store.py 파일
from pathlib import Path
from django.conf import settings
from django.core.management import BaseCommand
from chat import rag

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "txt_file_path",
            type=str,
            help="VectorStore로 저장할 원본 텍스트 파일 경로",
        )

    def handle(self, *args, **options):
        txt_file_path = Path(options["txt_file_path"])

        doc_list = rag.load(txt_file_path)
        print(f"loaded {len(doc_list)} documents")

        doc_list = rag.split(doc_list)
        print(f"split into {len(doc_list)} documents")

        vector_store = rag.VectorStore.make(doc_list)
        vector_store.save(settings.VECTOR_STORE_PATH)

6.9.3. 빽다방 RAG를 통한 답변 생성

make_ai_message 메서드는 삭제하고, 빽다방 AI 클래스를 정의했습니다.

클래스로 호출 가능한 인스턴스를 사용하여 함수처럼 사용할 수 있습니다. 인스턴스 생성 및 벡터 스토어 로딩은 서버 시작 시 한 번만 수행됩니다.

6.9.3.1. PaikdabangAI

import openai
from django.conf import settings
from . import rag


# 명시적으로 OPENAI_API_KEY 설정을 지정합니다.
client = openai.Client(api_key=settings.OPENAI_API_KEY)


# def make_ai_message(system_prompt: str, human_message: str) -> str:
#     completion = client.chat.completions.create(
#         model="gpt-4o-mini",
#         messages=[
#             {"role": "system", "content": system_prompt},
#             {"role": "user", "content": human_message},
#         ],
#     )
#     ai_message = completion.choices[0].message.content

#     return ai_message


class PaikdabangAI:
    # 서버 시작할 때에만 1회 호출되어, 벡터 스토어 파일을 로딩합니다.
    def __init__(self):
        try:
            self.vector_store = rag.VectorStore.load(settings.VECTOR_STORE_PATH)
            print(f"Loaded vector store {len(self.vector_store)} items")
        except FileNotFoundError as e:
            print(f"Failed to load vector store: {e}")
            self.vector_store = rag.VectorStore()

    # 매 AI 답변을 요청받을 때마다 호출됩니다.
    def __call__(self, question: str) -> str:
        # 답변과 유사한 지식을 찾습니다.
        search_doc_list = self.vector_store.search(question)
        # 찾은 지식을 문자열로 변환합니다.
        지식 = "\n\n".join(doc.page_content for doc in search_doc_list)

        res = client.chat.completions.create(
            messages=[
                {
                    "role": "system",
                    # 지식을 포함한 시스템 프롬프트를 생성합니다.
                    "content": f"넌 AI Assistant. 모르는 건 모른다고 대답.\n\n[[빽다방 메뉴 정보]]\n{지식}",
                },
                {
                    "role": "user",
                    # 커밋에서는 질문이 하드코딩되어있습니다.
                    "content": question,
                },
            ],
            model="gpt-4o-mini",
            temperature=0,
        )
        ai_message = res.choices[0].message.content

        return ai_message


# 함수처럼 사용할 수 있는 인스턴스를 생성합니다.
# 인자로 질문 문자열 인자 하나만 받습니다.
ask_paikdabang = PaikdabangAI()

6.9.3.2. ask_paikdabang

make_ai_message 함수가 아닌, ask_paikdabang 함수 호출을 통해 답변을 생성합니다.

chat/views.py
 # from chat.ai import make_ai_message
 from chat.ai import ask_paikdabang

 def reply(request):
     # ...

     # system_prompt = "당신은 친절한 AI 어시스턴트입니다."
     try:
         # ai_message = make_ai_message(system_prompt, human_message)
         ai_message = ask_paikdabang(human_message)
     except Exception as e:
         ai_message = f"죄송합니다. 응답을 생성하는 중 오류가 발생했습니다. ({e})"

     # ...