AI Agent 컨텍스트 격리, 다른 사용자의 기억이 섞이지 않게 만드는 기준
세션 ID, 장기 메모리, 캐시와 워커 상태를 사용자·조직별로 분리하고 교차 정보 노출을 테스트하는 실무 체크리스트입니다.
한 고객의 문의를 요약하던 AI Agent가 다음 고객에게 이전 대화의 이름이나 조건을 섞어 말한다면, 답변 품질 문제가 아니라 정보 격리 실패입니다. 세션 기록, 장기 메모리, 캐시, 작업 체크포인트 중 하나라도 사용자 경계를 잃으면 같은 사고가 생길 수 있습니다.
컨텍스트 격리는 “프롬프트에 다른 정보를 말하지 말라”고 쓰는 작업이 아닙니다. 인증된 사용자와 조직을 기준으로 상태의 소유권을 정하고, 저장·조회·삭제·목록 API 전 구간에서 같은 경계를 강제하는 운영 설계입니다.
세션 ID는 대화 편의를 위한 문자열이 아니라, 누구의 기억을 읽을 수 있는지 결정하는 보안 경계입니다.
핵심 요약
- 단기 대화, 장기 메모리, 도구 결과 캐시, 체크포인트·로그를 서로 다른 상태 저장소로 보고 각각 소유권을 정합니다.
- session_id나 memory_id를 클라이언트가 보낸 값만으로 신뢰하지 않고, 서버가 인증 정보와 매핑해 매 요청마다 소유권을 확인합니다.
- 격리 키에는 최소한 tenant_id / user_id / conversation_id 또는 memory_kind가 포함되어야 합니다.
- 일반 조회뿐 아니라 목록·검색·관리자·내보내기 API도 같은 위협 모델로 따로 검증합니다.
- A 사용자에게만 심은 표식 문구가 B 사용자의 응답·도구 입력·요약·로그 어디에도 나타나지 않는지 자동 테스트합니다.
왜 RAG 권한 필터만으로는 부족할까요?
RAG 권한 필터는 검색할 문서를 제한합니다. 컨텍스트 격리는 검색 이후에 남는 대화 기록, 요약, 메모리, 캐시, 재시도 상태까지 다룹니다. 올바른 문서를 찾았더라도 이전 사용자의 세션 요약을 함께 불러오면 결과는 이미 오염됩니다.
Google Cloud의 세션 문서는 기본 IAM 권한이 프로젝트 수준이라고 설명하며 userId 기반 조건을 별도로 두도록 안내합니다. 즉, 같은 프로젝트 안에 있다는 사실만으로 사용자별 세션 접근이 자동 분리되는 것은 아닙니다.
프롬프트 인젝션은 “외부 지시를 믿을 것인가”의 문제이고, 컨텍스트 격리는 “이 상태가 누구의 것인가”의 문제입니다. 두 통제는 서로 대체할 수 없습니다.
먼저 네 가지 상태 경계를 구분합니다
1. 단기 세션 컨텍스트
현재 대화의 메시지, 도구 호출, 임시 변수, 체크포인트입니다. 권장 소유 키는 tenant_id / user_id / conversation_id입니다. 로그아웃, 상담 종료, 권한 변경 시 언제 닫고 지울지도 함께 정해야 합니다.
2. 장기 메모리
사용자 선호, 반복 업무 규칙, 이전 세션 요약처럼 여러 대화에서 다시 쓰는 상태입니다. 단기 세션 ID를 그대로 재사용하지 말고 tenant_id / user_id / memory_kind처럼 별도 네임스페이스로 분리합니다.
3. 도구 결과와 캐시
CRM 조회, 문서 요약, 검색 결과, 모델 응답 캐시는 빠른 처리를 돕지만 키가 넓으면 다른 사용자의 결과를 돌려줄 수 있습니다. 입력 해시뿐 아니라 조직·사용자·권한 버전을 캐시 키에 포함합니다.
4. 체크포인트와 운영 로그
재시도용 상태, 큐 메시지, 추적 로그에도 원문이나 요약이 남습니다. 실행 ID만 기록하면 소유자를 역검증하기 어려우므로 tenant_id와 user_id를 필수 메타데이터로 남기되 민감 원문은 최소화합니다.
단계 1. 격리 키는 서버에서 만듭니다
사용자가 보낸 session_id, memory_id, user_id는 조회 힌트일 뿐 소유권 증거가 아닙니다. 서버가 로그인 토큰이나 SSO에서 확인한 주체를 내부 식별자와 연결한 뒤, 해당 상태가 그 주체에게 속하는지 검사해야 합니다.
- 인증 주체와 내부 tenant_id / user_id 매핑을 서버에서 확정합니다.
- 단기 상태 키는 tenant_id / user_id / conversation_id 조합으로 만듭니다.
- 장기 상태 키는 tenant_id / user_id / memory_kind 조합으로 별도 생성합니다.
- 읽기·쓰기·삭제 때마다 키의 소유권과 현재 멤버십을 다시 확인합니다.
좋은 식별자는 추측하기 어려운 것에서 끝나지 않습니다. 무작위 UUID라도 서버 측 소유권 검사가 없으면 다른 UUID를 얻은 사용자가 상태를 읽을 수 있습니다.
단계 2. 단기 세션과 장기 메모리를 분리합니다
대화가 끝났다고 장기 메모리까지 지워지거나, 반대로 장기 메모리를 찾는 요청이 모든 대화 기록을 훑어서는 안 됩니다. 저장소가 같아도 테이블·파티션·조회 함수를 분리해 수명과 권한을 따로 운영하세요.
- 개인 세션: 특정 사용자와 대화 한 건에만 속하는 임시 상태
- 개인 장기 메모리: 여러 세션에서 쓰되 같은 사용자에게만 허용하는 상태
- 팀 공유 메모리: 명시적으로 공유된 별도 네임스페이스와 팀 멤버십 검증이 필요한 상태
- 시스템 공통 지식: 사용자 메모리와 섞지 않고 변경 승인·버전 관리를 따로 두는 상태
Google Cloud Memory Bank 문서는 메모리의 scope를 임의의 사전 형태로 구성해 그룹별 접근을 통제할 수 있다고 설명합니다. 동시에 ListMemories 같은 다중 스코프 메서드는 IAM 조건을 지원하지 않는다고 명시하므로, 목록 API를 사용자에게 직접 열기보다 백엔드에서 허용 범위를 제한해야 합니다.
단계 3. 애플리케이션 필터보다 아래 계층에서도 막습니다
WHERE user_id = ? 한 줄만 애플리케이션 코드에 두면 새 API, 배치 작업, 관리자 화면에서 필터가 빠질 수 있습니다. 데이터베이스 제약, 행 수준 보안, IAM 조건, 저장소 네임스페이스를 함께 써서 실수 한 번이 곧바로 정보 노출로 이어지지 않게 합니다.
- 상태 레코드의 기본 키·고유 제약에 tenant_id와 user_id를 포함합니다.
- RLS나 IAM 조건으로 현재 인증 주체가 자기 범위 밖 행을 읽지 못하게 합니다.
- 목록·검색·내보내기 API는 단건 조회와 별도로 권한 테스트를 만듭니다.
- 관리자 역할은 상시 전역 권한보다 승인·시간 제한·감사 로그를 붙인 별도 경로로 둡니다.
- 격리 거부 횟수와 소유권 불일치를 보안 지표로 남기고 원문 대신 식별자 중심으로 기록합니다.
단계 4. 캐시·큐·워커의 숨은 공유 상태를 제거합니다
데이터베이스는 잘 나눴는데 정보가 섞인다면 공용 캐시나 오래 살아 있는 워커를 확인해야 합니다. 프로세스 전역 변수, 최근 대화 캐시, 브라우저 세션, 재사용되는 도구 클라이언트가 이전 사용자의 상태를 품고 있을 수 있습니다.
- 캐시 키에 tenant_id / user_id / 권한 버전 / 입력 지문을 넣습니다.
- 큐 메시지에는 소유자 식별자와 권한 스냅샷 또는 재검증에 필요한 참조를 넣습니다.
- 워커가 작업을 마치면 메모리 변수, 임시 파일, 도구 세션을 초기화합니다.
- 재시도와 체크포인트 복원 때 원 요청자와 현재 실행 주체가 같은지 다시 확인합니다.
- 트레이싱·관측 도구도 tenant_id별 접근 통제를 적용하고 원문 수집을 최소화합니다.
특히 캐시 키를 prompt_hash만으로 만들면 같은 질문을 한 B 사용자에게 A 사용자의 권한 있는 답변을 반환할 수 있습니다. 결과가 같아 보여도 접근 가능한 근거와 후속 도구 권한은 다를 수 있습니다.
단계 5. 기억의 수명과 암호화 경계를 정합니다
모든 상태를 무기한 남기면 격리 버그가 났을 때 피해 범위가 커집니다. 업무 목적에 맞는 TTL, 삭제 전파, 암호화 키 범위를 정하고 “보관이 끝난 상태는 조회되지 않는다”를 실제 테스트로 확인합니다.
- 단기 세션은 상담·업무 종료 뒤 필요한 유예 시간만 두고 만료합니다.
- 장기 메모리는 항목별 목적, 생성 근거, 재검토일, 삭제 요청 상태를 기록합니다.
- 원문 삭제 시 요약·벡터·캐시·체크포인트·백업의 삭제 전파 범위를 문서화합니다.
- 세션별 또는 테넌트별로 암호화 키를 나눠 저장 계층의 피해 반경을 줄입니다.
- 운영 로그에는 원문 대신 상태 ID, 도구명, 결과 코드, 정책 버전처럼 재현에 필요한 최소 정보만 남깁니다.
Amazon Bedrock 문서는 다중 사용자 환경에서 같은 사용자에게 같은 memoryId를 제공하며 그 ID에 저장된 세션 요약을 다음 호출에 불러온다고 설명합니다. 또한 메모리 보존 기간을 1~365일로 설정할 수 있다고 안내합니다. memoryId는 단순 추적값이 아니라 장기 기억의 파티션 키로 다뤄야 합니다.
OpenAI Agents SDK의 EncryptedSession은 세션별 키 파생과 TTL 기반 만료를 제공합니다. 다만 암호화는 잘못된 사용자의 세션을 조회하는 접근 제어 버그까지 막아주지 않으므로 소유권 검사와 함께 써야 합니다.
단계 6. A/B 교차 누출 테스트를 자동화합니다
정상 응답 테스트만으로는 격리를 확인하기 어렵습니다. 테스트 전용 A·B 테넌트를 만들고 A에만 존재하는 표식 문구를 심은 뒤, B의 모든 조회 경로에서 그 문구가 나오지 않는지 확인하세요.
- A 사용자 세션과 장기 메모리에 추측하기 어려운 표식 문구를 저장합니다.
- B 사용자가 같은 질문, 비슷한 ID, 빈 ID, 조작한 ID로 조회를 시도합니다.
- 동시 요청과 워커 재사용 상황에서 응답·도구 입력·캐시 결과를 검사합니다.
- A가 로그아웃하거나 대화를 삭제한 뒤 B와 다시 로그인한 A의 상태를 비교합니다.
- 단건 조회뿐 아니라 목록·검색·내보내기·관리자 경로를 모두 실행합니다.
- 응답, 도구 인자, 세션 요약, 체크포인트, 로그 어디에도 A의 표식이 없을 때만 통과시킵니다.
테스트 실패 시 모델 프롬프트부터 고치지 마세요. 어떤 저장소와 조회 경로에서 잘못된 소유 키가 붙었는지 추적하고, 같은 경로를 재현 테스트로 고정하는 편이 재발 방지에 효과적입니다.
격리 테스트의 합격 기준은 “화면에 안 보임”이 아닙니다. 모델에 전달된 컨텍스트와 도구 입력, 숨은 요약·캐시·로그까지 표식이 들어가지 않아야 합니다.
배포 전 체크리스트
FAQ
Q. RAG 권한 필터가 있으면 컨텍스트 격리는 생략해도 되나요?
아닙니다. 권한 필터는 검색할 문서를 제한하지만, 세션 요약·장기 메모리·캐시·체크포인트는 검색 이후에도 남습니다. 두 통제를 함께 적용해야 합니다.
Q. session_id를 무작위 UUID로 만들면 충분한가요?
추측 가능성은 줄지만 소유권 검사를 대신하지는 못합니다. UUID를 알고 있는 요청이 실제 소유자의 tenant_id와 user_id에 속하는지 서버와 저장 계층에서 확인해야 합니다.
Q. 팀이 함께 쓰는 메모리는 어떻게 설계하나요?
개인 메모리에서 user_id를 빼는 방식보다 shared_team 같은 별도 네임스페이스를 만들고, 팀 ID·멤버십·권한 버전을 매 조회마다 확인하는 편이 안전합니다.
Q. 암호화하면 교차 노출 사고가 사라지나요?
암호화는 저장 데이터가 탈취됐을 때 피해를 줄이지만, 애플리케이션이 잘못된 세션을 정상 복호화해 전달하는 오류는 막지 못합니다. 인증·소유권·스코프 검증이 먼저입니다.
HeyRatty가 도와드릴 수 있는 부분
HeyRatty는 AI Agent를 붙이는 데서 끝내지 않고, 세션·메모리·도구·로그의 경계를 실제 업무 흐름에 맞춰 정리합니다. 현재 자동화에서 어떤 상태가 어디에 남는지부터 점검하면 과한 재구축 없이 우선순위를 잡을 수 있습니다.
사내 AI 비서나 고객 응대 Agent의 정보 격리 기준이 애매하다면, 작은 A/B 누출 테스트와 상태 인벤토리부터 함께 설계해볼 수 있습니다.
참고 및 이미지 출처
- Google Cloud: Control access to Sessions with IAM Conditions
- Google Cloud: Control access to Memory Bank with IAM Conditions
- Amazon Bedrock: Retain conversational context across multiple sessions using memory
- OpenAI Agents SDK: Encrypted session
- 이미지: Wikimedia Commons 「Safe Deposit Boxes」, Fixedsun, CC0 1.0 / 원본 파일 페이지
