LangChain: FewShotPromptTemplate + ExampleSelector

앞에서 LangChainPromptTemplate과 ChatPromptTemplate에 대해 살펴봤습니다. 좋은 프롬프트를 만드는 방법 중 하나가 예시(one-shot, few-shot)를 주는 것이었죠? 이번에는 예시를 포함한 프롬프트를 만들기 위한 FewShotPromptTemplate에 대해 살펴보겠습니다. 먼저 사용할 예시들을 보겠습니다.

Few-shot 예시 리스트

examples = [
    {
        "question":"선희는 빨강색을 좋아하고 영호는 노란색을 좋아한다.",
        "answer":'"선희":"빨강색", "영호":"노란색"'
    },
    {
        "question":"영철이는 탕수육을 좋아하고, 숙희는 깐풍기를 좋아한다.",
        "answer":'"영철":"탕수육","숙희":"깐풍기"'
    },
    {
        "question":"한주는 국어를 좋아하고 영수는 수학을 좋아한다.",
        "answer":'"한주":"국어", "영수":"수학"'
    },
    {
        "question":"민지는 에너지자원공학과이고 민석이는 국어국문학과이다.",
        "answer":'"민지":"에너지자원공학과","민석":"국어국문학과"'
    },
    {
        "question":"남주는 부산에 살고, 남식이는 서울에 산다.",
        "answer":'"남주":"부산", "남식":"서울"'
    }
]

위의 예시는 문장을 입력받아 사람:색깔, 사람:음식, 사람:과목, 사람:학과, 사람:도시 – 이런 형태로 바꿔주는 예시들입니다. 딕셔너리들의 리스트로 주어져있고, 프롬프트에 넣기 위해서는 문자열로 바꿔야겠죠? 문자열로 바꾸기 위해 PromptTemplate를 이용하겠습니다.

from langchain.prompts.prompt import PromptTemplate
example_prompt = PromptTemplate(
    input_variables=["question","answer"],
    template="Text: {question}\nParsed: {answer}"
)
print(example_prompt.format(**examples[0]))

### 출력
Text: 선희는 빨강색을 좋아하고 영호는 노란색을 좋아한다.
Parsed: "선희":"빨강색", "영호":"노란색"

확인을 위해 첫 번째 예시만 추출해서 Text와 Parsed를 앞에 붙이도록 포맷했습니다. 이제 예시를 포함한 실제 프롬프트를 만들어보겠습니다.

FewShotPromptTemplate

from langchain.prompts.few_shot import FewShotPromptTemplate

prompt = FewShotPromptTemplate(
    examples = examples,
    example_prompt = example_prompt,
    suffix = "Text: {input}\nParsed: ",
    input_variables = ["input"]
)

input_prompt = prompt.format(input="성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다.")
print(input_prompt)

### 출력
Text: 선희는 빨강색을 좋아하고 영호는 노란색을 좋아한다.
Parsed: "선희":"빨강색", "영호":"노란색"

Text: 영철이는 탕수육을 좋아하고, 숙희는 깐풍기를 좋아한다.
Parsed: "영철":"탕수육","숙희":"깐풍기"

Text: 한주는 국어를 좋아하고 영수는 수학을 좋아한다.
Parsed: "한주":"국어", "영수":"수학"

Text: 민지는 에너지자원공학과이고 민석이는 국어국문학과이다.
Parsed: "민지":"에너지자원공학과","민석":"국어국문학과"

Text: 남주는 부산에 살고, 남식이는 서울에 산다.
Parsed: "남주":"부산", "남식":"서울"

Text: 성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다.
Parsed: 

여기에 나온 프롬프트가 LLM에 전달할 메시지입니다. 전달해서 응답을 얻어봅시다.

from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)

response = llm.predict(input_prompt)
print(response)

### 결과
"성훈":"김치찌개", "소영":"파스타"

원하는 결과가 잘 나왔습니다. 그런데, 예시가 많다보니 토큰 사용량이 많아지고 따라서 API 사용료가 많이 나오겠네요. 최종 질문과 유사한 예시 한 두 개만 남겨도 같은 결과를 얻을 수 있지 않을까요? 이 때 쓰는게 ExampleSelector입니다.

ExampleSelector

여러 개의 예시들중 질문과 비슷한 것만 실제 프롬프트에 넣으면 토큰 소모량을 줄일 수 있습니다. API 사용료도 줄일 수 있지만, 토큰 제한도 있으니 토큰 소모량을 줄일 수 있다면 줄이는게 좋습니다. ExampleSelector에도 여러 종류가 있습니다.

  • BaseExampleSelector: 상속 받아 사용자 정의 ExampleSelector를 만들 수 있습니다.
  • LengthBasedExampleSelector: 지정 길이가 넘어가지 않도록 예시 개수를 조절합니다.
  • MaxMarginalRelevanceExampleSelector: Maximal Marginal Relevance (MMR)을 이용해 질문과 가까우면서도 다양한 예시를 선택합니다.
  • SemanticSimilarityExampleSelector: 벡터 임베딩의 코사인 유사도를 이용해 질문과 의미가 가까운 예시를 추출합니다.
  • NGramOverlapExampleSelector: n-gram overlap score를 이용해 질문과 가까운 예시를 추출합니다.

SemanticSimilarityExampleSelector를 사용해보겠습니다. 벡터 임베딩을 이용하기 때문에 임베딩 모델도 필요하고 임베딩 벡터를 저장하기 위한 벡터 데이터베이스도 필요합니다. 임베딩은 OpenAIEmbedding, 벡터 데이터베이스는 ChromaDB를 사용하겠습니다.

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

example_selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(), # embedding
    Chroma, # VectorDB
    k=1, # number of examples
)

selected_examples = example_selector.select_examples(
    {"question": "성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다."}
)

### 결과 selected_examples
[{'answer': '"영철":"탕수육","숙희":"깐풍기"',
  'question': '영철이는 탕수육을 좋아하고, 숙희는 깐풍기를 좋아한다.'}]

SemanticSimilarityExampleSelector에서 선택할 예시 개수를 한 개로 지정했습니다(k=1). 질문으로 "성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다."를 넣었더니 음식이 들어간 예시를 선택했습니다!

FewShotPromptTemplate + ExampleSelector

이제 FewShotPromptTemplate과 ExampleSelector를 함께 사용해봅시다.

prompt = FewShotPromptTemplate(
    example_selector = example_selector,
    example_prompt = example_prompt,
    suffix = "Text: {input}\nParsed: ",
    input_variables = ["input"]
)

input_prompt = prompt.format(input="성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다.")
print(input_prompt)

### 결과
Text: 영철이는 탕수육을 좋아하고, 숙희는 깐풍기를 좋아한다.
Parsed: "영철":"탕수육","숙희":"깐풍기"

Text: 성훈이는 김치찌개를 좋아하고 소영이는 파스타를 좋아한다.
Parsed: 

FewShotPromptTemplate의 인자에 examples 대신 example_selector를 사용했고, 질문과 유사한 예시를 포함한 프롬프트를 얻었습니다. 이번에는 다른 질문을 넣어볼까요?

input_prompt2 = prompt.format(input="영훈이는 영어를 좋아하고 수영이는 체육을 좋아한다.")
print(input_prompt2)

### 결과
Text: 한주는 국어를 좋아하고 영수는 수학을 좋아한다.
Parsed: "한주":"국어", "영수":"수학"

Text: 영훈이는 영어를 좋아하고 수영이는 체육을 좋아한다.
Parsed: 

질문과 가장 가까운 예시가 잘 선택된 것을 볼 수 있습니다. LLM에 위의 프롬프트를 전달해서 원하는 결과가 나오는지 보겠습니다.

response = llm.predict(input_prompt)
print(response) # 결과 - "성훈":"김치찌개","소영":"파스타"

response = llm.predict(input_prompt2)
print(response) # 결과 - "영훈":"영어", "수영":"체육"

각각 예시를 하나만 추출해서 One-shot learning을 수행했고, 원하는 결과가 잘 나왔습니다.

댓글 남기기