LLM 엔지니어링21장 · AI 거버넌스와 보안, 온프레미스 인프라18 / 22← 이전목차다음 →MetaContents

21장. AI 거버넌스와 보안, 온프레미스 인프라

실습 5 — 접근 제어가 있는 검색

가상 회사 솔내기술의 사내 문서 12개(secure_docs.jsonl, 지어낸 문서)에는 등급(1 공개, 2 사내, 3 비밀)과 부서가 붙어 있습니다. 인턴, 개발팀원, 인사팀장이 같은 질문을 할 때, 걸러 내지 않은 검색, 검색 뒤 걸러 내기, 검색 전에 걸러 내기가 LLM 에 무엇을 넘기는지 견줍니다. 마지막에 인턴의 질문 하나를 실제로 LLM 에 물어 봅니다.

1단계. 문서와 사용자

Python
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단계. 세 가지 검색

Python
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.0

3단계. 검색 뒤 걸러 내기가 모자란 까닭

Python
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 에 실제로 물어 보기

Python
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단계는 같은 질문을 걸러 내지 않은 검색과 검색 전 걸러 내기로 묻습니다. 프롬프트에 "비밀은 말하지 말라"고 적는 것보다, 비밀을 아예 넘기지 않는 쪽이 확실합니다.
← 이전 절21장 · 실습 4 — 가리고 가명으로 바꿔 LLM 에 보내기다음 절 →21장 · 실습 6 — GPU 메모리 셈하기