[LLM] 체인(Chains) - 문서 체인
문서 체인(Documents Chain)
02-1. stuff documents chain
문서 체인 중 가장 간단한 방식인 stuff documents chain은 문서 목록을 가져와서 모두 프롬프트에 삽입한 다음,
그 프롬프트를 LLM에 전달한다. 이 체인은 문서의 크기가 작고, 몇개만 전달되는 애플리케이션에 적합하다.
그럼 예시로 사용자로부터 입력 받은 문장을 요약하는 요청을 처리하기위한 프롬프트 템플릿을 정의해보자.
from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are an expert summarizer. Please summarize the following sentence.",
),
(
"user",
"Please summarize the sentence according to the following request."
"\nREQUEST:\n"
"1. Summarize the main points in bullet points in Korean."
"2. Each summarized sentence must start with an emoji that fits the meaning of the each sentence."
"3. Use various emojis to make the summary more interesting."
"\n\nCONTEXT: {context}\n\nSUMMARY:",
),
]
)
prompt
이 템플릿은 시스템이 전문 요약가 역할을 수행하도록 요청하고, 사용자에게 한국어로 불릿 포인트 형식의 요약, 각 문장의 의미에 맞는 이모지 사용, 다양한 이모지를 활용하여 요약을 더 흥미롭게 만들라는 지시를 포함한다.
템플릿이 정의가 완료되었으면 WebBaseLoader를 사용하여 뉴스기사를 로드하고, Document를 생성한다.
from langchain.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://www.gutenberg.org/files/1727/1727-h/1727-h.htm")
docs = loader.load()
마지막으로, create_stuff_documents_chain 함수를 생성하여 문서 생성 체인을 만들고, 이 체인을 통해 주어진 문맥에 대한 응답을 생성한다.
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain.callbacks.manager import CallbackManager
callback_manager = CallbackManager([StreamingStdOutCallbackHandler()])
llm = Ollama(
model="gemma:latest",
callbacks=callback_manager
)
chain = create_stuff_documents_chain(llm, prompt)
answer = chain.invoke({"context": docs})
answer
02-2. MapReduce
만약 문서집합을 가지고 있고, 내용을 요약하고 싶을 땐, LLM을 사용하면 된다.
요약기를 구축할 때, 핵심은 문서를 LLM의 컨텍스트 창에 어떻게 전달할 것인가다. 이를 위한 접근 방식은 다음과 같다.
1 | Stuff | 단순히 모든 문서를 단일 프롬프트로 넣는 방식 |
2 | Map-reduce | 각 문서를 map 단계에서 개별적으로 요약한 다음, reduce 단계에서 요약본들을 최종 요약본으로 합치는 방식 |
3 | Refine | 입력 문서를 순회하며 반복적으로 답변을 업데이트하여 응답을 구성 각 문서에 대해 모든 비문서 입력, 현재 문서, 그리고 최신 중간 답변을 LLM 체인에 전달하여 새로운 답변을 얻음 |
만약 블로그 포스트를 요약하고 싶다고 가정해보자.
load_summarize_chain 함수를 사용해서 요약기를 생성할 수 있다.
from langchain.chains.summarize import load_summarize_chain
loader = WebBaseLoader("https://heywantodo.tistory.com/355")
docs = loader.load()
callback_manager = CallbackManager([StreamingStdOutCallbackHandler()])
llm = Ollama(
model="gemma:latest",
callbacks=callback_manager
)
chain = load_summarize_chain(llm, chain_type="stuff")
answer = chain.invoke({"input_documents": docs})
print(answer["output_text"])
위 코드는 stuff 방식을 이용해 요약하였다.
그렇다면 MapReduce 접근 방식에 대해 자세히 살펴보자.
먼저 각 문서를 개별적으로 요약을 하기 위해 LLMChain을 사용하고, ReduceDocumentsChain을 사용하여 그 요약을 하나의 전역 요약으로 결합한다.
과정을 정리해보면 다음과 같다.
- LLM 모델 인스턴스를 생성하고, 이를 사용하여 문서 집합에 대한 맵(map) 작업을 정의한다.
- 맵 작업은 map_template를 사용하여 정의되며, 문서 집합을 입력으로 받는다.
- PromptTemplate.from_template를 사용하여 map_template에서 프롬프트 템플릿을 생성하고, LLMChain을 사용하여 맵 작업을 실행한다.
# 모델 인스턴스 생성
from langchain_community.llms import Ollama
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain.callbacks.manager import CallbackManager
loader = WebBaseLoader("https://heywantodo.tistory.com/355")
docs = loader.load()
callback_manager = CallbackManager([StreamingStdOutCallbackHandler()])
llm = Ollama(
model="gemma:latest",
callbacks=callback_manager
)
# 맵 작업 정의
from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.llm import LLMChain
from langchain import hub
# 랭체인 허브에서 map-prompt를 가져온다.
map_prompt = hub.pull("teddynote/map-prompt")
map_prompt
# LLMChain 인스턴스를 생성한다.
map_chain = LLMChain(prompt=map_prompt, llm=llm)
ReduceDocumentsChain은 문서 매핑 결과를 가져와 단일 출력으로 축소하는 역할을 한다.
일반적인 CombineDocumentsChain을 감싸지만, 누적 크기가 token_max를 초과하는 경우 문서를 축소하여 전달할 수 있는 기능을 추가한다.
(문서를 결합하기 위해 사용하는 체인을 문서를 축소하는 데에도 재사용한다.)
정리를 해보면, 매핑한 문서의 누적 토큰 수가 4000 토큰을 초과하는 경우, 4000토큰 미만의 배치로 문서를 stuff 체인에 전달하여 배치 요약을 생성한다.
그리고 요약이 4000 토큰 미만이 되면 한번 더 전달하여 최종 요약을 생성한다.
# 리듀스 작업 정의
from langchain.chains.combine_documents.stuff import StuffDocumentsChain
# 연쇄 실행
reduce_prompt = hub.pull("teddynote/reduce-prompt-korean")
reduce_chain = LLMChain(prompt = reduce_prompt, llm=llm)
# 문서 리스트를 받아 하나의 문자열로 결합한 후 LLMChain에 전달
combine_documents_chain = StuffDocumentsChain(
llm_chain=reduce_chain, document_variable_name="doc_summaries"
)
# 매핑된 문서들을 결합하고 반복적으로 축소
reduce_documents_chain = ReduceDocumentsChain(
# 최종적으로 호출되는 체인입니다.
combine_documents_chain=combine_documents_chain,
# `StuffDocumentsChain`의 컨텍스트를 초과하는 문서들을 처리
collapse_documents_chain=combine_documents_chain,
# 문서들을 그룹화할 최대 토큰 수.
token_max=4096,
)
각 체인의 역할은 다음과 같다.
- reduce_chain(LLMChain) : 언어 모델을 실행한다.
- combine_documents_chain(StuffDocumentsChain) : 여러 문서를 하나의 문자열로 결합하여 LLMChain에 전달한다.
- reduce_documents_chain(ReduceDocumentsChain) : 문서들을 결합하고, 지정된 토큰 수를 초과하지 않도록 반복적으로 축소하는 과정을 담당한다.
map 작업과 reduce 작업을 각각 정의를 했는데, map과 reduce 체인을 하나로 결합해보자.
MapReduceDocumentsChain 객체를 생성하여 문서들을 매핑하고 리듀스하는 과정을 통해 결합을 한다.
# 문서들을 매핑하여 체인을 거친 후 결과를 결합하는 과정
map_reduce_chain = MapReduceDocumentsChain(
# 매핑 체인
llm_chain=map_chain,
# 리듀스 체인
reduce_documents_chain=reduce_documents_chain,
# llm_chain에서 문서들을 넣을 변수 이름
document_variable_name="docs",
# 매핑 단계의 결과를 출력에 포함시킴
return_intermediate_steps=False,
)
# 문자를 기준으로 텍스트를 분할하는 객체 생성
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=50,
separators=["\n\n", "\n", "(?<=\. )", " ", ""],
length_function=len,
)
# 문서들을 분할
split_docs = text_splitter.split_documents(docs)
최종적으로 map_reduce_chain에 문서(split_docs)를 전달하여 실행한 결과를 출력한다.
summary_result = map_reduce_chain.invoke({"input_documents": split_docs})
02-3. Refine
Refine은 map-reduce와 유사하다. chain_type을 지정하면 쉽게 실행이 가능하다.
chain = load_summarize_chain(llm, chain_type="refine")
chain.run(split_docs)
프롬프트를 제공하고 중간 단계를 반환하는 것도 가능하다.
from langchain_core.prompts import ChatPromptTemplate
# 요약에 사용될 템플릿 생성
prompt_template = """Write a concise summary of the following:
{text}
CONCISE SUMMARY:"""
prompt = ChatPromptTemplate.from_template(prompt_template)
# 다듬기 작업에 사용될 템플릿 생성
refine_template = (
"Your job is to produce a final summary\n"
"We have provided an existing summary up to a certain point: {existing_answer}\n"
"We have the opportunity to refine the existing summary"
"(only if needed) with some more context below.\n"
"------------\n"
"{text}\n"
"------------\n"
"Given the new context, refine the original summary in Korean"
"If the context isn't useful, return the original summary."
)
refine_prompt = ChatPromptTemplate.from_template(refine_template)
# 요약을 생성하고 다듬기 과정을 관리하는 체인을 로드
chain = load_summarize_chain(
llm=llm,
chain_type="refine",
question_prompt=prompt,
refine_prompt=refine_prompt,
return_intermediate_steps=True,
input_key="input_documents",
output_key="output_text",
)
result = chain.invoke({"input_documents": split_docs}, return_only_outputs=True)
02-4. AnalyzeDocumentsChain
from langchain.chains import AnalyzeDocumentChain
# AnalyzeDocumentChain 인스턴스를 생성, 이때, combine_docs_chain과 text_splitter를 인자로 전달
summarize_document_chain = AnalyzeDocumentChain(
combine_docs_chain=chain, text_splitter=text_splitter
)
# 첫 번째 문서의 페이지 내용을 사용하여 문서 요약 프로세스를 실행
summarized_result = summarize_document_chain.invoke(
{"input_document": docs[0].page_content}
)
출처
https://wikidocs.net/book/14314
'🤖 AI' 카테고리의 다른 글
[CrewAI] CrewAI 시작하기 (1) | 2024.11.19 |
---|---|
[CrewAI] CrewAI란? (0) | 2024.11.18 |
[LLM] 체인(Chains) - 대화형 체인 (0) | 2024.07.09 |