Langchain 공식문서의 내용을 정리한 것입니다. 내용 및 예제는 일부 변경하였지만 가능한 구조는 유지했습니다.
Conversational RAG - https://python.langchain.com/docs/tutorials/qa_chat_history/
많은 Q&A 애플리케이션에서 사용자가 대화를 주고받을 수 있도록 하기를 원한다. 이는 애플리케이션이 이전 질문과 답변을 기억하는 "메모리"와 이를 현재의 추론에 반영하는 로직이 필요하다는 의미이다.
이 가이드에서는 이전 메시지를 반영하는 로직을 추가하는 데 중점을 둔다. 대화 기록 관리에 대한 자세한 내용은 여기에서 다룬다.
우리는 두 가지 접근 방식을 다룰 것이다.
- 체인(Chains): 항상 검색 단계를 실행하는 방식.
- 에이전트(Agents): LLM이 검색 단계를 실행할지 여부와 방법을 자체적으로 판단하게 하는 방식 (또는 여러 단계를 실행할 수 있음).
외부 지식 소스로는 RAG 튜토리얼에서 사용한 특정 네이버 뉴스 게시물(한국을 뜨고싶은 이들, 왜?…"불안한 경제, 낮은 임금")을 사용한다.
준비
의존성(Dependency)
이 가이드에서는 OpenAI 임베딩과 Chroma 벡터 스토어를 사용할 것이다. 하지만 여기서 보여주는 모든 내용은 다른 임베딩, VectorStore, 또는 Retriever와도 작동한다.
다음 패키지들을 사용할 예정이다.
%%capture --no-stderr
%pip install --upgrade --quiet langchain langchain-community langchainhub langchain-chroma beautifulsoup4
환경 변수 OPENAI_API_KEY를 설정해야 한다. 이는 직접 설정하거나 다음과 같이 .env 파일에서 불러올 수 있다.
from dotenv import load_dotenv
load_dotenv()
LangSmith
LangChain을 사용하여 빌드하는 많은 애플리케이션은 여러 단계로 구성되며 LLM 호출이 여러 번 발생한다. 애플리케이션이 점점 더 복잡해짐에 따라 체인이나 에이전트 내부에서 정확히 어떤 일이 일어나고 있는지 확인하는 것이 매우 중요해진다. 이를 가장 잘 확인하는 방법은 LangSmith를 사용하는 것아다.
LangSmith는 필수는 아니지만 유용하다. LangSmith를 사용하려면 위의 링크에서 가입한 후, 추적 로그를 기록할 수 있도록 환경 변수를 설정해야 한다.
os.environ["LANGCHAIN_TRACING_V2"] = "true"
if not os.environ.get("LANGCHAIN_API_KEY"):
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()
체인 (Chains)
먼저, RAG에서 네이버 뉴스 게시물을 기반으로 구축한 Q&A 애플리케이션을 다시 살펴보겠다.
import bs4
from dotenv import load_dotenv
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
loader = WebBaseLoader(
web_paths=("https://n.news.naver.com/mnews/article/029/0002905111",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(class_=("media_end_head_headline", "newsct_article _article_body"))
),
)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
system_prompt = (
"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, say that you "
"don't know. Use three sentences maximum and keep the "
"answer concise."
"\n\n"
"{context}"
)
prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
response = rag_chain.invoke({"input": "한국을 뜨고 싶은 이유가 뭐야?"})
print(response["answer"])
실행결과
한국을 떠나고 싶어하는 주요 이유로는 불안정한 경제 상황(32.89%)과 낮은 임금(25%)이 있으며, 정치적 불신과 불평등한 사회에 대한 불만도 높은 순위를 차지하고 있습니다. 특히 젊은 세대는 해외에서의 개인적인 자유를 보장받을 것이라는 기대가 있습니다.
우리는 create_stuff_documents_chain
과 create_retrieval_chain
과 같은 내장 체인 생성자를 사용했다. 그래서 솔루션의 기본 요소는 다음과 같다.
- retriever (검색기)
- prompt (프롬프트)
- LLM (대형 언어 모델)
이 구성은 대화 기록을 통합하는 과정을 단순화해 준다.
채팅 이력 추가
우리가 만든 체인은 입력된 쿼리를 사용하여 관련된 컨텍스트를 검색한다. 그러나 대화형 환경에서는 사용자 쿼리가 대화의 맥락(Context)을 필요로 할 수 있다. 예를 들어, 다음 대화를 고려해 보자.
- 사용자: "한국을 뜨고 싶은 이유가 뭐야?"
- AI: "한국을 떠나고 싶어하는 주요 이유로는 불안정한 경제 상황(32.89%)과 낮은 임금(25%)이 있으며, 정치적 불신과 불평등한 사회에 대한 불만도 높은 순위를 차지하고 있습니다. 특히 젊은 세대는 해외에서의 개인적인 자유를 보장받을 것이라는 기대가 있습니다."
- 사용자: "그것에 가장 큰 영향을 미친 요인은 뭐야?"
두 번째 질문에 답변하려면, 시스템은 "그것"이 "한국을 뜨고 싶은 이유"를 의미한다는 것을 이해해야 한다.
기존 애플리케이션에서 두 가지를 업데이트해야 한다.
프롬프트: 대화 기록을 입력으로 지원하도록 프롬프트를 업데이트한다.
질문 문맥화
최신 사용자 질문을 대화 기록의 맥락에서 재구성하는 서브 체인을 추가한다. 이는 단순히 새로운 "이력을 인지하는 (history aware)" 검색기를 구축하는 것으로 생각할 수 있다. 이전에는 다음과 같이 구성했다.
query -> retriever
이제는 다음과 같이 구성된다.
(query, conversation history)
->LLM
->rephrased query
->retriever
우선, 대화 기록과 최신 사용자 질문을 받아, 이전 정보에 참조가 있는 경우 질문을 재구성하는 서브 체인을 정의해야 한다.
우리는 MessagesPlaceholder
변수를 "chat_history"라는 이름으로 포함하는 프롬프트를 사용할 것이다. 이를 통해 "chat_history" 입력 키를 사용하여 메시지 목록을 프롬프트에 전달할 수 있으며, 이러한 메시지는 시스템 메시지 다음, 그리고 최신 질문을 포함한 사용자 메시지 앞에 삽입된다.
이 단계에서 우리는 create_history_aware_retriever라는 헬퍼 함수를 활용한다. 이 함수는 대화 기록이 비어 있는 경우를 관리하며, 그렇지 않은 경우에는 prompt | llm | StrOutputParser() | retriever
를 순차적으로 적용한다.
create_history_aware_retriever는 input
과 chat_history
라는 키를 입력으로 받고, 출력 스키마는 기존 retriever와 동일한 체인을 구성한다.
from langchain.chains import create_history_aware_retriever
from langchain_core.prompts import MessagesPlaceholder
contextualize_q_system_prompt = (
"Given a chat history and the latest user question "
"which might reference context in the chat history, "
"formulate a standalone question which can be understood "
"without the chat history. Do NOT answer the question, "
"just reformulate it if needed and otherwise return it as is."
)
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
이 체인은 입력 쿼리의 재구성을 검색기에 추가하여, 검색 과정에서 대화의 맥락을 포함하도록 한다.
이제 전체 Q&A 체인을 구축할 수 있다. 이는 검색기를 새로운 history_aware_retriever
로 업데이트하는 것만큼 간단하다.
다시 한 번, 우리는 create_stuff_documents_chain을 사용하여 question_answer_chain
을 생성한다. 이 체인은 입력 키로 context
, chat_history
, input
을 사용하며, 검색된 컨텍스트와 대화 기록, 쿼리를 함께 받아 답변을 생성한다. 더 자세한 설명은 여기에서 확인할 수 있다.
우리는 create_retrieval_chain을 사용하여 최종 rag_chain
을 구축한다. 이 체인은 history_aware_retriever
와 question_answer_chain
을 순차적으로 적용하며, 편의성을 위해 검색된 컨텍스트와 같은 중간 출력을 유지한다. 입력 키로는 input
과 chat_history
를 사용하며, 출력에는 input
, chat_history
, context
, answer
가 포함된다.
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
이제 이 기능을 시도해 보자. 아래에서는 질문과 의미있는 답변을 리턴할 컨텍스트가 필요한 후속 질문을 한다. 우리의 체인에는 "chat_history"
입력이 포함되어 있기 때문에 호출자는 대화 기록을 관리해야 한다. 이는 입력 및 출력 메시지를 목록에 추가하여 수행할 수 있다.
from langchain_core.messages import AIMessage, HumanMessage
chat_history = []
question = "한국을 뜨고 싶은 이유가 뭐야?"
ai_msg_1 = rag_chain.invoke({"input": question, "chat_history": chat_history})
chat_history.extend(
[
HumanMessage(content=question),
AIMessage(content=ai_msg_1["answer"]),
]
)
second_question = "그것에 가장 큰 영향을 미친 요인은 뭐야?"
ai_msg_2 = rag_chain.invoke({"input": second_question, "chat_history": chat_history})
print(ai_msg_2["answer"])
해외 이주를 생각하는 데 가장 큰 영향을 미친 요인은 소셜 미디어로 나타났습니다. 그 다음으로 뉴스와 가족, 친구가 주요한 영향을 미치는 요소로 확인되었습니다. 이러한 요소들은 사람들이 해외 이주에 대한 관심을 높이는 데 기여하고 있습니다.
상태 기반 대화 기록 관리
이 튜토리얼의 이전 섹션에서는
RunnableWithMessageHistory
추상화를 사용했다. 해당 버전의 문서는 v0.2 문서에서 확인할 수 있다.LangChain의 v0.3 릴리스부터는 LangChain 사용자들이 LangGraph의 지속성을 활용하여 새로운 LangChain 애플리케이션에 메모리를 통합하는 것을 권장한다.
이미
RunnableWithMessageHistory
또는BaseChatMessageHistory
에 의존하는 코드가 있다면 변경할 필요는 없다. 이 기능은 단순한 채팅 애플리케이션에 유용하며, 가까운 시일 내에 해당 기능을 폐기할 계획이 없으므로RunnableWithMessageHistory
를 사용하는 코드는 계속해서 정상적으로 동작할 것이다.자세한 내용은 "LangGraph 메모리로 마이그레이션하는 방법"을 참조하라.
여기에서는 Chat History를 통합하기 위한 애플리케이션 로직을 추가하는 방법을 살펴보았지만, 여전히 대화 기록을 수동으로 업데이트하고 각 입력에 삽입하고 있다. 실제 Q&A 애플리케이션에서는 대화 기록을 지속적으로 저장하고 자동으로 삽입 및 업데이트하는 방법이 필요하다.
LangGraph는 내장된 영속성 계층을 구현하여, 여러 차례의 대화를 지원하는 채팅 애플리케이션에 이상적이다.
채팅 모델을 최소한의 LangGraph 애플리케이션으로 감싸면 메시지 기록이 자동으로 저장되어, 여러 차례의 애플리케이션 개발이 단순해진다.
LangGraph에는 간단한 인메모리 체크포인터가 내장되어 있으며, 아래에서는 이를 사용한다. 다른 영속성 백엔드(예: SQLite 또는 Postgres)를 사용하는 방법을 포함한 자세한 내용은 문서를 참조하라.
메시지 기록을 관리하는 방법에 대한 자세한 내용은 "메시지 기록(메모리) 추가하는 방법" 가이드를 참조하라.
from typing import Sequence
from langchain_core.messages import BaseMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from typing_extensions import Annotated, TypedDict
# We define a dict representing the state of the application.
# This state has the same input and output keys as `rag_chain`.
class State(TypedDict):
input: str
chat_history: Annotated[Sequence[BaseMessage], add_messages]
context: str
answer: str
# We then define a simple node that runs the `rag_chain`.
# The `return` values of the node update the graph state, so here we just
# update the chat history with the input message and response.
def call_model(state: State):
response = rag_chain.invoke(state)
return {
"chat_history": [
HumanMessage(state["input"]),
AIMessage(response["answer"]),
],
"context": response["context"],
"answer": response["answer"],
}
# Our graph consists only of one node:
workflow = StateGraph(state_schema=State)
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
# Finally, we compile the graph with a checkpointer object.
# This persists the state, in this case in memory.
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
이 애플리케이션은 기본적으로 여러 대화 스레드를 지원한다. 우리는 어떤 스레드를 실행할지 제어하기 위해 고유 식별자를 지정하는 config dict를 전달한다. 이를 통해 애플리케이션은 여러 사용자와의 상호작용을 지원할 수 있다.
config = {"configurable": {"thread_id": "abc123"}}
result = app.invoke(
{"input": "한국을 뜨고 싶은 이유가 뭐야?"},
config=config,
)
print(result["answer"])
한국을 떠나고 싶어하는 주요 이유는 불안정한 경제 상황과 낮은 임금에 대한 불만족입니다. 또한 정치적 불신과 불평등한 사회에 대한 불만도 높은 순위를 차지하고 있습니다. 이러한 요인들로 인해 많은 한국인들이 해외 이주를 고려하고 있습니다.
result = app.invoke(
{"input": "그것에 가장 큰 영향을 미친 요인은 뭐야?"},
config=config,
)
print(result["answer"])
해외 이주를 고려하는 데 가장 큰 영향을 미친 요인은 '소셜 미디어'(46.05%)로 나타났습니다. 그 뒤를 이어 '뉴스'(35.53%)와 '가족'(34.21%), '친구'(32.46%)가 주요 요인으로 꼽혔습니다.
대화 이력은 애플리케이션 상태에서 확인할 수 있다.
chat_history = app.get_state(config).values["chat_history"]
for message in chat_history:
message.pretty_print()
================================[1m Human Message [0m=================================
한국을 뜨고 싶은 이유가 뭐야?
==================================[1m Ai Message [0m==================================
AI: 한국을 떠나고 싶은 이유는 주로 불안정한 경제 상황(32.89%)과 낮은 임금(25%)에 대한 불만족입니다. 또한 불평등한 사회(28.73%)와 정치적 불신(28.29%) 등 정치적, 사회적 요인도 큰 영향을 미치고 있습니다. 응답자들은 직업과 학업을 주된 이유로 해외 이주를 고려하고 있습니다.
================================[1m Human Message [0m=================================
User: 그것에 가장 큰 영향을 미친 요인은 뭐야?
==================================[1m Ai Message [0m==================================
AI: 해외 이주를 고려하는 데 가장 큰 영향을 미친 요인은 '소셜 미디어'(46.05%)입니다. 그 다음으로 '뉴스'(35.53%)와 '가족'(34.21%), '친구'(32.46%)가 주요 요인으로 꼽혔습니다.
정리하자면
편의상, 단일 코드로 필요한 모든단계를 합쳤다.
from typing import TypedDict, Annotated, Sequence
import bs4
from dotenv import load_dotenv
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.history_aware_retriever import create_history_aware_retriever
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.checkpoint.memory import MemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
# 1. Load, chunk and index the contents of the blog to create a retriever.
loader = WebBaseLoader(
web_paths=("https://n.news.naver.com/mnews/article/029/0002905111",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(class_=("media_end_head_headline", "newsct_article _article_body"))
),
)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
contextualize_q_system_prompt = (
"Given a chat history and the latest user question "
"which might reference context in the chat history, "
"formulate a standalone question which can be understood "
"without the chat history. Do NOT answer the question, "
"just reformulate it if needed and otherwise return it as is."
)
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
system_prompt = (
"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, say that you "
"don't know. Use three sentences maximum and keep the "
"answer concise."
"\n\n"
"{context}"
)
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
### Statefully manage chat history ###
class State(TypedDict):
input: str
chat_history: Annotated[Sequence[BaseMessage], add_messages]
context: str
answer: str
def call_model(state: State):
response = rag_chain.invoke(state)
return {
"chat_history": [
HumanMessage(state["input"]),
AIMessage(response["answer"]),
],
"context": response["context"],
"answer": response["answer"],
}
workflow = StateGraph(state_schema=State)
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "abc123"}}
result = app.invoke(
{"input": "한국을 뜨고 싶은 이유가 뭐야?"},
config=config,
)
print(result["answer"])
result = app.invoke(
{"input": "그것에 가장 큰 영향을 미친 요인은 뭐야?"},
config=config,
)
print(result["answer"])
한국을 떠나고 싶은 이유는 주로 불안정한 경제 상황(32.89%)과 낮은 임금(25%)에 대한 불만족입니다. 또한 불평등한 사회(28.73%)와 정치적 불신(28.29%) 등 정치적, 사회적 요인도 큰 영향을 미치고 있습니다. 응답자들은 직업과 학업을 주된 이유로 해외 이주를 고려하고 있습니다.
해외 이주를 고려하는 데 가장 큰 영향을 미친 요인은 '소셜 미디어'(46.05%)입니다. 그 다음으로 '뉴스'(35.53%)와 '가족'(34.21%), '친구'(32.46%)가 주요 요인으로 꼽혔습니다.
Agents (에이전트)
Agents는 실행 중에 결정을 내리기 위해 LLM의 추론 능력을 활용한다. Agents를 사용하면 검색 과정에 대한 일부 재량을 위임할 수 있다. 체인보다 행동이 덜 예측 가능하지만, 이 컨텍스트에서 몇 가지 장점을 제공한다.
- Agents는 위에서 설명한 대로 반드시 컨텍스트를 명시적으로 구축할 필요 없이, 직접적으로 검색기에 입력을 생성한다.
- Agents는 쿼리를 지원하기 위해 여러 검색 단계를 실행할 수 있으며, 사용자의 일반적인 인사에 대한 응답으로 검색 단계를 전혀 실행하지 않을 수도 있다.
검색 도구
Agents는 "도구(Tools)"에 접근하고 그 실행을 관리할 수 있다. 이 경우, 우리는 검색기를 Agents(에이전트)가 사용할 수 있는 LangChain 도구로 변환할 것이다.
from langchain.tools.retriever import create_retriever_tool
tool = create_retriever_tool(
retriever,
"blog_post_retriever",
"Searches and returns excerpts from the Autonomous Agents blog post.",
)
tools = [tool]
도구(Tools)는 Langchain Runnables이고 보통 인터페이스를 구현한다.
tool.invoke("task decomposition")
'Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates ......
Agent 생성자
이제 도구와 LLM을 정의했으므로 에이전트를 생성할 수 있다. 우리는 LangGraph를 사용하여 에이전트를 구성할 것이다. 현재는 에이전트를 구성하기 위해 고수준 인터페이스를 사용하고 있지만, LangGraph의 장점은 이 고수준 인터페이스가 에이전트 로직을 수정하고 싶을 경우 사용할 수 있는 저수준의 높은 제어 가능성을 가진 API에 의해 지원된다는 것이다.
from langgraph.prebuilt import create_react_agent
agent_executor = create_react_agent(llm, tools)
이제 이를 시험해볼 수 있다. 지금까지는 상태를 유지하지 않는다는 점에 유의하라 (아직 메모리를 추가해야 합니다).
query = "What is Task Decomposition?"
for s in agent_executor.stream(
{"messages": [HumanMessage(content=query)]},
):
print(s)
print("----")
Error in LangChainTracer.on_tool_end callback: TracerException("Found chain run at ID 1a50f4da-34a7-44af-8cbb-c67c90c9619e, but expected {'tool'} run.")
``````output
{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_1ZkTWsLYIlKZ1uMyIQGUuyJx', 'function': {'arguments': '{"query":"Task Decomposition"}', 'name': 'blog_post_retriever'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 68, 'total_tokens': 87}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-dddbe2d2-2355-4ca5-9961-1ceb39d78cf9-0', tool_calls=[{'name': 'blog_post_retriever', 'args': {'query': 'Task Decomposition'}, 'id': 'call_1ZkTWsLYIlKZ1uMyIQGUuyJx'}])]}}
----
{'tools': {'messages': [ToolMessage(content='Fig. 1. Overview of a LLM-powered autonomous agent system.\nComponent One: ....n inputs.', name='blog_post_retriever', tool_call_id='call_1ZkTWsLYIlKZ1uMyIQGUuyJx')]}}
----
{'agent': {'messages': [AIMessage(content='Task decomposition is a technique used to ..., or human inputs.', response_metadata={'token_usage': {'completion_tokens': 119, 'prompt_tokens': 636, 'total_tokens': 755}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-4a701854-97f2-4ec2-b6e1-73410911fa72-0')]}}
----
LangGraph는 자체에서 영속성을 제공하므로 ChatMessageHistory를 사용할 필요가 없다. 대신, LangGraph 에이전트에 체크포인터를 직접 전달할 수 있다.
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
agent_executor = create_react_agent(llm, tools, checkpointer=memory)
이것이 대화형 RAG 에이전트를 구성하는 데 필요한 모든 것이다.
이제 그 동작을 관찰해 보자. 검색 단계가 필요하지 않은 쿼리를 입력하면 에이전트가 검색 단계를 실행하지 않는다는 점에 유의하라.
config = {"configurable": {"thread_id": "abc123"}}
for s in agent_executor.stream(
{"messages": [HumanMessage(content="Hi! I'm bob")]}, config=config
):
print(s)
print("----")
{'agent': {'messages': [AIMessage(content='Hello Bob! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 67, 'total_tokens': 78}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-022806f0-eb26-4c87-9132-ed2fcc6c21ea-0')]}}
----
또한, 검색 단계가 필요한 쿼리를 입력하면 에이전트가 도구에 대한 입력을 생성한다.
query = "What is Task Decomposition?"
for s in agent_executor.stream(
{"messages": [HumanMessage(content=query)]}, config=config
):
print(s)
print("----")
{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_DdAAJJgGIQOZQgKVE4duDyML', 'function': {'arguments': '{"query":"Task Decomposition"}', 'name': 'blog_post_retriever'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 91, 'total_tokens': 110}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-acc3c903-4f6f-48dd-8b36-f6f3b80d0856-0', tool_calls=[{'name': 'blog_post_retriever', 'args': {'query': 'Task Decomposition'}, 'id': 'call_DdAAJJgGIQOZQgKVE4duDyML'}])]}}
----
``````output
Error in LangChainTracer.on_tool_end callback: TracerException("Found chain run at ID 9a7ba580-ec91-412d-9649-1b5cbf5ae7bc, but expected {'tool'} run.")
``````output
{'tools': {'messages': [ToolMessage(content='Fig. 1. Overview of a LLM-powered autonomous agent system.\nComponent One: Planning#\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\nTask Decomposition#\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more te...task-specific instructions; e.g. "Write a story outline." for writing a novel, or (3) with human inputs.', name='blog_post_retriever', tool_call_id='call_DdAAJJgGIQOZQgKVE4duDyML')]}}
----
위에서 에이전트는 쿼리를 도구에 그대로 삽입하는 대신 "what"과 "is"와 같은 불필요한 단어를 제거했다.
이 동일한 원칙은 에이전트가 필요할 때 대화의 맥락을 사용할 수 있게 해준다.
query = "What according to the blog post are common ways of doing it? redo the search"
for s in agent_executor.stream(
{"messages": [HumanMessage(content=query)]}, config=config
):
print(s)
print("----")
{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_KvoiamnLfGEzMeEMlV3u0TJ7', 'function': {'arguments': '{"query":"common ways of task decomposition"}', 'name': 'blog_post_retriever'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 930, 'total_tokens': 951}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-dd842071-6dbd-4b68-8657-892eaca58638-0', tool_calls=[{'name': 'blog_post_retriever', 'args': {'query': 'common ways of task decomposition'}, 'id': 'call_KvoiamnLfGEzMeEMlV3u0TJ7'}])]}}
----
{'action': {'messages': [ToolMessage(content='Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thou....t tell the user the complete file path.', name='blog_post_retriever', id='c749bb8e-c8e0-4fa3-bc11-3e2e0651880b', tool_call_id='call_KvoiamnLfGEzMeEMlV3u0TJ7')]}}
----
{'agent': {'messages': [AIMessage(content='According to the blog post, common ways of task decomposition include:\n\n1. Using language models with simple prompting like "Steps for XYZ" or "What are the subgoals for achieving XYZ?"\n2. Utilizing task-specific instructions, for example, using "Write a story outline" for writing a novel.\n3. Involving human inputs in the task decomposition process.\n\nThese methods help in breaking down complex tasks into smaller and more manageable steps, facilitating better planning and execution of the overall task.', response_metadata={'token_usage': {'completion_tokens': 100, 'prompt_tokens': 1475, 'total_tokens': 1575}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'stop', 'logprobs': None}, id='run-98b765b3-f1a6-4c9a-ad0f-2db7950b900f-0')]}}
----
에이전트는 쿼리에서 "it"가 "작업 분해"를 참조하고 있다는 것을 추론할 수 있었으며, 그 결과로 합리적인 검색 쿼리인 "작업 분해의 일반적인 방법"을 생성했다는 점에 유의하라.
정리하자면
편의상, 단일 코드로 필요한 모든단계를 합쳤다.
import bs4
from langchain.tools.retriever import create_retriever_tool
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
memory = MemorySaver()
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
### Construct retriever ###
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
### Build retriever tool ###
tool = create_retriever_tool(
retriever,
"blog_post_retriever",
"Searches and returns excerpts from the Autonomous Agents blog post.",
)
tools = [tool]
agent_executor = create_react_agent(llm, tools, checkpointer=memory)