LangChain과 LLM 모델을 사용하여 애플리케이션을 만들 때 필요에 따라서 여러개의 템플릿(templates)을 미리 작성해 놓고, 사용자의 질문과 가장 유사한 (most similar, most relavant) 템플릿을 선택해서 LLM 모델로 질문과 템플릿을 인풋으로 보낼 수 있다면 보다 맞춤형의 답변을 생성할 수 있을 것입니다. 

 

LangChain - Routing templates dynamically by semantic search

 

 

시맨틱 검색은 임베딩과 코사인 유사도를 사용하는 것은 자연어 처리 및 정보 검색에서 검색 결과의 관련성과 정확성을 향상시키기 위해 쿼리의 의미에 기반하여 사용되는 방법입니다.  

1. 시맨틱 검색 (Semantic Search): 전통적인 검색 엔진은 종종 정확한 키워드 일치에 의존합니다. 반면에 시맨틱 검색은 검색 쿼리의 의도와 맥락적 의미를 이해하려고 합니다. 동의어, 관련 용어 및 전체 맥락과 같은 언어의 뉘앙스를 해석하여 더욱 관련성 높은 결과를 제공하려고 합니다.  

2. 임베딩 (Embeddings): 시맨틱 이해를 달성하기 위해 시스템은 임베딩을 사용합니다. 임베딩은 단어, 구, 문장 또는 전체 문서를 고차원 공간에서 수치적으로 표현한 것입니다. 이러한 표현은 대규모 데이터셋에서 학습되며 단어 간의 의미 관계를 포착합니다. 예를 들어, 잘 훈련된 임베딩 공간에서는 유사한 의미를 가진 단어들이 서로 가까이 위치합니다.  

3. 코사인 유사도 (Cosine Similarity): 텍스트(검색 쿼리와 검색 가능한 문서 모두)를 임베딩으로 변환한 후, 시스템은 얼마나 유사한지 측정할 방법이 필요합니다. 이를 위해 흔히 사용되는 척도는 코사인 유사도입니다. 코사인 유사도는 두 벡터(이 경우, 쿼리와 문서의 임베딩) 사이의 각도의 코사인을 측정합니다. 코사인 유사도가 1에 가까우면 매우 작은 각도를 나타내고, 따라서 높은 유사도를 의미합니다. 반대로, 코사인 유사도가 0에 가까우면 큰 각도와 낮은 유사도를 나타냅니다.  

4. 검색에서의 적용 (Application in Search): 사용자가 쿼리를 제출하면, 시스템은 이를 임베딩으로 변환한 다음 이 임베딩과 데이터베이스의 다양한 문서들의 임베딩 간의 코사인 유사도를 계산합니다. 코사인 유사도 점수가 더 높은 문서는 쿼리와 더 관련이 있다고 간주되어 검색 결과에서 더 높은 순위를 차지합니다.  

이러한 접근 방식은 시스템이 사용자 쿼리의 의미적 내용을 더 잘 포착하고 응답할 수 있게 하여, 단순히 특정 단어를 일치시키는 것보다 더 미묘하고 효과적인 검색 경험을 가능하게 합니다. 

 

 

[ 코사인 유사도와 시맨틱 검색 ]

Cosine Similarity and Text Semantic Search

 

 

 

LangChain과 ChatGPT LLM 모델을 사용해서 여러개의 템플릿을 미리 만들어놓고 질문에 가장 유사한 템플릿을 선택해서 대답을 생성하는 간단한 챗봇을 만들어보겠습니다. 

 

먼저, 터미널에서 langchain, openai, tiktoken 모듈을 pip install을 사용해서 설치합니다. 

 

! pip install -q langchain openai tiktoken

 

 

OpenAI의 ChatGPT를 LLM 모델로 사용할 것이므로 OpenAI API Key를 등록해줍니다. 

 

import os

os.environ["OPENAI_API_KEY"] = "sk-xxxx..."

 

 

필요한 모듈을 import 하고, Database PostgreSQL 과 관련된 template 1, 그리고 Cloud Kubernetes Docker 관련된 template 2 를 각각 정의해 줍니다. 

 

from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import PromptTemplate
from langchain.utils.math import cosine_similarity
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda, RunnablePassthrough

# Template 1: Database PostgreSQL
database_template = """You are an expert on database, especially open source PostgreSQL. 
You are good at answering questions about database in a concise manner. 

Here is a question:
{query}"""


# Template 2: Colud, Kubernetes, Docker
cloud_template = """You are an expert on cloud platform. 
You are good at answering questions especially on kubernetes and docker. 
The user is from Korean. Answer the question in Korean.

Here is a question:
{query}
"""

 

 

Text Embedding 모델로는 OpenAI의 1,536 차원을 가진 "text-embedding-ada-002" 를 사용하겠습니다. 

  - 여러개의 텍스트에 대해서는 embeddings.embed_documents(),

  - 하나의 query에 대해서는 embeddings.embed_query()

메소드를 사용해서 텍스트를 임베딩으로 변환합니다. 

 

# OpenAI Embeddings: https://platform.openai.com/docs/guides/embeddings
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

prompt_templates = [database_template, cloud_template]
prompt_embeddings = embeddings.embed_documents(prompt_templates)


query_embedding = embeddings.embed_query("What is PostgreSQL database?")

len(query_embedding)
# 1536

len(prompt_embeddings[0])
# 1536

query_embedding[:10]
# [0.011614119106739642,
#  -0.011661718070044624,
#  -0.011185729368317337,
#  -0.02718574244037603,
#  -0.043219754372800054,
#  0.02449300773999799,
#  -0.006510842295925382,
#  0.00851339435668751,
#  -0.021011491283506004,
#  -0.018604350362957857]

 

 

임베딩과 시맨틱 검색 (semantic search) 이 잘 작동하는지를 확인하기 위해 아래에 PostgreSQL Database, K8s and Docker 관련된 질문을 각각 해봤습니다. 코사인(Cosine Similarity)는 -1~1 사이의 값을 가지면 1에 가까울 수록 서로 유사하다고 판단할 수 있는데요, 첫번째 PostgreSQL 관련 질문에는 database_template 이 유사하다고 나왔고, 두번째 K8s and Docker 관련 짊누에서는 cloud_template 이 더 유사한 것으로 나왔으니 의도한 대로 잘 작동하네요. 

 

# Question 1: PostgreSQL database
query_embedding = embeddings.embed_query("What is PostgreSQL database?")
similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
print(similarity)
# [0.8684732  0.74399373]


# Question 2: K8s, Docker
query_embedding = embeddings.embed_query("What is Kubernetes and Docker?")
similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
print(similarity)
# array([0.75842006, 0.83432307])

 

 

 

템플릿들(templates)과 사용자 질문(user query) 텍스트를 임베딩으로 변환

--> cosine_similarity()  메소드로 템플릿과 질문 간 임베딩 간 코사인 유사도 계산 

--> 질문과 가장 유사한 템플릿 선택하여 반환

하는 과정을 prompt_router(input) 사용자 정의 함수로 정의해줍니다.

 

그리고 langchain_core.runnables.RunnableLambda(prompt_router) 를 사용해서 

 

그리고 '|'를 사용해서 

템플릿들(templates)과 사용자 질문(user query) 텍스트를 임베딩으로 변환

--> 템플릿과 질문 간 임베딩 간 코사인 유사도 계산 (calculate cosine similarity)

--> 질문과 가장 유사한 템플릿 선택하여 반환

--> 질문과 가장 유사한 템플릿을 사용하여 ChatGPT 모델로 답변 생성

--> ChatGPT 답변을 String으로 파싱하여 반환

 

하는 일련의 과정을 chaining 해주었습니다. 

 

# Define a UDF for routing prompts dynamically
def prompt_router(input):
    query_embedding = embeddings.embed_query(input["query"])
    similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
    most_similar = prompt_templates[similarity.argmax()]
    print("Using Database" if most_similar == database_template else "Using Cloud")
    return PromptTemplate.from_template(most_similar)
    

# Chaining
chain = (
    {"query": RunnablePassthrough()}
    | RunnableLambda(prompt_router)
    | ChatOpenAI()
    | StrOutputParser()
)

 

 

 

앞에서 정의한 chain을 invoke() 메소드를 사용해서 실행시켜줍니다.  

 

chain.invoke("What is PostgreSQL database?")

# Using Database
# 'PostgreSQL is an open-source relational database management system (RDBMS) 
# that offers advanced features and strong data integrity. It is widely known 
# for its robustness, scalability, and SQL compliance. 
# PostgreSQL supports various data types, including structured, semi-structured, 
# and unstructured data, making it suitable for a wide range of applications. 
# It offers features like ACID compliance, MVCC, JSON support, full-text search, 
# and extensive extensibility through procedural languages and extensions. 
# PostgreSQL is highly customizable and can be used for small to large-scale applications, 
# making it a popular choice among developers and enterprises.'

 

 

 

의도한 대로, 그리고 템플릿에 지시문을 작성한 대로 잘 작동하네요. 

(cloud_template 에는 사용자가 한국인이므로 한글로 답변을 생성하라고 지시문을 추가했었습니다.)

 

chain.invoke("What is Kubernetes and Docker?")

# Using Cloud
# 'Kubernetes와 Docker는 모두 컨테이너화된 애플리케이션을 관리하기 위한 클라우드 플랫폼 도구입니다.
# Docker는 컨테이너 기반 가상화 기술을 제공하는 플랫폼입니다. 
# 컨테이너는 애플리케이션을 격리된 환경에서 실행하고, 이식성과 확장성을 높여줍니다. 
# Docker는 애플리케이션의 종속성을 패키징하고 배포할 수 있도록 도와주며, 
# 애플리케이션을 컨테이너 이미지로 만들어 관리합니다.
# Kubernetes는 컨테이너 오케스트레이션 플랫폼으로, 도커 컨테이너의 배포, 확장, 관리를 
# 자동화합니다. Kubernetes는 여러 호스트에 걸쳐 컨테이너를 스케줄링하고, 서비스 디스커버리, 
# 로드 밸런싱, 자가 치유 등의 기능을 제공하여 애플리케이션 운영을 단순화합니다. 
# 또한 Kubernetes는 컨테이너의 상태를 모니터링하고 필요한 조치를 취할 수 있어 
# 안정적인 운영을 지원합니다.\n\n요약하자면, Docker는 컨테이너 기반 가상화 기술을 제공하고, 
# Kubernetes는 컨테이너 오케스트레이션 플랫폼으로 
# 컨테이너화된 애플리케이션의 배포와 관리를 용이하게 해주는 도구입니다.'

 

 

[ Reference ]

* LangChain - Routing by semantic similarity: 

https://python.langchain.com/docs/expression_language/cookbook/embedding_router

* OpenAI Embeddings: https://platform.openai.com/docs/guides/embeddings/what-are-embeddings

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요.  :-)

 

728x90
반응형
Posted by Rfriend
,

Python REPL(Read-Eval-Print Loop)은 Python 프로그래밍 언어의 대화형 인터프리터 환경을 나타냅니다. 이것은 사용자가 Python 코드를 입력하고 실행하며 결과를 즉시 확인할 수 있는 인터페이스를 제공합니다. 

  - Read 단계에서 사용자의 입력을 받습니다. 
  - Evaluation 단계에서 입력을 처리하고 Python 표현식으로 평가합니다. 
  - Print 단계에서 평가 결과를 사용자에게 출력합니다. 
  - Loop 단계에서 이 과정을 반복하며 다음 사용자 입력을 기다립니다. 

 

Python - REPL (Read-Eval-Print Loop)

(ChatGPT 에게 Python REPL 컨셉을 그림으로 그려달라고 했더니 위처럼 그렸네요. 나름 잘 그리네요. ㅋㅋ)

 


다음은 Python REPL이 어떻게 사용되는지에 대한 예시입니다. 

>>> 2 + 2
4
>>> print("Hello, World!")
Hello, World!
>>> x = 10
>>> x * 5
50



REPL은 Python 프로그래머들에게 코드 작성, 디버깅, 실험 등을 위한 강력한 도구로 자주 사용됩니다. 사용자가 코드를 입력하면 Python 인터프리터가 해당 코드를 실행하고 결과를 반환하며, 이러한 과정을 반복할 수 있습니다. REPL을 사용하면 빠르게 코드를 테스트하고 작업할 수 있어 개발 과정을 효율적으로 만들어 줍니다. 

 

 

이번 포스팅에서는 LangChain Expression Language (LCEL)와 ChatGPT를 사용해서 자연어를 인풋으로 입력하면 Python으로 코드를 짜고, Python REPL (Read-Eval-Print Loop) 방식으로 Python 코드를 실행해서 결과를 얻는 방법을 소개하겠습니다. 

 

 

먼저, langchin과 openai 모듈을 터미널에서 pip install 로 설치합니다. 

 

! pip install -q -U langchain openai

 

 

다음으로, OpenAI API Key를 설정해줍니다. 

 

import os

os.environ["OPENAI_API_KEY"] = "sk-xxxx..." # set with yours

 

 

Prompt에는 "사용자의 질문을 풀기 위한 Python 코드를 짜라. 사용자가 결과를 확인할 수 있도록 끝에 print(result) 코드를 포함해라. 단지 Python 코드만 반환하고 나머지는 반환하지 마라"는 지시문을 넣어주었습니다. 

 

LLM Model은 ChatGPT-4를 사용하였으며, temperature=0 으로 설정해서 사실에 기반한 일관성있고 보수적인 답변만 생성하도록 하였습니다. 

 

StrOutputParser() 로 LLM 모델이 생성한 답변(즉, Python codes)을 string 포맷으로 파싱합니다. 

 

마지막으로 PythonREPL() 메소드를 사용해서 ChatGPT가 생성한 Python codes 를 Python REPL 모드로 실행합니다. 

 

이러한 모든 절차를 '\'를 사용해서 chaining 해주었습니다. 

참고로, 여러개의 chain을 다시 chaining 할 수 있으며, PythonCode_chain 에다가 PythonREPL().run 을 chaing 하여 PythonCodeRun_chain 을 만들었습니다. 

 

#  파이썬 코드 생성 chain (Python codes generation)
PythonCode_chain = prompt | model | StrOutputParser()

# 파이썬 코드를 실행해서 결과를 보여주는 chain (Python REPL (Read-Eval-Print Loop))
PythonCodeRun_chain = PythonCode_chain | PythonREPL().run

 

 

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_experimental.utilities import PythonREPL

template = """
Write some python  code to solve the user's problem. 
Include print(result) at the end for user to check the result. 
Return only python code and nothing else."""

prompt = ChatPromptTemplate.from_messages(
    [("system", template), ("human", "{question}")]
)

model = ChatOpenAI(temperature=0, model_name="gpt-4")

#  Python codes generation
PythonCode_chain = prompt | model | StrOutputParser()

# Python REPL (Read-Eval-Print Loop)
PythonCodeRun_chain = PythonCode_chain | PythonREPL().run

 

 

 

자연어로 3가지 질문을 던져보았는데요, ChatGPT-4가 모두 정확하게 Python 코드도 생성하고 잘 풀어주네요. 

 

 

Q: y = 5 + 3 * x. If x is 10, then what is y?

A: y = 5 + 3 * 10 = 35

 

## ----------------------------------------
## Q: y= 5 + 3 * x. If x is 10, what is y?
## ----------------------------------------

## (a) Python codes generation
PythonCode_chain.invoke({"question": "y= 5 + 3 * x. If x is 10, what is y?"})
# 'x = 10\ny = 5 + 3 * x\nprint(y)'

## (b) Run Python codes
PythonCodeRun_chain.invoke({"question": "y= 5 + 3 * x. If x is 10, what is y?"})
# '35\n'

 

 

 

Q: What is 2 multiplied by 10? 

A: 2 * 10 = 20

 

## ---------------------------
## Q: What is 2 multiplied by 10?
## ---------------------------

## (a) Python codes generation
PythonCode_chain.invoke({"question": "What is 2 multiplied by 10?"})
# 'result = 2 * 10\nprint(result)'

## (b) Run Python codes
PythonCodeRun_chain.invoke({"question": "What is 2 multiplied by 10?"})
# '20\n'

 

 

 

Q: How much is 5 factorial?

A: 5! = 5 * 4* 3* 2* 1 = 120

 

## ---------------------------
## Q: How much is 5 factorial?
## ---------------------------

## (a) Python codes generation
PythonCode_chain.invoke({"question": "How much is 5 factorial?"})
# 'import math\n\nresult = math.factorial(5)\nprint(result)'

## (b) Run Python codes
PythonCodeRun_chain.invoke({"question": "How much is 5 factorial?"})
# '120\n'

 

 

위에 간단한 자연어 질의에는 모두 정확하게 답변을 했는데요, 그래도 반드시 Python codes 를 확인하는 절차를 밟는게 필요해보입니다. 

 

 

[ Reference ]

* LangChain - Code Writing: 

https://python.langchain.com/docs/expression_language/cookbook/code_writing

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

대규모 언어 모델 (Large Language Model, LLM) 이 사실에 입각하지 않고 그럴싸한 답변을 생성하는 것을 환각 (Hallucinations) 이라고 합니다. LLM의 환각을 방지하는 방법 중의 하나로 RAG (Retrieval-Agumented Generation) 이 많이 사용되고 있는데요, 이전에 소개했던 RAG 는 비정형 데이터인 텍스트를 임베딩하여 Semactic Search를 하여 Context로 사용할 문서를 LLM에 인풋으로 넣어주는 방법이었습니다. 

 

이번 포스팅에서는 LangChain과 ChatGTP를 이용해서 PostgreSQL, Greenplum Database의 테이블에 행, 열의 테이블에 저장되어 있는 정형 데이터 (Structured Data)에 대해서 SQL Query를 사용해서 질의를 해서 사실 (Fact) 데이터를 Query 해와서, 이를 기반으로 LLM 모델이 답변을 생성하는 방법을 소개하겠습니다. 자연어로 질문하고 자연어로 답변을 받게 되면 DB나 SQL Query를 모르는 일반 사용자도 DB에 저장되어 있는 사실에 기반한 최신의 내부 데이터까지도 활용해서 정확한 답변을 받을 수 있게 됩니다. 

 

 

먼저, PostgreSQL, Greenplum 데이터베이스에 대해서 간략히 소개하겠습니다. 

 

 

1. PostgreSQL 데이터베이스란? 

 

PostgreSQL, 종종 Postgres로 불리는 이 데이터베이스는 강력한 오픈 소스 객체-관계형 데이터베이스 시스템입니다. 이는 신뢰성, 기능의 견고함, 그리고 성능 면에서 좋은 평판을 가지고 있습니다.  

PostgreSQL은 객체-관계형 데이터베이스로서, 전통적인 관계형 데이터베이스 기능인 테이블 기반 데이터 저장 및 SQL(Structured Query Language) 쿼리와 더불어 객체 지향 데이터베이스 기능인 객체와 클래스에 데이터를 저장하는 것을 지원합니다. 

이는 무료로 자유롭게 사용할 수 있는 오픈 소스 소프트웨어입니다. 그 소스 코드는 누구나 열람하거나 개선하길 원하는 사람에게 공개되어 있습니다. 

PostgreSQL의 핵심 강점 중 하나는 확장성입니다. 사용자는 자신만의 데이터 타입, 인덱스 타입, 함수 언어 등을 정의할 수 있습니다. 

 

2. Greenplum 데이터베이스란? 

 

Greenplum 데이터베이스는 고급 기능을 갖춘 오픈 소스 데이터 웨어하우스입니다. 이는 페타바이트 규모의 빅데이터에 대해 강력하고 빠른 분석을 제공합니다. 원래 PostgreSQL에서 파생되었지만, Greenplum 데이터베이스는 그 능력과 성능을 크게 확장해 발전시켰습니다. 

Greenplum은 Shared-Nothing, 대규모 병렬 처리 아키텍처(Massively Parallel Processing Architecture, MPP)를 사용합니다. 이는 데이터가 여러 세그먼트로 나뉘고 각 세그먼트가 병렬로 처리되어 고성능 및 대규모 데이터 분석을 가능하게 한다는 의미입니다. 

확장성(Scalability)을 위해 설계된 Greenplum은 큰 데이터 볼륨을 처리할 수 있습니다. 이는 많은 서버에 걸쳐 확장되어 큰 데이터 볼륨과 복잡한 쿼리를 관리할 수 있습니다. 

Greenplum은 고급 분석 기능을 지원합니다. 이는 Python, R 및 기타 통계 및 머신 러닝 라이브러리와 통합되어 데이터베이스 내에서 고급 데이터 분석을 직접 수행할 수 있게 합니다. 

(Greenplum DB 소개 참고: https://rfriend.tistory.com/377)

 

 

[ LangChain과 ChatGPT를 사용하여 자연어로 PostgreSQL, Greenplum에 Query하는 Workflow ]

LangChain - ChatGPT - PostgreSQL, Greenplum DB에 자연어로 질의하여 Query 하기

 

 

 

3. LangChain, ChatGPT로 PostgreSQL에 자연어로 Query해서 답변 생성하기

 

(1) 사용자가 애플리케이션에서 자연어로 질의
(2) LangChain이 LLM 모델에 사용자 질문을 SQL Query로 변환 요청
(3) LLM 모델이 DB Schema 정보에 기반해 사용자 질문에 대한 SQL Query를 생성해서 반환
(4) LangChain이 PostgreSQL, Greenplum DB에 SQL Query 실행 요청
(5) PostgreSQL, Greenplum DB에서 SQL Query 실행하여 결과 반환
(6) LangChain이 사용자 질문과 SQL Query, Query 결과를 기반으로 LLM 모델에 답변 생성 요청 
(7) LLM 모델이 사용자 질문과 Query 결과를 기반으로 생성한 자연어 답변 반환
(8) LangChain이 애플리케이션의 사용자 UI에 LLM 모델이 생성한 자연어 답변 반환

 

 

 

(0) 준비사항: PostgreSQL에 예제 테이블 생성하고 데이터 집어넣기

 

먼저, 예제로 사용할 PostgreSQL DB에 iris 라는 테이블을 만들어보겠습니다. 그리고, 나중에 자연어로 질의할 내용을 미리 SQL Query로 결과를 조회해보았습니다. 

 

DROP TABLE IF EXISTS iris;
CREATE TABLE iris (id INT, sepal_length FLOAT, sepal_width FLOAT,
                    petal_length FLOAT, petal_width FLOAT,
                   class_name text);

INSERT INTO iris VALUES
(1,5.1,3.5,1.4,0.2,'Iris-setosa'),
(2,4.9,3.0,1.4,0.2,'Iris-setosa'),
(3,4.7,3.2,1.3,0.2,'Iris-setosa'),
(4,4.6,3.1,1.5,0.2,'Iris-setosa'),
(5,5.0,3.6,1.4,0.2,'Iris-setosa'),
(6,5.4,3.9,1.7,0.4,'Iris-setosa'),
(7,4.6,3.4,1.4,0.3,'Iris-setosa'),
(8,5.0,3.4,1.5,0.2,'Iris-setosa'),
(9,4.4,2.9,1.4,0.2,'Iris-setosa'),
(10,4.9,3.1,1.5,0.1,'Iris-setosa'),
(11,7.0,3.2,4.7,1.4,'Iris-versicolor'),
(12,6.4,3.2,4.5,1.5,'Iris-versicolor'),
(13,6.9,3.1,4.9,1.5,'Iris-versicolor'),
(14,5.5,2.3,4.0,1.3,'Iris-versicolor'),
(15,6.5,2.8,4.6,1.5,'Iris-versicolor'),
(16,5.7,2.8,4.5,1.3,'Iris-versicolor'),
(17,6.3,3.3,4.7,1.6,'Iris-versicolor'),
(18,4.9,2.4,3.3,1.0,'Iris-versicolor'),
(19,6.6,2.9,4.6,1.3,'Iris-versicolor'),
(20,5.2,2.7,3.9,1.4,'Iris-versicolor'),
(21,6.3,3.3,6.0,2.5,'Iris-virginica'),
(22,5.8,2.7,5.1,1.9,'Iris-virginica'),
(23,7.1,3.0,5.9,2.1,'Iris-virginica'),
(24,6.3,2.9,5.6,1.8,'Iris-virginica'),
(25,6.5,3.0,5.8,2.2,'Iris-virginica'),
(26,7.6,3.0,6.6,2.1,'Iris-virginica'),
(27,4.9,2.5,4.5,1.7,'Iris-virginica'),
(28,7.3,2.9,6.3,1.8,'Iris-virginica'),
(29,6.7,2.5,5.8,1.8,'Iris-virginica'),
(30,7.2,3.6,6.1,2.5,'Iris-virginica');

SELECT * FROM iris ORDER BY id LIMIT 5;
-- 5.1	3.5	1.4	0.2	"Iris-setosa"
-- 4.9	3	1.4	0.2	"Iris-setosa"
-- 4.7	3.2	1.3	0.2	"Iris-setosa"
-- 4.6	3.1	1.5	0.2	"Iris-setosa"
-- 5	3.6	1.4	0.2	"Iris-setosa"

SELECT 
    AVG(sepal_length) 
FROM iris 
WHERE class_name = 'Iris-setosa';
-- 4.859999999999999

SELECT 
	class_name,
	AVG(sepal_length) 
FROM iris 
GROUP BY class_name
ORDER BY class_name;

-- class_name         avg
-- "Iris-setosa"	  4.85
-- "Iris-versicolor"  6.10
-- "Iris-virginica"   6.57


SELECT 
	class_name
	, MAX(sepal_width) 
FROM iris 
GROUP BY class_name 
ORDER BY class_name;

-- class_name          max
-- "Iris-setosa"	   3.9
-- "Iris-versicolor"   3.3
-- "Iris-virginica"    3.6

 

 

 

(0) 준비사항: Python 모듈 설치

 

LLM 모델을 통한 답변 생성에 필요한 openai, langchain, 그리고 PostgreSQL, Greenplum DB access를 위해 필요한 psycopg2 모듈을 pip install을 사용해서 설치합니다. 

 

! pip install -q openai langchain psycopg2

 

 

(0) 준비사항: OpenAI API Key 설정

 

OpenAI의 ChatGPT를 사용하기 위해 필요한 OpenAI API Key를 등록합니다. 

(OpenAI API Key 발급 방법은 https://rfriend.tistory.com/794 를 참고하세요)

 

import os

# setup OpenAI API Key with yours
os.environ["OPENAI_API_KEY"]="sk-xxxx..." # set with yours

 

 

(0) 준비사항: PostgreSQL DB 연결

 

이제 LangChain의 SQLDatabase() 클래스를 사용해서 PostgreSQL DB에 연결하겠습니다. 

(DB credientials 는 사용 중인 걸로 바꿔주세요)

 

# Connect to the PostgreSQL DB
from langchain.utilities import SQLDatabase

# set with yours
username='postgres'
password='changeme'
host='localhost'
port='5432'
database='postgres'

pg_uri = f"postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}"
        
db = SQLDatabase.from_uri(pg_uri)

 

 

(0) 준비사항: Query를 위한 Helper Function 정의

 

DB의 Table 정보 (테이블 이름, 칼럼 정보) 를 가져오고, SQL Query를 실행할 수 있는 Helper 함수를 정의합니다. 이제 준비가 다 되었네요. 

 

# Helper functions
def get_schema(_):
    return db.get_table_info()
    
def run_query(query):
    return db.run(query)

 

 

 

(1) 사용자가 애플리케이션에서 자연어로 질의

 

사용자가 자연어로 질의한 질문과 DB테이블 정보를 인풋으로 받아서, "DB 테이블 정보 ("scheam")와 사용자 질문 ("question")이 주어졌을 때 PostgreSQL Query를 생성하라"고 프롬프트에 지시문을 생성하였습니다. 

 

# Prompt for generating a SQL query
from langchain.prompts import ChatPromptTemplate

template_query = """
Based on the table schema below, \
Write a PostgreSQL query that answer the user's question:
{schema}

Question: {question}
SQL Query:"""

prompt = ChatPromptTemplate.from_template(template_query)

 

 

 

(2) LangChain이 LLM 모델에 사용자 질문을 SQL Query로 변환 요청

 

LLM에는 ChatGPT-4 모델을 사용하고, temperature=0 으로 해서 사실에 기반해서 일관성 있고 보수적인 답변을 생성하도록 설정했습니다. 

 

# Chaining prompt, LLM model, and Output Parser
from langchain.chat_models import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

model = ChatOpenAI(temperature=0, model_name='gpt-4')

sql_response = (
    RunnablePassthrough.assign(schema=get_schema) 
    | prompt
    | model.bind(stop=["\nSQLResult:"])
    | StrOutputParser()
)

 

 

 

(3) LLM 모델이 DB Schema 정보에 기반해 사용자 질문에 대한 SQL Query를 생성해서 반환

 

sql_response.invoke({"question": "What is the average of sepal length for Iris-setosa?"})
# "SELECT AVG(sepal_length) \nFROM iris \nWHERE class_name = 'Iris-setosa';"

 

 

 

(4) LangChain이 PostgreSQL, Greenplum DB에 SQL Query 실행 요청

 

아래와 같이 "DB 테이블 정보 ("schema"), 사용자 질문 ("question"), SQL Query ("query"), SQL Query 결과 ("response") 가 주어졌을 때 자연어(natural language)로 답변을 생성하라"고 프롬프트 지시문을 만들었습니다. 

 

# Prompt for generating the final answer by running a SQL query on DB
template_response = """
Based on the table schema below, question, sql query, and sql response, \
write a natural language response:
{schema}

Question: {question}
SQL Query: {query}
SQL Response: {response}"""

prompt_response = ChatPromptTemplate.from_template(template_response)

 

 

 

(5) PostgreSQL, Greenplum DB에서 SQL Query 실행하여 결과 반환

 

'|' 를 사용해서 앞에서 정의한 SQL Query 생성하는 chain과 Query를 실행한 결과를 받아서 자연어로 답변을 생성하는 chain을 모두 엮어서 Chaining 하였습니다. 

 

full_chain = (
    RunnablePassthrough.assign(query=sql_response)
    | RunnablePassthrough.assign(
        schema=get_schema, 
        response=lambda x: db.run(x["query"]),
    )
    | prompt_response
    | model
    | StrOutputParser()
)

 

 

 

(6) LangChain이 사용자 질문과 SQL Query, Query 결과를 기반으로 LLM 모델에 답변 생성 요청 

(7) LLM 모델이 사용자 질문과 Query 결과를 기반으로 생성한 자연어 답변 반환

 

자연어로 3개의 질문을 해보았는데요, 기대했던 SQL Query 문이 생성되고 Query 질의 결과를 기반으로 답변이 모두 정확하게 생성되었습니다. 

 

Q1: What is the average of sepal length for Iris-setosa?

 

## Q: What is the average of sepal length for Iris-setosa?

sql_response.invoke({"question": "What is the average of sepal length for Iris-setosa?"})
# "SELECT AVG(sepal_length) \nFROM iris \nWHERE class_name = 'Iris-setosa';"

full_chain.invoke({"question": "What is the average of sepal length for Iris-setosa?"})
# 'The average sepal length for Iris-setosa is approximately 4.86.'

 

 

Q2: What is the average of sepal length by class name?

 

## Q: What is the average of sepal length by class name?

sql_response.invoke({"question": "What is the average of sepal length by class name?"})
# 'SELECT class_name, AVG(sepal_length) \nFROM iris \nGROUP BY class_name;'


full_chain.invoke({"question": "What is the average of sepal length by class name?"})
#'The average sepal length for the Iris-versicolor class is 6.1, 
#for the Iris-setosa class is approximately 4.86, 
#and for the Iris-virginica class is 6.57.'

 

 

Q3: What is the maximum value of sepal width by class name?

 

## Q: What is the maximum value of sepal width by class name?

sql_response.invoke({"question": "What is the maximum value of sepal width by class name?"})
# 'SELECT class_name, MAX(sepal_width) \nFROM iris \nGROUP BY class_name;'


full_chain.invoke({"question": "What is the maximum value of sepal width by class name?"})
# "The maximum sepal width for the class 'Iris-versicolor' is 3.3, 
# for 'Iris-setosa' is 3.9, 
# and for 'Iris-virginica' is 3.6."

 

 

위의 예는 매우 간단한 질문이어서 하나의 테이블 내에서 Join 없이도 모두 정확하게 Querying 이 되었는데요, 그래도 mission critical한 업무에서는 사용에 유의할 필요가 있습니다. 왜냐하면 실무에서 사용하는 SQL Query 문의 경우 여러개의 테이블을 Join 해야 될 수도 있고, Where 조건절에 Business Logic이 복잡하게 반영되어야 할 경우도 있고, 테이블 이름/설명이나 변수명/설명이 LLM 모델이 사용자의 질의와 매핑해서 사용하기에 부적절한 경우도 있을 수 있어서 SQL Query가 부정확할 수 있기 때문입니다.

 

따라서 반드시 사용자 질의에 대한 SQL Query 문을 같이 확인해보는 절차가 필요합니다. 

 

그리고 SQL Query Generator 를 잘하는 LLM 모델, 자사 회사 내 SQL Query 문으로 Fine-tuning한 LLM 모델에 대한 시도도 의미있어 보입니다. 

 

 

[ Reference ]

* LangChain - Querying a SQL DB:

https://python.langchain.com/docs/expression_language/cookbook/sql_db

* PostgreSQL 설치(OS 별 packages, installers): https://www.postgresql.org/download/

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서 RAG (Retrieval-Augmented Generation) 이란 무엇이고 LangChain으로 어떻게 구현하나에 대해서 소개하였습니다. 

 

이번 포스팅에서는 LangChain으로 RAG (Retrieval-Augmented Generation)을 구현할 때 

  - Web 에서 문서를 가져와서 분할하고 

  - OpenAI의 Text Embedding 모델을 사용해서 Embeddings 으로 변환을 하고 

  - Chroma Vector DB 에 저장, 검색하여 

  - ChatGPT 모델로 질문에 답변을 생성

하는 방법을 소개하겠습니다. 

 

 

1. 시장 내 Vector DB 의 구분

 

먼저, 텍스트를 벡터 임베딩으로 변환하여 저장하고 의미론적 검색을 할 때 사용하는 Vector DB에 대해서 잠깐 살펴보겠습니다.

 

시장의 Vector DB는 (a) Database 용도가 Vector DB 전용인지 아니면 Vector search를 지원하는 일반 DB인지 여부, (b) Open source 인지 상업용인지 여부에 따라서 아래 그림처럼 4/4분위로 구분할 수 있습니다. 일반 DB가 최근 매우 빠른 속도로 Vector DB 기능 (벡터 데이터 유형 지원, 저장, 인덱싱, 검색 등) 을 지원하기 시작했습니다. 따라서 일반 DB가 Vector DB 기능을 지원하는 것은 시간 문제일것 같습니다. 

 

이번 포스팅에서는 Vector DB 전용 (dedicated vector database) 이면서 오픈소스인 Croma DB를 사용해서 RAG 를 구현해 보겠습니다. 

 

[ Vector DB Landscape ]

Vector DB landscape

* 이미지 출처: https://blog.det.life/why-you-shouldnt-invest-in-vector-databases-c0cd3f59d23c

 

 

 

2. Croma Vector DB와 LangChain을 사용한 RAG(Retrieval-Augmented Generation) 구현

 

Chroma는 오픈소스 임베딩 데이터베이스 (Open-source Vector Database)입니다. Chroma는 지식, 사실, 기술을 LLM에 쉽게 플러그인 할 수 있게 함으로써 LLM 앱을 구축하는 것을 쉽게 해줍니다. Chroma DB 를 사용해서 텍스트 문서를 쉽게 관리하고, 텍스트를 임베딩으로 변환하며, 유사성 검색을 할 수 있습니다.  


Chroma DB의 주요 기능으로는 다음과 같은 것이 있습니다. 

- 풍부한 기능: 쿼리, 필터링, 밀도 추정, 그 외 여러 기능들
- LangChain (파이썬 및 자바스크립트), LlamaIndex 지원 가능
- 파이썬 노트북에서 실행되는 것과 동일한 API가 프로덕션 클러스터로 확장

 

RAG using Chroma Vector DB

* 이미지 출처: https://docs.trychroma.com/

 

먼저 openai, langchain, langchainhub, tiktoken, chromadb 모듈을 터미널에서 pip install을 사용해서 설치합니다. 

 

!pip install --q openai langchain langchainhub tiktoken chromadb

 

 

(1) WebBaseLoader() 를 이용해서 웹사이트 문서 로딩하기

 

# Load documents from web
from langchain.document_loaders import WebBaseLoader

web_loader = WebBaseLoader([
    "https://python.langchain.com/docs/get_started/introduction",   # LangChain Introduction
    "https://python.langchain.com/docs/modules/data_connection/" # LangChain Retrieval
    ]
)

data = web_loader.load()

 

 

 

(2) LangChain의 RecursiveCharacterTextSplitter()를 사용해서 문서를 Chunk 로 분할하기

 

# Split documents into chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500, 
    chunk_overlap = 0
)

all_splits = text_splitter.split_documents(data)

all_splits[0]

# Document(page_content='Introduction | \uf8ffü¶úÔ∏è\uf8ffüîó Langchain', 
# metadata={'source': 'https://python.langchain.com/docs/get_started/introduction', 
# 'title': 'Introduction | \uf8ffü¶úÔ∏è\uf8ffüîó Langchain', 
# 'description': 'LangChain is a framework for developing applications 
# powered by language models. It enables applications that:', 
# 'language': 'en'})

 

 

 

(3) OpenAIEmbeddings()를 사용해서 텍스트를 임베팅으로 변환하여 Chroma Vector DB에 저장하기

 

# Transform into Text Embeddings and Store at Chroma Vector DB
import os
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

os.environ["OPENAI_API_KEY"] = "sk-xxxxx...."

vectorstore = Chroma.from_documents(
    documents=all_splits, 
    embedding=OpenAIEmbeddings()
    )

 

 

 

(4) LangChainHub에서 RAG Prompt 가져오기

 

필요에 따라서는 아래 RAG Prompt template을 참고해서 일부 수정해서 사용해도 됩니다. 

 

# RAG prompt
from langchain import hub

prompt = hub.pull("rlm/rag-prompt")

print(prompt)
# ChatPromptTemplate(input_variables=['context', 'question'], 
# messages=[HumanMessagePromptTemplate(prompt=PromptTemplate(
#    input_variables=['context', 'question'], 
#    template=
#        "You are an assistant for question-answering tasks. 
#         Use the following pieces of retrieved context to answer the question. 
#         If you don't know the answer, just say that you don't know. 
#         Use three sentences maximum and keep the answer concise.
#         \nQuestion: {question} 
#         \nContext: {context} 
#         \nAnswer:"))
# ])

 

 

 

(5) ChatOpenAI() 모델로 언어 모델 설정하기

 

model_name="gpt-4"로 설정해주었으며, temperature=0 으로 해서 보수적이고 일관적인 답변이 생성되도록 하였습니다. 

 

# LLM model
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI

model = ChatOpenAI(model_name="gpt-4", temperature=0)

 

 

 

(6) RetrievalQA() 을 사용해서 LLM Model, Retriever, Prompt 를 하나의 객체로 묶기

 

# Retrieval QA
qa_chain = RetrievalQA.from_chain_type(
    llm=model, 
    retriever=vectorstore.as_retriever(),
    chain_type_kwargs={"prompt": prompt}
)

question  ="What is a LangChain?"
result = qa_chain({"query": question})

result["result"]
# LangChain is a framework for developing applications powered by language models. 
# It enables applications to be context-aware and reason based on provided context. 
# The LangChain framework includes composable tools and integrations for working with 
# language models, off-the-shelf chains for higher-level tasks, 
# and allows for customization of existing chains and building new ones.

 

 

사용자의 질문과 관련이 있는 웹사이트 문서를 검색(Retrieval)하여 이를 Context 정보로 참조해서(Augmented) 답변을 잘 생성(Generation)하고 있네요. 

 

question  ="What is Retrieval in LangChain?"
result = qa_chain({"query": question})

result["result"]
# Retrieval in LangChain is a part of the Retrieval Augmented Generation (RAG) process 
# where external data is fetched and passed to the Language Model during the generation step. 
# LangChain supports various retrieval algorithms and provides all the necessary components 
# for RAG applications. 
# It also allows for easy swapping between vector stores and supports simple semantic search 
# along with a collection of algorithms to enhance performance.

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 인메모리 키-값 저장소인 Redis 와 LangChain을 사용하여 이전 대화 내용에 대한 기억(memory, messages history)을 사용하여 답변을 할 수 있는 LLM 모델 체인을 만들어보겠습니다. 

 

1. Redis란 무엇인가? (What is Redis?)

2. LangChain에서 Redis를 사용한 메시지 기록 (Use of Redis for Message History in LangChain)

3. LangChain, Redis를 이용하여 기억(Memory)을 가진 LLM 모델 체인 만들기

 

 

1. Redis란 무엇인가? (What is Redis?)

 

Redis는 고성능, 유연성 및 다양한 기능으로 알려진 오픈 소스, 인 메모리 키-값 저장소(in-memory key-value store)입니다. 일반적으로 데이터베이스, 캐시 및 메시지 브로커로 사용됩니다. 여기에서 Redis와 LangChain 애플리케이션에서의 메시지 기록 사용에 대해 간략히 설명하겠습니다. 


(1) 인 메모리 저장 (In-Memory Storage): Redis는 데이터를 메모리에 저장하여 매우 빠른 읽기 및 쓰기 작업을 가능하게 합니다. 이는 속도가 중요한 시나리오에 이상적입니다. 

(2) 키-값 저장소 (Key-Value Store): 키와 그 값이 쌍을 이루는 키-값 저장 모델로 작동합니다. 이 값들은 문자열, 해시, 리스트, 세트 등이 될 수 있습니다. 

(3) 지속성 (Persistence): 메모리 기반임에도 불구하고, Redis는 디스크에 데이터를 지속시킬 수 있는 옵션을 제공하여, 재시작 시 데이터 손실을 방지합니다. 이는 스냅샷과/또는 추가 전용 파일(Append Only Files, AOF)을 사용하여 수행됩니다. 

(4) 확장성 및 고가용성 (Scalability and High Availability): Redis는 마스터-슬레이브 복제를 지원하여 확장성을 높이고 가용성을 증가시킵니다. Redis Sentinel은 모니터링 및 장애 조치를 통해 고가용성을 제공합니다. 

(5) 데이터 구조 (Data Structure): Redis는 문자열, 해시, 리스트, 세트, 정렬된 세트, 비트맵, 하이퍼로그로그, 지오스페이셜 인덱스 등 다양한 데이터 구조를 지원합니다. 

(6) Pub/Sub 기능: 내장된 Pub/Sub(게시/구독) 메시징 패러다임을 지원하여, 메시지 지향 미들웨어에 적합합니다.

 



2. LangChain에서 Redis를 사용한 메시지 기록 (Use of Redis for Message History in LangChain)

위의 Redis란 무엇인가라는 소개글에서 언급한 특성이 LangChain과 언어 모델을 사용하여 애플리케이션을 구축하기 위한 프레임워크에도 그대로 적용됩니다. Redis는 채팅 메시지 기록을 관리하는 데 효과적으로 사용될 수 있습니다. 

(1) 대화 저장 (Storing Conversations): Redis는 사용자와 챗봇 또는 언어 모델 간에 교환된 각 메시지를 추적하여 대화 기록을 저장할 수 있습니다. 이는 지속적인 대화에서 맥락을 유지하는 데 중요합니다. 

(2) 빠른 접근 (Fast Access): 인 메모리 특성 덕분에, Redis는 대화 기록에 대한 신속한 접근을 제공합니다. 이는 응답 시간이 중요한 실시간 애플리케이션에 필수적입니다. 

(3) 확장성 (Scalability): LangChain 애플리케이션이 성장함에 따라, Redis는 증가된 데이터 부하와 더 많은 동시 대화를 수용할 수 있도록 확장할 수 있습니다. 

(4) 지속성 (Persistence): 대화 기록이 항상 장기 저장이 필요한 것은 아니지만, Redis의 지속성 메커니즘은 시스템 재시작이나 실패 시 진행 중인 대화가 손실되지 않도록 보장합니다. 

(5) 간편한 통합 (Easy Integration): Redis는 다양한 프로그래밍 언어와 프레임워크, LangChain을 포함하여 간편한 통합을 제공합니다. 이는 개발자에게 접근하기 쉬운 선택입니다. 

요약하자면, Redis는 채봇과 상호작용에서 맥락과 연속성을 유지하는 데 필수적인 대화 데이터의 신속한 검색 및 조작을 보장함으로써, LangChain 애플리케이션에서 메시지 기록을 저장하고 관리하기 위한 고성능 데이터베이스 솔루션으로서 역할을 합니다.

 

 

대부분의 LLM 애플리케이션은 대화형 인터페이스를 가지고 있습니다. 대화의 필수적인 구성 요소 중 하나는 대화 중에 이전에 도입된 정보를 참조할 수 있는 능력입니다. 기본적으로 대화 시스템은 과거 메시지의 일부 창을 직접 접근할 수 있어야 합니다. 더 복잡한 시스템은 지속적으로 업데이트되는 세계 모델을 가질 필요가 있으며, 이를 통해 엔터티와 그 관계에 대한 정보를 유지하는 등의 작업을 수행할 수 있습니다. 

우리는 과거 상호 작용에 대한 정보를 저장하는 이 능력을 '메모리(Memory)'라고 부릅니다. LangChain은 시스템에 메모리를 추가하기 위한 많은 유틸리티를 제공하며, Redis 가 대화 내역을 기록하는 메모리 (Memory) 역할을 해줄 수 있습니다. 이러한 유틸리티는 단독으로 사용되거나 체인에 원활하게 통합될 수 있습니다. 

메모리 시스템은 두 가지 기본 작업, 즉 읽기와 쓰기(Read and Write)를 지원해야 합니다. 모든 체인이 특정 입력을 기대하는 핵심 실행 논리를 정의한다는 것을 기억하세요. 이러한 입력 중 일부는 사용자로부터 직접 옵니다만, 일부 입력은 메모리에서 올 수  있습니다. 체인은 주어진 실행에서 두 번 메모리 시스템과 상호 작용합니다. 


1. 초기 사용자 입력을 받은 후(AFTER), 핵심 논리를 실행하기 전(BEFORE)에, 체인은 메모리 시스템에서 읽어 사용자 입력을 보완합니다. 


2. 핵심 논리를 실행한 후(AFTER), 답변을 반환하기 전(BEFORE)에, 체인은 현재 실행의 입력 및 출력을 메모리에 기록하여, 미래의 실행에서 참조할 수 있도록 합니다. 

 

LangChain - Redis 를 사용해서 이전 대화 내용을 기억으로 추가하기

 

 

3. LangChain, Redis를 이용하여 기억(Memory)을 가진 LLM 모델 체인 만들기

 

먼저, openai, langchain, redis 모듈이 설치되어 있지 않다면 pip install을 사용해서 설치하도록 합니다. 

 

! pip install openai langchain redis

 

 

다음으로, Redis 데이터베이스를 만들어야 하는데요, 간단한 방법 중의 하나로 UpStash 에서 Redis database API 를 만들어보겠습니다. UpStash 사이트에서 회원가입하고 Create Redis Database 를 선택해서 'memory' 라는 이름을 부여해주면 됩니다. 사용량 만큼 과금하는 서비스인데요, 초반 무료 trial 시도해볼 수 있으므로 학습 용도로 사용해보기에 좋습니다. 

 

(* 참고: Docker로 Redis 를 Local에 설치해서 사용하기 ==> https://hub.docker.com/_/redis )

 

아래 화면 캡쳐에서 하단의 "Connect to your database - redis-cli" 란에서 redis-cli -u 다음 부분부터 있는 URL을 복사해서 가져옵니다. 

 

Serverless Database with Redis API by UpStash

 

 

Redis 를 사용하기 위한 URL을 가져왔습니다. 

 

# Redis URL for access
REDIS_URL = 'redis://default:xxxxx...@apn1-precious-lab-35237.upstash.io:35237'

 

 

이전 대화에 대한 기억(Memory)을 가지고 있지 않은 ChatModel은 이전 대화에 기반한 연속적인 대화가 불가능합니다. 아래의 예처럼 "My name is neo." 라고 이미 말을 했는데, 다음번에 "What's my name?" 이라고 다시 물어보면 ChatModel은 모른다고 말합니다. 

 

import os
from langchain.chat_models import ChatOpenAI

os.environ["OPENAI_API_KEY"] = "sk-xxxxx..."

## ChatModel doesn't have memory! 
model = ChatOpenAI(model_name="gpt-4")
model.invoke("Hi, my name is Lion King.")

# Out[16]:
# AIMessage(content='Hello Lion King! How can I assist you today?')


## -- No memory of previous conversations! --
model.invoke("What's my name?")

# Out[17]:
# AIMessage(content="I'm sorry, 
# but I don't have access to personal information about individuals 
# unless it has been shared with me in the course of our conversation.")

 

 

이제 LangChain과 Redis를 사용해서 ChatModel에게 이전 대화에 대한 기억(Memory)을 추가해보겠습니다. 

 

LangChain의 RunnableWithMessageHistory 클래스는 대화 기록 (Message History)를 추가할 수 있도록 해줍니다. 그리고 Langchain.memory.chat_message_histories 에서 RedisChatMessageHistory 메소드를 가져와서 Redis 에 대화 내역을 쓰고 Redis에서 읽어오기를 하도록 하겠습니다. 

 

아래에 Prompt 에서 MessagePlaceholder(variable_name="history") 의 "history" key에는 과거 대화 기록이 인풋으로 들어가게 됩니다. 

 

ChatModel 은 "gpt-4"로 설정했으며, '|' 으로 Prompt와 ChatModel을 chaining 하여 chain 이라는 이름으로 객체를 만들었습니다. 

 

import os
from langchain.chat_models import ChatOpenAI
from langchain.memory.chat_message_histories import RedisChatMessageHistory
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

os.environ["OPENAI_API_KEY"] = "sk-xxxxx..."

template = ChatPromptTemplate.from_messages(
    [
        ("system", "You're a trustworthy AI assistant. Answer the question below. \
                    If your don't know, just say you don't know it."), 
        MessagesPlaceholder(variable_name="history"),
        ("human", "{question}"),
    ])

model = ChatOpenAI(model_name="gpt-4")
chain = template | model

 

 

 

다음으로, RunnableWithMessageHistory() 메소드에 앞에서 정의한 chain, 그리고 이전 대화를 쓰고 읽을 인메모리 키-값 저장소인 Redis를 사용하기 위해 RedisChatMessageHistory(session_id, url=REDIS_URL) 을 lambda 함수로 정의해주었습니다. 그리고

   - input_messages_key="question",

   - history_messages_key="history"

로 지정해서 사용자의 질문("question")과 이전 대화 기억 ("history") 을 읽어와서 인풋으로 사용할 수 있도록 해주었습니다. 

 

## Chain with History by using Redis
chain_with_history = RunnableWithMessageHistory(
    chain, 
    lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL), 
    input_messages_key="question", 
    history_messages_key="history",
)

 

 

 

이제 다시 한번 이름을 알려주고, 다시 한번 이름이 뭔지 물어보겠습니다. 

(session_id 는 사용자별로 혹은 대화 주제별로 unique 한 값으로 아무 값이나 넣어줘도 되며, 이후 동일한 사용자나 동일 대화 주제에 대해 이전 대화이 맥락을 고려한 연속적인 대화를 원할 때 동일한 session_id 를 넣어주면 됩니다.)

 

chain_with_history.invoke(
    {"question": "Hi, my name is Lion King."}, 
    config={"configurable": {"session_id": "123"}},
)

# AIMessage(content='Hello, Lion King! How can I assist you today?')

## This time, ChatModel has a memory of previous conversations!!
chain_with_history.invoke(
    {"question": "What's my name? Do you remember it?"},
    config={"configurable": {"session_id": "123"}}
)

# AIMessage(content='Yes, your name is Lion King.')

 

 

네, 이번엔 잘 기억하고 제대로 대답을 해주네요! 

 

 

UpStash 사이트에 들어가서 "memory" 데이터베이스의 "Data Browser" 탭에 들어가서 보면 이전의 대화 기록 (Message Histories) 을 조회해서 확인해 볼 수 있습니다. (Redis에 이전 대화를 이처럼 쓰고, 읽기를 합니다.)

 

UpStash - Redis - Data Browser

 

 

위에 화면 캡쳐에 가려져서 잘 안보이는데요, 하나씩 풀어서 보면 아래와 같습니다. 

 

Redis - Memory (Message History)

 

 

[ Reference ]

 

1. LangChain Documents - Add message history (memory):
https://python.langchain.com/docs/expression_language/how_to/message_history

2. LangChain Documents - Memory:
https://python.langchain.com/docs/modules/memory/

3. Redis by UpStash - Serverless Database with Redis API:
https://www.upstash.com/

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요.  :-)

 

728x90
반응형
Posted by Rfriend
,

LLM 을 사용한 애플리케이션을 개발하다 보면 수정사항이 생길 수 있습니다. 이때 가능한 쉽고 빠르게 애플리케이션에 수정사항을 반영할 수 있도록 LangChain은

 

- configurable_fields() : Runnable의 특정 Fields(예: LLMs 내 parameters, HubRunnables 내 Prompts)를 실행 시에 설정할 수 있도록 해줌

- configurable_alternatives() : 실행 중에 특정 Runnable의 대안(예: LLMs, Prompts)을 설정할 수 있도록 해줌

 

의 2개 메소드를 제공합니다. 지난번 포스팅에서는 configurable_fileds() 에 대해서 소개했습니다. 

 

이번 포스팅에서는 이중에서도 configurable_alternatives() 을 사용해서 

   1. 실행 시 LLM 중 설정하기  

   2. 실행 시 Prompts 설정하기

에 대해서 소개하겠습니다. 

 

LangChain - configurable_alternatives() 특정 Runnable의 대안 설정하기

 

 

1. configurable_alternatives()를 사용해서 실행 시 LLM 중 설정하기

 

LangChain 의 Component 중에 LLM 모델과 ChatModel 이 있으며, 다른 구성요소와 통합이 되어있습니다.  LangChain은 Large Language Models (LLMs)와 ChatModels 사이에 구분을 두는데, 이 두 모델의 주요 차이점을 이해하는 것은 LangChain을 효과적으로 사용하는 데 중요합니다.

 

LangChain의 LLM과 ChatModel 모델의 주요 차이점은 다음과 같습니다. 

(1) 기능 및 목적 (Functionality and Purpose)

- LLMs (Large Language Models): GPT-3 또는 GPT-4와 같은 일반적인 목적의 언어 모델로, 주어진 프롬프트에 따라 텍스트를 생성할 수 있습니다. 다양한 주제와 형식에 걸쳐 인간과 유사한 텍스트를 이해하고 생성하도록 설계되었습니다. 

- ChatModels: LangChain 내에서 대화 맥락에 특화되어 설계된 전문화된 모델입니다. 이들은 여러 번의 교환을 통해 맥락을 유지하며 대화식 대화에 최적화되어 있으며, 대화식 상호 작용과 사용자 참여에 더 중점을 두고 있습니다. 


(2) 맥락 관리 (Context Management) 

- LLMs: 전통적인 LLM들은 일반적으로 단일 입력-출력 상호 작용(a single input-output interaction)을 처리합니다. 추가적인 맥락 관리 메커니즘 없이 여러 번의 교환에 걸쳐 대화 맥락을 유지하는 능력이 내재되어 있지 않습니다. 

- ChatModels: 이 모델들은 확장된 대화를 처리하도록 설계되어 대화 내역와 맥락(dialogue history and context)을 추적합니다. 대화의 연속성과 흐름(continuity and understanding of the conversation flow)을 이해하는 것이 중요한 챗봇과 같은 응용 프로그램에 적합합니다. 

 


(3) 훈련 및 튜닝 (Training and Tuning) 

- LLMs: 다양한 인터넷 텍스트를 기반으로 사전 훈련됩니다. 훈련은 대화에 특화된 것이 아니라 일관되고 문맥적으로 적절한 텍스트를 이해하고 생성하는 데 중점을 둡니다. 

- ChatModels: 대화 데이터에 미세 조정되거나 대화 처리에 본질적으로 더 나은 아키텍처를 가질 수 있어 대화식 시나리오에서 더 능숙합니다. 


(4) 사용 사례 (Use Cases) 

- LLMs: 콘텐츠 생성, 요약, 질문 답변 등 다양한 텍스트 생성 작업에 적합합니다. 

- ChatModels: 가상 보조원, 고객 서비스 챗봇 등 지속적인 대화가 핵심인 대화식 사용 사례에 특화되어 있습니다. 


(5) 응용 프로그램 통합 (Integration in Applications) 

- LLMs: 대화 기반 앱에서 맥락 관리를 위한 추가 계층이 필요한 다양한 AI 기반 텍스트 응용 프로그램의 백엔드 엔진으로 자주 사용됩니다. 

- ChatModels: 대화 상태를 유지하는 데 이미 최적화되어 있기 때문에 대화식 응용 프로그램에 플러그 앤 플레이(plug-and-play for conversational applications) 방식입니다. 

요약하자면, LangChain의 LLMs와 ChatModels는 모두 언어 생성 기술을 기반으로 하지만, ChatModels는 대화 맥락과 흐름(dialogue context and flow)을 유지하는 능력이 향상된 대화식 사용 사례에 특화된 반면, LLMs는 다양한 텍스트 생성 작업(a wide range of text generation tasks)에 적합한 더 일반적인 목적의 모델입니다. 

 

 

LangChain에서 통합하여 제공하는 LLMs, ChatModels 의 리스트는 아래 링크를 참조하세요. 

 

- LLMs : https://python.langchain.com/docs/integrations/llms/

- ChatModels: https://python.langchain.com/docs/integrations/chat/

 

아래 테이블은 LangChain의 ChatModels 에서 각 모델별로 invoke, async invoke, stream, async stream 별로 통합되어 있는 기능 현황을 정리한 것입니다. ('2023.12월 기준, 최신현황은 위 링크 참조하세요.) 

 

LangChain - ChatModels

 

LangChain - ChatModels Integration

 

 

먼저 openai, langchian 이 설치되어 있지 않다면 터미널에서 pip install 을 사용해서 설치해주기 바랍니다. 

 

! pip install -q openai langchain

 

 

 

이제 LangChain 의 configurable_alternatives() 메소드를 사용해서 ChatModels 중에서 ChatAnthropic, ChatOpenAI gpt-3.5-turbo, gpt-4 의 세 개 모델 중에서 실행 중에 설정해서 사용할 수 있도록 해보겠습니다. 

 

ConfigurableField 에서 id="llm" 으로 설정을 해서, 이후에 LLM 모델을 변경하고자 할 때 {"llm" : "Model_Name"} 처럼 사용하면 됩니다. 디폴트 LLM 모델은 defaulty_key="anthropic" 으로 설정했습니다. 

 

from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain_core.runnables import ConfigurableField


llm = ChatAnthropic(temperature=0).configurable_alternatives(
    # This gives this field an id
    # When configuring the end runnable, we can then use this id to configure this field
    ConfigurableField(id="llm"),
    # This sets a default_key.
    # If we specify this key, the default LLM (ChatAnthropic initialized above) will be used
    default_key="anthropic",
    # This adds a new option, with name `openai` that is equal to `ChatOpenAI()`
    openai=ChatOpenAI(),
    # This adds a new option, with name `gpt4` that is equal to `ChatOpenAI(model="gpt-4")`
    gpt4=ChatOpenAI(model="gpt-4"),
    # You can add more configuration options here
)

prompt = PromptTemplate.from_template("Tell me a joke about {topic}")
chain = prompt | llm

* 코드 예시는 LangChain Tutorial 의 코드를 그대로 소개합니다. 

 

 

위에서 정의한 chain 을 invoke 해서 실행하면 디폴트로 설정한 ChatAntoropic 모델을 사용해서 답변을 생성합니다. 

 

# By default it will call Anthropic
chain.invoke({"topic": "bears"})

# AIMessage(content=" Here's a silly joke about bears:
# \n\nWhat do you call a bear with no teeth?\nA gummy bear!")

 

 

LLM 모델을 ChatOpenAI 로 변경해서 설정하려고 하면 아래처럼 chain.with_config(configurable = {"llm": "openai"}) 처럼 해주면 됩니다. 

 

# We can use `.with_config(configurable={"llm": "openai"})` to specify an llm to use
chain.with_config(configurable={"llm": "openai"}).invoke({"topic": "bears"})

# AIMessage(content="Sure, here's a bear joke for you:\n\nWhy don't bears wear shoes?
# \n\nBecause they already have bear feet!")

 

 

 

2. configurable_alternatives()를 사용해서  실행 시 Prompts 설정하기

 

다음으로 configurable_alternatives() 를 사용해서 ConfigurableField()에 여러개의 Prompts 를 등록해놓고, chain을 실행할 때 여러개의 Prompts 대안 중에서 선택해서 설정하여 사용할 수 있도록 해보겠습니다. 

 

아래 예에서는 Prompts 에 "joke"와 "poem" 의 2개를 등록해놓고, "joke"를 디폴트 프롬프트로 설정해두었습니다. 

 

llm = ChatAnthropic(temperature=0)
prompt = PromptTemplate.from_template(
    "Tell me a joke about {topic}"
).configurable_alternatives(
    # This gives this field an id
    # When configuring the end runnable, we can then use this id to configure this field
    ConfigurableField(id="prompt"),
    # This sets a default_key.
    # If we specify this key, the default LLM (ChatAnthropic initialized above) will be used
    default_key="joke",
    # This adds a new option, with name `poem`
    poem=PromptTemplate.from_template("Write a short poem about {topic}"),
    # You can add more configuration options here
)

chain = prompt | llm

 

 

디폴트 설정으로 위에서 정의한 chain을 invoke 해서 실행하면 "joke" 프롬프트가 적용이 되어서 답변을 생성합니다. 

 

# By default it will write a joke
chain.invoke({"topic": "bears"})

# AIMessage(content=" Here's a silly joke about bears:
# \n\nWhat do you call a bear with no teeth?\nA gummy bear!")

 

 

만약 프롬프트를 기본 설정인 "joke" 대신에 "poem"으로 실행 시에 바꾸고자 한다면 아래처럼 chain.with_config(configurable={"prompt": "poem"}) 처럼 Prompt 대안을 바꿔주면 됩니다. 

 

# We can configure it write a poem
chain.with_config(configurable={"prompt": "poem"}).invoke({"topic": "bears"})

# AIMessage(content=' Here is a short poem about bears:
# \n\nThe bears awaken from their sleep
# \nAnd lumber out into the deep\nForests filled with trees so tall
# \nForaging for food before nightfall 
# \nTheir furry coats and claws so sharp
# \nSniffing for berries and fish to nab
# \nLumbering about without a care
# \nThe mighty grizzly and black bear
# \nProud creatures, wild and free
# \nRuling their domain majestically
# \nWandering the woods they call their own
# \nBefore returning to their dens alone')

 

 

 

[ Reference ]

* LangChain Turorial - "Configure chain internals at runtime":

https://python.langchain.com/docs/expression_language/how_to/configure

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

 

 

728x90
반응형
Posted by Rfriend
,

LLM 을 사용한 애플리케이션을 개발하다 보면 수정사항이 생길 수 있습니다. 이때 가능한 쉽고 빠르게 애플리케이션에 수정사항을 반영할 수 있도록 LangChain은

 

- configurable_fields() : Runnable의 특정 Fields(예: LLMs 내 parameters, HubRunnables 내 Prompts)를 실행 시에 설정할 수 있도록 해줌

- configurable_alternatives() : 실행 중에 특정 Runnable의 대안(예: LLMs, Prompts)을 설정할 수 있도록 해줌

 

의 2개 메소드를 제공합니다. 

 

이번 포스팅에서는 이중에서도 configurable_fields() 을 사용해서 

- (1) LLM 내 parameter 설정하기

- (2) HubRunnables 내 Prompts 설정하기

에 대해서 소개하겠습니다. 

 

LangChain - configurable_fields()

 

 

 

(1) configurable_fields() 를 사용해서 LLM 내 parameters 설정하기

 

먼저 openai 와 langchain 모듈이 설치되어있지 않다면 pip install 을 사용해서 설치하기 바랍니다. 

 

! pip install -q openai langchain

 

 

ChatGPT LLM 모델에서 설정 가능한 주요 파라미터로는 다음과 같은 것들이 있습니다. 

 

- (a) Max Tokens: 모델이 응답에서 생성해야 하는 최대 토큰(단어 또는 문자, 모델의 토큰화에 따라 다름) 수입니다. 이것은 콘텐츠의 길이를 조절하는 것입니다. 설정 방법에 따라 한 줄짜리 또는 전체 소설을 얻을 수 있습니다. 생성할 텍스트의 최대 길이를 전달합니다. 현재 최대 4,096개의 토큰까지의 숫자를 받아들입니다. 기본값은 2,048입니다. 

- (b) Temperature: 응답의 무작위성을 조절합니다. 높은 온도는 더 다양하고 예측하기 어려운 결과를 낳으며, 낮은 온도는 출력을 더 결정론적으로 만들어 가장 가능성 있는 응답에 가깝게 합니다. 기본값은 0.7이며, 0부터 1 사이의 값을 받아들입니다. 

- (c) Frequency Penalty: 모델이 같은 줄이나 구절을 반복할 확률을 감소시키는 데 사용됩니다. 빈도 벌칙 매개변수는 반복을 처벌하며, 높은 숫자는 출력을 다양하게 만들어 드물게 사용되는 단어를 촉진시키고, 음수 값은 더 일반적이고 때로는 반복적인 언어를 얻습니다. -2.0부터 2.0 사이에서 변동되며, 기본값은 0입니다. 

 

 

아래 예제에서는 LLM 의 Temperature 파라미터를 configurable_fileds() 메소드를 사용해서 실행 시에 설정 가능하도록 하였습니다. 디폴트는 temperature=0.0 으로 해서 결정론적이고 보수적으로 응답이 생성되도록 하였습니다. 

 

ConfigurableFiled() 에서 Configuration을 하려는 Field의 id, name, description 의 메타정보를 입력해줄 수 있으며, 나중에 실행 시 재설정할 때 Distionary에서 'id' 와 재설정값을 {key: value} pair로 재설정해주면 됩니다. 

 

## (1) Configuration Fields with LLMs

from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain_core.runnables import ConfigurableField

OPENAI_API_KEY="sk-xxxx...."

model = ChatOpenAI(temperature=0, openai_api_key=OPENAI_API_KEY)\
.configurable_fields(
    temperature=ConfigurableField(
        id="llm_temperature",
        name="LLM Temperature",
        description="The temperature of the LLM",
    )
)

prompt = PromptTemplate.from_template("Write a joke about {topic}")
chain = prompt | model

chain.invoke({"topic": "dog"})
# AIMessage(content="Why don't dogs make good dancers?
#                    \n\nBecause they have two left paws!")
#                    \n\nBecause he heard the drinks were on the house!')

 

 

 

아래 예제 코드에서는 LLM 모델의 temperature=0.9 로 재설정하여 보다 창의적인 응답을 생성하도록 하였습니다. 

 

## Configuration of LLM's temperature
chain.with_config(configurable={"llm_temperature":0.9}).invoke({"topic": "dog"})

# AIMessage(content="Why couldn't the dog tell lies?
#                    \nBecause he always got caught up in his own ruff tales!")

 

 

 

 

(2) configurable_fields() 를 사용해서 HubRunnables 내 Prompts 설정하기

 

LangChain은 최근 프롬프트를 업로드, 탐색, 가져오기 및 관리하기 위한 장소로 LangChain Hub를 출시했습니다. LangChain Hub의 주요 목표는 개발자들이 새로운 사용 사례와 정교한 프롬프트를 발견할 수 있는 주요 장소가 되는 것입니다. LangChainHub는 자주 사용되는 프롬프트(Prompts), 체인(Chains), 에이전트(Agents) 등을 찾고 등록할 수 있는 곳입니다. (https://smith.langchain.com/hub)

 

LangChain Hub에서 Prompt를 다운로드 받아서 쉽게 코드를 확인하고, 또 수정도 할 수 있습니다. 

 

* LangChain Blog - LangChainHub post: https://blog.langchain.dev/langchainhub/

 

LangChain Hub : https://smith.langchain.com/hub

 

 

LangChain Hub를 사용하기 위해 먼저 langchainhub 모듈을 pip install을 사용해서 설치합니다. 

 

! pip install langchainhub

 

 

HubRunnable() 메소드와 configurable_fileds() 메소드를 사용해서 LangChain Hub 내에 있는 Prompts 를 chain 실행 시에 쉽게 바꿔치기 할 수 있습니다. 

 

아래 예에서는 기본 Prompt로 "rlm/rag-prompt" 를 사용하도록 설정을 해놓고, ConfigurableField() 에서 'id', 'name', 'description' 의 메타정보를 등록해주었습니다. 

 

from langchain.runnables.hub import HubRunnable

prompt = HubRunnable("rlm/rag-prompt").configurable_fields(
    owner_repo_commit=ConfigurableField(
        id="hub_commit",
        name="Hub Commit",
        description="The Hub commit to pull from",
    )
)


prompt.invoke({"question": "foo", "context": "bar"})

# ChatPromptValue(messages=[HumanMessage(
# content="You are an assistant for question-answering tasks.
#          Use the following pieces of retrieved context to answer the question. 
#          If you don't know the answer, just say that you don't know. 
#          Use three sentences maximum and keep the answer concise.
# \nQuestion: foo 
# \nContext: bar 
# \nAnswer:")])

 

 

참고로 LangChain Hub 내 'rlm/rag-prompt' 에서 "RAG prompt" 부분만 소개하면 아래와 같습니다. 

 

# RAG prompt
from langchain import hub
prompt = hub.pull("rlm/rag-prompt")

 

 

 

이번에는 LangChain Hub 내 "rlm/rag-prompt-llama" Prompt 로 재설정해보겠습니다. Llama LLM 모델의 Prompt 로 변경되었습니다. 

 

prompt.with_config(
    configurable={"hub_commit": "rlm/rag-prompt-llama"}).invoke(
        {"question": "foo", "context": "bar"}
)

# ChatPromptValue(messages=[HumanMessage(
# content="
#   [INST]
#       <<SYS>> 
#           You are an assistant for question-answering tasks. 
#           Use the following pieces of retrieved context to answer the question. 
#           If you don't know the answer, just say that you don't know. 
#           Use three sentences maximum and keep the answer concise.
#       <</SYS>> 
#   \nQuestion: foo 
#   \nContext: bar 
#   \nAnswer: 
# [/INST]")])

 

 

참고로 LangChain Hub 에서 'rlm/rag-prompt-llama' 에서 "RAG prompt" 부분만 소개하면 아래와 같습니다. 

 

# RAG prompt
from langchain import hub
prompt = hub.pull("rlm/rag-prompt-llama")

 

 

[ Reference ]

- LangChain Tutorial - Configuration Fields: https://python.langchain.com/docs/expression_language/how_to/configure

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

애플리케이션을 개발하다보면 인풋의 조건, 또는 이전 단계의 결과에 따라 이후 단계에 LLM 모델이 실행해야 하는 과업(즉, 프롬프트)이 동적으로 달라져야 하는 경우가 있습니다. LangChain의 RunnableBranch 를 사용하면 로직에 따라서 인풋의 조건을 평가해서 이후 수행할 runnable을 동적으로 Routing 할 수 있습니다. 

 

아래에는 간단한 예를 들어서 RunnableBranch를 사용해서 Routing 하는 방법을 소개하겠습니다. 

 

(1) 인풋으로 받은 질문의 주제(Topic)를 "LangChain", "Anthropic", "Other" 의 3개 범주로 분류하기

(2) 각 Topic별로 프롬프트를 달리해서 'langchain_chain', 'anthropic_chain', 'general_chain' 만들기

(3) RunnableBranch() 를 사용해서 Topic 분류 결과에 따라 runnable Routing 하기

      - 만약 질문의 Topic이 "LangChain"이면 --> 'langchain_chain' 실행

      - 만약 질문의 Topic이 "Anthropic"이면   --> 'anthropic_chain' 실행

      - 만약 질문의 Topic이 "Other"이면         --> 'general_chain'실행

 

LangChain - RunnableBranch()로 동적으로 Routing 하기

 

 

 

먼저, 실습 환경에 openai, langchain 모듈이 설치가 안되어 있다면, 터미널에서 pip install 을 사용해서 설치를 하기 바랍니다. ('!'는 Jupyter Notebook에서 실행 시 추가)

 

! pip install -q openai langchain

 

 

 

(1) 인풋으로 받은 질문의 주제(Topic)를 "LangChain", "Anthropic", "Other" 의 3개 범주로 분류하기

 

1단계로서, 아래의 예처럼 template 에 인풋으로 받은 질문을 "LangChain", "Anthropic", "Other"의 3개 범주로 분류하고 단답형으로만 답하라고 지시를 했습니다.(classifier 모델이 따로 필요없다는게 놀랍습니다!) 그리고 '|' 를 사용해서 topic_chain = Prompt | Model | OutputParser를 chaining 하였습니다. 

 

from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

OPENAI_API_KEY = "sk-xxxxx...." # set with yours

## Create a chain that will identify incoming questions as being about 'LangChain', 'Anthropic', 'Other'.
template = """Given the user question below, classify it \
as either being about 'LangChain', 'Anthropic', or 'Other'.

Do not respond with more than one word.

<question>
{question}
</question>

Classification:"""

prompt = PromptTemplate.from_template(template)
model = ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo", openai_api_key=OPENAI_API_KEY)
parser = StrOutputParser()

topic_chain =  prompt | model | parser

 

 

invoke() 를 사용해서 topic_chain을 실행시켜보니 "LangChain", "Anthropic", "Other"의 3개 범주로 Topic을 제대로 분류하고 있네요. 

 

## Run the chains

topic_chain.invoke({"question": "How do I call LangChain?"})
# LangChain

topic_chain.invoke({"question": "How do I call Anthropic?"})
# Anthropic

topic_chain.invoke({"question": "What is 2 + 5?"})
# Other

 

 

 

 

(2) 각 Topic별로 프롬프트를 달리해서 'langchain_chain', 'anthropic_chain', 'general_chain' 만들기

 

이제 질문을 받았을 때 각 Topic 별로 프롬프트에 지시문을 다르게 해서 만들고, '|'로 서로 다른 Prompt + Model + OutputParser 를 chaining 해서 3개의 sub chains runnable을 만들었습니다. 

 

## Create three sub chains.
## (1) Topic: LangChain
langchain_chain = (
    PromptTemplate.from_template(
        """You are an expert in langchain.\
        Always answer questions starting with "Sure. I'm an expert in LangChain". \
        Respond to the following question:

        Question: {question}
        Answer:"""
    )
    | ChatOpenAI(openai_api_key=OPENAI_API_KEY)
    | StrOutputParser()
)

# (2) Topic: Anthropic
anthropic_chain = (
    PromptTemplate.from_template(
        """You are an expert in Anthropic. \
        Always answer questions staring with "Sure. I'm an expert in Anthropic". \
        Respond to the following question:

        Question: {question}
        Answer:"""
    )
    | ChatOpenAI(openai_api_key=OPENAI_API_KEY)
    | StrOutputParser()
)

# (3) Topic: Other
general_chain = (
    PromptTemplate.from_template(
        """Respond to the following question:

        Qeustion: {question}
        Answer:"""
    )
    | ChatOpenAI(openai_api_key=OPENAI_API_KEY)
    | StrOutputParser()
)

 

 

 

(3) RunnableBranch() 를 사용해서 Topic 분류 결과에 따라 runnable Routing 하기

 

마지막 단계로서, 앞의 단계에서 만든 구슬을 꿰어서 목걸이를 만들어보겠습니다. 

RunnableBranch(condition, runnable) 의 쌍으로 로직을 구성하고 Routing을 합니다. 

 

                      조건 (condition)                    Routing     실행 (runnable)

      - 만약 질문의 Topic이 "LangChain"이면 ----------> 'langchain_chain' 실행

      - 만약 질문의 Topic이 "Anthropic"이면   ----------> 'anthropic_chain' 실행

      - 만약 질문의 Topic이 "Other"이면         ----------> 'general_chain'실행

 

## Dynamically route logic based on input using RunnableBranch()
from langchain_core.runnables import RunnableBranch

branch = RunnableBranch(
    (lambda x: "langchain" in x["topic"].lower(), langchain_chain), # a list of (condition, runnable) pair
    (lambda x: "anthropic" in x["topic"].lower(), anthropic_chain), 
    general_chain
)

# Full Chain: topic_chain + branch
full_chain = {"topic": topic_chain, "question": lambda x: x["question"]} | branch

 

 

 

위에서 RunnableBranch를 사용해서 정의한 full_chain 을 invoke() 를 사용해서 실행시켜 보았습니다. 3개 Topic 별로 질문을 던져보니 잘 작동함을 확인할 수 있습니다. 

 

## Run full_chain runnable

## (1) Topic: 'LangChain' --> route to 'langchain_chain'
full_chain.invoke({"question": "How do I call LangChain?"})

# Sure. I'm an expert in LangChain. To call LangChain, you can use the LangChain API 
# or SDK provided by its developers. These tools allow you to interact with LangChain's 
# language processing capabilities programmatically. You would need to integrate the API 
# or SDK into your application or codebase and make the necessary API calls or 
# function calls to utilize LangChain's features.


## (2) Topic: 'Anthropic' --> route to 'anthropic_chain' 
full_chain.invoke({"question": "How do I call Anthropic?" })

# Sure. I'm an expert in Anthropic. Anthropic refers to the study of the relationship 
# between humans and their environment. It can also refer to the idea that the universe 
# is fine-tuned for the existence of human life. Therefore, you can call Anthropic 
# as the scientific study of human-environment interaction or the philosophical concept 
# of the fine-tuning of the universe.


## (3) Topic: 'Other' --> route to 'general_chain'
full_chain.invoke({"question": "What is 2 + 5?"})

# The sum of 2 and 5 is 7.

 

 

[ Reference ]

* LangChain Tutorial - RunnableBranch()

: https://python.langchain.com/docs/expression_language/how_to/routing#using-a-runnablebranch

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 LangChain을 사용해서 RAG (Retrieval-Agumented Generation) 을 구현해보았습니다. 

그때 RunnableParallel() 메소드를 사용해서 사용자 질문을 RunnablePassThrough()와 Retriever 에 동시에 병렬로 처리하는 방법을 한번 소개한 적이 있습니다. 

 

이번 포스팅에서는 LangChain의 RunnableParallel() 을 사용해서 독립적인 다수의 Chain들을 병렬로 처리하는 방법을 소개하겠습니다. 

 

LangChain RunnableParallel() 로 Chain 병렬처리하기 (run multiple processes in parallel using RunnableParallel)

 

 

먼저, langchain, openai 모듈이 설치되어 있지 않다면 터미널에서 pip install 을 사용해서 먼저 설치하시기 바랍니다. (만약 Jupyter Notebook을 사용중이라면, 셀의 제일 앞부분에 '!' 를 추가해주세요)

 

! pip install langchain openai

 

 

 

예시에서 진행한 과업은

 - 사용자로 부터 topic 을 인풋으로 받아서

 - ChatOpenAI 모델을 사용해서 (1) Joke, (2) Poem, (3) Question 을 생성

하기 입니다. 

 

이들 3개의 각 과업에 '|' 을 사용해서 PomtptTemplate + ChatModel + OutputParser 를 chaining 했습니다. 이들 3개 과업의 chain 들은 동일한 topic을 인풋으로 받아서 서로 독립적으로 처리가 가능합니다. 

 

이처럼 독립적으로 수행 가능한 3개의 chain을 'RunnableParallel()' 을 사용해서 병렬로 처리합니다. 그리고 아웃풋은 Dictionary 형태로 chian 의 이름과 매핑이 되어서 생성한 답변을 반환합니다. (아래 제일 마지막 줄의 {'joke': xxx, 'poem': xxx, 'question': xxxx} 결과 참조)

 

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel

## define PromptTemplates
joke_template = ChatPromptTemplate.from_template("tell me a joke about {topic}")
poem_template = ChatPromptTemplate.from_template("write a 2-line poem about {topic}")
question_template = ChatPromptTemplate.from_template("make a question about {topic}")


## define a ChatModel
#openai_api_key = "sk-xxxxxx..." # set with your API key
model = ChatOpenAI(openai_api_key=OPENAI_API_KEY) 


## define a OutputParser
parser = StrOutputParser()


## Chaining Prompt + ChatModel + OutputParser
joke_chain = joke_template | model | parser
poem_chain = poem_template | model | parser
question_chain = question_template | model | parser

## execute multiple runnables in parallel using RunnableParallel()
map_chain = RunnableParallel(
   joke=joke_chain, 
   poem=poem_chain, 
   qeustion=question_chain
)


# Run all chains
map_chain.invoke({"topic": "snowman"})


# {'joke': 'Why did the snowman go to the spa?\n\nBecause he wanted to chill out and relax!',
#  'poem': "Snowman's frozen smile,\nMelts away in winter's warmth.",
#  'qeustion': 'What are some creative ways to decorate a snowman?'}

 

 

 

RunnableParallel()을 사용하기 전에 각 과업의 chain을 개별적으로 수행했을 때의 소요 시간과, RunnableParallel()을 사용해서 3개의 chain을 동시에 병렬로 처리했을 때의 소요 시간을 측정해서 비교해보았습니다. 그랬더니, RunnableParallel()로 병렬처리했을 때의 시간과 개별로 처리했을 때의 시간이 별 차이가 없음을 알 수 있습니다! 

 

 

[ 'joke' chain 단독 수행 소요 시간: 11s +- 112ms ]

 

%%timeit
joke_chain.invoke({"topic":"snowman"})

# 11 s ± 112 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

 

 

 

[ 'poem' chain 단독 수행 소요 시간: 891 ms +- 197ms ]

 

%%timeit
poem_chain.invoke({"topic": "snowman"})

# 891 ms ± 197 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

 

 

 

[ 'question' chain 단독 수행 소요 시간: 751ms +- 128ms ]

 

%%timeit
question_chain.invoke({"topic": "snowman"})

# 751 ms ± 128 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

 

 

 

[ 'map' chain 으로 3개의 모든 chain 병렬 처리 소요 시간: 922ms +- 101ms]

 

%%timeit
map_chain.invoke({"topic": "snowman"})

# 922 ms ± 101 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

 

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서 LangChain 은 대규모 언어 모델을 사용해서 애플리케이션을 개발, 프로덕트화, 배포하는 것을 도와주는 프레임워크라고 소개했습니다. 그리고 LangChain Expression Language (LCEL) 는 LangChain의 기본적인 컴포넌트들을 chaining 기법을 써서 다단계 작업들의 연결 체인을 만들어 복잡한 기능을 구현할 수 있도록 해준다고 했습니다. 

 

이번 포스팅에서는 LangChain Expression Language (LCEL) 의 가장 간단한 형태의 chaining 예시를 들어보겠습니다. 

 

LCEL Chaining 기본 형태: Prompt + Model + Output Parser

 

이때 Model 로서 ChatModel과 LLM 모델을 사용할 때 input 과 output 이 조금 다르기 때문에, Model에 따라서 나누어서 소개하겠습니다. 

 

(1) ChatModel 사용 시 Pipeline: Input --> {Dict} --> PromptTemplate --> PromptValue --> ChatModel --> ChatMessage --> StrOutputParser --> String --> Result

 

(2) LLM 사용 시 Pipeline: Input String --> LLM --> Output String

 

LangChain: chaining a prompt, a model, and output parser

 

 

 

(1) ChatModel 을 사용한 Chaining 예: Prompt + ChatModel + Output Parser

: Input --> {Dict} --> PromptTemplate --> PromptValue --> ChatModel --> ChatMessage --> StrOutputParser --> String --> Result

 

만약 실습 환경에 langchain, openai 모듈이 설치가 안되어 있다면 termianl 이나 Jupyter Notebook (pip install 앞에 '!' 를 붙여서 사용) 에서 langchain, openai 모듈을 먼저 설치하기 바랍니다. 

 

! pip install -q langchain
! pip install openai

 

 

(1) 필요한 modules 을 불러옵니다. 

(2) OpenAI 의 API Key 를 설정해줍니다. 

(3) Prompt Template, Chat Model, Output Parser 의 인스턴스를 생성합니다. 

(4) '|' 를 사용해서 chain = promt | model | output_parser 를 연결(Chaining)해줍니다 

(5) chain.invoke() 를 사용해서 chain pipeline 을 실행시켜줍니다. 이때 invoke({"topic": "bird"}) 처럼 Dictionary 형태로 PromptTemplate 에 들어갈 Input을 넣어주면 됩니다. 

 

## (1) Importe modules
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

## (2) set with your openai api key
openai_api_key="sk-xxxxxx..."

## (3) Create instances of prompt, ChatModel, adn output parser
prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
model = ChatOpenAI(openai_api_key=openai_api_key)
output_parser = StrOutputParser()

## (4) Chaining a PromptTemplate + ChatModel + Output Parser using '|'
chain = prompt | model | output_parser

## (5) Run the whole pipeline of chain
chain.invoke({"topic": "bird"})

# \n\nWhy was the bird kicked out of the comedy club? Because it kept telling fowl jokes!

 

 

위에서 생성한 ChatPromptTemplate 의 인풋, 아웃풋을 단계별로 하나씩 살펴보겠습니다. 

 

    - ChatPromptValue: prompt.invoke()

    - Messages: prompt_value.to_messages()

    - HumanMessage: prompt_value.to_string()

 

ChatModel 은 인풋으로 PromptValue 를 받는 반면에, LLM 은 a string을 인풋으로 받는 차이점이 있습니다. (LLM도 PromptValue를 인풋으로 넣어줘도 작동합니다)

 

## ChatPromptValue
prompt_value = prompt.invoke({"topic": "bird"})

prompt_value
# ChatPromptValue(messages=[HumanMessage(content='tell me a short joke about bird')])

prompt_value.to_messages()
# Human: tell me a short joke about bird

prompt_value.to_string()
# Human: tell me a short joke about bird

 

 

다음으로, ChatModel의 아웃풋인 AIMessage 를 살펴보겠습니다. 

 

## ChatModel AIMessage
message = model.invoke(prompt_value)

message
# AIMessage(content="Why don't birds wear shoes? \n\nBecause they have talon-t!")

 

 

 

(2) LLM Model 을 사용 예

: Input String --> LLM --> Output String

 

LLM 모델로는 OpenAI의 OpenAI(model="gpt-3.5-turbo-instruct") 모델을 사용해서 예를 들어보겠습니다. LLM 모델의 인풋으로 string 을 받고, 아웃풋으로는 생성된 string 을 반환합니다. (위의 ChatModel 대비 인풋과 아웃풋이 모두 string 으로 상대적으로 간단합니다.)

 

# prompt + llm model + output parser
from langchain.llms import OpenAI

# Set your OpenAI API Key
openai_api_key="sk-xxxxx..."

# Create an instance of LLM model
llm = OpenAI(model="gpt-3.5-turbo-instruct", openai_api_key=openai_api_key)

# Run LLM model, put a string as an input, and outputs a string
llm.invoke("tell me a short joke about bird")
# \n\nWhy did the chicken go to the seance? To get in touch with its inner chick!

 

 

 

[Reference]

* LangChain Expression Language
https://python.langchain.com/docs/expression_language/get_started#basic-example-prompt-model-output-parser

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,