LangChain Meetup - R.A.G 우리가 절대 쉽게 결과물을 얻을 수 없는 이유
R.A.G 우리가 절대 쉽게 결과물을 얻을 수 없는 이유
https://youtu.be/NfQrRQmDrcc?si=kWmsM0cfv02ddpak
RAG을 위한 문서 전처리 방법...
- Document Load
- Split
- Embedding
- Vector Store
- Retriever
각각의 과정마다 선택할 수 있는 종류가 너무 많음. 이것들을 조합하면서 경험했던 내용을 공유하는 발표.
Document Loader
다양한 종류의 데이터를 지원하지만 보통 csv, Excel, PDF.
- Langchain은 load()를 인터페이스화해서, 어떤 document loader 객체라도 파일 로드할 때 load()함수 쓰면 되도록 했음
고려해야 했던 점들
- 데이터 원형 그대로 잘 가져오는가?
- 한글 인코딩 / 특수문자 같은 거
- 어떤 metadata를 제공하는가? -> 본문 외적의 내용도 가져올 수 있는가.
- page_content: 문서내용
- page: 페이지 번호
- 표, 차트, 문서의 coordinate (element의 좌표 - 위치 정보), 속성값 (title, table, image, text) 태깅해주는지
- 문서 읽는 속도는?
- 속도가 느리면, 많은 문서를 DB에 쌓는 데 오래 걸린다.
- i.e. 실시간으로 읽어서 요약만 할 거라면, 읽는 속도가 중요함. batch로 넣어두고 나중에 출처표기하는 등의 기능이 필요하다면, 속도보다는 Metadata 지원 여부가 중요하다.
fitz
PDF loader. 단순하게 텍스트만 읽어서 합치기 위한 용도
- 속도는 가장 빠름, 단 간혹 문장이 잘려서 읽히는 경우가 있음
- metadata는 page 번호 말고 지원하지 않음.
- 업로드해서 실시간으로 요약본 받아봐야 하는 형태의 작업에 유용함
PyPDFLoader
- 평균적으로 우수한 편. 한글인코딩 문제 없고, 속도 적당하고, metadata도 제공해준다.
- page단위로 데이터 로드.
- metadata로는 source: 파일명, page: 페이지 번호. 두 가지 제공
UnstructuredPDFLoader
- 가장 많은 종류의 metadata 제공
- component별 load 기능을 제공한다.
- title, 표, 이미지 등... 의 속성값.
- coordinates 좌표값 / layout 정보 제공
- 그래서 속도 느림
PDFPlumber
- 한글인코딩 잘하고, 다양한 metadata 포함
- total_page를 제공함. 안주는 경우도 있어서 이거 있으면 편함
- ModDate: 수정 날짜도 제공함.
- metadata 제공하는 측면에서는 훌륭함.
- 단, 속도 느림.
Text Splitter
예시 문장에서 'Retrieval Augmented Generation' 이라는 문장이 소제목이라고 한다면, 이 문장은 분할 없이 원문 그대로 가져오는 게 굉장히 중요하다.
- 이 소제목과 관련된 내용이 하위에 계속 이어지기 때문.
- 이걸 text Splitter가 어떻게 가져오는지 체크해보자.
CharacterTextSplitter
분할 가능한 최소 단위로 분할 시도한다. 공백 or '.' 단위.
- 예시의 경우 문장 단위로 분할. 글자가 누락되는 경우는 없지만, 문장이 완성되지 못하고 잘리는 경우가 더러 있다.
- 소제목을 원문 그대로 들고오지 못하고 잘라낸 것을 볼 수 있음.
- chunk_overlap 옵션으로도 완벽히 해결할 수 있는 문제가 아니다.
- 그래서 굳이 권장하지 않음.
RecursiveCharacterTextSplitter
- chunk가 충분히 작아질 때까지 재귀적으로 분할 시도.
- 분할 시도 순서: 단락 -> 문장 -> 단어.
- 그래서 소제목이 끊기지 않고 가져오는 효과가 있음. (단락으로 인식해서 살려둔 것)
- 텍스트에서 context가 유지되는 순서대로 분할을 시도하므로 (단락 -> 문장 -> 단어), 범용적으로 쓸만함.
TokenTextSplitter
한글은 글자 단위로 토큰이 되지 않는 문자이므로, 한글깨짐 현상 발생
- KonlpyTextSplitter를 쓰면 한글깨짐 문제는 해결됨. (Kkma 형태소 분석기 기반)
- text splitting 시간이 오래 걸리는 편.
- 정보 정확성이 우선시되고, 언어분석이 필요한 애플리케이션의 경우 쓸만함.
기타 오픈소스 (HuggingFace)
다양한 tokenizer 사용 가능. 신조어 or 특정 도메인 용어를 fine tuning하거나 add_vocab할 수 있다.
- BPE가 요즘 많이 회자되는 편.
SemanticChunker (experimental)
langchain Experimental에 새로 추가된 chunker.
- 텍스트를 의미 유사도에 따라 분할했음.
- chunk_size, chunk_overlap 같은 파라미터를 받지 않는다.
- Embedding을 지정해줘야 한다.
의미 파악해서, 비슷한 의미끼리 묶어서 분할하는 방식... 분할 결과물의 text size를 통제할 수가 없다. 길이가 제각각임
Embedding
Vector Space에서 '가장 유사한 텍스트를 찾아내는' Semantic Search 작업을 수행할 수 있음.
- 문서에 적합한 embedding model + 한글처리까지 잘 되는 걸 고민해야 함.
Embedding의 실행 시점은 두 곳. 둘 다 같은 Embedding model을 써야 semantic search에 문제가 없다.
- Document: PDF 같은 문서를 사전에 embedding + DB에 저장
- Query: 사용자의 입력값을 변환
OpenAIEmbedding 함수를 별다른 옵션 없이 default로 생성할 경우, default로 설정된 모델이 버전업되면서 바뀔 수 있다.
- 이 경우, DB에 저장할 때 사용한 모델과 사용자의 입력값을 변환할 때 사용한 모델이 달라질 수 있다.
OpenAIEmbedding
많이들 쓰는 기본모델이자 유료 Embedding. api로 embedding하는 거라 인프라 부담이 없으며, 한글성능도 괜찮음.
- 가격이 비싸다고는 볼 수 없지만, 호출할 때마다 돈 나가는 건 생각해야 함.
CacheBackedEmbeddings
호출하는 족족 돈이 나가는 OpenAIEmbedding 방식과 달리, embedding을 임시로 캐싱해서 다시 계산할 필요가 없도록 한 것.
- 한 번이라도 임베딩했다면, 동일한 문서에 한해서는 embedding을 수행하지 않는다.
- 로컬에 캐시파일 생성하고, 캐시 hit할 경우 캐시결과를 사용하는 식.
- 로컬캐시 결과물이 많을 경우 조회할 때도 3~4초까지 소요되는 경우가 있음 (0.00초 수렴한다고 보장할 수는 없다)
- OpenAIEmbedding + CacheBackedEmbedding을 쓸 경우, 동일한 문서를 여러 사람이 조회한다 해도 추가비용이 나가지 않는다
- namespace 지정하면, 모델별로 cache 관리하는 것도 가능하다.
MTEB (Massive Text Embedding Benchmark)
다양한 오픈소스 모델의 benchmark 점수를 비교할 수 있는 지표.
- 단, 한국어 쓸 거면 이 벤치마크 점수를 곧이곧대로 믿을 수는 없다. semantic search 해보면서 결과 비교해봐야 함.
- 오픈소스 Embedding 모델 써서, 인프라 직접 구축하거나.
- HuggingFace Inference API 가 있긴 한데 속도가 느림
- OpenAIEmbedding 사용하거나.
- 인프라 구축할 여력이 없고
- 한글 정확도를 오픈소스 모델 테스트할 여력이 없을 때
- 단, cache 반드시 적용해서 불필요한 과금 피해야 함
Vector Store
langchain에서 지원하는 종류만 80여 개. 선택 기준도 상황에 따라 다름.
- 로컬 vs 클라우드?
- 데이터 조회 / 업데이트 빈도?
등등..
FAISS
Meta에서 주도적으로 개발하는 오픈소스. 로컬 DB 세팅한다면 안정성 면에서 가장 나은 선택지
전처리
풀기 아까운 노하우지만, 공유하면 커뮤니티가 발전한다는 생각에 공유함.
페이지 단위 분할
전체 문서를 페이지 단위로 분할할 때, 필요한 metadata 정보는 태깅한다.
- 페이지 번호, 파일명: LLM응답의 신뢰도 확보
- MOD: 최신 정보인지 체크할 용도. metadata로 필터링할 때 유용함
- Author: 누가 썼는지
- 각 문서에서 키워드 추출해서 tagging할 수 있다면 더 좋다.
필요한 영역만 사용한다.
- PDF의 header나 footer의 정보가 vector 검색 시 포함되는 경우가 있음.
- 예컨대 PDF footer의
Licensed to: <사람 이름> ... (London)
항목의 경우, 문서의 'London 시장 이슈' 검색했을 때 포함되는 경우가 있다. London 문자열에 포함되어 있기 때문.
- 예컨대 PDF footer의
PDF 문서 구성방식에 따른 영향
- 일반적인 pdf 파싱 방식은 1 row. 문서가 1:1 분할 또는 1:2 분할 구성으로 되어 있다면, 분리된 열을 건너뛰기 때문에 문장 순서가 전부 꼬인다.
PDFPlumber Bounding Box 기능을 활용한다.
- page에 crop 수행
- 왼쪽 먼저 다 읽고, 오른쪽 문서를 읽은 뒤 합친다.
PDFminer: 자동으로 인식해서 해준다
- 단, 차트나 표 안의 텍스트를 '표 / 차트' 구성항목이 아니라 그냥 텍스트로 인식할 수 있음.
- 사전에 표 / 그래프를 제거하거나
- 한 줄에 글자수 N개 이하일 경우 무시하는 식으로 작업
표 추출
- 오픈소스: camelot, PaddleOCR. 관련 표를 추출해주는 기능
- PDFPlumber의 표 추출 기능 활용.
- table_setting의 여러 옵션값을 활용하면, 적절한 표를 추출할 수 있음.
Chunk Overlap?
문서마다 다양해서 정답은 없지만, 기본적으로는 "여유있게 설정한다"
- 이전 page의 마지막 부분 / 다음 page의 앞부분은 병합 처리한 뒤 Semantic Chunker (의미단위 분할)를 사용할 수도 있음.
- 단, 이 경우 page번호 출처를 어디로 할 것인지가 애매함
- 한 문서에 chunk size나 overlap을 여러 개 적용헤서 retriever 여러 개 만든 뒤 ensemble. 여러 retrieval 결과를 Union 처리한다.
이미지 추출
fitz 사용. 이미지 추출하거나 태깅이 필수인 경우를 생각해본다면...
- 커머스 관련 상품 (상품이미지)
- 그래프 / 차트가 이미지로 삽입된 형태
Retriever
Vector Store에 query -> 결과물 받아오는 것. 이것도 종류가 여러 가지 있음.
Multi-Query Retriever
Query 문구가 미묘하게 다른 경우라거나, Embedding이 텍스트의 의미를 제대로 파악하지 못하는 경우
- 이건 사실 사용자의 검색 패턴과 관련이 있음. 키워드 기반 검색에 익숙해져 있는데, 이건 semantic search 방식과 부합하지 않는다.
- LLM으로 사용자의 Input을 paraphrase -> 비슷한 여러 쿼리를 생성해서, 결과의 Union 값을 반환한다.
Ensemble Retriever
특정 단어가 정확히 포함된 데이터만 검색해야 할 경우에도 Semantic Search는 성능이 좋지 않다.
- 예컨대 '비타민A 영양제 추천해줘' -> '비타민' 이라는 단어와 유사도 높은 비타민C, 비타민D 등도 같이 검색됨.
일반적으로
- 키워드 기반 검색이 필요하면... Sparse Retriever. 코드상으로는 BM25Retriever
- 의미 검색이 필요하면... Dense Retriever. 코드상으로는 faiss_retriever
두 가지를 Ensemble해서 쓰는 게 Ensemble Retriever. 어느 정도 절충된 결과를 받을 수 있다.
- 가중치로 weight 설정 가능.
Long Context Retriever
보통 RAG로 정보를 가져온 다음, LLM에 유사도 높은 순서대로 정렬해서 포함한다.
- 그런데, LLM은 중간 부분의 context를 무시하는 경향이 있다. context 앞쪽, 뒤쪽 문서를 응답할 때 주요 문서로 확인하는 식으로 동작함.
- 그러면 LLM에 데이터 넣는 서순을 바꿔보자. 매우 중요한 것은 최상단, 그 다음으로 중요한 건 끝부분에 배치하는 식.
이걸 구현한 Langchain 객체가 LongContextReorder().
그 외에도
- Ensemble에 필요한 Multi Vector Retriever
- ContextualCompressos
- LLMChainFilter
등과 같은 Retriever 구현체가 있음.
Prompt Engineering
Few-shot 방식을 권장. 명령하는 것보다 예시를 제공하는 게 효과적임.
- 문자열로 입력하면 지저분하므로, yaml 등 별도의 파일로 빼서 관리하는 걸 추천한다.
- langsmith hub에 prompt 업로드 + pull해서 사용할 수 있다. 이 경우 버전관리도 가능.
langchainHub에 있는 완성형 prompt 사용해도 좋다.
요약의 경우... 워낙 초반부터 니즈가 많았던 task라서 많은 방법론이 공개돼 있다.
- Chain Of Density: 5번의 퇴고를 거쳐서 요약본을 수정해가는 것.
- 1.Sparse summary 작성 (간략한 형태의 요약)
- 2.Missing Entity (누락된 키워드 찾기)
- 3.Missing Entity 포함해서 다시 요악
- 위 step을 5번 반복한다.
- 중요한데 누락되어 있던 키워드를 추가해가며 Denser Summary로 만드는 것.
왜 굳이 5번?
- 논문에서 '5회 수행하면 인간의 summary보다 낫더라'고 밝혔음.