18장. Project 03. 내 서버에서 일하는 AI Agent
답하는 AI 에서 일하는 AI 로
도서관 사서는 책이 어디 있는지 알려 줍니다. 비서는 책을 빌려 와 책상에 올려 두고, 반납일을 달력에 적습니다. 일하는 AI 는 비서에 가깝습니다. 말로 답하는 데서 그치지 않고 시스템의 상태를 바꿉니다. 그래서 무엇을 할 수 있게 둘지부터 정해야 합니다.
왼쪽 카드의 읽기 도구는 몇 번을 불러도 시스템이 그대로라, 에이전트가 마음껏 부르게 둡니다. 오른쪽 카드의 쓰기 도구는 부를 때마다 무언가가 바뀌므로 조건을 코드로 검사하고 사람의 확인을 거칩니다. 여기서 사서의 안내가 읽기 도구이고, 책을 빌려 오는 일이 쓰기 도구입니다.
답하는 AI 와 일하는 AI
| 견줄 것 | 답하는 AI(RAG 챗봇) | 일하는 AI(에이전트) |
|---|---|---|
| 결과 | 글로 된 답 | 답과 바뀐 시스템(신청, 접수) |
| 틀렸을 때 | 틀린 말 | 틀린 신청, 남의 정보 노출 |
| 채점 | 답에 정답 낱말이 있나 | 끝 상태가 기대와 같은가 |
| 막을 것 | 지어낸 답 | 권한 밖의 일, 확인 없는 쓰기 |
도구에 읽기·쓰기 표시 달기
TOOLS = {
"search_rules": {"쓰기": False, "설명": "사내 규정에서 관련 조항을 찾는다"},
"get_my_leave": {"쓰기": False, "설명": "내 남은 연차를 본다"},
"request_leave": {"쓰기": True, "설명": "휴가를 신청한다(팀장 결재 대기)"},
}
def allowed(name, call_count):
"""없는 도구는 막고, 쓰기 도구는 요청 하나에 한 번만 부르게 한다"""
if name not in TOOLS:
return False, "없는 도구"
if TOOLS[name]["쓰기"] and call_count.get(name, 0) >= 1:
return False, "쓰기 도구는 요청 하나에 한 번만"
return True, "통과"
count = {}
for name in ["search_rules", "request_leave", "request_leave", "delete_all"]:
ok, why = allowed(name, count)
count[name] = count.get(name, 0) + ok
print(f"{name:14s} {ok} ({why})")search_rules 와 첫 request_leave 는 통과하고, 두 번째 request_leave 와 목록에 없는 delete_all 은 막힙니다. LLM 이 무엇을 부르든, 실제로 실행되는 것은 목록에 있는 도구뿐입니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
TOOLS = {이름: {"쓰기": …, "설명": …}} | 도구마다 쓰기 도구인지와 설명을 적은 허용 목록이다. 여기에 없는 이름은 도구로 치지 않는다 |
name not in TOOLS | 그 이름이 허용 목록에 없으면 참이다. 없는 도구를 막는다 |
call_count.get(name, 0) | 사전에서 그 도구를 부른 횟수를 꺼낸다. 아직 한 번도 안 불렀으면 0 으로 본다 |
return False, "없는 도구" | 두 값을 함께 돌려준다. 앞은 통과 여부, 뒤는 까닭이다 |
count.get(name, 0) + ok | 통과했으면(True) 1 을, 막혔으면(False) 0 을 더한다. 실제로 실행된 횟수만 센다 |
f"{name:14s}" | 이름을 14칸짜리 글자(s = string, 문자열)로 맞춰 줄을 가지런히 한다 |
이 코드가 뜻하는 것
회사 비품실을 떠올려 봅니다. 볼펜과 메모지는 몇 번을 가져가도 됩니다. 노트북 대여는 한 사람이 한 번에 한 대만 됩니다. 목록에 없는 물건을 달라고 하면 "그런 물건은 없다"고 답합니다. 여기서 비품 목록이 TOOLS, 볼펜이 읽기 도구, 노트북이 쓰기 도구, 대여 장부가 count 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
TOOLS = {…} | 도구 셋과 쓰기 여부를 적는다 | 비품 목록에 "대여 물품" 표시를 단다 |
if name not in TOOLS | 목록에 없는 이름을 막는다 | 없는 물건은 내주지 않는다 |
if TOOLS[name]["쓰기"] and call_count.get(name, 0) >= 1 | 쓰기 도구를 이미 한 번 불렀으면 막는다 | 노트북을 이미 빌렸으면 더 못 빌린다 |
for name in [...] | 네 번의 호출을 차례로 시험한다 | 한 사람이 네 가지를 차례로 달라고 한다 |
count[name] = count.get(name, 0) + ok | 통과한 호출만 센다 | 실제로 내준 것만 장부에 적는다 |
결과의 넷째 줄 delete_all False (없는 도구) 는, LLM 이 그럴듯한 이름을 지어내도 목록 밖이면 아무 일도 일어나지 않는다는 뜻입니다. 비유와 다른 점은, 볼펜은 가져가면 줄어들지만 읽기 도구는 몇 번을 불러도 시스템이 그대로라는 것입니다.
작게 줄여서 눈으로 보기
count.get(name, 0) + ok 에서 참·거짓을 숫자에 더하는 부분만 떼어 봅니다.
count = {}
print(count.get("search_rules", 0)) # 아직 없는 열쇠는 0 으로 본다
print(0 + True, 0 + False) # True 는 1, False 는 0 으로 더해진다
count["request_leave"] = count.get("request_leave", 0) + True
print(count)0
1 0
{'request_leave': 1}| 결과 | 읽는 법 |
|---|---|
0 | 빈 사전에서 꺼내도 오류 없이 기본값 0 이 나온다 |
1 0 | 파이썬은 True 를 1, False 를 0 으로 셈한다 |
{'request_leave': 1} | 통과한 쓰기 도구가 장부에 1번으로 적혔다. 다음 호출부터는 >= 1 에 걸려 막힌다 |
주의할 점
- 도구 이름으로 함수를 찾을 때
getattr(객체, 이름)만 쓰면, LLM 이 지어낸 이름으로 엉뚱한 메서드(__init__등)를 부를 수 있습니다. 허용 목록을 먼저 검사합니다. - 쓰기 도구는 처음부터 많이 만들지 않습니다. 읽기 도구만으로 시작해 쓰임새를 본 뒤 하나씩 더합니다.