18장. Project 03. 내 서버에서 일하는 AI Agent
실행 기록 남기기
택배 회사는 물건이 어느 센터를 언제 지났는지 하나하나 남깁니다. 물건이 사라지면 기록을 보고 어디서 멈췄는지 찾습니다. 에이전트도 요청 하나를 처리하며 부른 도구와 결과를 차례로 남겨, 틀린 답이 나왔을 때 어느 걸음에서 어긋났는지 찾습니다.
네 카드가 실행 기록 한 줄의 칸입니다. 몇 번째 걸음인지, 어떤 도구를 어떤 인자로 불렀는지, 도구가 무엇을 돌려줬는지, 얼마나 걸렸는지를 적습니다. 여기서 택배의 지나간 센터가 걸음이고, 송장 조회가 실행 기록 보기입니다.
기록에 남길 것과 남기지 않을 것
| 남긴다 | 남기지 않는다 |
|---|---|
| 요청 번호, 사용자 번호, 시각 | API 키, 비밀번호, 토큰 |
| 걸음마다 도구 이름, 인자, 결과의 요약, 밀리초 | 주민등록번호, 카드 번호 같은 개인정보 원문 |
| 최종 답, 만든 작업 번호, 오류 | 다른 사용자의 데이터 |
도구 호출을 기록하는 감싸개
import time
trace = []
def traced(step, name, func, **args):
"""도구를 부르고, 걸음·도구·인자·결과·밀리초를 기록한다. 오류도 기록하고 결과로 돌려준다"""
start = time.perf_counter()
try:
result = func(**args)
except Exception as e:
result = {"오류": type(e).__name__}
trace.append({"걸음": step, "도구": name, "인자": args, "결과": result,
"ms": round((time.perf_counter() - start) * 1000)})
return result
traced(1, "get_my_leave", lambda: {"남은연차": 12.0})
traced(2, "request_leave", lambda start_date, days: {"신청됨": start_date}, start_date="2026-11-10", days=1)
traced(3, "request_leave", lambda start_date, days: 1 / 0, start_date="2026-11-10", days=1)
for row in trace:
print(row)세 줄이 나오고, 셋째 줄의 결과는 {'오류': 'ZeroDivisionError'} 입니다. 도구가 예외를 내도 에이전트가 멈추지 않고, 무엇이 실패했는지가 기록에 남습니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
time.perf_counter() | performance counter(성능 측정용 시계)다. 짧은 시간을 잴 때 쓰고, 두 번 잰 값의 차이가 걸린 초다 |
def traced(step, name, func, **args) | 이름 붙인 나머지 인자(start_date=…, days=…)를 사전 args 로 모아 받는다 |
func(**args) | 모아 둔 사전을 다시 이름=값 꼴로 펼쳐 도구 함수에 넘긴다 |
type(e).__name__ | 오류의 종류 이름(예: ZeroDivisionError)만 글자로 꺼낸다 |
round((… - start) * 1000) | 초를 밀리초(ms = millisecond, 1000분의 1초)로 바꾸고 반올림한다 |
lambda: {"남은연차": 12.0} | 이름 없는 작은 함수다. 진짜 도구 대신 정해진 결과만 돌려주는 가짜 도구다 |
lambda start_date, days: 1 / 0 | 0 으로 나누어 일부러 오류(ZeroDivisionError, 0 으로 나누기 오류)를 내는 가짜 도구다 |
이 코드가 뜻하는 것
자동차 정비소의 작업 일지를 떠올려 봅니다. 정비사는 작업마다 순서, 작업 이름, 쓴 부품, 결과, 걸린 시간을 적습니다. 부품이 맞지 않아 실패한 작업도 "실패"라고 적은 뒤 다음 작업으로 넘어갑니다. 여기서 작업 순서가 걸음, 작업 이름이 도구, 쓴 부품이 인자, 걸린 시간이 ms 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
trace = [] | 빈 실행 기록을 만든다 | 새 작업 일지를 편다 |
traced(1, "get_my_leave", …) | 인자 없는 가짜 도구를 부르고 기록한다 | 첫 작업을 하고 적는다 |
traced(2, "request_leave", …, start_date=…, days=1) | 인자 둘을 넘겨 부르고, 인자도 함께 적는다 | 둘째 작업은 쓴 부품까지 적는다 |
traced(3, …) | 도구가 오류를 내도 결과 칸에 오류 이름을 적고 계속한다 | 실패해도 "실패"라 적고 넘어간다 |
for row in trace: print(row) | 쌓인 기록을 한 줄씩 본다 | 일지를 처음부터 읽는다 |
가짜 도구는 시간이 거의 들지 않아서 ms 칸이 0 으로 나오기 쉽습니다. 진짜 도구(검색, LLM 호출)를 감싸면 이 칸에서 어느 걸음이 느린지 보입니다. 비유와 다른 점은, 정비 일지는 사람이 손으로 적어 빠뜨릴 수 있지만 traced 는 도구를 부르는 길목에 있어 빠뜨리는 일이 없다는 것입니다.
작게 줄여서 눈으로 보기
**args 가 인자를 어떻게 모으는지와 오류 이름 꺼내기만 떼어 봅니다.
def show(**args):
return args
print(show()) # 이름 붙인 인자가 없으면 빈 사전
print(show(start_date="2026-11-10", days=1)) # 이름=값 이 사전으로 모인다
try:
1 / 0
except Exception as e:
print(type(e).__name__) # 예외의 종류 이름만 꺼낸다{}
{'start_date': '2026-11-10', 'days': 1}
ZeroDivisionError| 결과 | 읽는 법 |
|---|---|
{} | 첫 걸음의 get_my_leave 처럼 인자가 없으면 기록의 인자 칸이 빈 사전이 된다 |
{'start_date': '2026-11-10', 'days': 1} | 넘긴 인자가 이름과 값 그대로 사전에 담긴다. 이것이 기록의 인자 칸이다 |
ZeroDivisionError | 오류의 긴 설명 대신 종류 이름만 남긴다. 기록이 짧고 가지런해진다 |
주의할 점
- 기록은 요청 번호로 찾을 수 있게 합니다. 사용자가 "아까 그 신청이 왜 안 됐죠?"라고 물으면 그 번호로 기록을 엽니다. 프로젝트 2의 API 는 답에 추적 번호를 붙입니다.
- 기록도 데이터입니다. 보관 기간과 볼 수 있는 사람을 정합니다(21장).