# 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. **다국어**: 각 언어별 최적 프롬프트 구조는?