udemy 강좌 내용[LangGraph- Develop LLM powered AI agents with LangGraph]의 일부를 정리한 내용입니다. 예제의 일부 내용을 수정하였습니다. 문제가 되면 삭제하겠습니다.
https://www.udemy.com/course/langgraph/learn/lecture/44086612#overview
이번에는 Adative RAG를 구현해보자. Adaptive RAG은 연구 논문 기반이다. Adative RAG는 질문의 내용을 보고 각기 다른 RAG flow로 질문을 라우팅하는 역할을 한다.
아래 그림에서 보는 것과 같이 start부터 두 가지로 나뉜다. 첫 번째는 websearch를 하고 generate를 하는 방식이고 두 번째 방식은 vector store의 RAG를 사용하는 방식이다.
그래서 사용자 질문을 받고 답변을 만들기 위해 vector store에 정보가 있는지 판단하여, 없다면 단순히 websearch를 하여 답변을 작성 한다.
제일 먼저 할 일은 question router chain을 만들어 질문을 websearch로 보낼지 retrieve로 보낼 지 판단하는 것이다.
[router.py]
from typing import Literal
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class RouteQuery(BaseModel):
"""Route a user query to the most relevant datasource."""
datasource: Literal["vectorstore", "websearch"] = Field(
...,
description="Given a user question choose to route it to web search or a vectorstore.",
)
llm = ChatOpenAI(temperature=0)
structured_llm_router = llm.with_structured_output(RouteQuery)
system = """You are an expert at routing a user question to a vectorstore or web search.
The vectorstore contains documents related to baseball.
User the vectorstore for questions on these topics. For all else, use web-search."""
route_prompt = ChatPromptTemplate.from_messages(
[
("system", system),
("human", "{question}"),
]
)
question_router = route_prompt | structured_llm_router
우선 RouterQuery 클래스를 만들어 datasource로 vectorstore 혹은 websearch 값이 올 수 있게 한다.
다음은 with_structured_output로 위에서 만든 RouteQuery를 응답으로 받을 수 있게 한다.
이제 system 프롬프트를 작성한다. "Baseball" 관련 질문은 vector store를 검색하고 나머지는 web search를 이용한다는 내용이다.
[graph.py]
def route_question(state: GraphState) -> str:
print("---ROUTE QUESTION---")
question = state["question"]
source: RouteQuery = question_router.invoke({"question": question})
if source.datasource == WEBSEARCH:
print("---ROUTE QUESTION TO WEB SEARCH---")
return WEBSEARCH
elif source.datasource == "vectorstore":
print("---ROUTE QUESTION TO RAG---")
return RETRIEVE
workflow.set_conditional_entry_point(
route_question,
{
WEBSEARCH: WEBSEARCH,
RETRIEVE: RETRIEVE,
},
)
이제 route_question 함수를 만들자. state를 받고 다음 node로 string을 리턴한다.
question_router를 실행하고 결과로 datasource를 받는다. datasource가 websearch라면 검색을 하게 하고 vectorstore라면 retrieve를 리턴한다.
그리고 add_conditional_edgesf를 추가하여 route_question을 분기할 수 있게 한다.
이제 실행을 해보자.
[main.py]
from dotenv import load_dotenv
load_dotenv()
from graphs.graph import app
if __name__ == "__main__":
print(app.invoke(input={"question": "야구에서 홈런은 무엇이야?"}))
실행결과는 아래와 같다. route question에서 retrieve를 실행하는 것을 확인할 수 있다.
---ROUTE QUESTION---
---ROUTE QUESTION TO RAG---
---RETRIEVE---
---CHECK DOCUMENT RELEVANCE TO QUESTION---
---GRADE: DOCUMENT NOT RELEVANT---
---GRADE: DOCUMENT RELEVANT---
---GRADE: DOCUMENT RELEVANT---
---GRADE: DOCUMENT NOT RELEVANT---
---ASSESS GRADED DOCUMENTS---
---DECISION: GENERATE---
---GENERATE---
---CHECK HALLUCINATIONS---
---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---
---GRADE GENERATION vs QUESTION---
---DECISION: GENERATION ADDRESSES QUESTION---
{'question': '야구에서 홈런은 뭐야?', 'generation': '홈런은 타자가 홈 베이스를 밟을 수 있게 공을 치는 것으로, 수비수의 실책 없이 타자가 홈 베이스를 밟을 때 발생합니다. 홈런을 치면 주자가 순서대로 진루하여 홈 플레이트를 밟으면 최대 4점을 득점할 수 있습니다. 만루홈런을 치면 4점을 득점할 수 있습니다.', 'web_search': False, 'documents': [Document(metadata={'language': 'ko', 'source': 'https://namu.wiki/w/%EC%95%BC%EA%B5%AC/%EA%B2%BD%EA%B8%B0%20%EB%B0%A9%EC%8B%9D', 'title': '야구/경기 방식 - 나무위키'}, page_content='야구/경기 방식 - 나무위키'), Document(metadata={'language': 'ko', 'source': 'https://namu.wiki/w/%EC%95%BC%EA%B5%AC/%EA%B2%BD%EA%B8%B0%20%EB%B0%A9%EC%8B%9D', 'title': '야구/경기 방식 - 나무위키'}, page_content='살아서 올 세이프가 된 경우에는 안타가 아니라 야수선택으로 기록된다. 반대로 주자가 죽었더라도 주자가 한 개의 루를 지나서 추가 진루를 하려다가 죽은 경우(예를 들어 1사 1루 상황에서 우전 안타 - 주자가 2루를 지나 3루까지 가려다가 3루에서 죽은 경우)에는 타자에게는 안타를 인정한다.홈런(Home Run)수비수의 실책 없이 타자가 홈 베이스를 밟을 수 있게 공을 치는 것. 타자가 홈 베이스를 밟을 시간만큼 공을 치려면 수비가 못 잡도록 바운드가 되기 전에 아예 담장 밖으로 날려버리거나[25], 담장 안에 공이 떨어질 경우엔 수비수가 그동안 공을 처리하지 못할 정도로 멀리 오랫동안 처리할 타구를 보내야 한다. 현대 야구에서는 전자의 경우를 홈런으로 기록하고, 후자의 경우는 인사이드 파크 홈런으로 기록한다. 루상에 주자가 있으면 순서대로 진루하여 홈 플레이트를 밟으면 득점한다. 그렇기 때문에 만루홈런을[26] 치면 최대 4점을 득점할 수 있다. 다만 공의 착지점을 확인하기 애매할 때가 많은 만큼 이를 판정하기 위해 파울 라인과 페어 지역의 경계 지점에 파울 폴이 세워져 있는데, 일단은 당연히 이 안쪽으로 공이 들어가야 하고, 폴 위를 정확히 지나가거나 폴을 직접 맞추는 것도 홈런으로 인정된다. 또한 공이 펜스를 넘었다가 그물이나 관중에 의해 다시 공이 그라운드로 들어가는 경우에도 홈런으로 인정된다. 이를 확인하기 위해서 보통 단층 펜스 상단에 노란 줄을 긋고 줄을 넘어가면 홈런으로 인정하는 룰을 많이 쓴다.[27] 이중으로 펜스가 쳐진 경우는 예외. 획일적 구조로 지어진 한국과는 달리 미국은 구장마다 펜스의 생김새와 규정이 각각 달라서')]}
langsmith에서 확인하면 아래와 같다.
이제 웹검색하는 케이스를 테스트하기 위해 축구관련 질문을 해보자.
if __name__ == "__main__":
print(app.invoke(input={"question": "축구에서 파울은 뭐야?"}))
실행결과
---ROUTE QUESTION---
---ROUTE QUESTION TO WEB SEARCH---
---WEB SEARCH---
---GENERATE---
---CHECK HALLUCINATIONS---
---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---
---GRADE GENERATION vs QUESTION---
---DECISION: GENERATION ADDRESSES QUESTION---
{'question': '축구에서 파울은 뭐야?', 'generation': '축구에서 파울은 상대의 플레이에 지장을 주는 행위이거나 파울을 얻으려 지장을 얻은척 액션을 하는 경우를 말합니다. 파울은 경기의 흐름과 결과에 중대한 영향을 미칠 수 있으며, 반복적인 위반은 옐로우 카드 또는 레드카드로 징계될 수 있습니다. 파울을 피하기 위해서는 마인드컨트롤과 적절한 위치, 예상, 그리고 타이밍을 잘 지키는 것이 중요합니다.', 'documents': [Document(metadata={}, page_content="파울 (축구) - 위키백과, 우리 모두의 백과사전 본문으로 이동 사이드바로 이동 숨기기 계정 만들기 개인 도구 사이드바로 이동 숨기기 1 파울이 발생되는 원인 ...")]}
route_question에서 web search를 한 것을 확인할 수 있다.
langsmith에서 확인하면 아래와 같다.