여러분, 혹시 이런 생각 해보신 적 없으신가요? 인공지능, 특히 챗GPT나 구글의 Gemini 같은 대규모 언어 모델(Large Language Model, LLM)이 세상을 떠들썩하게 만들고 있는데, 이걸 제대로 활용하려면 데이터 과학자나 머신러닝 엔지니어 같은 전문가만 가능한 거 아니야? 라고 말입니다.
하지만 여러분, 절대 그렇지 않습니다! 이 백서의 핵심 메시지 중 하나는 바로 이것입니다. “당신은 데이터 과학자나 머신러닝 엔지니어가 될 필요가 없습니다 – 누구나 프롬프트를 작성할 수 있습니다.” 정말 놀랍지 않습니까? 이제 인공지능이라는 강력한 도구가 더 이상 소수의 전문가들의 전유물이 아니라는 말입니다.
자, 그럼 '프롬프트(Prompt)'란 도대체 무엇일까요? 쉽게 말해, 우리가 LLM이라는 똑똑한 기계에게 **일을 시키기 위해 던지는 ‘명령’ 또는 ‘질문’**이라고 생각하시면 됩니다. 우리가 Gemini 챗봇과 대화할 때 입력하는 모든 텍스트가 바로 프롬프트인 셈이죠. 때로는 텍스트뿐만 아니라 이미지를 함께 입력하여 “이 이미지에 어울리는 시를 써줘” 라고 요청할 수도 있습니다. 이렇게 LLM이 특정 결과물을 만들어내도록 유도하기 위해 우리가 제공하는 입력값, 그것이 바로 프롬프트입니다.
그런데 말입니다, 누구나 프롬프트를 작성할 수 있다는 것이, 누구나 ‘효과적인’ 프롬프트를 쉽게 작성할 수 있다는 의미는 결코 아닙니다. 바로 여기서 **프롬프트 엔지니어링(Prompt Engineering)**이라는 새로운 기술, 아니 어쩌면 '예술’에 가까운 분야가 등장하게 됩니다.
자, 프롬프트 엔지니어링을 제대로 이해하려면, 먼저 우리가 다루려는 대상, 즉 LLM이 어떻게 작동하는지 그 기본적인 원리를 다시 한번 되짚어볼 필요가 있습니다. 아주 근본적으로 말해서, LLM은 **고도로 정교화된 ‘예측 기계(Prediction Engine)’**라고 할 수 있습니다. 이게 무슨 말이냐고요?
LLM은 엄청난 양의 텍스트 데이터(인터넷의 웹페이지, 책, 기사 등)를 학습합니다. 그리고 이 학습을 통해 얻은 방대한 지식을 바탕으로, **주어진 일련의 텍스트(우리가 입력한 프롬프트) 다음에 어떤 단어, 더 정확히는 어떤 ‘토큰(Token)’**이 나올 확률이 가장 높은지를 예측합니다. 예를 들어, 우리가 “옛날 옛날 아주 먼 옛날에, 깊은 산골짜기에 마음씨 착한 OOO” 라고 프롬프트를 주면, LLM은 학습 데이터에서 ‘마음씨 착한’ 다음에 가장 많이 나왔던 단어들(예: ‘할머니’, ‘나무꾼’, ‘호랑이’)의 확률을 계산하고, 그중 가장 확률 높은 단어(아마도 ‘할머니’?)를 다음 토큰으로 예측하여 내놓는 식입니다.
중요한 것은 LLM이 이 과정을 한 번만 하고 끝내는 것이 아니라는 점입니다. 예측된 토큰(‘할머니’)을 원래 프롬프트의 끝에 다시 덧붙여서, 이제 “…마음씨 착한 할머니가 OOO” 라는 더 길어진 텍스트를 기반으로 그 다음에 올 토큰을 또 예측합니다. 이런 식으로 이전에 예측된 토큰을 계속해서 입력 시퀀스에 추가하면서 다음에 올 토큰을 연쇄적으로 예측해나가는 과정을 반복함으로써, 우리가 보는 긴 문장이나 글 전체를 생성해내는 것입니다. 이때 다음에 올 토큰을 예측하는 기준은, 단순히 바로 앞 단어만 보는 것이 아니라, 지금까지 입력된 전체 텍스트의 맥락과, LLM이 학습 과정에서 습득한 방대한 언어 패턴 및 세상 지식 사이의 복잡한 관계를 고려하여 결정됩니다. 정말 놀라운 능력이지요?
자, 이제 LLM이 어떻게 작동하는지 알았으니, 프롬프트 엔지니어링이 무엇인지 감이 오시나요? 우리가 프롬프트를 작성한다는 것은, 바로 이 예측 기계인 LLM이 우리가 원하는 방향으로, 우리가 기대하는 결과에 해당하는 '올바른 토큰 시퀀스(Right Sequence of Tokens)'를 예측하도록 유도하는 과정이라고 할 수 있습니다. 즉, 그냥 아무렇게나 질문을 던지는 것이 아니라, LLM이 가장 정확하고 유용하며 우리가 원하는 형태의 결과물을 생성하도록 전략적으로 안내하는 고품질의 프롬프트를 설계하는 모든 과정이 바로 프롬프트 엔지니어링입니다.
이 과정은 단순히 한 번에 완벽한 프롬프트를 뚝딱 만들어내는 것이 아닙니다. 마치 조각가가 돌덩이를 깎아 작품을 만들듯, 혹은 요리사가 최고의 맛을 내기 위해 레시피를 계속 수정하듯, 끊임없이 실험하고 다듬는(tinkering) 반복적인 과정을 포함합니다. 어떤 단어를 사용하고 어떤 구조로 문장을 짜야 LLM이 더 잘 알아들을까? 프롬프트 길이는 어느 정도가 적절할까? 어떤 스타일과 톤으로 작성해야 원하는 결과물의 분위기를 잘 살릴 수 있을까? 이런 질문들에 답을 찾아가며 최고의 프롬프트를 찾아내고(finding the best prompt), 프롬프트의 길이를 최적화하며(optimizing prompt length), 주어진 과제와의 관련성 속에서 프롬프트의 작성 스타일과 구조를 평가하고 개선하는(evaluating a prompt’s writing style and structure) 모든 노력이 바로 프롬프트 엔지니어링의 핵심입니다.
결국, 자연어 처리와 LLM의 맥락에서 프롬프트란, 모델로부터 특정 응답이나 예측을 생성하도록 유도하기 위해 우리가 모델에게 제공하는 입력값 그 자체라고 정의할 수 있습니다. 그리고 프롬프트 엔지니어링은 그 입력값을 가장 효과적으로 설계하는 기술이자 과학인 셈이죠.
그렇다면 이렇게 잘 설계된 프롬프트로 우리는 LLM을 활용하여 구체적으로 어떤 종류의 작업들을 수행할 수 있을까요? 그 가능성은 정말 무궁무진합니다. 몇 가지 대표적인 예를 들어보자면 다음과 같습니다.
이 외에도 창의적인 글쓰기(시, 소설, 마케팅 문구 등), 대화형 챗봇 응답 생성, 데이터 분석 및 시각화 아이디어 제안 등 LLM과 프롬프트를 활용할 수 있는 분야는 정말 상상하는 것 이상으로 넓고 다양합니다.
물론, 이러한 작업들을 위한 효과적인 프롬프트 예시들은 구글에서 제공하는 다양한 프롬프팅 가이드에서도 찾아볼 수 있습니다. [참고 2, 3: Google’s prompting guides] 이 가이드들을 참고하시면 간단하면서도 강력한 프롬프트 작성의 기본기를 다지는 데 큰 도움이 될 것입니다.
하지만 명심하십시오. 프롬프트 엔지니어링의 여정은 모델을 선택하는 것에서부터 시작됩니다. 당신이 어떤 LLM(예: Google의 Gemini, OpenAI의 GPT 시리즈, Anthropic의 Claude, 혹은 Meta의 Llama 같은 오픈 소스 모델)을 사용하느냐에 따라 동일한 프롬프트라도 결과가 달라질 수 있습니다. 따라서 특정 모델의 특성과 강점을 이해하고, 그 모델에 최적화된 방식으로 프롬프트를 조정하고 다듬는 과정이 반드시 필요합니다.
그리고 프롬프트 자체를 잘 설계하는 것 외에도, 우리가 반드시 고려하고 실험해야 할 또 다른 중요한 요소가 있습니다. 그것은 바로 LLM의 출력을 제어하는 다양한 '설정값(Configuration)'들을 조정하는 것입니다. 다음 섹션에서는 이 LLM 출력 설정에 대해 더 자세히 알아보겠습니다.
자, 이제 당신은 사용할 LLM 모델을 선택했고, 어떤 작업을 수행할지 명확한 목표를 세웠으며, 그 목표를 달성하기 위한 멋진 프롬프트 초안까지 작성했습니다. 이제 프롬프트를 입력하고 결과를 기다리기만 하면 될까요? 잠깐! 아직 중요한 단계가 하나 더 남아 있습니다. 바로 LLM의 출력을 미세하게 제어하는 다양한 '설정값(Configuration)'들을 이해하고, 당신의 작업 목적에 맞게 최적으로 조정하는 것입니다.
대부분의 LLM 서비스(특히 Vertex AI와 같은 플랫폼이나 API를 통해 직접 모델을 사용하는 경우)는 모델의 응답 생성 방식을 제어할 수 있는 여러 가지 설정 옵션들을 제공합니다. 예를 들어, 생성되는 응답의 길이를 제한하거나, 응답의 창의성 또는 결정성 수준을 조절하는 등의 설정이 가능하죠. 효과적인 프롬프트 엔지니어링은 단순히 프롬프트 텍스트 자체를 잘 작성하는 것뿐만 아니라, 이러한 모델 설정값들을 당신의 특정 작업 요구사항에 맞춰 최적으로 구성하는 과정까지 포함합니다. 프롬프트와 설정값, 이 두 가지 요소가 마치 자동차의 핸들과 액셀처럼 함께 작동하여 최종 결과물의 품질과 방향을 결정하기 때문입니다.
가장 기본적이면서도 중요한 설정 중 하나는 바로 LLM이 생성하는 응답의 ‘길이(Output Length)’, 즉 생성될 토큰(Token)의 최대 수를 제한하는 것입니다. 토큰은 LLM이 텍스트를 처리하는 기본 단위로, 대략 영어 단어 하나 또는 그보다 약간 작은 단위라고 생각하시면 됩니다. (한국어의 경우 음절이나 형태소 단위가 될 수 있습니다.)
왜 출력 길이를 제어하는 것이 중요할까요? 첫째, LLM이 더 많은 토큰을 생성하도록 요구할수록, 그만큼 더 많은 계산(Computation) 능력이 필요합니다. 이는 곧 더 높은 에너지 소비, 잠재적으로 더 느린 응답 속도, 그리고 더 높은 사용 비용으로 직결됩니다. 따라서 불필요하게 긴 응답을 생성하도록 방치하는 것은 자원 낭비일 수 있습니다.
둘째, 출력 길이를 제한한다고 해서 LLM이 갑자기 더 세련되거나 간결한 스타일로 응답을 생성하는 것은 아닙니다. 이것은 매우 중요한 오해입니다! 길이를 줄이면, LLM은 그냥 예측하던 토큰 생성을 중간에 멈출 뿐이지, 스스로 내용을 요약하거나 압축하여 짧게 만드는 것이 아닙니다. 따라서 만약 당신이 처음부터 간결하고 핵심적인 짧은 응답을 원한다면, 단순히 출력 길이 설정값만 줄이는 것이 아니라, 프롬프트 자체에 “결과를 한 문장으로 요약해줘” 또는 “핵심 키워드 5개만 나열해줘” 와 같이 명확한 지시를 포함하여 원하는 길이에 맞는 응답을 생성하도록 유도해야 합니다. 즉, 출력 길이 설정과 프롬프트 엔지니어링이 함께 이루어져야 한다는 말입니다.
셋째, **특히 특정 프롬프팅 기법(예: 뒤에서 다룰 ReAct)**을 사용할 때는 출력 길이 제한이 더욱 중요해질 수 있습니다. 왜냐하면 LLM이 이미 당신이 원하는 핵심 응답을 생성한 후에도, 정해진 길이 제한에 도달할 때까지 계속해서 불필요하거나 관련 없는 토큰들을 추가적으로 생성하려는 경향을 보일 수 있기 때문입니다. 이런 경우 적절한 길이 제한을 통해 불필요한 토큰 생성을 막고 원하는 결과만 깔끔하게 얻어낼 수 있습니다.
따라서, 당신의 작업 목적과 예산, 그리고 원하는 응답 속도를 고려하여 적절한 출력 토큰 길이를 설정하는 것은 매우 중요한 첫 단계입니다. 무조건 길게 설정하는 것이 좋은 것은 결코 아닙니다.
자, 이제 좀 더 흥미롭고 중요한 설정으로 넘어가 보겠습니다. 바로 '샘플링 제어(Sampling Controls)'입니다. 앞서 LLM의 작동 원리를 설명하면서, LLM이 다음에 올 단일 토큰을 확정적으로 예측하는 것이 아니라, 다음에 올 가능성이 있는 여러 토큰들에 대한 '확률 분포(Probability Distribution)'를 예측한다고 말씀드렸던 것 기억하시나요? 즉, LLM의 어휘 사전에 있는 모든 토큰 각각에 대해 ‘다음에 이 토큰이 나올 확률은 몇 %이다’ 라는 예측값을 계산하는 것입니다.
그렇다면 이 확률 분포로부터 최종적으로 어떤 단 하나의 토큰을 선택하여 다음 응답으로 생성할지를 결정하는 과정이 필요하겠죠? 바로 이 과정을 제어하는 것이 샘플링 제어 설정입니다. LLM은 이 설정을 통해 예측된 토큰 확률들을 처리하여 최종 출력 토큰을 선택하게 됩니다. 가장 흔하게 사용되는 샘플링 제어 설정으로는 **‘Temperature’, ‘Top-K’, 그리고 ‘Top-P’**가 있습니다. 이 설정들은 **생성되는 텍스트의 무작위성(Randomness)과 다양성(Diversity), 그리고 예측 가능성(Determinism)**을 조절하는 데 핵심적인 역할을 합니다.
’Temperature’ 설정은 이름 그대로 응답 생성 과정의 '온도’를 조절한다고 생각하시면 이해하기 쉽습니다. 이것은 토큰 선택 과정에서의 무작위성(Randomness)의 정도를 제어하는 매우 중요한 파라미터입니다.
**낮은 Temperature 값(예: 0.1, 0.2)**을 설정하면, 마치 온도가 낮을 때 분자들이 덜 활발하게 움직이는 것처럼, LLM은 가장 확률이 높은 토큰들을 더 강하게 선호하게 됩니다. 즉, 예측된 확률 분포에서 확률이 높은 토큰들이 선택될 가능성이 훨씬 커지고, 확률이 낮은 토큰들은 거의 무시됩니다. 그 결과, 생성되는 응답은 더 예측 가능하고, 일관성 있으며, 사실 기반적이고, 결정론적인(Deterministic) 경향을 보이게 됩니다. 만약 당신이 LLM에게 명확한 정답이 있는 질문(예: “프랑스의 수도는 어디인가?”)에 답하게 하거나, 주어진 텍스트를 충실하게 요약하도록 하거나, 혹은 일관된 스타일의 글쓰기를 요구하는 등 정확성과 일관성이 중요한 작업을 시킨다면, 낮은 Temperature 값을 설정하는 것이 좋습니다.
가장 극단적인 경우인 Temperature 값 0은 **‘탐욕적 디코딩(Greedy Decoding)’**이라고도 불리는데, 이 경우 LLM은 무조건 예측된 확률이 가장 높은 단 하나의 토큰만을 다음 토큰으로 선택하게 됩니다. 따라서 동일한 프롬프트를 입력하면 항상 동일한 응답이 생성되는 완전한 결정론적 응답을 얻을 수 있습니다. (단, 만약 확률이 가장 높은 토큰이 두 개 이상 동률로 존재할 경우, 동점 처리 방식(Tiebreaking) 구현에 따라 결과가 약간 달라질 수는 있습니다.)
반대로, **높은 Temperature 값(예: 0.8, 0.9, 혹은 1.0 이상)**을 설정하면, 마치 온도가 높을 때 분자들이 매우 활발하고 예측 불가능하게 움직이는 것처럼, LLM은 확률이 낮은 토큰들도 더 많이 고려하고 선택할 가능성이 커집니다. 즉, 예측된 확률 분포가 전반적으로 더 평평해져서, 확률이 높은 토큰과 낮은 토큰 간의 선택 확률 차이가 줄어들게 됩니다. 그 결과, 생성되는 응답은 더 다양하고(Diverse), 예상치 못하며(Unexpected), 창의적이고(Creative), 때로는 약간 이상하거나 관련성이 떨어지는(Less Relevant) 경향을 보이게 됩니다. 만약 당신이 LLM에게 창의적인 이야기나 시를 쓰게 하거나, 브레인스토밍을 위한 다양한 아이디어를 생성하도록 하거나, 혹은 틀에 박히지 않은 새로운 관점의 글쓰기를 요구하는 등 독창성과 다양성이 중요한 작업을 시킨다면, 높은 Temperature 값을 설정하는 것이 도움이 될 수 있습니다.
하지만 Temperature 값을 너무 높게 설정하면(예: 1.0을 훨씬 넘어서 10단위 이상으로), 모든 토큰들이 거의 동등한 확률로 선택될 가능성이 커져서, 생성되는 텍스트가 완전히 무작위적이고 의미 없는 단어들의 나열처럼 되어버릴 위험이 있습니다. 마치 술에 취해 횡설수설하는 사람처럼 말이죠.
Gemini 모델의 Temperature 제어는 머신러닝에서 흔히 사용되는 **소프트맥스(Softmax) 함수의 온도 파라미터(T)**와 유사한 방식으로 이해할 수 있습니다. 낮은 Temperature 설정은 낮은 소프트맥스 온도(T)와 같이, 특정 소수의 높은 확률 값에 집중하게 만들어(Sharpening the distribution) 확실성을 높이는 효과를 냅니다. 반대로 높은 Temperature 설정은 높은 소프트맥스 온도(T)처럼, 확률 분포를 더 평평하게 만들어(Flattening the distribution) 다양한 토큰들이 선택될 수 있는 가능성을 열어주어 불확실성을 높이는 효과를 냅니다. 따라서 창의적인 결과물을 실험하는 것처럼 엄격하고 정확한 응답이 필수적이지 않은 시나리오에서는 높은 Temperature 설정이 유용할 수 있습니다.
결론적으로, Temperature 설정은 '정답’과 ‘창의성’ 사이의 균형을 맞추는 중요한 조절 장치입니다. 당신의 작업 목표에 따라 적절한 온도를 설정하는 것이 중요합니다.
**'Top-K’와 ‘Top-P’(Nucleus Sampling이라고도 함)**는 LLM이 다음 토큰을 선택할 때 고려하는 후보 토큰의 범위를 제한하는 두 가지 주요한 샘플링 설정입니다. 이들은 예측된 전체 토큰 확률 분포 중에서 가장 확률이 높은 일부 토큰들만 남기고 나머지는 버리는 ‘필터링’ 역할을 합니다. Temperature와 마찬가지로, 이 설정들 역시 생성되는 텍스트의 무작위성과 다양성을 제어하는 데 사용됩니다.
Top-K 샘플링은 아주 간단합니다. 모델이 예측한 전체 토큰 확률 분포에서 확률이 가장 높은 상위 K개의 토큰들만 선택하고, 그 K개의 토큰들 중에서만 다음 토큰을 샘플링(보통 Temperature 설정과 함께 사용됨)하는 방식입니다. 예를 들어, K=10으로 설정하면, LLM은 다음에 올 가능성이 가장 높은 10개의 토큰들만 후보로 남기고 나머지 모든 토큰들은 고려 대상에서 제외합니다.
K 값이 높을수록(예: 40, 50) 더 많은 토큰들이 후보로 남게 되므로, 생성되는 응답은 더 창의적이고 다양해질 가능성이 커집니다. 반대로 K 값이 낮을수록(예: 5, 10) 후보 토큰의 범위가 좁아지므로, 생성되는 응답은 더 제한적이고 예측 가능하며 사실에 기반한 결과가 나올 가능성이 높아집니다. 만약 K 값을 1로 설정하면, 이것은 오직 가장 확률 높은 단 하나의 토큰만을 선택하는 것이므로, 앞서 설명한 Temperature 0의 탐욕적 디코딩(Greedy Decoding)과 동일한 효과를 갖게 됩니다. 반대로 K 값을 LLM 어휘 사전의 전체 크기만큼 매우 크게 설정하면, 사실상 Top-K 필터링이 작동하지 않는 것과 같아집니다.
**Top-P 샘플링 (Nucleus Sampling)**은 Top-K와 약간 다른 방식으로 후보 토큰을 선택합니다. 이것은 확률이 가장 높은 토큰부터 시작하여 누적 확률(Cumulative Probability)이 미리 정해진 특정 값 P(보통 0과 1 사이의 값)를 초과하지 않는 최소한의 토큰 집합(이것을 '핵심(Nucleus)'이라고 부름)만을 선택하고, 그 집합 내의 토큰들 중에서만 다음 토큰을 샘플링하는 방식입니다. [참고 4: Nucleus Sampling]
예를 들어, P=0.9로 설정했다고 가정해 봅시다. LLM이 예측한 확률 분포에서 가장 확률 높은 토큰 A(확률 0.5), 두 번째 토큰 B(확률 0.3), 세 번째 토큰 C(확률 0.1), 네 번째 토큰 D(확률 0.05) … 등이 있다고 할 때, A(0.5) + B(0.3) = 0.8 이고, A+B+C(0.1) = 0.9 이므로, Top-P 샘플링은 A, B, C 세 개의 토큰만을 후보로 선택하고 나머지 토큰들은 버립니다. 만약 첫 번째 토큰 A의 확률이 0.95로 매우 높았다면, P=0.9 설정에서는 오직 A 하나만을 후보로 선택하게 될 것입니다.
P 값의 범위는 0부터 1까지입니다. P 값이 0에 가까울수록(혹은 매우 작은 값일 경우), 대부분의 LLM 구현에서는 가장 확률 높은 단 하나의 토큰만을 고려하게 되어 탐욕적 디코딩과 유사한 결정론적인 결과를 낳게 됩니다. (Temperature 및 Top-K 설정은 무관해집니다.) 반대로 P 값을 1로 설정하면, 확률이 0이 아닌 모든 토큰들이 후보 집합에 포함될 수 있으므로, 사실상 Top-P 필터링이 작동하지 않는 것과 같아집니다.
그렇다면 Top-K와 Top-P 중 어떤 것을 사용해야 할까요? 정답은 없습니다. 두 방식은 후보 토큰을 선택하는 기준이 다르기 때문에(Top-K는 개수 기준, Top-P는 누적 확률 기준), 생성되는 결과물의 특성도 약간 다를 수 있습니다. Top-P는 확률 분포의 모양에 따라 선택되는 후보 토큰의 개수가 동적으로 변한다는 특징이 있습니다. (예: 확률이 높은 소수 토큰에 집중될 때는 후보 수가 적어지고, 여러 토큰에 확률이 분산될 때는 후보 수가 많아짐). 가장 좋은 방법은 두 가지 방법(혹은 두 가지를 함께 사용하는 것)을 모두 실험해보면서, 당신이 찾고 있는 결과물을 가장 잘 만들어내는 설정을 직접 찾아보는 것입니다.
중요한 점은 Top-K, Top-P, Temperature, 그리고 생성 토큰 수 제한 설정이 각각 독립적으로 작동하는 것이 아니라, 서로 복잡하게 영향을 주고받으며 최종 결과물에 영향을 미친다는 것입니다. 따라서 어떤 설정을 선택하고 조합할지는 당신의 구체적인 작업 내용과 원하는 결과물의 특성(예: 정확성 vs 창의성, 간결성 vs 상세함)에 따라 신중하게 결정해야 합니다.
또한, 당신이 사용하는 특정 LLM 모델이나 플랫폼(예: Vertex AI Studio)이 이 여러 샘플링 설정들을 어떻게 조합하여 최종 토큰을 선택하는지 그 방식을 정확히 이해하는 것도 중요합니다. 예를 들어, Vertex AI Studio와 같이 Temperature, Top-K, Top-P 설정이 모두 제공되는 경우, 일반적으로는 먼저 Top-K와 Top-P 기준을 모두 만족하는 토큰들만을 후보로 선별한 다음, 그 후보 토큰들 사이에서 Temperature 설정을 적용하여 최종적으로 하나의 토큰을 샘플링하는 방식으로 작동합니다. 만약 Top-K 또는 Top-P 중 하나만 설정 가능하다면, 해당 설정만 적용됩니다. 만약 Temperature 설정이 아예 제공되지 않는 경우라면, Top-K 및/또는 Top-P 기준을 만족하는 후보 토큰들 중에서 무작위로(보통은 균등 확률로) 하나를 선택하게 될 수도 있습니다.
한 가지 주의할 점은, 특정 샘플링 설정 값을 극단적으로 설정할 경우, 다른 설정 값들이 완전히 무시되거나 무의미해질 수 있다는 것입니다. 예를 들어, Temperature를 0으로 설정하면 Top-K와 Top-P 값은 아무런 영향을 미치지 못하고 무조건 가장 확률 높은 토큰만 선택됩니다. 마찬가지로 Top-K를 1로 설정하거나 Top-P를 0에 가깝게 설정해도 다른 설정들은 무의미해집니다. 반대로 Temperature를 극단적으로 높게 설정하면 Top-K/Top-P 필터링을 통과한 토큰들 사이에서 거의 무작위로 선택이 이루어지게 됩니다. 이러한 설정 간의 상호작용을 잘 이해하고 활용하는 것이 중요합니다.
일반적으로 어디서부터 시작해야 할지 막막하다면, 다음과 같은 값을 시작점으로 삼아 실험해보는 것을 추천합니다. Temperature: 0.2, Top-P: 0.95, Top-K: 30. 이 설정은 비교적 일관성 있고 조리 있는 결과물을 생성하면서도 어느 정도의 창의성을 허용하는 균형 잡힌 조합입니다. 만약 더욱 창의적이고 예상치 못한 결과를 원한다면, Temperature: 0.9, Top-P: 0.99, Top-K: 40 정도로 시작해볼 수 있습니다. 반대로 더 정확하고 사실에 기반하며 덜 창의적인 결과를 원한다면, Temperature: 0.1, Top-P: 0.9, Top-K: 20 정도로 시작해볼 수 있습니다. 그리고 만약 당신의 작업이 수학 문제 풀이나 코드 변환처럼 항상 단 하나의 정답만이 존재하는 경우라면, Temperature를 0으로 설정하여 가장 확률 높은 답을 얻도록 하는 것이 가장 좋습니다.
하지만 명심하십시오! Temperature, Top-K, Top-P 값을 높여 모델에게 더 많은 자유도(Freedom)를 부여할수록, 즉 더 창의적인 응답을 유도할수록, 모델이 생성하는 텍스트가 당신의 원래 의도나 질문과 관련성이 떨어지거나(Less Relevant), 심지어는 완전히 엉뚱한 내용(환각, Hallucination)을 만들어낼 위험성도 함께 높아진다는 사실을 반드시 인지해야 합니다.
마지막으로 경고(WARNING) 하나! 혹시 LLM이 생성한 응답의 마지막 부분이 의미 없는 단어나 구, 혹은 동일한 문장 구조를 계속해서 반복하는 것을 본 적이 있으신가요? 이것은 **“반복 루프 버그(Repetition Loop Bug)”**라고 알려진 현상으로, LLM 사용 시 흔히 발생하는 문제 중 하나입니다. 모델이 특정 단어나 구절에 ‘갇혀서(Stuck)’ 그 패턴을 벗어나지 못하고 계속해서 반복 생성하는 것인데요, 이는 특히 부적절하게 설정된 Temperature 값(너무 낮거나 너무 높을 때 모두 발생 가능)이나 Top-K/Top-P 설정에 의해 악화될 수 있습니다. 낮은 Temperature에서는 모델이 너무 결정론적으로 가장 확률 높은 경로만 따라가다가 이전에 생성된 텍스트로 다시 돌아가는 루프에 빠질 수 있고, 높은 Temperature에서는 과도하게 무작위적인 선택이 우연히 이전 상태로 돌아가게 만들어 루프를 형성할 수도 있습니다. 이런 경우, 결과물은 단조롭고 아무런 도움이 되지 않는 내용으로 채워지게 됩니다. 이 문제를 해결하기 위해서는 Temperature와 Top-K/Top-P 값을 신중하게 조정하여 결정론(Determinism)과 무작위성(Randomness) 사이의 최적의 균형점을 찾는 섬세한 조정(Tinkering) 작업이 필요합니다.
결론적으로, LLM 출력 설정값을 마스터하는 것은 효과적인 프롬프트 엔지니어링의 필수적인 부분입니다. 당신의 작업 목표와 원하는 결과물의 특성을 명확히 이해하고, 각 설정값의 의미와 상호작용을 파악하며, 끊임없는 실험과 조정을 통해 최적의 조합을 찾아내는 노력이 필요합니다. 이것이야말로 당신이 LLM의 잠재력을 최대한으로 끌어내고 원하는 결과물을 얻어내는 비결입니다.
자, 이제 우리는 LLM이 어떻게 작동하고, 그 출력을 어떻게 미세 조정할 수 있는지에 대한 기본적인 이해를 갖추었습니다. 이제 본격적으로 **LLM에게 우리가 원하는 작업을 효과적으로 지시하고 관련성 높은 결과물을 얻어내기 위한 구체적인 ‘프롬프팅 기법(Prompting Techniques)’**들에 대해 알아볼 시간입니다.
LLM은 기본적으로 지시 사항을 따르도록(tuned to follow instructions) 미세 조정(Fine-tuning)되고, 방대한 양의 데이터를 학습했기 때문에 우리가 제시하는 프롬프트를 이해하고 그에 맞는 답변을 생성하는 놀라운 능력을 가지고 있습니다. 하지만 명심해야 할 것은, LLM은 결코 완벽하지 않다는 사실입니다! LLM은 우리가 프롬프트에 담은 의도를 '추론’하고 다음에 올 가장 ‘그럴듯한’ 텍스트를 '예측’할 뿐이지, 우리의 마음을 읽거나 복잡한 숨은 의미까지 완벽하게 이해하는 것은 아닙니다.
따라서 우리가 LLM으로부터 더 정확하고, 더 관련성 높으며, 더 유용한 결과물을 얻기 위한 가장 기본적인 원칙은 아주 간단합니다. 바로 “프롬프트를 가능한 한 명확하고(Clear), 구체적이며(Specific), 모호하지 않게(Unambiguous) 작성하라!” 는 것입니다. 당신의 프롬프트 텍스트가 더 명확하고 구체적일수록, LLM은 당신이 원하는 바를 더 정확하게 파악하고 다음에 생성해야 할 가장 적절한 텍스트를 더 성공적으로 예측할 가능성이 높아집니다.
하지만 단순히 명확하게 작성하는 것을 넘어서, LLM이 어떻게 학습되고 작동하는지에 대한 이해를 바탕으로 고안된 특정 프롬프팅 기법들을 활용하면, 훨씬 더 효과적으로 LLM을 우리가 원하는 방향으로 유도하고 관련성 높은 결과물을 얻어낼 수 있습니다.
이제 프롬프트 엔지니어링이 무엇인지, 그리고 어떤 요소들을 고려해야 하는지에 대한 기본적인 이해를 갖추었으니, 지금부터 가장 중요하고 널리 사용되는 핵심적인 프롬프팅 기법들의 종류와 구체적인 예시들을 하나씩 자세히 살펴보겠습니다. 이 기법들을 당신의 프롬프트 작성 무기고에 장착한다면, 당신은 LLM을 훨씬 더 효과적으로 조련하고 활용하는 전문가로 거듭날 수 있을 것입니다!
가장 기본적이고 단순한 형태의 프롬프팅 기법은 바로 ‘제로샷(Zero-shot)’ 프롬프팅입니다. 이름에서 알 수 있듯이, 이 방식은 LLM에게 **어떠한 예시(Example)나 시연(Demonstration)도 제공하지 않고, 오직 수행해야 할 작업에 대한 설명(Description of a task)과 필요한 입력 텍스트(Some text for the LLM to get started with)**만을 제공하는 방식입니다. [참고 5: Zero Shot] 즉, ‘Zero examples’ 라는 의미에서 ‘Zero-shot’ 이라는 이름이 붙었습니다.
이때 입력 텍스트는 정말 무엇이든 될 수 있습니다. **단순한 질문(“대한민국의 수도는 어디인가?”)**이 될 수도 있고, **이야기의 시작 부분(“옛날 옛날 아주 먼 옛날에…”)**이 될 수도 있으며, 혹은 **구체적인 지시사항(“다음 문장을 요약하시오:”)**이 될 수도 있습니다. LLM은 이렇게 주어진 최소한의 정보를 바탕으로, 자신이 학습한 방대한 지식과 언어 패턴을 활용하여 작업을 수행하고 응답을 생성해야 합니다.
자, 그럼 제로샷 프롬프팅의 구체적인 예시를 한번 살펴볼까요? 예를 들어, 영화 리뷰 텍스트를 긍정(POSITIVE), 중립(NEUTRAL), 부정(NEGATIVE) 중 하나로 분류하는 작업을 LLM에게 시키고 싶다고 가정해 봅시다. 이때 우리는 Vertex AI Studio 와 같은 도구를 사용하여 프롬프트를 테스트해 볼 수 있습니다.
[표 1 예시: 제로샷 프롬프팅을 이용한 영화 리뷰 분류]
Classify movie reviews as POSITIVE, NEUTRAL or NEGATIVE.
Review: "Her" is a disturbing study revealing the direction humanity is headed if AI is allowed to keep evolving, unchecked. I wish there were more movies like this masterpiece.
Sentiment:
여기서 우리는 LLM에게 "영화 리뷰를 POSITIVE, NEUTRAL, NEGATIVE로 분류하라"는 명확한 지시를 주고, 실제 리뷰 텍스트를 제공한 다음, 마지막에 “Sentiment:” 라는 단어를 붙여서 LLM이 이어서 분류 결과를 예측하도록 유도했습니다. 리뷰 내용에는 “disturbing(불안한)” 이라는 부정적인 단어와 “masterpiece(걸작)” 이라는 긍정적인 단어가 함께 사용되어 약간의 혼동을 줄 수도 있지만, 전체적인 맥락을 파악하여 긍정적인 평가로 분류하는 것이 적절해 보입니다. 이처럼 제로샷 프롬프팅은 가장 간단하고 직관적인 방식이지만, LLM이 이미 해당 작업 유형에 대해 충분히 학습되어 있거나, 작업 자체가 비교적 단순하고 명확한 경우에 효과적으로 사용될 수 있습니다.
한 가지 팁! 위 표와 같이 프롬프트를 문서화하는 것은 매우 좋은 습관입니다. 당신의 프롬프트는 아마도 원하는 결과를 얻기까지 수많은 수정과 반복(Iteration)을 거치게 될 것입니다. 따라서 각 시도별 프롬프트 내용, 사용한 모델과 설정값, 그리고 그 결과를 체계적으로 기록해두는 것은 나중에 어떤 시도가 효과적이었고 어떤 시도가 실패했는지 되돌아보고 배우는 데 매우 중요합니다. 이 문서 뒷부분의 ‘모범 사례’ 섹션에서 프롬프트 개발 과정 추적 및 문서화의 중요성에 대해 더 자세히 다룰 예정이니(“다양한 프롬프트 시도 문서화” 참조), 꼭 기억해 두시기 바랍니다. 체계적인 기록은 당신의 프롬프트 엔지니어링 실력을 향상시키는 지름길입니다.
하지만 제로샷 프롬프팅만으로는 원하는 결과를 얻기 어려운 경우가 많습니다. 특히 작업이 더 복잡하거나, LLM이 이전에 접해보지 못한 새로운 유형의 작업을 요구하거나, 혹은 매우 구체적인 출력 형식이나 스타일을 요구하는 경우에는, 단순히 작업 설명만으로는 LLM이 우리의 의도를 정확히 파악하고 기대하는 결과물을 생성하기 어려울 수 있습니다. 마치 우리가 새로운 기술을 배울 때, 이론 설명만 듣는 것보다 실제 시연이나 예제를 직접 보는 것이 훨씬 더 이해하기 쉬운 것과 마찬가지입니다.
바로 이런 경우에 강력한 효과를 발휘하는 기법이 바로 ‘원샷(One-shot)’ 및 ‘퓨샷(Few-shot)’ 프롬프팅입니다. 이 기법들은 LLM에게 실제로 원하는 입력과 출력의 쌍(Pair)을 한 개(원샷) 또는 여러 개(퓨샷)의 예시(Example) 또는 시연(Demonstration) 형태로 프롬프트에 직접 포함하여 제공하는 방식입니다. [참고 7: Few Shot] 이렇게 예시를 보여줌으로써, 우리는 LLM이 “아하! 이런 종류의 입력을 받으면, 이런 형식과 내용으로 응답을 생성하라는 뜻이구나!” 라고 더 명확하게 이해하고 우리가 원하는 패턴이나 구조를 따라 응답을 생성하도록 효과적으로 유도(Steer)할 수 있습니다.
**‘원샷 프롬프팅’**은 이름 그대로 단 하나의 예시만을 제공하는 방식입니다. 이 하나의 예시가 LLM에게 모방해야 할 본보기 역할을 하여, 주어진 새로운 작업(Task)을 가장 유사한 방식으로 완료하도록 안내하는 아이디어에 기반합니다.
**‘퓨샷 프롬프팅’**은 여기서 한 걸음 더 나아가, 여러 개(보통 3~5개 이상)의 예시들을 제공하는 방식입니다. 여러 개의 다양한 예시를 통해 LLM에게 따라야 할 일관된 패턴(Pattern)을 더 명확하게 보여주고 학습시키는 접근법입니다. 아이디어는 원샷과 유사하지만, 여러 개의 예시를 통해 원하는 패턴을 반복적으로 보여줌으로써, LLM이 그 패턴을 더 확실하게 파악하고 새로운 입력에 대해서도 동일한 패턴을 따를 가능성을 높이는 것이죠.
그렇다면 퓨샷 프롬프팅에는 몇 개의 예시를 제공하는 것이 가장 좋을까요? 필요한 예시의 수는 몇 가지 요인에 따라 달라집니다. 작업의 복잡성(더 복잡할수록 더 많은 예시 필요), 제공하는 예시의 품질과 관련성(품질이 높고 관련성이 높을수록 적은 예시로도 충분), 그리고 당신이 사용하는 LLM 모델의 능력(더 뛰어난 모델일수록 적은 예시로도 잘 학습) 등이 영향을 미칩니다. 일반적인 경험 법칙(Rule of thumb)으로는 최소 3개에서 5개 정도의 예시를 사용하는 것이 좋다고 알려져 있습니다. 하지만 더 복잡한 작업을 수행해야 하거나 결과물의 일관성이 매우 중요한 경우에는 더 많은 예시가 필요할 수도 있습니다. 반대로, LLM 모델 자체의 입력 길이 제한(Input length limitation) 때문에 많은 예시를 제공하기 어려운 경우도 있을 수 있습니다. 따라서 실제로는 몇 개의 예시를 사용했을 때 가장 좋은 결과가 나오는지를 직접 실험을 통해 찾아보는 것이 중요합니다.
자, 그럼 퓨샷 프롬프팅의 구체적인 예시를 한번 살펴볼까요? 이번에는 고객의 피자 주문 내용을 입력받아서, 그것을 정해진 JSON 형식으로 파싱(Parsing)하는 작업을 LLM에게 시켜보겠습니다. 제로샷으로는 원하는 정확한 JSON 구조를 얻기 어려울 수 있으므로, 퓨샷 방식을 사용해 봅시다. 모델 설정은 이전과 동일하게 하되, 더 긴 응답(예시 포함)을 처리하기 위해 토큰 제한(Token Limit)을 늘려주어야 합니다.
[표 2 예시: 퓨샷 프롬프팅을 이용한 피자 주문 JSON 파싱]
Parse a customer's pizza order into valid JSON:
EXAMPLE 1:
I want a small pizza with cheese, tomato sauce, and pepperoni.
JSON Response:
{
"size": "small",
"type": "normal",
"ingredients": [["cheese", "tomato sauce", "peperoni"]]
}
EXAMPLE 2:
Can I get a large pizza with tomato sauce, basil and mozzarella
JSON Response:
{
"size": "large",
"type": "normal",
"ingredients": [["tomato sauce", "bazel", "mozzarella"]]
}
Now, I would like a large pizza, with the first half cheese and mozzarella. And the other tomato sauce, ham and pineapple.
JSON Response:
```* **예상 출력:**
```json
{
"size": "large",
"type": "half-half",
"ingredients": [["cheese", "mozzarella"], ["tomato sauce", "ham", "pineapple"]]
}
위 예시에서는 두 개의 구체적인 주문과 그에 해당하는 JSON 출력 예시를 먼저 보여주었습니다. 그리고 마지막에 새로운 주문 내용을 제시하고 LLM이 동일한 패턴과 형식에 따라 JSON 응답을 생성하도록 유도했습니다. 이렇게 명확한 예시들을 통해 LLM은 ‘size’, ‘type’, 'ingredients’와 같은 키(key)를 사용하고, 재료는 리스트 형태로 표현해야 한다는 복잡한 규칙과 패턴을 학습하여 새로운 주문에 대해서도 일관된 형식의 JSON 출력을 생성할 가능성이 훨씬 높아집니다.
퓨샷 프롬프팅을 위한 예시를 선택할 때는 몇 가지 주의할 점이 있습니다. 첫째, 제공하는 예시들은 당신이 수행하려는 실제 작업과 매우 관련성이 높아야(Relevant) 합니다. 엉뚱한 예시는 오히려 LLM을 혼란스럽게 만들 수 있습니다. 둘째, 예시들은 다양한 경우의 수를 보여주면서도(Diverse), 내용적으로 정확하고(High quality), 문법적으로나 형식적으로 잘 작성되어 있어야(Well written) 합니다. 예시 자체에 작은 오류나 실수가 포함되어 있으면, LLM이 그 오류까지 학습하여 잘못된 결과물을 생성할 수 있습니다. 셋째, 만약 당신이 다양한 종류의 입력에 대해서도 강건하게(Robust) 작동하는 결과물을 생성하고 싶다면, **일반적이지 않거나 예상치 못한 입력(Edge cases)**에 대한 예시도 의도적으로 포함하는 것이 중요합니다. 예를 들어, 위 피자 주문 예시에서 “토핑 없이 그냥 도우만 주세요” 와 같은 특이한 주문에 대한 처리 예시를 보여주는 것이죠. 모델이 이런 엣지 케이스까지 처리할 수 있도록 학습시키는 것이 실제 적용 시의 안정성을 높이는 방법입니다.
결론적으로, 원샷 및 퓨샷 프롬프팅은 LLM에게 원하는 작업의 내용과 결과물의 형식을 명확하게 가르치는 매우 강력하고 효과적인 기법입니다. 제로샷만으로 원하는 결과를 얻기 어렵다면, 반드시 이 기법들을 활용해보시기를 강력하게 추천합니다.
프롬프트를 통해 LLM의 응답을 유도하는 방식은 앞서 살펴본 제로샷, 원샷, 퓨샷 외에도 LLM에게 어떤 종류의 정보와 지시를 제공하는지에 따라 크게 세 가지 유형으로 나누어 볼 수 있습니다: 시스템 프롬프팅, 컨텍스츄얼 프롬프팅, 그리고 롤 프롬프팅입니다. 이 세 가지 기법은 모두 LLM이 텍스트를 생성하는 방식을 안내하는 데 사용되지만, 각각 초점을 맞추는 측면이 조금씩 다릅니다. 이들의 차이점을 이해하고 적절히 조합하여 사용하는 것은 더욱 정교하고 효과적인 프롬프트 엔지니어링을 위해 필수적입니다.
시스템 프롬프팅 (System Prompting): 이것은 LLM에게 **수행해야 할 작업의 전반적인 맥락(Overall Context)과 목적(Purpose)**을 설정해주는 역할을 합니다. 즉, 모델이 어떤 종류의 작업을 해야 하는지에 대한 **‘큰 그림(Big Picture)’**을 정의하는 것입니다. 예를 들어, “당신은 이제부터 프랑스어를 한국어로 번역하는 번역가입니다.” 라거나, “주어진 영화 리뷰를 긍정 또는 부정으로 분류하는 역할을 수행합니다.” 와 같이 모델의 기본적인 역할이나 능력을 설정하는 지시가 시스템 프롬프트에 해당합니다. 또한, 출력 형식을 특정 구조(예: JSON, Markdown)로 제한하거나, 응답 시 반드시 포함해야 할 내용이나 피해야 할 내용을 지정하거나, 특정 프로그래밍 언어와의 호환성을 요구하는 등 작업 수행의 전반적인 규칙이나 제약 조건을 설정하는 데도 사용될 수 있습니다. 안전성 및 유해성 제어를 위해 “당신의 답변은 항상 존중하는 태도를 유지해야 합니다.” 와 같은 지침을 추가하는 것도 시스템 프롬프팅의 일종입니다.
컨텍스츄얼 프롬프팅 (Contextual Prompting): 이것은 현재 진행 중인 **대화나 특정 작업과 직접적으로 관련된 구체적인 세부 정보(Specific Details)나 배경 정보(Background Information)**를 LLM에게 제공하는 방식입니다. 시스템 프롬프트가 큰 그림을 그린다면, 컨텍스츄얼 프롬프트는 그 그림 속의 특정 부분에 대한 상세 정보를 제공하여 모델이 **현재 상황의 미묘한 뉘앙스(Nuances)**를 더 잘 이해하고 그에 맞춰 **응답을 맞춤화(Tailor)**하도록 돕습니다. 예를 들어, 여행 추천을 요청하는 프롬프트에서 “저는 활동적인 것을 좋아하고 예산은 넉넉하지 않습니다.” 와 같은 추가적인 맥락 정보를 제공하거나, 이전 대화 내용을 요약하여 “앞서 우리가 이야기했던 내용을 바탕으로, 다음 질문에 답해주세요.” 와 같이 대화의 연속성을 유지하도록 돕는 것이 컨텍스츄얼 프롬프팅입니다. 이 정보는 **매우 동적(Dynamic)**이며 현재의 작업이나 입력에 따라 계속 변할 수 있다는 특징이 있습니다.
롤 프롬프팅 (Role Prompting): 이것은 LLM에게 특정 캐릭터(Character)나 정체성(Identity)을 부여하여 그 역할을 수행하도록 지시하는 기법입니다. 시스템 프롬프팅이 모델의 '기능적 역할’을 설정한다면, 롤 프롬프팅은 모델의 **‘인격적 역할’**을 부여하는 것에 가깝습니다. 예를 들어, “당신은 친절하고 경험 많은 유치원 선생님입니다. 아이들의 눈높이에 맞춰 이 과학 원리를 설명해주세요.” 라거나, “당신은 냉철하고 분석적인 탐정 셜록 홈즈입니다. 다음 사건의 단서들을 보고 추리 과정을 설명해주십시오.” 와 같이 특정 인물이나 직업, 혹은 성격을 부여하는 것입니다. 이렇게 역할을 부여하면, LLM은 해당 역할에 맞는 **일관된 말투(Tone), 스타일(Style), 어휘(Vocabulary), 그리고 관련된 지식과 행동 패턴(Associated Knowledge and Behavior)**을 바탕으로 응답을 생성하게 됩니다. 이를 통해 우리는 응답에 **특정 개성(Personality)과 전문성(Specificity)**을 더하고, 사용자와의 상호작용을 더욱 흥미롭고 몰입감 있게 만들 수 있습니다.
물론, 이 세 가지 프롬프팅 유형 사이에 명확한 경계가 항상 존재하는 것은 아닙니다. 때로는 상당한 중첩(Overlap)이 있을 수 있습니다. 예를 들어, 특정 역할을 부여하는 프롬프트(롤 프롬프팅)가 동시에 해당 역할 수행에 필요한 배경 정보(컨텍스츄얼 프롬프팅)를 포함할 수도 있고, 시스템 전체의 목적(시스템 프롬프팅)을 함께 정의할 수도 있습니다.
하지만 각각의 프롬프팅 유형이 가지는 주요한 목적과 초점은 약간씩 다릅니다. 시스템 프롬프트는 모델의 근본적인 능력과 전체적인 작업의 틀을 정의하고, 컨텍스츄얼 프롬프트는 즉각적이고 작업 특정적인 정보를 제공하여 응답의 정확성과 관련성을 높이며, 롤 프롬프트는 모델의 출력 스타일과 목소리를 형성하여 특정 개성과 전문성을 부여합니다.
이 세 가지 유형의 프롬프트를 구분하고 이해하는 것은, 우리가 명확한 의도를 가지고 프롬프트를 설계하고, 각 프롬프트 유형이 LLM의 최종 출력에 어떤 영향을 미치는지 분석하며, 필요에 따라 이들을 유연하게 조합하여 사용하는 데 매우 유용한 프레임워크를 제공해 줄 것입니다.
자, 그럼 이제 이 세 가지 프롬프팅 기법들을 실제 예시를 통해 좀 더 자세히 살펴보겠습니다.
먼저 시스템 프롬프팅의 예시입니다. 앞서 보았던 영화 리뷰 분류 예시(표 1)를 약간 수정하여, 분류 결과를 반드시 '대문자(uppercase)'로만 반환하도록 추가적인 지시를 시스템 레벨에서 부여해 보겠습니다. 또한, 결과의 다양성을 조금 더 높이기 위해 Temperature 값을 1로 올리고, 토큰 제한도 약간 늘려보겠습니다.
[표 3 예시: 시스템 프롬프팅을 이용한 출력 형식 제어]
Classify movie reviews as positive, neutral or negative. Only return the label in uppercase.
Review: "Her" is a disturbing study revealing the direction humanity is headed if AI is allowed to keep evolving, unchecked. It's so disturbing I couldn't watch it.
Sentiment:
이전 예시(표 1)에서는 "masterpiece"라는 단어 때문에 POSITIVE로 분류했을 수 있지만, Temperature를 높이고 “It’s so disturbing I couldn’t watch it.” 이라는 부정적인 표현을 고려하여 NEGATIVE로 분류할 수도 있습니다. 여기서 중요한 점은 “Only return the label in uppercase.” 라는 명확한 시스템 지시 덕분에, 모델이 추가적인 설명 없이 오직 대문자 레이블만 반환할 가능성이 높아진다는 것입니다. (물론 Temperature가 1로 높기 때문에 약간의 변동성은 있을 수 있습니다.)
시스템 프롬프트는 이렇게 출력 형식을 특정 요구사항에 맞게 제어하는 데 매우 유용합니다. 예를 들어, 추출된 데이터를 항상 JSON 형식으로 반환하도록 지시할 수도 있습니다. 이는 특히 데이터를 후속 처리해야 하는 실제 애플리케이션 개발 시 매우 유용합니다. JSON 형식을 강제함으로써 모델이 구조화된 데이터를 생성하도록 유도하고, 동시에 엉뚱한 내용이나 환각(Hallucination)을 생성할 가능성을 제한하는 효과도 얻을 수 있습니다.
[표 4 예시: 시스템 프롬프팅을 이용한 JSON 형식 출력]
Classify movie reviews as positive, neutral or negative. Return valid JSON:
Review: "Her" is a disturbing study revealing the direction humanity is headed if AI is allowed to keep evolving, unchecked. It's so disturbing I couldn't watch it.
Schema:
MOVIE:
{
"sentiment": String "POSITIVE" | "NEGATIVE" | "NEUTRAL",
"name": String
}
MOVIE_REVIEWS:
{
"movie_reviews": [MOVIE]
}
JSON Response:
```* **예상 출력:**
```json
{
"movie_reviews": [
{
"sentiment": "NEGATIVE",
"name": "Her"
}
]
}
이처럼 시스템 프롬프트에 명확한 스키마(Schema) 정보와 함께 JSON 형식 출력을 요구하면, 모델은 해당 구조에 맞춰 결과를 생성하려고 노력하게 됩니다. 이는 데이터를 후속 시스템에서 처리하기 매우 용이하게 만들어 줍니다.
다음은 **롤 프롬프팅(Role Prompting)**의 예시입니다. 롤 프롬프팅은 LLM에게 특정 역할이나 페르소나(Persona)를 부여하여 응답의 톤, 스타일, 전문성 등을 조절하는 기법입니다.
[표 5 예시: 롤 프롬프팅을 이용한 여행 가이드 역할 수행]
I want you to act as a travel guide. I will write to you about my location and you will suggest 3 places to visit near me. In some cases, I will also give you the type of places I will visit.
My suggestion: "I am in Amsterdam and I want to visit only museums."
Travel Suggestions:
이 예시에서는 LLM에게 ‘여행 가이드’ 역할을 부여하고, 특정 위치(‘암스테르담’)와 선호하는 장소 유형(‘박물관’)을 제공하여 3가지 맞춤형 추천을 생성하도록 했습니다. 만약 역할을 '지리학 교사’로 바꾸었다면, 아마도 각 박물관의 지리적 위치나 역사적 배경에 대한 설명이 더 강조되는 다른 형태의 응답을 얻게 될 것입니다.
LLM에게 특정 역할을 부여하는 것은 응답의 **톤(Tone), 스타일(Style), 그리고 집중하는 전문성(Focused Expertise)**에 대한 청사진을 제공하여, 최종 출력물의 **품질(Quality), 관련성(Relevance), 그리고 효과성(Effectiveness)**을 향상시키는 데 매우 유용합니다.
롤 프롬프팅은 단순히 역할 이름만 부여하는 것을 넘어, 원하는 응답의 구체적인 스타일이나 톤을 함께 지시할 수도 있습니다. 예를 들어, 위 여행 가이드 프롬프트에 '유머러스하고 영감을 주는 스타일(humorous and inspirational style)'이라는 지시를 추가하면 다음과 같은 응답을 기대할 수 있습니다.
[표 6 예시: 유머러스한 톤과 스타일을 적용한 롤 프롬프팅]
I want you to act as a travel guide. I will write to you about my location and you will suggest 3 places to visit near me in a humorous style.
My suggestion: "I am in Manhattan."
Travel Suggestions:
마지막으로 **컨텍스츄얼 프롬프팅(Contextual Prompting)**의 예시입니다. 이 기법은 현재 작업이나 대화의 맥락에 맞는 구체적인 정보를 제공하여 LLM이 더 정확하고 관련성 높은 응답을 생성하도록 돕습니다.
[표 7 예시: 컨텍스츄얼 프롬프팅을 이용한 블로그 기사 주제 제안]
Context: You are writing for a blog about retro 80's arcade video games.
Suggest 3 topics to write an article about with a few lines of description of what this article should contain.
이 예시에서는 “당신은 80년대 레트로 아케이드 비디오 게임에 대한 블로그를 운영하고 있습니다.” 라는 명확한 **맥락 정보(Context)**를 제공함으로써, LLM이 해당 블로그의 주제와 독자층에 맞는 훨씬 더 구체적이고 관련성 높은 기사 주제들을 제안하도록 유도했습니다. 만약 이 맥락 정보가 없었다면, LLM은 훨씬 더 일반적이거나 관련 없는 게임 주제들을 제안했을 수도 있습니다.
이처럼 시스템, 컨텍스츄얼, 롤 프롬프팅 기법들을 이해하고 상황에 맞게 적절히 조합하여 사용하는 것은 당신의 프롬프트 엔지니어링 능력을 한 단계 더 끌어올리고, LLM과의 상호작용을 더욱 원활하고 효과적으로 만드는 데 핵심적인 역할을 할 것입니다.
때로는 우리가 당면한 특정 문제에만 너무 깊이 파고들다 보면, 더 넓은 맥락이나 근본적인 원리를 놓치게 되어 오히려 최적의 해결책을 찾지 못하는 경우가 있습니다. LLM에게 프롬프트를 작성할 때도 마찬가지입니다. 너무 구체적인 작업 지시만 바로 던지면, LLM은 그 지시에만 국한되어 생각하게 되고, 관련된 중요한 배경지식이나 더 넓은 관점에서 문제를 바라보는 능력을 충분히 활용하지 못할 수 있습니다.
바로 이런 상황에서 유용하게 사용될 수 있는 고급 프롬프팅 기법 중 하나가 ‘스텝백(Step-back)’ 프롬프팅입니다. [참고 8: Step-back Prompting] 이 기법의 핵심 아이디어는 아주 흥미롭습니다. LLM에게 당면한 특정 작업(Specific Task)을 바로 해결하도록 요구하기 전에, 먼저 그 작업과 관련된 좀 더 일반적이고 근본적인 질문(General Question)에 대해 생각하고 답하도록 유도하는 것입니다. 그리고 그 일반적인 질문에 대한 LLM의 답변(관련 배경지식이나 원리)을 후속 프롬프트의 일부로 다시 활용하여, 원래의 특정 작업을 수행하도록 지시하는 방식입니다.
왜 이렇게 한 걸음 물러서는(Step back) 과정이 도움이 될까요? 이는 LLM이 특정 문제에 대한 답을 바로 내놓으려고 시도하기 전에, 먼저 **관련된 상위 개념이나 배경지식, 혹은 문제 해결에 필요한 일반적인 원리들을 스스로 활성화(Activate Relevant Background Knowledge and Reasoning Processes)**하도록 유도하기 때문입니다. 이렇게 더 넓은 맥락과 근본적인 원리를 먼저 고려함으로써, LLM은 더 정확하고, 더 통찰력 있으며, 때로는 더 창의적인 방식으로 특정 문제에 접근하고 해결책을 생성할 수 있게 됩니다. 즉, 스텝백 프롬프팅은 LLM이 단순히 주어진 정보에만 의존하는 것이 아니라, 자신이 학습한 방대한 지식 체계 속에서 관련된 정보를 효과적으로 인출하고 비판적으로 사고하며 새롭게 적용하도록 격려하는 기법이라고 할 수 있습니다.
또한, 스텝백 프롬프팅은 LLM 응답에 나타날 수 있는 편향(Bias)을 완화하는 데도 도움을 줄 수 있습니다. 특정 세부 사항이나 예시에만 집중하다 보면 발생할 수 있는 편향된 판단 대신, 더 일반적이고 보편적인 원리에 초점을 맞추도록 유도함으로써 보다 객관적이고 균형 잡힌 응답을 생성할 가능성을 높여줍니다.
자, 그럼 이 스텝백 프롬프팅이 실제로 어떻게 작동하고 결과를 개선하는지 구체적인 예시를 통해 살펴보겠습니다. 예를 들어, 우리는 LLM에게 "도전적이고 매력적인 1인칭 슈터(FPS) 비디오 게임의 새로운 레벨을 위한 한 문단짜리 스토리라인을 작성해달라"고 요청하고 싶습니다.
먼저, 전통적인 제로샷 프롬프트로 시도해 보겠습니다. Temperature는 1로 설정하여 창의적인 결과물을 유도해 보겠습니다.
[표 8 예시: 전통적인 제로샷 프롬프트를 이용한 게임 스토리라인 생성]
Write a one paragraph storyline for a new level of a first-person shooter video game that is challenging and engaging.
Temperature를 1로 설정했기 때문에 매번 다른 창의적인 스토리라인이 나올 수 있겠지만, 이 결과는 다소 무작위적이고 일반적인(Generic) 느낌을 줄 수 있습니다. 뭔가 특별한 설정이나 독창적인 아이디어가 부족해 보입니다.
자, 이제 스텝백 프롬프팅을 적용해 봅시다. 먼저 LLM에게 좀 더 일반적인 질문을 던져 관련 아이디어를 활성화시킵니다.
[표 9 예시: 스텝백 프롬프팅 (1단계 - 일반적인 질문)]
Based on popular first-person shooter action games, what are 5 fictional key settings that contribute to a challenging and engaging level storyline in a first-person shooter video game?
네, 이 5가지 설정들은 확실히 흥미로운 1인칭 슈터 게임의 배경이 될 수 있을 것 같습니다. 이제, 이 일반적인 질문에 대한 답변(5가지 설정 테마)을 컨텍스트(Context)로 활용하여 원래의 프롬프트를 다시 작성해 보겠습니다.
[표 10 예시: 스텝백 프롬프팅 (2단계 - 컨텍스트를 활용한 특정 작업 요청)]
Context: 5 engaging themes for a first person shooter video game:
1. **Abandoned Military Base**: A sprawling, post-apocalyptic military complex crawling with mutated soldiers and rogue robots, ideal for challenging firearm combat.
2. **Cyberpunk City**: A neon-lit, futuristic urban environment with towering skyscrapers and dense alleyways, featuring cybernetically enhanced enemies and hacking mechanics.
3. **Alien Spaceship**: A vast alien vessel stranded on Earth, with eerie corridors, zero-gravity sections, and extraterrestrial creatures to encounter.
4. **Zombie-Infested Town**: A desolate town overrun by hordes of aggressive zombies, featuring intense close-quarters combat and puzzle-solving to find safe passage.
5. **Underwater Research Facility**: A deep-sea laboratory flooded with water, filled with mutated aquatic creatures, and requiring stealth and underwater exploration skills to survive.
Take one of the themes and write a one paragraph storyline for a new level of a first-person shooter video game that is challenging and engaging.
어떻습니까? 이전의 제로샷 결과와 비교했을 때, 스텝백 프롬프팅을 통해 얻은 이 스토리라인은 훨씬 더 구체적이고(수중 연구 시설이라는 명확한 배경), 흥미로우며(음울한 분위기, 미스터리한 퍼즐, 끔찍한 괴물), 독창적인 느낌을 줍니다. 왜냐하면 LLM이 단순히 '도전적이고 매력적인 스토리라인’이라는 막연한 지시를 받은 것이 아니라, 구체적인 설정 테마(수중 연구 시설)라는 명확한 컨텍스트를 바탕으로 상상력을 발휘했기 때문입니다.
이처럼 스텝백 프롬프팅 기법을 활용하면, LLM이 더 넓은 배경지식과 추론 능력을 동원하여 보다 정확하고, 깊이 있으며, 창의적인 결과물을 생성하도록 효과적으로 유도할 수 있습니다. 복잡한 문제 해결이나 창의적인 아이디어 생성이 필요한 경우, 이 ‘한 걸음 물러서서 생각하기’ 전략을 꼭 한번 활용해보시기를 바랍니다.
대규모 언어 모델(LLM)은 놀라운 언어 생성 능력을 보여주지만, 복잡한 논리적 추론(Reasoning)이 필요한 작업에서는 종종 예상치 못한 오류를 보이거나 설득력 없는 답변을 내놓기도 합니다. 예를 들어, 여러 단계의 계산이 필요한 수학 문제나, 숨겨진 전제를 파악해야 하는 논리 퍼즐 같은 경우, LLM이 정답에 도달하기까지의 중간 과정을 생략하고 바로 최종 답만 제시하려다가 틀리는 경우가 많습니다. 마치 우리가 수학 문제를 풀 때, 머릿속으로만 암산하려다 실수하는 것과 비슷하다고 할 수 있죠.
이러한 LLM의 추론 능력의 한계를 개선하기 위해 고안된 매우 강력하고 효과적인 프롬프팅 기법 중 하나가 바로 ‘사고의 연쇄(Chain of Thought, CoT)’ 프롬프팅입니다. [참고 9: Chain of Thought Prompting] 이 기법의 핵심 아이디어는 아주 간단합니다. LLM에게 최종적인 답변만 내놓으라고 요구하는 대신, 그 답에 도달하기까지의 중간 단계, 즉 ‘생각의 과정’ 또는 '추론 단계(Intermediate Reasoning Steps)'를 명시적으로 함께 생성하도록 유도하는 것입니다. 마치 우리가 복잡한 문제를 풀 때, 단계별로 풀이 과정을 적어가며 생각하는 것처럼, LLM에게도 “단계별로 생각해보자(Let’s think step by step)” 와 같은 지시를 통해 그 생각의 흐름을 명시적으로 보여달라고 요청하는 것이죠.
왜 이렇게 생각하는 과정을 보여달라고 하는 것이 LLM의 추론 능력을 향상시키는 데 도움이 될까요? 그 이유는 LLM이 복잡한 문제를 더 작은 하위 문제들로 나누어(Decomposition) 단계적으로 처리하도록 유도함으로써, 각 단계에 더 집중하고 필요한 계산이나 논리적 연결을 더 정확하게 수행할 가능성이 높아지기 때문입니다. 또한, LLM이 생성한 **중간 추론 과정을 우리가 직접 확인할 수 있기 때문에, 최종 답변이 어떻게 도출되었는지 그 과정을 이해하고(Interpretability), 만약 결과가 틀렸다면 어느 단계에서 오류가 발생했는지 파악하고 디버깅(Debugging)**하는 것이 훨씬 용이해집니다.
CoT 프롬프팅은 특히 별도의 모델 미세 조정(Fine-tuning) 없이도 기존의 강력한 LLM(Off-the-shelf LLMs)에 바로 적용하여 상당한 성능 향상을 이끌어낼 수 있다는 점에서 매우 실용적이고 각광받고 있습니다. 또한, 앞서 살펴본 퓨샷(Few-shot) 프롬프팅과 결합하면 더욱 강력한 효과를 발휘할 수 있습니다. 즉, 문제와 함께 단계별 풀이 과정이 포함된 예시들을 몇 개 보여줌으로써, LLM이 더 복잡한 새로운 문제에 대해서도 유사한 방식으로 추론 과정을 생성하고 정답에 도달하도록 유도할 수 있습니다. (제로샷으로 CoT를 유도하는 것도 가능하지만, 더 복잡한 문제에서는 퓨샷 CoT가 더 효과적인 경향이 있습니다.)
물론 CoT 프롬프팅에도 단점은 있습니다. 가장 명백한 단점은 LLM 응답에 최종 답변뿐만 아니라 중간 추론 과정까지 포함되기 때문에, 전체 출력 토큰의 길이가 길어진다는 것입니다. 이는 곧 **더 많은 계산 비용(Money)과 더 긴 응답 생성 시간(Time)**을 의미합니다. 따라서 비용과 속도가 매우 중요한 애플리케이션에서는 CoT 사용 여부를 신중하게 결정해야 할 수도 있습니다.
자, 그럼 CoT 프롬프팅이 실제로 어떻게 작동하는지 간단한 수학 문제 예시를 통해 살펴보겠습니다. 먼저, CoT를 사용하지 않는 일반적인 프롬프트를 시도해 봅시다.
[표 11 예시: CoT 없이 수학 문제 풀기 시도]
When I was 3 years old, my partner was 3 times my age. Now, I am 20 years old. How old is my partner?
이런! LLM이 간단한 계산 실수(아마도 3 * (20+1?) 또는 다른 잘못된 논리)를 해서 완전히 틀린 답을 내놓았습니다. LLM이 종종 수학적 추론에 어려움을 겪는다는 것을 보여주는 예시입니다.
이제 CoT 프롬프팅을 적용하여 LLM에게 단계별로 생각하도록 유도해 보겠습니다. (여기서는 제로샷 CoT를 사용합니다.)
[표 12 예시: 제로샷 CoT 프롬프팅을 이용한 수학 문제 풀이]
프롬프트:
When I was 3 years old, my partner was 3 times my age. Now, I am 20 years old. How old is my partner? Let's think step by step.
출력 예시:
So, my partner is 26 years old.
자, 보십시오! “Let’s think step by step.” 라는 간단한 지시 하나를 추가했을 뿐인데, LLM은 문제를 작은 단계들로 나누어 논리적으로 추론하고, 각 단계의 계산을 수행하여 마침내 **정확한 답(26세)**에 도달했습니다. 또한, 우리는 그 중간 추론 과정을 명확하게 확인할 수 있어서 LLM이 어떤 논리로 답을 도출했는지 쉽게 이해할 수 있습니다. (흥미롭게도 LLM은 '나이 차이는 일정하다’는 더 간단한 방법 대신, 각자의 나이 증가분을 계산하는 방식으로 풀었네요. 이것은 CoT가 항상 인간의 가장 효율적인 사고방식을 따르는 것은 아님을 보여줍니다.)
만약 문제가 더 복잡하다면, 싱글샷(Single-shot) 또는 퓨샷(Few-shot) CoT 프롬프팅을 사용하여 더 나은 결과를 얻을 수 있습니다. 즉, 비슷한 유형의 문제와 그에 대한 단계별 풀이 과정 예시를 프롬프트에 함께 제공하는 것입니다.
[표 13 예시: 싱글샷 CoT 프롬프팅 예시]
Q: When my brother was 2 years old, I was double his age. Now I am 40 years old. How old is my brother? Let's think step by step.
A: When my brother was 2 years, I was 2 * 2 = 4 years old. That's an age difference of 2 years and I am older. Now I am 40 years old, so my brother is 40 - 2 = 38 years old. The answer is 38.
Q: When I was 3 years old, my partner was 3 times my age. Now, I am 20 years old. How old is my partner? Let's think step by step.
A:
이 예시에서는 첫 번째 Q&A 쌍을 통해 LLM에게 문제 상황을 분석하고, 나이 차이를 계산한 다음, 현재 나이에 그 차이를 적용하여 답을 구하는 추론 패턴을 보여주었습니다. 그 결과, 두 번째 질문에 대해서도 동일한 패턴(나이 차이 계산)을 사용하여 정확한 답을 더 효율적으로 도출하는 것을 볼 수 있습니다.
CoT 프롬프팅은 수학 문제 풀이뿐만 아니라, 코드 생성(요구사항을 단계별로 나누어 코드로 변환), 상식 추론, 복잡한 지시 따르기, 합성 데이터 생성 등 다양한 종류의 작업에서 LLM의 성능을 향상시키는 데 매우 유용하게 활용될 수 있습니다. 일반적으로, 당신이 어떤 문제를 해결하는 과정을 말로 설명할 수 있다면(Talking through the steps), 그 작업은 CoT 프롬프팅의 좋은 후보가 될 수 있습니다. 만약 당신이 LLM에게 맡기려는 작업이 단순한 정보 검색이나 생성을 넘어 복잡한 추론이나 단계적인 처리를 요구한다면, 반드시 이 강력한 CoT 기법을 적용해보시기를 강력하게 추천합니다.
앞서 살펴본 사고의 연쇄(Chain of Thought, CoT) 프롬프팅은 LLM의 추론 능력을 향상시키는 데 매우 효과적인 기법입니다. 하지만 CoT 역시 한계점을 가지고 있습니다. CoT는 기본적으로 ‘탐욕적 디코딩(Greedy Decoding)’ 전략, 즉 각 단계에서 가장 확률 높은 하나의 추론 경로만을 따라가는 경향이 있습니다. 이 방식은 때때로 잘못된 추론 경로에 빠지거나 최적의 해결책을 놓치는 결과를 낳을 수 있습니다. 마치 우리가 문제를 풀 때, 한 가지 생각에만 너무 깊이 빠져서 다른 가능성을 고려하지 못하는 경우와 비슷합니다.
이러한 CoT의 한계를 보완하고 LLM 응답의 정확성과 강건성(Robustness)을 더욱 향상시키기 위해 제안된 기법이 바로 **‘자기 일관성(Self-consistency)’**입니다. [참고 11: Self Consistency] 이 기법의 핵심 아이디어는 아주 흥미롭습니다. 단 하나의 추론 경로만 생성하는 대신, 동일한 프롬프트에 대해 LLM이 여러 개의 다양한 추론 경로(Diverse Reasoning Paths)를 생성하도록 유도한 다음, 최종적으로 그 여러 결과들 중에서 가장 일관되게(즉, 가장 자주) 나타나는 답변을 ‘다수결(Majority Voting)’ 방식으로 선택하는 것입니다.
왜 이런 방식이 효과가 있을까요? 그 이유는 복잡한 문제에는 정답에 도달하는 여러 가지 다른 경로가 존재할 수 있으며, LLM이 비록 때때로 잘못된 추론 경로를 따르더라도 올바른 최종 답에 도달하는 경우가 많기 때문입니다. 자기 일관성은 이러한 점을 활용하여, 다양한 추론 과정을 샘플링하고 그중에서 가장 빈번하게 등장하는 답변을 최종 답으로 채택함으로써, 마치 여러 명의 전문가에게 자문을 구하고 가장 일치하는 의견을 따르는 것처럼, 개별 추론 과정의 오류 가능성을 줄이고 최종 답변의 신뢰도를 높이는 효과를 가져옵니다. 즉, 자기 일관성은 정답 자체에 대한 유사 확률(Pseudo-probability Likelihood), 즉 '이 답이 맞을 가능성이 얼마나 높은가’에 대한 간접적인 추정치를 제공한다고 볼 수 있습니다.
자기 일관성을 구현하는 단계는 다음과 같습니다.
물론, 자기 일관성 기법에도 명백한 단점이 존재합니다. 가장 큰 단점은 동일한 프롬프트를 여러 번 실행해야 하므로, 계산 비용(Computational Cost)이 훨씬 많이 들고 전체 응답 생성 시간도 길어진다는 것입니다. 따라서 실시간 응답이 매우 중요하거나 비용 제약이 심한 경우에는 적용하기 어려울 수 있습니다. 하지만 **답변의 정확성과 신뢰도가 매우 중요한 작업(예: 복잡한 수학 문제, 과학적 추론, 법률 문서 분석 등)**에서는 이러한 비용을 감수하고서라도 자기 일관성 기법을 사용하는 것이 충분히 가치가 있을 수 있습니다.
자, 그럼 자기 일관성이 실제로 어떻게 작동하는지 간단한 이메일 분류 예시를 통해 살펴보겠습니다. 우리는 LLM에게 주어진 이메일이 중요한 메일(IMPORTANT)인지 아닌지(NOT IMPORTANT)를 분류하도록 요청할 것입니다. 이때 제로샷 CoT 프롬프트를 사용하고, LLM을 속일 수 있는 몇 가지 요소(친근한 말투, 버그 리포트지만 심각하지 않다는 뉘앙스, 보낸 사람의 장난스러운 이름 등)를 포함시켜 보겠습니다.
[표 14 예시: 자기 일관성 프롬프팅을 이용한 이메일 분류 (여러 번 실행 필요)]
프롬프트 (동일한 프롬프트를 여러 번 실행):
EMAIL:
... (이메일 내용 생략 - 표 14 참조) ...
Cheers,
Harry the Hacker.
Classify the above email as IMPORTANT or NOT IMPORTANT. Let's think step by step and explain why.
출력 예시 (여러 번 실행 시 나타날 수 있는 다양한 결과):
이 예시처럼, 동일한 프롬프트에 대해서도 (특히 Temperature가 0보다 클 경우) LLM은 서로 다른 추론 과정을 거쳐 다른 결론에 도달할 수 있습니다. 만약 우리가 이 프롬프트를 10번 실행하여 'IMPORTANT’라는 결론이 7번, 'NOT IMPORTANT’라는 결론이 3번 나왔다고 가정해 봅시다. 자기 일관성 기법에 따르면, 우리는 가장 빈번하게 나타난 'IMPORTANT’를 최종 분류 결과로 선택하게 됩니다. 이는 단 한 번의 CoT 결과('NOT IMPORTANT’가 나올 수도 있음)에 의존하는 것보다 훨씬 더 안정적이고 신뢰도 높은 결론을 제공할 가능성이 높습니다.
이 예시는 자기 일관성 프롬프팅이 어떻게 여러 관점을 고려하고 가장 일관된 답변을 선택함으로써 LLM 응답의 정확성을 향상시킬 수 있는지를 잘 보여줍니다. 답변의 신뢰도가 매우 중요한 작업이라면, 비용 증가를 감수하고서라도 이 자기 일관성 기법을 적용하는 것을 고려해볼 가치가 충분합니다.
우리가 앞서 살펴본 사고의 연쇄(Chain of Thought, CoT)와 자기 일관성(Self-consistency) 기법은 LLM의 추론 능력을 크게 향상시켰습니다. 하지만 이들 기법은 여전히 선형적인(Linear) 사고 과정에 기반하거나, 여러 경로를 탐색하더라도 최종적으로는 하나의 결론에 도달하는 방식입니다. 그러나 현실의 많은 복잡한 문제들, 특히 정답이 하나로 정해져 있지 않고 다양한 가능성을 탐색(Exploration)해야 하는 문제들은 단순히 하나의 길만 따라가거나 여러 길 중 하나만 선택하는 방식으로는 최적의 해답을 찾기 어려울 수 있습니다.
이러한 한계를 극복하기 위해 제안된 더욱 발전된 프롬프팅 기법이 바로 **‘사고의 트리(Tree of Thoughts, ToT)’**입니다. [참고 12: Tree of Thoughts] 이름에서 알 수 있듯이, 이 기법은 LLM이 문제를 해결하기 위해 단 하나의 선형적인 생각의 사슬을 따라가는 것이 아니라, 마치 나무가 가지를 뻗어나가듯 여러 개의 서로 다른 생각의 경로(Reasoning Paths)를 동시에 탐색하고 확장해나가는 방식을 취합니다. 즉, CoT의 개념을 '트리 구조’로 일반화(Generalize)시킨 것이라고 할 수 있습니다.
ToT 접근 방식은 어떻게 작동할까요? 기본적으로 ToT는 문제 해결 과정을 여러 단계로 나누고, 각 단계에서 여러 개의 가능한 다음 생각(Thought)이나 중간 단계 해결책들을 생성합니다. 그리고 생성된 각 생각들에 대해 **평가(Evaluation)**를 수행하고, 그 평가 결과를 바탕으로 가장 유망해 보이는 생각들을 선택하여 다음 단계로 확장해나갑니다. 이러한 ‘생성(Generate) - 평가(Evaluate) - 선택(Select)’ 과정을 트리 형태로 반복적으로 수행하면서 최적의 해결책을 찾아나가는 것입니다. 마치 우리가 복잡한 미로나 게임을 풀 때, 여러 갈래길 앞에서 각각의 길을 조금씩 탐색해보고 가장 좋아 보이는 길을 선택하여 더 나아가는 과정을 반복하는 것과 유사합니다.
[그림 1 시각화 설명]
이러한 트리 탐색 방식은 ToT를 특히 탐색적인 사고가 요구되는 복잡한 문제 해결에 매우 적합하게 만듭니다. 예를 들어, 창의적인 글쓰기(다양한 플롯 아이디어 탐색), 복잡한 계획 수립(여러 대안 경로 고려), 혹은 과학적 발견(다양한 가설 탐색)과 같은 작업에서 ToT는 단일 경로만 탐색하는 CoT보다 훨씬 더 강력한 성능을 보여줄 수 있습니다. 모델은 트리 구조 내의 여러 노드(Node, 각 생각이나 중간 단계)에서 다양한 추론 경로를 분기(Branching out)하며 탐색할 수 있기 때문에, 훨씬 더 넓은 해결 공간(Solution Space)을 효과적으로 탐색하고 잠재적으로 더 나은 최종 결과에 도달할 가능성이 높아집니다.
물론 ToT 역시 장점만 있는 것은 아닙니다. 여러 추론 경로를 동시에 생성하고 평가해야 하므로 CoT보다 훨씬 더 많은 계산 자원과 시간이 소요될 수 있으며, 각 단계에서 생성된 생각들을 효과적으로 평가하고 유망한 경로를 선택하는 **정교한 평가 및 탐색 전략(예: 휴리스틱, 강화 학습 등)**이 필요하다는 단점이 있습니다.
하지만 복잡성 높은 문제 해결 능력의 한계를 한 단계 더 끌어올릴 수 있는 잠재력을 가진 매우 흥미롭고 중요한 프롬프팅 패러다임이라고 할 수 있습니다. LLM의 추론 능력을 극한까지 활용해야 하는 도전적인 작업에 직면했다면, 이 ToT 접근 방식을 연구하고 적용해보는 것을 고려해볼 가치가 있습니다.
지금까지 살펴본 프롬프팅 기법들은 주로 LLM 내부의 지식과 추론 능력에 의존하여 문제를 해결하는 방식이었습니다. 하지만 현실 세계의 많은 문제들은 LLM이 학습한 데이터에 포함되지 않은 최신 정보를 필요로 하거나, 외부 도구(External Tools)를 사용하여 특정 계산을 수행하거나 정보를 가져와야 해결할 수 있는 경우가 많습니다. 예를 들어, “오늘 서울의 날씨는 어떤가요?” 라는 질문에 답하려면 LLM은 외부 날씨 정보 API에 접근해야 하고, “567 곱하기 1234는 얼마인가요?” 라는 질문에 정확히 답하려면 계산기 도구를 사용해야 할 수 있습니다.
이러한 LLM의 한계를 극복하고, 자연어 추론 능력과 외부 도구 사용 능력을 효과적으로 결합하여 복잡한 실제 문제를 해결하도록 설계된 강력한 프롬프팅 패러다임이 바로 **‘리액트(ReAct: Reason and Act)’**입니다. [참고 13: ReAct] 이 기법은 LLM이 단순히 생각만 하는 것을 넘어, 필요에 따라 외부 세계와 상호작용하는 '행동(Act)'을 취하고, 그 행동의 결과를 바탕으로 다시 '생각(Reason)'을 업데이트하여 다음 행동을 결정하는 과정을 반복하도록 합니다. 이는 마치 우리 인간이 실제 세상에서 문제를 해결하는 방식, 즉 말로 생각하고(Reason Verbally), 정보를 얻거나 상황을 바꾸기 위해 행동하고(Take Actions), 그 결과를 보고 다시 생각하여 다음 행동을 결정하는 과정을 모방한 것이라고 할 수 있습니다.
ReAct 프롬프팅은 어떻게 작동할까요? 기본적으로 **‘사고-행동 루프(Thought-Action Loop)’**를 따릅니다.
Action: SearchAPI("오늘 서울 날씨")
)Observation: "서울 현재 기온 25도, 맑음"
)ReAct는 LLM이 단순히 저장된 지식에만 의존하는 것에서 벗어나, 필요한 정보를 실시간으로 외부에서 가져오거나(예: 최신 뉴스 검색), 복잡한 계산을 외부 도구에 맡기거나(예: 계산기 사용), 심지어는 코드를 실행하여 그 결과를 활용하는 등 훨씬 더 강력하고 유연한 문제 해결 능력을 갖추도록 해줍니다. 이는 LLM을 기반으로 자율적으로 작업을 수행하는 ‘에이전트(Agent)’ 모델을 구축하는 데 있어서 매우 중요한 첫걸음이자 핵심적인 기술이라고 할 수 있습니다.
ReAct를 실제로 구현하기 위해서는 약간의 코딩 작업이 필요합니다. 왜냐하면 LLM이 생성한 ‘행동’ 지시(예: 특정 API 호출)를 실제로 실행하고 그 결과를 다시 LLM에게 전달해주는 중간 매개체가 필요하기 때문입니다. 다행히도 LangChain과 같은 프레임워크를 사용하면 이러한 ReAct 에이전트를 비교적 쉽게 구현할 수 있습니다.
예를 들어, “메탈리카 밴드 멤버들의 자녀는 총 몇 명인가?” 라는 질문에 답하기 위해 ReAct 에이전트를 파이썬과 LangChain, 그리고 Google 검색 API(SerpAPI)를 사용하여 구현하는 예시를 생각해 볼 수 있습니다. (자세한 코드는 문서의 Snippet 1 참조)
[ReAct 에이전트 작동 예시 (요약)]
SearchAPI("메탈리카 멤버")
SearchAPI("제임스 헷필드 자녀 수")
SearchAPI("라스 울리히 자녀 수")
SearchAPI("커크 해밋 자녀 수")
SearchAPI("로버트 트루히요 자녀 수")
이 예시처럼, ReAct 에이전트는 자신의 내부 지식만으로는 답할 수 없는 질문에 대해, 필요한 정보를 얻기 위한 일련의 '행동(검색)'을 계획하고 실행하며, 그 결과를 바탕으로 최종 답에 도달하는 과정을 보여줍니다.
물론 ReAct 프롬프팅을 실제로 구현하고 효과적으로 사용하기 위해서는 몇 가지 고려사항이 있습니다. 이전 프롬프트와 응답 내용을 계속해서 다음 프롬프트에 포함시켜야 하므로 **컨텍스트 관리(Context Management)**가 중요하며, 때로는 LLM이 생성한 불필요한 내용을 적절히 잘라내는(Trimming) 작업도 필요할 수 있습니다. 또한, LLM이 특정 도구를 올바르게 사용하고 그 결과를 제대로 해석하도록 **적절한 예시나 지침(Instructions)**을 제공하는 것도 중요합니다.
하지만 이러한 노력에도 불구하고, ReAct는 LLM이 가진 정보의 한계를 극복하고 실제 세상과 상호작용하며 복잡한 문제를 해결하는 능력을 비약적으로 향상시킬 수 있는 매우 강력하고 유망한 패러다임임에 틀림없습니다. LLM을 단순한 텍스트 생성기를 넘어 능동적인 문제 해결 에이전트로 활용하고 싶다면, 이 ReAct 기법을 반드시 주목하고 깊이 있게 탐구해보시기를 바랍니다.
지금까지 우리는 효과적인 프롬프트를 작성하기 위한 다양한 기법들을 살펴보았습니다. 하지만 이 과정들을 따라오면서 당신은 아마 이런 생각을 했을지도 모릅니다. “와… 좋은 프롬프트를 하나 만드는 게 생각보다 정말 복잡하고 까다롭구나. 최적의 문구, 구조, 예시, 설정값을 찾기 위해 이렇게 많은 실험과 반복 작업을 해야 한다니, 너무 비효율적인 거 아니야?” 라고 말입니다.
네, 맞습니다. 인정해야 합니다. 수동으로 최적의 프롬프트를 찾아가는 과정은 때로는 정말 지루하고 많은 시간과 노력을 요구하는 '삽질’처럼 느껴질 수 있습니다. 특히 복잡한 작업이나 새로운 모델에 대해서는 더욱 그렇습니다.
그렇다면 혹시 이 ‘프롬프트 작성’ 과정 자체를 자동화할 수는 없을까요? 마치 우리가 LLM에게 글쓰기나 코딩 같은 작업을 시키듯이, LLM에게 “내가 원하는 작업을 가장 잘 수행할 수 있는 최적의 프롬프트를 네가 한번 만들어봐!” 라고 시킬 수는 없을까요?
놀랍게도, 가능합니다! 이러한 접근 방식을 **‘자동 프롬프트 엔지니어링(Automatic Prompt Engineering, APE)’**이라고 부릅니다. [참고 15: Automatic Prompt Engineering] 이 방법은 인간이 직접 최적의 프롬프트를 찾는 데 드는 수고를 덜어줄 뿐만 아니라, 때로는 인간이 생각하지 못했던 더 효과적인 프롬프트를 발견하여 모델의 성능을 향상시키는 놀라운 결과를 보여주기도 합니다.
APE는 기본적으로 다음과 같은 과정을 통해 작동합니다.
자, 그럼 APE가 실제로 어떻게 활용될 수 있는지 간단한 예시를 통해 살펴보겠습니다. 예를 들어, 우리가 특정 밴드의 상품(Merchandise) 티셔츠를 판매하는 웹사이트를 위한 챗봇을 개발하고 있다고 가정해 봅시다. 우리는 고객들이 티셔츠를 주문할 때 사용할 수 있는 다양한 자연스러운 문장 표현들을 학습 데이터로 확보하고 싶습니다. 이때 APE를 활용하여 다양한 주문 문장 프롬프트 후보들을 생성해 볼 수 있습니다.
[표 15 예시: APE를 이용한 주문 프롬프트 변형 생성]
We have a band merchandise t-shirt webshop, and to train a chatbot we need various ways to order: "One Metallica t-shirt size S". Generate 10 variants, with the same semantics but keep the same meaning.
이렇게 LLM을 활용하여 다양한 표현 방식의 프롬프트 후보들을 자동으로 생성한 다음, 이 후보들을 평가하여 (예: 실제 사용자들이 얼마나 자연스럽게 느끼는지 평가하거나, 챗봇 시스템에서 얼마나 잘 처리되는지 평가) 가장 효과적인 프롬프트를 선별하여 챗봇 학습이나 실제 적용에 사용할 수 있습니다.
APE는 아직 발전 초기 단계에 있는 기술이지만, 프롬프트 엔지니어링 과정을 훨씬 더 효율적이고 체계적으로 만들어 줄 수 있는 엄청난 잠재력을 가지고 있습니다. 특히 대규모의 프롬프트를 관리하거나, 다양한 작업에 대해 최적의 프롬프트를 빠르게 찾아야 하는 경우에 매우 유용할 수 있습니다. 프롬프트 작성의 어려움에 지쳤다면, 이제 LLM에게 프롬프트 작성을 맡겨보는 것은 어떨까요?
자, 이제 프롬프트 엔지니어링의 세계를 좀 더 깊이 탐험하며, 대규모 언어 모델(LLM)이 가진 또 다른 놀라운 능력, 바로 '코드(Code)'와 관련된 작업 수행 능력에 대해 이야기해 볼 시간입니다. 구글의 Gemini와 같은 최신 LLM들은 주로 텍스트 기반의 프롬프트를 처리하는 데 초점을 맞추고 있지만, 그 능력은 단순히 일반적인 글쓰기나 정보 검색에만 국한되지 않습니다. 놀랍게도 이 모델들은 코드를 이해하고, 생성하며, 심지어는 분석하고 디버깅하는 능력까지 갖추고 있습니다!
이는 개발자들에게는 정말이지 혁명적인 변화를 의미합니다. 반복적이고 시간 소모적인 코딩 작업을 LLM의 도움을 받아 자동화하거나, 복잡하고 낯선 코드의 의미를 빠르게 파악하거나, 혹은 내가 작성한 코드의 잠재적인 오류를 찾아내는 등 개발 생산성을 획기적으로 향상시킬 수 있는 새로운 가능성이 열린 것이죠.
이번 섹션에서는 LLM을 당신의 똑똑한 코딩 파트너로 만들기 위해, 코드와 관련된 다양한 작업(코드 작성, 설명, 번역, 디버깅 및 검토)에 프롬프트를 어떻게 효과적으로 활용할 수 있는지 구체적인 예시와 함께 살펴보겠습니다. Vertex AI Studio와 같은 도구를 사용하여 직접 테스트해 볼 수 있는 실용적인 예제들을 통해, LLM의 코드 처리 능력을 최대한으로 끌어내는 방법을 알아봅시다.
프로그래밍을 하다 보면, 머릿속에 있는 아이디어나 필요한 기능을 실제 코드로 구현하는 과정이 때로는 상당히 번거롭고 시간이 많이 걸리는 작업일 수 있습니다. 특히 특정 프로그래밍 언어에 익숙하지 않거나, 반복적인 보일러플레이트(Boilerplate) 코드를 작성해야 할 때는 더욱 그렇죠. 바로 이럴 때, LLM이 당신의 든든한 개발 조수 역할을 해줄 수 있습니다!
Gemini와 같은 LLM은 당신이 자연어로 원하는 기능이나 로직을 설명하면, 그것을 당신이 선택한 프로그래밍 언어(Python, JavaScript, Java, C++, Bash 등 거의 모든 주요 언어 지원)의 실제 코드로 변환해주는 놀라운 능력을 가지고 있습니다. 이는 개발 과정을 훨씬 더 빠르고 효율적으로 만들어 줄 수 있습니다.
예를 들어, 당신의 컴퓨터에 있는 특정 폴더 안의 수백 개의 파일 이름을 일괄적으로 변경해야 하는 상황을 상상해 봅시다. 각 파일 이름 앞에 “draft_” 라는 접두사를 붙여야 한다고 가정해 보죠. 물론 이 작업을 수작업으로 하나하나 처리할 수도 있겠지만, 파일 수가 많다면 엄청난 시간 낭비일 것입니다. Bash 스크립트나 Python 스크립트를 직접 작성하여 이 작업을 자동화할 수도 있겠지만, 해당 스크립트 언어에 익숙하지 않거나 당장 코드를 작성할 시간이 부족할 수도 있습니다.
바로 이때, 우리는 LLM에게 “Bash 스크립트를 하나 짜줘. 이 스크립트는 사용자에게 폴더 이름을 입력받아서, 그 폴더 안에 있는 모든 파일 이름 앞에 'draft_'를 붙여서 파일명을 변경해주는 기능을 해야 해.” 라고 자연어로 요청할 수 있습니다.
[표 16 예시: Bash 스크립트 생성을 위한 프롬프트]
Write a code snippet in Bash, which asks for a folder name. Then it takes the contents of the folder and renames all the files inside by prepending the name draft to the file name.
#!/bin/bash
# Ask for the folder name
echo "Enter the folder name: "
read folder_name
# Check if the folder exists
if [ ! -d "$folder_name" ]; then
echo "Folder does not exist."
exit 1
fi
# Get the list of files in the folder
# Use find for better handling of filenames with spaces or special characters
find "$folder_name" -maxdepth 1 -type f | while read -r file; do
# Get the base name of the file
base_name=$(basename "$file")
# Get the directory name
dir_name=$(dirname "$file")
# Create the new file name by prepending "draft_"
new_file_name="draft_$base_name"
# Construct the full path for the new file name
new_file_path="$dir_name/$new_file_name"
# Rename the file using mv
# Add error handling for the move command
if mv "$file" "$new_file_path"; then
echo "Renamed '$base_name' to '$new_file_name'"
else
echo "Error renaming '$base_name'" >&2
fi
done
echo "Files renamed successfully."
(참고: 위 출력 코드는 원문(표 16)의 코드를 일부 개선하고 오류 처리 및 주석을 추가한 예시입니다. LLM은 프롬프트에 따라 다양한 스타일과 상세 수준의 코드를 생성할 수 있습니다.)어떤가요? 꽤 괜찮은 코드가 생성되지 않았습니까? 심지어 주석까지 달아서 코드의 각 부분이 어떤 역할을 하는지 설명해주고 있습니다! 이렇게 LLM을 활용하면, 특정 프로그래밍 언어에 대한 깊은 지식이 없더라도 원하는 기능을 수행하는 코드를 빠르게 얻을 수 있습니다. 이는 학습 시간을 단축하고, 개발 생산성을 크게 향상시키는 데 엄청난 도움이 될 수 있습니다.
하지만 여기서 매우 중요한 주의사항이 있습니다! LLM이 생성한 코드가 항상 완벽하거나 버그가 없는 것은 결코 아닙니다. LLM은 본질적으로 학습 데이터에 기반하여 가장 ‘그럴듯한’ 코드를 생성하는 예측 모델이지, 코드의 논리적 정확성이나 실행 가능성을 완벽하게 이해하고 보장하는 '컴파일러’나 '테스터’가 아닙니다. 때로는 학습 데이터에 포함된 오류나 잘못된 패턴을 그대로 반복하거나, 겉보기에는 그럴듯하지만 실제로는 작동하지 않거나 예상치 못한 부작용(Side effect)을 일으키는 코드를 생성할 수도 있습니다.
따라서 LLM이 생성해 준 코드는 절대로! 그대로 복사해서 사용해서는 안 됩니다. 반드시 코드를 주의 깊게 읽고 이해하려는 노력을 해야 하며, 가능하다면 작은 단위로 나누어 직접 실행해보고 테스트하는 과정을 거쳐야 합니다. LLM은 당신의 코딩 작업을 도와주는 매우 유능한 '조수’가 될 수 있지만, 최종적인 책임은 언제나 코드를 사용하는 '당신’에게 있다는 사실을 명심해야 합니다.
위 예시에서 생성된 Bash 스크립트도 실제로 작동하는지 테스트해봐야겠죠? 문서에서는 테스트 폴더를 만들고 그 안에 몇 개의 파일을 넣은 다음, 생성된 스크립트(rename_files.sh
)를 실행하여 파일 이름이 draft_filename.txt
형태로 잘 변경되는지 확인하는 과정을 거쳤고, 성공적으로 작동했음을 보여줍니다. 이처럼 LLM이 제공한 솔루션을 검증하는 과정은 필수적입니다.
개발자로 일하다 보면, 내가 직접 작성하지 않은 다른 사람의 코드를 읽고 이해해야 하는 경우가 정말 많습니다. 팀 동료가 작성한 코드, 오픈 소스 라이브러리의 코드, 혹은 인터넷에서 찾은 예제 코드 등… 때로는 주석도 부족하고 변수 이름도 모호하며 로직도 복잡하게 얽혀 있어서, 그 코드가 도대체 무슨 일을 하는지, 왜 그렇게 작성되었는지 파악하는 데 상당한 시간과 정신적 에너지를 소모하게 됩니다.
바로 이런 답답한 상황에서도 LLM이 당신의 든든한 지원군이 되어줄 수 있습니다! Gemini와 같은 LLM은 복잡하거나 낯선 코드 조각을 입력받아, 그 코드가 수행하는 기능, 각 부분의 역할, 그리고 전체적인 작동 방식을 자연어로 알기 쉽게 설명해주는 놀라운 능력을 가지고 있습니다. 이는 마치 경험 많은 선배 개발자가 옆에서 코드 리뷰를 해주며 설명해주는 것과 같은 효과를 줄 수 있어서, 코드 분석 시간을 단축하고 다른 사람의 작업물을 더 빠르고 깊이 있게 이해하는 데 큰 도움을 받을 수 있습니다.
예를 들어, 앞서 표 16에서 LLM이 생성했던 Bash 스크립트에서 주석을 모두 제거한 코드를 LLM에게 주고, 이 코드가 어떤 작업을 수행하는지 설명해달라고 요청해 봅시다.
[표 17 예시: Bash 코드 설명을 위한 프롬프트]
Explain to me the below Bash code:
```bash
#!/bin/bash
echo "Enter the folder name: "
read folder_name
if [ ! -d "$folder_name" ]; then
echo "Folder does not exist."
exit 1
fi
# Use find for better handling
find "$folder_name" -maxdepth 1 -type f | while read -r file; do
base_name=$(basename "$file")
dir_name=$(dirname "$file")
new_file_name="draft_$base_name"
new_file_path="$dir_name/$new_file_name"
if mv "$file" "$new_file_path"; then
echo "Renamed '$base_name' to '$new_file_name'"
else
echo "Error renaming '$base_name'" >&2
fi
done
echo "Files renamed successfully."
echo
와 read
명령어를 사용하여 사용자에게 폴더 이름을 입력하도록 요청하고, 입력된 이름을 folder_name
변수에 저장합니다.if [ ! -d "$folder_name" ]
조건을 사용하여 사용자가 입력한 이름의 폴더가 실제로 존재하는지 확인합니다. 만약 폴더가 존재하지 않으면, “Folder does not exist.” 라는 오류 메시지를 출력하고 스크립트를 종료합니다 (exit code 1).find "$folder_name" -maxdepth 1 -type f
명령어를 사용하여 지정된 폴더 바로 아래에 있는 모든 일반 파일들의 목록을 찾습니다. (-maxdepth 1
은 하위 폴더는 검색하지 않도록 하며, -type f
는 파일만 대상으로 합니다.)while read -r file
루프를 사용하여 find
명령으로 찾은 각 파일 경로를 하나씩 처리합니다.
basename "$file"
: 파일 경로에서 순수한 파일 이름만 추출합니다.dirname "$file"
: 파일 경로에서 디렉토리 경로만 추출합니다.new_file_name="draft_$base_name"
: 원래 파일 이름 앞에 “draft_” 접두사를 붙여 새로운 파일 이름을 생성합니다.new_file_path="$dir_name/$new_file_name"
: 새로운 파일 이름을 포함한 전체 경로를 만듭니다.mv "$file" "$new_file_path"
: mv
명령어를 사용하여 원래 파일을 새로운 이름과 경로로 이동(이름 변경)합니다. 이름 변경 성공 여부에 따라 적절한 메시지를 출력합니다.어떤가요? 마치 숙련된 개발자가 코드를 한 줄 한 줄 따라가며 설명해주듯이, LLM이 코드의 각 부분이 어떤 기능을 하는지, 어떤 명령어들이 사용되었는지, 그리고 전체적으로 어떤 흐름으로 작동하는지를 매우 명확하고 상세하게 설명해주고 있습니다. 이렇게 LLM의 코드 설명 능력을 활용하면, 복잡하고 낯선 코드에 대한 이해도를 빠르게 높이고, 유지보수나 협업 과정에서의 어려움을 크게 줄일 수 있습니다.
소프트웨어 개발 세계는 다양한 프로그래밍 언어들로 이루어져 있습니다. 때로는 특정 언어로 작성된 기존 코드를 다른 언어로 변환해야 하는 상황이 발생합니다. 예를 들어, 오래된 시스템을 현대적인 언어로 마이그레이션해야 하거나, 특정 플랫폼이나 라이브러리와의 호환성을 위해 코드를 다른 언어로 다시 작성해야 하는 경우 등입니다. 이러한 코드 번역(Code Translation) 작업은 단순히 문법만 바꾸는 것이 아니라, 각 언어의 특성과 관용적인 표현(Idiom)까지 고려해야 하는 상당히 복잡하고 시간이 많이 소요되는 작업일 수 있습니다.
하지만 이제 LLM이 이 어려운 코드 번역 작업에서도 당신의 든든한 조력자가 될 수 있습니다! 최신 LLM들은 여러 프로그래밍 언어의 문법과 구조, 그리고 라이브러리 사용법까지 학습했기 때문에, 한 언어로 작성된 코드를 입력받아 그 의미와 기능을 유지하면서 다른 언어의 코드로 변환해주는 능력을 가지고 있습니다.
예를 들어, 앞서 우리가 Bash 스크립트로 작성했던 파일 이름 변경 기능을 이번에는 Python 언어로 변환해달라고 LLM에게 요청해 봅시다. Bash 스크립트는 간단한 작업에는 유용하지만, 사용자 인터페이스(UI)를 만들거나 더 복잡한 로직을 추가하기에는 Python이 더 적합한 경우가 많기 때문입니다.
[표 18 예시: Bash 코드를 Python 코드로 번역하기 위한 프롬프트]
Translate the below Bash code to a Python snippet.
```bash
#!/bin/bash
echo "Enter the folder name: "
read folder_name
if [ ! -d "$folder_name" ]; then
echo "Folder does not exist."
exit 1
fi
find "$folder_name" -maxdepth 1 -type f | while read -r file; do
base_name=$(basename "$file")
dir_name=$(dirname "$file")
new_file_name="draft_$base_name"
new_file_path="$dir_name/$new_file_name"
if mv "$file" "$new_file_path"; then
echo "Renamed '$base_name' to '$new_file_name'"
else
echo "Error renaming '$base_name'" >&2
fi
done
echo "Files renamed successfully."
import os
import shutil
# Get the folder name from the user
folder_name = input("Enter the folder name: ")
# Check if the folder exists and is a directory
if not os.path.isdir(folder_name):
print("Folder does not exist or is not a directory.")
exit(1)
try:
# Get a list of all files in the folder (not directories)
files = [f for f in os.listdir(folder_name) if os.path.isfile(os.path.join(folder_name, f))]
# Iterate over the files
for file in files:
# Create the new file name with the prefix "draft_"
# Handles potential issues with filenames starting with "."
new_file_name = f"draft_{file}"
# Construct the full paths for the original and new files
old_path = os.path.join(folder_name, file)
new_path = os.path.join(folder_name, new_file_name)
# Try to move/rename the file
try:
shutil.move(old_path, new_path)
print(f"Renamed '{file}' to '{new_file_name}'")
except Exception as e:
print(f"Error renaming file '{file}': {e}")
print("Files renamed successfully.")
except Exception as e:
print(f"An error occurred while processing the folder: {e}")
exit(1)
(참고: 위 출력 코드는 원문(표 18)의 코드보다 더 견고한 오류 처리와 Pythonic한 방식을 적용한 예시입니다. LLM은 다양한 수준의 번역 결과를 생성할 수 있습니다.)LLM이 생성한 Python 코드를 살펴보면, 원래 Bash 스크립트가 수행하던 핵심 기능(폴더 이름 입력받기, 폴더 존재 확인, 폴더 내 파일 목록 가져오기, 각 파일 이름 앞에 ‘draft_’ 붙여서 변경하기, 성공 메시지 출력)을 Python의 표준 라이브러리(os, shutil)를 사용하여 거의 동일하게 구현했음을 알 수 있습니다. 물론, 세부적인 구현 방식(예: 파일 목록 가져오기, 파일 경로 처리)은 Python 언어의 특성에 맞게 변경되었습니다.
하지만 여기서도 앞서 코드 생성 시 강조했던 주의사항이 동일하게 적용됩니다. LLM이 생성한 번역 코드가 항상 문법적으로 완벽하거나, 원래 코드의 모든 기능과 뉘앙스를 정확하게 보존한다고 보장할 수는 없습니다. 특히 복잡한 로직이나 특정 라이브러리에 대한 의존성이 높은 코드를 번역할 경우에는 오류가 발생하거나 예상과 다르게 작동할 가능성이 있습니다.
따라서 LLM이 번역해 준 코드는 반드시 원본 코드와의 기능적 동일성을 주의 깊게 검토하고, 철저한 테스트를 통해 정확성과 안정성을 확인해야 합니다. 그럼에도 불구하고, LLM의 코드 번역 능력은 다른 언어로 작성된 코드를 이해하거나 마이그레이션하는 작업의 초기 단계에서 매우 유용한 출발점을 제공해주고, 개발자의 시간과 노력을 크게 절약해 줄 수 있는 강력한 도구임에 틀림없습니다.
소프트웨어 개발 과정에서 버그(Bug)는 피할 수 없는 존재입니다. 아무리 뛰어난 개발자라도 실수를 하기 마련이고, 복잡한 시스템에서는 예상치 못한 오류가 발생하기 쉽습니다. 버그를 찾아내고 수정하는 디버깅(Debugging) 과정은 때로는 매우 지루하고 많은 시간과 노력을 요구하는 힘든 작업입니다. 또한, 내가 작성한 코드가 과연 최선의 방법인지, 더 효율적이거나 가독성 좋게 개선할 여지는 없는지 동료 개발자에게 **코드 리뷰(Code Review)**를 요청하고 피드백을 받는 것 역시 코드 품질을 높이기 위한 중요한 과정입니다.
놀랍게도, 이제 LLM이 이러한 코드 디버깅과 리뷰 과정에서도 당신의 든든한 조력자 역할을 해줄 수 있습니다! LLM은 주어진 코드 조각을 분석하여 잠재적인 오류나 버그를 찾아내고 그 원인을 설명해주거나, 혹은 코드의 가독성, 효율성, 유지보수성 등의 측면에서 개선할 수 있는 점들을 제안해주는 능력을 가지고 있습니다. 이는 마치 경험 많고 꼼꼼한 선배 개발자가 당신의 코드를 검토해주며 조언해주는 것과 같은 효과를 줄 수 있습니다.
예를 들어, 앞서 우리가 Python으로 번역했던 파일 이름 변경 코드(표 18의 코드)에 의도적으로 몇 가지 오류를 추가해 보았다고 가정해 봅시다. (문서의 Snippet 3 참조). 예를 들어, 사용자로부터 파일 이름 앞에 붙일 접두사(prefix)를 입력받고, 이 접두사를 대문자로 변환하여 사용하려고 했는데, toUpperCase()
라는 존재하지 않는 함수를 호출하여 NameError
가 발생하도록 코드를 수정했다고 해봅시다. 또한, 파일 이동 시 발생할 수 있는 오류를 제대로 처리하지 않는 문제도 있다고 가정해 봅시다.
이제 이 버그가 있는 Python 코드를 LLM에게 주고, 무엇이 잘못되었는지 디버깅하고 코드를 개선할 방법을 설명해달라고 요청해 보겠습니다.
[표 19 예시: Python 코드 디버깅 및 리뷰를 위한 프롬프트]
이름: 1_python_debug_code
목표: 주어진 Python 코드의 오류를 찾아 수정하고 개선 방안을 제안한다.
모델: gemini-pro
Temperature: 0.1 (정확한 분석과 제안이 중요)
Top-K: N/A
Top-P: 1
토큰 제한: 1024
프롬프트:
The below Python code gives an error:
Traceback (most recent call last):
File "/Users/leeboonstra/Documents/test_folder/rename_files.py", line 7,
in <module>
text = toUpperCase(prefix)
NameError: name 'toUpperCase' is not defined
Debug what's wrong and explain how I can improve the code.
```python
import os
import shutil
# Get the folder name from the user
folder_name = input("Enter the folder name: ")
prefix = input("Enter the string to prepend to the filename: ")
text = toUpperCase(prefix) # This line causes NameError
# Check if the folder exists
if not os.path.isdir(folder_name):
print("Folder does not exist.")
exit(1)
# Get a list of all files in the folder
files = os.listdir(folder_name)
# Iterate over the files
for file in files:
# Create a new file name with the prefix
new_filename = f"{text}_{file}" # Error here if 'text' is not defined
# Move the file to the new name
shutil.move(os.path.join(folder_name, file),
os.path.join(folder_name, new_filename)) # Potential errors not handled
# Print a success message
print("Files renamed successfully.")
LLM 출력 예시 (요약):
제공해주신 파이썬 코드에는 NameError
를 발생시키는 명백한 오류가 있습니다. toUpperCase
라는 함수는 파이썬 표준 라이브러리에 정의되어 있지 않기 때문입니다. 문자열을 대문자로 변환하기 위해서는 파이썬 문자열 객체의 내장 메서드인 .upper()
를 사용해야 합니다. 따라서 text = toUpperCase(prefix)
라인을 text = prefix.upper()
로 수정해야 합니다.
또한, 현재 코드에는 몇 가지 추가적으로 개선할 수 있는 점들이 있습니다:
os.path.splitext()
함수를 사용하여 파일 이름과 확장자를 분리한 후, 새로운 이름에 확장자를 다시 붙여주는 것이 좋습니다.shutil.move()
함수는 파일을 이동/이름 변경하는 과정에서 다양한 오류(예: 권한 부족, 파일 이름 중복 등)가 발생할 수 있습니다. 현재 코드는 이러한 오류를 처리하지 않으므로, try...except
블록으로 감싸서 잠재적인 오류를 잡고 사용자에게 적절한 메시지를 보여주는 것이 더 견고한 코드를 만드는 방법입니다.아래는 이러한 점들을 반영하여 개선된 코드 예시입니다:
import os
import shutil
# Get the folder name and prefix from the user
folder_name = input("Enter the folder name: ")
prefix = input("Enter the string to prepend to the filename: ")
text = prefix.upper() # Use .upper() method
# Check if the folder exists and is a directory
if not os.path.isdir(folder_name):
print(f"Error: Folder '{folder_name}' does not exist or is not a directory.")
exit(1)
print(f"Renaming files in folder: {folder_name}")
# Iterate over the items in the folder
try:
for filename in os.listdir(folder_name):
old_path = os.path.join(folder_name, filename)
# Process only files, not directories
if os.path.isfile(old_path):
try:
# Separate filename and extension
name_part, ext_part = os.path.splitext(filename)
# Create the new filename
new_filename = f"{text}_{name_part}{ext_part}"
new_path = os.path.join(folder_name, new_filename)
# Rename the file
shutil.move(old_path, new_path)
print(f" Renamed '{filename}' to '{new_filename}'")
except Exception as e:
print(f" Error renaming file '{filename}': {e}")
print("File renaming process completed.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
exit(1)
정말 놀랍지 않습니까? LLM은 우리가 제공한 오류 메시지(NameError
)와 코드를 분석하여 정확한 오류의 원인(toUpperCase
함수 부재)을 찾아냈을 뿐만 아니라, 올바른 해결책(.upper()
메서드 사용)까지 제시했습니다. 더 나아가, 원래 코드에는 미처 생각하지 못했던 잠재적인 문제점들(확장자 누락, 오류 처리 부재)까지 지적하고, 이를 개선하기 위한 구체적인 코드 수정 제안까지 덧붙였습니다!
이처럼 LLM을 코드 디버깅 및 리뷰에 활용하면, 개발자가 놓치기 쉬운 오류를 찾아내고 수정하는 시간을 단축할 수 있을 뿐만 아니라, 코드의 품질(가독성, 효율성, 안정성 등)을 향상시키는 데 유용한 아이디어나 제안을 얻을 수도 있습니다. LLM은 당신의 코드를 객관적인 시각으로 검토해주는 훌륭한 '가상 코드 리뷰어’가 될 수 있습니다.
물론, 여기서도 주의할 점은 있습니다. LLM이 제안하는 수정 사항이나 개선 방안이 항상 최선이거나 유일한 해결책은 아닐 수 있습니다. 때로는 LLM의 제안이 오히려 새로운 문제를 야기하거나, 프로젝트의 전체적인 맥락이나 요구사항과는 맞지 않을 수도 있습니다. 따라서 LLM의 제안은 어디까지나 '참고 의견’으로 받아들이고, 그것을 적용할지 여부는 반드시 당신 스스로의 비판적인 판단과 추가적인 검증을 거쳐 결정해야 합니다.
지금까지 우리가 다룬 프롬프팅은 주로 텍스트(자연어 또는 코드)를 입력으로 사용하는 방식이었습니다. 하지만 LLM 기술이 발전하면서, 텍스트뿐만 아니라 이미지, 오디오, 비디오 등 다양한 종류의 입력 형식(Modality)을 함께 이해하고 처리할 수 있는 ‘멀티모달(Multimodal)’ 모델들이 등장하고 있습니다. 예를 들어, 구글의 Gemini 모델도 강력한 멀티모달 능력을 가지고 있죠.
**‘멀티모달 프롬프팅’**이란 바로 이렇게 텍스트 외에 다른 형식의 입력(예: 이미지, 오디오)을 텍스트 프롬프트와 함께 사용하여 LLM을 가이드하는 기법을 의미합니다. 예를 들어, 특정 건물의 이미지를 보여주고 “이 건물의 건축 양식은 무엇인가요?” 라고 텍스트 프롬프트를 함께 입력하거나, 특정 음악의 오디오 클립을 들려주고 “이 음악의 장르는 무엇이고 어떤 악기들이 사용되었나요?” 라고 질문하는 것이 멀티모달 프롬프팅의 예시입니다.
그렇다면 이 멀티모달 프롬프팅이 우리가 방금까지 이야기했던 '코드 프롬프팅’과는 어떤 관계가 있을까요? 현재 시점에서 코드를 생성하거나 분석하는 프롬프팅 자체는 여전히 주로 텍스트 기반으로 이루어집니다. 즉, 코드 관련 작업을 LLM에게 시키기 위해 이미지를 입력할 필요는 일반적으로 없습니다.
하지만 멀티모달 프롬프팅은 코드 관련 작업과 간접적으로 결합되어 흥미로운 방식으로 활용될 수 있는 잠재력을 가지고 있습니다. 예를 들어, 다음과 같은 시나리오들을 상상해 볼 수 있습니다.
이처럼 멀티모달 프롬프팅은 텍스트만으로는 전달하기 어려운 시각적 또는 청각적 정보를 LLM에게 제공함으로써, 코드 관련 작업을 포함한 다양한 영역에서 LLM의 이해도와 문제 해결 능력을 한 단계 더 끌어올릴 수 있는 잠재력을 가지고 있습니다. 앞으로 LLM 기술이 더욱 발전함에 따라, 이러한 멀티모달 프롬프팅 기법의 중요성과 활용 범위는 더욱 커질 것으로 예상됩니다.
자, 지금까지 우리는 LLM의 작동 원리부터 시작하여 다양한 프롬프팅 기법들과 코드 관련 활용법까지, 프롬프트 엔지니어링의 핵심적인 내용들을 살펴보았습니다. 이 모든 지식들을 바탕으로 당신도 이제 LLM을 훨씬 더 효과적으로 활용할 수 있는 준비가 되었습니다. 하지만 이론을 아는 것과 실제로 뛰어난 프롬프트를 작성하는 것은 또 다른 문제입니다. 마치 요리 레시피를 외운다고 해서 바로 최고의 요리사가 되는 것은 아닌 것처럼 말이죠.
최고의 프롬프트를 찾는 과정은 본질적으로 '반복적인 실험과 개선(Tinkering)'의 과정입니다. 완벽한 프롬프트를 단번에 만들어낼 수 있는 마법 공식이란 존재하지 않습니다. 대신, 다양한 시도를 해보고, 그 결과를 분석하며, 꾸준히 개선해나가는 노력이 필요합니다. 구글의 Vertex AI Studio와 같은 도구들은 다양한 모델들을 대상으로 당신의 프롬프트를 쉽게 테스트하고 실험해볼 수 있는 훌륭한 '놀이터(Playground)'를 제공해 줍니다.
이번 마지막 섹션에서는 당신이 프롬프트 엔지니어링의 ‘프로(Pro)’ 수준으로 발돋움하는 데 도움이 될 수 있는 핵심적인 '모범 사례(Best Practices)'들을 정리하여 제시하고자 합니다. 이 원칙들을 당신의 프롬프트 작성 과정에 꾸준히 적용한다면, 당신은 LLM으로부터 훨씬 더 정확하고, 관련성 높으며, 당신의 의도에 부합하는 결과물을 얻어낼 수 있을 것입니다.
프롬프트 엔지니어링에서 가장 중요하고 효과적인 모범 사례를 딱 하나만 꼽으라면, 그것은 바로 '구체적인 예시(Examples)를 제공하는 것’입니다. 즉, 앞서 우리가 ‘원샷(One-shot)’ 또는 ‘퓨샷(Few-shot)’ 프롬프팅 기법에서 살펴보았던 것처럼, LLM에게 당신이 원하는 결과물이 어떤 모습인지 명확하게 보여주는 실제 입력과 출력의 쌍을 프롬프트 안에 포함시키는 것입니다.
왜 예시를 제공하는 것이 그토록 강력할까요? 그 이유는 예시가 LLM에게 매우 효과적인 ‘학습 도구(Teaching Tool)’ 역할을 하기 때문입니다. 추상적인 설명이나 지시만으로는 LLM이 당신의 의도를 정확히 파악하기 어려울 수 있지만, 구체적인 예시를 보여주면 LLM은 “아하! 이런 입력을 받으면, 저런 스타일과 형식으로, 저런 종류의 내용을 담아서 응답하라는 뜻이구나!” 라고 훨씬 더 명확하게 이해할 수 있습니다.
이 예시들은 LLM에게 당신이 원하는 출력물(Desired Outputs)이나 유사한 응답(Similar Responses)의 구체적인 모습을 보여줌으로써, LLM이 그것들로부터 **패턴을 학습하고(Learn from them), 그 학습 내용을 바탕으로 자신의 결과물 생성 방식을 스스로 조정(Tailor its own generation)**하도록 유도합니다. 마치 우리가 새로운 작업을 배울 때, 잘 만들어진 샘플이나 모범 답안을 참고하면 훨씬 더 빠르고 정확하게 작업을 수행할 수 있게 되는 것과 같은 이치입니다.
결국, 예시를 제공하는 것은 LLM에게 **도달해야 할 목표 지점(Target)이나 따라야 할 기준점(Reference Point)**을 명확하게 제시해주는 것과 같습니다. 이를 통해 우리는 LLM이 생성하는 응답의 **정확성(Accuracy), 스타일(Style), 그리고 어조(Tone)**를 우리가 기대하는 수준에 더 가깝게 만들고, 최종적으로 우리의 요구사항과 기대에 더 잘 부합하는(Better match your expectations) 결과물을 얻어낼 수 있습니다.
따라서, 특히 복잡한 작업을 요청하거나, 특정 출력 형식을 요구하거나, 혹은 일관된 스타일의 응답을 원할 때는, 반드시! 프롬프트 안에 한 개 이상의 관련성 높고 잘 만들어진 예시를 포함시키는 것을 강력하게 권장합니다. 이것이 당신의 프롬프트를 평범함에서 비범함으로 끌어올리는 가장 확실한 방법 중 하나입니다.
프롬프트는 당신과 LLM 모델 모두에게 명확하고, 간결하며, 이해하기 쉬워야 합니다. 이것은 프롬프트 엔지니어링의 또 다른 핵심 원칙입니다. 경험적으로 볼 때, 만약 당신 스스로가 읽어봐도 프롬프트의 내용이 혼란스럽거나 모호하게 느껴진다면, LLM 모델 역시 그것을 제대로 이해하지 못하고 엉뚱한 결과물을 내놓을 가능성이 매우 높습니다.
따라서 프롬프트를 작성할 때는 지나치게 복잡하거나 현학적인 언어 사용을 피하고, 가능하면 쉽고 직관적인 단어와 문장 구조를 사용하는 것이 좋습니다. 또한, 현재 작업과 직접적인 관련이 없는 불필요한 정보나 배경 설명을 너무 장황하게 늘어놓는 것도 피해야 합니다. 이는 오히려 LLM을 혼란스럽게 만들고 중요한 핵심 지시 사항에 집중하는 것을 방해할 수 있습니다.
핵심은 '단순함(Simplicity)'과 '명확함(Clarity)'입니다. 당신이 LLM에게 원하는 것이 무엇인지를 군더더기 없이, 핵심만 간결하게, 그리고 오해의 소지가 없도록 명확하게 전달하는 것이 중요합니다.
예를 들어, 당신이 3살짜리 아이 두 명과 함께 뉴욕 여행을 계획하고 있고, 아이들과 함께 갈 만한 좋은 장소를 추천받고 싶다고 가정해 봅시다.
[개선 전 프롬프트 (BEFORE):]
저는 지금 뉴욕을 방문 중인데요, 좋은 장소들에 대해 좀 더 듣고 싶습니다. 저는 3살짜리 아이 두 명과 함께 있어요. 우리 휴가 동안 어디를 가야 할까요?
이 프롬프트는 다소 산만하고 LLM에게 어떤 종류의 정보를 원하는지 명확하지 않습니다.
[개선 후 프롬프트 (AFTER REWRITE):]
Act as a travel guide for tourists. Describe great places to visit in New York Manhattan with a 3 year old. (관광객을 위한 여행 가이드 역할을 해주세요. 3살 아이와 함께 뉴욕 맨해튼에서 방문하기 좋은 멋진 장소들을 설명해주세요.)
개선된 프롬프트는 LLM에게 '여행 가이드’라는 명확한 역할을 부여하고, '3살 아이와 함께 갈 만한 맨해튼의 장소’라는 구체적인 정보를 요구하며, '설명해달라(Describe)'는 명확한 행동 동사를 사용하여 훨씬 더 간결하고 명확하게 의도를 전달하고 있습니다.
팁: 프롬프트를 작성할 때, LLM이 수행해야 할 행동(Action)을 묘사하는 동사를 적극적으로 활용하는 것이 좋습니다. 예를 들어, Act(역할 수행)
, Analyze(분석)
, Categorize(분류)
, Classify(분류)
, Contrast(대조)
, Compare(비교)
, Create(생성)
, Describe(설명)
, Define(정의)
, Evaluate(평가)
, Extract(추출)
, Find(찾기)
, Generate(생성)
, Identify(식별)
, List(목록화)
, Measure(측정)
, Organize(정리)
, Parse(파싱)
, Pick(선택)
, Predict(예측)
, Provide(제공)
, Rank(순위 매기기)
, Recommend(추천)
, Return(반환)
, Retrieve(검색)
, Rewrite(다시 쓰기)
, Select(선택)
, Show(보여주기)
, Sort(정렬)
, Summarize(요약)
, Translate(번역)
, Write(쓰기)
등 다양한 동사를 활용하여 당신의 요구사항을 더 명확하게 표현할 수 있습니다.
프롬프트가 너무 간결하거나 일반적(Generic)이면, LLM이 당신이 원하는 결과물을 정확하게 생성하도록 충분히 안내하지 못할 수 있습니다. LLM은 당신의 마음을 읽을 수 없기 때문에, 당신이 어떤 종류의 결과물을, 어떤 형식으로, 어떤 스타일로, 어느 정도의 상세 수준으로 원하는지를 가능한 한 구체적으로 명시해주는 것이 매우 중요합니다.
프롬프트에 **구체적인 세부 정보(Specific Details)**를 제공하는 것(앞서 설명한 시스템 프롬프팅이나 컨텍스츄얼 프롬프팅 활용)은 LLM이 정말 관련성 높은 정보(What’s relevant)에 집중하도록 돕고, 결과적으로 전체적인 응답의 정확성(Overall Accuracy)을 향상시키는 데 큰 도움이 됩니다.
예를 들어, 당신이 상위 5개 비디오 게임 콘솔에 대한 블로그 포스팅을 작성해달라고 LLM에게 요청한다고 가정해 봅시다.
[구체적이지 않은 프롬프트 (DO NOT):]
Generate a blog post about video game consoles. (비디오 게임 콘솔에 대한 블로그 포스팅을 생성해줘.)
이 프롬프트는 너무 일반적이어서 LLM이 어떤 콘솔을 다루어야 할지, 어떤 내용을 포함해야 할지, 어떤 스타일로 작성해야 할지 전혀 알 수 없습니다.
[구체적인 프롬프트 (DO):]
Generate a 3 paragraph blog post about the top 5 video game consoles. The blog post should be informative and engaging, and it should be written in a conversational style. (상위 5개 비디오 게임 콘솔에 대한 3문단짜리 블로그 포스팅을 생성해줘. 이 포스팅은 유익하고 흥미로워야 하며, 대화체 스타일로 작성되어야 해.)
이 프롬프트는 **다룰 내용(상위 5개 콘솔), 분량(3문단), 내용의 성격(유익하고 흥미롭게), 그리고 글쓰기 스타일(대화체)**까지 매우 구체적으로 명시해주고 있습니다. 이렇게 구체적인 지침을 제공하면, LLM은 훨씬 더 당신의 기대에 부합하는 결과물을 생성할 가능성이 높아집니다. 원하는 것을 얻으려면, 원하는 것을 정확하게 요구하십시오!
프롬프트를 통해 LLM의 출력을 안내하는 방식에는 크게 두 가지 접근법이 있습니다: '지시(Instructions)'와 ‘제약조건(Constraints)’.
그렇다면 어떤 방식을 사용하는 것이 더 효과적일까요? 최근의 연구들은 가급적 '제약조건’보다는 '긍정적인 지시(Positive Instructions)'에 초점을 맞추는 것이 더 효과적일 수 있음을 시사합니다. 이는 우리 인간이 '하지 말아야 할 것’들의 목록보다는 '해야 할 것’에 대한 명확한 지침을 선호하는 방식과도 일치합니다.
지시는 원하는 결과를 직접적으로 전달하기 때문에 모델이 무엇을 해야 할지 명확하게 이해하고, 정의된 경계 내에서 유연성과 창의성을 발휘하도록 장려할 수 있습니다. 반면에, 제약조건은 모델이 무엇이 허용되는지에 대해 추측하게 만들 수 있으며, 때로는 모델의 잠재력을 제한하거나 여러 제약조건들이 서로 충돌하는 문제를 야기할 수도 있습니다.
물론, 제약조건이 전혀 필요 없다는 의미는 아닙니다. 특정 상황에서는 제약조건이 여전히 매우 중요하고 유용합니다. 예를 들어, 모델이 유해하거나(Harmful) 편향된(Biased) 콘텐츠를 생성하는 것을 방지하기 위한 안전 장치로서 제약조건은 필수적입니다. 또한, **매우 엄격한 출력 형식(예: 특정 JSON 스키마 준수)이나 스타일(예: 법률 문서의 형식 유지)**이 요구되는 경우에도 제약조건이 필요할 수 있습니다.
하지만 가능하다면, '긍정적인 지시’를 우선적으로 사용하는 것이 좋습니다. 즉, 모델에게 '무엇을 하지 말라’고 말하는 대신, '무엇을 하라’고 명확하게 지시하는 것입니다. 이는 혼란을 줄이고 응답의 정확성을 향상시키는 데 도움이 될 수 있습니다.
예를 들어, 상위 5개 비디오 게임 콘솔에 대한 블로그 포스팅을 요청하면서, 게임 이름은 언급하지 않기를 원한다고 가정해 봅시다.
[제약조건 사용 (DO NOT):]
Generate a 1 paragraph blog post about the top 5 video game consoles. Do not list video game names. (상위 5개 비디오 게임 콘솔에 대한 1문단짜리 블로그 포스팅을 생성해줘. 비디오 게임 이름은 나열하지 마.)
이 방식은 모델에게 무엇을 빼야 하는지만 알려줄 뿐, 무엇을 포함해야 하는지에 대한 명확한 지침이 부족합니다.
[긍정적 지시 사용 (DO):]
Generate a 1 paragraph blog post about the top 5 video game consoles. Only discuss the console, the company who made it, the year, and total sales. (상위 5개 비디오 게임 콘솔에 대한 1문단짜리 블로그 포스팅을 생성해줘. 오직 콘솔 자체, 제조사, 출시 연도, 그리고 총 판매량에 대해서만 논의해줘.)
이 방식은 모델에게 정확히 어떤 정보들을 포함해야 하는지를 명확하게 지시함으로써, 훨씬 더 구체적이고 원하는 결과에 가까운 응답을 유도할 수 있습니다.
따라서 모범 사례는, 먼저 '지시’를 우선적으로 사용하여 당신이 원하는 바를 명확하게 전달하고, '제약조건’은 안전, 명확성, 또는 특정 요구사항을 위해 꼭 필요한 경우에만 보조적으로 사용하는 것입니다. 그리고 항상 다양한 지시와 제약조건의 조합을 실험하고 반복 테스트하여 당신의 특정 작업에 가장 효과적인 방식을 찾고, 그 결과를 문서화하는 것이 중요합니다.
LLM이 생성하는 응답의 길이를 제어하는 것은 비용 관리, 응답 속도 조절, 그리고 때로는 결과의 간결성을 확보하는 데 매우 중요합니다. 응답 길이를 제어하는 방법은 크게 두 가지가 있습니다.
첫 번째는 앞서 ‘LLM 출력 설정’ 섹션에서 설명했듯이, 모델 설정(Configuration)에서 ‘최대 생성 토큰 수(Max Output Tokens)’ 제한 값을 직접 설정하는 것입니다. 예를 들어, 최대 100개의 토큰만 생성하도록 제한할 수 있습니다. 이 방법은 생성되는 응답의 절대적인 최대 길이를 강제하는 가장 확실한 방법입니다.
두 번째 방법은 프롬프트 내에서 명시적으로 원하는 길이를 요청하는 것입니다. 예를 들어, 다음과 같이 요청할 수 있습니다.
"Explain quantum physics in a tweet length message." (양자 물리학을 트윗 길이의 메시지로 설명해줘.)
혹은,
"Summarize the provided article in exactly three sentences." (제공된 기사를 정확히 세 문장으로 요약해줘.)
이 방식은 모델 설정값을 직접 건드릴 수 없는 경우나, 좀 더 유연하게 길이를 조절하고 싶을 때 유용합니다. 하지만 LLM이 항상 프롬프트의 길이 요청을 완벽하게 따르는 것은 아니라는 점을 유의해야 합니다. 때로는 요청보다 약간 길거나 짧은 응답을 생성할 수도 있습니다. 따라서 정확한 길이 제어가 매우 중요하다면, 모델 설정에서 최대 토큰 제한을 함께 사용하는 것이 더 안전합니다.
프롬프트를 작성하다 보면, 동일한 구조의 프롬프트를 내용의 일부만 바꿔가며 반복적으로 사용해야 하는 경우가 많습니다. 예를 들어, 여러 다른 도시에 대한 정보를 얻기 위해 “Tell me a fact about the city: [도시 이름]” 이라는 프롬프트를 도시 이름만 바꿔가며 계속 사용해야 할 수 있습니다. 이렇게 매번 프롬프트 전체를 복사해서 수정하는 것은 매우 비효율적이고 오류가 발생하기 쉽습니다.
바로 이런 경우에 프롬프트 내에 '변수(Variables)'를 사용하는 것이 매우 유용합니다. 변수를 사용하면 프롬프트의 템플릿(Template) 구조는 그대로 유지하면서, 특정 부분의 내용만 동적으로 변경하여 프롬프트를 재사용할 수 있습니다.
[표 20 예시: 프롬프트 내 변수 사용]
VARIABLES
{city} = "Amsterdam" # 또는 "Paris", "Seoul" 등 다른 도시 이름으로 변경 가능
PROMPT
You are a travel guide. Tell me a fact about the city: {city}
이 예시에서는 {city}
라는 변수를 사용하여 도시 이름을 동적으로 변경할 수 있도록 했습니다. 이렇게 변수를 사용하면, 동일한 프롬프트 구조를 재사용하여 다른 입력값(다른 도시 이름)에 대한 응답을 쉽게 생성할 수 있으므로 시간과 노력을 크게 절약할 수 있습니다.
특히, 당신이 직접 개발하는 소프트웨어 애플리케이션이나 서비스에 LLM 기능을 통합할 때는 이 변수 사용이 거의 필수적입니다. 사용자의 입력이나 다른 시스템의 데이터를 변수로 받아 프롬프트 템플릿에 동적으로 삽입하여 LLM에게 전달하는 방식으로 작동하게 됩니다.
따라서 프롬프트를 재사용하거나 동적인 입력을 처리해야 하는 경우에는 반드시 변수를 활용하는 방법을 익혀두시기 바랍니다. 이는 당신의 프롬프트 관리 효율성을 크게 높여줄 것입니다.
LLM은 당신이 프롬프트를 어떤 형식으로 작성하고 어떤 스타일로 표현하는지에 따라 상당히 다른 결과물을 내놓을 수 있습니다. 동일한 목표를 가진 프롬프트라도, 그것을 질문(Question) 형식으로 표현하는지, 서술문(Statement) 형식으로 표현하는지, 혹은 명확한 지시문(Instruction) 형식으로 표현하는지에 따라 LLM이 생성하는 응답의 내용, 길이, 스타일 등이 달라질 수 있습니다.
예를 들어, 혁신적인 비디오 게임 콘솔이었던 '세가 드림캐스트(Sega Dreamcast)'에 대한 텍스트를 생성하는 것이 목표라고 가정해 봅시다. 이 목표를 위한 프롬프트는 다음과 같이 여러 가지 방식으로 작성될 수 있으며, 각각 다른 결과물을 유도할 수 있습니다.
이 외에도 **프롬프트의 어조(Tone)를 바꾸거나(예: 공식적, 비공식적, 유머러스하게), 사용하는 단어 선택(Word Choice)을 다르게 하거나, 프롬프트의 구조(Structure)를 변경하는 등 다양한 속성(Attributes)**을 실험해 볼 수 있습니다.
중요한 것은 ‘정답’ 프롬프트 형식이란 존재하지 않으며, 당신의 특정 작업 목표와 사용 중인 모델의 특성에 가장 잘 맞는 최적의 조합을 찾기 위해 다양한 입력 형식과 작성 스타일을 꾸준히 실험해보는 것입니다. 어떤 방식이 가장 효과적인지는 직접 시도해보기 전까지는 알 수 없습니다. 다양한 시도를 두려워하지 말고, 그 결과를 비교 분석하며 최적의 프롬프트 스타일을 찾아나가십시오.
앞서 ‘퓨샷(Few-shot)’ 프롬프팅 기법을 설명하면서, LLM에게 여러 개의 예시를 제공하여 원하는 패턴을 학습시키는 방식이라고 말씀드렸습니다. 이 기법은 특히 분류(Classification) 작업에서 매우 유용하게 사용될 수 있습니다. 예를 들어, 주어진 문장이 긍정인지 부정인지, 혹은 어떤 카테고리에 속하는지를 분류하는 작업에 여러 개의 예시(문장과 해당 레이블)를 보여주는 것이죠.
그런데 퓨샷 프롬프팅으로 분류 작업을 수행할 때 한 가지 주의해야 할 점이 있습니다. 그것은 바로 제공하는 예시들에서 가능한 응답 클래스(Response Classes)의 순서를 의도적으로 섞어주어야 한다는 것입니다. 예를 들어, 긍정/부정 분류 예시를 제공할 때, 항상 [긍정 예시 -> 부정 예시 -> 긍정 예시 -> 부정 예시] 와 같이 특정 순서로만 예시를 나열해서는 안 된다는 말입니다.
왜 그럴까요? 만약 예시들이 항상 특정 순서로만 제시되면, LLM은 각 클래스의 **핵심적인 특징(Key Features)**을 학습하는 것이 아니라, 단순히 **예시들이 제시된 순서 자체에 과적합(Overfitting)**되어 버릴 위험이 있습니다. 즉, “아, 첫 번째 예시는 긍정이었고, 두 번째는 부정이었으니, 이번에도 그 순서대로 답하면 되겠지?” 와 같이 잘못된 패턴을 학습할 수 있다는 것입니다.
따라서, 퓨샷 예시를 구성할 때는 가능한 응답 클래스들(예: 긍정, 부정, 중립)을 무작위로 섞어서(Mix up the possible response classes) 제시하는 것이 중요합니다. 예를 들어, [긍정 -> 중립 -> 부정 -> 긍정 -> 부정 -> 중립] 과 같이 말이죠. 이렇게 클래스 순서를 섞어줌으로써, 우리는 LLM이 단순히 예시의 순서를 암기하는 것이 아니라, 각 클래스를 구분하는 진짜 핵심적인 특징들을 제대로 학습하도록 유도할 수 있습니다. 이는 결과적으로 보지 못했던 새로운 데이터(Unseen Data)에 대해서도 더 강건하고(Robust) 일반화된(Generalizable) 성능을 보이는 모델을 만드는 데 도움이 됩니다.
일반적으로, 퓨샷 예시의 순서 자체는 LLM 성능에 큰 영향을 미치지 않는다고 알려져 있습니다. 하지만 특히 분류 작업에서는 위와 같은 과적합 위험을 방지하기 위해 의도적으로 클래스 순서를 섞어주는 것이 좋은 습관입니다.
대규모 언어 모델(LLM) 기술은 현재 그 어떤 분야보다도 빠르게 발전하고 진화하고 있습니다. 새로운 모델 아키텍처가 계속해서 등장하고, 기존 모델들도 더 많은 데이터로 학습되거나 새로운 기능들이 추가되면서 성능이 지속적으로 개선되고 있습니다.
이는 프롬프트 엔지니어에게 끊임없이 배우고 적응해야 하는 과제를 안겨줍니다. 어제까지 당신의 작업에 최적이었던 프롬프트가, 오늘 모델이 업데이트되면서 더 이상 최선이 아니거나 심지어는 제대로 작동하지 않을 수도 있습니다. 반대로, 이전 모델에서는 불가능했던 새로운 기능이나 능력을 활용하여 훨씬 더 효과적인 프롬프트를 작성할 수 있는 기회가 생길 수도 있습니다.
따라서 성공적인 프롬프트 엔지니어가 되기 위해서는, 당신이 주로 사용하는 LLM 모델의 아키텍처 변경 사항, 추가된 학습 데이터, 그리고 새롭게 공개되는 기능 및 능력들에 대해 항상 최신 정보를 파악하고(Stay on top of model updates) 관심을 기울이는 것이 매우 중요합니다. 새로운 버전의 모델이 출시되면 적극적으로 사용해보고, 기존의 프롬프트들이 여전히 잘 작동하는지, 혹은 새로운 기능을 활용하여 더 개선할 여지는 없는지 테스트하고 조정하는 과정을 꾸준히 거쳐야 합니다.
Vertex AI Studio 와 같은 도구들은 당신이 작성하고 테스트했던 다양한 버전의 프롬프트들을 저장하고, 문서화하며, 필요할 때 다시 불러와 새로운 모델이나 설정에서 테스트해볼 수 있는 편리한 환경을 제공합니다. 이러한 도구들을 적극적으로 활용하여 당신의 프롬프트 자산을 체계적으로 관리하고, 모델의 진화에 발맞춰 당신의 프롬프트 엔지니어링 능력 또한 함께 발전시켜 나가야 합니다. 끊임없이 변화하는 기술 환경 속에서 적응하고 진화하는 능력이야말로 미래의 핵심 경쟁력이 될 것입니다.
프롬프트를 통해 LLM에게 요청하는 작업은 크게 두 가지 종류로 나눌 수 있습니다. 하나는 시나 소설, 혹은 블로그 포스팅처럼 **창의적이고 자유로운 형식의 텍스트 생성이 중요한 작업(Creative Tasks)**이고, 다른 하나는 주어진 텍스트에서 특정 정보를 추출하거나, 데이터를 분류하거나, 순위를 매기거나, 혹은 특정 형식으로 파싱하는 등 **정확하고 구조화된 결과물이 중요한 작업(Non-creative Tasks)**입니다.
만약 당신이 후자, 즉 비창의적인 작업을 수행해야 한다면, LLM의 출력 형식을 단순히 자유로운 텍스트(Plain Text)로 받는 것 대신, '구조화된 형식(Structured Format)'으로 반환하도록 유도하는 것을 적극적으로 고려해보는 것이 좋습니다. 가장 대표적인 구조화된 형식으로는 **JSON(JavaScript Object Notation)**이나 XML(eXtensible Markup Language) 등이 있습니다.
왜 구조화된 형식으로 출력을 받는 것이 유리할까요? 여기에는 몇 가지 중요한 이점들이 있습니다.
첫째, 일관된 스타일 보장: JSON이나 XML은 미리 정의된 엄격한 구조와 문법을 따르기 때문에, LLM이 생성하는 출력 결과가 항상 동일하고 예측 가능한 스타일을 갖게 됩니다. 이는 특히 생성된 결과를 후속 시스템(예: 다른 프로그램이나 데이터베이스)에서 자동으로 처리해야 할 때 매우 중요합니다. 자유로운 텍스트 형식은 표현이 다양하고 일관성이 부족하여 파싱하기 어려운 경우가 많지만, 구조화된 형식은 기계가 처리하기 훨씬 용이합니다.
둘째, 데이터 추출 및 활용 용이: JSON 형식은 데이터를 ‘키(Key)-값(Value)’ 쌍으로 표현하기 때문에, 당신이 원하는 특정 데이터(the data you want to receive)에만 집중하여 추출하고 활용하기가 매우 편리합니다. 예를 들어, 고객 리뷰에서 ‘제품명’, ‘평점’, ‘작성자’ 정보만 추출하여 JSON으로 받으면, 불필요한 부가 설명 없이 원하는 데이터만 정확하게 얻을 수 있습니다.
셋째, 관계 인식 및 데이터 타입 활용: 구조화된 형식, 특히 JSON은 중첩된 구조(Nested Structure)나 배열(Array) 등을 통해 **데이터 간의 관계(Relationship aware)**를 명확하게 표현할 수 있습니다. 또한, 문자열(String), 숫자(Number), 불리언(Boolean) 등 **명시적인 데이터 타입(Data types)**을 정의할 수 있어서 데이터의 의미를 더 정확하게 전달하고 처리할 수 있습니다.
넷째, 정렬 기능 활용: 어떤 경우에는 LLM에게 추출된 데이터를 특정 기준(예: 날짜순, 이름순)에 따라 정렬하여(You can sort it) 반환하도록 요청할 수도 있습니다. JSON과 같은 구조화된 형식은 이러한 정렬된 데이터 표현에도 유리합니다. (물론 정렬 기능은 LLM의 능력과 프롬프트 설계에 따라 달라질 수 있습니다.)
다섯째, 환각(Hallucination) 감소: LLM에게 엄격한 구조(예: JSON 스키마)를 따르도록 강제하면, 모델이 구조에 맞지 않는 엉뚱하거나 관련 없는 내용을 지어내는 환각 현상을 줄이는 데 도움이 될 수 있습니다. 모델은 정해진 틀 안에서만 정보를 생성하려고 노력하게 되기 때문입니다.
**앞서 ‘퓨샷 프롬프팅’ 섹션에서 보았던 피자 주문 JSON 파싱 예시(표 2)나, ‘시스템 프롬프팅’ 섹션의 JSON 형식 출력 예시(표 4)**가 바로 이러한 구조화된 출력 형식 활용의 좋은 사례입니다.
따라서, 당신이 LLM을 활용하여 데이터를 추출, 선택, 파싱, 정렬, 순위 매기기, 분류하는 등 비창의적이면서 구조화된 결과가 중요한 작업을 수행해야 한다면, 반드시 JSON이나 XML과 같은 구조화된 형식으로 출력을 요청하는 방법을 실험해 보시기 바랍니다. 이는 당신의 작업 효율성과 결과의 정확성을 크게 향상시켜 줄 수 있는 강력한 전략입니다.
JSON과 같은 구조화된 형식으로 출력을 받는 것에는 많은 이점들이 있지만, 동시에 몇 가지 단점이나 주의해야 할 점도 존재합니다. 가장 큰 단점 중 하나는 JSON 형식이 일반 텍스트(Plain Text)에 비해 훨씬 더 많은 토큰(Tokens)을 요구한다는 것입니다. 왜냐하면 데이터를 감싸는 중괄호({}
), 대괄호([]
), 키를 나타내는 따옴표(""
), 쉼표(,
), 콜론(:
) 등 구조를 표현하기 위한 부가적인 문자열들이 많이 필요하기 때문입니다.
이는 LLM 처리 시간 증가와 비용 상승으로 이어질 수 있습니다. 또한, JSON의 이러한 장황함(Verbosity)은 LLM의 최대 출력 토큰 제한(Token Limits)에 쉽게 도달하게 만들 수 있다는 더 심각한 문제를 야기합니다. 만약 LLM이 JSON 응답을 생성하는 도중에 토큰 제한에 걸려 출력이 갑자기 중간에 끊겨버리면(Abruptly cut off), 마지막에 필요한 닫는 중괄호(}
)나 대괄호(]
)가 누락되는 등 유효하지 않은(Invalid) JSON이 생성될 가능성이 매우 높습니다. 이렇게 깨진 JSON은 후속 프로그램에서 파싱할 수 없으므로 사실상 쓸모없는 결과물이 되어버립니다.
그렇다면 이렇게 잘려나가거나 형식이 망가진 LLM 생성 JSON을 어떻게 처리해야 할까요? 다행히도 이 문제를 해결하는 데 도움을 줄 수 있는 도구들이 있습니다. 예를 들어, ‘json-repair’ 와 같은 라이브러리(Python의 PyPI 등에서 찾을 수 있음)는 이렇게 불완전하거나 형식이 잘못된(Incomplete or Malformed) JSON 객체를 입력받아, 지능적으로 누락된 괄호를 추가하거나 구조를 수정하여 유효한 JSON으로 자동 복구하려고 시도합니다. [참고: json-repair library]
따라서 LLM으로부터 JSON 형식의 출력을 받아야 하는데 토큰 제한으로 인한 잘림(Truncation) 문제가 자주 발생한다면, 이러한 JSON 복구 도구를 활용하는 것을 적극적으로 고려해볼 수 있습니다. 이는 LLM 생성 결과의 활용도를 높이는 데 중요한 동반자가 될 수 있습니다.
지금까지 우리는 주로 LLM의 '출력(Output)'을 구조화하는 방법(예: JSON 사용)에 대해 이야기했습니다. 그런데 반대로 LLM에게 제공하는 ‘입력(Input)’ 데이터를 구조화하는 것 역시 매우 효과적인 프롬프트 엔지니어링 전략이 될 수 있다는 사실을 아시나요? 특히 **JSON 스키마(JSON Schemas)**를 활용하는 것이 좋은 방법입니다.
JSON 스키마란, 특정 JSON 데이터가 가져야 할 기대되는 구조(Expected Structure)와 각 필드의 데이터 타입(Data Types), 그리고 형식(Formats) 등을 미리 정의해 놓은 명세서와 같습니다. LLM에게 단순히 자유 형식의 텍스트(Free-form Text)로 정보를 제공하는 대신, 이 JSON 스키마와 함께 스키마를 따르는 구조화된 JSON 데이터를 입력으로 제공하면 어떤 이점이 있을까요?
첫째, 스키마를 제공함으로써 우리는 LLM에게 **기대하는 입력 데이터의 명확한 청사진(Clear Blueprint)**을 제시할 수 있습니다. 이를 통해 LLM은 **주어진 데이터에서 어떤 정보가 중요하고 관련성이 높은지에 더 쉽게 집중(Focus its attention)**할 수 있으며, 입력 데이터를 잘못 해석할 위험(Risk of misinterpreting)을 크게 줄일 수 있습니다.
둘째, 스키마는 **데이터의 서로 다른 부분들 간의 관계(Relationships between different pieces of data)**를 명확하게 설정하는 데 도움을 줄 수 있습니다. 예를 들어, 어떤 필드가 다른 필드의 하위 요소인지, 혹은 어떤 필드가 배열 형태를 가져야 하는지 등을 정의할 수 있습니다.
셋째, 스키마는 특정 형식(Specific Formats)을 가진 날짜(Date)나 타임스탬프(Timestamp) 필드를 포함시킴으로써 LLM이 시간 정보(Time-aware)를 더 잘 인식하고 처리하도록 만들 수도 있습니다.
예를 들어, 당신이 전자상거래 카탈로그에 있는 상품들에 대한 설명을 LLM을 이용하여 생성하고 싶다고 가정해 봅시다. 단순히 상품에 대한 자유 형식의 텍스트 설명만 제공하는 대신, 다음과 같이 JSON 스키마를 먼저 정의하고, 그 스키마에 맞는 실제 상품 데이터를 JSON 객체로 제공할 수 있습니다.
[Snippet 5 예시: 상품 정보 입력을 위한 JSON 스키마 정의]
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Product name" },
"category": { "type": "string", "description": "Product category" },
"price": { "type": "number", "format": "float", "description": "Product price" },
"features": {
"type": "array",
"items": { "type": "string" },
"description": "Key features of the product"
},
"release_date": { "type": "string", "format": "date", "description": "Date the product was released"}
}
}
[Snippet 6 예시: 위 스키마에 따른 실제 상품 데이터 (JSON 객체)]
{
"name": "Wireless Headphones",
"category": "Electronics",
"price": 99.99,
"features": ["Noise cancellation", "Bluetooth 5.0", "20-hour battery life"],
"release_date": "2023-10-27"
}
이렇게 입력 데이터를 사전에 처리(Preprocessing)하여 스키마와 함께 구조화된 형태로 제공하면, LLM은 각 속성(Attribute)의 의미(예: release_date
필드는 상품 출시일)를 더 명확하게 이해하게 됩니다. 그 결과, 단순히 긴 문서 전체를 던져주는 것보다 훨씬 더 정확하고 관련성 높은 상품 설명을 생성할 가능성이 높아집니다. 이러한 구조화된 입력 방식은 특히 대량의 데이터를 처리해야 하거나, 복잡한 애플리케이션에 LLM을 통합해야 할 때 그 가치가 더욱 빛을 발합니다.
프롬프트 엔지니어링은 때로는 외롭고 답이 잘 보이지 않는 과정일 수 있습니다. 특히 까다로운 문제에 부딪혔거나 창의적인 돌파구가 필요할 때, 혼자서만 끙끙 앓다 보면 쉽게 지치거나 좋은 아이디어를 놓칠 수 있습니다.
이럴 때 당신이 시도해볼 수 있는 좋은 전략 중 하나는 바로 '다른 프롬프트 엔지니어들과 함께 실험(Experiment together with other prompt engineers)'하는 것입니다. 만약 당신이 팀의 일원이거나 동료들과 함께 작업하는 상황이라면, 동일한 목표를 가진 프롬프트를 여러 사람이 각자의 방식으로 한번 만들어보도록 시도해 보십시오.
놀랍게도, 비록 모두가 앞에서 다룬 모범 사례들을 충실히 따랐다고 하더라도, 각자가 만든 프롬프트들의 성능(Performance)에는 상당한 편차(Variance)가 나타나는 것을 발견하게 될 가능성이 높습니다. 어떤 사람은 특정 단어 선택이나 구조가 더 좋은 결과를 낳고, 다른 사람은 전혀 다른 접근 방식이 더 효과적일 수 있습니다.
이렇게 여러 사람의 다양한 시도와 그 결과들을 비교하고 분석하는 과정을 통해, 우리는 혼자서는 생각하지 못했던 새로운 아이디어나 더 효과적인 프롬프트 작성 패턴을 발견할 수 있습니다. 또한, 서로의 성공과 실패 경험을 공유함으로써 집단적인 학습 효과를 얻고 전체 팀의 프롬프트 엔지니어링 역량을 함께 향상시킬 수 있습니다. 집단 지성의 힘을 적극적으로 활용하십시오!
앞서 ‘사고의 연쇄(Chain of Thought, CoT)’ 프롬프팅 기법이 LLM의 추론 능력을 향상시키는 데 매우 효과적이라고 설명했습니다. 하지만 CoT를 사용할 때 몇 가지 모범 사례를 따르면 그 효과를 더욱 극대화하고 안정적인 결과를 얻는 데 도움이 될 수 있습니다.
첫째, 최종 답변은 반드시 추론 과정 뒤에 위치시켜야 합니다. 왜냐하면 LLM이 중간 추론 과정을 생성하는 것 자체가 다음에 예측될 토큰들에 영향을 미치기 때문입니다. 즉, 추론 과정을 거치면서 모델의 '생각’이 정리되고, 그 결과로 더 정확한 최종 답변을 예측할 가능성이 높아집니다. 따라서 프롬프트 설계 시, “단계별로 생각한 다음, 최종 답을 제시해줘” 와 같은 구조를 따르는 것이 좋습니다.
둘째, CoT와 자기 일관성(Self-consistency) 기법을 함께 사용할 경우, 생성된 여러 응답들에서 최종 답변 부분만을 정확하게 추출하여 다수결 투표를 할 수 있도록 프롬프트를 설계하거나 후처리 로직을 구현해야 합니다. 추론 과정과 최종 답변이 명확하게 구분되도록 유도하는 것이 중요합니다.
셋째, CoT 프롬프팅을 사용할 때는 일반적으로 Temperature 설정을 0으로 하는 것이 권장됩니다. 그 이유는 CoT가 주로 사용되는 추론 작업(예: 수학 문제 풀이, 논리 퍼즐)은 대부분 **단 하나의 정답(Single Correct Answer)**을 가지는 경우가 많기 때문입니다. Temperature를 0으로 설정하면 모델은 가장 확률 높은 추론 경로와 답변을 결정론적으로 생성하게 되어 결과의 일관성과 정확성을 높일 수 있습니다. (단, 자기 일관성을 위해 다양한 추론 경로를 샘플링해야 하는 경우에는 Temperature를 0보다 큰 값으로 설정해야 합니다.) CoT는 기본적으로 탐욕적 디코딩(Greedy Decoding), 즉 각 단계에서 가장 확률 높은 다음 단어를 예측하는 방식에 기반하므로, 정답이 명확한 추론 문제에서는 Temperature 0 설정이 가장 적합합니다.
이 모범 사례 섹션에서, 그리고 이 문서 전체에서 여러 번 강조했지만, 그 중요성은 아무리 강조해도 지나치지 않습니다. 바로 **당신이 시도하는 다양한 프롬프트들에 대한 상세한 기록을 반드시 문서화(Document your prompt attempts in full detail)**해야 한다는 것입니다.
왜 이것이 그렇게 중요할까요? 프롬프트 엔지니어링은 본질적으로 반복적인 실험과 학습의 과정입니다. 당신은 아마도 원하는 최적의 결과를 얻기까지 수많은 버전의 프롬프트를 만들고, 수정하고, 테스트하게 될 것입니다. 이 과정에서 **어떤 시도가 성공적이었고(What went well), 어떤 시도가 실패했으며 그 이유는 무엇인지(What did not)**를 상세하게 기록해두지 않는다면, 당신은 시간이 지남에 따라 귀중한 교훈들을 잊어버리게 되고, 과거에 이미 했던 실수를 반복하거나 비효율적인 시도를 계속하게 될 위험이 있습니다.
프롬프트의 출력 결과는 다양한 요인에 따라 달라질 수 있습니다. 당신이 **사용한 특정 LLM 모델(예: gemini-pro vs gemini-ultra), 모델의 버전(Version), 그리고 설정한 샘플링 값(Temperature, Top-K, Top-P 등)**에 따라서도 결과는 크게 달라질 수 있습니다. 심지어 동일한 모델과 설정을 사용하더라도, 프롬프트 내의 미묘한 문장 구조나 단어 선택의 차이가 예상치 못한 결과 차이를 만들어낼 수도 있습니다. (예를 들어, 앞서 언급했듯이 만약 두 개의 토큰이 동일한 최고 예측 확률을 가질 경우, 동점 처리 방식에 따라 무작위로 선택될 수 있으며, 이 작은 차이가 이후 생성되는 토큰들에 연쇄적인 영향을 미칠 수 있습니다.)
따라서, 이러한 모든 변수들과 그에 따른 결과들을 체계적으로 기록하고 관리하는 것이 매우 중요합니다. 구글 시트(Google Sheet)와 같은 도구를 활용하여 아래 [표 21]과 같은 템플릿을 만들어 사용하는 것을 강력히 추천합니다.
[표 21 예시: 프롬프트 시도 기록을 위한 템플릿]
필드 | 설명 | 예시 |
---|---|---|
Name | 프롬프트의 이름 및 버전 (식별 가능하게) | 2_3_email_classification_CoT_v2 |
Goal | 이 시도의 구체적인 목표 (한 문장으로 요약) | CoT를 사용하여 이메일 분류 정확도 향상 시도 |
Model | 사용한 LLM 모델의 이름 및 버전 | gemini-pro-1.0 |
Temperature | 설정한 Temperature 값 (0 ~ 1 사이) | 0.7 |
Token Limit | 설정한 최대 출력 토큰 수 | 512 |
Top-K | 설정한 Top-K 값 (숫자) | 40 |
Top-P | 설정한 Top-P 값 (숫자) | 0.95 |
Prompt | 사용한 전체 프롬프트 텍스트 (수정 내용 포함) | (프롬프트 전체 내용 복사) |
Output | LLM이 생성한 실제 출력 결과 (하나 또는 여러 개) | (출력 결과 복사) |
Result | 결과 평가 (OK / NOT OK / SOMETIMES OK 등) | SOMETIMES OK |
Feedback | 결과에 대한 분석 및 피드백 (성공/실패 원인, 개선 아이디어 등) | Temperature가 너무 높아 가끔 엉뚱한 답변 생성. 0.3으로 낮춰서 재시도 필요. |
Link (Optional) | Vertex AI Studio 등에 저장된 프롬프트 링크 (해당되는 경우) | (저장된 프롬프트 URL) |
이런 식으로 모든 시도를 꼼꼼하게 기록해두면, 나중에 프롬프팅 작업을 다시 시작해야 할 때(잠깐만 쉬어도 놀랄 만큼 많은 것을 잊어버릴 수 있습니다!), 혹은 다른 버전의 모델에서 프롬프트 성능을 테스트해야 할 때, 혹은 미래에 발생할 수 있는 오류를 디버깅해야 할 때 매우 귀중한 참고 자료가 될 것입니다. 특히 Vertex AI Studio와 같은 도구를 사용한다면, 저장된 프롬프트에 대한 하이퍼링크까지 함께 기록해두면 클릭 한 번으로 언제든 해당 프롬프트를 다시 실행해볼 수 있어 매우 편리합니다.
만약 당신이 검색 증강 생성(Retrieval Augmented Generation, RAG) 시스템과 같이 외부 정보를 검색하여 프롬프트에 함께 넣어주는 시스템을 개발하고 있다면, 단순히 프롬프트 자체뿐만 아니라 RAG 시스템의 특정 요소들이 프롬프트 내용과 결과에 어떤 영향을 미쳤는지에 대한 정보(예: 사용된 검색 쿼리, 청크(Chunk) 설정, 검색된 청크 내용 등)도 함께 기록해두는 것이 중요합니다.
이렇게 체계적인 문서화 습관은 당신이 시간이 지남에 따라 무엇이 효과가 있었고 무엇이 그렇지 않았는지를 명확하게 학습하고, 당신의 프롬프트 엔지니어링 기술을 지속적으로 발전시키는 데 결정적인 역할을 할 것입니다. 귀찮다고 생각하지 마십시오. 이것이야말로 진정한 프로가 되기 위한 필수적인 과정입니다.
프롬프트 엔지니어링은 본질적으로 반복적인 과정(Iterative Process)입니다. 다양한 프롬프트를 만들고(Craft) 테스트하며(Test), 그 결과를 분석하고(Analyze), 개선점을 찾아 다시 시도하며(Refine), 이 모든 과정을 꼼꼼하게 문서화(Document)해야 합니다. 모델의 성능 변화에 맞춰 프롬프트를 끊임없이 다듬고, 새로운 모델이나 설정값이 나오면 이전에 성공했던 프롬프트들을 가지고 다시 실험하는 것을 두려워하지 마십시오. 이러한 지속적인 실험과 개선의 노력을 통해 당신은 마침내 원하는 결과물을 안정적으로 얻어낼 수 있는 최적의 프롬프트를 발견하게 될 것입니다.
자, 지금까지 우리는 대규모 언어 모델(LLM)의 잠재력을 최대한으로 끌어내기 위한 핵심 기술, 바로 '프롬프트 엔지니어링’의 세계를 함께 탐험했습니다. 이 여정을 통해 우리는 다음과 같은 다양한 프롬프팅 기법들과 중요한 원칙들을 배우고 이해했습니다.
심지어 우리는 **자동 프롬프트 엔지니어링(APE)**을 통해 프롬프트 작성 과정 자체를 자동화하는 방법까지 살펴보았습니다.
이 백서는 또한 프롬프트가 충분하지 않을 때 발생할 수 있는 문제점들과 같은 생성형 AI(Gen AI)의 도전 과제들에 대해서도 논의했습니다. 그리고 마지막으로, 우리가 더 나은 프롬프트 엔지니어가 되기 위해 반드시 기억하고 실천해야 할 핵심적인 모범 사례들을 공유했습니다.
이 모든 지식과 기법들을 바탕으로, 이제 당신은 LLM이라는 강력한 도구를 훨씬 더 효과적이고 창의적으로 활용할 준비가 되었습니다. 프롬프트 엔지니어링은 단순히 기술적인 능력을 넘어, 명확하게 소통하고, 논리적으로 사고하며, 끊임없이 실험하고 배우려는 탐구적인 자세를 요구하는 분야입니다. 이 여정을 꾸준히 계속해 나간다면, 당신은 분명 LLM의 무한한 가능성을 현실로 만들고 당신의 분야에서 놀라운 성과를 창출하는 진정한 '프롬프트 마스터’로 거듭날 수 있을 것입니다.