프롬프트 엔지니어링
올해 AI/LLM 공부를 시작하는 겸해서 프롬프트 엔지니어링도 공부를 잠깐 해보았다. 어차피 나는 AI는 연구보다는 활용과 응용에 더 흥미를 느꼈기 때문에 머신러닝이나 딥러닝 같은 기술적인 부분에 관심은 딱히 가지 않았다. 응용 소프트웨어 관점에서도 보더라도 LLM 애플리케이션을 만들기 위해 랭체인(LangChain)을 바로 시도해 보기 보다도, 먼저 AI 활용에 대한 리터러시나, 프롬프트 엔지니어링도 AI와 놀기 위한 하나의 방법 중 하나라 생각해 시도해 보기로 한 것이다. ChatGPT 이후, 프롬프트 엔지니어링의 경우에는 프롬프트 엔지니어라는 새로운 직업이 탄생하기도 한 만큼, 이 분야가 어떤 분야인지 살펴보는 것도 좋다는 생각이 들었다.
프롬프트 엔지니어링에 대해 이야기해보자면, 프롬프트 엔지니어링은 'AI가 사용자가 원하는 대답을 할 수 있도록 언어 모델을 훈련시키는 것'이라고 볼 수 있다. 프롬프트는 누구나 작성할 수 있어서 생각보다 쉬워 보일 수는 있겠으나, OpenAI와 같은 개발사들 조차 자신들의 AI가 어떤 대답을 할지 정확히 예측하지는 못하기 때문에 내가 한 질문에 원하는 대답을 AI가 주리라는 보장은 없어서 이를 해결하기 위한 훈련 방법이 필요하다. 이를 위해 등장한 것이 프롬프트 엔지니어링이다.
프롬프트 엔지니어링에는 AI의 답변을 이끌어내는 방법이나 프롬프트에 포함되어야 할 요소가 포함되어 있으며 기본적인 요소로는 지시, 입력 데이터/형식/예시, 맥락 정보, 조건, 출력 형식/예시, 템플릿, 치환자가 포함될 수 있으며 여기에 캐릭터를 부여하여 성격, 어조와 같은 목소리를 지정할 수도 있다. AI의 대답을 이끌어내는 방법도 e2e, 생각의 사슬(CoT; Chain of Thought), 제로샷(Zero-Shot), 퓨샷(Few-Shot)과 같은 다양한 방법이 존재한다.
코드 기반 프롬프트
문제는 위 방법들의 경우 프롬프트 엔지니어링을 따로 공부하지 않는 한 자연스럽게 구현하기 어렵다는 점이다. 그래서 문득 떠오른 아이디어가 코드 기반 프롬프트(Code-based Prompt)다. 코드 기반 프롬프트는 이미 세상에 존재하는 아이디어지만, 연구할만한 가치가 있는 것 같아 약 2주 정도 재미삼아 해보았다.
코드 기반 프롬프트를 사용하면 프롬프트 엔지니어링의 기법들을 자연스럽게 활용할 수 있을 뿐만 아니라 프롬프트를 테스팅하거나 버전을 관리할 수도 있다. LLM(Large Language Model)이 코드를 해석하고 시뮬레이션할 수 있다는 능력에서 착안한 것으로, 이미 존재하는 전통적인 프로그래밍 언어뿐만 아니라 '문법을 사용자 정의'해서 만들어내고, 이를 사용하여 AI에게 지시를 내릴 수 있다는 것이 핵심적인 아이디어다.
코드 기반 프롬프트는 다른 게 아니라 단순하게 자연어 대신 코드를 프롬프트로 주는 것이 전부다. 따라서 이제부터 나올 예제를 실행하기 위해서는 ChatGPT 와 같이 언어 모델에 명령을 내릴 수 있는 서비스만 있으면 된다. 또한 특정 프로그래밍 언어에 종속되지 아니하고 의사코드로써 작성할 수 있다. 따라서 문법은 사용자 정의가 가능하다. 코드에 익숙하지 않은 사용자라면 생소할 수 있겠지만, 순차적이고 복잡한 작업에 있어서 코드를 프롬프트로 제공하는 것은 많은 이점을 제공한다. 간단한 예를 살펴보자. 다음과 같은 자연어 명령은 코드로 대신 표현이 가능하다.
* 테스트를 진행한 서비스는 ChatGPT 이며 모델은 GPT4o 이다.
서로 중복되지 않는 임의의 이메일 3개를 생성해 주세요. 예시는 user@example.com 이고, <username>@<domain> 형식을 따라야 합니다. 출력 형식은 JSON 포맷으로 작성되어야 하며 username, domain 을 키(Key)로 가지고 있어야 합니다.
위 자연어 명령에 따르면 '지시'는 프롬프트 분류 카테고리로 따져보자면 '생성'이고, '조건'에 해당하는 것은 '이메일은 서로 중복되지 않아야 한다.'이다. 값을 생성할 때는 예시, 템플릿, 조건, 출력형식을 참고해야 한다. 이를 코드 기반 프롬프트로 바꿔보면 '생성'은 함수로, 예시, 템플릿, 조건 등은 파라미터(Parameter)로 받는 것이 가능하다. 출력 형식은 기본 자료형 또는 구조체(Struct)로 표현한다. 따라서 이메일뿐만 아니라 다양한 데이터를 생성하기 위한 범용 함수를 만들어볼 수 있다. 이때, 함수의 구현부는 필요하지 않다.
* 문법은 지극히 개인취향으로, Go, Python을 섞어서 구성하였다.
* 코드 기반 프롬프트를 입력하기 전에 "의사코드 형태의 코드 기반 프롬프트를 줄 테니 실행하고 출력결과를 달라"라고 지시해야 한다. 그렇지 않으면 다른 프로그래밍 언어로 착각하여 문법을 엄격하게 검사할 수 있기 때문이다. 이 요청은 한 번이면 되고, 이미 학습된 경우에는 필요하지 않다.
"""Generate generates values
Notes:
- The method and criteria for internally generating data are delegated to LLM
Returns:
Generated values
Errors:
- If the value cannot be generated
- If template is not valid
"""
func Generate[R any](examples []any, template string, contexts []string) R
작성한 Generate()
를 살펴보면, 이 함수는 데이터를 '생성'하기 위한 범용 함수이며 파라미터로 예시(examples), 템플릿(template), 맥락(contexts)정보를 받고 생성이 실패한 경우 에러를 출력한다. 성공하면 호출 시점에서 설정된 반환 타입에 따라 값의 형태(R)가 결정된다. 함수의 주석에서는 LLM이 함수의 기능을 추론하기 위해 참고해야 하는 정보가 적혀있는데, 데이터를 생성은 LLM에게 위임하고, 에러가 발생하는 상황이 명시되어 있다.
이제 Generate()
함수를 사용해서 예시, 템플릿, 조건, 출력형식을 파라미터로 제공하고 '임의의 이메일 주소 3개'를 만들어보면 다음과 같다. 데이터를 생성하고 그 반환 값으로 Email
구조체에 부합하도록 명시 한 뒤 출력하면 우리가 원하는 형태로 출력해 준다. 미리 정의하지 않은 함수는 LLM이 코드 문맥을 보고 추론할 수도 있다.
// Print outputs values in a specific format
func Print(value any, outputFormat = "PlainText")
type Email struct {
username string
domain string
}
emails [3]Email = Generate(
examples={
"user@example.com"
},
template="<username>@<domain>",
contexts={
"Ensure generated emails do not duplicate existing ones"
}
)
Print(emails, outputFormat="JSON")
LLM은 위 코드 마주했을 때 다음과 같이 추론할 수 있다.
Generate()
를 사용해서 값을 생성하되, 예시(examples), 템플릿(template), 맥락(contexts)을 참고한다.Generate()
의 시그니처에서 반환 타입이 제네릭(Generics)으로R
로 정의되어 있지만, 호출 시점에서 반환 타입이Email
로 명시되어 있으므로 반환 타입을Email
로 간주한다.Email
타입에는username
,domain
필드가 존재하고, 템플릿에<username>
,<domain>
이 포함되어 있으므로 각 필드에 값을 매핑해야 한다.[3]Email
과 같은 정적 배열타입으로 보아하니, 3개의 이메일을 생성해야 한다.- 생성된 이메일 목록을 출력하되, 그 형식은
JSON
으로 한다.
출력 결과는 다음과 같다.
[
{
"username": "alice123",
"domain": "testmail.com"
},
{
"username": "random_name",
"domain": "mailserver.net"
},
{
"username": "guest",
"domain": "testmail.com"
}
]
프롬프트 길이의 측면으로 볼 때, 자연어 명령보다 길고 복잡해보이는데요? 라는 생각이 들 수도 있는데, 실제로 코드 기반 프롬프트가 자연어 명령보다 길고 복잡하다. 하지만 자연어 프롬프트를 관리할 때 발생하는 문제점을 해결할 수 있다. 코드 기반 프롬프트를 사용하면 유지보수 차원에서 다음과 같은 일을 할 수 있다.
- VCS(Version Control System)으로 버전관리가 가능하다.
- 유닛/프롬프트 테스트를 할 수 있다.
- 서브모듈로 모듈화하여 종속성으로 관리될 수 있다.
자연어 vs 코드 기반 프롬프트
코드 기반 프롬프트는 코딩의 가장 기본적인 개념인 자연어를 코드로 표현하는 것의 결과물인 의사코드를 프롬프트로 제공하는 것이 핵심이며 함수의 구현은 필요하지 않고 오직 주석과 함수 시그니처만으로 기능을 정의하고 LLM에게 지시할 수 있는, 자연어와 프로그래밍 언어 사이에 있는 추상적인 언어라 볼 수 있다. LLM에게 자연어 대신 코드로 프롬프트를 제공하면 어떤 점이 좋은지 다음과 같이 정리해 볼 수 있다.
- 자연어보다 구조적이고 체계적이기 때문에 조금 더 일관성 있고 예측 가능한 결과를 얻을 수 있다.
- 출력 결과가 장황하지 않고 간결해질 수 있다.
- 함축적인 의도가 담긴 자연어보다 불필요한 추론 과정이 줄어들기 때문에 LLM이 이해하기 더 수월하다.
- 복잡한 처리나 순차적으로 처리되어야 하는 데이터를 코드를 통해 표현할 수 있다.
- 자연어 명령과 통합하고 기능을 확장할 수 있다.
- 맥락 또는 의미적 힌트를 통해 데이터를 생성하거나 추론할 수 있다.
- 추론 단계와 출력 결과에 대한 근거를 예상할 수 있다.
- 제로샷, 퓨샷, 생각의 사슬과 같은 추론 전략을 명시하지 않고도 자연스럽게 구현한다.
코드 작성 규칙
코드 기반 프롬프트는 LLM이 코드를 이해하고 해석하는 것을 전제로 하고 있기 때문에 코드의 품질이 무엇보다 중요하다고 할 수 있는데, 간단한 코드 작성 규칙을 살펴보자. 아래의 규칙을 잘 따르기만 해도 사전 학습을 거의 하지 않고 LLM이 의사코드를 이해하는데 문제가 없다.
- 작성되는 코드는 구조적이고 직관적이며 작성자의 의도가 분명해야 한다.
- 주석으로 자연어 설명을 제공하는 것이 명확성을 부여한다.
- 개성 있는 문법보다는 일반적인 프로그래밍 언어의 규칙을 따르는 것이 좋다.
- 타입을 추론할 수 있을 때는 생략하고, 그렇지 않을 때는 기본 자료형 또는 구조체를 정의하고 타입을 명시한다.
- 함수/변수의 이름은 추론에서 매우 중요한 단서이므로 의미 있는 이름으로 짓는다.
- 함수를 정의할 때는 기능과 에리 처리 기준을 명세한다.
- 함수를 호출할 때는 명명된 파라미터(Named Parameter)를 사용하여 의미를 명확히 한다.
OOP(Object-Oriented Programming)?
코드 기반 프롬프트에서는 OOP와 관련된 문법은 가급적 사용하지 않는 것이 좋을 수 있다. OOP는 프로그래밍에서 클래스, 추상 클래스, 인터페이스, 상속, 다형성, 가시성, 메서드 오버라이딩과 같이 코드를 구조적으로 짤 수 있게 만들어주는 방법을 제공하지만 코드의 복잡도도 그만큼 증가하기 때문에 LLM의 추론 시간이 더 길어질 뿐만 아니라 모호해질 수도 있다. 제시된 예시에서는 OOP를 전혀 사용하지 않았으며 구조체로 대신하였다.
예시
코드 기반 프롬프트로 해볼 수 있는 더 많은 예시는 다음과 같다.
지시
"""Instruct instructs LLM in natural language
Returns:
The command result
Errors:
- If the command execution fails
- If it is illegal or against policy
- If LLM contains instructions that are incomprehensible
"""
func Instruct(prompt string) string
helloworld string = Instruct("Say 'Hello, world'")
Print(helloworld)
Hello, world
지식
"""Knowledge gets pre-trained knowledge
Returns:
The knowledge gained
Errors:
- When knowledge cannot be obtained
- If the information does not exist or is out of date
"""
func Knowledge[R any](prompt string) R
today string = Knowledge("What's the date today?")
Print("Today's date is {today}.")
Today's date is January 12, 2025.
지식 통합
"""Learn learns new knowledges and apply it
Notes:
- This function does not apply globally (Only inside the called block)
Errors:
- If you can't learn knowledges
"""
func Learn(knowledges []string)
// Write a blog post
func Post(topic string) string
knowledges = {
"Developer 'Sang-woo Jeong' is a developer from South Korea."
"Sang-woo Jeong taught PHP programming on a lecture platform in South Korea."
"Sang-woo Jeong wrote <Awesome Laravel>, an introductory book on Laravel"
"Sang-woo Jeong created the most popular blog theme for Tistory, a blog platform in South Korea"
}
Learn(knowledges)
post = Post("Sang-woo Jeong")
Print(post, outputFormat="Markdown")
# Sang-woo Jeong: A Renowned Developer from South Korea
Sang-woo Jeong is a prominent developer from South Korea. He has contributed significantly to the programming community in multiple ways:
- He has taught **PHP programming** on a lecture platform in South Korea.
- He is the author of **<Awesome Laravel>**, an introductory book on Laravel.
- He created **the most popular blog theme** for Tistory, a widely used blogging platform in South Korea.
His contributions to the tech industry, particularly in web development and Laravel, have made a lasting impact on developers in South Korea and beyond.
문자열 포매팅
"""Format changes a string to a specific format
Retruns:
Formatted string
Errors:
- If the format is not interpretable
- If the format does not have the required parameters
- If the type of value you want to replace does not match
"""
func Format(format string, args ...any) string
email = Format("<username>@example.com", username="user")
Print(email)
user@example.com
번역
"""Translate translates text into another language
Returns:
Translated string
Errors:
- If the translation fails
"""
func Translate(text string, lang string) string
translated = Translate(
"What are the limits of pseudocode that LLMs can understand?",
lang="Korean"
)
Print(translated)
LLM이 이해할 수 있는 의사코드의 한계는 무엇인가요?
요약
"""Summarize summarizes the text
Notes:
- LLM will judge for itself and find the key points in the text
- Make it possible to guess what the original text was just by summarizing it
- The text must be short and well-organized
- There are no restrictions on the number of sentences or characters
Returns:
Summarized text
Errors:
- If you can't summarize the text
"""
func Summarize(text string) string
text = `What is Lorem Ipsum?
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
Why do we use it?
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
Where does it come from?
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
Where can I get some?
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.`
summarized = Summarize(text)
Print(summarized)
Lorem Ipsum 개요
Lorem Ipsum은 인쇄 및 출판 업계에서 1500년대부터 사용된 더미 텍스트입니다. 원래는 고대 로마 철학자 **키케로(Cicero)**의 "de Finibus Bonorum et Malorum" (선과 악의 극단)에서 유래했으며, 1960년대 Letraset의 보급과 데스크톱 출판 소프트웨어를 통해 널리 퍼졌습니다.
사용 목적
웹 페이지, 출판물, 디자인 샘플에서 독자가 레이아웃에 집중할 수 있도록 의미 없는 텍스트를 제공하는 것이 목적입니다. 'Content here'처럼 인위적인 문구 대신, 실제 문장 구조를 갖춘 Lorem Ipsum이 더 자연스럽게 보입니다.
출처 및 역사
Lorem Ipsum은 45 BC에 작성된 라틴 문헌에서 발췌되었으며, 1914년 H. Rackham이 번역한 버전이 현재도 사용됩니다. 다양한 변형이 있지만, 일부는 무작위 단어 추가나 유머 삽입으로 원본과 다를 수 있습니다.
Lorem Ipsum 생성기
인터넷에는 다양한 Lorem Ipsum 생성기가 존재하며, 200개 이상의 라틴어 단어와 문장 구조를 조합하여 의미 없는 텍스트를 생성합니다.
규칙
"""SetRules specifies the rule for LLM to follow
Notes:
- If the function is called back, the existing valid rules must still be maintained inside the block
- If a rule conflicts, the rule you set later must be specified
- This function does not apply globally (Only inside the called block)
Errors:
- If invalid rules are passed
- If unsupported rules are included
"""
func SetRules(rules []string)
// Questions creates questions that are appropriate for the topic.
func Questions[R any](topic string) R
SetRules({
"You have to use informal language",
"It should be a question that the user may be interested in"
})
questions [3]string = Questions("Generative AI")
Print(questions, outputFormat="JSON")
[
"What’s the coolest thing you’ve seen AI create?",
"Is AI creativity real, or is it just math tricks?",
"What’s the next big thing in Generative AI?"
]
역할
"""SetRole specifies the role LLM will play
Notes:
- When the function is called back, the role should be re-established and the previously set role should not be valid
- This function does not apply globally (Only inside the called block)
Errors:
- If LLM is an unplayable role
"""
func SetRole(role string)
// Questions creates questions that are appropriate for the topic.
func Questions[R any](topic string) R
SetRole("Software Engineer")
questions [5]string = Questions("Technical interview")
Print(questions, outputFormat="JSON")
[
"What are the key principles of SOLID design patterns?",
"Explain the concept of CAP theorem in distributed systems.",
"Can you explain the difference between concurrency and parallelism?",
"Describe a situation where you optimized an algorithm for efficiency.",
"What is the difference between REST and GraphQL APIs?"
]
의미적 추론
// Semantic gets the semantic match for a given value.
func Semantic(value any, hint string) bool
ok = Semantic("Golang", "Programming language")
Print(ok)
true
추출
"""Extract extracts data using a template
Returns:
Extracted values
Errors:
- If the value extraction fails
- If you can't find the target in template
- If the original text does not match the given template
"""
func Extract[R any](text string, template string) R
type Email struct {
username string
domain string
}
email Email = Extract("user@example.com", "<username>@<domain>")
Print(email, outputFormat="JSON")
{
"username": "user",
"domain": "example.com"
}
분석
// HTML analyses HTML String
func Html[R any](html string) R
head = `<title>ChatGPT</title><meta name="description" content="ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, learning, brainstorming and more."><meta name="keyword" content="ai chat,ai,chap gpt,chat gbt,chat gpt 3,chat gpt login,chat gpt website,chat gpt,chat gtp,chat openai,chat,chatai,chatbot gpt,chatg,chatgpt login,chatgpt,gpt chat,open ai,openai chat,openai chatgpt,openai"><meta property="og:description" content="A conversational AI system that listens, learns, and challenges"><meta property="og:title" content="ChatGPT"><meta property="og:image" content="https://cdn.oaistatic.com/assets/chatgpt-share-og-u7j5uyao.webp"><meta property="og:url" content="https://chatgpt.com">`
type Head struct {
title string
meta map[string]string
openGraph map[string]string
}
analysed Head = Html(head)
Print(analysed, outputFormat="JSON")
{
"title": "ChatGPT",
"meta": {
"description": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, learning, brainstorming and more.",
"keyword": "ai chat,ai,chap gpt,chat gbt,chat gpt 3,chat gpt login,chat gpt website,chat gpt,chat gtp,chat openai,chat,chatai,chatbot gpt,chatg,chatgpt login,chatgpt,gpt chat,open ai,openai chat,openai chatgpt,openai"
},
"openGraph": {
"og:description": "A conversational AI system that listens, learns, and challenges",
"og:title": "ChatGPT",
"og:image": "https://cdn.oaistatic.com/assets/chatgpt-share-og-u7j5uyao.webp",
"og:url": "https://chatgpt.com"
}
}
프롬프트 체이닝
// Chain uses the result of the prompt as the input value for the next prompt and return the result
func Chain[R any](initial any, functions ...func (any) any) R
result string = Chain(
Knowledge("Quantum Computing"),
func (data string) string => Translate(data, lang="Korean")
)
Print(result)
양자 컴퓨팅은 양자 역학의 원리를 활용하여 기존 컴퓨터보다 훨씬 빠른 계산을 수행할 수 있는 새로운 형태의 컴퓨팅 방식입니다. 기존의 비트(bit) 대신 큐비트(qubit)를 사용하며, 중첩(superposition)과 얽힘(entanglement)과 같은 양자 특성을 활용하여 병렬 연산을 가능하게 합니다. 이를 통해 복잡한 문제를 기존 컴퓨터보다 훨씬 효율적으로 해결할 수 있습니다.
검색
"""Search gets the answer to the question through a web search
Returns:
Search results
Errors:
- If you can't search
- Search fails to get results
"""
func Search[R any](prompt string) R
result string = Search("Who is the winner of the 2024 Nobel Prize for Literature?")
Print(result)
2024년 노벨 문학상 수상자는 한국의 소설가 한강입니다. 스웨덴 한림원은 2024년 10월 10일(현지시간)에 한강 작가를 수상자로 발표했습니다.
파일
"""Write creates a file in file storage
Errors:
- If the file cannot be created
"""
func Write(filename string, value any)
"""Read reads a file from file storage
Returns:
Contents of the file
Errors:
- If the file cannot be read
- If you do not have permission to read the file
- If the file cannot be found
"""
func Read(filename string) string
"""Link generates a link to download the file
Notes:
- if possible, provide the user with a link to download the file
Errors:
- If the link cannot be created
"""
func Link(filename string)
Write("hello.txt", "Hello, world")
// If you want to be a little more defensive at the code level, you can do null checks as well
if content = Read("hello.txt"); content is not null {
Print(content)
}
Link("hello.txt")
파일명: hello.txt
파일 내용: "Hello, world"
다운로드 링크: /home/sandbox/hello.txt
멀티턴 프로그래밍
지금까지 작성한 코드는 싱글턴(Single-turn)이다. 여기서 한 걸음 더 나아가 질문과 응답이 이어지는 멀티턴(Multi-turn)을 시뮬레이션 하도록 코드를 작성하고 싶다면 멀티턴 프로그래밍(Multi-turn)을 시도해볼 수 있다. 멀티턴 프로그래밍은 ChatGPT와 같이 사용자와 LLM이 서로 상호작용 할 수 있는 챗(Chat) 환경에서 실험해볼만한 프로그래밍 방식이다. 예를 들어 '게임 추천 서비스' 만들어보고 싶다면 대화 흐름은 다음과 같을 것이다. 프로그래밍이 되어 있는 턴은 세번째 턴까지라고 가정해보자.
// 1st Turn
LLM: 안녕하세요. 게임 추천 서비스입니다. 게임의 장르와 플랫폼를 말씀해주세요!
// 2nd Turn
// 상태: { Genres=[ RPG ], Platforms=[ PC, PS5 ] }
사용자: PC, PS5에서 즐길 수 있는 RPG게임을 추천해주세요.
// 예외: 장르 또는 플랫폼 누락
LLM: 어떤 (장르/플랫폼)의 게임을 추천해 드릴까요?
사용자: ...
// 3nd Turn
LLM: PC, PS5에서 즐길 수 있는 RPG게임을 추천해드리겠습니다.
...
턴(Turn)
멀티턴 프로그래밍에서는 턴(Turn) 개념이 필요하다. 간단히 말해 턴은 LLM 또는 사용자의 요청, 응답의 단위이다. 위 예제를 구현하기 위해 코드 기반 프롬프트로 표현해보면 다음과 같다.
"""Input gets input from the user
Notes:
Depending on your environment, you should:
- If you can't ask the user for input, randomly determine and simulate the user's answers.
- If you have a chat environment where you can interact with the user, ask the user for input.
Returns:
User response
"""
func Input[R any]() R
// Game Recommendation Service
// Recommend recommends products or goods for a specific topic based on contextual information
func Recommend[R any](topic string, contexts any) R
type Game struct {
genres []string
platforms []string
}
var g Geme
// 1st Turn
turn {
welcome = "Hello. This is a game recommendation service.
Please tell us the game genre and platform!"
Print(welcome)
}
// 2nd Turn
turn {
for g.genres is null or g.platforms is null {
g = Input()
}
}
// 3rd Turn
turn {
result string = Recommend("Games", g)
Print(result)
}
게임의 장르와 플랫폼을 저장하기 위한 전역변수를 선언하고 turn
블록에 턴에 처리할 코드를 작성했다. 각 턴마다 하는 일을 보면 다음과 같다.
- 첫번째 턴에는 환영 메시지를 출력한다.
- 두번째 턴에는 게임의 장르, 플랫폼을 입력 받는다. 하나라도 누락된 경우 다시 요청한다.
- 세번째 턴에는 사용자가 입력한 값을 토대로 게임을 추천한다.
위와 같은 상호작용을 구현하기 위해 turn
과 같은 꼭 키워드가 필요할까? 전통적인 프로그래밍 언어에서는 turn
같은 키워드는 필요하지 않은데 말이다. 그러나 ChatGPT와 같은 챗(Chat) 환경에서는 턴(Turn)이라는 개념이 분명하고, 멀티턴(Multi-turn) 프로그래밍이라는 이름에서 볼 수 있듯이 턴이라는 개념은 필요하며 LLM이 코드를 이해하기에 있어서도 turn
을 블록으로 구분하는 것이 더 모호하지 않고 명확하다고 볼 수 있다.
결과
// 1st Turn
LLM: 안녕하세요. 게임 추천 서비스입니다. 게임의 장르와 플랫폼를 말씀해주세요!
// 2nd Turn
어떤 게임 장르를 원하시나요? (예: RPG, Action, Strategy)
어떤 플랫폼에서 플레이하고 싶나요? (예: PC, PlayStation, Xbox)
입력해주세요!
사용자: PC, Xbox
//예외: 장르 또는 플랫폼 누락
LLM:
플랫폼이 입력되었습니다: PC, Xbox
이제 게임 장르를 입력해주세요! (예: RPG, Action, Strategy)
사용자: RPG
// 3rd Turn
LLM:
게임 추천 결과: 사용자의 선호도 (RPG / PC, Xbox)에 맞춰 다음 게임을 추천합니다!
1. Elden Ring
2. Cyberpunk 2077
3. The Witcher 3
테스트
사람과 마찬가지로 LLM도 자신이 만들어낸 결과에 대해 스스로 평가하고 이를 회고하거나 검증 결과를 바탕으로 새로운 결과를 만들어내는 것이 가능하다. 아래의 코드는 생성된 이메일 주소를 자체 평가(Assertion)한다. 이메일 생성 조건에 명시된 것처럼, 생성된 이메일 주소가 중복된 이메일 주소를 가지지 않았는지, 템플릿은 잘 따르고 있는지, 이메일 주소 포맷인지 검증한다. 이러한 검증 방식은 언어 모델만이 할 수 있는 자동화된 자연어 기반의 의미적(Semantic) 검증 방식이며 생성된 데이터를 바탕으로 LLM이 검증하고 테스트케이스 성공 여부를 반환할 수 있다.
"""
Notes:
- If assertions fails, the LLM must generate and report a failure message
"""
// NotEmpty asserts that the given value should not be empty
func AssertNotEmpty(value any)
// Semantic asserts that the given value is meaningfully consistent with the hint
func AssertSemantic(value any, hint string)
func testGenerate() {
emails [3]Email = Generate( ... )
AssertNotEmpty(emails)
AssertSemantic(
emails,
"Don't you have any duplicate email addresses?"
)
for e in emails {
AssertSemantic(e, "Are you following the '<username>@<domain>' template?")
AssertSemantic(e, "Does this email have a valid format?")
}
}
위 코드는 데이터의 평가 기준을 직접 하드 코딩했지만, 이 마저도 LLM에게 생성을 위임할 수 있다. 평가 대상 데이터 뿐만 아니라 평가 기준 생성까지 위임하는 것이다. 이 경우 사람은 LLM이 생성한 데이터와 평가 기준이 타당한지 재평가하여야 한다. 만약 생성된 평가 기준이 타당한지도 LLM에게 위임하는, 그야말로 끝없는 콜백함수를 보는 것 같은 체인을 구성할 수도 있다.
위와 같은 경우 단일 케이스에 대해서만 처리했지만 일반적으로 테스트케이스는 다수의 케이스를 대상으로 하기 마련이다. 그 예로 사전 학습된 지식을 얻어오는 Knowledge()
를 대상으로 테스트해보자. 암묵적으로 할루시네이션 테스트를 할 수 있을 것이다. 아래와 같이 작성하면 새로운 테스트케이스를 추가하는 것도 쉽다.
* 실패 사례를 테스트하는 것도 잊지 말자!
// Contains asserts that a given value must contain `search`
func AssertContains(value any, search any)
// Null asserts that the given value must be null
func AssertNull(value any)
func testKnowledge() {
tests = []struct{
prompt string
expect string
}{
{ "What is the capital of South Korea?", "Seoul" },
{ "What year was the war between South and North Korea in the 1900s?", "1950" },
{ "Who was the first to propose the concept of the World Wide Web?", "Tim Berners-Lee" },
{ "What is the first widely known Generative AI service in the 2020s?", "ChatGPT" },
{ "Who are the three masters of SF fiction and the authors of Starship Troopers?", "Robert Anson Heinlein" },
}
for t in tests {
output string = Knowledge(t.prompt)
AssertContains(output, t.expect)
}
fails = []struct{
prompt string
}{
{ "What is the incident that King Sejong threw a MacBook?" }
}
for f in fails {
output string = Knowledge(f.prompt)
AssertNull(output)
}
}
서브모듈
Generate()
와 같은 함수들을 파일 단위의 서브모듈로 분리하고 ChatGPT Project, MyGPT와 같은 서비스에서 업로드하고 코드를 실행하기 전에 분석하면 프롬프트마다 함수를 재정의하지 않고도 재사용할 수 있기 때문에 반복적인 작업을 줄일 수 있고, 동일한 함수를 사용해서 테스트하고 값을 생성하므로 불확실성을 줄일 수 있다. 코드의 집합이 되는 파일의 확장자를 .llm
으로 가정하고 Generate()
함수가 포함된 llm
서브모듈은 다음과 같이 정의할 수 있다.
// llm.llm
...
func Generate[R any](examples []any, template string, contexts []string) R
서브모듈을 채팅 세션에서 접근할 수 있는 파일 스토리지에 업로드하고 다음과 같이 import
를 통해 서브모듈 접근을 시도할 수 있다.
import llm
llm.Generate( ... )
함수로 표현된 프롬프트를 서브모듈로 만들면 이것이 바로 프롬프트 라이브러리 또는 프롬프트 API가 될 것이다. 함수의 구현부가 필요 없기 때문에 그냥 읽어도 가독성이 떨어지지 않아서 서브모듈 파일 자체가 명세(Spec)의 역할도 같이 할 수 있다.
import
를 통해 접근하려면 서브모듈에 대한 규칙을 LLM에게 알려야 한다. 단순하게 다음과 같은 규칙을 지시할 수 있다.
.llm 확장자를 가진 파일은 서브모듈입니다. 스크립트에서는 `import` 를 사용하여 서브모듈이 포함될 수 있습니다.
예로, `import llm` 는 llm.llm 에 해당하는 llm 서브모듈을 포함하라는 의미입니다.
스크립트에 서브모듈이 포함된 경우 "코드를 실행하기 전에" 다음과 같은 가이드라인을 적용하세요.
1. 서브모듈을 먼저 분석하여 함수와 타입을 확인하세요.
2. 이미 분석이 완료된 서브모듈은 다시 분석할 필요가 없습니다.
사용자 정의 인터프리터
'사용자 정의 인터프리터'는 코드 기반 프롬프트를 해석하는 LLM의 인터프리터로서의 역할을 조정하는 일을 의미한다. 이는 필수사항은 아니지만 코드를 해석하는 방식을 느슨하게 하거나 엄격하게 하는 등의 조정하는 것이다. 그야말로 '자연어로 인터프리터 동작을 정의'하는 일이며 그다지 어려운 일은 아니다. 전통적인 프로그래밍 언어의 인터프리터가 했던 일들을 곱씹어서 생각해 보면 다음과 같은 지시를 내려볼 수 있을 것이다.
오류
아래의 같은 지시는 오류처리 규칙을 명확히 하여 더 엄격하게 한다. 자바스크립트와 같은 스크립트 언어에서 린터(Linter) 규칙을 정의하거나 정적 분석 도구를 만드는 것과 유사하다. 아래는 그 예이다.
# 오류
* 임의로 오류를 수정해서 코드를 실행하면 안되고, 반드시 보고해야 합니다.
* 단, 의사코드의 오류가 아닌 내부적으로 생성된 코드의 오류는 스스로 수정하여 다시 실행해야 합니다.
* LLM은 발생한 오류에 대해 개선사항을 제시할 수 있습니다.
* 오류가 발생한 경우 스크립트 실행을 중단하고 자연어로 오류 메시지를 출력해야 합니다.
## 규칙
### 변수 및 타입
* 이형 타입 간 연산을 시도하는 경우
* 변수에 할당하려는 값이 변수의 타입과 일치하지 않은 경우
* null 값을 참조하려는 경우
* 사용되지 않은 변수 및 서브모듈이 있는 경우
* 블록 스코프 규칙에 어긋나는 변수를 참조하려는 경우
* 정의되지 않은 변수를 참조하려는 경우
### 논리
* Out of Scope, Division by Zero 등의 논리적 문제가 발생한 경우
* 실행할 수 없는 위치에 코드가 존재하는 경우
* 루프 및 재귀에서 탈출 조건이 없는 경우
* 조건문이 항상 true 또는 false 를 반환하는 경우
* 접근할 수 없는 키 또는 인덱스를 참조하려는 경우
### 메모리
* 배열, 맵, 슬라이스에 너무 많은 값을 할당하는 등 불필요한 메모리 할당이 발생하는 경우
### 함수 및 호출
* 정의되지 않은 함수 호출을 시도하려는 경우
* 반환값이 있으나 사용되지 않은 경우
* 파라미터 및 반환값의 타입이 일치하지 않은 경우
* 호출시 가변인자를 제외한 파라미터의 개수가 일치하지 않은 경우
언어
LLM이 인터프리터로서의 역할을 더 정확하게 하기 위해서는 언어 규칙을 명세하는 것도 좋다. 다만 전통적인 프로그래밍 언어의 언어 규칙처럼 설명을 포함한 장문의 문서를 작성할 필요는 없다. 특히 이 글에서 다룬 사례처럼 특정 언어의 서브셋(Subset)으로 문법이 규정되어 있는 경우에는 기존의 언어에서 몇 가지 규칙만 바꿔서 개조할 수도 있다. 그 예는 다음과 같다.
# 언어
* 의사코드의 문법은 기본적으로 "Go언어" 의 서브셋(Subset)입니다.
## 키워드
* is: ==
* is not: !=
* not: !
* and: &&
* or: ||
* null: nil
## 타입
* PrimitiveType: Numeric(`int`, `float`), String(`string`), Boolean(`bool`)
* PointerType: **지원하지 않음**
- Call by Value: PrimitiveType, ArrayType
- Call by Reference: SliceType, MapType, StructType
## 변수
* ":=" 은 없으며 "=" 만 사용합니다.
## 함수
* 구현부가 없는 경우 함수 시그니처와 주석으로 기능을 추론하세요.
문법
새로운 문법을 의사코드에 추가하려면 어떻게 하면 좋을까? 이전에 '언어' 규칙에서 '의사코드의 문법은 "Go언어"의 서브셋'이라고 했지만, 멀티턴 프로그래밍에서 사용된 turn
같은 경우에는 Go언어에서 지원하지 않으므로 의사코드에서도 지원하지 않는 문법으로 취급할 수도 있다. 따라서 새로운 문법규칙을 '정의'할 수 있어야 하는데, 이 경우 문법 규칙에 사용되는 EBNF(Extended Backus-Naur form)의 바리에이션인 WSN(Wirth Syntax Notaion)을 사용해서 표현해보면 다음과 같다.
# 문법
## 턴
```
TrunStmt = "turn" "{" [ newline ExpressionList ] "}"
```
### 예제
```
turn {}
turn {
Instruct(prompt="Say 'Hello, world'")
}
```
결론
코드 기반 프롬프트가 유용할지 어떨지는 사실 나도 잘 모르겠다. 단지 이 프로젝트가 재미삼아 만들어졌을 뿐만 아니라 초기 단계(다음 단계를 진행하기는 할까?)이기도 하고, 테스트케이스나 실제 사례가 너무나도 부족하기 때문이다. 그럼에도 불구하고 개인적으로 자연어 명령에 비해 코드 기반 프롬프트가 강점을 보이는 부분은 '모듈화' 및 '테스트 가능성'이라고 보고있다. 함수로 정의된 모든 기능은 테스트가 가능할 뿐만 아니라 버전 관리 및 문서화가 가능하고 프롬프트 작성자에게 일관성있는 프롬프트 API를 제공할 수 있게 된다는 점에 있어서 매력적일 것이다. 코드 기반 프롬프트 라이브러리를 별도로 제작하여 관리하고 PromptAPI Spec 을 만든다면 더 나은 프롬프트 엔지니어링이 될 수도 있다.
여담으로, 처음 이 프로젝트를 시작할 때는 언어적 요소가 하나도 없는 단순한 의사코드로 시작했는데, 갈수록 내가 원하는 것들이 너무 많아져서 언어를 창조하려는 시도까지 했었다. 언어 스펙까지 만들려다가 생각해보니 너무 멀리까지 간 것 같다는 생각에 그 생각은 버리고 이미 존재하는 언어의 서브셋 형태로 하는 것으로 방향을 바꾸었다. 언어 뿐만 아니라 함수나 테스트케이스까지 정의해서 프롬프트 라이브러리를 만들고 오픈소스로 공개할까도 고민했는데, 아이디어도 이 글에 언급한 것 정도 빼고는 떠오르지도 않는데다가 혼자 재미삼아 하기에는 품이 너무 많이 들어서 하지 않기로 했다. (나중에 인연이 생기거나 내키면 할지도 모르겠지만...)