LangChain으로 LLM RAG 챗봇 구축 VI

2024. 4. 22. 20:19python/intermediate

4단계: LangChain에서 그래프 RAG 챗봇 구축

 

지금까지 수행한 모든 준비 설계 및 데이터 작업이 끝나면 마침내 챗봇을 구축할 준비가 되었습니다! Neo4j에 저장된 병원 시스템 데이터와 LangChain 추상화의 힘을 사용하면 챗봇 구축에 많은 작업이 필요하지 않다는 것을 알게 될 것입니다. 이는 AI 및 ML 프로젝트의 공통 주제입니다. 대부분의 작업은 AI 자체를 구축하는 것이 아니라 설계, 데이터 준비 및 배포에 있습니다.

시작하기 전에 chatbot_api/다음 파일과 폴더가 포함된 폴더를 프로젝트에 추가하세요.

 

 

.env 파일에 몇 가지 환경 변수를 더 추가할 수도 있습니다.

 

# .env

OPENAI_API_KEY=

NEO4J_URI=
NEO4J_USERNAME=
NEO4J_PASSWORD=

HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv

HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125

 

이제 .env 파일에는 챗봇의 다양한 구성 요소에 사용할 LLM을 지정하는 변수가 포함되어 있습니다. 코드를 변경하지 않고도 다양한 OpenAI 모델 간에 쉽게 전환할 수 있도록 이러한 모델을 환경 변수로 지정했습니다. 그러나 각 LLM은 고유한 프롬프트 전략의 이점을 누릴 수 있으므로 다른 LLM 제품군을 사용하려는 경우 프롬프트를 수정해야 할 수도 있습니다.

hospital_neo4j_etl/ 폴더가 이미 완성되어 있어야하며 docker-compose.yml과 .env는 이전과 동일합니다. chatbot_api/pyproject.toml을 열고 아래 종속성을 추가합니다.

 

# chatbot_api/pyproject.toml

[project]
name = "chatbot_api"
version = "0.1"
dependencies = [
     "asyncio==3.4.3",
     "fastapi==0.109.0",
     "langchain==0.1.0",
     "langchain-openai==0.0.2",
     "langchainhub==0.1.14",
     "neo4j==5.14.1",
     "numpy==1.26.2",
     "openai==1.7.2",
     "opentelemetry-api==1.22.0",
     "pydantic==2.5.1",
     "uvicorn==0.25.0"
]

[project.optional-dependencies]
dev = ["black", "flake8"]

 

사용 가능한 경우 이러한 종속성의 최신 버전을 사용할 수 있지만 더 이상 사용되지 않을 수 있는 기능을 염두에 두십시오. 터미널을 열고, 가상 환경을 활성화하고, chatbot_api/폴더로 이동하고, 프로젝트 pyproject.tomlM의 종속성을 설치합니다.

 
[ ]:
%cd chatbot_api
!python -m pip install .
%cd ..
 
 
 

모든 것이 설치되면 리뷰 체인을 구축할 준비가 된 것입니다!

 

Neo4j 벡터 체인 생성

 

1단계 에서는 리뷰를 사용하여 환자 경험에 대한 질문에 답변하는 체인을 구축하여 LangChain을 직접 소개했습니다. 이 섹션에서는 Neo4j를 벡터 인덱스로 사용한다는 점을 제외하고 유사한 체인을 구축합니다.

벡터 검색 인덱스는 Neo4j 5.11에서 공개 베타로 출시되었습니다. 이를 통해 그래프에서 직접 의미론적 쿼리를 실행할 수 있습니다. 이는 구조화된 병원 시스템 데이터와 동일한 위치에 리뷰 임베딩을 저장할 수 있기 때문에 챗봇에 매우 편리합니다.

LangChain에서는 Neo4jVector를 사용하여 체인에 필요한 리뷰 임베딩과 검색기를 생성할 수 있습니다. 리뷰 체인을 생성하는 코드는 다음과 같습니다.

 
[ ]:
# chatbot_api/src/chains/hospital_review_chain.py
 
import os
from langchain.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
 
HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
 
neo4j_vector_index = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
index_name="reviews",
node_label="Review",
text_node_properties=[
"physician_name",
"patient_name",
"text",
"hospital_name",
],
embedding_node_property="embedding",
)
 
review_template = """Your job is to use patient
reviews to answer questions about their experience at a hospital. Use
the following context to answer questions. Be as detailed as possible, but
don't make up any information that's not from the context. If you don't know
an answer, say you don't know.
{context}
"""
 
review_system_prompt = SystemMessagePromptTemplate(
prompt=PromptTemplate(input_variables=["context"], template=review_template)
)
 
review_human_prompt = HumanMessagePromptTemplate(
prompt=PromptTemplate(input_variables=["question"], template="{question}")
)
messages = [review_system_prompt, review_human_prompt]
 
review_prompt = ChatPromptTemplate(
input_variables=["context", "question"], messages=messages
)
 
reviews_vector_chain = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
chain_type="stuff",
retriever=neo4j_vector_index.as_retriever(k=12),
)
reviews_vector_chain.combine_documents_chain.llm_chain.prompt = review_prompt
 
 
 

111행에서는 Neo4j로 검토 체인을 구축하는 데 필요한 종속성을 가져옵니다. 13행에서는 리뷰 체인에 사용하고 을 HOSPITAL_QA_MODEL에 저장할 채팅 모델의 이름을 로드합니다. 1529행은 Neo4j에서 벡- 터 인덱스를 생성합니다. 각 매개변수에 대한- 분석은 다음과 같습니다- - .

  • embedding: 임베딩을 생성하는 데 사용되는 모델입니다. 이 예에서는 OpenAIEmeddings()을 사용하고 있습니다.
  • url, username 및 password: Neo4j 인스턴스 자격 증명입니다.
  • index_name: 벡터 인덱스에 지정된 이름입니다.
  • node_label: 임베딩을 생성할 노드입니다.
  • text_node_properties: 임베딩에 포함할 노드 속성입니다.
  • embedding_node_property: 포함 노드 속성의 이름입니다.

일단 Neo4jVector.from_existing_graph()이 실행되면 Neo4j의 모든 Review 노드에는 doctor_name, Patient_name, text 및 Hospital_name 속성의 벡터 표현인 임베딩 속성이 있음을 알 수 있습니다. 이를 통해 긍정적인 평가를 받은 병원은 어디입니까?와 같은 질문에 답할 수 있습니다. 또한 LLM은 귀하의 질문과 일치하는 리뷰를 작성한 환자와 의사를 알려줄 수 있습니다.

31- ~50 행에서는 1단계에서와 동일한 방식으로 검토 체인에 대한 프롬프트 템플릿을 생성합니다.

마지막으로 52~57행은 유사성 검색에서 12개의 리뷰 임베딩을 반환하는 Neo4j 벡터 인덱스 검색기를 사용하여 리뷰 벡터 체인을 만듭니다. chain_type을 .from_chain_type()의 "stuff"으로 설정하면 체인이 12개의 리뷰를 모두 프롬프트에 전달하도록 지시하는 것입니다. 체인에 대한 LangChain의 체인에 대한 문서에서 다른 체인 유형을 탐색할 수 있습니다.

새로운 리뷰 체인을 시험해 볼 준비가 되었습니다. 프로젝트의 루트 디렉터리로 이동하여 Python 인터프리터를 시작한 후 다음 명령을 실행합니다.

 
[ ]:
Selection deleted
import dotenv
dotenv.load_dotenv()
 
 
 
[ ]:
from chatbot_api.src.chains.hospital_review_chain import (
reviews_vector_chain
)
 
 
 
[ ]:
query = """What have patients said about hospital efficiency?
Mention details from specific reviews."""
 
response = reviews_vector_chain.invoke(query)
 
response.get("result")
 
 
 

이 블록에서는 dotenv를 가져오고 .env로부터 환경 변수를 로드합니다. 그런 다음 병원 효율성에 대한 질문으로 hospital_review_chain에서 reviews_vector_chain를 가져오고 그것을 호출합니다. 귀하의 체인 응답은 이와 동일하지 않을 수 있지만 LLM은 귀하가 지시한 대로 훌륭하고 자세한 요약을 반환해야 합니다.

이 예에서는 응답에서 구체적인 환자 및 병원 이름이 어떻게 언급되는지 확인하세요. 이는 리뷰 텍스트와 함께 병원 및 환자 이름을 삽입하여 LLM이 이 정보를 사용하여 질문에 답할 수 있기 때문에 발생합니다.

참고: 계속 진행하기 전에 다양한 쿼리에 어떻게 응답하는지 보기 위하여 reviews_vector_chain을 분석해야 합니다. 응답이 올바른 것 같나요? reviews_vector_chain의 품질을 어떻게 평가할 수 있나요? 이 튜토리얼에서는 RAG 시스템을 평가하는 방법을 배우지 않지만 MLFlow가 포함된 이 포괄적인 Python 예제를 보고 이것이 어떻게 수행되는지 느낄 수 있습니다.

다음으로 구조화된 병원 시스템 데이터에 대한 쿼리에 응답하는 데 사용할 Cypher 생성 체인을 만듭니다.

 

Neo4j 암호 체인 생성

 

2단계 에서 본 것처럼 Neo4j Cypher 체인은 사용자의 자연어 쿼리를 받아들이고, 자연어 쿼리를 Cypher 쿼리로 변환하고, Neo4j에서 Cypher 쿼리를 실행하고, Cypher 쿼리 결과를 사용하여 사용자 쿼리에 응답합니다. 이를 위해 LangChain의 GraphCypherQAChain을 활용하게 됩니다.

참고 : Cypher 체인에서와 마찬가지로 사용자가 데이터베이스에 쿼리하도록 허용할 때마다 필요한 권한만 있는지 확인해야 합니다. 이 프로젝트에서 사용 중인 Neo4j 자격 증명을 통해 사용자는 데이터베이스에서 데이터를 읽고, 쓰고, 업데이트하고 삭제할 수 있습니다.

실제 프로젝트를 위해 이 애플리케이션을 구축하는 경우 사용자의 권한을 읽기 전용으로 제한하여 중요한 데이터를 쓰거나 삭제할 수 없도록 하는 자격 증명을 만들고 싶을 것입니다.

LLM을 사용하여 정확한 Cypher 쿼리를 생성하는 것은 어려울 수 있으며, 특히 그래프가 복잡한 경우 더욱 그렇습니다. 이 때문에 그래프 구조와 쿼리 사용 사례를 LLM에 표시하려면 많은 프롬프트 엔지니어링이 필요합니다. 쿼리를 생성하기 위해 LLM을 미세 조정하는 것도 옵션이지만 이를 위해서는 수동으로 선별되고 레이블이 지정된 데이터가 필요합니다.

Cypher 생성 체인 생성을 시작하려면 종속성을 가져오고 Neo4jGraph을 인스턴스화하세요.

 
[ ]:
# chatbot_api/src/chains/hospital_cypher_chain.py
 
import os
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
 
HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
HOSPITAL_CYPHER_MODEL = os.getenv("HOSPITAL_CYPHER_MODEL")
 
graph = Neo4jGraph(
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
)
 
graph.refresh_schema()
 
 
 

Neo4jGraph 객체는 LLM이 Neo4j 인스턴스에서 쿼리를 실행할 수 있게 해주는 LangChain 래퍼입니다. Neo4j 자격 증명을 사용하여 graph를 인스턴스화하고 최근 변경 사항을 인스턴스에 동기화하기 위해 graph.refresh_schema()를 호출합니다.

Cypher 생성 체인의 다음이자 가장 중요한 구성 요소는 프롬프트 템플릿입니다. 그 모습은 다음과 같습니다.

 
[ ]:
# chatbot_api/src/chains/hospital_cypher_chain.py
 
# ...
 
cypher_generation_template = """
Task:
Generate Cypher query for a Neo4j graph database.
 
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
 
Schema:
{schema}
 
Note:
Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything other than
for you to construct a Cypher statement. Do not include any text except
the generated Cypher statement. Make sure the direction of the relationship is
correct in your queries. Make sure you alias both entities and relationships
properly. Do not run any queries that would add to or delete from
the database. Make sure to alias all statements that follow as with
statement (e.g. WITH v as visit, c.billing_amount as billing_amount)
If you need to divide numbers, make sure to
filter the denominator to be non zero.
 
Examples:
# Who is the oldest patient and how old are they?
MATCH (p:Patient)
RETURN p.name AS oldest_patient,
duration.between(date(p.dob), date()).years AS age
ORDER BY age DESC
LIMIT 1
 
# Which physician has billed the least to Cigna
MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[t:TREATS]-(phy:Physician)
WHERE p.name = 'Cigna'
RETURN phy.name AS physician_name, SUM(c.billing_amount) AS total_billed
ORDER BY total_billed
LIMIT 1
 
# Which state had the largest percent increase in Cigna visits
# from 2022 to 2023?
MATCH (h:Hospital)<-[:AT]-(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Cigna' AND v.admission_date >= '2022-01-01' AND
v.admission_date < '2024-01-01'
WITH h.state_name AS state, COUNT(v) AS visit_count,
SUM(CASE WHEN v.admission_date >= '2022-01-01' AND
v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
SUM(CASE WHEN v.admission_date >= '2023-01-01' AND
v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WITH state, visit_count, count_2022, count_2023,
(toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1
 
# How many non-emergency patients in North Carolina have written reviews?
MATCH (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' and v.admission_type <> 'Emergency'
RETURN count(*)
 
String category values:
Test results are one of: 'Inconclusive', 'Normal', 'Abnormal'
Visit statuses are one of: 'OPEN', 'DISCHARGED'
Admission Types are one of: 'Elective', 'Emergency', 'Urgent'
Payer names are one of: 'Cigna', 'Blue Cross', 'UnitedHealthcare', 'Medicare',
'Aetna'
 
A visit is considered open if its status is 'OPEN' and the discharge date is
missing.
Use abbreviations when
filtering on hospital states (e.g. "Texas" is "TX",
"Colorado" is "CO", "North Carolina" is "NC",
"Florida" is "FL", "Georgia" is "GA", etc.)
 
Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
Never return embedding properties in your queries. You must never include the
statement "GROUP BY" in your query. Make sure to alias all statements that
follow as with statement (e.g. WITH v as visit, c.billing_amount as
billing_amount)
If you need to divide numbers, make sure to filter the denominator to be non
zero.
 
The question is:
{question}
"""
 
cypher_generation_prompt = PromptTemplate(
input_variables=["schema", "question"], template=cypher_generation_template
)
 
 
 

cypher_generation_template의 내용을 주의 깊게 읽어보세요. Cypher 쿼리를 생성할 때 수행해야 할 작업과 수행하지 말아야 할 작업에 대한 매우 구체적인 지침을 LLM이 어떻게 제공하는지 확인하세요. 가장 중요한 것은 LLM에 schema 매개변수, 몇 가지 예제 쿼리 및 몇 가지 노드 속성의 범주형 값을 사용하여 그래프의 구조를 표시한다는 것입니다.

프롬프트 템플릿에 제공하는 모든 세부 정보는 LLM이 특정 질문에 대해 올바른 Cypher 쿼리를 생성할 가능성을 높여줍니다. 이 모든 세부 사항이 얼마나 필요한지 궁금하다면 세부 사항을 최대한 적게 포함하여 나만의 프롬프트 템플릿을 만들어 보세요. 그런 다음 Cypher 체인을 통해 질문을 실행하고 Cypher 쿼리가 올바르게 생성되는지 확인하세요.

여기에서 프롬프트 템플릿을 반복적으로 업데이트하여 LLM이 생성하는 데 어려움을 겪는 쿼리를 수정할 수 있지만 사용 중인 입력 토큰의 수도 알고 있는지 확인하세요. 검토 체인과 마찬가지로 프롬프트 템플릿과 체인에서 생성된 Cypher 쿼리의 정확성을 평가하기 위한 견고한 시스템이 필요합니다. 그러나 보시다시피 위에 있는 템플릿은 훌륭한 시작점입니다.

참고 : 위의 프롬프트 템플릿은 그래프에 대한 유효한 Cypher 쿼리의 네 가지 예를 LLM에게 제공합니다. LLM에 몇 가지 예를 제공한 다음 작업을 수행하도록 요청하는 것을 소수 프롬프트 라고 하며 생성 정확도를 향상시키는 간단하면서도 강력한 기술입니다.

그러나 특히 복잡한 그래프가 있는 경우에는 Cypher 쿼리 생성에 몇 번 메시지를 표시하는 것만으로는 충분하지 않을 수 있습니다. 이를 개선하는 한 가지 방법은 예제 사용자 질문/쿼리를 포함하고 해당 Cypher 쿼리를 메타데이터로 저장하는 벡터 데이터베이스를 만드는 것입니다.

사용자가 질문을 하면 의미상 유사한 질문의 Cypher 쿼리를 프롬프트에 삽입하여 LLM에 현재 질문에 답하는 데 필요한 가장 관련성이 높은 예를 제공합니다.

다음으로 체인의 질문-답변 구성 요소에 대한 프롬프트 템플릿을 정의합니다. 이 템플릿은 LLM에게 Cypher 쿼리 결과를 사용하여 사용자 쿼리에 대한 적절한 형식의 답변을 생성하도록 지시합니다.

 
[ ]:
# chatbot_api/src/chains/hospital_cypher_chain.py
 
# ...
 
qa_generation_template = """You are an assistant that takes the results
from a Neo4j Cypher query and forms a human-readable response. The
query results section contains the results of a Cypher query that was
generated based on a user's natural language question. The provided
information is authoritative, you must never doubt it or try to use
your internal knowledge to correct it. Make the answer sound like a
response to the question.
 
Query Results:
{context}
 
Question:
{question}
 
If the provided information is empty, say you don't know the answer.
Empty information looks like this: []
 
If the information is not empty, you must provide an answer using the
results. If the question involves a time duration, assume the query
results are in units of days unless otherwise specified.
 
When names are provided in the query results, such as hospital names,
beware of any names that have commas or other punctuation in them.
For instance, 'Jones, Brown and Murray' is a single hospital name,
not multiple hospitals. Make sure you return any list of names in
a way that isn't ambiguous and allows someone to tell what the full
names are.
 
Never say you don't have the right information if there is data in
the query results. Always use the data in the query results.
 
Helpful Answer:
"""
 
qa_generation_prompt = PromptTemplate(
input_variables=["context", "question"], template=qa_generation_template
)
 
 
 

이 템플릿에는 Cypher 생성 템플릿보다 훨씬 적은 세부 정보가 필요하므로 LLM이 다르게 응답하도록 하거나 쿼리 결과를 원하는 방식으로 사용하지 않는 경우에만 수정해야 합니다. Cypher 체인 생성의 마지막 단계는 GraphCypherQAChain 객체를 인스턴스화하는 것입니다.

 
[ ]:
# chatbot_api/src/chains/hospital_cypher_chain.py
 
# ...
 
hospital_cypher_chain = GraphCypherQAChain.from_llm(
cypher_llm=ChatOpenAI(model=HOSPITAL_CYPHER_MODEL, temperature=0),
qa_llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
graph=graph,
verbose=True,
qa_prompt=qa_generation_prompt,
cypher_prompt=cypher_generation_prompt,
validate_cypher=True,
top_k=100,
)
 
 
 

다음은 GraphCypherQAChain.from_llm()에 사용된 매개변수에 대한 분석입니다 .

  • cypher_llm: Cypher 쿼리를 생성하는 데 사용되는 LLM입니다.
  • qa_llm: Cypher 쿼리 결과에 따라 답변을 생성하는 데 사용되는 LLM입니다.
  • graph: Neo4j 인스턴스에 연결되는 Neo4jGraph 객체입니다.
  • verbose: 체인이 수행하는 중간 단계를 인쇄해야 하는지 여부입니다.
  • qa_prompt: 질문/쿼리에 응답하기 위한 프롬프트 템플릿입니다.
  • cypher_prompt: Cypher 쿼리 생성을 위한 프롬프트 템플릿입니다.
  • validate_cypher: true인 경우 Cypher 쿼리에 오류가 있는지 검사하고 실행하기 전에 수정합니다. 이는 Cypher 쿼리가 유효하다는 것을 보장하지 않는다는 점에 유의하세요. 대신 정규식을 사용하여 쉽게 감지할 수 있는 간단한 구문 오류를 수정합니다.
  • top_k: qa_prompt에 포함할 쿼리 결과 수입니다.

귀하의 병원 시스템 Cypher 생성 체인을 사용할 준비가 되었습니다! 리뷰 체인과 동일한 방식으로 작동합니다. 프로젝트 디렉토리를 항해하고 새로운 파이썬 인터프리터 세션을 시작하여 실행해 보세요.

 
[ ]:
Selection deleted
import dotenv
dotenv.load_dotenv()
 
 
 
[ ]:
Selection deleted
from chatbot_api.src.chains.hospital_cypher_chain import (
hospital_cypher_chain
)
 
question = """What is the average visit duration for
emergency visits in North Carolina?"""
response = hospital_cypher_chain.invoke(question)
 
 
 

환경 변수를 로드하고, hospital_cypher_chain을 가져오고, 질문과 함께 호출한 후, 체인이 질문에 답하기 위해 수행하는 단계를 확인할 수 있습니다. Cypher 쿼리를 생성할 때 체인이 달성한 몇 가지 성과를 잠시 감상해 보세요.

  • Cypher 생성 LLM은 제공된 그래프 스키마를 통해 방문 횟수와 병원 간의 관계를 파악했습니다.
  • 노스 캐롤라이나에 대해 질문했지만 LLM은 주 약어 NC를 사용하라는 프롬프트를 통해 알고 있었습니다.
  • LLM은 admission_type 속성에는 첫 글자만 대문자로 표시되고 상태 속성은 모두 대문자로 표시된다는 것을 알고 있었습니다.
  • QA 생성 LLM은 프롬프트를 통해 쿼리 결과가 일 단위라는 것을 알았습니다.

병원 시스템에 관한 모든 종류의 쿼리를 실험해 볼 수 있습니다. 예를 들어 Cypher로 변환하기에는 비교적 어려운 질문이 있습니다.

 
[ ]:
Selection deleted
question = """Which state had the largest percent increase
in Medicaid visits from 2022 to 2023?"""
response = hospital_cypher_chain.invoke(question)
 
 
 
[ ]:
response.get("result")
 
 
 

2022년부터 2023년까지 Medicaid 방문이 가장 많이 증가한 주는 어느 주입니까?, LLM은 여러 노드, 관계 및 필터를 포함하는 상당히 장황한 Cypher 쿼리를 생성해야 했습니다. 그럼에도 불구하고 정답에 도달할 수 있었습니다.

챗봇에 필요한 마지막 기능은 대기 시간에 대한 질문에 답변하는 것이며, 이에 대해서는 다음에 다루겠습니다.

 

대기 시간 함수 생성

 

챗봇에 필요한 마지막 기능은 병원 대기 시간에 대한 질문에 답하는 것입니다. 앞에서 설명한 것처럼 조직에서는 대기 시간 데이터를 어디에도 저장하지 않으므로 챗봇은 외부 소스에서 해당 데이터를 가져와야 합니다. 이를 위해 두 가지 함수를 작성합니다. 하나는 병원의 현재 대기 시간을 찾는 것을 시뮬레이션하는 함수이고 다른 하나는 대기 시간이 가장 짧은 병원을 찾는 함수입니다.

참고 : 대기 시간 기능을 만드는 목적은 LangChain 에이전트가 체인이나 다른 LangChain 메서드뿐만 아니라 임의의 Python 코드를 실행할 수 있음을 보여주는 것입니다. 이 기능은 이론적으로 코드로 표현될 수 있는 모든 작업을 수행하는 에이전트를 만들 수 있다는 의미이므로 매우 중요합니다.

병원에서 현재 대기 시간을 가져오는 함수를 정의하는 것부터 시작하세요.

 
[ ]:
# chatbot_api/src/tools/wait_times.py
 
import os
from typing import Any
import numpy as np
from langchain_community.graphs import Neo4jGraph
 
def _get_current_hospitals() -> list[str]:
"""Fetch a list of current hospital names from a Neo4j database."""
graph = Neo4jGraph(
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
)
 
current_hospitals = graph.query(
"""
MATCH (h:Hospital)
RETURN h.name AS hospital_name
"""
)
 
return [d["hospital_name"].lower() for d in current_hospitals]
 
def _get_current_wait_time_minutes(hospital: str) -> int:
"""Get the current wait time at a hospital in minutes."""
current_hospitals = _get_current_hospitals()
 
if hospital.lower() not in current_hospitals:
return -1
 
return np.random.randint(low=0, high=600)
 
 
def get_current_wait_times(hospital: str) -> str:
"""Get the current wait time at a hospital formatted as a string."""
wait_time_in_minutes = _get_current_wait_time_minutes(hospital)
 
if wait_time_in_minutes == -1:
return f"Hospital '{hospital}' does not exist."
 
hours, minutes = divmod(wait_time_in_minutes, 60)
 
if hours > 0:
return f"{hours} hours {minutes} minutes"
else:
return f"{minutes} minutes"
 
 
 

첫 번째 함수는 Neo4j 데이터베이스에서 병원 이름 목록을 반환하는 _get_current_hospitals()을 정의한 것입니다. 그런 다음 _get_current_wait_time_minutes()가 병원 이름을 입력으로 사용합니다. 병원 이름이 유효하지 않은 경우 _get_current_wait_time_minutes()는 -1을 반환합니다. 병원 이름이 유효하면 _get_current_wait_time_minutes()는 대기 시간(분)을 시뮬레이션하여 0에서 600 사이의 임의의 정수를 반환합니다.

그런 다음 문자열 형식의 대기 시간을 반환하는 _get_current_wait_time_minutes() 래퍼하는 get_current_wait_times()을 정의합니다.

대기 시간이 가장 짧은 병원을 찾는 두 번째 함수를 정의하는 데 _get_current_wait_time_minutes()를 사용할 수 있습니다.

 
[ ]:
# chatbot_api/src/tools/wait_times.py
 
# ...
 
def get_most_available_hospital(_: Any) -> dict[str, float]:
"""Find the hospital with the shortest wait time."""
current_hospitals = _get_current_hospitals()
 
current_wait_times = [
_get_current_wait_time_minutes(h) for h in current_hospitals
]
 
best_time_idx = np.argmin(current_wait_times)
best_hospital = current_hospitals[best_time_idx]
best_wait_time = current_wait_times[best_time_idx]
 
return {best_hospital: best_wait_time}
 
 
 

여기에서는 각 병원에 _get_current_wait_time_minutes()을 요청하는 get_most_available_hospital()를 정의하고 대기 시간이 가장 짧은 병원으로 돌아갑니다. 어떻게 get_most_available_hospital()에게 throwaway input _이 있는지 확인하십시오. 이는 입력을 함수에 전달하도록 설계되었기 때문에 나중에 에이전트에서 필요합니다.

get_current_wait_times() 및 get_most_available_hospital()을 어떻게 사용할 지는 다음과 같습니다.

 
[ ]:
Selection deleted
import dotenv
dotenv.load_dotenv()
 
 
 
[ ]:
Selection deleted
from chatbot_api.src.tools.wait_times import (
get_current_wait_times,
get_most_available_hospital,
)
 
get_current_wait_times("Wallace-Hamilton")
 
 
 
[ ]:
get_current_wait_times("fake hospital")
 
 
 
[ ]:
get_most_available_hospital(None)
 
 
 

환경 변수를 로드한 후 Wallace-Hamilton 병원의 현재 대기 시간을 분 단위로 반환하는 get_current_wait_times("Wallace-Hamilton")를 호출을 합니다. get_current_wait_times("fake hospital")을 시도하면 데이터베이스에 가짜 병원이 존재하지 않는다는 문자열이 표시됩니다.

마지막으로 get_most_available_hospital()이 대기 시간이 가장 짧은 병원의 대기 시간을 분 단위로 저장한 사전을 반환합니다. 다음으로 Cypher 및 검토 체인과 함께 이러한 기능을 사용하여 병원 시스템에 대한 임의의 질문에 답하는 에이전트를 만듭니다.

 

챗봇 에이전트 생성

 

여기까지 해냈다면 스스로에게 칭찬을 해주세요. 많은 정보를 다루었으며 마침내 모든 정보를 하나로 모아 챗봇 역할을 할 에이전트를 구성할 준비가 되었습니다. 제공한 쿼리에 따라 에이전트는 Cypher 체인, 검토 체인 및 대기 시간 기능 중에서 결정해야 합니다.

에이전트의 종속성을 로드하고, 환경 변수에서 에이전트 모델 이름을 읽고, LangChain Hub에서 프롬프트 템플릿을 로드하는 것부터 시작하세요 .

 
[ ]:
# chatbot_api/src/agents/hospital_rag_agent.py
 
import os
from langchain_openai import ChatOpenAI
from langchain.agents import (
create_openai_functions_agent,
Tool,
AgentExecutor,
)
from langchain import hub
from chains.hospital_review_chain import reviews_vector_chain
from chains.hospital_cypher_chain import hospital_cypher_chain
from tools.wait_times import (
get_current_wait_times,
get_most_available_hospital,
)
 
HOSPITAL_AGENT_MODEL = os.getenv("HOSPITAL_AGENT_MODEL")
 
hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")
 
 
 

reviews_vector_chain, hospital_cypher_chain, get_current_wait_times() 및 get_most_available_hospital()을 가져오는 방법을 확인하세요. 에이전트는 이를 도구로 직접 사용합니다. HOSPITAL_AGENT_MODEL은 에이전트의 두뇌 역할을 하여 호출할 도구와 전달할 입력을 결정하는 LLM입니다.

에이전트에 대한 프롬프트를 직접 정의하는 대신 LangChain Hub에서 사전 정의된 프롬프트를 로드할 수 있습니다. LangChain 허브를 사용하면 프롬프트를 업로드, 탐색, 가져오기, 테스트 및 관리할 수 있습니다. 이 경우 OpenAI 함수 에이전트의 기본 프롬프트가 훌륭하게 작동합니다.

다음으로 에이전트가 사용할 수 있는 도구 목록을 정의합니다.

 
[ ]:
# pital_rag_agent.py
 
# ...
 
tools = [
Tool(
name="Experiences",
func=reviews_vector_chain.invoke,
description="""Useful when you need to answer questions
about patient experiences, feelings, or any other qualitative
question that could be answered about a patient using semantic
search. Not useful for answering objective questions that involve
counting, percentages, aggregations, or listing facts. Use the
entire prompt as input to the tool. For instance, if the prompt is
"Are patients satisfied with their care?", the input should be
"Are patients satisfied with their care?".
""",
),
Tool(
name="Graph",
func=hospital_cypher_chain.invoke,
description="""Useful for answering questions about patients,
physicians, hospitals, insurance payers, patient review
statistics, and hospital visit details. Use the entire prompt as
input to the tool. For instance, if the prompt is "How many visits
have there been?", the input should be "How many visits have
there been?".
""",
),
Tool(
name="Waits",
func=get_current_wait_times,
description="""Use when asked about current wait times
at a specific hospital. This tool can only get the current
wait time at a hospital and does not have any information about
aggregate or historical wait times. Do not pass the word "hospital"
as input, only the hospital name itself. For example, if the prompt
is "What is the current wait time at Jordan Inc Hospital?", the
input should be "Jordan Inc".
""",
),
Tool(
name="Availability",
func=get_most_available_hospital,
description="""
Use when you need to find out which hospital has the shortest
wait time. This tool does not have any information about aggregate
or historical wait times. This tool returns a dictionary with the
hospital name as the key and the wait time in minutes as the value.
""",
),
]
 
 
 

에이전트에는 Experiences, Graph, Waits 및 Availability 라는 네 가지 도구를 사용할 수 있습니다. 경험 및 그래프 도구는 해당 체인에서 .invoke()를 호출하는 반면, 대기 및 가용성은 정의한 대기 시간 함수를 호출합니다. 많은 도구 설명에는 에이전트에게 도구를 사용해야 하는 시기를 알려주고 어떤 입력을 전달해야 하는지에 대한 예를 제공하는 몇 번의 프롬프트가 있습니다.

체인과 마찬가지로 좋은 프롬프트 엔지니어링은 에이전트의 성공에 매우 중요합니다. 에이전트가 쿼리로 인해 혼동되지 않도록 각 도구와 사용 방법을 명확하게 설명해야 합니다.

마지막 단계는 에이전트를 인스턴스화하는 것입니다.

 
[ ]:
# chatbot_api/src/agents/hospital_rag_agent.py
 
# ...
 
chat_model = ChatOpenAI(
model=HOSPITAL_AGENT_MODEL,
temperature=0,
)
 
hospital_rag_agent = create_openai_functions_agent(
llm=chat_model,
prompt=hospital_agent_prompt,
tools=tools,
)
 
hospital_rag_agent_executor = AgentExecutor(
agent=hospital_rag_agent,
tools=tools,
return_intermediate_steps=True,
verbose=True,
)
 
 
 

먼저 LLM으로서 HOSPITAL_AGENT_MODEL을 사용하여 ChatOpenAI 개체를 초기화합니다. 그런 다음 create_openai_functions_agent()를 사용하여 OpenAI 함수 에이전트를 생성합니다. 그러면 입력을 함수에 전달하도록 OpenAI에서 설계한 에이전트가 생성됩니다. 함수 입력과 해당 값을 저장하는 JSON 개체를 반환하여 이를 수행합니다.

에이전트 런타임을 생성하려면 에이전트와 도구를 AgentExecutor에 전달합니다. return_intermediate_steps과 verbose를 true로 설정하면 에이전트의 사고 과정과 에이전트가 호출하는 도구를 볼 수 있습니다.

이로써 병원 시스템 에이전트 구축이 완료되었습니다. 이를 사용해 보려면 해당 chatbot_api/src/폴더로 이동하여 거기에서 새 REPL 세션을 시작해야 합니다.

참고 : 이는 나중에 Docker 컨테이너 내에서 실행될 hospital_rag_agent.py에서 상대 가져오기를 설정하기 때문에 필요합니다. 지금은 chatbot_api/src/ 가져오기가 작동하도록 탐색한 후에만 Python 인터프리터를 시작해야 함을 의미합니다.

이제 명령줄에서 병원 시스템 에이전트를 시험해 볼 수 있습니다.

 
[ ]:
Selection deleted
import dotenv
dotenv.load_dotenv()
 
 
 
[ ]:
Selection deleted
from agents.hospital_rag_agent import hospital_rag_agent_executor
 
response = hospital_rag_agent_executor.invoke(
{"input": "What is the wait time at Wallace-Hamilton?"}
)
 
 
 
[ ]:
response.get("output")
 
 
 
[ ]:
Selection deleted
response = hospital_rag_agent_executor.invoke(
{"input": "Which hospital has the shortest wait time?"}
)
 
 
 
[ ]:
response.get("output")
 
 
 

환경 변수를 로드한 후 에이전트에게 대기 시간에 대해 문의합니다. 각 쿼리에 대한 응답으로 정확히 무엇을 하는지 확인할 수 있습니다. 예를 들어, "월리스-해밀턴의 대기 시간은 얼마나 되나요?"라고 물을 때, Wait 도구를 호출하고 Wallace-Hamilton을 입력으로 전달합니다. 이는 에이전트가 get_current_wait_times("Wallace-Hamilton")을 호출하고 반환 값을 관찰하며 반환 값을 사용하여 질문에 답변하고 있음을 의미합니다.

에이전트의 전체 기능을 보려면 환자 검토가 필요한 환자 경험에 대해 질문할 수 있습니다.

 
[ ]:
Selection deleted
response = hospital_rag_agent_executor.invoke(
{
"input": (
"What have patients said about their "
"quality of rest during their stay?"
)
}
)
 
 
 
[ ]:
response.get("output")
 
 
 

질문에 리뷰나 경험을 명시적으로 언급하지 않는 방법을 여기에서 확인하세요. 에이전트는 도구 설명을 기반으로 Experiences를 호출해야 한다는 것을 알고 있습니다. 마지막으로 상담원에게 Cypher 쿼리로 답변해야 하는 질문을 할 수 있습니다.

 
[ ]:
Selection deleted
response = hospital_rag_agent_executor.invoke(
{
"input": (
"Which physician has treated the "
"most patients covered by Cigna?"
)
}
)
 
 
 
[ ]:
response.get("output")
 
 
 

에이전트는 쿼리를 기반으로 어떤 도구를 사용할지, 어떤 입력을 전달할지 파악하는 놀라운 능력을 갖추고 있습니다. 이것은 완벽하게 작동하는 챗봇입니다. 주어진 요구 사항을 기반으로 이해관계자가 물어볼 수 있는 모든 질문에 답할 수 있는 잠재력이 있으며 지금까지 훌륭한 작업을 수행하고 있는 것으로 보입니다.

챗봇에게 더 많은 질문을 하면 챗봇이 잘못된 도구를 호출하거나 잘못된 답변을 생성하는 상황에 거의 확실히 직면하게 될 것입니다. 프롬프트를 수정하면 잘못된 답변을 해결하는 데 도움이 될 수 있지만 때로는 입력 쿼리를 수정하여 챗봇을 도울 수도 있습니다. 이 예를 살펴보십시오.

 
[ ]:
Selection deleted
response = hospital_rag_agent_executor.invoke(
{"input": "Show me reviews written by patient 7674."}
)
 
 
 
[ ]:
response.get("output")
 
 
 

이 예에서는 상담원에게 환자 7674가 작성한 리뷰를 보여달라고 요청합니다. 상담원이 Experiences을 호출했지만 원하는 답변을 찾지 못했습니다. 시맨틱 벡터 검색을 사용하여 답변을 찾을 수도 있지만, 환자 ID 7674에 해당하는 리뷰를 조회하는 Cypher 쿼리를 생성하여 정확한 답변을 얻을 수 있습니다. 상담원이 이를 이해하는 데 도움이 되도록 쿼리에 추가 세부정보를 추가할 수 있습니다.

 
[ ]:
Selection deleted
response = hospital_rag_agent_executor.invoke(
{
"input": (
"Query the graph database to show me "
"the reviews written by patient 7674"
)
}
)
 
 
 
response.get("output")
 

여기서는 환자 ID 7674와 일치하는 리뷰를 찾기 위해 Graph를 올바르게 호출하는 그래프 데이터베이스를 쿼리하고 싶다고 에이전트에 명시적으로 알립니다. 이와 같은 쿼리에 더 자세한 정보를 제공하는 것은 에이전트가 명확하게 잘못된 도구를 호출할 때 에이전트를 안내하는 간단하면서도 효과적인 방법입니다.

리뷰 및 Cypher 체인과 마찬가지로 이를 이해관계자에게 공개하기 전에 에이전트를 평가하기 위한 프레임워크를 마련해야 합니다. 평가하려는 주요 기능은 올바른 입력으로 올바른 도구를 호출하는 에이전트의 능력과 호출하는 도구의 출력을 이해하고 해석하는 능력입니다.

마지막 단계에서는 FastAPI 및 Streamlit을 사용하여 병원 시스템 에이전트를 배포하는 방법을 알아봅니다. 이렇게 하면 API 엔드포인트를 호출하거나 Streamlit UI와 상호 작용하는 모든 사람이 에이전트에 액세스할 수 있습니다.

 

<출처 : https://realpython.com/build-llm-rag-chatbot-with-langchain/>