Skip to main content

생성 (Generation)

생성은 RAG 파이프라인의 마지막 단계로, 검색된 문서를 컨텍스트로 활용하여 LLM이 답변을 만드는 과정입니다. 프롬프트 설계와 컨텍스트 관리가 답변 품질을 결정합니다.

생성 파이프라인

프롬프트 구성

RAG 프롬프트는 세 가지 요소로 구성됩니다: 시스템 프롬프트, 컨텍스트, 사용자 질문.

기본 프롬프트

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", """당신은 주어진 컨텍스트를 기반으로 질문에 답변하는 AI 어시스턴트입니다.

규칙:
- 컨텍스트에 포함된 정보만 사용하여 답변하세요
- 컨텍스트에 답변이 없으면 "제공된 문서에서 관련 정보를 찾을 수 없습니다"라고 답변하세요
- 답변에 출처를 명시하세요

컨텍스트:
{context}"""),
    ("human", "{question}"),
])

출처 인용 프롬프트

prompt_with_citation = ChatPromptTemplate.from_messages([
    ("system", """주어진 컨텍스트를 참고하여 질문에 답변하세요.

규칙:
- 각 주장 뒤에 [출처: 문서명] 형식으로 출처를 표기하세요
- 여러 문서의 정보를 종합할 경우 모든 출처를 명시하세요
- 컨텍스트에 없는 정보는 사용하지 마세요

컨텍스트:
{context}"""),
    ("human", "{question}"),
])

구조화된 답변 프롬프트

from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List

class RAGAnswer(BaseModel):
    answer: str = Field(description="질문에 대한 답변")
    sources: List[str] = Field(description="답변에 사용된 출처 목록")
    confidence: str = Field(description="답변 신뢰도: high, medium, low")

parser = PydanticOutputParser(pydantic_object=RAGAnswer)

prompt_structured = ChatPromptTemplate.from_messages([
    ("system", """컨텍스트를 기반으로 질문에 답변하세요.

{format_instructions}

컨텍스트:
{context}"""),
    ("human", "{question}"),
]).partial(format_instructions=parser.get_format_instructions())

컨텍스트 윈도우 관리

LLM의 컨텍스트 윈도우 크기는 제한적입니다. 검색된 문서를 효율적으로 구성해야 합니다.

컨텍스트 크기 계산

모델컨텍스트 윈도우권장 컨텍스트 비율
GPT-4o-mini128K tokens60~70%
GPT-4o128K tokens60~70%
Claude 3.5 Sonnet200K tokens60~70%
Llama 3.1 8B128K tokens50~60%
import tiktoken

def count_tokens(text: str, model: str = "gpt-4o-mini") -> int:
    """텍스트의 토큰 수를 계산합니다."""
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

def build_context(documents, max_tokens: int = 3000, model: str = "gpt-4o-mini"):
    """토큰 제한 내에서 컨텍스트를 구성합니다."""
    context_parts = []
    total_tokens = 0

    for i, doc in enumerate(documents):
        doc_text = f"[문서 {i+1}] {doc.page_content}"
        doc_tokens = count_tokens(doc_text, model)

        if total_tokens + doc_tokens > max_tokens:
            break

        context_parts.append(doc_text)
        total_tokens += doc_tokens

    return "\n\n".join(context_parts)

컨텍스트 압축

검색된 문서에서 질문과 관련된 부분만 추출하여 컨텍스트를 압축합니다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain.chat_models import init_chat_model

llm = init_chat_model("gpt-4o-mini", temperature=0)
compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=retriever,
)

# 압축된 결과 검색
compressed_docs = compression_retriever.invoke("RAG의 생성 단계란?")

출처 인용 (Citation)

사용자가 답변의 근거를 확인할 수 있도록 출처를 명시합니다.
def format_context_with_sources(documents):
    """출처 정보가 포함된 컨텍스트를 구성합니다."""
    formatted = []
    for i, doc in enumerate(documents, 1):
        source = doc.metadata.get("source", "unknown")
        page = doc.metadata.get("page", "")
        source_info = f"{source}" + (f", p.{page}" if page else "")
        formatted.append(f"[{i}] (출처: {source_info})\n{doc.page_content}")
    return "\n\n---\n\n".join(formatted)

def generate_with_citations(question, documents, llm):
    """출처가 포함된 답변을 생성합니다."""
    context = format_context_with_sources(documents)

    prompt = ChatPromptTemplate.from_messages([
        ("system", """컨텍스트의 각 문서에는 번호가 부여되어 있습니다.
답변 시 해당 정보의 출처를 [1], [2] 형식으로 인라인 인용하세요.

컨텍스트:
{context}"""),
        ("human", "{question}"),
    ])

    chain = prompt | llm | StrOutputParser()
    return chain.invoke({"context": context, "question": question})

스트리밍 생성

긴 답변을 생성할 때 스트리밍으로 실시간 출력합니다.
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser

llm = init_chat_model("gpt-4o-mini", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    ("system", "컨텍스트를 참고하여 답변하세요.\n\n{context}"),
    ("human", "{question}"),
])

chain = prompt | llm | StrOutputParser()

# 스트리밍 출력
for chunk in chain.stream({"context": context, "question": question}):
    print(chunk, end="", flush=True)

LangGraph 생성 노드

from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = init_chat_model("gpt-4o-mini", temperature=0)

def generate(state: GraphState) -> GraphState:
    """검색된 문서를 기반으로 답변을 생성합니다."""
    question = state["question"]
    documents = state["documents"]

    # 컨텍스트 구성
    context = "\n\n".join(
        f"[{i+1}] {doc.page_content}"
        for i, doc in enumerate(documents)
    )

    prompt = ChatPromptTemplate.from_messages([
        ("system", """다음 컨텍스트를 참고하여 질문에 답변하세요.
컨텍스트에 없는 내용은 답변하지 마세요.

컨텍스트:
{context}"""),
        ("human", "{question}"),
    ])

    chain = prompt | llm | StrOutputParser()
    generation = chain.invoke({"context": context, "question": question})
    return {"generation": generation}

생성 품질 체크리스트

시스템 프롬프트에서 “컨텍스트에 없는 정보는 사용하지 말 것”을 명시하세요. temperature를 0에 가깝게 설정하면 환각을 줄일 수 있습니다.
사용자가 답변의 근거를 확인할 수 있도록 인라인 인용이나 출처 목록을 포함하세요.
불필요한 내용은 제거하고, 질문과 관련된 핵심 정보만 컨텍스트에 포함하세요.
검색 결과가 부족하거나 질문과 무관할 때, “알 수 없습니다”라고 정직하게 답변하도록 설계하세요.