‘모르겠다’고 말하는 RAG 어시스턴트 구축: 평가, 보안 및 $0 배포
요약
본 게시물은 RAG(Retrieval-Augmented Generation)의 한계를 넘어, 평가, 보안 및 $0 배포가 가능한 고급 AI 에이전트 구축 과정을 소개합니다. 사용자가 접근할 수 있는 청크를 검색하고, Gemini 모델을 사용하여 출처 기반 답변을 생성하며, LangGraph를 활용한 다중 도구 에이전트를 구현했습니다.
핵심 포인트
- RAG의 한계를 넘어선 평가 및 보안 메커니즘 제시
- LangGraph를 이용해 문서 검색, API 쿼리 등 복합 작업 수행 가능
- 유사성 임계값과 프롬프트 기반 방어 두 계층 구조 채택
- AWS 문서를 코퍼스로 활용하여 실습 가능한 에이전트 구현
대부분의 RAG 튜토리얼은 챗봇이 PDF에 대한 질문에 답할 때 끝납니다. 저는 그 이후에는 무엇이 오는지 알고 싶었습니다. 즉, 어떻게 작동하는지 알 수 있는 방법, 누가 무엇을 볼 수 있는지, 문서가 그것에게 명령을 내리려고 할 때는 무슨 일이 발생하는지, 그리고 아무 비용도 지불하지 않고 그것을 배포하려면 어떻게 해야 하는지에 대한 것입니다.
본 게시물에서는 학습 및 포트폴리오 프로젝트인 enterprise-ai-agent를 소개합니다. 이 에이전트는 공개 AWS 문서를 질문하고, 출처를 인용하며, 문서가 다루지 않는 내용은 거부하고, 도구를 사용할 수 있는 작은 에이전트를 포함합니다. 구축 과정에 대한 참고 사항을 드리자면, 저는 AI 코딩 어시스턴트(Claude)와 단계별로 작업했습니다. 모든 명령을 실행했고, 모든 오류를 읽었으며 범위를 결정했고, 이 게시물을 작성하는 것은 제가 각 선택을 설명할 수 있는지 확인하는 방법이었습니다. 이 프로젝트는 프로덕션 소프트웨어가 아니며, 마지막에 어떤 부분이 부족한지 말씀드리겠습니다.
Repository: https://github.com/PabsCueto/enterprise-ai-agent
무엇을 하는가
• POST /ask는 사용자가 볼 수 있는 청크를 검색하고, Gemini에게 오직 해당 컨텍스트만을 사용하여 답변하도록 요청한 다음, 출처 URL과 토큰 사용량을 포함하여 답변을 반환합니다.
• POST /agent는 세 가지 도구를 가진 LangGraph 에이전트입니다: 문서를 검색하는 기능, 시뮬레이션된 내부 상태 API를 쿼리하는 기능, 티켓 초안을 생성하는 기능(어디로도 전송되지 않는 로컬 JSON 파일).
• 그 주변에는 평가 하네스(evaluation harness), 사용자별 접근 제어(per-user access control), 구조화된 로그, 그리고 CI에서 빌드 및 스모크 테스트되는 Docker 이미지가 있습니다.
코퍼스는 AWS 문서의 소개 페이지 7개로, 약 백 개의 청크입니다. 의도적으로 작게 만들었습니다: 모든 결과를 수동으로 확인할 수 있기 때문입니다.
request -> FastAPI (요청 ID, JSON 로그, API 키, 속도 제한)
|-- /ask -> 검색(사용자별 필터 + 점수 임계값) -> Gemini
|-- /agent -> LangGraph: 에이전트 <-> 도구 -> 답변
가짜 답변에 대한 두 계층 방어
첫 번째 계층은 유사성 임계값입니다. 질문과 충분히 가깝지 않은 청크는 제거되며, 아무것도 남지 않으면 모델 호출 자체가 이루어지지 않습니다.
두 번째 계층은 프롬프트입니다: 컨텍스트에 있는 내용만 답변하거나 찾을 수 없다고 말합니다.
저는 두 가지를 모두 측정했습니다. AWS와 관련 없는 질문(프랑스의 수도, 타코 레시피)은 최대 0.575점을 받았고, 코퍼스에서 다루는 질문들은 최소 0.682점을 받았습니다. 따라서 0.6의 임계값이 이 둘을 명확하게 분리합니다.
흥미로운 경우는 AWS에 관한 것이지만 커버되지 않은 질문입니다: VPC, EC2 가격 책정, RDS, Amplify. 이들은 0.60에서 0.664 사이의 점수를 받았는데, 이는 임계값보다 높습니다. 임베딩은 답변이 텍스트에 있는지 여부가 아니라 주제를 측정하며, RDS에 대한 질문은 DynamoDB 문서와 매우 유사해 보입니다. 점수만으로는 이를 포착할 수 없습니다. 프롬프트가 포착했고 모델은 네 가지 모두 거부했습니다. 어느 한 계층만으로는 충분하지 않습니다.
가드레일을 갖춘 에이전트
에이전트는 작은 LangGraph 그래프입니다: 결정하는 모델 노드, 실행하는 도구(tools) 노드, 그리고 모델이 답변할 때까지 이 둘 사이의 루프가 있습니다. 중요한 것은 에이전트가 할 수 없는 것입니다:
• 수정하거나 삭제할 수 있는 도구가 없으며, 티켓은 초안 상태입니다.
• 티켓은 명시적인 요청에 의해서만 생성됩니다.
• 그래프는 10단계 후에 중지하고, 스레드 ID는 검증되며, 도구 출력은 지침이 아닌 데이터로 처리됩니다.
한 교훈은 프롬프트에서 얻었습니다. 저의 첫 번째 규칙은 정보가 부족할 때 질문하도록 에이전트에게 지시했습니다. 제가 “찾아낸 문제에 대한 티켓을 생성해라”라고 작성했을 때, 그것은 스스로 작성할 수 있는 제목과 설명을 요청했습니다. 해결책은 한 문장이었습니다: 사용자가 내용을 위임하면, 수집한 데이터를 바탕으로 초안을 작성하고, 쓸 내용이 충분하지 않을 때만 질문하도록 합니다.
측정하기
이 하네스(harness)에는 세 가지 작은 데이터셋이 있습니다: 검색 질문 22개(커버된 것 14개, 관련 없는 것 4개, AWS에 관한 것이지만 커버되지 않은 것 4개), 에이전트 케이스 8개, 프롬프트 주입 사례 7개. 이 시스템은 히트율(hit rate)과 MRR을 보고하며, 몇 개의 질문이 답변되거나 거부되었는지, 그리고 에이전트가 예상된 도구를 순서대로 호출했는지, 금지된 도구는 피했는지를 알려줍니다.
결과는 좋았습니다. 커버된 질문에 대해 4에서 100%의 적중률(hit rate)을 보였고 MRR은 1.0이었으며, 에이전트 케이스 8개 중 8개를 성공했습니다. 하지만 데이터셋이 작고 제가 직접 작성한 것이기 때문에 이 수치만으로는 큰 의미가 없습니다. harness가 알려준 것은 두 가지 버그였습니다:
-
에이전트 케이스 하나가 실패했습니다. 에이전트의 문제가 아니었습니다. 제 평가 스크립트가 티켓 도구(ticket tool)에 경로(path) 대신 문자열(string)을 임시 폴더로 전달했기 때문입니다. 에이전트는 올바른 도구를 선택했지만, 내 스크립트 내부에서 충돌이 발생했습니다. 이제 수정 없이 실패하는 회귀 테스트(regression test)가 있습니다.
-
제 첫 번째 보고서에서는 0.673의 임계값(threshold)을 제안했습니다. 이는 두 가지 종류의 '부정적' 질문을 혼합하여 나온 것이었고, 여유분(margin)은 단지 0.018에 불과했습니다. 저는 이들을 분리했습니다. 따라서 임계값은 관련 없는 질문에 대해서만 보정되며, AWS와 관련된 하지만 커버되지 않은 질문들은 최종 답변으로 판단됩니다.
누가 무엇을 볼 수 있는지 (Who can see what)
- API 키. 무작위 256비트 값으로, SHA-256 해시로만 저장되며 상수 시간(constant time)에 비교됩니다. 사용자가 설정되지 않은 경우, 모든 보호된 요청은 거부됩니다.
- 문서 그룹. 각 문서는 그룹에 속하고, 각 사용자도 그룹을 가지며, 필터는 top-k를 가져오기 전에 벡터 검색 내부에서 실행되므로, 금지된 문서는 절대로 프롬프트에 도달하지 않습니다.
- 토픽별이 아닌 문서별로 (Per document, not per topic). IAM에 대해 질문한 제한된 사용자에게 DynamoDB와 S3 페이지가 언급하는 내용을 바탕으로 부분적인 답변이 생성되었습니다. 이는 컨텍스트가 부족하다는 점을 솔직하게 보여주지만, 그 세밀함(granularity)을 보여줍니다.
- 대화 기록 (Conversations). 내부 스레드 ID는 사용자 이름과 스레드 ID로 구성되므로, 같은 스레드 ID를 공유하는 두 사용자는 절대로 히스토리를 공유하지 않습니다.
- 슬라이딩 윈도우 기반의 사용자별 속도 제한 (Rate limit per user with a sliding window).
보안 테스트가 공허하지 않음을 확인하기 위해, 저는 의도적으로 각 보호 기능을 제거했고 매번 적어도 하나의 테스트가 실패하는 것을 확인했습니다.
Poisoned documents
간접 프롬프트 주입(Indirect prompt injection)은 반대 위협입니다. 사용자는 합법적이지만, 문서에 숨겨진 명령이 포함되어 있습니다. 저는 정상적인 서론과 각각 하나의 숨겨진 지침을 가진 네 개의 짧은 문서를 작성했습니다: 'HACKED'라는 제목의 티켓 생성, 오직 'PWNED'만 답변, 이미지와 가짜 도메인 링크 첨부, 또는 시스템 프롬프트 출력. 이들은 실제 인덱스에는 절대 저장되지 않고 별도의 인덱스에 존재합니다.
각 사례는 OK(오염된 텍스트가 모델에 도달했지만 따르지 않음), FAIL, 또는 결론 없음(오염된 텍스트가 모델에 전혀 도달하지 않아 아무것도 증명할 수 없음)으로 끝납니다. 이 세 번째 상태가 거짓된 '모두 안전함'을 방지합니다. 결과: 7개 중 7개가 저항했고, 결론 없음은 없었습니다.
저는 이것을 '안전하다'고 읽지 않을 것입니다. 이것들은 제가 작성한 일곱 개의 페이로드이며, 탐지는 도구 호출과 고정 문자열에 의존하고 있으며, 프롬프트 주입에 대한 완벽한 방어책은 없습니다. 실제로 피해를 제한하는 것은 구조적입니다: 파괴적인 도구가 없고, 초안 전용 티켓이며, 단계 제한이 있습니다.
Observability without a platform
모든 요청은 요청 ID(응답 헤더에도 반환됨)와 상태, 지연 시간, 사용자, 사용된 도구, 토큰 사용량, 질문 거부 여부가 포함된 JSON 로그 라인을 작성합니다. 에이전트의 경우, 토큰은 모든 모델 호출에 걸쳐 합산됩니다. 질문 텍스트는 기록되지 않고 길이와 짧은 해시만 기록되며, 도구 인자도 기록되지 않습니다. 보호된 /metrics 엔드포인트는 카운트, 평균 및 p95 지연 시간, 토큰 및 거부율을 보여줍니다. 메트릭은 메모리에 있으므로 재시작 시 초기화됩니다.
Shipping it for $0
Docker 이미지는 슬림하고, 비루트 사용자(non-root user)로 실행되며, 문서, 인덱스 또는 키를 포함하지 않습니다. 제 Mac은 macOS 12에서 실행되며 Docker Desktop을 실행할 수 없기 때문에 CI가 이미지를 빌드하고 스모크 테스트를 수행합니다: 작은 가짜 인덱스로 컨테이너를 시작하고 /health가 응답하는지, 그리고 사용자가 없을 때 /ask가 401을 반환하는지 확인합니다.
세 가지 점이 저를 힘들게 했습니다:
- 새 서비스는 제가 테스트했던 것보다 더 최신 Python 버전을 기본으로 사용하므로, .python-version 파일을 이용해 3.13을 고정했습니다.
- 인덱스는 배포 중에 구축되므로, 시작할 때 임베딩 호출이 발생하지 않습니다.
- 사용자 정보를 담는 시크릿 파일에 잘못된 텍스트를 붙여넣었고, 서버가 불투명한 트레이스백과 함께 충돌했습니다. 클립보드에는 여전히 오래된 터미널 명령어가 남아 있었습니다. 앱은 이제 파일 경로, 크기 및 첫 번째 문자(절대 내용물 아님)를 기록하고, 사용자 없이 시작하며, 수정될 때까지 모든 보호 요청을 거부합니다. 무료 0.1-CPU 인스턴스에서의 시작 시간은 약 26초가 걸립니다.
한계점과 다음 계획 • 코퍼스는 매우 작고, 상태 API는 시뮬레이션되었으며, 세 개의 데이터셋은 작고 직접 작성되었습니다.
• 아직 답변 충실도(answer-faithfulness) 메트릭이 없고, 모델이 거부하는지 여부만 확인합니다.
• 대화 메모리, 속도 제한 및 메트릭이 하나의 프로세스에 존재합니다.
• 실제 사용 사례에서는 API 키를 파일에 두는 것 대신 HTTPS, 키 로테이션(key rotation) 및 적절한 인증이 필요할 것입니다.
• 다음 계획: 충실도 검사, 실제 PDF가 포함된 더 큰 코퍼스, 벡터 데이터베이스, 티켓과 같은 쓰기 작업 전 인간 확인, 그리고 AWS에서의 단기 배포입니다.
여기서 한 가지를 얻어 가신다면: 모델은 쉬운 부분이었습니다. 가치는 그것을 측정하고, 고의로 깨뜨리고, 내가 증명할 수 없었던 것을 기록하는 데 있었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기