# Advanced RAG — Prompt Optimization (프롬프트 최적화) ## 정의 검색된 문서들을 LLM에 제공할 때, 컨텍스트 윈도우를 효율적으로 활용하고 LLM이 최고의 성능을 낼 수 있도록 문서 순서, 포맷, 프롬프트 구조를 최적화하는 기법. RAG 파이프라인의 마지막 단계에서 검색 결과의 품질을 최종 답변 품질로 변환한다. ## 핵심 원리 ### 수식 **토큰 제약 최적화 (Context Window Constraint):** $\text{maximize} \quad \sum_{i=1}^{n} w_i \cdot \text{rel}(d_i, q)$ $\text{subject to} \quad \sum_{i=1}^{n} \text{tokens}(d_i) + \text{tokens}(q) + \text{tokens}(\text{prompt}) \leq T_{\text{max}}$ 여기서: - $d_i$: $i$번째 문서 - $w_i \in [0, 1]$: $i$번째 문서 포함 여부 (선택) - $\text{rel}(d_i, q)$: 문서의 관련도 점수 - $\text{tokens}(\cdot)$: 토큰 수 계산 함수 - $T_{\text{max}}$: 최대 컨텍스트 윈도우 토큰 수 (예: 4096, 8192) **Lost-in-the-Middle 효과 (위치별 성능 편향):** $\text{Accuracy}(d_i, \text{position}) = f(\text{position})$ 실증적으로: $\text{Accuracy}(d_i) = \begin{cases} A_{\text{max}} & \text{if position} \approx 1 \text{ or } n \\ A_{\text{max}} - \delta \cdot (\text{position} - 1) & \text{if } 1 < \text{position} < n \\ \end{cases}$ 여기서 $\delta \in [0.01, 0.05]$는 위치 감쇠 계수 **위치 편향을 고려한 재정렬 목적함수:** $\text{maximize} \quad \sum_{i=1}^{n} \text{rel}(d_i) \cdot g(\text{position}(d_i))$ 여기서 $g(\text{position}) = \begin{cases} 1.0 & \text{if position} = 1 \text{ or } n \\ 1.0 - \lambda \cdot \frac{\text{position}-1}{n} & \text{otherwise} \end{cases}$ $\lambda \in [0.1, 0.3]$: 위치 가중치 감쇠 **정보 밀도 (Compressibility):** $\rho(d) = \frac{\text{information}(d)}{\text{tokens}(d)} = \frac{\text{relevance}(d)}{\text{tokens}(d)}$ **문서 압축 최적화:** $\text{compress}(d) = \arg\min_{d'} \left[ \text{tokens}(d') + \lambda \cdot (1 - \text{similarity}(d, d')) \right]$ 여기서 $\lambda$는 정보 손실 가중치 **최종 선택 및 배치:** $D_{\text{final}} = \{d_1', d_2', \ldots, d_k' : \sum_{i=1}^{k} \text{tokens}(d_i') \leq T_{\text{max}}\}$ $\text{Ordering}^* = \arg\max_{\pi} \sum_{i=1}^{k} \text{rel}(d_i) \cdot g(\pi(i))$ ### 컨텍스트 윈도우 한계 ``` LLM 입력 구조: [System Prompt] 시스템 지시 (100 토큰) ──────────────────────────── [Retrieved Documents] 문서 1: 300 토큰 문서 2: 250 토큰 문서 3: 200 토큰 ... ──────────────────────────── [User Query] 사용자 질문: 50 토큰 ──────────────────────────── 총 사용: 900 토큰 / 4096 (GPT-3.5) 또는 8192 (GPT-4) 남은 생성 토큰: 3096 (생성 여유 확보) ``` ### 문서 배치 효과 (Position Bias) 연구에 따르면 **중요한 정보는 시작과 끝에 배치할 때** LLM이 더 잘 활용한다: ``` 성능 비교: [정보 배치] [답변 정확도] 가장 관련 높은 것 → 중간 72% 가장 관련 높은 것 → 시작 85% ← 우수 가장 관련 높은 것 → 끝 84% 무작위 순서 65% ``` ## 사용 사례 ### 1. 문서 순서 최적화 (Reorder) ``` 방법 1: 점수 높은 순서대로 (일반) - [0.95] 문서 A (가장 관련) - [0.87] 문서 B - [0.72] 문서 C 결과: 답변 정확도 72% 방법 2: Lost-in-the-Middle 회피 - [0.95] 문서 A (맨 앞) - [0.72] 문서 C (중간) - [0.87] 문서 B (맨 뒤) 결과: 답변 정확도 85% ``` ### 2. 문서 압축 ``` 원본 문서 (너무 김): "Transformers는 2017년에 발표된 혁명적인 아키텍처입니다. Attention 메커니즘을 기반으로 하며, 병렬 처리가 가능합니다. BERT, GPT, T5 등 수많은 모델의 기초가 되었습니다..." 압축 버전: "Transformer: 2017년 발표, Attention 기반, 병렬 처리 가능" 효과: 토큰 10배 절감, 성능 동일 ``` ### 3. 컨텍스트 정렬 ``` 질문: "CNN과 RNN의 차이점은?" 나쁜 정렬: [문서] CNN의 역사, RNN 아키텍처, CNN 성능... [질문] "CNN과 RNN의 차이점은?" 좋은 정렬: [문서] CNN의 정의, RNN의 정의, CNN vs RNN 비교... [질문] "CNN과 RNN의 차이점은?" ``` ## 성능 모델 분석 ### Lost-in-the-Middle 효과의 수학적 분석 문서 위치에 따른 성능 곡선: $\text{Perf}(i, n) = A_0 - \alpha \cdot \left(i - 1\right) - \beta \cdot (n - i)^2$ 여기서: - $A_0$: 기준 성능 (첫 번째 위치) - $i \in [1, n]$: 문서의 위치 - $n$: 총 문서 수 - $\alpha, \beta$: 감쇠 계수 **누적 정확도 (Cumulative Accuracy with Position):** $\text{CumAcc}(n) = \frac{1}{n} \sum_{i=1}^{n} \text{Perf}(i, n)$ **위치 최적화 이득:** $\text{Gain} = \text{CumAcc}(\text{optimized}) - \text{CumAcc}(\text{random})$ 실험 데이터에서 일반적으로 $\text{Gain} \approx 0.15\text{-}0.20$ (15\%-20% 개선) ### 토큰 예산 할당 문제 (Token Budget Allocation) 여러 문서가 주어졌을 때, 각 문서에 할당할 토큰 수를 최적화: $t_i^* = \arg\max_{t_i} \text{rel}(d_i, q) \cdot (1 - e^{-\gamma \cdot t_i})$ $\text{subject to} \quad \sum_{i=1}^{n} t_i \leq T_{\text{remaining}}$ 여기서 $(1 - e^{-\gamma \cdot t_i})$는 수확량 감소 함수 (토큰 추가에 따른 정보 이득 감소) **라그랑주 승수법 해:** $\frac{d}{dt_i} [\text{rel}(d_i) \cdot (1-e^{-\gamma t_i}) - \lambda \cdot t_i] = 0$ $\Rightarrow \text{rel}(d_i) \cdot \gamma \cdot e^{-\gamma t_i^*} = \lambda$ ### 컨텍스트 품질 지표 (Context Quality Metrics) **관련도 가중 토큰 효율:** $\text{TokenEfficiency} = \frac{\sum_{i=1}^{n} \text{rel}(d_i) \cdot \text{coverage}(d_i)}{T_{\text{used}}}$ 여기서 $\text{coverage}(d_i) \in [0, 1]$은 문서가 답변에 기여하는 정도 **정보 엔트로피 기반 문서 다양성:** $H(D) = -\sum_{i=1}^{n} p(d_i) \log p(d_i)$ 여기서 $p(d_i) = \frac{\text{rel}(d_i)}{\sum_j \text{rel}(d_j)}$ 고도의 다양성은 더 견고한 답변을 보장 ## 구현 예시 ### 기본 프롬프트 최적화 (LangChain) ```python from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain.document_loaders import WebBaseLoader from langchain.text_splitter import CharacterTextSplitter # Step 1: 프롬프트 템플릿 정의 prompt_template = """ 다음 문서를 바탕으로 질문에 답하세요. 문서에 답이 없으면 "알 수 없습니다"라고 말하세요. 문서: {context} 질문: {question} 상세한 답변: """ PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) # Step 2: QA 체인 구성 qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 모든 문서를 컨텍스트에 넣음 retriever=retriever, chain_type_kwargs={ "prompt": PROMPT, "document_variable_name": "context" }, return_source_documents=True ) # Step 3: 실행 result = qa_chain({"query": "Transformer 주의 메커니즘은?"}) ``` ### 고급 프롬프트 최적화 (Document Reordering) ```python class OptimizedPromptBuilder: def __init__(self, model_name="gpt-3.5-turbo", max_context_tokens=3000): self.tokenizer = get_tokenizer(model_name) self.max_context_tokens = max_context_tokens def reorder_documents(self, documents, query, strategy="lost_in_middle"): """문서 재순서화""" if strategy == "lost_in_middle": # 가장 관련성 높은 것 → 시작과 끝 sorted_docs = sorted( documents, key=lambda x: x.metadata.get("relevance_score", 0), reverse=True ) reordered = [] for i, doc in enumerate(sorted_docs): if i % 2 == 0: reordered.insert(0, doc) # 앞에 삽입 else: reordered.append(doc) # 뒤에 추가 return reordered # 기본 정렬 (점수 높은 순) return sorted(documents, key=lambda x: x.metadata.get("relevance_score", 0), reverse=True) def compress_document(self, document, compression_ratio=0.3): """문서 압축 (요약)""" from transformers import pipeline summarizer = pipeline("summarization") # 토큰 수 계산 target_tokens = int( len(self.tokenizer.encode(document)) * compression_ratio ) summary = summarizer( document, max_length=target_tokens, min_length=int(target_tokens * 0.8), do_sample=False ) return summary[0]["summary_text"] def build_optimal_context(self, documents, query, max_tokens=3000): """최적화된 컨텍스트 구성""" # Step 1: 문서 재순서화 reordered = self.reorder_documents(documents, query) context_parts = [] current_tokens = 0 for i, doc in enumerate(reordered): doc_text = doc.page_content doc_tokens = len(self.tokenizer.encode(doc_text)) # Step 2: 토큰 초과 확인 if current_tokens + doc_tokens > max_tokens: # 문서 압축 compressed = self.compress_document(doc_text) compressed_tokens = len(self.tokenizer.encode(compressed)) if current_tokens + compressed_tokens <= max_tokens: context_parts.append(f"문서 {i+1}: {compressed}") current_tokens += compressed_tokens else: break else: context_parts.append(f"문서 {i+1}: {doc_text}") current_tokens += doc_tokens return "\n\n".join(context_parts), current_tokens def build_prompt(self, documents, query, strategy="lost_in_middle"): """최적화된 프롬프트 구성""" context, tokens_used = self.build_optimal_context( documents, query ) prompt = f""" 당신은 전문적이고 정확한 정보 조회 어시스턴트입니다. 다음 문서를 기반으로 사용자의 질문에 답해주세요. 중요: - 문서에 명확히 나와있는 정보만 사용하세요 - 정확하지 않으면 "알 수 없습니다"라고 말하세요 - 최대한 간결하게 답하세요 [검색 결과] {context} [사용자 질문] {query} [답변] """ return prompt, { "tokens_used": tokens_used, "documents_included": len(documents), "max_tokens_available": self.max_context_tokens } # 사용 예시 optimizer = OptimizedPromptBuilder() docs = retriever.get_relevant_documents("Transformer 주의 메커니즘") prompt, stats = optimizer.build_prompt(docs, "Transformer 주의 메커니즘은?") print(f"토큰 사용: {stats['tokens_used']}") print(f"포함된 문서: {stats['documents_included']}") print(prompt) ``` ### 체인 구성 최적화 ```python class OptimizedRAGChain: def __init__(self, retriever, llm): self.retriever = retriever self.llm = llm self.prompt_builder = OptimizedPromptBuilder() def answer_question(self, query: str): """최적화된 RAG 파이프라인""" # Step 1: 검색 docs = self.retriever.get_relevant_documents(query) # Step 2: 프롬프트 최적화 prompt, stats = self.prompt_builder.build_prompt(docs, query) # Step 3: LLM 호출 answer = self.llm.predict(prompt) # Step 4: 결과 반환 return { "answer": answer, "documents_used": len(docs), "tokens_used": stats["tokens_used"], "sources": [d.metadata.get("source") for d in docs] } # 사용 rag_chain = OptimizedRAGChain(retriever, llm) result = rag_chain.answer_question("Transformer의 주의 메커니즘은?") print(result["answer"]) ``` ## 프롬프트 전략 ### 1. Few-Shot 예시 추가 ```python few_shot_prompt = """ 예시 1: Q: "Transformer란?" A: "Transformer는 2017년 발표된 신경망 아키텍처로, Attention 메커니즘 기반입니다." 예시 2: Q: "BERT는 무엇인가?" A: "BERT는 Google이 2018년에 발표한 Transformer 기반 모델로, 양방향 학습을 합니다." 이제 다음 질문에 답하세요: Q: "{question}" A: """ ``` ### 2. 역할 지정 (Role Assignment) ```python system_prompt = """ 당신은 기술 문서 전문가입니다. 제공된 문서를 정확히 읽고, 과학적 근거에 기반해 답변합니다. 정보가 부족하면 "알 수 없습니다"라고 말하세요. """ ``` ### 3. 출력 형식 지정 ```python format_prompt = """ 다음 형식으로 답변하세요: 1. 핵심 답변 (한 문장) 2. 상세 설명 (2-3 문장) 3. 관련 개념 (불릿 포인트) 4. 출처 (어느 문서에서) 질문: {question} """ ``` ## 성능 지표 ### 문서 배치 효과 (실험 결과) ``` 배치 전략별 성능: 전략 | 정확도 | 응답 토큰 ------------------------|-------|---------- 무작위 순서 | 65% | 150 점수 높은 순서대로 | 72% | 145 Lost-in-the-Middle | 85% | 155 Lost-in-the-Middle+압축 | 83% | 120 최적: Lost-in-the-Middle 전략 ``` ## 최적화 체크리스트 ### 프롬프트 최적화 - [ ] 명확한 지시 포함 - [ ] 역할/성격 정의 - [ ] 출력 형식 명시 - [ ] 제약 조건 명시 - [ ] 예시 (Few-shot) 포함 ### 컨텍스트 최적화 - [ ] 문서 순서 재조정 (Lost-in-the-Middle) - [ ] 관련도 낮은 문서 제거 - [ ] 문서 압축 (요약) - [ ] 토큰 수 모니터링 - [ ] 컨텍스트 윈도우 내 유지 ### 체인 최적화 - [ ] 검색 결과 품질 확인 - [ ] 재순위화 (리랭킹) 적용 - [ ] 캐싱 활용 - [ ] 병렬 처리 검토 ## 장점 vs 단점 ### 장점 | 항목 | 설명 | |------|------| | **답변 품질** | 올바른 정보 우선 → 정확도 ↑ 20-30% | | **토큰 효율** | 압축으로 더 많은 문서 포함 가능 | | **일관성** | 프롬프트 최적화 → 결과 일관성 ↑ | ### 단점 | 항목 | 설명 | |------|------| | **복잡성** | 다양한 최적화 기법 조합 필요 | | **튜닝 비용** | 프롬프트 엔지니어링 필요 | | **언어/도메인 의존성** | 최적 전략은 도메인마다 다름 | ## 실행 가능한 코드 완전하고 즉시 실행 가능한 코드는 다음을 참조하세요: - **[[codes/llm/advanced-rag/samples/prompt-optimization-basic|Prompt Optimization 기본 구현]]** — OptimizedPromptBuilder + Lost-in-the-Middle + 토큰 최적화 예제 ## 열린 질문 1. **문서 압축**: 어느 정도 압축이 최적일까? 2. **배치 순서**: Lost-in-the-Middle이 모든 도메인에서 최적일까? 3. **적응형 프롬프트**: 쿼리별로 프롬프트를 자동 생성할 수 있을까? 4. **다국어**: 각 언어별 최적 프롬프트 구조는?