본문 바로가기

Software

LLM 애플리케이션을 위한 사내 프롬프트 관리 패키지 개발기

이번 글은 사내의 LLM 프롬프트를 관리하면서 겪은 문제와 해당 문제 개선을 위한 패키지를 개발한 과정에 대해 작성하였습니다.

 

  제품 개발에서 프롬프트 엔지니어링의 어려움

 

1) 타 팀과 협업

프롬프트 엔지니어링에는 언어 모델을 원하는 출력으로 안내하는 고품질 프롬프트를 만드는 작업이 포함됩니다. 그러나 프로덕트를 위한 과정에서 신속한 엔지니어링은 단독으로 수행하는 경우가 드뭅니다. 종종 프롬프트를 형성하고 미세 조정하기 위해 전문 지식을 제공하는 다른 팀 구성원과의 협업이 필요합니다. 효과적인 협업 없이는 다양한 팀원의 집단적 지식과 통찰력을 활용하는 것이 어려워지고 신속한 엔지니어링 프로세스를 방해합니다.

협업 프롬프트 엔지니어링의 주요 과제 중 하나는 다양한 프롬프트 반복에서 일관성을 보장하는 것입니다. 여러 팀에서 실험한 프롬프트를 중앙 저장소에서 관리하도록 하고, 팀이 즉각적인 수정 및 개선을 반복함에 따라 변경 사항을 추적하고 기록 기록을 유지하며 필요한 경우 이전 버전으로 되돌릴 수 있는 기능이 필수적이게 되었습니다.

 

2) 더욱 복잡해진 프롬프트 템플릿 및 입력 변수

그러나 이러한 프롬프트 및 템플릿에 필요한 입력 변수를 관리하는 것은 금방 어려운 작업이 될 수 있습니다. 복잡성과 프롬프트 수가 증가함에 따라 입력 변수를 처리하기 위한 효율적이고 간소화된 접근 방식이 필요해졌습니다.

 

프롬프트 및 템플릿

  1. LLM 프롬프트:

    언어 모델 개발 컨텍스트에서 프롬프트는 모델에 제공된 초기 입력을 의미하며, 이는 컨텍스트를 설정하고 원하는 출력을 생성하기 위한 지침을 제공합니다. 한 문장, 단락 또는 일련의 질문일 수도 있습니다. 프롬프트는 언어 모델에 대한 안내 신호 역할을 하며 이해를 형성하고 생성하는 응답에 영향을 줍니다.
  2. LLM 템플릿:

    LLM 템플릿은 프롬프트 구성을 위한 구조화된 프레임워크를 제공하고 쉽게 사용자 지정할 수 있습니다. 템플릿은 프롬프트 내의 형식 및 변수 부분을 정의합니다. 템플릿을 사용하여 개발자와 콘텐츠 작성자는 특정 정보나 컨텍스트를 동적으로 삽입하면서 유사한 구조로 여러 프롬프트를 생성하는 프로세스를 간소화할 수 있습니다.

예를 들어 텍스트에서 명사를 추출하는 프롬프트의 경우,

프롬프트

엔티티 인식 또는 품사 태깅과 같은 자연어 처리 도구를 사용하여 다음 문서에 나타나는 명사 및 명사구를 '최대한 많이' 추출합니다.
부연 설명없이 키워드만 콤마로 연결하여 나열해주세요.
문서는 아래와 같습니다.

인텐트 마케팅이란 무엇인가?\n 인텐트 마케팅(의도 마케팅)은 소비자가 암시적으로 표현하고 있는 의도를 정확히 이해해서 소비자가 원하는 제품이나 서비스 혹은 관련 정보를 제공하고, 소비자 입장에서 기업이 자신을 잘 이해하고 있다고 느끼게 하는 마케팅이라고 정의할 수 있다. 피터드러커가 마케팅의 목적을 “판매를 불필요하게 하는 것” “소비자들의 충족되지 못한 욕구를 발견하고, 그것을 충족시킬 방법을 마련하는 것” 이라고 했는데, 이런 관점에서 보면 인텐트 마케팅이야말로 마케팅의 목적에 가장 충실한 마케팅이 아닐까? 생각한다. 이러한 인텐트 마케팅의 컨셉이 디지털 마케팅 분야에 한정되는 것은 아니지만 현재는 디지털 마케팅 분야에서 보다 손쉽게 사용될 수 있는 개념이다. 인텐트 마케팅을 수행하기 위해서는 당연히 소비자 인텐트를 파악할 수 있는 인텐트 데이터가 필요하다. 이들 인텐트 데이터가 대부분 디지털 데이터 형태로 존재하기 때문이다.\n\n소비자 의도를 이해해서 원하는 것을 선제적으로 소비자에게 제공하는 마케팅과 애초에 소비자에 대한 이해 없이 만든 제품이나 서비스를 팔기위해 소비자 마음 속에 기업이 목표하는 인식(포지션)을 만드는 마케팅은 서로 다를 수 밖에 없다. 광고 캠페인을 통한 푸시형 마케팅 커뮤니케이션 활동에 비교해서 인텐트 마케팅은 소비자가 애초에 원하던 것을 소비자가 원할 때 제공하기 때문에 대부분의 경우 더 높은 ROI를 기업에게 제공할 수 있다.
...

템플릿

엔티티 인식 또는 품사 태깅과 같은 자연어 처리 도구를 사용하여 다음 문서에 나타나는 명사 및 명사구를 '최대한 많이' 추출합니다.
부연 설명없이 키워드만 콤마로 연결하여 나열해주세요.
문서는 아래와 같습니다.

{text}

템플릿에는 프롬프트에서 동적으로 변경되는 부분에 대해 placeholder로 표시됩니다.

 

입력 변수의 복잡성

 

1. 복잡성 및 규모:

언어 모델이 더욱 발전되고 복잡해짐에 따라 프롬프트 및 템플릿에 대한 입력 변수 관리가 복잡해질 수 있습니다. 서로 다른 프롬프트에는 고유한 데이터 유형과 형식이 있는 다양한 수의 입력 변수가 필요할 수 있습니다. 이러한 변수와 해당 값을 추적하고 다양한 프롬프트에서 일관성을 유지하는 것은 어려운 작업이 될 수 있습니다.

2. 버전 제어 및 재현성:

팀과 협업하거나 반복적인 개선 작업을 할 때 신속한 버전 유지 및 변경 추적이 중요합니다. 버전 제어에 대한 구조화된 접근 방식이 없으면 특정 출력을 재현하거나 신속한 수정의 영향을 분석하기가 어려워집니다.

3. 효율성 및 생산성:

입력 변수 및 프롬프트 변형을 수동으로 관리하는 것은 시간이 많이 걸리고 오류가 발생하기 쉽습니다. 다양한 사용 사례에 대한 프롬프트를 생성, 수정 및 구성하는 프로세스는 생산성을 저해할 수 있습니다. 이러한 작업을 간소화하고 개발자가 관리 오버헤드에 얽매이지 않고 프롬프트의 내용과 컨텍스트에 집중할 수 있도록 하려면 효율적인 솔루션이 필요합니다.

예를 들어, 위의 예시에서는 입력 변수가 단순 text이지만 입력 변수가 아래와 같이 복잡한 프로세스를 거쳐 생성되어야 하는 경우일 수 있습니다.

 

 

  프롬프트 엔지니어링 워크플로우 개선하기

따라서 사내 LLM 프롬프트 및 템플릿에 대한 입력 변수를 관리하고 구성하는 프로세스를 단순화하기 위하여 프롬프트 관리를 위한 유틸리티를 개발하게 되었습니다. 해당 패키지는 프롬프트를 파일 또는 중앙 저장소에 저장하고, langchain 템플릿으로 로드하고, 템플릿에 필요한 입력 변수를 동적으로 생성하는 기능을 제공합니다.

 

Prompt Manager

이 유틸리티를 활용하여 프롬프트 엔지니어링 워크플로우를 향상시키고자 하였습니다. 입력 변수를 수동으로 관리하고 업데이트하는 데 시간을 소비하는 대신 의미 있는 프롬프트와 템플릿을 만드는 데 더 집중할 수 있는 것을 목표로 합니다.

 

즉, 주요 목적은 아래와 같습니다.

  • 프롬프트 엔지니어링 워크플로우 향상
  • 프롬프트 버전 관리
  • 공통 저장소에 프롬프트 저장 및 로드
  • 프롬프트 템플릿에 필요한 입력 변수 동적 생성
  • 사내 다른 부서간의 협업 향상

 

주요 기능

프롬프트 매니저는 중앙 저장소를 제공하여 프롬프트 관리를 간소화하고, 입력 변수 사용에 대한 공통된 규칙을 제시하여 프롬프트 및 템플릿 관리를 표준화합니다. 이때 사용한 중앙 저장소는 promptlayer이며, 이전 글에서 설명을 기술해두었습니다.
프롬프트 매니저는 중앙 저장소를 통해 다음과 같은 기능을 제공합니다.

  1. 프롬프트 및 템플릿의 중앙 저장소(promptlayer)로의 업로드
  2. 저장소에서의 프롬프트 및 템플릿 다운로드
  3. 동적 입력 변수 생성

이를 통해 팀원 간에 프롬프트를 쉽게 공유하고, 입력 변수 사용에 대한 표준화된 접근 방식을 적용할 수 있습니다.

 

사용 예시

해당 패키지의 사용예시는 아래와 같습니다.

1. 설정 기반 채팅 모델 생성

from langchainimport LLMChain
from langchain.chat_modelsimport AzureChatOpenAI
from pmang.utils.settingsimport azure_openai_settings_from_dot_env

deployment, api_key, api_base, api_version = azure_openai_settings_from_dot_env()
chat = AzureChatOpenAI(
    deployment_name=deployment,
    openai_api_key=api_key,
    openai_api_base=api_base,
    openai_api_version=api_version,
)

2. 프롬프트 로더 생성

from pmang.utils.template_loader import TemplateFileLoader, TemplatePromptLayerLoader

file_loader = TemplateFileLoader()
ptlayer_loader = TemplatePromptLayerLoader()

3. file 혹은 promptlayer로부터 로드

# file로부터 로드하는 경우
prompt = file_loader.load(parent_directory='YOUR_LOCAL_DIRECTORY')

# promptlayer로부터 로드하는 경우
prompt = ptlayer_loader.load(prompt_name='YOUR_PROMPT_NAME')

4. 입력 변수 생성

from pmang.variables import SearchResult

search_result = SearchResult(lang='ko').render(
    feature_num=10, 
    serp=serp_sample
)

5. chain 생성 및 LLM 요청

llm_chain = LLMChain(
    prompt=prompt,
    llm=chat
)
response = llm_chain.run(
    query='교보문고', 
    intent='이동형',
    search_result=search_result
)

 

프로젝트 설계

프로젝트는 크게 아래와 같은 구조로 되어있습니다.

$ ds-prompt-manager/src/pmang $ tree -L 1  # depth1
.
├── config.py
├── dataset
├── db
├── __init__.py
├── lang
├── log
├── preprocess
├── schemas.py
├── test
├── utils
└── variables
  • dataset에는 입력 데이터 샘플 및 변수 생성 시 필요한 데이터가 포함되어있습니다.
├── dataset
│   ├── ja_serp.json
│   ├── ko_cluster.json
│   ├── ko_graph.json
│   ├── ko_html.txt
│   ├── ko_pathfinder.json
│   ├── ko_serp.json
│   └── serp_features_distribution.csv
  • dblang 에는 사내의 국가별 데이터베이스 접근 유틸이 포함되어있습니다.
├── db
│   ├── config.py
│   ├── database.py
│   └── __init__.py
├── __init__.py
├── lang
│   ├── __init__.py
│   └── lang_config.py
  • preprocess에는 입력 변수를 생성하는 데 필요한 전처리 유틸이 포함되어있습니다.
├── preprocess
│   ├── features.py
│   ├── __init__.py
│   ├── path_filter.py
│   ├── serp_info_generator.py
│   └── text_summarizer.py
  • schemas.py 에는 입력 데이터를 정의한 pydantic 모델이 포함되어있으며, 앱 내에서 작업하는 동안 데이터가 정의된 스키마를 준수하도록 합니다.
  • utils 에는 각종 로더 및 유틸리티가 포함되어있습니다.
├── utils
│   ├── feature_utils.py
│   ├── __init__.py
│   ├── serp_utils.py
│   ├── settings.py
│   ├── template_loader.py
│   ├── text_utils.py
│   └── tokenizer_utils.py
  • variables 에는 동적으로 생성되는 변수들에 대한 생성기를 포함하였습니다.
└── variables
    ├── base.py
    ├── dominant_features.py
    ├── __init__.py
    ├── intent_nodes.py
    ├── intent_nodes_with_ratio.py
    ├── intent_ratio.py
    ├── interest_ratio.py
    ├── node_ratio.py
    ├── path_nodes.py
    ├── primary_nodes.py
    ├── primary_serps.py
    └── search_result.py

base.py 를 아래와 같이 정의한 후 해당 추상 클래스를 변수 생성기들이 상속받아 render 를 구현하도록 하였습니다. 각 변수 생성기들은 변수를 생성할 때 preprocess 의 유틸리티를 사용합니다.

from abc import ABC, abstractmethod
import typing as t

class VariableBase(ABC):
    VARIABLE_RENDERER = {
        'ko': '_render_ko',
        'ja': '_render_ja'
    }
    _instance = None

    def __init__(self, lang: str):
        self._lang = lang
        if self._lang not in self.VARIABLE_RENDERER:
            raise ValueError(f'Unknown language: {self._lang}')


    @classmethod
    def __call__(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @abstractmethod
    def render(self, **kwargs) -> str:
        """변수 생성 함수"""
        pass

 

  개발 과정에서 느낀 점..

  • 아키텍처 설계의 어려움
    • Jupyter Notebook과의 통합, OpenAI API와의 원활한 상호 작용, 사용자 친화적인 인터페이스 제공 등 다양한 요소를 신중하게 고려해야 했습니다.
  • 명확하게 정의된 요구사항의 중요성
    • 요구사항에 대해 명확히 정의되지 않은 모호한 시작으로 인해 추가 반복, 재작업 및 지연이 발생했습니다.

 

  Reference