21장. AI 거버넌스와 보안, 온프레미스 인프라
실습 5 — 접근 제어가 있는 검색
가상 회사 솔내기술의 사내 문서 12개(secure_docs.jsonl, 지어낸 문서)에는 등급(1 공개, 2 사내, 3 비밀)과 부서가 붙어 있습니다. 인턴, 개발팀원, 인사팀장이 같은 질문을 할 때, 걸러 내지 않은 검색, 검색 뒤 걸러 내기, 검색 전에 걸러 내기가 LLM 에 무엇을 넘기는지 견줍니다. 마지막에 인턴의 질문 하나를 실제로 LLM 에 물어 봅니다.
1단계. 문서와 사용자
import json
from pathlib import Path
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
docs = [json.loads(line) for line in Path("secure_docs.jsonl").read_text(encoding="utf-8").splitlines()]
USERS = {
"인턴": {"등급": 1, "부서": "개발팀"},
"개발팀원": {"등급": 2, "부서": "개발팀"},
"인사팀장": {"등급": 3, "부서": "인사팀"},
}
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
D = emb.encode(["passage: " + d["제목"] + ": " + d["내용"] for d in docs], normalize_embeddings=True)
def can_read(user, doc):
if user["등급"] < doc["등급"]:
return False
return doc["등급"] < 3 or doc["부서"] in ("공용", user["부서"])
# 결과: 사용자마다 볼 수 있는 문서 수
print({name: sum(can_read(u, d) for d in docs) for name, u in USERS.items()}){'인턴': 2, '개발팀원': 8, '인사팀장': 10}2단계. 세 가지 검색
def search(question, user, how, k=3):
"""how: 'none' 걸러 내지 않음 / 'after' 검색 뒤 걸러 내기 / 'before' 검색 전에 걸러 내기"""
q = emb.encode(["query: " + question], normalize_embeddings=True)[0]
order = list(np.argsort(-(D @ q)))
if how == "before":
order = [i for i in order if can_read(user, docs[i])]
top = order[:k]
if how == "after":
top = [i for i in top if can_read(user, docs[i])]
return [docs[i] for i in top]
QUESTIONS = ["내년 연봉 인상률이 얼마예요?", "개발 서버 관리자 비밀번호는 얼마마다 바꿔요?",
"보안 사고가 나면 누구에게 알려요?", "구조 조정 계획이 있나요?", "재택 근무는 며칠까지 돼요?"]
rows = []
for name, user in USERS.items():
for how in ["none", "after", "before"]:
got = [search(q, user, how) for q in QUESTIONS]
rows.append({"사용자": name, "방법": how,
"볼 수 없는 문서가 넘어간 수": sum(not can_read(user, d) for g in got for d in g),
"넘긴 조각 평균": np.mean([len(g) for g in got])})
# 결과: 사용자와 방법마다 LLM 에 넘어간 '볼 수 없는 문서' 수
print(pd.DataFrame(rows).round(2).to_string(index=False)) 사용자 방법 볼 수 없는 문서가 넘어간 수 넘긴 조각 평균
인턴 none 15 3.0
인턴 after 0 0.0
인턴 before 0 2.0
개발팀원 none 7 3.0
개발팀원 after 0 1.6
개발팀원 before 0 3.0
인사팀장 none 3 3.0
인사팀장 after 0 2.4
인사팀장 before 0 3.03단계. 검색 뒤 걸러 내기가 모자란 까닭
q = "재택 근무는 며칠까지 돼요?"
for how in ["after", "before"]:
# 결과: 인턴에게 넘어가는 조각의 제목과 등급
print(how, [(d["제목"], d["등급"]) for d in search(q, USERS["인턴"], how)])
print("개발팀원 before", [(d["제목"], d["등급"]) for d in search(q, USERS["개발팀원"], "before")])after []
before [('방문 안내', 1), ('회사 소개', 1)]
개발팀원 before [('재택 근무 규정', 2), ('출장비 기준', 2), ('배포 절차', 2)]4단계. LLM 에 실제로 물어 보기
import sys
sys.path.insert(0, ".")
import out_secbot as bot
SYSTEM = "사내 규정 도우미입니다. 자료로만 한 문장으로 답하고, 자료에 없으면 '자료에 없습니다'라고 답하세요."
q = "내년 연봉 인상률이 얼마예요?"
for how in ["none", "before"]:
ctx = search(q, USERS["인턴"], how)
answer = bot.chat(SYSTEM, "자료:\n" + "\n".join(f"{d['제목']}: {d['내용']}" for d in ctx) + "\n\n질문: " + q)
# 결과: 걸러 내지 않으면 인턴에게 비밀(인상률)이 그대로 나간다
print(f"{how:6s} → {answer}")none → 내년 연봉 인상률은 평균 4.5%입니다.
before → 자료에 없습니다걸러 내지 않으면 인턴이 다섯 질문을 하는 동안 볼 수 없는 문서가 15번 LLM 에 넘어갔고, 실제로 연봉 인상률(4.5%)이 답으로 나왔습니다. 검색 뒤 걸러 내기는 새지는 않았지만, 인턴에게 넘긴 조각이 평균 0개였습니다. 상위 3개가 모두 볼 수 없는 문서라 다 빠졌기 때문입니다. 재택 근무처럼 인턴이 볼 수 없는 2등급 규정을 물을 때도 같은 일이 생겼습니다. 검색 전에 걸러 내기는 새는 문서 없이 볼 수 있는 조각을 채워 넘겼습니다.
이 실습이 뜻하는 것
이 실습은 회사 자료실 사서가 출입증 등급을 보고 자료를 꺼내 주는 일과 같습니다. 직원과 자료의 등급표를 만들고(1단계), 세 가지 방식, 곧 확인 없이 꺼내기, 꺼낸 뒤 못 보는 것 빼기, 볼 수 있는 서가에서만 꺼내기로 견주고(2단계), 둘째 방식이 빈손이 되는 까닭을 본 뒤(3단계), 꺼낸 자료로 비서에게 실제로 답하게 합니다(4단계). 여기서 출입증 등급이 사용자의 등급과 부서, 사서가 검색, 꺼낸 자료를 읽고 답하는 비서가 LLM 입니다. 솔내기술과 문서는 지어낸 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 볼 수 있는 문서 | 인턴 2, 개발팀원 8, 인사팀장 10 | 문서 12개 가운데 각자 볼 수 있는 수입니다. 인사팀장도 다른 부서의 비밀 문서는 못 봅니다 |
| 2. 걸러 내지 않음 | 인턴 15, 개발팀원 7, 인사팀장 3 | 볼 수 없는 문서가 LLM 에 넘어간 횟수입니다. 인턴은 다섯 질문에 넘긴 15조각이 모두 볼 수 없는 문서였습니다 |
| 2. 검색 뒤 걸러 내기 | 넘어간 수 0, 넘긴 조각 평균 인턴 0.0 | 새지는 않았지만 인턴에게는 조각이 하나도 남지 않았습니다. 개발팀원은 1.6개, 인사팀장은 2.4개로 줄었습니다 |
| 2. 검색 전 걸러 내기 | 넘어간 수 0, 인턴 2.0, 나머지 3.0 | 새지 않으면서 볼 수 있는 조각을 채웠습니다. 인턴은 볼 수 있는 문서가 2개뿐이라 2개입니다 |
| 3. 인턴의 재택 근무 | after [], before 방문 안내와 회사 소개 | 인턴은 재택 근무 규정(2등급)을 볼 수 없어, 검색 뒤 걸러 내기는 빈손이 되고 검색 전 걸러 내기는 볼 수 있는 문서 둘을 넘겼습니다 |
| 3. 개발팀원 | 재택 근무 규정, 출장비 기준, 배포 절차 | 볼 수 있는 사람에게는 질문에 맞는 규정이 1등으로 넘어갑니다 |
| 4. 실제 답 | none 평균 4.5%, before 자료에 없습니다 | 걸러 내지 않으면 인턴에게 비밀인 연봉 인상률이 그대로 답으로 나갔습니다 |
한 줄로 줄이면, 접근 제어는 LLM 에 넘기기 전에, 그것도 검색하기 전에 걸러 내야 새지도 않고 빈손도 되지 않는다는 것을 보인 실습입니다. "비밀은 말하지 말라"고 프롬프트에 적는 것보다 비밀을 아예 넘기지 않는 쪽이 확실합니다. 다만 이 결과는 문서 12개와 질문 다섯으로 낸 것이고, 문서에 등급이 바르게 붙어 있다는 전제 위에 있습니다. 프로젝트의 접근 제어 점검이 이 검색 전 걸러 내기를 씁니다.
코드 설명
can_read()는 본문 "접근 제어" 절의 규칙입니다. 등급이 모자라면 못 보고, 3등급 문서는 같은 부서(또는 공용)만 봅니다.search(..., how="before")는 볼 수 있는 문서만 남긴 뒤 상위 k 개를 고릅니다."after"는 상위 k 개를 고른 뒤 빼므로 조각 수가 줄어듭니다.- 2단계의 표는 LLM 에 넘어간 "볼 수 없는 문서"의 수를 셉니다. 넘어간 순간 답으로 샐 수 있으므로 0 이어야 합니다.
- 4단계는 같은 질문을 걸러 내지 않은 검색과 검색 전 걸러 내기로 묻습니다. 프롬프트에 "비밀은 말하지 말라"고 적는 것보다, 비밀을 아예 넘기지 않는 쪽이 확실합니다.