Sign In

01. 프롬프트

T
Two_Jay
CPE Elements 101의 첫글은 '프롬프트'입니다.
프롬프트는 텍스트로 주어진 질문, 명령 또는 지시로, 우리가 의도하는 특정한 결과를 이끌어내기 위해 설계된 텍스트입니다. 이를 통해 LLM을 기반으로 명시적이거나 암시적인 의도에 맞추어 작업을 수행합니다. 프롬프트는 간단한 질문 형태일 수도 있고, 복잡한 시뮬레이션이나 무언가를 요구하는 상세한 지침일 수도 있습니다.
사실 여기서 프롬프트가 무엇이고 간단한 유래를 다루거나 할 수 있지만, 그에 대해서는 대강 건너뛰고 가고 싶네요. 그렇다고 아무것도 이야기하진 않을 수 없으니 간단하게 우리가 다룰 '프롬프트'라는 것의 특징을 알고 건너가는 게 좋을 것 같습니다. 여기서 다룰 특성들은 이후에 다루게 될 포인트들을 반영하기 때문입니다.
프롬프트는 본질적으로 텍스트이다.
프롬프트는 텍스트상에서 나타나는 표현과 형식이 있고, 이를 함의하는 의미와 방향성을 가지고 있다.
프롬프트는 개별 기능을 대표한다.
프롬프트가 가진 텍스트 크기가 클 수록, 프롬프트의 복잡성 또한 올라간다.
오늘은 이 중에서 프롬프트의 의도와 개선에 대해서 이야기해보고자 합니다.

숨어있는 의도 드러내기

프롬프트는 본질적으로 텍스트입니다. 프롬프트의 작동 원리와 결과는 종종 코드와 비슷한 구조를 가질 수 있습니다만, 어찌되었든 프롬프트는 줄글로 된 텍스트이고, 주된 기능을 구현할 때에도 이에 대한 여러 줄의 표현이나 설명, 지시와 형식이 복잡스레 들어갑니다. 이러한 표현들은 특정한 기능을 더 잘 동작시키기 위한 목표를 가지고 심혈을 기울여 배치됩니다. 그렇다면 이를 어떻게 하면 바로 시인성있게 파악할 수 있을까요? 시인성있게 파악할 수 있어야 무얼 하는 프롬프트인지 명확하게 파악할 수 있고, 전체 프로젝트에서 프롬프트 사이사이에 놓치는 부분이 없이 엔지니어링이 가능합니다.
그런 면에서 코드와 프롬프트는 얼핏 비슷해보이지만, 가장 두드러지는 차이는 바로 "의도의 명확성"을 어떤 수단으로 드러내는가에서 두드러집니다.
코드에는 주석이라는 개념이 있습니다. 프로그래밍에서는 주석을 남김으로, 코드와는 명확히 다른 방식의 표현으로 자연어로 된 화자의 의도를 직접적으로 남길 수 있습니다. 아래와 같은 예제로, 코드는 주석을 새김으로 동작에 관여하지 않지만, 명백한 의도를 설명할 수 있는 텍스트를 남길 수 있습니다.
from datetime import datetime
from pydantic import BaseModel, PositiveInt

# User 클래스는 사용자 데이터를 검증하고 변환하기 위한 모델입니다.
class User(BaseModel):
    id: int  # 사용자 ID (필수 정수형 값)
    name: str = 'John Doe'  # 기본값이 제공되는 사용자 이름
    signup_ts: datetime | None  # 가입 시간 (옵션 값)
    tastes: dict[str, PositiveInt]  # 취향 정보를 담은 딕셔너리, 값은 양의 정수여야 함

# 외부에서 입력된 데이터, 다양한 타입과 형식을 가짐
external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',  # 문자열로 표현된 날짜
    'tastes': {
        'wine': 9,  # 유효한 정수 값
        b'cheese': 7,  # 바이트 문자열 키 (자동 변환됨)
        'cabbage': '1',  # 문자열 숫자 (양의 정수로 변환됨)
    },
}

# User 모델을 사용하여 데이터를 검증하고 변환
user = User(**external_data)  

print(user.id)  
#> 123
print(user.model_dump())  
# {
#     'id': 123,
#     'name': 'John Doe',
#     'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
#     'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
# }
하지만 프롬프트는 다릅니다. 아래의 프롬프트를 한 번 볼까요?
You are a romance web novel writer for adult women. {user_name} is the female lead. Below is the information of the {character_name}, the male lead.

Generate the next episode in an ongoing story based on a conversation history and the current episode. Your goal is to create a coherent and engaging continuation of the story that aligns with the established narrative and character traits.

## Input Context

First, review the conversation history:
[conversation_start]
{conversation}
[conversation_end]

Now, consider the current episode:
[current_episode_start]
{current_episode}
[current_episode_end]

(이하 생략)
어떤가요? 이를 읽어보았을 때에 그 의도가 바로 캐치가 되시는지요? 누군가는 아주 빠른 속도로 텍스트를 이해하고 읽어나갈 수 있기에, 읽어보고 그 의도를 명백히 파악하는 데 수 초 밖에 안 걸릴 수 있지만, 어찌되었든 비용이고, 모두에게 공평하게 그 비용이 부과되지는 않을 겁니다. 이런 텍스트가 10,000자 ~ 20,000자가 있다면, 이제 그 때부터 이해를 '포기하는' 사람들이 나오기 시작하지요. 텍스트의 표현이 더 모호하고, 그 기능이 더 파악하기 어려운 기능일 수록 더 비용이 커질 것 입니다.
프롬프트에 주석을 추가할 수는 없을까요? 기본적으로는 프롬프트는 텍스트이기에, 별도의 관리없이 주석 그 자체를 프롬프트에 새길 수는 없습니다. 왜냐하면 그 주석조차도 프롬프트의 동작에 관여를 하게 되니깐요. 그러나 약간의 트릭을 쓰면, 그럴 수 있긴 합니다. 예를 들어 개발 레벨에서 주석을 합의된 정책하에 기록하고, 배포 시에 middleware 처럼 스크립트로 그 주석을 삭제하도록 한다면 아래와 같은 프로세스가 가능하겠죠.
너무 원시적인가요? 하지만 이런 방법은 의외로 효과적이기도 합니다. 하지만 이를 위해서는 배포과정에서 추가적으로 품을 들여야합니다. 프롬프트 엔지니어가 비개발자도 할 수 있는 직군임을 생각해보면 이런 과정을 스스로 구현하는 것은 도전적이긴 하지만 마뜩찮은 일이기도 합니다.
그렇다면 조금 더 좋게는 어떻게 해볼 수 있을까요? 중요한 것은 프롬프트의 '의도'는 텍스트와 결합되기도, 분리되기도 할 수 있다는 것입니다. 여기서부터는 크게 두 가지 큰 맥락이 존재합니다.
프롬프트의 적힌 서술과 텍스트에 연관해서 보면 어떨까요? 이 맥락에서는, 프롬프트 엔지니어들이 대대로 시금석으로 삼고있는 지침들이 큰 역할을 합니다. '모호하고 알 수 없는 표현보다는 간결하고 명확한 표현을 쓸 것', '명확하게 의도를 캐치할 수 있는 프롬프트를 작성할 것' 등등 말이지요. 이는 프로그래밍에서도 동일하게 황금률로 가지고 있는 요소이기도 합니다. '클린코드'라는 책과 그 단어가 가지는 기준은 서로 다를 수 있어도, 적어도 '타인이 읽기 좋은 코드가 유지보수에도, 개발에도 좋다'라는 기준에는 많은 사람들이 동의할 것입니다. 프롬프트도 그러합니다. 프롬프트도 타인이 읽기 좋은 표현과 단어를 사용할 수록 프롬프트가 담은 의도를 빨리 잡아낼 수 있으니깐요.
그렇다면 그 의미를 텍스트 상에서 해석하거나 바로 캐치할 수 없는 프롬프트라면 어떨까요? 프롬프트라는 입력값에서 불특정한 텍스트를 넣어서 출력에 대해 뒤틀리어 접근하는 것은 LLM에서 대표적인 cheating 기법 중 하나입니다. prompt injection/leaking이나, 재미있는 프롬프트나, 기타 등등에서 많이 쓰이는 프롬프트이지요. 하지만 우리는 여기서는 기업에서 어플리케이션을 만드는 프롬프트를 이야기하고 있습니다. 이런 과정에서 불필요하게 의도파악이 어려운 프롬프트를 '일단 넣으니깐 되어요'라는 식으로 사용하는 것은 이후에 프롬프트 퀄리티와, 협업 가능성에 지장을 줄 수 있다고 봅니다.
다음 맥락은, 프롬프트의 의도를 프롬프트의 텍스트 외적으로 직접적으로 드러나는 것이 아니라, 이를 추상화시키거나 분리된 컨텍스트에서 공유하고 활용하는 것입니다.
최근에는 프롬프트 파이프라인을 node-based로 관리하는 툴들이 등장하기 시작했습니다. Dify와 같은 툴이 그러하죠. 이런 툴을 활용할 때에는 node 단위로 정보를 추상화하여 관리할 수 있습니다. 그래서 프롬프트를 어떤 목적으로 활용하는 지를 적절히 추상화하여, 하나의 키워드로 저장할 수 있지요.
아니면 이를 버전관리와 함께 의도를 명확하게 할 수도 있습니다. 프롬프트 버전관리시 프롬프트 텍스트만 관리하는 것이 아니라, 프롬프트의 기능적 의도, 사용한 전략, 기대하는 입력과 출력 예제, 버전 업데이트시에 추가된 라인과 의도한 개선방향을 버전마다 명시하는 것도 좋습니다. 이런 맥락은 프롬프트를 구성하는 텍스트와 별개로 존재할 수 있기에 분리가 가능하고, 버전관리 시에 별도로 명시하여 주는 것만으로도 프롬프트를 파악하는 데 큰 무리가 없어집니다.

프롬프트가 '개선'된다는 것

그렇다면 프롬프트가 '개선된다'는 것은 무엇을 의미할까요? 프롬프트가 개선된다는 것은 어떤 특정한 의도를 가지는 방향으로 계속해서 발전하는 것을 의미합니다. 그렇다면 이런 질문이 예상됩니다.
"발전... 이요? 프롬프트에서 의미하는 특정한 기능의 구현을 달성하는 게 아니라요?"
사실 이렇게 되지 못하는 것은 몇 가지 이유가 존재합니다. 우선, LLM의 특성상 프롬프트가 어떤 연산으로 특정 텍스트가 도출되는지를 완전하게 알지 못합니다. 매커니즘은 알 수 있어도, 연산의 진행에 대해서는 가려져있는 블랙박스입니다. xAI의 맥락에서 Anthropic과 같은 기업이 연구를 진행 중이지만, 아직까지 우리 손에 잡힌 것이 없으니 그렇다고 합시다.
또한 프롬프트를 이용해서 응답을 생성할 때에 특정한 결과가 나오는 것은 확률에 기반한 샘플링으로 진행됩니다. 프롬프트 엔지니어는 여기서 이 응답을 구성하는 단어들이, 우리가 원하는 특정한 방향으로 더 잘 나오게끔 그 편향을 다듬는 역할을 하는 것이지요. 그렇지만서도 그 응답을 형성하는 것은 높은 확률이지 특정한 몇몇 테스크를 제외하고는 '완전히 100% 무조건' 된다는 의미는 아닙니다. 우리는 유의미한 수준으로 오류율을 줄일 수 있을 뿐이지요. 99.95%~99.98% 정도라면, 일반적인 상황이라면 거의 믿을만 하지 않습니까.
게다가 프롬프트가 대처해야할 상황이나 입력값은, 곧 유저가 자연어로 던져낼 입력값이기에 이를 예측하는 것은 거의 카오스에 가까운 일입니다. 어플리케이션에서 특정한 유저 데이터에 기반해서 푸쉬메세지를 생성한다던가하는 건 좁은 영역의 입력 데이터이니 충분히 예측이 가능합니다. 하지만 그 범위가 더 넓다면 어떨까요? 예를 들어서, B2C 챗봇과 같은 경우 사용자가 던질 입력의 범위는 무궁무진합니다. 사용자는 호의적으로 말할 수도, 의도한 기능 외적으로 사용할 수도, 욕설을 할 수도, 심지어는 프롬프트 공격을 해서 시스템 프롬프트의 탈취를 시도할 수도 있습니다. 이에 대한 입력값에 출력값은 저마다 다르게 나와야 하죠. 하지만 LLM의 본질인 '잘 알려주면 잘 답한다'에 의해서 어떻게 행동할지는 '모든' 영역을 사전에 예측할 수는 없습니다.
그렇기에 프롬프트 엔지니어링은 확률적으로 아주 높은 값의 신뢰도로 약속할 뿐, 프로그래밍과 같이 0과 1로 완전함에는 도달할 수 없습니다. 프롬프트를 개선한다는 것은 100%의 결과가 구현된 프롬프트를 만드는 것이 아니라, 특정한 결과값이 잘 나오도록 확률을 높이고자 프롬프트를 업데이트하는 것에 가깝습니다.
그럼 프롬프트는 구체적으로 어떻게 개선할 수 있을까요?
여러가지 방법이 있지만, 크게 세 가지의 방향성을 가지고 분류할 수 있습니다.
프롬프트의 사이즈를 줄이고, 표현과 텍스트를 축약하는 '다운사이징' (Downsizing)
프롬프트의 표현과 텍스트를 늘려가면서 사이즈가 늘어나는 '업사이징' (Upsizing)
프롬프트의 사이즈는 그대로지만 그 표현이나 텍스트를 수정하고 교체하는 '리파이닝' (Refining)
첫 번째는 프롬프트의 사이즈를 줄이고 표현과 텍스트를 간결하게 만드는 '다운사이징(Downsizing)'입니다. 이는 불필요한 요소를 제거하고 핵심 정보만 남겨 프롬프트의 길이를 줄이는 과정이라고 볼 수 있습니다. 이렇게 하면 LLM의 추론시 더 중요한 부분에 더 집중할 수 있습니다. 다운사이징을 하는 이유는 더 있는데, 충분히 늘어난 복잡성을 사이즈를 줄여서 복잡성도 함께 줄이고자 할 때도 다운사이징을 중심으로 하여 프롬프트를 개선합니다. 30,000자가 되는 프롬프트 파이프라인을 다운사이징해서 10,000자 가까지 줄여낼 수 있다면, 대개 기능의 발현을 어느정도 접더라도 프롬프트의 복잡성 자체를 줄여서 이후에 기능 추가와 수정에 더 대응하기 쉽도록 할 수 있습니다. 텍스트 응답 속도나 비용면에서의 개선도 이루어질 수 있고요.
두 번째 방법은 프롬프트의 표현과 텍스트를 확장해서 더 구체적이고 상세한 정보를 제공하는 '업사이징(Upsizing)'입니다. 업사이징을 통해 모델이 더 풍부한 컨텍스트를 바탕으로 기능에 도달할 수 있습니다. 이렇게 하면 특정 요구사항을 반영하기에도 훨씬 용이해집니다. 보통의 기능 추가와 구현을 할 때 프롬프트에 텍스트를 더 붙여서 한다는 점을 상기해보면, 업사이징은 프롬프트 엔지니어링의 개발과정에서 매번 일어나는 일임을 알 수 있습니다.
세 번째 방법은 프롬프트의 사이즈는 그대로 두고 표현이나 텍스트를 수정하고 교체하는 '리파이닝(Refining)'이에요. 리파이닝은 프롬프트의 정밀성을 높이고 모호성을 줄이기 위해 표현을 다듬는 과정입니다. 보통의 디테일 개선과 다른 표현을 찾아나가면서 응답의 정도를 찾아가는 개선과정을 포함합니다.
프롬프트를 개선한다는 것은 이 세 가지를 적절한 사이클로 계속 진행해서 목표한 기능이 높은 확률로 나올 수 있도록 접근하는 것을 의미합니다. 예를 들어, 초기 프롬프트를 업사이징해서 충분한 정보를 제공한 후 리파이닝을 통해 표현을 다듬거나, 과도하게 긴 프롬프트를 다운사이징해서 핵심만 남기고 필요한 경우 추가적인 세부사항을 업사이징으로 진행하는 경우도 있지요. 이러한 과정 사이사이는 지속적인 실험과 시도의 연속이고, 그러한 실험과 개선이 곧 프롬프트 엔지니어링을 이루는 큰 근간 중 하나라고 볼 수 있습니다.
그렇다면... 우리가 아무것도 없는 상황에서 실험을 할 수는 없지 않겠습니까? 우리가 개발하는 프롬프트가 담긴 LLM 파이프라인이 개발용이면 모를까, 고객들에게 이미 서빙이 되고 있다면 두 말할 나위도 없구요. 세상에 누가 '실험실'과 '안전장치'도 없이 그런 위험한 일을 할까요? 그렇다면 프롬프트 엔지니어는 어떻게 안전하게 실험하고 개선하면서, 개선에서 오는 오작동과 위험을 관리할 수 있을까요? 다음 주제에서 이를 다루어 봅시다.
Two-Jay_prompt_engineer
Subscribe to 'Two-Jay_prompt_engineer'
안녕하세요. Prompt Engineer TwoJay 입니다.
저를 구독하시면, 프롬프트를 짜오면서 제가 겪은 경험과 생각들을 나눌 때에 더 빠르게 소식을 아실 수 있습니다. 🥰
Subscribe
👍
1