langchain 문서 요약 정리

twonezero·2026년 2월 23일

TextSplitter

  1. RecursiveCharacterTextSplitter
  2. TokenTextSplitter

📊 RecursiveCharacterTextSplitter vs TokenTextSplitter

특징 / 동작RecursiveCharacterTextSplitter (일반)TokenTextSplitter
측정 단위문자(Character)토큰(Token)
의미적 분할✅ 예 – 단락, 문장, 단어 순으로 재귀적으로 시도❌ 아니오 – 토큰 수에 기반한 강제 분할
토크나이저 인식❌ 아니오 – 토큰 제한을 고려하지 않음✅ 예 – 토큰 수 기반 분할 (tiktoken 통해)
문맥 보존 (중첩)✅ 지원됨✅ 지원됨
분할 대체 전략시도: \\n\\n, \\n, " ", 그 다음 문자없음 – N토큰마다 고정 분할
일반적인 사용 사례토큰 이전 LLM 입력 준비, 작은 모델용 청킹토큰 제한이 엄격한 LLM (예: OpenAI, Claude)
크기 제어 정확도❌ 근사치 (문자 수만)✅ 정확함 (토크나이저 기반)
토큰 중간 분할 위험N/A – 토큰 사용하지 않음❌ 가능 – 분할이 문장 논리를 깨뜨릴 수 있음
성능⚡ 빠름 (토큰화 단계 없음)토큰화로 인해 약간 느림
청크 크기 일관성✅ 문자 길이 기반 일관적✅ 토큰 길이 기반 일관적
최적 용도대략적 분할, 짧은 텍스트, 프로토타이핑토큰 정확 분할, 프로덕션 LLM 워크플로우

✅ 요약 권장사항

상황사용할 Splitter
자연어 경계가 토큰보다 중요한 경우RecursiveCharacterTextSplitter
GPT 같은 토큰 제한 모델로 작업하는 경우TokenTextSplitter
정밀한 토큰 제어가 필요한 경우 (예: 청크당 최대 500토큰)TokenTextSplitter
LLM 토큰 제약 없는 일반 문서를 처리하는 경우RecursiveCharacterTextSplitter

PyPDFLoader

  • 각 줄 끝에 /n을 읽어들임 (문장 중간이라도)
  • 원본 문서의 포맷을 완전히 캡처하지 못함 (/n/n 없음). PDF의 경우 분할이 덜 정확함.
  • 대부분의 PDF 리더는 시각적 줄바꿈을 실제 \\n 줄바꿈으로 읽어들이며, 이는 단순한 줄 래핑 때문일지라도 실제 문장 경계가 아닐 수 있음.
  • → 더 큰 청크 크기와 중첩으로 이러한 영향을 완화할 수 있음

Map-Reduce-Chain (배치 처리)


from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 1. PDF 문서 로드
pdf_path = "파일 이름"
loader = PyPDFLoader(pdf_path, mode = "single")
doc = loader.load()
#full_text = doc[0].page_content

# 2. 의미 있는 청크로 분할
splitter = RecursiveCharacterTextSplitter(
    chunk_size=10000, # 문자 단위! ~ 2000-2500 토큰
    chunk_overlap=2000, # 10-20%
    separators=["\\n\\n", "\\n", ".", "!", "?", " "],  # 광범위 -> 좁게
)
chunks = splitter.split_documents(doc)

# 3. 배치 API용 입력 준비
inputs = [{"text": chunk.page_content} for chunk in chunks]

# 4. 청크 요약용 프롬프트 (MAP)
map_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a business anylyst and helpful assistant."),
    ("human", "Summarize the following text briefly in 3 bullet points:\\n\\n{text}\\n\\nSummary:")
])

# 5. 최종 요약용 프롬프트 (REDUCE)
reduce_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a business anylyst and helpful assistant."),
    ("human", "Summarize the following texts in a consistent final summary {reduce_style}:\\n\\n{text}\\n\\nFinal Summary:")
])

# 6. LLM
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

# 7. 파서
parser = StrOutputParser()

# 8. 두 개의 별도 체인
map_chain = map_prompt | llm | parser
reduce_chain = reduce_prompt | llm | parser

# 9. MAP 단계: 각 청크를 *병렬로* 요약
summaries = map_chain.batch(
    inputs, # 청크 목록
    config={"max_concurrency": 4},   # AI API Rate limits 고려 필요
)

# 10. REDUCE 단계: 청크 요약들로부터 최종 요약
final_summary = reduce_chain.invoke({"reduce_style":"in a very detailed, structured and comprehensive manner while dropping duplicated info",
                                     "text": "\\n\\n".join(summaries)})

# 11. 최종 출력
print("\\n📄 Summary:\\n", final_summary)

Refine-Chain (반복적 업데이트)


from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 1. PDF 문서 로드
pdf_path = "파일 이름"
loader = PyPDFLoader(pdf_path, mode = "single")
doc = loader.load()

# 2. 의미 있는 청크로 분할
splitter = RecursiveCharacterTextSplitter(
    chunk_size=20000, # 문자 단위! ~ 4000-5000 토큰
    chunk_overlap=2000, # 10-20%
    separators=["\\n\\n", "\\n", ".", "!", "?", " "],  # 광범위 -> 좁게
)
chunks = splitter.split_documents(doc)

# 3. 첫 번째 청크 프롬프트
question_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a business anylyst and helpful assistant. Do not speculate or invent facts; rely only on the provided text."),
    ("human",
     "Create an initial brief summary of the following passage. Only include information supported by the text."
     "The Summary should contain 3-5 bullet points. \\n\\n"
     "Text:\\n{text}\\n\\n"
     "Initial summary:")
])

# 4. 개선 프롬프트 (REFINE)
refine_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a business anylyst and helpful assistant. Do not speculate or invent facts; rely only on the provided text."),
    ("human",
     "We have an existing summary that must remain consistent and high-quality:\\n"
     "{existing_answer}\\n\\n"
     "Incorporate the following new passage:\\n"
     "{text}\\n\\n"
     "Update rules:\\n"
     "If this passage is irrelevant to the current summary or fully redundant, leave the summary unchanged. Otherwise:\\n"
     "1) Crtically review the entire summary and fully rewrite the summary.\\n"
     "2) Use 5-12 Topic Headers (main topics!) and keep the structure consistent.\\n"
     "3) Add only new, material information; remove duplicates and redundancy.\\n"
     "4) Remove info from the previous summary that becomes more and more niche or irrelevant in the updated total context.\\n"
     "5) If the new text contradicts or clarifies earlier statements, correct the summary.\\n"
     "6) The summary length should not execeed the length of a typical executive summary. No irrelevant details.\\n"
     "7) Prefer concise wording; replace rather than append where possible.\\n\\n"
     "Return only the refined summary (no commentary):")
])

# 5. LLM
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

# 6. 파서
parser = StrOutputParser()

# 7. 2개 체인 구성
question_chain = question_prompt | llm | parser   # 첫 번째 청크
refine_chain    = refine_prompt   | llm | parser  # 이후 청크들

# 8. 첫 번째 청크에 대한 초기 요약
current_summary = question_chain.invoke({"text": chunks[0].page_content})
intermediate_summaries = [] # 중간 요약 저장
intermediate_summaries.append(current_summary)

# 9. 반복적 개선
for chunk in chunks[1:]:
    current_summary = refine_chain.invoke({
        "existing_answer": current_summary,
        "text":            chunk.page_content
    })
    intermediate_summaries.append(current_summary)

# 10. 최종 요약 출력
print(current_summary)

요약 전략 정리

전략동작 방식사용 시기장점단점
Stuff모든 텍스트를 연결해서 하나의 프롬프트로 전송입력이 컨텍스트에 맞고 전체적 일관성이 필요할 때간단, 빠름, 최고의 전체 인식컨텍스트 제한 엄격; 병렬 처리 불가
Map-Reduce청크들을 요약("map"), 그 다음 요약들을 병합("reduce")입력이 길거나 많을 때; 청크들이 독립적으로 이해될 때대규모 말뭉치로 확장 가능; 병렬 map; 프롬프트 튜닝 가능호출/비용 증가; 일부 청크 간 뉘앙스 손실
Refine초기 요약을 만든 다음, 각 청크로 반복적으로 업데이트순차적 의존성이 중요할 때; 업데이트가 문맥/수정사항을 전달해야 할 때청크 간 강력한 일관성; 결정적 범위순차적 (map 병렬화 불가); 더 느림; 순서 민감

실용 휴리스틱스

  • 청킹: 1~2k 토큰에 10–15% 중첩이 좋은 시작점
  • 일관성: 초기 및 refine/reduce 단계에서 동일한 출력 형식 유지 (예: 고정 제목이나 고정 불렛 개수)
  • 가드레일: 간결한 표현 요청, "추가보다는 교체", 그리고 "지원되는 사실만 포함" 요청. 엄격한 구조가 필요할 때 JSON + 파서 고려
  • 병렬화: map 단계만 병렬화. Rate limit에 맞는 보수적인 max_concurrency 사용
  • 비용/시간: stuff < map_reduce < refine (일반적으로). 청크 크기 최적화, map 출력을 짧게 유지, 불필요한 재작성 피하기

경험적인 전략 시도

확실하지 않다면, 먼저 Map-Reduce 시도 (크기 + 속도에 좋은 기본값).
중요한 교차 섹션 의존성이 놓치고 있다면 Refine로 전환.
문서가 작다면 Stuff로 간단하게 유지.

profile
I Enjoy Learn-and-Run Vibe😊

0개의 댓글