| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 파이썬
- fake model
- AI Agent
- 숫자 formatting
- list comprehension
- #독점의 저주 #Curse of Monopoly #카카오 #카카오택시 #마이크로소프트 #Internet Explorer(IE) #지갑 #독점 #사랑
- Markdown 코드블록
- TOOL 실행과 안전장치
- TOOL ROUTER
- #다산 정약용 #유배지에서 보낸 편지 #도덕 #용기 #염 #주역 #호연지기 #효제 #근검
- xls
- 오블완
- context window
- 에이전트
- # 암호(비밀번호) 분실 # 암호(비밀번호) 찾기 #오피스(doc
- 파워포인트) #집(zip)파일 #아래한글(HWP) #brute-force(무차별 대입)
- LLM
- LLM 종료이유 처리
- 시스템 프롬프트
- 티스토리챌린지
- ppt) 파일 #오피스(워드
- Agent loop
- #선진국 대한민국 #선진국 #대한민국 #아이들 #청소년 #고민 #해결 #심리
- #Netflix #넷플릭스 #자기잠식(Self-Cannibalization) #해법 중심 경영 #Disney Disney+ #Amazon Prime Video # Apple Apple TV+
- trim()
- JSON 문자열
- 인공지능
- 엑셀
- JSON parsing 파싱
- AI
- Today
- Total
아톨러브
V. AI Agent의 핵심 부품들 하나씩 만들어보기 본문
#-------- 1. Tool 실행과 안전장치
TOOLS = {"calculator": lambda a: eval_expr(a.get("expr",""))}
def eval_expr(expr):
return f"{expr}의 결과값"
def run_tool(name, args, steps, max_steps):
if steps >= max_steps:
return "중지되었습니다. 단계 제한에 도달하였습니다."
if name not in TOOLS:
return f"중지되었습니다. {name}이라는 툴이 없습니다."
if not isinstance(args, dict):
return "중지되었습니다. Arguments는 딕셔너리어야 합니다."
if not args:
return "중지되었습니다. Arguments가 없습니다."
return TOOLS[name](args)
print(run_tool("calculator", {"expr":"9*9"}, steps=1, max_steps=5))
print(run_tool("emailer", {"to":"x"}, steps=1, max_steps=5))
print(run_tool("calculator", {}, steps=1, max_steps=5))
print(run_tool("calculator", {"expr":1}, steps=5, max_steps=5))
#9*9의 결과값
# 중지되었습니다. emailer이라는 툴이 없습니다.
# 중지되었습니다. Arguments가 없습니다.
# 중지되었습니다. 단계 제한에 도달하였습니다.
#-------- 2. LLM 종료 이유(stop_reason) 처리
def describe(stop_reason):
match stop_reason:
case "end_turn":
return "종료되었습니다."
case "tool_use":
return "툴을 원합니다."
case "max_tokens" | "length":
return "값이 잘렸습니다."
case _:
return f"unknown ({stop_reason})"
for reason in ["end_turn", "tool_use", "length", "filtered"]:
print(reason, "->", describe(reason))
#-------- 3. 기본값 처리와 현재 상태 판단
temperature = None
steps = 4
max_steps = 5
temp = temperature if temperature is not None else 0.7
status = "last step" if steps == max_steps-1 else "in progress"
print(f"temperature = {temp} status = {status}")
#-------- 4. 간단한 규칙 기반 Tool Router
TOOLS = {"calculator", "search", "read_file", "finish"}
MATH_CHARS = set("0123456789+-*/")
def route(message):
text = message.strip().lower()
if not text:
return {"tool":None, "why":"메시지가 없습니다."}
if text in ("done", "thanks", "that is all"):
return {"tool":"finish", "why":"사용자가 완료 신호를 보냈습니다."}
if ".txt" in text or "file" in text:
return {"tool":"read_file", "why":"file을 언급하였습니다."}
digits = sum(1 for c in text if c in MATH_CHARS)
if digits >=3:
return {"tool":"calculator", "why":"수학계산처럼 보입니다."}
return {"tool":"search", "why":"디폴트가 lookup입니다."}
messages = [
"what is 150*13",
"notes.txt 파일을 열고, 그것을 요약하세요.",
"누가 그 회사를 발견했어요",
"done",
"",
]
for m in messages:
decision = route(m)
tool = decision["tool"] or "none"
print(f"{m.strip()[:32]:<40} -> {tool:<11}({decision['why']})")
1. 첫 번째 코드: Tool 실행과 안전장치
TOOLS = {"calculator": lambda a: eval_expr(a.get("expr",""))}
def eval_expr(expr):
return f"{expr}의 결과값"
먼저 이 부분부터 보겠습니다.
1-1. TOOLS는 무엇인가?
TOOLS = {
"calculator": lambda a: eval_expr(a.get("expr",""))
}
TOOLS는 사용 가능한 Tool들을 모아놓은 사전(Dictionary) 입니다.
구조는 이렇게 생각하면 됩니다.
TOOLS
│
└── calculator
│
└── 실행할 함수
즉:
TOOLS["calculator"]
를 실행하면 계산기 역할을 하는 함수가 실행됩니다.
1-2. lambda가 조금 복잡해 보이는데
이 코드:
lambda a: eval_expr(a.get("expr",""))
를 일반 함수로 바꾸면 훨씬 이해하기 쉽습니다.
def calculator_tool(a):
return eval_expr(a.get("expr", ""))
그러면:
TOOLS = {
"calculator": calculator_tool
}
와 거의 같은 의미입니다.
즉 전체를 풀어쓰면:
def calculator_tool(args):
expr = args.get("expr", "")
return eval_expr(expr)
TOOLS = {
"calculator": calculator_tool
}
입니다.
2. eval_expr()
def eval_expr(expr):
return f"{expr}의 결과값"
이 함수는 실제 계산을 하는 것이 아닙니다.
예를 들어:
eval_expr("9*9")
를 실행하면:
9*9의 결과값
을 반환합니다.
즉 지금은 계산기 Tool의 동작을 흉내 내는 예제입니다.
나중에는 실제로:
eval()
같은 것을 사용할 수도 있지만, 실제 서비스에서는 eval() 사용에 보안 문제가 있으므로 주의해야 합니다.

3. run_tool() — Tool 실행 관리자
def run_tool(name, args, steps, max_steps):
이 함수는 Agent 입장에서 보면:
LLM이 요청한 Tool을 실제로 실행하기 전에 여러 가지 안전 검사를 하는 관리자
입니다.
구조는:
LLM
↓
"calculator를 사용하세요"
↓
run_tool()
↓
① Step 제한 확인
② Tool 존재 확인
③ Arguments 형식 확인
④ Arguments 존재 확인
↓
실제 Tool 실행
입니다.
3-1. Step 제한 확인
if steps >= max_steps:
return "중지되었습니다. 단계 제한에 도달하였습니다."
예를 들어:
steps = 5
max_steps = 5
라면:
5 >= 5
이므로 True입니다.
Agent가 계속:
LLM
↓
Tool
↓
LLM
↓
Tool
↓
LLM
↓
Tool
무한 반복하는 것을 막기 위한 장치입니다.
이전 코드에서 배운:
MAX_STEPS = 6
와 정확히 같은 개념입니다.
4. Tool이 존재하는지 확인
if name not in TOOLS:
return f"중지되었습니다. {name}이라는 툴이 없습니다."
예를 들어:
name = "emailer"
인데:
TOOLS = {
"calculator": ...
}
밖에 없다면:
emailer not in TOOLS
입니다.
따라서:
중지되었습니다. emailer이라는 툴이 없습니다.
를 반환합니다.
이것은 매우 중요한 안전장치입니다.
LLM이 가끔 존재하지 않는 Tool을 만들어낼 수도 있기 때문입니다.
예:
send_email
google_search
database_query
super_calculator
Agent 입장에서는:
"내가 실제로 가지고 있는 Tool인지 확인해야 한다."
는 것입니다.
5. Arguments가 Dictionary인지 확인
if not isinstance(args, dict):
return "중지되었습니다. Arguments는 딕셔너리어야 합니다."
isinstance()는:
이 객체가 특정 타입인가?
를 검사합니다.
예:
isinstance({"expr":"9*9"}, dict)
결과:
True
하지만:
isinstance("9*9", dict)
결과:
False
입니다.
즉 Agent가 예상하는:
{
"expr": "9*9"
}
형태가 아니라:
"9*9"
같은 형태가 들어오면 실행하지 않습니다.
6. Arguments가 비어있는지 확인
if not args:
return "중지되었습니다. Arguments가 없습니다."
예:
args = {}
빈 dictionary입니다.
Python에서 빈 객체는 대부분 False처럼 취급됩니다.
{} → False
[] → False
"" → False
None → False
따라서:
if not args:
는:
Arguments가 비어있으면
이라는 의미입니다.
7. 실제 Tool 실행
모든 검사를 통과하면:
return TOOLS[name](args)
를 실행합니다.
예:
name = "calculator"
args = {
"expr": "9*9"
}
그러면:
TOOLS["calculator"](args)
입니다.
TOOLS["calculator"]에는:
lambda a: eval_expr(a.get("expr",""))
가 들어 있습니다.
따라서:
lambda a: eval_expr(a.get("expr",""))({"expr":"9*9"})
↓
eval_expr("9*9")
↓
9*9의 결과값
이 됩니다.
8. 전체 실행 흐름
print(run_tool(
"calculator",
{"expr":"9*9"},
steps=1,
max_steps=5
))
흐름:
run_tool()
│
├─ steps >= max_steps?
│ 1 >= 5 → False
│
├─ calculator in TOOLS?
│ True
│
├─ args는 dict?
│ True
│
├─ args가 비었는가?
│ False
│
└─ calculator 실행
↓
eval_expr("9*9")
↓
"9*9의 결과값"
9. 두 번째 코드: match / case
def describe(stop_reason):
match stop_reason:
이것은 Python의 match-case 문법입니다.
Java의 switch와 비슷하다고 생각하면 됩니다.
기존 방식
예전 방식은:
if stop_reason == "end_turn":
...
elif stop_reason == "tool_use":
...
elif stop_reason == "max_tokens":
...
였습니다.
match-case 방식
match stop_reason:
case "end_turn":
return "종료되었습니다."
의 의미:
stop_reason 값이 "end_turn"이면 실행해라.
"max_tokens" | "length"
case "max_tokens" | "length":
return "값이 잘렸습니다."
이것은:
"max_tokens" 또는 "length" 둘 중 하나면
이라는 의미입니다.
즉:
max_tokens
OR
length
입니다.
_
case _:
return f"unknown ({stop_reason})"
_는:
위의 어느 경우에도 해당하지 않으면
입니다.
즉 default입니다.
Java의:
default:
와 비슷합니다.
결과
for reason in ["end_turn", "tool_use", "length", "filtered"]:
실행하면:
end_turn -> 종료되었습니다.
tool_use -> 툴을 원합니다.
length -> 값이 잘렸습니다.
filtered -> unknown (filtered)
가 됩니다.
10. Agent에서 stop_reason은 왜 중요한가?
LLM의 응답을 받았을 때 Agent는:
LLM 응답
↓
stop_reason 확인
을 해야 합니다.
예:
end_turn
↓
정상적으로 답변 종료
tool_use
↓
Tool 실행 필요
max_tokens
↓
응답이 너무 길어서 잘림
filtered
↓
특별한 처리 필요
입니다.
따라서 Agent는 stop_reason에 따라 행동을 바꿔야 합니다.
11. 세 번째 코드: 기본값 처리
temperature = None
steps = 4
max_steps = 5
여기서:
temperature = None
은:
아직 temperature 값이 지정되지 않았다.
는 의미입니다.
삼항 연산자
temp = temperature if temperature is not None else 0.7
처음 보면 조금 복잡합니다.
일반적인 if문으로 바꾸면:
if temperature is not None:
temp = temperature
else:
temp = 0.7
입니다.
즉:
temperature가 있으면 그것을 사용하고, 없으면 0.7을 사용해라.
현재:
temperature = None
이므로:
temp = 0.7
이 됩니다.
12. 왜 is not None을 사용하는가?
이 부분은 중요합니다.
만약:
if temperature:
라고 하면:
temperature = 0
인 경우도 False로 판단됩니다.
하지만:
temperature = 0
은 실제 값입니다.
따라서:
if temperature is not None:
이라고 하면:
0 → 값 있음
0.5 → 값 있음
1.0 → 값 있음
None → 값 없음
을 정확하게 구분할 수 있습니다.
13. 현재가 마지막 Step인가?
status = "last step" if steps == max_steps-1 else "in progress"
현재:
steps = 4
max_steps = 5
입니다.
따라서:
max_steps - 1
=
5 - 1
=
4
그리고:
steps == 4
이므로:
4 == 4
True입니다.
그래서:
status = "last step"
이 됩니다.
왜 max_steps가 아니라 max_steps - 1일까?
Agent의 Step을:
Step 0
Step 1
Step 2
Step 3
Step 4
로 사용한다고 가정하면 총 5번입니다.
마지막 Step은:
max_steps - 1
입니다.
즉:
0부터 시작하는 index
개념과 관련이 있습니다.
14. 네 번째 코드: Tool Router
이 부분이 상당히 재미있습니다.
TOOLS = {"calculator", "search", "read_file", "finish"}
이번에는 Dictionary가 아니라 Set입니다.
왜냐하면:
TOOLS["calculator"]
처럼 함수를 실행하는 것이 아니라:
어떤 Tool들이 존재하는지 목록만 관리
하기 때문입니다.
15. MATH_CHARS
MATH_CHARS = set("0123456789+-*/")
결과는 대략:
{
'0','1','2','3','4','5','6','7','8','9',
'+','-','*','/'
}
입니다.
문자열의 문자들을 Set으로 만드는 것입니다.
16. route() 함수
def route(message):
이 함수의 역할은:
사용자 메시지를 보고 어떤 Tool을 사용할지 결정
하는 것입니다.
즉 간단한 Tool Router입니다.
실제 LLM 대신 아주 단순한 규칙을 사용합니다.
17. 문자열 정리
text = message.strip().lower()
두 가지를 합니다.
.strip()
앞뒤 공백 제거
" Hello "
↓
"Hello"
.lower()
소문자로 변환
HELLO
↓
hello
따라서:
" DONE "
↓
"done"
으로 만들어서 비교하기 쉬워집니다.
18. 빈 메시지
if not text:
return {
"tool":None,
"why":"메시지가 없습니다."
}
사용자가:
""
를 입력하거나:
" "
처럼 공백만 입력하면:
tool = None
으로 처리합니다.
19. 완료 신호 감지
if text in ("done", "thanks", "that is all"):
text가 다음 중 하나면:
done
thanks
that is all
작업이 끝났다고 판단합니다.
그리고:
return {
"tool":"finish",
"why":"사용자가 완료 신호를 보냈습니다."
}
를 반환합니다.
즉:
사용자
↓
done
↓
Router
↓
finish Tool
입니다.
20. 파일 관련 메시지 감지
if ".txt" in text or "file" in text:
예:
notes.txt 파일을 열어줘
에는:
.txt
가 들어 있습니다.
따라서:
{
"tool":"read_file"
}
를 반환합니다.
21. 수학 문자 개수 세기
digits = sum(
1 for c in text
if c in MATH_CHARS
)
이 코드는 처음 보면 어렵습니다.
풀어쓰면:
digits = 0
for c in text:
if c in MATH_CHARS:
digits += 1
입니다.
즉 메시지 안에:
숫자
+
-
*
/
같은 문자가 몇 개 있는지 세는 것입니다.
22. "what is 150*13" 분석
문자 중:
1
5
0
*
1
3
은 MATH_CHARS에 포함됩니다.
따라서 최소 6개 정도가 계산됩니다.
그리고:
if digits >= 3:
조건을 만족합니다.
따라서:
{
"tool":"calculator",
"why":"수학계산처럼 보입니다."
}
가 됩니다.
23. 기본값은 search
위 조건에 모두 해당하지 않으면:
return {
"tool":"search",
"why":"디폴트가 lookup입니다."
}
입니다.
즉:
계산도 아니고
파일도 아니고
완료도 아니고
빈 메시지도 아니다
↓
search
입니다.
24. Router 전체 구조
이 코드를 그림으로 보면:
사용자 메시지
↓
┌───────────────┐
│ 빈 메시지인가? │
└───────┬───────┘
│ Yes
↓
None
No
↓
┌────────────────┐
│ 완료 신호인가? │
└────────┬───────┘
│ Yes
↓
finish
No
↓
┌────────────────┐
│ 파일을 언급? │
└────────┬───────┘
│ Yes
↓
read_file
No
↓
┌────────────────┐
│ 계산처럼 보임? │
└────────┬───────┘
│ Yes
↓
calculator
No
↓
search
이것이 바로 Rule-based Router입니다.
25. 마지막 출력 코드
for m in messages:
decision = route(m)
각 메시지를 Router에 넣습니다.
예:
m = "what is 150*13"
↓
decision = {
"tool":"calculator",
"why":"수학계산처럼 보입니다."
}
or "none"
tool = decision["tool"] or "none"
만약:
decision["tool"] = None
이면:
None or "none"
이므로:
"none"
을 사용합니다.
즉 화면 출력용 기본값입니다.
26. 출력 포맷
print(
f"{m.strip()[:32]:<40} -> {tool:<11}({decision['why']})"
)
이것도 지금까지 배운 f-string 정렬 기능입니다.
m.strip()[:32]
문자열:
- 앞뒤 공백 제거
- 앞에서 32글자만 사용
:<40
40칸의 공간을 확보하고 왼쪽 정렬합니다.
메시지 -> calculator
이렇게 보기 좋게 정렬됩니다.
27. 예상 결과
대략 이렇게 나옵니다.
what is 150*13 -> calculator (수학계산처럼 보입니다.)
notes.txt 파일을 열고, 그것을 요약하세요. -> read_file (file을 언급하였습니다.)
누가 그 회사를 발견했어요 -> search (디폴트가 lookup입니다.)
done -> finish (사용자가 완료 신호를 보냈습니다.)
-> none (메시지가 없습니다.)
28. 이번 코드에서 가장 중요한 개념
이번 코드는 사실 Agent의 내부 구조를 아주 작은 단위로 나눈 것입니다.
┌──────────────────┐
│ User Input │
└────────┬─────────┘
↓
┌──────────────────┐
│ Tool Router │
│ route(message) │
└────────┬─────────┘
↓
calculator?
search?
read_file?
↓
┌──────────────────┐
│ run_tool() │
│ │
│ Step 확인 │
│ Tool 존재 확인 │
│ Args 확인 │
└────────┬─────────┘
↓
┌──────────────────┐
│ 실제 Tool 실행 │
└────────┬─────────┘
🌟 지금까지 배운 Agent 코드와 연결하면
지금까지 공부하신 코드들을 하나로 합치면 Agent는 이렇게 동작합니다.
사용자 질문
↓
history에 저장
↓
System Prompt + Tools
↓
LLM 호출
↓
응답 분석
│
├── end_turn
│ ↓
│ 최종 답변
│
└── tool_use
↓
tool 이름 추출
↓
args 추출
↓
run_tool()
│
├── MAX_STEPS 확인
├── Tool 존재?
├── args 정상?
│
↓
Tool 실행
↓
결과를 history에 추가
↓
다시 LLM 호출
한 문장으로 정리하면
이번 코드는 "LLM이 어떤 Tool을 요청했을 때, 그 Tool을 안전하게 선택하고 실행하며, 실행 횟수를 제한하는 Agent의 핵심 안전장치와 Router를 배우는 코드"입니다.
특히 지금까지 공부하신 Agent 코드 흐름에서 보면 run_tool()은 Agent의 실행 엔진, route()는 간단한 판단기, match/case는 LLM 응답 상태 처리기라고 이해하시면 가장 정확합니다.
'AI, 클라우드, 문서, 자동화 > AI_AGENT' 카테고리의 다른 글
| VI. Python의 반복문과 자료구조를 이용해 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 |
| II. AI Agent를 만들 때 자주 사용하는 패턴들 (0) | 2026.08.22 |
| I. Python 실행 환경이 제대로 갖춰져 있는지 확인하는 간단한 점검 프로그램 (0) | 2026.08.22 |
