4. 빽다방 모델 및 최대 토큰수 유효성 검사¶
전형적인 RAG에서 빽다방 메뉴 문서를 파이썬 리스트에 저장했던 임베딩을
pgvector
확장과 장고 모델을 통해 Postgres 데이터베이스에 저장해보겠습니다.
4.1. 커스텀 settings¶
RAG 관련 설정들을 일원화하여 관리하기 위해 다음과 같이 커스텀 settings를 정의하고, 환경변수 값을 파싱하여 각 설정값들을 초기화합니다.
OpenAI API는 서비스 전반적으로 사용되기에 전역 설정값을 정의하고, RAG에서는 다른 API Key 혹은 다른 서비스의 API을 호출할 수 있기에 설정값들은 별도로 정의합니다.
RAG_*
설정값을 사용하는 각종 RAG 모듈에서도 RAG 모듈에 따라 다른 API Key 혹은 다른 서비스를 사용할 수도 있기에
RAG_*
설정값을 디폴트 값으로 사용하겠습니다.
# 디폴트 값으로 사용할 OpenAI API key와 BASE_URL
OPENAI_API_KEY = env.str("OPENAI_API_KEY", default=None)
OPENAI_BASE_URL = env.str("OPENAI_BASE_URL", default=None)
# RAG에서 사용할 OpenAI API key와 BASE_URL
RAG_OPENAI_API_KEY = env.str("RAG_OPENAI_API_KEY", default=OPENAI_API_KEY)
RAG_OPENAI_BASE_URL = env.str("RAG_OPENAI_BASE_URL", default=OPENAI_BASE_URL)
# RAG에서 사용할 임베딩 모델
RAG_EMBEDDING_MODEL = env.str("RAG_EMBEDDING_MODEL", default="text-embedding-3-small")
# RAG에서 사용할 임베딩 모델의 차원수
RAG_EMBEDDING_DIMENSIONS = env.int("RAG_EMBEDDING_DIMENSIONS", default=1536)
4.2. 문서 모델 정의¶
PaikdabangMenuDocument
모델을 새롭게 정의하고, 랭체인의 Document
클래스 구성을 따라
page_content
필드와 metadata
필드를 추가하고,
임베딩 데이터를 저장할 embedding
필드를 VectorField
타입으로 정의합니다.
최대 2000 차원까지 저장할 수 있습니다.
각 모델에서 사용할 api key, base url, 임베딩 모델, 차원수 설정값은 클래스 변수로 두어,
각 설정값을 모델 클래스 내에서 일관되게 참조할 수 있도록 합니다. 설정값 변경 시 클래스 변수 한 곳만 수정하면 됩니다.
아래 모델 구성은 다른 모델에도 동일하게 적용할 수 있구요.
단, 인덱스 name
속성값은 데이터베이스에서 유일한 값으로 지정해주어야 합니다.
1from pgvector.django import VectorField, HnswIndex
2
3class PaikdabangMenuDocument(models.Model):
4 openai_api_key = settings.RAG_OPENAI_API_KEY
5 openai_base_url = settings.RAG_OPENAI_BASE_URL
6 embedding_model = settings.RAG_EMBEDDING_MODEL
7 embedding_dimensions = settings.RAG_EMBEDDING_DIMENSIONS
8
9 page_content = models.TextField()
10 metadata = models.JSONField(default=dict)
11 embedding = VectorField(dimensions=embedding_dimensions, editable=False)
12 created_at = models.DateTimeField(auto_now_add=True)
13 updated_at = models.DateTimeField(auto_now=True)
14
15 class Meta:
16 indexes = [
17 HnswIndex(
18 name="paikdabang_menu_doc_idx", # 데이터베이스 내에서 유일한 이름이어야 합니다.
19 fields=["embedding"],
20 m=16,
21 ef_construction=64,
22 opclasses=["vector_cosine_ops"],
23 ),
24 ]
인덱스 name
속성값이 중복될 경우
인덱스 name
속성값이 중복될 경우, makemigrations
명령을 실행할 때 SystemCheckError
오류가 발생합니다.
반드시 인덱스 이름은 데이터베이스 내에서 유일한 값으로 지정되어야 합니다.
?: (models.E030) index name 'paikdabang_menu_doc_idx' is not unique among models:
chat.PaikdabangMenuDocument, chat.StarbucksMenuDocument
VectorField 타입은 최대 2000 차원까지 지원합니다.
OpenAI의 임베딩 모델은 차원수가 다릅니다. text-embedding-3-small
모델은 1536 차원을,
text-embedding-3-large
모델은 3072 차원을 사용합니다.
VectorField
는 최대 2000 차원까지만 지원하므로, 3072 차원의 text-embedding-3-large
모델을 사용할 때는
HalfVectorField
를 사용해야 합니다. 임베딩 차원수에 따라 적절한 필드 타입을 자동으로 선택하도록 다음과 같이 구현할 수 있습니다:
from pgvector.django import VectorField, HalfVectorField, HnswIndex
class PaikdabangMenuDocument(models.Model):
embedding_model = "text-embedding-3-large"
embedding_dimensions = 1536 * 2
if embedding_dimensions <= 2000:
embedding = VectorField(dimensions=embedding_dimensions, editable=False)
elif embedding_dimensions <= 4000:
embedding = HalfVectorField(dimensions=embedding_dimensions, editable=False)
else:
raise ValueError(f"Unsupported embedding dimensions : {embedding_dimensions}")
embedding_dimensions
설정값을 변경할 경우 데이터베이스 스키마도 함께 변경되어야 하므로,
반드시 마이그레이션 과정을 수행해야만 합니다.
4.3. 최대 토큰 수 유효성 검사¶
OpenAI 임베딩 공식문서에 따르면 OpenAI 임베딩 API는 총 3개의 모델을 지원하며, 각 모델의 최대 토큰 수는 다음과 같습니다.
text-embedding-3-small
:8191
text-embedding-3-large
:8191
text-embedding-ada-002
:8191
최대 토큰 수를 초과한 임베딩 API 요청은 다음과 같은 BadRequestError
예외가 발생합니다.
BadRequestError 예외 메시지
BadRequestError: Error code: 400 - {‘error’: {‘message’: “This model’s maximum context length is 8192 tokens, however you requested 8193 tokens (8193 in your prompt; 0 for the completion). Please reduce your prompt; or completion length.”, ‘type’: ‘invalid_request_error’, ‘param’: None, ‘code’: None}}
page_content
필드에 저장한 문자열을 줄이지 않으면 임베딩을 진행할 수 없게 됩니다.
page_content
필드에 값을 저장하기 전에 최대 토큰 수를 초과하지 않는 지 반드시 검사를 수행해야 할 것입니다.
백엔드 단에서의 유효성 검사는 필수입니다.
유효성 검사를 수행하고 유효성 검사 통과 여부를 판단하는 것은 장고의 기본 기능입니다. 직접 유효성 검사 루틴을 구성할 필요가 전혀 없습니다. 장고에서는 모델 필드나 폼 필드에 유효성 검사 함수만 지정하면, 유효성 검사 수행 시점에 유효성 검사 통과 여부를 판단하고 에러 메세지도 자동으로 생성해주며, 에러 메시지에 대한 HTML 태그도 자동으로 생성해줍니다.
models.CharField
모델 필드는 max_length
인자가 필수이며 이를 통해 최대 글자수를 검증하는 유효성 검사기가 자동으로 추가됩니다.
models.TextField
모델 필드는 기본적으로 문자열 길이를 검증하지 않지만, validators
인자를 통해 원하는 유효성 검사기를 추가할 수 있습니다.
장고 기본에서는 최대 글자수를 검증하는 MaxLengthValidator
를 제공하지만, 토큰 수를 검증하는 유효성 검사기는 없습니다.
그래서 입력 값의 최대 토큰 수를 검증하는 MaxTokenValidator
를 만들겠구요.
인자로 임베딩 모델명을 받아 최대 토큰 수를 인지하도록 합니다.
1from .validators import MaxTokenValidator # 곧 구현할 유효성 검사기
2
3class PaikdabangMenuDocument(LifecycleModelMixin, models.Model):
4 openai_api_key = settings.RAG_OPENAI_API_KEY
5 embedding_model = settings.RAG_EMBEDDING_MODEL
6 embedding_dimensions = settings.RAG_EMBEDDING_DIMENSIONS
7
8 page_content = models.TextField(
9 validators=[MaxTokenValidator(embedding_model)],
10 )
OpenAI Cookbook의 How to count tokens with Tiktoken 문서를
참고하여 MaxTokenValidator
를 아래와 같이 구현했습니다.
임베딩 모델마다 최대 토큰 수가 고정되어있으므로, 인자로 임베딩 모델명만 받으면 내부적으로 최대 토큰 수를 인지할 수 있습니다.
지정 임베딩 모델의 최대 토큰 수 값을 찾지 못해
KeyError
예외가 발생하면, 이를 유효성 검사 에러로 발생시킬려면ValidationError
예외 발생 시점을 Validator 생성자가 아니라 유효성 검사 시점으로 미루어야 합니다.BaseValidator
에서는limit_value
인자로 인자없는 함수를 지원하며, 유효성 검사 시점에 호출하여 반환값을 사용합니다.limit_value
인자로get_limit_value
함수를 전달하고 함수 내부에서KeyError
예외가 발생하면ValidationError
예외를 발생시키도록 했습니다.
1import tiktoken
2from django.core.exceptions import ValidationError
3from django.core.validators import BaseValidator
4from django.utils.deconstruct import deconstructible
5from django.utils.translation import ngettext_lazy
6
7@deconstructible
8class MaxTokenValidator(BaseValidator):
9 message = ngettext_lazy(
10 "토큰 수는 최대 %(limit_value)d개여야 합니다 (현재 %(show_value)d개).",
11 "토큰 수는 최대 %(limit_value)d개여야 합니다 (현재 %(show_value)d개).",
12 "limit_value",
13 )
14 code = "max_tokens"
15
16 # https://platform.openai.com/docs/guides/embeddings
17 max_input_tokens = {
18 "text-embedding-3-small": 8191,
19 "text-embedding-3-large": 8191,
20 "text-embedding-ada-002": 8191,
21 }
22
23 def __init__(
24 self,
25 model_name="text-embedding-3-small",
26 message=None,
27 ):
28 # limit_value 인자로 함수를 전달하면 유효성 검사를 수행하는 시점에 함수가 호출되어
29 # 반환된 값을 limit_value 값으로 사용합니다.
30 def get_limit_value():
31 try:
32 return self.max_input_tokens[model_name]
33 except KeyError:
34 raise ValidationError("Not found max input tokens for '%s'" % model_name)
35
36 self.model_name = model_name
37 super().__init__(limit_value=get_limit_value, message=message)
38
39 def compare(self, a, b) -> bool:
40 return a > b
41
42 def clean(self, x: str) -> int:
43 """주어진 텍스트의 토큰 수를 계산합니다.
44
45 Args:
46 x: 토큰 수를 계산할 텍스트 문자열
47
48 Returns:
49 int: 계산된 토큰 수
50
51 Raises:
52 ValidationError: 유효하지 않은 임베딩 모델명이 지정된 경우
53
54 References:
55 https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken
56 """
57 try:
58 encoding: tiktoken.Encoding = tiktoken.encoding_for_model(self.model_name)
59 except KeyError:
60 raise ValidationError("Not found encoding for '%s'" % self.model_name)
61 num_tokens = len(encoding.encode(x or ""))
62 return num_tokens
MaxTokenValidator
유효성 검사기를 통해 직접 사용해보겠습니다. text-embedding-3-small
모델의 최대 토큰 수는 8191개이므로,
8191개 이하의 토큰 수를 가진 문자열은 유효성 검사를 통과하고, 8192개 이상의 토큰 수를 가진 문자열은 유효성 검사를 실패합니다.
>>> import tiktoken
>>> from chat.validators import MaxTokenValidator
>>> validator = MaxTokenValidator("text-embedding-3-small")
>>> encoding = tiktoken.encoding_for_model("text-embedding-3-small")
>>> x1 = "hello" * 8191
>>> len(encoding.encode(x1))
8191
>>> validator(x1) # 유효성 검사 통과 ✅
>>> x2 = "hello" * 8192
>>> len(encoding.encode(x2))
8192
>>> validator(x2) # 유효성 검사 실패 ❌
ValidationError: ['토큰 수는 최대 8191개여야 합니다 (현재 8192개).']
장고에서 유효성 검사 통과 여부는 ValidationError
예외 발생 여부로만 판단합니다.
장고에서는 유효성 검사 시에 각 유효성 검사 함수를 호출하여 ValidationError
예외 발생 여부로만 유효성 검사 통과 여부를 판단합니다.
함수 반환값도 사용되지 않기에 값을 반환하셔도 전혀 사용되지 않습니다.
import re
def validate_has_korean(value: str) -> None:
if not re.search("[가-힣]", value):
raise ValidationError("한글이 포함되지 않은 문자열은 스팸으로 판단되어 허용되지 않습니다.")
return value # 값을 반환되어도 사용되어지지 않고, 무시됩니다.
반면 장고 폼에서의 clean_필드명
, clean
메서드는 유효성 검사를 비롯하여 값 변환 기능도 제공하기에,
clean
메서드의 반환값은 변환된 값이 됩니다.
4.4. MaxTokenValidator 활용 예¶
MaxTokenValidator
유효성 검사기가 적용된 page_content
필드에 8192개 토큰을 가지는 문자열을 저장하면,
유효성 검사를 통과하지 못하고 ValidationError
예외가 발생합니다.
>>> from chat.models import PaikdabangMenuDocument
>>> doc = PaikdabangMenuDocument(page_content=x2)
>>> doc.full_clean()
ValidationError: {'page_content': ['토큰 수는 최대 8191개여야 합니다 (현재 8192개).']}
PaikdabangMenuDocument
모델 기반으로 ModelForm
을 구성하면,
폼 유효성 검사 시점(.is_valid()
메서드 호출)에 모델의 .full_clean()
메서드를 호출하여 유효성 검사를 수행합니다.
1from django import forms
2from .models import PaikdabangMenuDocument
3
4class PaikdabangMenuDocumentForm(forms.ModelForm):
5 class Meta:
6 model = PaikdabangMenuDocument
7 fields = ["page_content", "metadata"]
8192개 토큰을 가지는 문자열을 지정하고 .is_valid()
메서드를 호출하여 유효성 검사를 수행하면
유효성 검사에 실패했기에 False
을 반환하구요.
.errors
속성은 유효성 검사 실패 시 발생한 에러 메세지를 포함한 딕셔너리를 반환합니다.
>>> from chat.forms import PaikdabangMenuDocumentForm
>>> form = PaikdabangMenuDocumentForm(data={"page_content": x2})
>>> form.is_valid()
False
>>> form.errors
{'page_content': ['토큰 수는 최대 8191개여야 합니다 (현재 8192개).']}
PaikdabangMenuDocument
모델을 장고 관리자에 등록을 하셨다면,
관리자 페이지를 통해 장고 모델폼을 사용하실 수 있고, 유효성 검사 에러도 확인하실 수 있습니다.
1from django.contrib import admin
2from .models import PaikdabangMenuDocument
3
4@admin.register(PaikdabangMenuDocument)
5class PaikdabangMenuDocumentAdmin(admin.ModelAdmin):
6 # form 인자를 지정하지 않으면, 내부에서 모델폼 클래스를 직접 생성하여 사용합니다.
7 # form = PaikdabangMenuDocumentForm
8 pass
http://localhost:8000/admin/ 페이지에 접속하시면 아래와 같이 PaikdabangMenuDocument
레코드 내역을 확인하실 수 있습니다.
슈퍼유저 계정 생성
슈퍼유저 계정이 생각나지 않으시면, uv run python manage.py createsuperuser
명령을 통해
새 슈퍼유저 계정을 생성하실 수 있습니다.
혹은 User
모델을 통해 슈퍼 유저를 조회하고 암호를 직접 변경하실 수도 있습니다.
# uv run python manage.py shell
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model() # 현 프로젝트의 User 모델 클래스 조회
>>> user = User.objects.first() # 첫 번째 유저 조회
>>> user.set_password("원하는 암호") # 지정 암호를 해싱하여 .password 필드에 저장 (아직 데이터베이스 저장 전)
>>> user.save() # 데이터베이스에 User 인스턴스의 모든 모델 필드 저장
모델폼을 통해 입력폼도 자동으로 구성되구요. 새로운 PaikdabangMenuDocument
레코드를 생성할 때
8192개 토큰을 가지는 문자열을 지정하면 자동으로 유효성 검사가 수행되고 유효성 검사에 실패하고 유효성 검사 에러 메세지가 표시 됨을 확인하실 수 있습니다.
참 편리하죠? 😉
명령행에서 파이썬 코드 실행하기
8192개 토큰을 가지는 문자열은 아래 파이썬 코드로 손쉽게 클립보드에 복사해서 사용하실 수 있습니다. 클립보드에 복사하는 명령은 운영체제마다 다릅니다.
uv run python -c "print('hello' * 8192)" | Set-Clipboard
uv run python -c "print('hello' * 8192)" | clip
uv run python -c "print('hello' * 8192)" | pbcopy