해결한 문제와 담당 역할
사내 문서 RAG는 검색이 잘 되는 것만으로는 부족합니다. 인사팀 문서가 다른 부서 사람 질문에 섞여 나오면 안 되고, 근거가 없는데 그럴듯하게 답해도 안 됩니다.
문서 등록과 분할, 권한별 검색, 출처를 붙인 답변과 거절 응답 API를 만들었습니다.
저장소와 모델을 인터페이스로 분리해 메모리·테스트 모델로 회귀 테스트를 구성하고, PostgreSQL·pgvector와 OpenAI 어댑터를 별도 설정으로 연결했습니다.
검색 결과, 문서 관리, 질문 이력을 볼 수 있는 관리 화면과 정적 데모를 만들었습니다.
핵심 요구사항
- 다른 워크스페이스나 권한 밖 문서는 검색 결과와 출처에 나오지 않습니다.
- 근거가 하나도 없으면 답변을 만들지 않습니다.
- 기본 실행은 메모리 저장소와 가짜 모델을 쓰고, OpenAI 연동은 켜야 동작합니다.
- 문서 등록·분할
- 권한으로 먼저 거르기
- 관련 문단 검색
- 출처를 붙여 답변
- 질의 로그 기록
기술 선택과 이유
권한 확인은 검색 전에
- 적용 방식
- 볼 수 있는 문서만 검색 후보에 올립니다.
- 검토한 대안
- 전부 검색한 뒤 답변 단계에서 거르기
- 선택 이유
- 나중에 거르면 이미 모델 입력에 들어간 뒤입니다. 모델이 한 번이라도 본 내용은 답변에 새어 나올 수 있습니다.
근거 없으면 답하지 않기
- 적용 방식
- 인용할 문서가 없으면 답변과 출처가 빈 거절 응답을 돌려줍니다.
- 검토한 대안
- 모델이 일반 상식으로 답하게 두기
- 선택 이유
- 사내 규정 질문에 출처 없는 답이 나오면 맞는지 확인할 방법이 없습니다.
외부 의존성 분리
- 적용 방식
- 모델과 저장소를 인터페이스로 분리했습니다.
- 검토한 대안
- OpenAI API와 pgvector를 서비스 코드에서 바로 호출
- 선택 이유
- 테스트는 가짜 모델로 빠르게 돌리고, 실제 모델 연동은 따로 확인하고 싶었습니다.
주요 구현과 관련 테스트
핵심 로직과 동작을 확인한 테스트를 함께 정리했습니다.
주요 구현
RetrievalService.retrieve
app/retrieval.py
주요 구현
RetrievalService.retrieve
app/retrieval.py코드
소스 파일 열기access_reason = _access_reason(chunk, query)
if access_reason is None:
continue
구현 내용
저장소에서 한 번 거른 뒤에도 워크스페이스, 작성자, 부서 권한을 다시 확인합니다.
관련 테스트
test_retrieval_allows_public_owner_and_department_documents_onlytests/test_retrieval_permissions.py테스트 결과볼 수 있는 3건만 나옵니다.
추가 구현
AnswerService.answer
app/answer.py
추가 구현
AnswerService.answer
app/answer.py코드
소스 파일 열기if not citations:
return AnswerResponse(
query=query.query,
answer=None,
citations=[],
refusal_reason="insufficient_evidence",
)
구현 내용
인용할 문서가 없으면 답변과 출처를 비우고 insufficient_evidence를 돌려줍니다.
관련 테스트
test_answer_api_refuses_when_no_evidence_is_availabletests/test_answer_api.py테스트 결과insufficient_evidence로 거절합니다.
추가 구현
PostgresPolicyRepository.search_candidate_chunks
app/repository.py
추가 구현
PostgresPolicyRepository.search_candidate_chunks
app/repository.py코드
소스 파일 열기WHERE c.workspace_id = %s
AND d.indexing_status = 'ready'
AND (
d.owner_user_id = %s
OR d.visibility = 'public'
OR (d.visibility = 'department' AND d.department_ids && %s::text[])
)
ORDER BY c.embedding <=> %s::vector
구현 내용
벡터 유사도로 정렬하기 전에 SQL WHERE 절에서 권한 조건부터 적용합니다.
관련 테스트
test_postgres_repository_retrieval_filters_mixed_access_control_rowstests/test_postgres_repository_integration.py테스트 결과SQL 후보 조회와 검색 결과에서 허용된 문서만 반환하도록 검증합니다.
테스트 결과
문서 5건 검색
볼 수 있는 3건만 반환
관련 코드test_retrieval_permissions.py test_retrieval_allows_public_owner_and_department_documents_only 관련 문서 없음
답변하지 않고 거절
관련 코드test_answer_api.py test_answer_api_refuses_when_no_evidence_is_available 접근 권한 없는 부서 문서
응답 출처 목록에 포함되지 않음
관련 코드test_answer_api.py test_answer_api_keeps_retrieval_permission_filter_for_citations 테스트 방법 보기
| 구분 | 테스트 방법 | 결과 |
|---|---|---|
| 단위 테스트 | 권한이 서로 다른 문서 5건으로 검색을 실행합니다. | 볼 수 있는 3건만 나옵니다. |
| 통합 테스트 | 관련 문서가 없는 질문을 답변 API에 보냅니다. | insufficient_evidence로 거절합니다. |
| 통합 테스트 | 선택 실행 PostgreSQL 테스트에서 권한이 다른 문서와 색인 대기 문서를 등록합니다. | SQL 후보 조회와 검색 결과에서 허용된 문서만 반환하도록 검증합니다. |
| 정적 데모 | 관리 화면을 정적 빌드로 띄워 봅니다. | 고정 데이터로 검색 결과·출처·질문 이력을 보여 줍니다. 실제 백엔드 검색 결과는 아닙니다. |
프로젝트 범위
검증 한계와 미구현 범위
- 기본 실행은 메모리 저장소와 가짜 모델입니다. 실제 사내 인증 연동이나 운영 환경 pgvector는 해 보지 않았습니다.
- 화면의 토큰 수와 비용은 글자 수로 어림잡은 값이라 실제 청구액과 다릅니다.
- 고정 질문 평가는 권한·검색·출처 회귀 확인용입니다. 실제 모델의 답변 품질이나 RAG 검색 품질을 일반화할 수 없습니다.
- 출처는 권한 확인을 거친 검색 결과에서 구성합니다. 생성 답변의 모든 문장이 출처에 근거하는지 자동 검증하는 기능은 없습니다.
향후 개선
- 사내 인증과 감사 로그를 붙이고, 실제 모델로 평가를 따로 쌓아 보려고 합니다.
관련 자료
