LLM 엔지니어링18장 · Project 03. 내 서버에서 일하는 AI Agent5 / 13← 이전목차다음 →MetaContents

18장. Project 03. 내 서버에서 일하는 AI Agent

권한은 코드가 지킨다

은행 창구 직원은 통장 주인의 신분증을 확인한 뒤에야 그 통장의 잔액을 알려 줍니다. 손님이 "옆 사람 통장도 보여 주세요"라고 해도 보여 주지 않습니다. 직원의 친절이 아니라 은행의 규칙이 막습니다. 에이전트도 같습니다. 누구의 데이터를 볼지는 LLM 이 정하지 않고, 서버가 로그인한 사용자로 도구를 묶어 둡니다.

로그인, 사용자 묶기, 도구 실행의 세 걸음으로 권한을 지키는 흐름도

1번에서 API 가 API 키(또는 로그인)로 사용자를 알아냅니다. 2번에서 서버가 그 사용자로 도구 묶음을 만듭니다. 3번에서 LLM 은 도구를 부를 수만 있고 사용자를 바꿀 수는 없습니다. 도구의 인자에 "사번"이나 "고객 번호" 칸이 아예 없습니다. 여기서 신분증 확인이 로그인이고, 은행의 규칙이 사용자를 묶은 도구입니다.

사용자를 어디서 받나

방식생김새위험
LLM 이 인자로 넘긴다get_leave(employee_id="E005")"E005 도 보여 줘" 한마디에 남의 정보가 나간다
프롬프트에 적어 둔다"지금 직원은 E001 입니다"프롬프트 인젝션으로 바꿀 수 있다
서버가 도구에 묶는다Office(user="E001").get_my_leave()LLM 이 바꿀 길이 없다

사용자를 묶은 도구

Python
class Orders:
    """서버가 로그인한 고객으로 만든다. 도구의 인자에는 고객 번호가 없다"""
    DATA = {"O-1": ("C01", "텀블러"), "O-2": ("C02", "도마")}

    def __init__(self, customer):
        self.customer = customer

    def get_order(self, order_id):
        owner, item = self.DATA.get(order_id, (None, None))
        if owner != self.customer:                      # 없는 주문과 남의 주문을 똑같이 답한다
            return {"오류": "고객님의 주문에서 찾을 수 없습니다."}
        return {"주문번호": order_id, "상품": item}


tools = Orders(customer="C01")                         # API 키로 알아낸 고객
print(tools.get_order("O-1"))
print(tools.get_order("O-2"))
print(tools.get_order("O-9"))

O-1 은 상품까지 나오고, 남의 주문 O-2 와 없는 주문 O-9 는 같은 오류가 나옵니다. 둘을 다르게 답하면 "그 번호의 주문이 있기는 하다"는 사실이 새어 나갑니다.

코드에 나오는 함수

코드쉬운 뜻
DATA = {"O-1": ("C01", "텀블러"), …}주문번호마다 (주인 고객, 상품)을 적은 연습용 주문 장부다. 클래스에 붙어 있어 모든 객체가 함께 본다
Orders(customer="C01")서버가 로그인한 고객으로 도구 묶음을 만든다. 이 객체의 도구는 C01 의 주문만 다룬다
self.customer묶어 둔 고객 번호다. 도구의 인자가 아니라 객체 안에 있어 LLM 이 바꿀 길이 없다
self.DATA.get(order_id, (None, None))주문을 찾고, 없으면 (None, None) 을 돌려준다. 없는 주문도 오류 없이 다음 줄로 간다
if owner != self.customer주인이 로그인한 고객이 아니면 막는다. 없는 주문은 주인이 None 이라 여기서 함께 막힌다

이 코드가 뜻하는 것

아파트의 무인 택배 보관함을 떠올려 봅니다. 입주민 카드를 대면 내 칸만 열립니다. "옆집 칸도 열어 주세요"라고 말해도 보관함은 카드가 정한 칸만 엽니다. 여기서 입주민 카드가 API 키, 카드로 정해진 칸이 Orders(customer="C01"), 보관함의 칸이 주문입니다.

줄하는 일쉽게 말하면
tools = Orders(customer="C01")로그인한 고객 C01 로 도구를 만든다내 입주민 카드를 댄다
tools.get_order("O-1")C01 의 주문이라 상품까지 돌려준다내 칸이 열린다
tools.get_order("O-2")C02 의 주문이라 오류를 돌려준다옆집 칸은 열리지 않는다
tools.get_order("O-9")없는 주문이라 같은 오류를 돌려준다없는 칸 번호를 눌러도 똑같이 "열 수 없음"이 뜬다

결과의 둘째 줄과 셋째 줄은 글자 하나 다르지 않습니다. 남의 주문인지 없는 주문인지 바깥에서는 가릴 수 없다는 뜻입니다. 비유와 다른 점은, 실제 보관함은 빈 칸이 겉으로 보이기도 하지만 이 코드는 일부러 둘을 구별할 수 없게 만든다는 것입니다.

작게 줄여서 눈으로 보기

앞 절의 주의할 점에서 본 getattr 의 위험을 눈으로 봅니다. 객체에는 도구가 아닌 이름도 들어 있습니다.

Python
class Orders:
    def __init__(self, customer):
        self.customer = customer

    def get_order(self, order_id):
        return {"주문번호": order_id}


tools = Orders(customer="C01")
ALLOWED = {"get_order"}                              # 부를 수 있는 도구 이름
for name in ["get_order", "__init__", "customer"]:
    print(name, "있는 이름:", hasattr(tools, name), "/ 허용:", name in ALLOWED)
출력
get_order 있는 이름: True / 허용: True
__init__ 있는 이름: True / 허용: False
customer 있는 이름: True / 허용: False
결과읽는 법
get_order … True / 허용: True진짜 도구다. 있고, 허용 목록에도 있다
__init__ … True / 허용: False객체를 만드는 메서드도 이름으로 찾을 수 있다. 허용 목록이 없으면 LLM 이 이 이름을 불러도 막지 못한다
customer … True / 허용: False묶어 둔 고객 번호도 이름으로 찾힌다. 도구가 아니므로 허용 목록에서 걸러야 한다

hasattr(객체, 이름) 은 has attribute(그 이름의 속성을 가졌나)의 줄임말로, 그 이름이 객체에 있으면 True 를 돌려줍니다. 있다는 것과 불러도 된다는 것은 다릅니다.

주의할 점

  • 프롬프트에 "다른 사람의 정보는 알려 주지 마세요"라고 적는 것은 보조 수단입니다. 막는 것은 코드여야 합니다.
  • 도구가 돌려준 결과는 LLM 이 다시 읽으므로, 사용자가 봐서는 안 되는 칸(다른 고객의 주소, 내부 메모)은 처음부터 결과에 넣지 않습니다.
← 이전 절18장 · 답하는 AI 에서 일하는 AI 로다음 절 →18장 · 쓰기 작업은 확인을 거친다