증강 질의(Augmented Query)의 이해

LLM 기반의 서비스를 개발하려면 단순한 프롬프트 작성 능력을 넘어, 모델의 작동 원리를 깊이 이해하고 외부 데이터를 효과적으로 활용하는 '증강 질의' 개념을 숙지해야 합니다.
증강 질의는 LLM이 단순히 학습된 지식을 넘어, 실시간으로 필요한 정보를 통합하여 보다 정확하고 풍부한 답변을 생성하도록 돕는 핵심 기술입니다.


1. 프롬프트 LLM과의 효과적인 소통

LLM과 효과적으로 소통하기 위한 프롬프트는 단순히 질문을 던지는 것을 넘어, 모델이 최적의 답변을 생성하도록 유도하는 가이드 역할을 합니다.

.1 프롬프트 작성의 기본 원리

LLM은 Attention 기반의 토큰 생성 원리로 작동합니다.
이는 주어진 모든 토큰의 가중치를 고려하여 다음 토큰 생성을 결정한다는 의미입니다.

  • 셀프 어텐션(Self-Attention): 모든 입력 토큰을 참조하므로, 프롬프트 내의 역할, 지시사항 등이 생성 과정에 지속적으로 영향을 미칩니다.

  • 위치 우선(Positional Preference): 어텐션은 보통 프롬프트 앞에 나온 토큰에 더 강하게 반영되는 경향이 있습니다.
    중요한 지시나 정보는 프롬프트 앞쪽에 배치하는 것이 유리합니다.

  • 의미 유사성(Semantic Similarity): 프롬프트의 역할, 지시사항 등이 생성된 토큰과 상호작용하여 생성 방향을 유도합니다.

  • 학습 적합성(Training Congruence): 모델의 학습 구조와 유사한 질의일수록 더 좋은 생성을 유도합니다.

모델이 특정 형식의 데이터를 학습했다면, 해당 형식에 맞춰 질의하는 것이 좋습니다.

이러한 원리를 이해하면 무작위적인 토큰 생성을 특정 구조의 토큰 생성으로 유도하고, 모델 내 학습 내용 외적으로도 컨텍스트를 추가하여 답변의 품질을 높일 수 있습니다.

1.2 일반적인 프롬프트 5대 요소

효과적인 프롬프트는 일반적으로 다음 5가지 요소를 포함합니다.

  • Instruction (지시): 모델에게 요구하는 구체적인 요구사항이나 지시사항입니다.

  • Context (맥락): 생성 시 참고할 추가 정보나 배경 지식입니다.

  • Example (예시): 입력과 출력에 대한 구체적인 예시로, 모델이 원하는 응답 형식을 이해하도록 돕습니다.
    제로샷(예시 없음), 원샷(1개 예시), 퓨샷(여러 개 예시)으로 구분됩니다.

  • Input Data (입력 데이터): 사용자의 질의가 담긴 실제 데이터 형식입니다.

  • Output Data (출력 데이터): 모델이 생성할 결과물의 원하는 형식입니다.

1.3 잘 알려진 프롬프팅 기법

  • Few-Shot Learning (예시 추가): 예시를 제공하여 모델이 입출력 포맷을 이해하고 추가 컨텍스트를 얻도록 합니다.
    • 제로샷(Zero-shot): 예시 없이 지시만으로 답변을 요청합니다.
    • 원샷(One-shot): 하나의 입력-출력 예시를 제공합니다.
    • 퓨샷(Few-shot): 여러 개의 입력-출력 예시를 제공합니다.

예시 (퓨샷):

다음 문장을 긍정/부정으로 분류하세요.

입력: 이 영화 정말 재미있었어요.
출력: 긍정

입력: 배송이 너무 느리고 제품도 실망스러웠습니다.
출력: 부정

입력: 오늘 날씨가 정말 좋네요!
출력: 긍정

입력: 어제 본 드라마는 내용이 복잡하고 지루했습니다.
출력:
  • Chain of Thought (CoT): 문제를 단계별로 해결하도록 지시하여 복잡한 추론 능력을 향상시킵니다.
    "단계별로 생각해보세요"와 같은 문구를 추가합니다.

예시 (CoT):

Q: 다음 문제의 답은 무엇일까요?
학생 3명에게 연필 5개씩 나눠주면, 총 몇 개의 연필이 필요할까요?

A: 단계별로 생각해보겠습니다.
1. 학생 수는 3명입니다.
2. 각 학생에게 5개씩 연필을 나눠줍니다.
3. 필요한 연필의 총 개수는 (학생 수) * (각 학생에게 나눠주는 연필 수)입니다.
4. 3 * 5 = 15개입니다.
따라서 총 15개의 연필이 필요합니다.
  • Generated Knowledge: LLM이 스스로 필요한 지식을 생성한 후, 이를 바탕으로 최종 답변을 생성하도록 유도합니다.

  • Prompt Chaining: 복잡한 문제를 여러 개의 작은 하위 문제로 분할하고, 각 하위 문제에 대한 답변을 연결하여 최종 답변을 도출하는 기법입니다.

2. LLM 모델에 대한 이해: 적절한 모델 선택 및 튜닝

다양한 LLM 모델의 특성을 이해하고 서비스 목적에 맞는 모델을 선택하고 튜닝하는 것은 매우 중요합니다.

2.1 학습 구조에 따른 모델 선택

  • Pretrained-only (Seed Model): 기초 지식만 학습한 상태로, 편향이 낮고 중립적입니다. 특정 목적 없이 일반적인 지식 생성이 필요할 때 고려할 수 있습니다.

  • Supervised Fine-tuned (SFT): 특정 목적에 맞게 추가 튜닝된 모델입니다. 특정 도메인이나 태스크에 특화된 성능을 원할 때 적합합니다.

  • Aligned/Instruction Tuned: SFT 모델에서 프롬프트에 더 잘 반응하도록 정렬된(Align) 모델입니다 (DPO, RLHF 등을 통해). 사용자 지시에 대한 이해도가 높습니다.

  • Role-optimized: 특정 역할(예: 고객 상담원, 코딩 도우미)에 특화되어 학습된 모델입니다.

  • Mixture of Experts (MoE): 단일 모델이 아니라 여러 전문성이 결합하여 동작하는 모델로, 다양한 유형의 질의에 유연하게 대응할 수 있습니다.

  • Continually Pretrained: 기존 Pretrained 모델에 일반 지식을 더 밀어넣어 강화한 모델입니다.
    최신 정보나 특정 분야의 지식을 업데이트할 때 유용합니다.

  • Agentic Model: 외부 도구(Tool)와 연동 가능한 옵션이 제공되는 모델입니다.
    검색 엔진, 데이터베이스 등과 연결하여 외부 정보를 활용할 수 있습니다.

2.2 모델 튜닝 기법

모델 튜닝은 크게 Full Fine-tuning, 경량 튜닝, Instruction/Alignment 튜닝으로 나눌 수 있습니다.

Full Fine-tuning

  • Full Fine-tuning: 모델의 모든 파라미터를 업데이트하여 특정 데이터셋에 완벽하게 맞춥니다.
    높은 성능을 기대할 수 있지만, 많은 컴퓨팅 자원과 데이터가 필요합니다.

  • Continued Pretraining: 기존 Pretrained 모델에 추가 데이터를 사용하여 사전 학습을 계속하는 방식입니다.
    특정 도메인의 지식을 심층적으로 학습시키는 데 유리합니다.

  • 경량 튜닝 (Parameter-Efficient Fine-tuning, PEFT)
    모델의 일부분만 학습시켜 효율성을 높이는 방법입니다.

  • LoRA (Low-Rank Adaptation): 특정 행렬의 저차원만 업데이트하도록 학습합니다.
    파인튜닝 비용을 크게 줄일 수 있습니다.

  • QLoRA: LoRA를 4bit로 압축하여 더 빠르고 가볍게 학습합니다.
    리소스 제약이 있는 환경에서 유용합니다.

  • Prompt Tuning: 프롬프트에 추가되는 **'소프트 프롬프트'**만 학습하여 모델의 행동을 유도합니다.
    모델 파라미터는 건드리지 않아 효율적입니다.

  • Instruction/Alignment 튜닝
    모델이 사용자 지시에 더 잘 반응하도록 정렬하는 과정입니다.

  • SFT (Supervised Fine-tuning): 사람이 직접 제공한 **'입력-출력'**(Completion)으로 모델을 지도 학습합니다.

  • Instruction Tuning: SFT의 일종으로, 하나의 답변에 대해 여러 개의 질문(Instruction)으로 구성된 데이터를 활용합니다.

  • RLHF (Reinforcement Learning from Human Feedback): 사람이 선호하는 답변에 보상을 주어 모델을 튜닝합니다.
    가장 선호도가 높은 답변을 생성하도록 학습시킵니다.

  • DPO (Direct Preference Optimization): RLHF 없이 '승/패' 쌍과 같은 직접적인 선호도 데이터를 사용하여 튜닝합니다.
    RLHF보다 구현이 간단하고 안정적입니다.

  • RLAIF (Reinforcement Learning from AI Feedback): AI가 스스로 생성한 피드백을 기반으로 튜닝합니다.
    사람의 개입을 줄일 수 있습니다.

3. 컨텍스트(메모리) 관리: LLM의 한계 극복

LLM은 컨텍스트 윈도우(Context Window)라는 한계가 있어, 프롬프트에 포함할 수 있는 정보의 양이 제한적입니다.
컨텍스트를 효과적으로 관리하는 것은 비용과 성능 측면에서 중요합니다.

3.1 컨텍스트 관리의 주요 논제

  • 비용 증가: 프롬프트에 컨텍스트가 많이 포함될수록 API 호출 비용이 커집니다.
  • 효과적인 데이터 주입 방식: 필요한 데이터를 어떻게 효율적으로 LLM에 전달할 것인가?
  • 데이터 압축법: 많은 양의 데이터를 어떻게 압축하여 컨텍스트 윈도우 내에 넣을 것인가?

3.2 해결책

  • LLM을 통한 요약 생성: 대규모 텍스트를 LLM으로 요약하여 컨텍스트를 줄입니다.
  • LLM으로 정제한 데이터 사용: 불필요한 정보를 제거하고 핵심만 남도록 데이터를 정제합니다.
  • 데이터 의미 구조 해석 및 변형: 데이터를 단순히 요약하는 것을 넘어, 의미 구조를 파악하여 LLM이 더 잘 이해할 수 있는 형태로 변형합니다.
  • 인메모리(In-Memory) 및 영구 저장소(Persistent Storage)의 효과적인 활용: 자주 사용되는 데이터는 인메모리에 캐싱하고, 방대한 데이터는 영구 저장소에 저장하여 필요할 때 로딩합니다.
  • 검색을 통한 필요한 부분만 로딩 (RAG - Retrieval Augmented Generation): 벡터 데이터베이스 등에서 사용자의 질의와 관련된 정보를 검색하여 LLM 프롬프트에 주입하는 방식입니다.
    이 방식이 컨텍스트의 근본적인 해결책으로 가장 많이 사용됩니다.

RAG 시스템의 예시 (Python - LangChain 및 FAISS)

# 필요한 라이브러리 설치
# pip install langchain langchain-community langchain-openai faiss-cpu tiktoken

from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
import os

# OpenAI API 키 설정 (실제 환경에서는 환경 변수 사용 권장)
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

# 1. 외부 데이터 로드
# 예시 텍스트 파일 생성
with open("external_data.txt", "w", encoding="utf-8") as f:
    f.write("김철수는 2020년에 경주에서 태어났습니다. 그는 현재 서울에 살고 있습니다. 이영희는 1998년에 부산에서 태어났고, 지금은 제주도에서 일을 합니다.")

loader = TextLoader("external_data.txt", encoding="utf-8")
documents = loader.load()

# 2. 데이터 분할 (Split)
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)

# 3. 임베딩 및 벡터 저장소 생성
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(texts, embeddings)

# 4. LLM 모델 초기화
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# 5. RetrievalQA 체인 구성
# retriever를 통해 관련 문서를 찾고, LLM이 그 문서를 바탕으로 질문에 답변
qa_chain = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever())

# 6. 질문 및 답변 생성
query = "김철수는 어디에서 태어났나요?"
response = qa_chain.invoke({"query": query})
print(f"질문: {query}")
print(f"답변: {response['result']}")

query = "이영희는 어디에 살고 있나요?"
response = qa_chain.invoke({"query": query})
print(f"질문: {query}")
print(f"답변: {response['result']}")

이 예시 코드는 외부 텍스트 파일에서 데이터를 로드하고, 이를 작은 덩어리로 분할한 뒤, 각 덩어리를 임베딩하여 벡터 데이터베이스(FAISS)에 저장합니다.
사용자의 질문이 들어오면 벡터 데이터베이스에서 관련성 높은 문서 조각을 검색하여 LLM에 함께 전달함으로써, LLM이 외부 데이터를 참조하여 답변을 생성하도록 합니다.

4. 외부 데이터 가공 및 활용: LLM의 정보 확장

LLM은 학습된 데이터에 한정되므로, 실시간 정보나 특정 도메인 데이터에 접근하기 위해서는 외부 데이터의 효과적인 가공 및 활용이 필수적입니다.

4.1 Data Import (데이터 로딩)

  • 주요 과제: 외부 데이터는 대부분 시맨틱하지 않고(정형화되지 않음), 다양한 포맷 해석(PDF, HTML, JSON 등), 비텍스트 처리(이미지, 오디오), 대용량 분산 로딩, 여러 IO(입출력) 처리 등을 요구합니다.

  • 데이터 정제: 로딩한 데이터는 정합성 검사, 일반화 변환, 특수 변환 등을 통해 정제(Cleansing)해야 합니다.

  • 해결 전략: 알고리즘적인 해결책과 LLM을 통한 해결책을 믹스하여 최적화합니다.
    이를 지속적으로 처리할 수 있는 구조(파이프라인)와 운영 및 모니터링이 필요합니다.

4.2 Split (데이터 분할)

전처리가 끝난 데이터를 기반으로 다시 의미 있는 단위로 분할합니다.

  • 명확한 분할 목적 정의: 어떤 기준으로 데이터를 분할할 것인지 명확히 정의합니다 (예: 문단 단위, 챕터 단위, 고정 길이 등).

  • 다양한 분할 전략: 분할 목적과 데이터 형태(정형/비정형, 텍스트/코드 등)를 조합하여 다양한 분할 전략을 구성합니다.

  • 분할 결과 평가: 분할된 데이터가 LLM에 얼마나 효과적으로 사용될 수 있는지 평가하는 방법을 정의하고, 결과가 좋지 않으면 지속적으로 전략을 변경합니다.

  • 지속적인 감시: 한 번 잘 분할되었다고 해서 끝이 아니라, 데이터 변화에 따라 분할 전략도 계속 감시하고 개선해야 합니다.

결론

LLM 기반 서비스 개발은 단순히 프롬프트 작성에서 끝나지 않습니다.
증강 질의의 핵심인 프롬프트의 작동 원리 이해, 다양한 LLM 모델의 특성 파악, 효율적인 컨텍스트 관리, 그리고 외부 데이터의 효과적인 가공 및 활용 능력이 필수적입니다.
특히 RAG(Retrieval Augmented Generation)와 같은 기법을 통해 LLM의 정보 한계를 극복하고, 사용자에게 더 정확하고 풍부한 경험을 제공할 수 있습니다.
지속적인 학습과 실험을 통해 최적의 LLM 기반 서비스를 구축하시길 해보면 좋을것 같다는 생각이 들었습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글