본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로AI › 개발 도구 › LangChain

8. LLM 성능을 높이는 패턴

목차

LLM 성능을 높이는 패턴

앞 장에서는 에이전트가 도구를 고르고, 성찰하거나, 다른 에이전트에 작업을 넘기는 구조를 살펴봤다. 구조가 복잡해질수록 응답이 좋아질 것 같지만, LLM 호출을 추가하는 것 자체는 성능 개선을 보장하지 않는다. 먼저 어떤 답이 실패인지 정의하고, 실패가 발생한 단계를 찾아야 한다.

예를 들어 사내 문서 질문에 답하는 RAG 애플리케이션이 “휴가 신청 기한”을 틀렸다고 하자. 검색 결과에 최신 규정이 없었다면 프롬프트를 고쳐도 답의 근거가 생기지 않는다. 반대로 최신 규정이 검색됐는데 오래된 규정을 인용했다면 검색보다 답변 생성과 근거 확인 단계가 문제다.

flowchart LR
    Q[사용자 질문] --> R[문서 검색]
    R --> C[검색 결과 확인]
    C -->|관련 문서 없음| I[인덱스·검색 개선]
    C -->|관련 문서 있음| G[답변 생성]
    G --> V[근거·형식 검증]
    V -->|실패| P[프롬프트·검증 개선]
    V -->|통과| A[답변]

1. 먼저 측정할 것을 정하기

[품질을 한 숫자로만 표현하면 놓치는 것]

관찰 항목확인할 질문가능한 조치
검색 적중정답을 뒷받침하는 문서가 상위 결과에 있는가?청크 크기, 메타데이터 필터, 검색 방식을 조정
근거 충실성답변의 주장마다 검색 문서가 실제로 뒷받침하는가?근거 인용, 답변 검증, 모르면 모른다고 답하기
과업 성공사용자가 요청한 형식과 행동을 마쳤는가?출력 스키마, 도구 결과 검증
지연 시간검색·모델·도구 중 어디에 시간이 걸리는가?불필요한 호출 제거, 독립 작업 병렬화
비용입력 문맥과 반복 호출이 얼마나 늘어났는가?문맥 축약, 호출 횟수 상한

같은 테스트 질문 집합을 고정하고 변경 전후 결과를 비교한다. 질문 난이도와 문서 버전이 달라지면 수치 차이가 패턴의 효과인지 판단할 수 없다. 평가 데이터 구성은 10장에서 다룬다.

2. 프롬프트 연결(prompt chaining)

한 번에 “검색하고, 비교하고, 요약하고, JSON으로 답해”라고 요구하면 어느 단계에서 실패했는지 알기 어렵다. 작업을 검색 → 근거 추출 → 답변 작성 → 형식 검사처럼 쪼개면 중간 결과를 검사할 수 있다.

flowchart LR
    A[질문] --> B[관련 규정 검색]
    B --> C[날짜·조건 추출]
    C --> D[답변 작성]
    D --> E{형식과 근거 통과?}
    E -->|예| F[응답]
    E -->|아니요| G[오류 처리 또는 수정]

검색 실패 시 바로 “근거를 찾지 못했습니다”라고 반환할 수 있다. 없는 문서를 바탕으로 답변을 만들지 않는 것이 핵심이다. 다만 단계마다 모델을 호출하면 지연과 비용이 늘어난다. 단순 추출은 코드나 구조화 출력으로 처리할 수 있는지 먼저 확인한다. LangGraph 워크플로 문서는 프롬프트 연결을 순서가 정해진 단계에 적용하는 패턴으로 설명한다.

3. 라우팅(routing)

질문의 유형이 다르면 사용할 자료와 처리 단계도 달라진다. 예를 들어 “내 휴가 잔여일”은 사용자별 데이터 조회가 필요하지만 “휴가 정책은?”은 문서 검색이 필요하다.

flowchart TD
    Q[질문] --> R{요청 유형}
    R -->|정책 설명| D[문서 검색]
    R -->|개인 잔여일| U[권한 확인 후 사용자 데이터 조회]
    R -->|범위 밖| X[지원 범위 안내]
    D --> A[답변 생성]
    U --> A

라우터는 허용된 경로 중 하나만 선택하게 한다. 분류 결과가 불확실하면 임의의 도구로 보내지 말고 안전한 기본 경로를 둔다. 개인정보 조회 경로는 라우터의 판단과 별개로 서버에서 인증·권한을 다시 검사해야 한다. 모델의 분류 결과를 권한으로 취급하면 안 된다.

4. 독립 작업의 병렬화(parallelization)

문서의 정확성과 표현 형식을 검사하는 일은 서로 독립적일 수 있다. 두 검사를 동시에 시작한 뒤 결과를 합치면 순차 실행보다 대기 시간이 줄어들 여지가 있다.

flowchart LR
    A[답변 초안] --> B[근거 검사]
    A --> C[형식 검사]
    B --> D[검사 결과 합치기]
    C --> D
    D --> E{모두 통과?}

하지만 병렬 요청 두 개가 모델 호출 두 번이라는 사실은 그대로다. 요청량 제한, 동시 실행 상한, 일부 작업 실패 시 처리 방식을 정해야 한다. 두 번째 작업이 첫 번째 결과를 필요로 한다면 병렬화할 수 없다.

5. 평가-개선 루프(evaluator-optimizer)

초안 생성 후 평가자가 구체적인 결함을 찾아 수정 요청을 돌려준다. 7장의 성찰과 닮았지만, 여기서는 종료 기준과 반복 상한을 명시한다.

flowchart LR
    S[요청] --> G[초안 생성]
    G --> E[근거·형식 평가]
    E -->|통과| O[최종 응답]
    E -->|실패, 재시도 가능| G
    E -->|재시도 상한| F[불확실성 안내]

[간단한 LangGraph 구성 예시] 아래 코드는 인용 표기가 있는지만 검사한다. 인용한 문서가 실제로 주장을 뒷받침하는지는 별도 평가가 필요하다. 실행하려면 langgraph, langchain-ollama를 설치하고 OLLAMA_MODEL에 로컬에 준비된 모델 이름을 지정한다.

import os
from typing import TypedDict

from langchain_ollama import ChatOllama
from langgraph.graph import END, START, StateGraph

model = ChatOllama(model=os.environ["OLLAMA_MODEL"], temperature=0)

class State(TypedDict):
    question: str
    context: str
    answer: str
    feedback: str
    attempts: int

def draft(state: State) -> dict:
    response = model.invoke([
        ("system", "제공된 문서만 근거로 답하세요. 인용은 [문서:ID]로 표시하세요. "
                   "근거가 없으면 모른다고 답하세요."),
        ("user", f"질문: {state['question']}\n문서: {state['context']}\n"
                 f"이전 피드백: {state['feedback']}"),
    ])
    return {"answer": str(response.content), "attempts": state["attempts"] + 1}

def check(state: State) -> dict:
    # 문자열 검사일 뿐, 인용의 사실 여부를 검증하지는 않는다.
    has_citation = "[문서:" in state["answer"]
    return {"feedback": "" if has_citation else "답변에 문서 ID 인용이 없습니다."}

def route(state: State) -> str:
    if not state["feedback"]:
        return "done"
    return "retry" if state["attempts"] < 2 else "stop"

builder = StateGraph(State)
builder.add_node("draft", draft)
builder.add_node("check", check)
builder.add_edge(START, "draft")
builder.add_edge("draft", "check")
builder.add_conditional_edges("check", route, {
    "done": END, "retry": "draft", "stop": END,
})
graph = builder.compile()

result = graph.invoke({
    "question": "휴가 신청 기한은?",
    "context": "[문서:HR-12] 휴가는 시작일 3영업일 전까지 신청한다.",
    "answer": "", "feedback": "", "attempts": 0,
})
print(result["answer"] if not result["feedback"] else "인용 검사를 통과하지 못했습니다.")

이 예시는 검증 가능한 규칙부터 코드로 처리한다는 점을 보여준다. 실제 서비스에서는 답변 속 문서 ID가 검색 결과에 포함되는지 검사하고, 문장과 근거의 의미 일치 여부는 사람 검토 또는 별도 평가자로 확인한다. 재시도할 때 모델이 같은 오류를 반복할 수 있으므로 상한 뒤에는 실패를 숨기지 않는다. 공식 evaluator-optimizer 설명도 생성, 평가, 피드백을 반복하는 구조를 제시한다.

6. RAG 성능 문제를 분리해서 해결하기

2~3장에서 인덱싱과 검색을 다뤘다. 실제 개선 순서는 다음과 같다.

  1. 문서 수집 확인: 최신 정책이 인덱스에 들어왔는지, 파싱 과정에서 표·제목이 사라지지 않았는지 확인한다.
  2. 검색 확인: 질문에 맞는 문서가 상위 결과에 있는지 본다. 없으면 청크 경계, 메타데이터 필터, 검색 질의를 조정한다.
  3. 문맥 확인: 검색 결과가 너무 많아 중요한 근거가 잘리지 않는지 확인한다.
  4. 생성 확인: 문서에 있는 답을 모델이 누락하거나 다른 내용을 보태는지 평가한다.
  5. 거절 확인: 자료가 없는 질문에 근거 없는 답을 만들지 않는지 검사한다.

한 번에 여러 요소를 바꾸면 무엇이 효과가 있었는지 알 수 없다. 예를 들어 청크 크기만 바꾸고 동일한 질문 집합에서 검색 적중과 최종 답변을 각각 비교한다.

7. 패턴 선택 기준

증상먼저 시도할 것추가로 확인할 비용
긴 지시를 한 번에 처리하다 중간 조건을 놓침프롬프트 연결단계별 호출 지연
질문 종류마다 다른 도구가 필요함라우팅오분류와 기본 경로
서로 무관한 검사 때문에 대기함병렬화동시 호출량과 부분 실패
초안에 반복 가능한 오류가 남음평가-개선 루프무한 반복 방지, 평가 비용
답의 근거가 검색되지 않음인덱스·검색 개선재인덱싱 비용

[정리] 패턴은 문제를 푸는 수단이다. 먼저 실패 사례를 기록하고, 한 단계씩 바꾸고, 같은 데이터로 품질·지연·비용을 비교한다. 다음 장에서는 이런 그래프를 사용자가 호출할 수 있는 서비스로 배포할 때 필요한 경계를 살펴본다.

8. 같은 질문으로 변경 전후를 비교하는 방법

패턴을 고르는 기준은 “이 구조가 멋있는가”가 아니라 실패 유형이 줄어드는가다. 휴가 규정 검색 예제라면 최소한 네 가지 질문군을 만든다. (1) 현재 문서에 정답이 있는 질문, (2) 말만 바꾼 질문, (3) 근거가 없는 질문, (4) 오래된 규정과 최신 규정이 함께 검색되는 질문이다. 질문군마다 문서 버전과 기대하는 문서 ID를 고정해야 한다.

사례검색 결과에서 볼 것최종 답에서 볼 것
“휴가 신청 기한은?”현행 HR-12가 상위 결과에 있는가3영업일과 HR-12가 함께 있는가
“언제까지 연차를 올려야 해?”같은 규정을 다른 표현으로 찾는가질문 표현이 달라도 조건이 같은가
“해외 출장 일비는?”근거 없는 문서가 상위로 뜨지 않는가숫자를 추측하지 않는가
“2024년 신청 기한은?”적용 연도 필터가 작동하는가현행 규정을 과거 규정처럼 쓰지 않는가

이 표는 실제 측정값이 아니라 평가 사례 설계 예시다. 검색 결과에 HR-12가 없으면 검색 실패, 있는데도 답이 3일로 바뀌었다면 생성 실패로 기록한다. 이렇게 단계를 나누면 문서가 없는 문제에 평가-개선 루프를 추가하는 낭비를 막을 수 있다. 검색의 recall@k는 “정답 문서가 상위 k개에 포함되는 비율”로 정의할 수 있다. 다만 문서 ID가 맞아도 문서의 오래된 버전이 나왔다면 성공으로 세면 안 된다.

아래 함수는 호출별 시간과 결과를 함께 기록하는 작은 경계다. retrieve, answer는 각자의 애플리케이션 함수로 교체한다. 수치를 출력하는 예제라기보다 어느 단계에서 시간을 쓰는지 기록하는 위치를 보여준다.

from time import perf_counter
from typing import Callable

def run_case(
    question: str,
    retrieve: Callable[[str], list[dict]],
    answer: Callable[[str, list[dict]], str],
) -> dict:
    started = perf_counter()
    docs = retrieve(question)
    retrieved_at = perf_counter()
    result = answer(question, docs) if docs else "근거 문서를 찾지 못했습니다."
    finished = perf_counter()
    return {
        "question": question,
        "retrieved_ids": [doc["id"] for doc in docs],
        "answer": result,
        "retrieval_ms": round((retrieved_at - started) * 1000, 1),
        "generation_ms": round((finished - retrieved_at) * 1000, 1),
    }

검색 문서의 ID, 그래프·프롬프트 버전, 모델 이름을 결과와 함께 보관한다. retrieval_ms만 늘었다면 인덱스·네트워크·재정렬 단계를, generation_ms가 늘었다면 입력 문맥 길이와 모델 호출 횟수를 본다. 한 번의 시간만으로 결론을 내리지 말고 같은 사례를 반복 실행해 중앙값과 느린 쪽의 분포를 비교한다. 모델 응답의 의미 평가는 별도로 수행한다(LangSmith 평가 가이드).

9. 라우터와 병렬 검사를 실제로 연결할 때

라우터가 "personal"을 골랐다고 해서 개인 데이터를 조회할 권한이 생기지 않는다. 라우팅은 작업 종류 선택, 인증과 권한 검사는 서버의 강제 규칙이다. 예를 들어 아래처럼 라우트 이름을 제한하고 기본 경로를 둔다.

ALLOWED_ROUTES = {"policy", "personal", "unsupported"}

def choose_route(raw_route: str) -> str:
    route = raw_route.strip().lower()
    return route if route in ALLOWED_ROUTES else "unsupported"

def dispatch(route: str, user_id: str, subject_user_id: str) -> str:
    if route == "personal":
        if user_id != subject_user_id:
            raise PermissionError("다른 사용자의 정보를 조회할 수 없습니다.")
        return "개인 데이터 조회 단계"
    if route == "policy":
        return "정책 문서 검색 단계"
    return "지원 범위 안내"

실서비스의 권한 규칙은 단순한 ID 비교보다 복잡하다. 핵심은 모델 출력인 raw_route가 잘못된 값이어도 허용된 경로만 실행하고, 개인정보 조회 직전에 서버의 신뢰할 수 있는 사용자 정보로 다시 검사하는 것이다. LangGraph에서는 이 선택을 조건부 엣지로 표현할 수 있다.

병렬 검사는 두 입력이 동일한 초안에만 의존할 때 적용한다. 근거 검사가 문서를 조회한 뒤에야 형식 검사가 가능하다면 순차 단계다. 독립 검사라면 각 검사에 시간 제한을 걸고, 하나라도 실패하면 성공으로 합치지 않는다. 병렬화 전후에는 최종 품질뿐 아니라 최대 동시 요청 수와 느린 검사에 묶이는 시간을 비교한다. 요청량 제한에 닿아 재시도가 늘면 병렬화가 오히려 느려질 수 있다.

10. 개선이 실패했을 때 되돌리는 단위

프롬프트, 모델, 검색 인덱스, 그래프 코드를 한 번에 바꾸면 실패 원인을 찾기 어렵다. 새 청크 규칙을 적용했다면 인덱스 버전만 바꾸고 이전 데이터셋을 실행한다. 그 뒤 라우터를 추가한다면 같은 인덱스에서 라우팅 전후를 비교한다. 문서가 개정될 때는 그 문서의 적용일과 인덱스 생성 시각을 함께 남긴다.

평가-개선 루프가 통과했다고 해도 평가자의 규칙이 빈약하면 틀린 답을 통과시킨다. 위의 "[문서:" 문자열 검사처럼 쉽게 속일 수 있는 기준은 형식 검사로만 취급한다. “인용한 문서에 해당 조건이 실제로 적혀 있는가”를 별도 검사하거나 사람이 표본을 읽어야 한다. 오류가 두 번 반복되면 같은 입력을 무한히 다시 모델에 보내지 말고 검증 실패 상태와 요청 ID를 반환한다. 사용자에게 확정적 답을 보여주지 않는 편이 다음 수정 지점을 분명하게 한다.

참고 문서

같은 카테고리의 글