Skip to content
This page has been auto-translated and may contain errors.View in English

RAG

docs.scrimba.com

먼저 이름부터 살펴봅시다. RAG는 **검색 증강 생성(retrieval-augmented generation)**의 줄임말입니다. 질문이 들어오는 순간 관련 텍스트를 직접 가져와 모델에 전달하면, 모델은 고정된 기억이 아닌 여러분이 제공한 사실을 바탕으로 답변합니다. 이 장에서는 그 검색-후-답변 루프가 어떻게 작동하고 어디서 잘못될 수 있는지를 다룹니다.

검색 후 생성 패턴

모델은 여러분 회사의 도움말 문서, 제품 세부 정보, 또는 학습 이후에 발생한 일들을 알지 못합니다. 그런 내용을 물어보면 모델은 도움을 줄 수 없다고 인정하거나, 더 나쁜 경우에는 그럴듯하게 들리는 내용을 꾸며내기도 합니다. RAG는 질문이 들어오는 순간 모델에 관련 텍스트를 전달함으로써 이 문제를 해결합니다.

이 아이디어는 임베딩을 이해하면 비로소 명확해집니다. 임베딩은 텍스트를 벡터로 변환해 근접도(두 벡터가 얼마나 가까운 방향을 가리키는지를 측정하는 코사인 유사도)로 비교할 수 있게 하는 기법입니다. **검색 증강 생성**은 일반적인 모델 호출에 두 단계를 추가한 것입니다.

  1. 검색. 사용자의 질문을 받아 문서에서 가장 관련성 높은 부분을 찾고, 상위 결과를 수집합니다.
  2. 생성. 해당 부분을 컨텍스트로 프롬프트에 삽입한 뒤, 모델에 그 컨텍스트만을 사용하여 질문에 답하도록 요청합니다.

모델의 언어 능력이 글쓰기를 담당하고, 검색된 텍스트가 사실을 제공합니다. 여러분이 무엇을 넣을지 제어하기 때문에, 여러분의 콘텐츠에 근거하고 최신 상태를 유지하는 유창한 답변을 얻을 수 있습니다.

이것이 왜 잘 작동하는지, 예측 루프의 관점에서 살펴보면 도움이 됩니다. RAG 없이는 모델이 파라미터에 고정된 패턴에서 사실을 떠올려야 하는데, 그 사실이 없을 수도 있어 자신감 있는 추측으로 이어집니다. RAG를 사용하면 사실이 컨텍스트 안에 바로 있으므로, 모델의 역할이 "이것을 기억해"에서 "눈앞에 있는 것을 읽고 답해"로 바뀝니다. 검색은 작업을 기억에서 읽기로 전환합니다. 시험으로 비유하자면, 책 없이 보는 시험은 추측을 유도하는 반면, RAG는 관련 페이지가 책상 위에 펼쳐진 오픈북 시험으로 만들어 줍니다.

Juno검색 후 생성 패턴 RAG는 일반 모델 호출에 두 단계를 추가합니다. 질문에 가장 관련성 높은 문서를 검색하고, 그것을 컨텍스트로 사용해 답변을 생성하는 것입니다. 모델이 언어를 제공하고, 검색된 텍스트가 사실을 제공합니다. 이를 통해 모델이 기억이 아닌 여러분의 콘텐츠를 바탕으로 답변할 수 있으며, 이것이 바로 모델이 답을 꾸며내는 것을 막는 원리입니다. 처음엔 신뢰하기까지 시간이 걸렸지만, 오픈북 모델이 클로즈드북 모델보다 정말로 추측을 덜 한다는 것을 알게 됐습니다.

모델은 여러분의 도움말 문서, 제품 세부 정보, 또는 학습 기간 이후의 내용을 담고 있지 않습니다. 그런데도 물어보면 거절이나 그럴듯하게 읽히는 꾸며낸 답변이 돌아옵니다. **검색 증강 생성**은 요청 시점에 관련 텍스트를 가져와 프롬프트에 배치함으로써 그 간극을 메웁니다. 모델이 사실을 떠올리는 대신 직접 읽게 되는 것입니다. 이 메커니즘은 임베딩에 기반합니다. 각 텍스트 조각이 벡터가 되고, 유사도는 벡터 간의 거리(두 벡터 사이 각도의 코사인, 즉 코사인 유사도)로 측정됩니다.

루프는 두 단계로 구성됩니다.

  1. 검색. 질문을 문서와 동일한 벡터 공간에 임베딩한 뒤, 유사도가 가장 높은 청크를 가져옵니다.
  2. 생성. 해당 청크들을 컨텍스트로 프롬프트에 이어 붙이고, 모델에 그것만을 사용하여 답하도록 지시합니다.

검색 단계에는 여러분이 설정하는 다이얼이 있습니다. top-k: 실제로 유지할 가장 가까운 청크의 개수입니다. k는 균형의 문제입니다. 너무 작으면 답이 담긴 청크를 놓쳐 모델이 참고할 것이 없어집니다. 너무 크면 느슨하게 관련된 텍스트로 컨텍스트가 채워져 신호가 희석되고, 비용이 증가하며, 중간 부분을 놓치는 문제가 악화됩니다. 3에서 5 사이의 k가 시작하기 좋은 지점이며, 실제 질문으로 튜닝합니다.

핵심 포인트: 인덱스를 청크 텍스트와 사전 계산된 벡터를 담은 레코드 목록으로 구성하고, 검색된 컨텍스트가 질문 위에 명확히 구분된 블록으로 자리 잡도록 프롬프트를 설계하세요. 모델의 언어 능력이 글을 쓰고, 검색된 텍스트가 무엇이 사실인지를 결정합니다.

python
def retrieve(question, index, k=4):
    query_vector = embed(question)
    scored = [
        {"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])}
        for item in index
    ]
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
Juno검색 후 생성 패턴 RAG는 검색 후 생성입니다. 질문을 임베딩하고, 가장 가까운 청크를 가져오고, 프롬프트에 넣은 다음, 그것을 바탕으로 답변합니다. 여기서 중요한 다이얼은 top-k, 즉 유지할 청크의 개수입니다. 너무 적으면 답을 놓치고, 너무 많으면 노이즈 속에 묻혀 그 값어치를 치르게 됩니다. 3에서 5 사이에서 시작해 실제 질문으로 튜닝하세요. 감에 의존하지 말고요.

모델은 여러분의 비공개 코퍼스나 학습 기간 이후의 내용을 갖고 있지 않으며, 어느 쪽인지 알려주지도 않습니다. 어느 경우든 같은 자신감 있는 어조로 답변하기 때문입니다. **검색 증강 생성**은 요청 시점에 관련 텍스트를 가져와 답변의 근거로 삼는 패턴입니다. 정확도는 여러분이 통제하는 소스에 달려 있으며, 파라미터 기억에 의존하지 않습니다. 임베딩을 기반으로 동작합니다. 텍스트가 벡터로 매핑되고, 코사인 유사도(두 벡터 사이 각도의 코사인, 길이와 무관하게 의미의 근접도)로 순위가 매겨집니다.

검색 후 생성이라는 두 단계 프레임워크는 맞지만, 실제 작업이 어디서 일어나는지를 숨깁니다. 검색은 생성의 탈을 쓴 검색 문제이며, 전체 시스템의 품질은 모델 호출이 아닌 검색에서 결정됩니다. 따라서 처음부터 검색-재순위-후-생성으로 접근하세요. 단순한 버전, 즉 쿼리를 임베딩하고 가장 가까운 k개를 가져와 붙여넣는 방식은, 하위 수준에서 아직 마주치지 못한 실패 사례들, 즉 정확한 일치 누락, 커버리지를 가리는 거의 중복된 청크들, 자신감 있게 답변하는 오래된 텍스트에서 무너집니다.

초기에 갖춰야 할 첫 번째 습관은, 이것이 감이 아닌 측정 가능한 시스템이라는 것입니다. 실제 질문과 그것에 답해야 하는 청크로 구성된 검증 세트를 보유하고, recall@k를 추적하세요. 검색되었어야 할 청크 중 실제로 상위 k에 포함된 비율입니다. recall@k는 여러분의 상한선입니다. 올바른 청크가 컨텍스트에 없으면 아무리 뛰어난 프롬프트 기술도 그것을 복구할 수 없으며, 생성 단계는 검색이 전달하지 못한 것에 답할 수 없습니다. 이후의 모든 것이 이 수치에 의해 제한되므로, 가장 먼저 측정하고 가장 먼저 개선해야 할 지표입니다.

python
def retrieve(question, index, k=8):
    query_vector = embed(question)
    scored = [
        {"text": it["text"], "score": cosine_similarity(query_vector, it["vector"]), "meta": it["meta"]}
        for it in index
    ]
    # 여기서 많이 가져온 뒤, 더 작은 최종 세트로 재순위 매기기 (아래 참조)
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
Juno검색 후 생성 패턴 RAG는 생성의 탈을 쓴 검색 문제입니다. 품질은 모델 호출이 아닌 검색에서 결정되므로, 단순한 임베딩-k개 가져오기-붙여넣기 방식은 정확한 일치와 오래된 텍스트에서 실패합니다. 모든 것을 제한하는 수치는 recall@k, 즉 검색되었어야 할 청크 중 실제로 상위 k에 포함된 비율입니다. 올바른 청크가 컨텍스트에 오지 않으면 어떤 프롬프트도 구해주지 못합니다. 다른 것을 튜닝하기 전에 이것부터 측정하세요. 그렇지 않으면 잘못된 쪽을 닦고 있는 겁니다.

청킹

문서 전체를 임베딩하지 않습니다. 먼저 문단이나 몇 개의 문장 크기로 작은 **청크**로 분할한 뒤 임베딩합니다. 이유는 두 가지입니다. 첫째, 검색이 더 정밀해집니다. 40페이지짜리 매뉴얼 전체 대신 관련된 문단 하나만 가져올 수 있습니다. 둘째, 청크가 컨텍스트 창에 맞습니다. 거대한 문서는 맞지 않을 수도 있거든요.

python
# 간단한 청커: 문단 기준으로 분할
def chunk(text):
    return [c.strip() for c in text.split("\n\n") if c.strip()]

청크 크기는 균형의 문제이며, 그 이유는 임베딩으로 거슬러 올라갑니다. 각 청크는 전체 의미를 요약하는 단일 벡터로 변환됩니다. 청크가 너무 크면 여러 주제를 다루게 되어, 하나의 벡터가 모든 주제의 흐릿한 평균이 되어 특정 질문에 잘 맞지 않습니다. 너무 작으면 의미를 이해하는 데 필요한 주변 맥락을 잃어버려, 벡터가 단편을 가리키게 됩니다. 문단 크기의 청크는 각각 하나의 일관된 아이디어를 담습니다. 검색 성능에 따라 그 기준점에서 조정해 나가면 됩니다.

Juno청킹 임베딩 전에 문서를 더 작은 청크로 분할하세요. 대략 문단 크기로, 그것을 임베딩합니다. 작은 청크는 검색을 더 정밀하게 만들고 컨텍스트 창에 더 잘 맞습니다. 청크 크기는 균형의 문제입니다. 너무 크면 흐릿하고 낭비이며, 너무 작으면 주변 맥락을 잃습니다. 답변이 모호하게 돌아올 때, 청크가 거의 항상 너무 컸습니다.

문서 전체가 아닌 청크를 임베딩합니다. 각 청크가 전체 의미를 요약하는 하나의 벡터가 되어야 하기 때문입니다. 문서를 문단 크기의 조각으로 분할하고 각각을 임베딩하세요. 청크가 너무 크면 여러 주제가 하나의 벡터로 평균화되어 어떤 것에도 정확히 맞지 않고, 너무 작으면 의미를 만들었던 맥락을 잃습니다.

따라서 **청크 크기**를 고정된 규칙이 아닌 튜닝 다이얼로 취급하세요. 몇 백 토큰 크기의 청크는 하나의 아이디어를 깔끔하게 담는 경향이 있습니다. 이 다이얼은 top-k와 상호작용합니다. 청크가 클수록 컨텍스트 예산에 맞는 청크 수가 줄어들어 요청당 커버리지가 낮아집니다. 청크가 작을수록 정밀도는 높아지지만, 여러 청크에 걸쳐 있는 아이디어를 재조립하려면 더 큰 k가 필요합니다.

두 번째 다이얼은 **겹침(overlap)**입니다. 인접한 청크가 경계에서 텍스트의 일부를, 예를 들어 10~20%를 공유하게 하세요. 겹침이 없으면 질문에 답하는 문장이 경계에 걸쳐 나뉠 수 있습니다. 한 청크에 절반, 다음 청크에 절반이 들어가 어느 청크도 잘 검색되지 않습니다. 겹침은 약간의 중복과 몇 개의 벡터를 더 소비하는 대신, 경계에서 잘렸을 답변들을 되살립니다.

python
def chunk(text, size=800, overlap=120):
    # 여기서 size와 overlap은 문자 단위; 실제 시스템에서는 토큰 기준으로 예산을 잡으세요
    out, start = [], 0
    while start < len(text):
        out.append(text[start:start + size])
        start += size - overlap
    return out
Juno청킹 청크 크기와 겹침은 한 번 맞추면 끝나는 설정이 아니라 튜닝 다이얼입니다. 하나의 아이디어를 담는 청크, 즉 몇 백 토큰을 목표로 하되, 이 다이얼이 top-k와 맞바꾸는 관계임을 기억하세요. 청크가 클수록 맞는 개수가 줄어듭니다. 경계에 걸친 답변이 최소한 하나의 청크 안에 온전히 들어올 수 있도록 10~20% 겹침을 추가하세요. 경계는 검색이 조용히 답을 잃는 곳입니다.

청킹은 단 하나의 쿼리가 실행되기 전에 검색 recall이 결정되는 곳입니다. 각 **청크**는 전체 의미를 대변해야 하는 하나의 벡터가 되므로, 분할 방식이 무엇을 찾을 수 있는지를 결정합니다. 문단이나 섹션 경계는 고정 문자 창보다 구조가 있는 문서에서 더 좋습니다. 하나의 일관된 섹션에 대한 벡터가 두 섹션의 절반에 걸친 벡터보다 더 명확한 쿼리 대상이 되기 때문입니다.

청크 크기와 겹침은 자유 파라미터가 아니라 전체 예산과 연결되어 있습니다. 크기는 top-k와, 또 컨텍스트 창과 맞바꿉니다. 큰 청크는 각각 더 많은 맥락을 담지만 맞는 개수가 줄어 커버리지가 낮아지고, 작은 청크는 정밀도를 높이지만 아이디어를 분산시켜 재조립에 더 높은 k가 필요하며, 토큰 비용이 들고 중간 부분을 잃는 문제가 재발합니다. 대략 10~20%의 겹침은 경계에 걸친 답변을 살리지만, 인덱스에 중복을 만드는 비용이 있습니다. 이 중복에는 나중에 나타나는 부작용이 있습니다. 거의 동일한 겹치는 청크들이 top-k 슬롯 여러 개를 같은 내용으로 채워 커버리지를 굶길 수 있습니다. 이것이 나중에 재순위 매기기와 중복 제거가 자리를 차지하는 이유 중 하나입니다.

대부분의 사람들이 불편해질 때까지 건너뛰는 비용: 청킹 전략은 인덱스에 구워져 있으므로, 변경하면 전체 코퍼스를 다시 임베딩하고 재인덱싱해야 합니다. 이는 실제 계산 비용이고, 실제 비용이며, 이전 벡터와 새 벡터를 혼합하지 않아야 하는 마이그레이션 기간이 필요합니다. 실제 문서와 쿼리를 기준으로 청크 크기를 일찍 결정하고, 보류 세트에서 recall@k를 측정하며, 재청킹을 금요일에 배포하는 설정 변경이 아닌 의도적인 재인덱싱으로 취급하세요.

Juno청킹 구조가 있다면 구조를 기준으로 청킹하세요. 각 청크는 전체 의미를 대변하는 하나의 벡터이고, 크기는 top-k와, 또 컨텍스트 창과 한꺼번에 맞바꿔야 하기 때문입니다. 10~20% 겹침은 경계에 걸친 답변을 살리지만 거의 중복된 청크를 만들어 top-k를 가득 채울 수 있으며, 이것이 부분적으로 재순위 매기기가 존재하는 이유입니다. 그리고 청킹 선택이 인덱스에 구워져 있다는 것을 기억하세요. 변경하면 전체를 다시 임베딩하고 재인덱싱해야 합니다. 실제 비용과 마이그레이션이 필요하며, 설정 하나 바꾸는 것이 아닙니다.

벡터가 사는 곳

소수의 청크라면 벡터를 메모리에 보관하고 임베딩 장의 코사인 유사도 함수로 비교할 수 있습니다. 이후 예제에서도 그렇게 합니다. 수천 또는 수백만 개의 청크라면 속도가 느려지고, **벡터 저장소**가 필요해집니다. 대규모에서 가장 가까운 벡터를 빠르게 찾도록 만들어진 데이터베이스입니다.

RAG를 배우는 데 하나가 꼭 필요하지는 않으며, 지금 당장 하나를 선택할 필요도 없습니다. 호스팅 서비스와 라이브러리 형태로 여러 가지가 존재합니다. 중요한 것은 개념입니다. 임베딩을 저장하고 빠르게 검색하는 어딘가가 필요하다는 것입니다. 메모리에서 시작하고, 컬렉션이 커지면 벡터 저장소로 이동하세요.

Juno벡터가 사는 곳 몇 개의 청크라면 벡터를 메모리에 보관하고 직접 비교하세요. 대규모 컬렉션에는 벡터 저장소가 대규모에서 가장 가까운 벡터를 빠르게 찾는 데이터베이스입니다. RAG를 배우는 데 하나가 꼭 필요하지 않습니다. 메모리에서 시작하고 컬렉션이 커지면 벡터 저장소로 이동하세요. 첫날부터 데이터베이스를 고를 필요는 없습니다.

수백 개의 청크라면 벡터를 메모리에 보관하고 모든 벡터를 쿼리와 비교해 점수를 매깁니다. 이는 선형 스캔입니다. 소규모에서는 괜찮지만, 컬렉션이 수천 또는 수백만 개로 늘어나면 모든 쿼리마다 모든 벡터와 비교하기 때문에 느려집니다. 그 시점에서 **벡터 저장소**로 이동합니다. 빠른 최근접 이웃 검색을 위해 벡터를 인덱싱하는 데이터베이스입니다.

기억해 둘 비대칭성은 한 번 임베딩 대 쿼리당 임베딩입니다. 모든 문서 청크는 사전에 정확히 한 번 임베딩되고 인덱싱되며, 그 작업은 이후의 모든 질문에 재사용됩니다. 쿼리는 요청당 한 번, 요청 시점에 임베딩되며, 그것이 핫 경로에서 유일한 임베딩 비용입니다. 따라서 무거운 인덱싱 비용은 선불로 지불되고 분산되며, 쿼리당 비용은 임베딩 호출 하나와 검색입니다. 이것이 구축된 후 RAG를 서비스하는 비용이 저렴한 이유입니다.

패턴을 배우는 데 저장소를 선택할 필요가 없습니다. 메모리 내 레코드 목록으로 시작하고, 규모나 영속성이 요구될 때 벡터 저장소를 교체하며, 인터페이스를 동일하게 유지하세요. 쿼리를 임베딩하고, 검색하고, top-k를 반환하는 것입니다.

Juno벡터가 사는 곳 메모리에서는 쿼리당 모든 벡터를 스캔하는데, 컬렉션이 커지기 전까지는 괜찮습니다. 그러면 벡터 저장소가 인덱싱된 최근접 이웃 검색을 제공합니다. 비대칭성을 머릿속에 담아 두세요. 문서는 한 번 임베딩되고 인덱싱되어 영원히 재사용되지만, 쿼리는 각 요청마다 새로 임베딩됩니다. 선불 비용은 한 번 지불되고 분산되므로 서비스 비용이 저렴하게 유지됩니다. 메모리에서 시작하고, 규모나 영속성이 강제할 때 저장소를 교체하세요.

메모리에서는 선형 스캔을 수행합니다. 모든 청크 벡터에 대해 쿼리의 점수를 매기는데, 정확하지만 쿼리당 O(n)이며 수만 개 어딘가에서 확장이 멈춥니다. **벡터 저장소**는 스캔을 근사 최근접 이웃 인덱스(ANN)로 대체합니다. 모든 벡터를 확인하지 않음으로써 약간의 recall을 포기하는 대신 큰 속도 향상을 얻습니다. 이 트레이드오프는 조절 가능하며, 프로덕션 규모에서 인덱스가 때때로 진짜 최근접 이웃을 놓칠 수 있다는 것을 인정해야 하고, 이는 바로 recall@k에 반영됩니다.

한 번만 임베딩하는 비대칭성이 비용 구조를 결정합니다. 인덱싱은 전체 코퍼스를 한 번에 임베딩하는 대규모 선불 배치 비용이고, 서비스는 요청당 쿼리 하나를 임베딩하고 검색 하나를 수행합니다. 따라서 안정적인 지연 시간 예산은 대략 쿼리 임베딩, 인덱스 검색, 생성 호출입니다. 검색은 보통 가장 작은 부분이고, 생성 호출이 지연 시간을 지배하며, 쿼리 임베딩은 검색 전에 지불해야 하는 고정 세금입니다. 각 단계를 측정할 가치가 있습니다. 사람들이 더 빠른 벡터 저장소를 찾는데, 실제 지연 시간은 처음부터 생성에 있었던 경우가 많기 때문입니다.

그 다음은 오래됨(staleness)입니다. 메모리 내 장난감이 절대 보여주지 않는 실패 모드입니다. 인덱스는 스냅샷이고, 소스 문서는 변화합니다. 정책이 바뀌고, 문서가 편집되고, 오래된 청크가 여전히 인덱스에 앉아 과거를 가리키지만, 검색은 완전한 자신감으로 그것을 제공합니다. 따라서 인덱스는 한 번만 쓰는 것이 아닙니다. 변경된 문서를 다시 임베딩하는 재인덱싱 경로가 필요하고, 이상적으로는 각 청크에 메타데이터(소스 id, 버전, 타임스탬프)가 있어야 오래된 항목을 무효화하거나 필터링할 수 있습니다. 진실이 얼마나 빨리 변하는지에 따라 갱신 주기를 결정하고, "인덱스가 최신 상태"라는 것을 가정이 아닌 검증하는 무언가로 취급하세요.

Juno벡터가 사는 곳 벡터 저장소는 정확한 선형 스캔을 근사 최근접 이웃 인덱스로 교체합니다. 더 빠르지만 진짜 이웃을 놓칠 수 있으며, 이는 recall@k에 반영됩니다. 임베딩은 코퍼스에 한 번, 쿼리당 한 번이므로, 안정적인 지연 시간은 쿼리 임베딩 더하기 검색 더하기 생성이고, 생성 호출이 보통 지배합니다. 저장소를 탓하기 전에 측정하세요. 장난감이 절대 보여주지 않는 함정은 오래됨입니다. 인덱스는 스냅샷이고 소스는 변화하므로, 재인덱싱 경로를 만들고 버전과 타임스탬프로 청크에 스탬프를 찍어 인덱스가 최신이라고 믿는 대신 확인하세요.

근거 제공과 인용

생성 단계는 프롬프트이며, 프롬프트를 어떻게 작성하느냐가 RAG가 실제로 꾸며낸 답변을 줄이는지를 결정합니다. 두 가지 지시사항이 핵심적인 역할을 합니다. 제공된 컨텍스트에서**** 답하라고 모델에 지시하고, 컨텍스트에 답이 없을 때 그렇게 말하라고 지시하세요.

python
context_text = "\n\n".join(retrieved_chunks)

system_prompt = f'''아래 컨텍스트만을 사용하여 질문에 답하세요.
컨텍스트에 답이 없으면 "해당 정보를 가지고 있지 않습니다."라고 말하세요.
답변에 컨텍스트의 관련 부분을 인용하세요.

컨텍스트:
"""
{context_text}
"""'''

이것은 두 가지 앞선 교훈을 결합합니다. 구분자와 "이것만으로 답하라"는 명시적 지시는 프롬프팅에서 왔습니다. "해당 정보를 가지고 있지 않습니다"라고 말할 수 있는 허가는 환각 방어입니다. 탈출구가 있는 근거 있는 모델은 추측에 맡겨진 모델보다 훨씬 덜 꾸며냅니다. 근거 제공과 탈출구의 조합이 꾸며낸 답변을 줄입니다. 소스를 인용하도록 요청하면 사용자가 확인할 수 있는 무언가도 제공됩니다.

Juno근거 제공과 인용 생성 단계는 프롬프트이며, RAG가 작동하는지를 결정합니다. 제공된 컨텍스트에서만 답하고, 답이 없으면 그렇게 말하도록 모델에 지시하세요. 그 근거 제공과 모른다고 인정할 수 있는 허가가 꾸며낸 답변을 줄이는 것입니다. 소스를 인용하도록 요청하면 사용자가 확인할 무언가를 제공하고, 검색이 빗나갔을 때 단서도 줍니다.

검색이 완벽한 컨텍스트를 모델에 전달해도 모델이 여전히 그것을 무시할 수 있습니다. 생성 프롬프트가 답변을 컨텍스트에 묶는 것이므로, 의도적으로 작성하세요. 제공된 컨텍스트에서**** 답하고, 컨텍스트에 답이 없을 때 인정하며, 사용한 부분을 인용하도록 모델에 지시하세요.

python
context_text = "\n\n".join(f"[{i}] {c}" for i, c in enumerate(retrieved_chunks))

system_prompt = f'''아래 컨텍스트만을 사용하여 답하세요.
컨텍스트에 답이 없으면 정확히 다음과 같이 답하세요: "해당 정보를 가지고 있지 않습니다."
사용한 청크 번호를 [2]처럼 인용하세요.

컨텍스트:
"""
{context_text}
"""'''

청크에 번호를 매기면 "소스를 인용하라"가 프로그래밍 방식으로 확인할 수 있는 인용으로 바뀝니다. 모델이 청크 [2]를 가리키면, 청크 [2]가 실제로 그 주장을 뒷받침하는지 확인할 수 있습니다. 이것이 RAG에서 가장 많이 놓치는 실패 모드, 즉 맞는 답변이지만 잘못된 청크가 검색된 경우에 대한 핸들입니다. 제공된 컨텍스트가 뒷받침하지 않는 그럴듯한 답변을 모델이 생성하는 경우입니다. 인용이 없으면 근거 있는 답변과 운 좋은 추측을 구별할 수 없습니다.

탈출구는 지시만큼 중요합니다. 컨텍스트에서만 답하라고 지시받았지만 실패할 허가가 없는 모델은 빈약한 검색에서도 무언가를 꿰매어 낼 것입니다. 정확한 "해당 정보를 가지고 있지 않습니다" 문자열이라는 명시적인 출구를 제공하면, 검색 실패가 자신감 있는 꾸며낸 내용 대신 깔끔한 공백으로 표면화됩니다. 여기서 메시지 구조는 OpenAI 스타일이며, 시스템-후-컨텍스트 배열은 제공자마다 다르지만 원칙은 동일합니다.

Juno근거 제공과 인용 생성 프롬프트가 답변을 컨텍스트에 묶습니다. 컨텍스트에서만 답하고, 없으면 인정하며, 청크를 인용하세요. 청크에 번호를 매겨 인용을 확인 가능하게 만드세요. 그러면 답변은 맞지만 검색된 청크가 그것을 전혀 뒷받침하지 않는 교묘한 실패를 잡을 수 있습니다. 모델에 정확한 탈출 문자열을 제공하세요. 그렇지 않으면 빈약한 검색이 자신감 있는 잘못된 답변으로 꿰매어집니다. 구조는 제공자마다 다르지만, 시스템-후-컨텍스트 구조는 이어집니다.

근거 제공은 검색과 모델 사이의 계약이며, 여러분이 가진 가장 저렴한 신뢰성 레버이므로 프롬프트를 여기에 쏟으세요. 제공된 컨텍스트에서**** 답하고, 컨텍스트가 답을 뒷받침하지 않을 때 고정된 기권 문자열을 반환하며, 사용한 청크 id를 인용하도록 모델에 지시하세요.

python
context_text = "\n\n".join(f"[{c['id']}] {c['text']}" for c in retrieved)

system_prompt = f'''아래 컨텍스트만을 사용하여 답하세요.
컨텍스트가 답을 뒷받침하지 않으면 정확히 다음과 같이 답하세요: "해당 정보를 가지고 있지 않습니다."
각 주장에 대해 [doc12]처럼 괄호 안에 청크 id를 인용하세요.

컨텍스트:
"""
{context_text}
"""'''

인용은 장식이 아닙니다. 근거 제공을 측정 가능하게 만드는 방법입니다. 출력에 청크 id가 있으면, 인용된 청크가 실제로 그 주장을 포함하는지 자동으로 확인할 수 있습니다. 이는 근거 있는 답변과 유창한 추측을 분리합니다. 이것이 단순한 RAG를 무너뜨리는 실패를 감지하는 방법이기도 합니다. 답변이 그럴듯하게 맞지만 검색된 컨텍스트가 그것을 전혀 뒷받침하지 않아, 검색이 빗나가고 모델이 파라미터 기억으로 공백을 채운 경우입니다. id가 없으면 표본 검사에 의존하고, id가 있으면 평가에서 충실도를 점수로 매기고 수치로 관찰할 수 있습니다.

두 가지 프로덕션 참고사항. 기권 경로는 저렴하고 허가되어야 합니다. 실패할 허가가 없는 모델은 항상 무언가를 생성하며, 빈약한 검색은 깔끔한 공백이 아닌 자신감 있는 잘못된 답변이 됩니다. 그리고 여기서 메시지 구조는 OpenAI SDK 형태입니다. 시스템 대 사용자 분리와 컨텍스트 첨부 방식은 제공자마다 다르므로, 배열은 이식 가능하지만 정확한 필드 이름은 그렇지 않다고 취급하세요. 그 아래의 지속적인 규칙: 모델이 소스가 되게 하지 말고, 인용하고 확인할 수 있는 소스의 표현자가 되게 하세요.

Juno근거 제공과 인용 근거 제공은 계약입니다. 컨텍스트에서만 답하고, 고정 문자열로 기권하며, 청크 id를 인용해 충실도가 눈대중이 아닌 평가에서 점수를 매길 수 있는 무언가가 되게 하세요. 인용은 맞지만 검색된 청크가 그것을 지지하지 않는 답변을 잡는 방법입니다. 검색이 빗나가고 모델이 기억으로 공백을 메운 경우입니다. 기권 경로를 저렴하고 허가되게 유지하세요. 그렇지 않으면 빈약한 검색이 자신감 있는 잘못된 답변이 됩니다. 구조는 제공자마다 다르고, 필드 이름은 이식되지 않습니다.

최근접 이웃을 넘어서: 하이브리드 검색과 재순위 매기기

순수 벡터 검색은 의미를 기반으로 일치시킵니다. 대부분의 경우 이것이 원하는 것입니다. 하지만 맹점이 있습니다. 중요한 정확한 단어를 놓칠 수 있습니다. E_4021 같은 오류 코드나 특정 제품 이름을 검색하면, 의미 기반 검색이 동일한 주제에 관한 청크들을 반환하면서 정확한 문자열을 포함한 청크를 건너뛸 수 있습니다.

해결책에는 이름이 있습니다. 하이브리드 검색: 의미 기반 검색을 구식 키워드 검색과 결합하여 정확한 용어와 일반적인 의미 둘 다 발언권을 갖게 합니다. RAG를 시작하는 데 이것을 구축할 필요는 없습니다. 정확한 용어가 중요하고 순수 벡터가 계속 놓칠 때 이것을 사용하세요.

Juno최근접 이웃을 넘어서: 하이브리드 검색과 재순위 매기기 벡터 검색은 의미를 일치시키는데, 누군가가 정확한 코드나 제품 이름을 검색하면 올바른 청크가 건너뛰어질 때까지는 훌륭합니다. 하이브리드 검색은 일반 키워드 검색을 혼합해 정확한 용어도 발언권을 갖게 합니다. RAG를 하이브리드 없이 시작하고, 정확한 일치 누락이 쌓이는 것을 발견하면 추가할 수 있습니다.

최근접 이웃 벡터 검색은 의미에 기반해 일치시키며, 그것이 또한 약점입니다. 필요한 정확한 토큰, 즉 오류 코드, SKU, 함수 이름, 희귀한 고유 명사를 포함한 청크 위에 주제적으로 가까운 청크를 순위 매길 수 있습니다. 임베딩은 그것들을 "대략 이 의미 영역"으로 흐릿하게 만들고, 정확한 문자열은 흐릿함 속에서 사라집니다.

**하이브리드 검색**은 두 개의 검색기를 실행하고 합칩니다. 의미를 위한 벡터 검색과, 키워드 검색(클래식 용어 매칭, 종종 BM25, 문서가 쿼리의 정확한 단어를 얼마나 많이 포함하고 그 단어가 얼마나 희귀한지를 기준으로 점수를 매김)입니다. 양쪽의 후보를 가져와 순위를 결합하므로, 정확한 용어나 의미적 근접도 중 하나에서 이기는 청크가 표면화될 수 있습니다. 실질적인 이득은 순수 벡터가 실패하는 정확한 쿼리에서의 recall입니다.

두 번째 기법은 **재순위 매기기**입니다. 더 정확하고 더 비싼 모델로 첫 번째 패스 후보를 다시 점수 매기는 두 번째 패스입니다. 패턴은 많이 가져온 다음 좁히는 것입니다. 저렴하게 넉넉한 세트, 예를 들어 상위 20개를 검색한 다음, 각 후보를 쿼리와 함께 읽고 재순위를 매기는 재순위기를 실행하고, 실제로 보낼 상위 4개를 유지합니다. 첫 번째 패스 검색은 빠르고 거칠며, 재순위기는 느리고 정확합니다. 따라서 짧은 목록에서만 실행합니다. 결과는 전체 인덱스에 재순위기 비용을 지불하지 않고도 더 나은 최종 k입니다.

Juno최근접 이웃을 넘어서: 하이브리드 검색과 재순위 매기기 순수 벡터 검색은 임베딩이 의미 영역으로 흐릿하게 만들기 때문에 오류 코드나 SKU 같은 정확한 문자열을 놓칩니다. 하이브리드 검색은 키워드 매칭(BM25)을 추가해 정확한 용어가 의미론 옆에서 발언권을 갖게 합니다. 재순위 매기기는 저렴-후-정밀 패턴입니다. 빠른 검색으로 넓은 세트를 많이 가져온 다음, 느리고 정확한 모델로 짧은 목록을 다시 점수 매기고 최고의 것들을 유지합니다. 비싼 스코어러를 전체에 실행하지 않고도 더 나은 최종 결과를 얻습니다.

순수 근사 최근접 이웃은 강력한 기본값이며 알려진 실패 표면입니다. 의미적 근접도에 최적화되어 있으므로, 쿼리가 정확한 토큰에 달려 있는 경우, 즉 오류 코드, 식별자, SKU, 희귀한 고유 명사일 때 정확히 성능이 저하됩니다. 임베딩이 해당 토큰을 정확한 문자열이 흐릿해지는 의미의 이웃으로 압축하기 때문입니다. 이것은 튜닝 문제가 아닙니다. 밀도 벡터가 하는 일입니다.

**하이브리드 검색**은 밀도 검색(벡터, 의미)을 희소 검색(키워드 용어 매칭, 일반적으로 BM25, 용어 희귀성으로 가중된 정확한 용어 빈도로 점수 매김)과 함께 실행하고 결과 목록을 융합함으로써 이를 해결합니다. 일반적으로 비교 불가능한 원시 점수가 아닌 각 항목의 위치에 의해 두 순위를 결합하는 역순위 융합을 사용합니다. 이득은 순수 밀도가 조용히 떨어뜨리는 정확한 일치 쿼리에서 recall@k로 측정 가능합니다. 비용은 두 번째 인덱스와 융합 단계이므로, 트래픽이 식별자와 희귀 용어를 포함할 때 정확히 효과가 있고, 모두 자연어 패러프레이즈일 때는 덜합니다.

**재순위 매기기**는 다른 누락을 공격합니다. 첫 번째 패스 검색(밀도, 희소, 또는 하이브리드)은 대규모 인덱스에 대한 속도를 위해 구축되어 대략적으로 순위를 매깁니다. 재순위기는 크로스 인코더(사전 계산된 두 벡터를 비교하는 것이 아니라 쿼리와 후보 청크를 함께 읽고 관련성을 공동으로 점수 매기는 모델)입니다. 더 정확하고 전체 코퍼스에 실행하기에는 너무 느립니다. 따라서 저렴한 검색기로 많이 가져오고, 상위 20~50개를 가져온 다음 짧은 목록을 재순위 매기고 최종 소수를 유지합니다.

이것은 대부분의 프로덕션 RAG에서 가장 큰 효과를 내는 recall 수정입니다. 실제로 답변을 뒷받침하는 청크를 단순히 주제적인 것보다 위로 올려 맞는-답변-잘못된-청크 실패를 직접 공격하며, 재순위 단계 전후 recall@k로 이득을 측정합니다. 예산을 잡으세요. 재순위기는 핫 경로에 지연 시간과 쿼리당 비용을 추가하므로, 추상적으로 점수 매길 수 있는 후보 수가 아닌 지연 시간 목표에 맞춰 많이 가져오는 크기를 조정하세요.

Juno최근접 이웃을 넘어서: 하이브리드 검색과 재순위 매기기 밀도 벡터는 정확한 토큰을 흐릿하게 만드므로, 하이브리드 검색은 밀도 옆에 희소 키워드 검색(BM25)을 실행하고 역순위 융합으로 목록을 융합해 식별자와 희귀 용어에서 recall@k를 되찾습니다. 재순위 매기기가 더 큰 이득입니다. 저렴하게 많이 가져온 다음, 쿼리와 청크를 함께 읽는 크로스 인코더로 짧은 목록을 다시 점수 매기면, 답변을 뒷받침하는 청크를 올려 맞는-답변-잘못된-청크를 직접 고칩니다. 전후 recall@k를 측정하고, 재순위기의 지연 시간과 쿼리당 비용을 핫 경로 목표에 맞춰 예산을 잡으세요.

실제 적용

임베딩 장의 searchembed 헬퍼를 재사용한 최소한의 RAG 답변:

python
def answer_from_docs(question, index):
    # 1. 검색: 질문에 가장 잘 맞는 상위 청크들
    query_vector = embed(question)
    scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
    top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]

    # 2. 생성: 해당 청크들만을 사용하여 답변
    context = "\n\n".join(t["text"] for t in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": f'이 컨텍스트만을 사용하여 답하세요. 여기 없으면 모른다고 말하세요.\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

가장 가까운 세 개의 청크를 검색하고, 프롬프트에 넣고, 모델이 그것으로 답하게 합니다. 이것이 전체 **RAG 파이프라인**입니다. 임베딩 검색이 모델 호출에 공급되고, 근거 있는 프롬프트가 그것을 하나로 묶습니다. 품질의 거의 전부는 올바른 청크를 검색하는 데 있습니다.

Juno실제 적용 최소한의 RAG 답변은 임베딩 검색이 모델 호출에 공급되는 것입니다. 가장 가까운 청크를 검색하고, 프롬프트에 넣고, 모델에 그것만으로 답하도록 요청합니다. 검색이 사실을 제공하고 근거 있는 프롬프트가 답변을 그것에 고정시킵니다. 품질의 거의 전부는 올바른 청크를 검색하는 데 있으므로, 거기에 시간을 쓰세요.

작동하는 RAG 호출은 검색, 근거 제공, 생성이며, 앞서 나온 인용 핸들이 연결됩니다.

python
def answer_from_docs(question, index, k=4):
    top = retrieve(question, index, k)  # 패턴 섹션에서 가져옴
    context = "\n\n".join(f"[{i}] {t['text']}" for i, t in enumerate(top))
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'이 컨텍스트만을 사용하여 답하세요. 여기 없으면 '
                f'"해당 정보를 가지고 있지 않습니다."라고 답하세요. [1]처럼 청크를 인용하세요.\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

전체는 임베딩 검색이 모델 호출에 공급되고 근거 있는 프롬프트로 묶인 것입니다. chat.completions 형태는 OpenAI 스타일이며, 메시지 역할과 호출 서명은 제공자마다 다르지만, 검색-후-근거 제공-후-생성 흐름은 어디서나 동일합니다.

답변이 잘못되었을 때는 **단계 순서대로 디버그**하세요. 먼저 검색된 청크를 확인하세요. 관련 텍스트가 top에 들어왔나요? 그렇지 않다면 수정은 검색에 있습니다. k를 높이거나, 청킹을 수정하거나, 하이브리드나 재순위 매기기를 추가하세요. 프롬프트에 있지 않습니다. 올바른 청크가 검색되었는데도 답변이 여전히 잘못되었다면, 그때 프롬프트 문제입니다. 이 순서는 수렴하지 않는 검색 버그에 맞서 프롬프트 문구를 튜닝하는 것을 방지합니다.

Juno실제 적용 작동하는 RAG 호출은 검색, 인용으로 근거 제공, 생성이며, OpenAI 스타일 메시지 형태는 필드 이름이 아닌 구조로 다른 제공자에게 이식됩니다. 고정된 순서로 디버그하세요. 먼저 검색된 청크를 보세요. 올바른 텍스트가 들어오지 않았다면 검색(k, 청킹, 하이브리드, 재순위)을 수정하고 프롬프트는 건드리지 마세요. 올바른 청크가 컨텍스트에 있을 때만 잘못된 답변이 프롬프트 문제입니다.

실제로 검색-재순위-생성 파이프라인은 하나의 함수이며, 계측하는 부분은 단계 사이의 접점입니다.

python
def answer_from_docs(question, index, k_fetch=20, k_final=4):
    candidates = retrieve(question, index, k=k_fetch)   # 밀도 (또는 하이브리드)
    top = rerank(question, candidates)[:k_final]        # 크로스 인코더 패스
    context = "\n\n".join(f"[{c['id']}] {c['text']}" for c in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'이 컨텍스트만을 사용하여 답하세요. 뒷받침되지 않으면 '
                f'"해당 정보를 가지고 있지 않습니다."라고 답하세요. [doc12]처럼 id를 인용하세요.\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content, [c["id"] for c in top]

답변과 함께 청크 id를 반환하는 것이 시스템을 관찰 가능하게 만드는 핵심입니다. 검색된 것, 인용된 것, 인용된 id가 실제로 답변을 뒷받침하는지를 기록하면, 평가에서 충실도 신호가 됩니다. chat.completions 형태는 OpenAI SDK이며, 흐름은 이식 가능하고 필드 이름은 제공자별로 다르게 취급하세요.

이 파이프라인의 지연 시간과 비용 지도는 최적화가 시간이 있는 곳으로 가기 때문에 머릿속에 담아 둘 가치가 있습니다.

  • 쿼리 임베딩은 고정된 작은 세금입니다.
  • 첫 번째 패스 검색은 저렴합니다.
  • 재순위 매기기는 많이 가져오는 크기에 비례하는 실제 쿼리당 비용을 추가합니다.
  • 생성 호출이 지연 시간과 달러 비용 모두를 지배합니다.

따라서 더 빠른 벡터 저장소를 찾기 전에 검색이 병목이었는지 확인하세요. 보통 생성이었습니다.

그리고 단계 순서대로 실패를 디버그하세요. 검색된 세트, 재순위 매겨진 세트, 인용, 프롬프트 순으로. 올바른 청크가 candidates에 들어오지 않은 잘못된 답변은 검색 버그이며, 어떤 프롬프트 변경도 결코 없었던 청크를 수정하지 못합니다.

Juno실제 적용 파이프라인은 검색, 재순위, 근거 제공, 생성이며, 답변 옆에 청크 id를 반환하는 것이 관찰 가능하게 만드는 것입니다. 검색된 것 대 인용된 것을 기록하면 추측이 아닌 평가에서 충실도를 점수 매길 수 있습니다. 비용 지도를 보세요. 생성이 지연 시간과 비용을 지배하므로, 벡터 저장소를 교체하기 전에 검색이 병목이었는지 확인하세요. 단계 순서대로 디버그하세요. 검색된, 재순위 매겨진, 인용된, 프롬프트 순으로. 후보 세트에 들어오지 않은 청크를 어떤 프롬프트도 수정하지 못합니다. 형태는 OpenAI SDK이며, 흐름은 이식되고 필드 이름은 그렇지 않습니다.

RAG가 필요 없는 경우

RAG는 무료가 아니며 항상 답은 아닙니다. 다음과 같은 경우 건너뛰세요.

  • 데이터가 작고 프롬프트에 맞을 때. 전체 지식 기반이 몇 문단이라면 직접 붙여넣으세요. RAG는 한 번에 모두 보내기에 너무 많을 때를 위한 것입니다.
  • 긴 컨텍스트 모델이 모두 담을 수 있을 때. 일부 모델은 매우 큰 입력을 받아들여 전체 문서를 한 번에 처리할 수 있습니다. 검색을 구축하는 것보다 덜 작업일 수 있습니다.

그리고 실제 한계를 기억하세요. RAG 품질은 대부분 검색 품질입니다. 검색 단계가 잘못된 청크를 반환하면 완벽한 프롬프트도 답변을 구할 수 없습니다. 실제 RAG 시스템의 대부분의 노력은 최종 모델 호출이 아닌 올바른 텍스트를 검색하는 데 들어갑니다.

임베딩과 RAG는 모델 자체를 변경하지 않고 질문 순간에 모델에 지식을 제공합니다. 다음 장인 파인 튜닝은 다른 옵션을 다룹니다. 여러분 자신의 예제로 모델을 변경하는 것과 그것이 프롬프팅이나 RAG보다 가치 있는 경우입니다.

JunoRAG가 필요 없는 경우 데이터가 프롬프트에 붙여넣을 만큼 작거나 긴 컨텍스트 모델이 한 번에 모두 담을 수 있을 때 RAG를 건너뛰세요. 그리고 실제 한계를 염두에 두세요. RAG 품질은 대부분 검색 품질이므로, 검색이 잘못된 청크를 반환하면 어떤 프롬프트도 답변을 구할 수 없습니다. 실제 작업의 대부분은 최종 호출이 아닌 올바른 텍스트를 검색하는 데 있습니다.

RAG는 **인프라**입니다. 인덱스, 임베딩 단계, 검색 단계, 재인덱싱 경로이며, 데이터가 보내기에 너무 크고 재훈련이 옵션이 아닐 만큼 자주 변할 때만 그 오버헤드를 정당화합니다. 더 저렴한 형태가 충분할 때 건너뛰세요.

  • 코퍼스가 프롬프트에 맞을 때. 몇 문단이나 작은 정책 문서는 직접 컨텍스트에 넣으세요. RAG는 한 번에 보낼 수 있는 것보다 더 많을 때만 그 가치를 합니다.
  • 긴 컨텍스트 모델이 전체 소스를 담을 때. 문서가 큰 창에 맞는다면 전체를 보내는 것이 구축하고 튜닝해야 하는 검색 파이프라인을 이길 수 있습니다. 하지만 모든 호출에 그 토큰 비용을 지불하고 여전히 중간 부분을 잃습니다.

핵심 사실을 기억하세요. RAG 품질은 검색 품질에 의해 제한됩니다. 잘못된 청크에 대한 완벽한 프롬프트는 여전히 잘못 답합니다. 따라서 인덱스와 검색 단계가 작업과 이득이 있는 곳이며, 최종 모델 호출이 아닙니다. 사실을 제공하는 것이 아닌 동작을 변경해야 할 때 파인 튜닝이 다음 장이며, RAG와는 다른 문제를 해결합니다.

JunoRAG가 필요 없는 경우 RAG는 실제 오버헤드가 있는 인프라이므로, 코퍼스가 프롬프트에 맞거나 긴 컨텍스트 모델이 전체 소스를 삼킬 수 있을 때 건너뛰세요. 긴 컨텍스트 경로는 모든 호출에 토큰을 지불하고 중간에서 여전히 흐릿해진다는 것을 기억하세요. 핵심 사실은 변함없습니다. 품질은 검색에 의해 제한되므로, 잘못된 청크에 대한 완벽한 프롬프트는 여전히 잘못 답합니다. 최종 호출이 아닌 인덱스와 검색에 투자하세요.

RAG는 구축하고 튜닝하고 모니터링하고 재인덱싱하는 검색 시스템의 가격으로 최신성과 근거 제공을 제공합니다. 따라서 결정은 기본값이 아닌 비용-편익 분석입니다. 더 가벼운 형태가 기준을 충족할 때 건너뛰세요.

  • 작고 안정적인 코퍼스. 프롬프트에 맞고 거의 변하지 않는다면 컨텍스트에 넣고, 반복 호출에서 고정 블록이 저렴하게 제공되도록 프롬프트 캐싱에 기대세요. 여기서 검색 파이프라인은 이득 없는 오버헤드입니다.
  • 긴 컨텍스트, 적은 재사용. 소스가 큰 창에 맞는다면 전체를 보내면 구축을 건너뛸 수 있습니다. 하지만 모든 요청에 그 입력 토큰 비용을 지불하고 recall은 여전히 중간에서 약화되므로, 볼륨이나 문서 크기가 오르면 RAG에 집니다.

전체 장을 관통하는 지배적 제약은 동일합니다. 종단간 품질은 recall@k에 의해 제한됩니다. 어떤 프롬프트도, 어떤 모델 업그레이드도, 어떤 재순위도 검색이 컨텍스트에 넣지 않은 사실을 복구하지 못합니다. 따라서 엔지니어링 투자는 검색, 청킹, 하이브리드, 재순위, 평가, 재인덱싱에 들어가고, 모델 호출은 올바르게 하기 위한 저렴한 부분입니다. 그리고 RAG와 파인 튜닝을 **별도의 영역**에 유지하세요. 검색은 무엇이 사실인지를 제공하고 소스가 변함에 따라 최신 상태를 유지하는 반면, 파인 튜닝은 모델이 응답하는 방식을 형성하고 사실을 주입하는 낡고 소스 없는 방법입니다. 지식을 위해서는 검색을 사용하고, 형식을 위해서는 파인 튜닝을 사용하며, 어느 쪽도 다른 쪽의 일을 하도록 요청하지 마세요.

JunoRAG가 필요 없는 경우 RAG는 구축하고 재인덱싱하는 시스템의 가격으로 최신성과 근거 제공을 제공하므로, 코퍼스가 작고 안정적일 때(붙여넣고 프롬프트 캐싱에 기대세요) 또는 적은 재사용으로 긴 창에 맞을 때 건너뛰세요. 모든 것을 관통하는 제약: 어떤 것도 검색이 표면화하지 않은 사실을 복구하지 못하므로 모델 호출이 아닌 검색에 투자하세요. 영역을 깔끔하게 유지하세요. 무엇이 사실인지를 위한 검색, 어떻게 들리는지를 위한 파인 튜닝, 그리고 절대 하나에 다른 쪽의 일을 요청하지 마세요.