AISmarteasy – 시맨틱 커널 포크로 개발하기 2 – PromptTemplate

llm 연동 ai 애플리케이션은 llm과 연동이 핵심이고, llm 연동의 유일한 입력은 프롬프트.

프롬프트를 잘 작성해야 하는 것은 llm 연동해서 뭘 하려면 중요하다. llm에게 왜, 뭘 지시 하려는 지, llm이 지시를 해 낼 때 고려해야 할 건 뭔지, 고려해야 할 절차가 있거나 참고할 것이 있거나 좋은 예시가 있으면  이것도 언급한다. llm 응답에 원하는 형식이 있으면 이것도 알려준다. 가능한 구체적으로 명확하게. 더 작성하고 싶다면 prompt engineering으로.

 

프롬프트를 작성하다 보면 특정 부분만 달라지고 나머지 부분이 같은 프롬프트들이 등장한다. 프롬프트 템플릿을 만들고 싶어진다.

* 일부는 고정되고 일부는 변경되어 전체를 완성하는 개념을 템플릿이라고 한다. 템플릿에서 변경되는 부분을 placeholder 또는 hook이라고 한다.

 

템플릿에서 가장  중요하게 다뤄야 하는 부분은 변경 가능한 부분을 그렇지 않은 부분과 구분하는 것이다. llm이 언어 모델이니 프롬프트는 텍스트로 작성된다. 시맨틱 커널에서는 변경 부분을 구분하는 구분자로 ‘{{‘ ‘}}‘를 사용한다.

 

C# 에서는 텍스트는 문자열로 작성하고, 변경 부분은 변수로 작성한다. 변수를 변경 부분 구분자 안에 써주면 되겠다.  예를 들어 변수가 name이라면 ‘{{name}}’으로 작성하면 되겠다. 공백은 무시되므로 더 읽기 쉽게 하려면, ‘{{ $name }}’과 같이 작성해도 된다.

변경 부분 텍스트가 정해지는데 좀 복잡한 과정이 요구된다면, 함수 호출로 만들어 함수 호출 결과로 변경 부분이 결정되도록 한다.

함수 호출에는 인자가 있을 수 있다. 시맨틱 커널에서는 함수 호출 시 인자가 하나 인 경우는 변수 값을 사용하거나 직접 값을  작성할 수 있다. 인자가 둘 이상이면, 두 번째 부터는 NamedArg를 사용한다. NamedArg는 “{변수 이름} = {값}”과 같이 이름을 갖는 인자다. NamedArg의 값 부분은 함수 호출 첫 번째 인자처럼 변수 값을 사용하거나 직접 작성한 값을 사용할 수 있다.

 

변경 부분의 값은 변수 값이나 값이나 함수 호출 결과가 될 수 있다. 구분이 되어야 한다. 변수 가 많이 사용 될 거니 변수 구분자를 사용하자. 변수 이름 앞에 ‘$‘를 쓴다. 예를 들어 변수가 name이라면 ‘{{$name}}’와 같이 작성한다.

값은 ” “ 나 ‘ 안에 작성한다. C#이 문자열을 작은 따옴표나 큰 따옴표를 사용해서 작성하기 때문에, 두 가지를 경우에 따라 작성하면 된다.

 

시맨틱 커널은  객체지향 언어인 C#을 주 프로그래밍 언어로 생각했을 것이기 때문에 ‘객체.메소드 호출’ 과 같은 형태를 도입하고 있다. 이렇게 하려면 클래스도 등장해야 한다. 시맨틱 커널에서는 클래스라고 하지 않고 함수들의 묶음으로 네임스페이스가 되는 플러그인이라는 개념을 등장 시키고 있다. 함수 호출 부분은 “{{플러그인.함수 호출}}“와 같이 작성한다. 플러그인은 여러 함수들을 묶는 그룹, 네임스페이스 역할을 한다.

예를 들어 특정 지역 날씨를 알려주는 함수가 weather 플러그인의 getForecast이고, Schio라는 지역을 인자로 넘겨 호출한다면, ‘{{weather.getForecast “Schio”}}‘와 같이 작성한다. 직접 작성한 값 Schio를 사용하지 않고 city라는 변수를 사용하면 더 많은 경우를 다룰 수 있다.

시맨틱 커널에서 input 변수는 함수 호출 시 자동으로 설정된다.

예를 들어, “The weather today is {{weather.getForecast}}.”는 “The weather today is {{weather.getForecast $Input}}.”와 같다. 

 

프롬프트 템플릿 작성에 사용되는 구분자를 프롬프트에 사용하고자 할 수도 있다.

  • “{{“나 “}}”를 작성해야 하면, {{“{{” }}와 {{ “}}” }}와 같이 작성한다.
    • {{ “{{” }} and {{ “}}” }} are special SK sequences. => {{ and }} are special SK sequences.

 

C#에서 문자열은 ” “나 ‘ ‘로 작성한다. 대부분 큰 따옴표를 사용하는데, 큰 따옴표가 포함되어야 한다면 작은 따옴표(‘ ‘)를 사용한다. \’와 같이 \뒤에 작성해도 된다. \를 특수한 방법으로 사용하고 있기 때문에 ‘\’를 포함하고자 할 때도 고려해야 한다. \뒤에 작성하도록 하고 있다. 문자열을 다루는 데는 여러가지 방법이 있을 수 있으니 프로그래밍 언어가 지원하는 방법을 잘 사용하면 된다.

  •  {{ “two special chars \\\’ here”}} => two special chars \’ here

이러한 작성 규칙을 BNF로 작성해보자.

 

[설계]

  • 요약 에서 한글로 작성된 내용을 한글로 요약해야 하는 경우와 같이, 언어를 맞춰줘야 하는 것은 프롬프트도 인코딩 문제가 안 생기도록 새로운 텍스트 파일을 추가하고, 한글로 작성한다. 한글로 작성한 프롬프트 파일은 skprompt.txt로, 영어로 작성된 것은 skprompt_en.txt로 한다.
  • 시맨틱 커널의 설계 결정을 대부분 따른다. 그렇지 않은 부분은 {다른 설계 결정}과 같이 표현한다.

 

프롬프트 템플릿은 먼저 변경되지 않는 부분과 변경 되는 부분으로 구분한다.

변경되는 부분은 작성된 텍스트가 그대로 사용되니까 TextBlock이라고 하고, 변경 되는 부분은 PlaceHolderBlock라고 하자. 시맨틱 커널은CodeBlock이라고 한다.

TextBlock에서 좀 주의를 기울여야 하는 곳은 ‘\’가 등장하는 부분이다.

PlaceHolderBlock은 좀 더 세분화된 부분들로 구분한다. 함수 이름 부분과 인자로 작성되는 부분이 구분 된다. FunctionId, VariableBlock, ValueBlock, NamedArgBlock

 

프롬프트 템플릿 문자들을 읽어가면서 두 부분으로 구분해야 한다. 이를 Tokenize 한다고 한다. PromptTemplateTokenizer가 필요하다.

PlaceHolderBlock 은 다시 Tokenize 해야 한다. 이를 위해 PlaceHolderTokenizer를 둔다.

 

{다른 설계 결정} Tokenize하고 PlaceHolderBlock이 정해지면 프롬프트 템플릿은 프롬프트가 된다. 시맨틱 커널은 이것을 하는 역할을 PromptTemplateEngine이라고 하고 있는데, 프롬프트를 만드는 것은 PromptTemplate이 할 일이니 PromptTemplate 정도면 된다.

그리고 이것은 프롬프트 부분보다는 커널 부분에 더 가깝다. AISmarteasy.Core.Kernel 프로젝트로 옮긴다.

 

{다른 설계 결정} 함수 호출 부분은 함수 실행이 안 되면 독립적으로 테스트하기 어렵다. 이를 위해 함수 실행을 할 것인지 그렇지 않을 것 인지에 따라 결과가 달라지도록 해야 한다. 함수 호출이 안 되는 경우, 함수 호출 부분은 템플릿 프롬프트 내용대로 렌더링 되도록 한다.

예를 들어, “The weather today in {{$city}} is {{weather.getForecast $city}}.”에서 city가 Schio일 때 프롬프트는 “The weather today in Schio is {{weather.getForecast $city}}.”와 같이 렌더링 된다.

 

[NamedArg]

인자 전달 시  매개변수 위치 보다 이름을 직접 사용할 수 있다. 이렇게 전달되는 인자를 NamedArg라 한다.

코드 블록은 NamedArg를 반영해 VariableBlock, ValueBlock, NamedArgBlock으로 구성 된다.

이름 지정된 인자를 작성하고자 하는 경우는 {매개변수이름} = {값}으로 작성한다. 변수 값을 사용하는 경우 변수 작성 방법에 따라 값  부분을 $변수명으로 작성하면 된다.

이에 대해서는 BNF가 반영하지 않고 있다. 이 부분은 소스 코드를 보면 더 정확하게 알 수 있다.

NamedArg를 포함해서 다루는 것은 바로 동의가 안 된다 . 구현 복잡도가 높아진다.  변수로도 가능한데.

추후 설계 결정에서 NamedArg를 없애고. 인자는 모두 변수로 전달되는 것으로.

 

[프로젝트 분리]

prompt template 부분은 코어 중의 코어다. 이 부분을 완전 독립적인 부분으로 분리한다. 의존 되는 부분들은 인터페이스 의존 수준으로 바꾼다. 여기서 의존하는 클래스나 인터페이스는 분리된 프로젝트로 만들어 관리한다.

[ 프롬프트 설계 ]

llm 연동 ai 애플리케이션에서 프롬프트는 가장 중요한 역할이다. 기존 소프트웨어 설계에서는 요구사항으로 다뤄질 대상이지만 llm 연동 ai 애플리케이션에서는 가장 중요하게 설계되어야 하는 대상이다. 이게 제대로 안 되면 개발된 애플리케이션이 잘 돌아가도 고객이 원하는 결과를 주지 못할 수 있다. 개발자(사)는 애플리케이션 개발에서 분리해서 다루고 싶을 수 있지만, 그렇게 하는 건 기존 소프트웨어 개발에서 분석 설계는 안 하고 구현만 하겠다는 것과 비슷하다. 기존에는 그렇게 해도 경쟁력이 있을 수 있었지만, llm 연동 ai 애플리케이션은 요구사항과 구현이 대부분 비슷하기 때문에 그렇게 하는 것은 경쟁력도 떨어지고, 가장 중요한 프롬프트가 반영되지 않는 ai 애플리케이션, 누구나 가져다 써서 쉽게 만들 수 있는 부분만 다루는 개발자(사)가 될 수 있다.

 

[ 프롬프트 특성 반영 구조 설계 필요 ]

프롬프트 설계 영역이 프롬프트 내용 설계와 프롬프트 구조 설계로 구분될 필요가 있다.

지금은 ChatGPT와 같이 llm 벤더가 llm 연동 ai 애플리케이션을 공급하고 있기 때문에, 프롬프트 내용 설계에만 초점을 맞추고 있다.

여러 도메인에서 기업들이 자신의 llm 연동 ai 애플리케이션을 만든다면, 프롬프트 특성이 반영된 소프트웨어 구조 설계가 필요해 질 거다. 소프트웨어 공학에서 이 부분을 다뤄야 한다.

 

[ 프롬프트 내용 설계  – prompt engineering ]
프롬프트는 자연어로 작성되는, llm에게 전달되는, 질의나 지시이다. 자연어로 작성 되니 사용되는 단어나 심볼이 중요하다. 원하는 형식이 있다면 포맷도 중요하다.

프롬프트는 llm에게 전달되고, llm의 특성에 따라 응답하기 때문에, 프롬프트 내용 설계자는 llm을 잘 동작하게 하려면, 어떻게 해야 하는 지를 잘 알아야 한다. 어떻게에 대한 답은 프롬프트로 작성된다. prompt engineering에서는 이 주제를 다룬다.

프롬프트 설계 결과 테스트는 아직 소프트웨어 개발에 테스트 환경 만큼 자동화를 지원하지는 않는다. 실제 해 보면서 결과를 살펴보는 시행착오 과정이 필요하다. llm 서비스 벤더들은 프롬프트 가지고 테스트해 볼 수 있는 환경을 playgroud로 제공한다.

 

llm 연동 ai 애플리케이션 개발자는 소프트웨어 공학에 더해 prompt engineering도 알아야 한다. Prompt Engineering Guide

About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*