| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- key error
- 티스토리챌린지
- 파이썬
- 인공지능
- Agent loop
- AI Agent
- 재시도 retry
- LLM
- 에이전트
- TOOL 실행과 안전장치
- 예외처리 exception
- #다산 정약용 #유배지에서 보낸 편지 #도덕 #용기 #염 #주역 #호연지기 #효제 #근검
- 오블완
- keyword argument
- JSON 문자열
- #선진국 대한민국 #선진국 #대한민국 #아이들 #청소년 #고민 #해결 #심리
- Tool Registry
- 엑셀
- 파워포인트) #집(zip)파일 #아래한글(HWP) #brute-force(무차별 대입)
- TOOL ROUTER
- AI
- fake model
- Mutable Default Argument
- ppt) 파일 #오피스(워드
- LLM 종료이유 처리
- # 암호(비밀번호) 분실 # 암호(비밀번호) 찾기 #오피스(doc
- Markdown 코드블록
- list comprehension
- try / except / else / finally
- xls
- Today
- Total
아톨러브
VII. Python 함수 → Tool 함수 → Agent의 Tool Dispatcher 구조 본문
VII. Python 함수 → Tool 함수 → Agent의 Tool Dispatcher 구조
아톨 2026. 8. 23. 15:50
#-------- 1. 함수의 기본값과 키워드 인수
def search(query, limit=3, safe=True):
mode = "safe" if safe else "unfiltered"
return f"searching '{query}' | limit={limit} | {mode}"
print(search("환불정책"))
print(search("환불정책", 10))
print(search("환불정책", limit=5, safe=False))
print(search(limit=1, query="환불정책"))
#-------- 2. 함수 기본값의 함정 (mutable default)
def add_bad(item, bucket=[]):
bucket.append(item)
return bucket
def add_good(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_bad("a"), add_bad("b"), add_bad("c"))
print(add_good("a"), add_good("b"), add_good("c"))
#-------- 3. 함수의 표준화된 반환값
def read_stock(sku):
inventory = {"5441": 200, "9876": 0}
if sku not in inventory:
return {"ok":False, "error":f"제품번호 {sku}는 자료가 없습니다.", "data":None}
return {"ok":True, "error":None, "data":{"sku":sku, "units":inventory[sku]}}
for sku in ["5441", "1234"]:
result = read_stock(sku)
if result["ok"]:
print(f"{sku}: {result['data']['units']}개의 재고가 있습니다.")
else:
print(f"{sku}: 실패하였습니다 - {result['error']}")
#-------- 4. 지역변수와 외부변수
counter = 0
def bump_local():
counter = 99
return counter
def bump_properly(current):
return current+1
print(bump_local(), "but outside it is still", counter)
counter = bump_properly(counter)
counter = bump_properly(counter)
print("after passing it in and out", counter)
#-------- 5. *args, **kwargs
def log(*parts, **fields):
line = " ".join(str(p) for p in parts)
extras = ", ".join(f"{k} = {v}" for k, v in fields.items())
print(f"{line} | {extras}")
log("tool", "finished", tool="search", ms=150, ok=True)
def make_note(title, body, pinned=False):
star = "*" if pinned else "-"
return f"{star} {title}: {body}"
args_from_model = {"title":"환불정책", "body":"30일 이내", "pinned":True}
print(make_note(**args_from_model))
#-------- 6. 함수를 변수에 저장하기
def add(a,b):
return a+b
operation = add
print(operation)
print(operation(10,100))
#-------- 7. Tool Registry + Dispatcher
def calculator(expr):
allowed = set("0123456789+-*/().")
if not set(expr) <= allowed:
return "표현식에 지원되지 않는 문자가 포함되어 있습니다."
return str(eval(expr))
def word_count(text):
return f"{len(text.split())} words"
def reverse(text):
return text[::-1]
TOOLS = {
"calculator":calculator,
"word_count":word_count,
"reverse":reverse,
}
def dispatch(name, args):
if name not in TOOLS:
known = ",".join(TOOLS)
return {"ok":False, "result":None, "error":f"unknown tool '{name}', try: {known}"}
if not isinstance(args, dict):
return {"ok":False, "result":None, "error":"인수(args)는 딕셔너리이어야 합니다."}
try:
value= TOOLS[name](**args)
return {"ok":True, "result":value, "error": None}
except TypeError as e:
return {"ok":False, "result":None, "error":f"잘못된 인수: {e}"}
calls = [
("calculator", {"expr":"(50*3)+15"}),
("word_count", {"text":"에이전트는 루프입니다."}),
("reverse", {"text":"stressed"}),
("emailer", {"to":"boss"}),
("calculator", {"wrong":"5+5"})
]
for name, args in calls:
out = dispatch(name, args)
status = "OK" if out["ok"] else "FAIL"
print(f"{status:>5} {name:<10} {out['result'] or out['error']}")
1. 함수의 기본값과 키워드 인수
def search(query, limit=3, safe=True):
mode = "safe" if safe else "unfiltered"
return f"searching '{query}' | limit={limit} | {mode}"
이 함수의 구조는:
search(
query, ← 반드시 필요
limit=3, ← 기본값 3
safe=True ← 기본값 True
)
입니다.
query
query
검색어입니다.
기본값이 없으므로 반드시 넣어야 합니다.
search("환불정책")
limit=3
limit=3
검색 결과 개수의 기본값입니다.
사용자가 지정하지 않으면 자동으로:
limit = 3
이 됩니다.
safe=True
safe=True
안전 검색 여부입니다.
지정하지 않으면:
safe = True
입니다.
호출 1
print(search("환불정책"))
실제로는:
search(
query="환불정책",
limit=3,
safe=True
)
처럼 동작합니다.
결과:
searching '환불정책' | limit=3 | safe
호출 2
print(search("환불정책", 10))
순서대로 전달됩니다.
query → "환불정책"
limit → 10
safe → True
결과:
searching '환불정책' | limit=10 | safe
호출 3
print(search("환불정책", limit=5, safe=False))
여기서는 이름을 지정했습니다.
limit=5
safe=False
이런 방식을 키워드 인수(keyword argument)라고 합니다.
결과:
searching '환불정책' | limit=5 | unfiltered
호출 4
print(search(limit=1, query="환불정책"))
매개변수 순서가 바뀌었습니다.
하지만 이름을 명시했기 때문에 가능합니다.
limit → 1
query → 환불정책
즉:
search(
limit=1,
query="환불정책"
)
는 정상입니다.
Agent에서 왜 중요할까?
LLM이 Tool을 호출할 때 보통 이런 JSON을 만듭니다.
{
"query": "환불정책",
"limit": 5,
"safe": False
}
그리고 Python에서는:
search(**args)
처럼 호출할 수 있습니다.
2. 매우 중요한 개념
Mutable Default Argument의 함정
def add_bad(item, bucket=[]):
bucket.append(item)
return bucket
이 코드는 겉보기에는 문제가 없어 보입니다.
하지만:
print(add_bad("a"))
print(add_bad("b"))
print(add_bad("c"))
결과는:
['a']
['a', 'b']
['a', 'b', 'c']
가 됩니다.
왜냐하면:
bucket=[]
가 매번 새로 만들어지는 것이 아니기 때문입니다.
실제 내부 느낌
Python은 기본값 리스트를 한 번 만들어 놓고 재사용합니다.
bucket
↓
[]
첫 번째:
add_bad("a")
↓
['a']
두 번째:
add_bad("b")
↓
기존 리스트를 다시 사용:
['a', 'b']
세 번째:
['a', 'b', 'c']
올바른 방법
def add_good(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
기본값으로:
None
을 사용합니다.
그리고 함수가 호출될 때마다:
if bucket is None:
bucket = []
새로운 리스트를 만듭니다.
결과
print(add_good("a"))
print(add_good("b"))
print(add_good("c"))
결과:
['a']
['b']
['c']
입니다.
이건 정말 중요합니다.
Agent에서 다음처럼 쓰면 위험할 수 있습니다.
def run(history=[]):
대신:
def run(history=None):
if history is None:
history = []
를 사용하는 것이 안전합니다.

3. Tool의 표준화된 반환값
def read_stock(sku):
재고를 읽는 Tool입니다.
재고 데이터
inventory = {
"5441": 200,
"9876": 0
}
즉:
제품번호 5441 → 재고 200개
제품번호 9876 → 재고 0개
입니다.
제품이 없는 경우
if sku not in inventory:
return {
"ok":False,
"error":f"제품번호 {sku}는 자료가 없습니다.",
"data":None
}
예:
read_stock("1234")
결과:
{
"ok": False,
"error": "제품번호 1234는 자료가 없습니다.",
"data": None
}
정상인 경우
return {
"ok":True,
"error":None,
"data":{
"sku":sku,
"units":inventory[sku]
}
}
예:
read_stock("5441")
결과:
{
"ok": True,
"error": None,
"data": {
"sku": "5441",
"units": 200
}
}
왜 이렇게 복잡하게 반환할까?
단순히:
return 200
하면 간단합니다.
하지만 실패했을 때 문제가 생깁니다.
제품이 없음
서버 오류
권한 없음
시간 초과
이런 상황을 표현하기 어렵습니다.
그래서 Agent Tool에서는 보통:
ok
data
error
같은 표준 구조를 사용합니다.
Tool 결과
│
├── ok → 성공 여부
├── data → 실제 데이터
└── error → 실패 이유
이것은 실제 Agent Tool 설계에서 매우 좋은 습관입니다.
4. 지역변수와 외부변수
counter = 0
전역 변수입니다.
bump_local()
def bump_local():
counter = 99
return counter
함수 안에서:
counter = 99
를 만들었습니다.
하지만 이것은 밖의 counter가 아닙니다.
밖
counter = 0
함수 안
counter = 99
서로 다른 변수입니다.
따라서:
print(bump_local(), "but outside it is still", counter)
결과:
99 but outside it is still 0
입니다.
좋은 방식
def bump_properly(current):
return current+1
현재 값을 함수에 넣고:
counter = bump_properly(counter)
결과를 다시 받습니다.
처음:
counter = 0
첫 번째:
bump_properly(0)
↓
1
두 번째:
bump_properly(1)
↓
2
최종:
counter = 2
Agent에서 좋은 이유
이런 방식:
step = increase(step)
은 상태 변화가 명확합니다.
Agent에서는:
history
step
answer
cost
token
등 상태가 많기 때문에 함수에 값을 넣고 결과를 받는 방식이 이해하기 쉽고 안전합니다.
5. *args와 **kwargs
def log(*parts, **fields):
이 부분은 매우 중요합니다.
*parts
여러 개의 위치 인수를 받습니다.
log(
"tool",
"finished",
...
)
함수 안에서는:
parts
가:
("tool", "finished")
라는 tuple이 됩니다.
**fields
이름이 붙은 여러 값을 받습니다.
tool="search",
ms=150,
ok=True
함수 안에서는:
fields
가:
{
"tool":"search",
"ms":150,
"ok":True
}
가 됩니다.
첫 번째 부분
line = " ".join(str(p) for p in parts)
parts:
("tool", "finished")
↓
tool finished
두 번째 부분
extras = ", ".join(
f"{k} = {v}"
for k, v in fields.items()
)
결과:
tool = search, ms = 150, ok = True
최종
tool finished | tool = search, ms = 150, ok = True
이런 함수는 Agent 로그를 만들 때 매우 유용합니다.
6. **dictionary로 함수 호출
def make_note(title, body, pinned=False):
필요한 값:
title
body
pinned
입니다.
그런데 모델이 JSON 형태로 보내왔다고 합시다.
args_from_model = {
"title":"환불정책",
"body":"30일 이내",
"pinned":True
}
이걸:
make_note(**args_from_model)
이라고 호출합니다.
그러면 Python이 자동으로:
make_note(
title="환불정책",
body="30일 이내",
pinned=True
)
로 풀어줍니다.
이게 바로:
function(**args)
입니다.
이것이 Tool Calling의 핵심입니다.
LLM
{
"tool": "make_note",
"args": {
"title": "...",
"body": "...",
"pinned": true
}
}
↓
Python
TOOLS[name](**args)
7. Python에서는 함수도 변수에 저장할 수 있다
def add(a,b):
return a+b
함수 이름 자체를 변수에 넣을 수 있습니다.
operation = add
중요한 것은:
operation = add
와:
operation = add()
는 다릅니다.
함수 자체 저장
operation = add
↓
operation
↓
add 함수
따라서:
operation(10,100)
는:
add(10,100)
과 같습니다.
결과:
110
이 개념이 바로:
TOOLS = {
"calculator": calculator,
"search": search,
}
같은 Tool Registry를 가능하게 합니다.
8. 마지막 코드
⭐ 실제 Agent Tool Dispatcher 구조
이 부분이 가장 중요합니다.
8-1. calculator
def calculator(expr):
계산기 Tool입니다.
허용 문자
allowed = set("0123456789+-*/().")
즉 허용되는 문자:
숫자
+
-
*
/
(
)
.
입니다.
안전 검사
if not set(expr) <= allowed:
예:
expr = "10+20"
문자를 Set으로 만들면:
{'1','0','+','2'}
이것들이 모두:
allowed
안에 있으면 OK입니다.
하지만:
expr = "import os"
라면:
i
m
p
o
r
t
...
같은 허용되지 않은 문자가 있습니다.
따라서 거부합니다.
표현식에 지원되지 않는 문자가 포함되어 있습니다.
eval()
return str(eval(expr))
예:
eval("(50*3)+15")
↓
165
따라서 문자열:
"165"
를 반환합니다.
참고로 실제 서비스에서는 이 정도 문자 검사만으로 eval()을 완전히 안전하다고 보기는 어렵습니다. 실무에서는 AST 기반 계산기 등을 사용하는 것이 더 안전합니다.
8-2. word_count
def word_count(text):
return f"{len(text.split())} words"
예:
text = "에이전트는 루프입니다."
text.split()
↓
["에이전트는", "루프입니다."]
길이는:
2
결과:
2 words
8-3. reverse
def reverse(text):
return text[::-1]
문자열을 거꾸로 뒤집습니다.
예:
stressed
↓
desserts
[::-1]은 문자열 전체를 역순으로 가져오는 Python 문법입니다.
9. Tool Registry
TOOLS = {
"calculator":calculator,
"word_count":word_count,
"reverse":reverse,
}
이것은 Agent가 사용할 수 있는 Tool 목록입니다.
TOOLS
│
├── calculator ───→ calculator()
│
├── word_count ───→ word_count()
│
└── reverse ──────→ reverse()
10. Dispatcher
def dispatch(name, args):
역할은:
Tool 이름과 Arguments를 받아서 적절한 Tool을 찾아 실행하는 중앙 관리자
입니다.
실제 흐름:
LLM
↓
{
"tool": "calculator",
"args": {
"expr": "(50*3)+15"
}
}
↓
dispatch()
↓
TOOLS에서 calculator 찾기
↓
calculator(**args)
↓
결과 반환
11. Tool 존재 확인
if name not in TOOLS:
예:
name = "emailer"
하지만:
TOOLS = {
"calculator": ...,
"word_count": ...,
"reverse": ...
}
에는 없습니다.
따라서:
known = ",".join(TOOLS)
결과:
calculator,word_count,reverse
오류:
unknown tool 'emailer', try: calculator,word_count,reverse
12. args가 Dictionary인지 확인
if not isinstance(args, dict):
Tool 호출은 보통:
{
"expr":"50*3"
}
처럼 Dictionary여야 합니다.
예:
"50*3"
같은 문자열이면:
FAIL
처리합니다.
13. 가장 중요한 부분
value = TOOLS[name](**args)
이 한 줄이 핵심입니다.
예:
name = "calculator"
args = {
"expr":"(50*3)+15"
}
먼저:
TOOLS[name]
↓
TOOLS["calculator"]
↓
calculator
그리고:
calculator(**args)
↓
calculator(expr="(50*3)+15")
↓
165
14. try / except TypeError
try:
value = TOOLS[name](**args)
예를 들어 모델이:
{
"wrong":"5+5"
}
를 보냈다고 합시다.
그러면:
calculator(wrong="5+5")
가 됩니다.
하지만 calculator 함수는:
def calculator(expr):
이므로:
expr
이 필요합니다.
따라서 TypeError가 발생합니다.
이걸 잡아서:
except TypeError as e:
에러를 프로그램 전체가 죽지 않도록 처리합니다.
결과:
{
"ok":False,
"result":None,
"error":"잘못된 인수: ..."
}
15. calls 실행
첫 번째:
("calculator", {"expr":"(50*3)+15"})
↓
165
두 번째:
("word_count", {"text":"에이전트는 루프입니다."})
↓
2 words
세 번째:
("reverse", {"text":"stressed"})
↓
desserts
네 번째:
("emailer", {"to":"boss"})
↓
Tool 없음
FAIL
다섯 번째:
("calculator", {"wrong":"5+5"})
↓
인수 이름이 잘못됨
FAIL
예상 출력 구조
OK calculator 165
OK word_count 2 words
OK reverse desserts
FAIL emailer unknown tool 'emailer', try: calculator,word_count,reverse
FAIL calculator 잘못된 인수: calculator() got an unexpected keyword argument 'wrong'
🌟 이번 코드 전체의 가장 중요한 흐름
이제 지금까지 배운 내용을 연결하면:
사용자 질문
↓
LLM 판단
↓
Tool Call 생성
↓
{
"tool": "calculator",
"args": {
"expr": "(50*3)+15"
}
}
↓
dispatch(name, args)
↓
Tool 존재 확인
↓
args 형식 확인
↓
TOOLS[name]
↓
실제 함수 선택
↓
TOOLS[name](**args)
↓
Tool 실행 결과
↓
{
"ok": True,
"result": "165",
"error": None
}
이번 코드에서 꼭 기억할 핵심 5개
① 기본값
def search(query, limit=3):
필요한 경우에만 값을 변경할 수 있습니다.
② 리스트 기본값 주의
❌ 위험:
def func(items=[]):
⭕ 안전:
def func(items=None):
if items is None:
items = []
③ Tool 결과는 표준화
{
"ok": True,
"result": ...,
"error": None
}
또는:
{
"ok": False,
"result": None,
"error": "..."
}
④ 함수도 변수에 넣을 수 있다
TOOLS = {
"calculator": calculator
}
⑤ Agent Tool Calling의 핵심
TOOLS[name](**args)
이 한 줄입니다.
LLM이:
name = "calculator"
args = {"expr":"5+5"}
를 보내면:
TOOLS[name](**args)
↓
calculator(expr="5+5")
가 됩니다.
한 문장으로 정리하면
이번 코드는 AI Agent가 LLM으로부터 받은 tool 이름 + arguments를 안전하게 검증하고, 실제 Python 함수를 찾아 실행한 뒤, 성공 또는 실패 결과를 일정한 형식으로 반환하는 Tool Dispatcher 구조를 배우는 코드입니다.
'AI, 클라우드, 문서, 자동화 > AI_AGENT' 카테고리의 다른 글
| VIII. Agent Loop + Tool Dispatcher 다음 단계인 예외 처리(Exception), 재시도(Retry), Logging 스터디 (0) | 2026.08.23 |
|---|---|
| VI. Python의 반복문과 자료구조를 이용해 AI Agent의 실제 실행 흐름을 만드는 과정 (0) | 2026.08.23 |
| V. AI Agent의 핵심 부품들 하나씩 만들어보기 (0) | 2026.08.23 |
| IV. AI Agent가 LLM의 응답을 JSON으로 받고, 그 안에서 Tool 이름과 인자(arguments)를 꺼내서 실제 Tool 실행으로 연결하기 위한 기초 (0) | 2026.08.22 |
| III. 대화 기록(history)을 어떻게 Python으로 관리하는가 (0) | 2026.08.22 |
