파일은 하나인데 답변의 근거는 왜 달라질까: 버전과 처리 회차를 분리하기

읽는 데 12분
목차

문서를 새로 올린 직후 검색 처리에 실패했다고 하자. 화면에는 새 파일이 보이는데 질문에 대한 답변은 이전 파일의 문장을 인용한다면, 시스템은 무엇을 잘못한 걸까? 벡터 검색의 정확도만으로 설명할 수 없다. 업로드된 원본, 변환 중인 산출물, 검색 가능한 색인, 답변에 붙은 인용이 서로 다른 시점의 파일을 가리킬 수 있기 때문이다.

여기서는 여러 단계로 이어지는 문서 처리 파이프라인의 식별 문제를 다룬다. 입력은 원본 파일에서 청크, 임베딩, 벡터로 변환되고 검색 결과가 답변의 근거가 된다. 각 단계는 비동기로 움직이고 재시도도 한다. 그래서 모든 단계를 하나의 요청처럼 묶는 대신, 각 단계가 어떤 원본의 어떤 처리 결과를 다루는지 검증해야 한다.

파일 버전과 처리 회차는 다른 숫자다

논리 파일에는 변하지 않는 파일 ID가 있다. 새 원본을 확정할 때마다 내용 버전이 증가한다. 반면 처리 회차는 그 원본에서 검색 자료를 만드는 한 번의 실행이다. 같은 2번 버전의 임베딩을 재시도해도 내용은 여전히 2번 버전이며 처리 회차만 달라진다.

각 단계가 전달할 식별자는 (출처, 파일 ID, 내용 버전, 처리 회차)다. 빠른 처리와 정밀 처리처럼 여러 경로가 있으면 lane도 붙인다.

식별자답하는 질문
출처·파일 ID어느 파일인가?
내용 버전어떤 원본 바이트인가?
처리 회차그 원본을 처리한 어느 실행인가?
lane빠른 처리와 정밀 처리 중 어느 경로인가?

이 구분이 없으면 오래 걸린 1번 버전의 작업이 뒤늦게 끝나 2번 버전의 색인을 덮거나, 새 버전의 처리 실패를 과거 버전의 검색 성공으로 가릴 수 있다. 과거 인용을 다시 열 때도 파일 ID만 조회하면 새 원본이 열리는 문제가 생긴다.

실패 시점에 따라 검색 상태가 달라진다

실패 시점원본 상태기본 검색
새 원본 확정 전기존 최신 버전 유지기존 버전 기준 유지
새 원본 확정 후 파싱·임베딩 실패새 원본과 실패 상태 보존해당 파일 제외

새 버전의 처리가 실패했다고 과거 버전을 현재 근거처럼 대신 내놓지 않는다. 저장된 과거 인용은 현재 권한을 다시 확인한 뒤 해당 버전으로만 연다. 이 전이를 누가 확정하고 공개할지는 원본 메타데이터를 소유한 서비스가 결정해야 한다.

그림 크게 보기: RAG 파이프라인의 버전 식별자

바이트와 영수증으로 처리 결과를 묶는다

비동기 이벤트에 파일 ID와 버전만 적는 것으로는 부족하다. 객체 저장소에서 실제로 읽은 바이트가 이벤트가 가리킨 산출물과 같은지 확인해야 한다. 신뢰할 수 있는 처리 계약에서 받은 입력 산출물의 SHA-256을 이벤트에 담고, 소비자는 다운로드한 바이트의 해시와 메타데이터의 출처·버전·회차·lane을 모델 호출이나 저장 전에 대조한다. JSON을 다시 직렬화해서 만든 해시가 아니라 다운로드한 실제 바이트의 해시를 비교한다.

해시가 같다는 것은 바이트 일치의 근거다. 읽을 권한이나 이벤트 발행자의 신원까지 증명하지는 않는다. 권한과 출처 검증은 별도로 유지한다.

처리가 성공하면 출력 위치와 출력 바이트 해시를 처리 회차의 영수증에 묶는다. 응답만 유실된 재시도에서는 그 영수증이 가리키는 결과를 다시 읽어 검증한 후 완료 이벤트만 재전달할 수 있다. 원본을 무조건 다시 처리하거나 벡터를 다시 삽입하면 중복 또는 덮어쓰기가 생길 수 있다.

Kafka 문서도 출력 처리 후 offset 저장 전에 장애가 나면 메시지가 재처리될 수 있으며, 발행 응답을 잃으면 발행의 성공 여부가 모호해질 수 있다고 설명한다. Apache Kafka Design의 보장은 외부 객체 저장소와 벡터 DB까지 자동으로 하나의 트랜잭션으로 묶어 주지 않는다.

벡터 쓰기의 응답을 잃었을 때

벡터 저장소에서는 (출처, 파일, 버전, 회차, lane) 조합을 쓰기와 읽기의 단위로 삼는다. 이미 같은 조합이 있는지 확인하고 삽입 응답 수와 사후 행 수를 대조한다. 응답 유실로 삽입 여부를 알 수 없다면 성공으로 표시하거나 같은 쓰기를 즉시 반복하지 않는다. Milvus의 Strong 읽기는 최신 시스템 시점의 데이터가 보일 때까지 기다리도록 정의된다.

이것은 사후 확인에 필요한 성질이지만, 특정 배포의 스키마·필터·삽입 동작이 맞는지는 실제 환경에서 검증해야 한다. Milvus Consistency

검색 결과도 요청 범위 안에 있는지 확인한다

검색 요청에서 여러 파일을 지정할 때 파일 ID 목록과 버전 목록을 따로 OR로 묶으면 요청하지 않은 조합이 섞일 수 있다. 각 대상 내부에서는 파일·버전·회차·lane을 AND로 결합하고, 완성된 대상끼리만 OR로 연결한다. 벡터 DB가 응답한 후에도 각 결과의 전체 식별자가 요청한 대상 중 하나에 속하는지 다시 확인한다. 다른 버전의 결과를 조용히 버리고 빈 검색으로 답하면 계약 오류가 사용자에게 숨겨진다.

검색 서비스가 확인한 식별자는 답변 생성 단계까지 유지한다. 인용을 중복 제거하거나 대화를 저장·재개할 때 본문이나 파일 ID가 같다는 이유로 서로 다른 버전의 청크를 합치지 않는다. 과거 인용을 열 때는 저장된 ID를 권한 증명으로 취급하지 않고, 현재 사용자의 권한을 확인한 뒤 정확한 과거 원본을 읽는다. 파일 읽기의 바이트 한도와 스트림 종료도 이 경계에 속한다.

다음 코드는 검색 결과를 요청 대상에 묶는 규칙을 작게 표현한 설명용 모델이다. 실제 서비스의 인증, 스키마 검사, 오류 응답을 대체하지 않는다.

def identity(row):
    return tuple(row[key] for key in ("origin", "file_id", "version", "attempt", "lane"))


def checked_hits(targets, hits):
    allowed = {identity(target) for target in targets}
    if any(identity(hit) not in allowed for hit in hits):
        raise ValueError("search result escaped requested version")
    return hits


v1 = dict(origin="file", file_id="f", version=1, attempt="a", lane="fast")
v2 = dict(origin="file", file_id="f", version=2, attempt="b", lane="fast")
assert checked_hits([v1], [v1]) == [v1]
try:
    checked_hits([v1], [v2])
except ValueError:
    pass
else:
    raise AssertionError("another version was accepted")

무엇을 통과해야 운영 계약이 되는가

로컬 단위 검사는 이벤트·산출물·검색 결과에서 식별자가 빠지거나 달라졌을 때 실패하는지 보여준다. 운영에서 같은 보장을 주장하려면 경계를 가로지르는 전이 검사가 더 필요하다. 1번 버전이 준비된 상태에서 2번 원본을 확정하고 처리를 실패시켜 기본 검색이 1번을 대신 내놓지 않는지 확인한다. 저장된 1번 인용은 권한 재검사 후 정확한 1번을 열어야 한다. 같은 회차에 다른 입력 해시가 들어오면 쓰기를 막고, 완료 응답 유실 뒤에는 검증된 결과만 재전달해야 한다. 처리 lane의 청크 번호가 이어지는 경우와 broker 재할당의 일시 오류 뒤 회복도 포함해야 한다.

이 설계를 살핀 로컬 구현에서는 해시·영수증·정확한 검색 대상의 일부 경계가 검사되었지만, 종료 중 처리 회차 충돌, 두 번째 lane의 청크 번호, broker 재할당 복구에 미해결 리뷰 항목이 남았다. 실제 broker·벡터 저장소·객체 저장소·권한 서비스가 연결된 검증과 배포 확인도 별개다. 따라서 여기의 흐름은 완료된 운영 결과가 아니라, 인용의 원본을 잃지 않기 위해 구현과 검증을 이어 갈 수 있는 계약이다.

앱의 내용 버전과 저장소 버전은 다르다

객체 저장소 자체의 버전 관리도 별도 문제다. 예를 들어 S3에서 버전 관리가 켜진 객체에 버전 ID 없이 DELETE하면 이전 바이트가 곧바로 영구 삭제되는 대신 삭제 마커가 생긴다. 앱의 “내용 버전 2 삭제”와 저장소의 “객체 버전 삭제”를 같은 동작으로 간주할 수 없다. AWS S3 객체 버전 삭제 문서가 보여 주듯, 정리 정책은 정확한 버전 소유권을 확인한 후 정해야 한다.