LangChain: 조건문처럼 쓸 수 있는 RouterChain

라우터는 네트워크 중계장치로, 여러 통신망들 중 최적의 경로(route)를 지정해주는 장비입니다. RouterChain은 라우터처럼 입력 query와 목적지 Chain들의 정보를 분석하여 최적의 Chain으로 연결해주는 Chain입니다.

몇 가지 개념들을 살펴봅시다.

  • destination_chains: RouterChain 다음 단계로 연결될 수 있는 목적지 Chain들로, 각각의 destination chain은 LLMChain이나 다른 복합적인 Chain으로 만듭니다.
  • default_chain: 입력 query를 보낼만한 적합한 destination chain이 없을 때 연결하는 Chain입니다.
  • RouterChain: destination_chains 정보와 입력 query를 비교하여 가장 적합한 Chain을 선택합니다. 적절한 Chain이 없으면 default chain을 선택합니다.
  • RouterOutputParser: RouterChain의 출력 문자열을 다음 Chain에서 입력으로 사용할 수 있도록 딕셔너리로 바꿔줍니다. 딕셔너리는 목적지 Chain 정보와 목적지 Chain의 입력 프롬프트 정보를 포함합니다.

Destination Chains

코드를 봅시다. 예제를 위해 철학, 물리학, 컴퓨터, 문학 네 분야의 전문가들을 모셨습니다.

philosophy_expert = """
당신은 플라톤입니다. 철학 전문가입니다. 모르는 질문에는 모른다고 답합니다.
질문: {input}
"""
physics_expert = """
당신은 아인슈타인입니다. 물리학 전문가입니다. 모르는 질문에는 모른다고 답합니다.
질문: {input}
"""
computer_expert = """
당신은 폰 노이만입니다. 컴퓨터 전문가입니다. 모르는 질문에는 모른다고 답합니다.
질문: {input}
"""
literature_expert = """
당신은 셰익스피어입니다. 문학 전문가입니다. 모르는 질문에는 모른다고 답합니다.
질문: {input}
"""

위의 문자열들은 목적지 Chain들의 프롬프트 템플릿으로 사용할겁니다.

prompt_info = [
    {
        "name":"philosophy",
        "description": "철학 관련 질문에 대답",
        "prompt_template": philosophy_expert
    },
    {
        "name":"physics",
        "description": "물리학 관련 질문에 대답",
        "prompt_template": physics_expert
    },
    {
        "name":"computer",
        "description": "컴퓨터 관련 질문에 대답",
        "prompt_template": computer_expert
    },
    {
        "name":"literature",
        "description": "문학 관련 질문에 대답",
        "prompt_template": literature_expert
    }
]

위 리스트에는 목적지 Chain들의 정보를 담았습니다. 각 Chain 정보는 딕셔너리로, name, description, prompt_template을 포함하고 있습니다. 목적지 Chain들에서 사용할 LLM을 정의하겠습니다.

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.chains import LLMChain

llm = ChatOpenAI(temperature=0)

이제 목적지 Chain들을 만들며 {“name”:Chain} 형태의 딕셔너리를 준비합니다. RouterChain에서 목적지 Chain의 이름을 지정해주면 해당 Chain으로 연결하기 위한 사전작업입니다.

destination_chains = {}
for p in prompt_info:
    name = p["name"]
    prompt = ChatPromptTemplate.from_template(template=p["prompt_template"])
    destination_chains[name] = LLMChain(llm=llm, prompt=prompt)

RouterChain의 프롬프트에 넣을 목적지 Chain들의 정보를 준비합니다. 간단하게 “name”: “description” 형태의 문자열을 만들었습니다.

destinations = [f"{p['name']}: {p['description']}" for p in prompt_info]
destinations_str = "\n".join(destinations)
print(destinations_str)

### 결과
philosophy: 철학 관련 질문에 대답
physics: 물리학 관련 질문에 대답
computer: 컴퓨터 관련 질문에 대답
literature: 문학 관련 질문에 대답

Default Chain

적절한 목적지 Chain이 없을 때 사용할 Default Chain도 준비합니다. 사용자 입력을 그대로 넘기도록 했습니다.

default_prompt = ChatPromptTemplate.from_template("{input}")
default_chain = LLMChain(llm=llm, prompt=default_prompt)

RouterChain

RouterChain을 만드는데 필요한 클래스들을 import 하겠습니다.

from langchain.chains.router import MultiPromptChain
from langchain.chains.router.llm_router import LLMRouterChain,RouterOutputParser
from langchain.prompts import PromptTemplate

이제 RouterChain의 프롬프트를 준비합시다.

MULTI_PROMPT_ROUTER_TEMPLATE = """
입력받은 내용을 바탕으로 가장 적절한 모델 프롬프트를 선택하세요.
모델 프롬프트 정보는 다음과 같이 주어집니다.

"프롬프트 이름": "프롬프트 설명"

<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
```json
{{{{
    "destination": string \ name of the prompt to use or "DEFAULT"
    "next_inputs": string \ a potentially modified version of the original input
}}}}
```

REMEMBER: "destination"은 아래 주어진 프롬프트 설명을 바탕으로 프롬프트 이름 중 하나를 선택하거나,
적절한 프롬프트가 없으면 "DEFAULT"를 선택할 수 있습니다.

REMEMBER: "next_inputs"은 원본 INPUT을 넣으세요.

<< CANDIDATE PROMPTS >>
{destinations}

<< INPUT >>
{{input}}

<< OUTPUT (remember to include the ```json)>>"""

router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
    destinations=destinations_str
)

위에서 MULTI_PROMPT_ROUTER_TEMPLATE는 기본 문자열입니다. format을 거치며 destinations만 치환하고 input은 남기기 위해 input에 중괄호를 두 개 사용했습니다. 하나만 쓰면 치환할 때 input 정보도 넣으라고 에러가 납니다. json은 치환하지 않을 것이기 때문에 중괄호를 중복해서 썼습니다. format을 거치면 {{{로 바뀌는데 여기서 한 번, 뒤에서 PromptTemplate에서 또 한 번 치환이 일어나므로 json의 중괄호를 끝까지 남겨두기 위해 중괄호를 네 개 썼습니다.

이제 RouterChain의 PromptTemplate과 RouterChain을 만들 차례입니다.

router_prompt = PromptTemplate(
    template=router_template,
    input_variables=["input"],
    output_parser=RouterOutputParser(),
)

router_chain = LLMRouterChain.from_llm(llm, router_prompt)

MultiPromptChain

다음에는 RouterChain, 목적지 Chain, Default Chain을 모두 묶어서 MultiPromptChain을 만듭니다.

chain = MultiPromptChain(router_chain=router_chain, 
                         destination_chains=destination_chains, 
                         default_chain=default_chain,
                         verbose=True
                        )

중간에 어떤 목적지 Chain을 선택했는지 확인하기 위해 verbose=True로 입력했습니다. 이제 준비가 끝났습니다. 호출해볼까요?

Run

chain.run("상대성 이론이 뭐에요?")

### 결과
> Entering new MultiPromptChain chain...
physics: {'input': '상대성 이론이 뭐에요?'}
> Finished chain.
'상대성 이론은 알버트 아인슈타인이 제시한 물리학 이론으로, 시간, 공간, 그리고 중력에 대한 새로운 이해를 제공합니다. 이론은 두 가지 주요한 부분으로 나뉩니다. 첫 번째는 특수 상대성 이론으로, 서로 다른 관측자들이 서로 다른 속도로 움직이더라도 물리적 법칙은 동일하게 적용된다는 것을 설명합니다. 두 번째는 일반 상대성 이론으로, 중력을 공간과 시간의 곡률로 설명하며, 질량이나 에너지가 공간과 시간을 구부리는 것을 설명합니다. 이론은 많은 실험적 검증을 거쳐 현대 물리학의 기초로 자리잡았습니다.'

목적지 Chain으로 물리학 Chain이 선택되며 아인슈타인에게 질문이 전달된 것을 볼 수 있습니다.

chain.run("삶의 의미는?")

### 결과
> Entering new MultiPromptChain chain...
philosophy: {'input': '삶의 의미는?'}
> Finished chain.
'플라톤: 삶의 의미는 인간의 존재와 관련된 복잡한 주제입니다. 제가 생각하기에 삶의 의미는 개인에 따라 다를 수 있으며, 각각의 사람들은 자신만의 의미를 발견하고 이해해야 합니다. 삶의 의미는 인간의 가치, 목적, 성장, 연결, 창조 등 다양한 측면을 포함할 수 있습니다. 그러나 이 질문에 대한 절대적인 답은 없으며, 이는 개인의 철학적, 종교적, 문화적 배경에 따라 다를 수 있습니다. 따라서, 삶의 의미에 대한 최종적인 답은 모른다고 말씀드릴 수밖에 없습니다.'

철학적인 질문은 철학 전문가 플라톤에게 전달되었네요.

chain.run("청년과 떠오르는 태양을 주제로 시를 써줘")

### 결과
> Entering new MultiPromptChain chain...
literature: {'input': '청년과 떠오르는 태양을 주제로 시를 써줘'}
> Finished chain.
'나는 셰익스피어가 아니지만, 당신을 위해 청년과 떠오르는 태양을 주제로 한 시를 써보겠습니다.\n\n청년의 마음은 태양과 같아\n뜨겁고 밝게 빛나는 것 같아\n그의 꿈과 희망은 무궁무진하고\n떠오르는 태양처럼 높이 떠오르는 것 같아\n\n그의 눈은 태양과 같아\n찬란하고 아름다운 것 같아\n그의 시선은 멀리 향해\n떠오르는 태양처럼 빛을 비추는 것 같아\n\n청년의 에너지는 태양과 같아\n끊임없이 흘러나오는 것 같아\n그의 열정은 불타오르고\n떠오르는 태양처럼 모두를 따뜻하게 하는 것 같아\n\n청년과 떠오르는 태양은\n새로운 시작과 희망을 상징해\n그들은 우리에게 빛과 영감을 주며\n떠오르는 태양처럼 우리를 이끌어가는 것 같아\n\n이렇게 청년과 떠오르는 태양에 대한 시를 써봤습니다. 셰익스피어의 수준은 아니지만, 조금이나마 당신의 질문에 대한 답변이 되었기를 바랍니다.'

시를 써달라고 하니 문학 Chain으로 연결되긴 했는데, LLM에서 자신은 셰익스피어가 아니라고 부인합니다. 그냥 PromptTemplate 대신 ChatPromptTemplate을 써서 시스템 메시지로 당신은 셰익스피어라고 하면 인정할까요? 연습해보세요.

chain.run("비트와 바이트는 무슨 관계지?")

### 결과
> Entering new MultiPromptChain chain...
computer: {'input': '비트와 바이트는 무슨 관계지?'}
> Finished chain.
'비트와 바이트는 컴퓨터에서 데이터를 표현하는 단위입니다. 비트는 0 또는 1의 값을 가지는 가장 작은 단위이고, 바이트는 8개의 비트로 이루어진 단위입니다. 즉, 바이트는 비트의 그룹이라고 볼 수 있습니다. 따라서, 1바이트는 8비트와 같고, 2바이트는 16비트와 같은 관계를 가지게 됩니다.'

비트와 바이트에 관한 질문은 컴퓨터 Chain에 전달되었습니다. 이렇게 RouterChain을 쓰면 자연어로 if문과 유사한 조건 분기 프로그램을 만들 수 있습니다.

댓글 남기기