RAG 파이프라인에 대한 검색 패턴 구현

Completed

Tip

자세한 내용은 텍스트 및 이미지 탭을 참조하세요.

RAG(검색 보강 생성)는 데이터에서 관련 컨텍스트를 제공하여 언어 모델 응답을 향상시킵니다. 검색기 구성 요소는 기술 자료를 검색하고 LLM이 정확하고 근거가 있는 답변을 생성하는 데 사용하는 문서를 반환합니다. pgvector를 사용하는 Azure Database for PostgreSQL은 문서 임베딩을 저장하고 생성 단계에 활용되는 유사성 쿼리를 실행하는 효과적인 검색 도구 역할을 합니다.

이 단원에서는 RAG 워크로드에 대한 스키마 디자인, 청크 검색 쿼리 구현, 문서 수집 파이프라인 빌드, 인용 메타데이터 반환 및 검색 품질 평가에 대해 설명합니다.

RAG 아키텍처 및 검색기 역할 이해

RAG 시스템은 3단계 패턴을 따릅니다.

  1. 쿼리 포함: 포함 모델을 사용하여 사용자의 질문을 벡터로 변환
  2. 검색: 쿼리 포함과 유사한 문서를 벡터 저장소 검색
  3. 세대: 검색된 문서를 LLM에 컨텍스트로 전달하여 응답을 생성합니다.

PostgreSQL은 이 아키텍처에서 검색기 역할을 합니다. 검색 품질은 세대 품질에 직접적인 영향을 줍니다. 검색기가 관련 없는 문서를 반환하는 경우 LLM은 올바르게 응답하는 데 필요한 컨텍스트가 부족하고 잘못된 정보를 생성할 수 있습니다. 검색기가 관련 문서를 누락하면 답변이 불완전할 수 있습니다.

법률 연구 보조의 경우 효과적인 검색은 변호사의 질문을 다루는 사건 판례, 법령 및 계약 조항을 찾는 것을 의미합니다. 그런 다음 LLM은 이러한 소스를 인용과 함께 일관된 답변으로 합성합니다.

RAG 워크로드에 대한 스키마 디자인

RAG 애플리케이션은 일반적으로 전체 문서가 아닌 문서 청크에서 작동합니다. 긴 문서는 LLM 컨텍스트 제한을 초과하며 특정 쿼리와 관련이 없는 섹션을 포함할 수 있습니다. 청크 분할은 문서를 독립적으로 검색할 수 있는 더 작고 집중된 세그먼트로 분할합니다.

문서 및 청크 구분

두 개의 테이블을 사용합니다. 하나는 소스 문서용이고, 다른 하나는 임베딩된 청크에 사용합니다.

CREATE TABLE source_documents (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    source_url TEXT,
    document_type TEXT,
    ingested_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE document_chunks (
    id SERIAL PRIMARY KEY,
    document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    token_count INTEGER,
    start_char INTEGER,
    end_char INTEGER,
    UNIQUE (document_id, chunk_index)
);

이 디자인은 다음과 같은 몇 가지 이점을 제공합니다.

  • 유연한 검색: 개별 청크를 검색하거나 문서 섹션을 재구성할 수 있습니다.
  • 원본 추적: 모든 청크는 인용을 위해 원본으로 다시 연결됩니다.
  • 효율적인 스토리지: 문서 메타데이터는 청크당 중복되지 않고 한 번 저장됩니다.
  • 쉬운 업데이트: 문서를 교체하면 해당 문서의 청크가 삭제되고 다시 수집됩니다.

필터링 및 컨텍스트에 대한 메타데이터 추가

필터링 및 컨텍스트 재구성에 도움이 되는 메타데이터를 포함합니다.

CREATE TABLE document_chunks (
    id SERIAL PRIMARY KEY,
    document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    token_count INTEGER,
    section_title TEXT,
    page_number INTEGER,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

섹션 제목 및 페이지 번호는 사용자가 검색된 정보를 확인하고 LLM이 각 청크의 컨텍스트를 이해하는 데 도움이 됩니다.

RAG 쿼리 패턴에 대한 인덱스 만들기

벡터 유사성 인덱스(HNSW 또는 IVFFlat)는 이 모듈의 앞부분에서 설명한 대로 빠른 포함 조회를 가능하게 합니다. RAG 워크로드는 청크 검색에 고유한 조인 및 필터를 가속화하는 B-트리 인덱스의 이점을 활용합니다. 청크를 검색할 때 자주 인용 메타데이터를 위해 원본 문서에 다시 조인하고 문서 내 청크 위치별로 필터링하거나 정렬합니다. 이러한 인덱스가 없으면 PostgreSQL은 모든 검색 쿼리에 대해 전체 청크 테이블을 검색합니다.

-- B-tree index for document lookups (speeds up JOINs to source_documents)
CREATE INDEX document_chunks_document_id_idx
ON document_chunks (document_id);

-- Composite index for chunk ordering (supports context window queries)
CREATE INDEX document_chunks_doc_chunk_idx
ON document_chunks (document_id, chunk_index);

(document_id, chunk_index)의 복합 인덱스는 주변 컨텍스트를 제공하기 위해 인접한 청크를 가져오는 컨텍스트 창 검색에서 특히 중요합니다. 이 항목이 없으면 PostgreSQL은 문서의 모든 청크를 스캔하여 인접 항목을 찾아야 합니다.

컨텍스트 빌드를 위한 청크 검색 구현

RAG 쿼리는 LLM의 컨텍스트가 되는 청크를 검색합니다. 선택한 검색 패턴은 응답 품질과 토큰 효율성 모두에 영향을 줍니다. 세 가지 경쟁 관심사인 충분한 관련 콘텐츠 검색, 토큰 제한 내에 유지, LLM이 각 청크를 이해할 수 있는 충분한 컨텍스트 제공의 균형을 유지해야 합니다.

가장 간단한 방법은 상위 k개의 가장 유사한 청크를 직접 검색합니다. 이는 청크가 자체적으로 완결되고 질문이 각각의 청크와 잘 맞아떨어질 때 잘 작동합니다. 그러나 법률 문서, 기술 사양 및 내러티브 콘텐츠는 종종 여러 단락에 걸쳐 개념을 분산합니다. 청크는 참조된 텍스트를 포함하지 않고 "앞서 언급한 조건" 또는 "위에서 설명한 대로"를 참조할 수 있습니다.

이러한 경우 컨텍스트 창 검색은 각 일치 항목과 함께 인접한 청크를 가져옵니다. 청크 47이 쿼리와 가장 유사한 경우 청크 46 및 48도 검색합니다. 이렇게 하면 토큰 사용량이 증가하지만 불완전한 답변의 위험이 줄어듭니다. 장단점은 콘텐츠 구조에 따라 달라집니다. 명확한 구역 경계가 있는 고도로 구조화된 문서에는 설명 텍스트가 흐르는 것보다 주변 컨텍스트가 덜 필요합니다.

토큰 제한은 다른 제약 조건을 추가합니다. LLM에는 고정 컨텍스트 창이 있으며 시스템 프롬프트, 사용자 질문 및 생성된 응답을 위한 공간이 필요합니다. 각각 평균 500개의 토큰으로 10개의 청크를 가져오는 경우, LLM이 단어를 하나도 쓰기 전에 5,000개의 토큰을 사용하게 됩니다. 검색하는 동안 누적 토큰 수를 추적하여 예산 내에서 유지합니다.

다음 쿼리는 이러한 패턴을 결합합니다. 인접 항목과 가장 유사한 청크를 검색하고 누적 토큰을 추적하며 인용 메타데이터를 포함합니다.

WITH matched_chunks AS (
    -- Find the most similar chunks
    SELECT id, document_id, chunk_index, embedding <=> $1 AS distance
    FROM document_chunks
    ORDER BY embedding <=> $1
    LIMIT 3
),
context_window AS (
    -- Expand to include adjacent chunks for context
    SELECT DISTINCT dc.id, dc.document_id, dc.chunk_index, dc.content,
           dc.token_count, mc.distance
    FROM matched_chunks mc
    JOIN document_chunks dc ON dc.document_id = mc.document_id
        AND dc.chunk_index BETWEEN mc.chunk_index - 1 AND mc.chunk_index + 1
),
token_limited AS (
    -- Track cumulative tokens to respect LLM context limits
    SELECT cw.*, sd.title AS source_title, sd.source_url,
           SUM(cw.token_count) OVER (ORDER BY cw.distance, cw.chunk_index) AS cumulative_tokens
    FROM context_window cw
    JOIN source_documents sd ON cw.document_id = sd.id
)
SELECT id, content, source_title, source_url, distance
FROM token_limited
WHERE cumulative_tokens <= 3000
ORDER BY distance, chunk_index;

사용 사례에 따라 매개 변수를 조정합니다. 특정 조항 질문에 대답하는 법률 연구 도우미의 경우 컨텍스트 창이 없는 5개의 일치 항목을 검색할 수 있습니다. 제품 설명서에 대한 개방형 질문에 답변하는 고객 지원 봇의 경우 각각 두 개의 주변 컨텍스트 청크가 있는 세 개의 일치 항목을 검색할 수 있습니다.

문서 수집 파이프라인 처리

새 문서를 검색하려면 먼저 처리해야 합니다. 수집 파이프라인은 문서를 청크로 분할하고, 임베딩을 생성하며, 모든 데이터를 PostgreSQL에 저장합니다.

문서를 분할하는 방법은 검색 품질에 직접적인 영향을 줍니다. 올바른 청크 전략은 콘텐츠 구조 및 쿼리 패턴에 따라 달라집니다.

  • 고정 크기 청크: 모든 N자 또는 토큰을 분할합니다. 이 방법은 구현하기 쉽고 예측 가능한 토큰 수를 생성하지만 문장이나 단락을 중간에 잘라낼 수 있습니다. 콘텐츠에 명확한 구조적 경계가 없거나 토큰 예산 계획에 일관된 청크 크기가 필요한 경우 고정 크기 청크를 사용합니다.

  • 의미 체계 청크: 단락, 절, 문장 같은 자연스러운 경계로 분할합니다. 이렇게 하면 각 청크 내에서 의미를 유지하지만 가변 크기가 생성됩니다. 섹션이 법률 조항, API 설명서 또는 FAQ 항목과 같은 완전한 생각을 나타내는 구조화된 문서에 의미 체계 청크를 사용합니다.

  • 겹치는 청크: 경계에서 컨텍스트를 유지하기 위해 인접한 청크의 텍스트를 포함합니다. 예를 들어 1,000자 청크 사이에 200자 겹치는 경우 청크 경계를 가로지르는 개념이 하나 이상의 전체 청크에 표시됩니다. 쿼리가 청크 경계 근처의 콘텐츠와 일치할 수 있는 경우 겹침을 사용합니다.

법률 연구 조교 시나리오에서는 법률 텍스트가 개별 절과 논증으로 구성되어 있기 때문에 단락이나 구획 경계에서의 의미 단위화가 잘 작동합니다. 각 청크는 LLM의 컨텍스트에서 단독으로 사용할 수 있는 완전한 법적 개념을 나타냅니다.

문서를 분할하여 청크로 나누고 임베딩을 생성한 후, 배치 작업을 사용하여 효율적으로 삽입합니다. 두 테이블 디자인에서는 먼저 원본 문서를 삽입하여 ID를 가져온 다음 모든 청크를 단일 다중 행 INSERT에 삽입해야 합니다. 이 방법은 데이터베이스에 대한 왕복을 최소화하고 문서-청크 관계를 그대로 유지합니다. 대부분의 포함 API는 요청당 여러 텍스트를 허용하므로 삽입하기 전에 하나의 API 호출에서 전체 문서의 청크에 대한 포함을 생성할 수 있습니다.

문서 업데이트에 대한 결정이 필요합니다. 문서의 버전을 지정하거나 대체하시겠습니까? 대부분의 RAG 애플리케이션의 경우 교체가 더 간단합니다. 기존 청크를 삭제한 다음( ON DELETE CASCADE 원본 문서를 삭제할 때 제약 조건이 이를 자동으로 처리함) 업데이트된 콘텐츠를 다시 수집합니다. 이 방법을 사용하면 검색 결과가 항상 현재 문서 상태를 반영합니다. 버전 기록이 필요한 경우 열을 version 추가하고 source_documents 이전 청크를 새 청크와 함께 유지하여 쿼리 시 버전별로 필터링합니다.

HNSW 인덱스는 다시 빌드하지 않고 삭제를 정상적으로 처리합니다. IVFFlat 인덱스는 중요한 변경 후 조각화를 누적할 수 있으므로 쿼리 성능을 유지하기 위해 주기적인 다시 작성이 필요합니다.

인용을 사용하여 검색 구현

인용은 RAG를 블랙박스에서 투명한 연구 도구로 변환합니다. 법률 연구 보조가 계약 조항을 인용할 때, 변호사는 그 문구를 원래 문서와 대조하여 확인해야 합니다. 인용 메타데이터가 없으면 사용자는 LLM의 출력을 맹목적으로 신뢰해야 합니다. 이는 정확도가 중요한 법적, 의료적 또는 재무적 맥락에서 중대한 책임입니다.

효과적인 인용에는 청크 콘텐츠 이상이 필요합니다. 검색 쿼리는 사용 가능한 경우 원본 문서 제목, URL 또는 문서 ID, 섹션 제목 및 페이지 번호를 반환해야 합니다. 이 메타데이터를 사용하면 사용자가 실제로 원본으로 돌아갈 수 있는 애플리케이션 형식 인용을 사용할 수 있습니다. 또한 거리 점수는 높은 신뢰도의 인용을 눈에 띄게 표시하도록 도와주며, 신뢰도가 낮은 일치 항목은 사용자 확인을 위해 플래그를 지정합니다.

동일한 문서의 여러 청크가 쿼리와 일치하는 경우 일반적인 문제가 발생합니다. "고용 계약 템플릿"의 청크 12, 15 및 18이 모두 상위 결과에 표시되면 세 개의 개별 인용으로 나열하면 응답이 복잡합니다. 대신 원본 문서별로 청크를 그룹화하고 여러 관련 발췌 내용이 포함된 단일 인용으로 표시합니다. 이 방법은 더 깨끗한 출력을 생성하고 사용자가 각 소스에서 전체 컨텍스트를 볼 수 있도록 도와줍니다.

다음 쿼리는 문서 그룹화된 검색을 보여 줍니다. 각 문서 내의 청크 순위를 지정하고, 컨텍스트를 압도하지 않도록 문서당 3개의 청크로 제한하고, 더 깨끗한 인용 서식을 위해 콘텐츠를 집계합니다.

WITH ranked_chunks AS (
    SELECT
        dc.*,
        sd.title AS document_title,
        sd.source_url,
        dc.embedding <=> $1 AS distance,
        ROW_NUMBER() OVER (PARTITION BY dc.document_id ORDER BY dc.embedding <=> $1) AS rank_in_doc
    FROM document_chunks dc
    JOIN source_documents sd ON dc.document_id = sd.id
    WHERE dc.embedding <=> $1 < 0.5
)
SELECT
    document_id,
    document_title,
    source_url,
    array_agg(content ORDER BY chunk_index) AS chunks,
    MIN(distance) AS best_distance
FROM ranked_chunks
WHERE rank_in_doc <= 3
GROUP BY document_id, document_title, source_url
ORDER BY best_distance
LIMIT 5;

그러면 애플리케이션에서 사용자 친화적인 인용으로 형식을 지정할 수 있습니다.

"고용주는 30일 전에 본 계약을 해지할 수 있습니다." — 고용계약 서식 파일, 섹션 4.2, 3페이지

검색 품질 평가 및 개선

검색 품질은 RAG 효과를 결정합니다. LLM이 얼마나 유능한지에 관계없이 검색이 부족하면 생성이 저하됩니다. 리트리버가 관련 없는 청크를 반환하는 경우 LLM은 쓸모없는 텍스트에 컨텍스트 창 용량을 낭비합니다. 리트리버가 관련된 청크를 누락하면 LLM이 올바르게 응답하는 데 필요한 정보가 부족합니다. 생성 품질과 별도로 검색 품질을 측정하면 RAG 파이프라인에 개선이 필요한 위치를 진단할 수 있습니다.

세 가지 메트릭은 검색 성능의 다양한 측면을 캡처합니다.

  • 정밀도: 실제로 관련된 검색된 청크의 비율입니다. 정밀도가 낮으면 LLM이 관련 없는 컨텍스트를 수신하여 혼동하거나 잘못된 정보를 생성할 수 있습니다. 거리 임계값을 강화하거나 검색된 청크 수를 줄여 정밀도를 향상시킵니다.

  • 재현율: 검색된 관련 청크의 비율입니다. 회수량이 적다는 것은 LLM이 중요한 정보를 누락하여 불완전하거나 잘못된 답변을 초래한다는 것을 의미합니다. 거리 임계값을 완화하거나 검색된 청크 수를 늘리거나 인덱스 매개 변수를 조정하여 회수를 개선할 수 있습니다 ef_search.

  • MRR(평균 상호 순위): 순위가 지정된 목록에서 첫 번째 관련 결과가 얼마나 높은 순위로 나타나는지를 나타냅니다. MRR은 LLM이 이전 컨텍스트에 더 많은 가중치를 두고, 인용을 스캔하는 사용자가 상위 결과를 첫 번째로 주목하기 때문에 중요합니다. 포함 모델 또는 쿼리 전처리를 구체화하여 MRR을 개선합니다.

이러한 메트릭을 측정하려면 평가 데이터 세트( 관련 청크에 대한 인간의 판단과 쌍을 이루는 대표 쿼리 집합)가 필요합니다. 이 데이터 세트를 빌드하려면 먼저 노력해야 합니다. 즉, 사용자가 샘플 쿼리를 실행하고 결과에 레이블을 지정해야 하지만 추측하는 대신 데이터 기반 개선을 수행하여 비용을 절감할 수 있습니다. 사용자가 실제로 묻는 질문 유형을 나타내는 20~50개의 쿼리로 시작합니다. 각 쿼리에 대해 상위 10~20개 청크를 검색하고 도메인 전문가가 해당 관련성을 간단한 규모로 평가합니다(관련이 없으며 관련성이 높고 관련성이 높음).

이 평가가 설정된 상태에서 다른 컷오프(precision@5, recall@10)에서 정밀도 및 재현율을 측정하고 파이프라인 변경이 이러한 메트릭에 미치는 영향을 추적할 수 있습니다. 평가 집합에 대해 검색 쿼리를 실행하고, 결과를 사람의 판단과 비교하고, 메트릭을 계산합니다. 대부분의 팀은 청크 전략, 모델 포함 또는 인덱스 매개 변수를 변경할 때마다 실행되는 점수 매기기 스크립트로 자동화합니다.

메트릭이 목표값 이하로 떨어질 경우, 정밀도와 재현율 간의 상충 관계에 영향을 미치는 매개 변수를 체계적으로 실험합니다.

  • 청크 크기: 청크가 작을수록 더 집중된 콘텐츠를 반환하여 정밀도가 향상되지만 여러 청크에서 관련 정보를 조각화하여 회수에 해를 끼칠 수 있습니다. 청크가 클수록 재현율이 개선되지만 관련성이 희석됩니다.

  • 청크 겹침: 더 많은 중복은 경계를 넘어 컨텍스트를 유지하므로 청크 가장자리 근처의 콘텐츠와 일치하는 쿼리를 지원합니다. 그러나 중복은 스토리지 요구 사항을 증가시키고 중복 콘텐츠를 반환할 수 있습니다.

  • 포함 모델: 다른 모델은 서로 다른 의미 체계 관계를 캡처합니다. 법률 텍스트에 대해 학습된 모델은 법률 연구를 위한 범용 모델을 능가할 수 있습니다. 일반 모델이 저조한 경우 도메인별 또는 미세 조정된 모델을 고려합니다.

  • 인덱스 매개 변수: 더 높은 ef_search (HNSW) 또는 probes (IVFFlat)은 쿼리 대기 시간의 비용으로 더 많은 후보를 검색하여 회수를 향상시킵니다. 기본 매개 변수로 시작하고 회수가 부족한 경우에만 증가합니다.

  • 거리 임계값: 임계값이 더 엄격해지면 한계 일치 항목을 제외하여 정밀도가 향상됩니다. 더 느슨한 임계값은 더 많은 후보를 포함하여 회수를 향상시킵니다. 평가 데이터 세트를 사용하여 사용 사례에 대한 두 메트릭의 균형을 맞추는 임계값을 찾습니다.