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

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

 

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

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

 

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

 

프로그래밍 언어들은 텍스트는 문자열로 작성하고, 변경 부분은 변수로 작성한다. 변수를 변경 부분 구분자 안에 써주면 되겠다.  예를 들어 변수가 name이라면 ‘{{name}}’으로 작성하면 되겠다.

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

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

 

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

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

 

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

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

 

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

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

 

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

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

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

[설계]

시맨틱 커널의 설계 결정을 대부분 따른다. 그렇지 않은 부분은 {다른 설계 결정}과 같이 표현한다.

 

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

변경되는 부분은 작성된 텍스트가 그대로 사용되니까 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 부분은 코어 중의 코어다. 이 부분을 완전 독립적인 부분으로 분리한다. 의존 되는 부분들은 인터페이스 의존 수준으로 바꾼다. 여기서 의존하는 클래스나 인터페이스는 분리된 프로젝트로 만들어 관리한다.

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

Leave a Reply

*