W&B 문서를 위한 질의응답 봇 만들기
이 글에서는 Weights & Biases 문서를 위한 질문·답변(Q&A) 봇을 만드는 방법을 설명합니다. 이 글은 AI 번역본입니다. 오역이 있을 수 있으니 댓글로 자유롭게 알려 주세요.
Created on September 15|Last edited on September 15
Comment
소개
이 글은 제가 만든 문서 Q&A 봇에 대한 설명으로, 다음의 일부로 구축했습니다 Replit x Weights & Biases ML 해커톤. 이 봇은 OpenAI의 GPT-3를 사용해 자연어 질문과 개발자 관련 문의에 답변합니다 Weights & Biases 문서. 저는 사용합니다 LangChain, OpenAI Embeddings, 그리고 FAISS Q&A 백엔드를 만들기 위해, 그리고 봇은 …로 제공됩니다 Gradio 애플리케이션.
이것은 문서 기반 Q&A가 어떤 모습일지 감을 잡기 위한, 매우 단순하고 기초적인 개념 증명입니다. 프로덕션 수준의 애플리케이션으로 만들기 위해 파이프라인의 여러 부분에서 개선할 여지가 많이 있습니다.
💡
이 글에서 다루는 내용을 간단히 미리 살펴보면 다음과 같습니다:
소개문서 데이터셋 만들기데이터 수집전처리데이터 수집문서 생성FAISS 인덱스 생성Q&A 봇 만들기LLM을 위한 견고한 프롬프트 설계Q&A 파이프라인 만들기채팅 인터페이스 만들기사용자 인터페이스마무리 및 향후 과제
시작하기 전에, 이 repl이 어떻게 생겼는지 먼저 보여드리겠습니다:

출처: 이 봇은 다음 트윗에서 큰 영감을 받았습니다.
💡
문서 데이터셋 만들기
데이터 수집
이 작업은 처음 생각했던 것보다 훨씬 까다로웠습니다. 저는 대신에 문서를 다음에서 수집하는 우회 경로를 선택했습니다. W&B/docodile 대신 GitHub 리포지토리를 사용했습니다. 해당 리포지토리에서는 웹사이트의 트리 구조를 반영해 각 웹페이지가 하위 디렉터리로 조직된 마크다운 파일로 저장되어 있습니다. 덕분에 문서를 파싱하기도 한층 수월했습니다. 완전성을 위해 상위 자료도 추가했습니다. 포럼 질문, 지원 로테이션 티켓, 그리고 API 개발자 레퍼런스. 이 추가 데이터는 다음 구글 스프레드시트에 정리되어 있습니다 - wandb_bot 파인튜닝 데이터.
전처리
제가 한 전처리는 줄바꿈이 여러 번 연속된 부분을 제거하는 것뿐이었습니다 (예:" \n) 문서 텍스트에서 제거하고, 스프레드시트 데이터를 단일 문서로 병합했습니다. 최종 데이터는 메타데이터와 함께 JSONL 파일로 저장했습니다. source 텍스트의 출처를 나타내는 키입니다. 데이터셋이 담긴 아티팩트는 다음과 같습니다:
docs_dataset
Version overview
Full Name
parambharat/wandb_docs_bot/docs_dataset:v1
Aliases
v1
Tags
Digest
cc8d0cc556fc5f399cda1dca42ce468f
Created By
Created At
February 8th, 2023 05:58:10
Num Consumers
19
Num Files
2
Size
2.7KB
TTL Remaining
Inactive
Upstream Artifacts
Description
다음 코드를 실행하면 데이터셋 아티팩트를 가져올 수 있습니다:
PROJECT = "wandb_docs_bot"run = wandb.init(project=PROJECT)def download_raw_dataset():dataset_artifact_path = 'parambharat/wandb_docs_bot/docs_dataset:latest'artifact = run.use_artifact(dataset_artifact_path, type='dataset')artifact_path = artifact.get_path("wandb_docs.json")file = artifact_path.download()return file
데이터 수집
문서 생성
다음 단계는 검색과 검색을 위해 문서, 메타데이터, 그리고 해당 OpenAI 임베딩을 저장하는 것이었습니다. LangChain은 기본적으로 text-embedding-ada-002 문서를 임베딩하기 위한 모델입니다. 이 모델은 최대 8191토큰 길이의 문서에 대해 1536차원 임베딩을 생성합니다.
하지만 쿼리 시점에는, 검색된 문서들도 함께 전달하여 text-davinci-003 모델입니다. 이 모델의 컨텍스트 길이는 4096입니다. 따라서 텍스트를 1024자 단위로 분할하고 각 청크를 LangChain의 Document로 저장했습니다. 문서 해당 메타데이터에 원본 소스 파일을 함께 저장했습니다.
참고: 저는 CharacterTextSplitter 청크 분할에는 LangChain의 class를 사용했습니다. 사실은 …을(를) 썼어야 했습니다. TokenTextSplitter.from_tiktoken_encoder 대신
💡
이 단계에서 제가 사용한 코드는 다음과 같습니다:
import jsonfrom langchain.docstore.document import Documentfrom langchain.text_splitter import CharacterTextSplitterdef load_documents(fname):source_chunks = []splitter = CharacterTextSplitter(separator=" ", chunk_size=1024, chunk_overlap=0)for line in open(fname, "r"):line = json.loads(line)for chunk in splitter.split_text(line["reference"]):source_chunks.append(Document(page_content=chunk, metadata={"source": line["source"]}))return source_chunks
FAISS 인덱스 생성
마침내 우리는 호출할 준비가 되었습니다 OpenAI Embeddings API 엔드포인트 그리고 임베딩에 인덱싱한 문서로 저장해 조밀 벡터 검색과 검색 결과 반환에 활용합니다. 벡터를 저장하는 방법은 여러 가지가 있지만, 저는 사용했습니다 faiss-gpu pip으로 쉽게 설치해 Replit에서 실행할 수 있었기 때문입니다. 프로덕션용이라면 더 견고한 벡터 스토어 데이터베이스나 다음과 같은 서비스들을 사용하는 것이 좋습니다 Qdrant 또는 Weaviate.
LangChain을 사용해 문서 임베딩을 생성하고 저장하는 코드입니다.
from langchain.embeddings.openai import OpenAIEmbeddingsfrom langchain.vectorstores.faiss import FAISSdef create_and_save_index(documents):store = FAISS.from_documents(documents,OpenAIEmbeddings())artifact = wandb.Artifact("faiss_store", type="search_index")faiss.write_index(store.index, "docs.index")artifact.add_file("docs.index")store.index = Nonewith artifact.new_file("faiss_store.pkl", "wb") as f:pickle.dump(store, f)wandb.log_artifact(artifact, "docs_index", type="embeddings_index")return store
위 코드에서는 단일 아티팩트 안에 문서 인덱스와 임베딩을 별도의 파일로 저장합니다. 아래에서 해당 아티팩트를 확인해 보세요.
💡
faiss_store
💡
Q&A 봇 만들기
데이터가 준비되고 올바른 형식으로 정리되었으니, 이제 문서화 봇을 만들 준비가 거의 되었습니다.
기억해 두세요: 우리가 만들고자 하는 봇은 대화형 에이전트입니다. GPT-3는 문맥 내 질의응답에서 제로샷 성능이 꽤 준수한 것으로 알려졌지만, 환각을 최소화할 수 있도록 견고한 프롬프트를 설계해야 합니다. 이제 프롬프트 설계로 넘어가 보겠습니다.
LLM을 위한 견고한 프롬프트 설계
최근에는 프롬프트 설계가 예술에서 과학으로 발전해 왔지만, 나는 여전히 그것을 예술에 가깝게 다루는 편입니다. 예를 들어, 저는 영감을 얻었다 from 및 모방했다 해당 분야의 다른 프롬프트 엔지니어들에게서 영감을 받아 이 사용 사례에 잘 맞는 프롬프트를 설계했습니다. 아래는 제가 봇을 위해 최종적으로 만든 프롬프트입니다:
프롬프트는 다소 길지만, 언어 모델이 어떻게 동작하길 원하는지 정확히 규정하고, 원하는 방식으로 응답을 생성하도록 몇샷 예시를 제공합니다.
참고: LangChain 프롬프트는 사용합니다 Jinja 템플릿. {xxx} 플레이스홀더 텍스트에 사용됩니다. 프롬프트에 코드 블록이 있는 경우 이스케이프할 수 있습니다." { 이중으로 {{ .
💡
위의 프롬프트 템플릿은 다음 코드 스니펫으로 쉽게 다운로드할 수 있습니다.
def load_prompt():dataset_artifact_path = 'parambharat/wandb_docs_bot/docs_dataset:latest'artifact = run.use_artifact(dataset_artifact_path, type='dataset')artifact_path = artifact.get_path("combine_prompt.txt")file = artifact_path.download()prompt_template = (open(file, "r").read())prompt = PromptTemplate(input_variables=["question", "summaries"],template=prompt_template)return prompt
Q&A 파이프라인 만들기
쿼리 시점에 문서를 참조하는 Q&A 파이프라인을 만들기 위해, 저는 다음을 활용했습니다 VectorDBQAWithSourcesChain LangChain에서. 이름에서 알 수 있듯이, 이 체인은 벡터 스토어를 사용해 주어진 쿼리와 가장 가까운 임베딩을 먼저 조회합니다. 이렇게 검색된 문서들은 쿼리와 함께 프롬프트에 삽입된 뒤 LLM으로 전달되어 응답을 생성합니다. 이를 구현하는 코드 스니펫은 다음과 같습니다:
def load_chain(openai_api_key):if validate_openai_key(openai_api_key):vectorstore = load_vectostore()prompt = load_prompt()chain = VectorDBQAWithSourcesChain.from_chain_type(llm=OpenAI(temperature=0, openai_api_key=openai_api_key),chain_type="map_reduce",vectorstore=vectorstore,combine_prompt=prompt,)return chaindef get_answer(question, chain):if chain is not None:result = chain({"question": question,},return_only_outputs=True,)response = f"Answer:\t{result['answer']}\n\nSources:\t{result['sources']}\n"return response
채팅 인터페이스 만들기
채팅 인터페이스를 만들 때는 사용자 입력, 데이터, 그리고 모델 응답이 상태를 유지하도록 저장되는지가 중요합니다. 이렇게 해야 후속 질의가 기존 채팅의 상태를 활용할 수 있고, UI에서 사용자 질의와 모델 응답을 포함해 전체 채팅을 완전하게 렌더링할 수 있습니다. 이를 위해 채팅 시작 시 위의 Q&A 체인이 초기화되도록 보장하는 래퍼 클래스를 생성합니다. 코드는 다음과 같습니다:
class Chat:def __init__(self):self.chain = Nonedef __call__(self, message, history, openai_api_key):if self.chain is None:self.chain = load_chain(openai_api_key)history = history or []message = message.lower()response = get_answer(message, self.chain)if response is None:response = "Please enter a valid Openai API Key and try again. "history.append((message, response))return history, history
첫 호출 시 초기화된 체인을 저장할 수 있도록 클래스 를 사용하고, 채팅 상태는 history 변수에 유지한다는 점에 유의하세요.
💡
사용자 인터페이스
사용하기 Gradio 응용 프로그램을 위한 간단한 UI를 만드는 것은 놀라울 정도로 쉬웠습니다. 이 라이브러리는 심지어 Chatbot 텍스트 챗봇 인터페이스를 구현하는 클래스입니다. 사용자 질문과 OpenAI API 키를 텍스트 입력으로 받아, 이에 대한 LLM의 출력을 표시하는 매우 최소하고 단순한 인터페이스를 만들었습니다. 이를 구현하는 코드는 다음과 같습니다:
with gr.Blocks() as demo:with gr.Row():question = gr.Textbox(label='Type in your questions about wandb here and press Enter!',placeholder='How do i log images with wandb ?')openai_api_key = gr.Textbox(type='password',label="Enter your OpenAI API key here",)state = gr.State()chatbot = gr.Chatbot()question.submit(Chat(), [question, state, openai_api_key], [chatbot, state])
💡
최종 애플리케이션은 아래에서 확인할 수 있습니다:
마무리 및 향후 과제
이번 해커톤은 정말 멋지고 즐거운 기회였고, 마음껏 즐길 수 있었습니다. 기존 데이터와 리소스 위에 LLM을 활용해 흥미로운 애플리케이션을 많이 만들 수 있다는 점을 배웠습니다. 또한 임베딩과 시맨틱 검색을 사용하면 LLM의 프롬프트 길이 한계를 극복할 수 있다는 것도 이해하게 되었습니다. 비록 이번에 개발한 챗봇은 꽤 단순하고 개선의 여지가 많지만, LLM을 활용해 반복적인 작업을 자동화하고 풍부한 사용자 경험을 만들어 내는 강력한 방법이라고 생각합니다.
이 프로젝트는 LLM의 더 많은 활용 분야에 도전해 보도록 영감을 주었습니다. 현재 사이드 프로젝트로 탐구 중인 아이디어 중 하나는 장과 요약을 생성하는 것입니다. 그라디언트 디센트 LLM 임베딩과 LangChain을 활용해 에피소드를 생성하고 있습니다. 곧 보고서를 게시하고 소식 전해 드릴게요!
Add a comment
