
긴 3일의 연휴가 끝나고 화요일이 찾아왔습니다.
저는 어제 롯데월드를 갔다왔구요..
일부로 오후에 가서 생각보다 많이 돌아다니지는 않았어요.
마지막 퍼레이드까지 야무지게 즐기고 왔답니다. 그럼 시작..!
아침과제 30분,,, 날아갔어요,,,, 왜지,,,?
다시 ㅜㅜ레츠고
1. 왜 이걸 하는지 찾아봅니다
1.1 LLM 을 API 로 부를 때 왜 키를 요구할까요?
> LLM API 키는 사용자를 식별하고, 요청을 승인하며, 사용한 만큼의 비용을 부과하기 위한 고유한 암호화 문자열입니다.
LLM API 키가 필요한 이유는 1) 프로젝트 및 앱 식별 2) 사용량 추적 및 과금 3) 속도 제한 및 남용 방지 4) 보안 및 접근 제어
API 키 유출 사고 주요 원인은 하드코딩(소스 코드 내에 API 키를 직접 적어둔 후 GitHub에 업로드), 클라이언트 번들 노출, 로그 파일 노출 등이 있다.
1.2 보안 담당자가 다루는 서버는 어떤 운영체제를 가장 많이 쓸까요? 윈도우 PC 에서 PowerShell 대신 깃배시를 쓰면 무엇이 좋을까요?
> 보안 담장자가 가장 많이 다루는 서버 운영체제는 리눅스(Linux)이다.
윈도우 PC에서 PowerShell 대신 깃배시(Git Bash)를 사용할 때의 장점은
1) 서버 환경과의 일치 : 다루는 대부분의 서버가 리눅스라 Bash 명령어를 그대로 연습하고 활용할 수 있다.
2) Mac/Linux와의 호환성 : 작성한 쉘 스크립스(.sh)를 수정없이 그대로 실행할 수 있다.
3) 오픈소스 도구 친화성 : 많은 오픈소스 들이 Linux/Bash 환경을 기준으로 설명되어 있다.
Bash(Bourne Again Shell)는 Linux 및 macOS의 기본 쉘로 사용되는 가장 표준적이고 널리 쓰이는 명령어 인터페이스(CLI)이자 스크립트 언어이다.
Git Bash는 텍스트(Text) 기반 흐름 제어이고, PowerShell은 객체(Object) 기반 데이터 처리이다.
2. 이 말들이 무슨 뜻인지 찾아봅니다
2.1 LLM
> LLM(대형 언어 모델)은 기본적으로 인간의 언어(텍스트)를 입력받아 다음으로 이어질 가장 자연스러운 언어(텍스트)를 내놓는 프로그램이다.
2.2 무료 등급 (free tier)
> 클라우드 서비스 나 SaaS(Software as a Service) 프로그램의 무료 등급(Free Tier)은 대개 기능, 사용량, 사용 기간, 이용 대상에 제한을 둔다.
1) 사용량 및 용량 제한 2) 기능 제한 3) 시간 및 성능 제한 4) 사용자 및 협업 제한
오늘의 순서
- 아침 과제 5 — Gemini API 키 발급 · .env · .gitignore
- LLM을 API로 호출한다 — 주소 · 헤더 · 본문 · 응답에서 답 꺼내기
- 프롬프트 — 역할 · 지시 · 출력 형식 · 예시
- JSON으로 받기 — llm_client.py
- AI 에이전트와 도구 호출 — 도구를 만들고 LLM이 고르게 한다
- 라우터 — 이름으로 함수를 찾아 실행한다 · tool_router.py
- 승인 게이트 — 위험한 도구 앞에 사람을 둔다 · agent_result.json
- 오늘 만든 것 확인 · 남은 도전
LLM을 API로 호출한다
아래 셀을 먼저 실행합니다. requests를 설치합니다.
.env는 security-agent-toolkit 폴더에 있습니다.
「시작하기」의 위치 확인 셀이 지금 폴더에서 위쪽 폴더로 올라가며 .env를 찾아, 그 경로를 변수 ENV_PATH에 담아 두었습니다.
코드에서는 open(ENV_PATH, …)로 읽습니다.
⚠ NameError: name 'ENV_PATH' is not defined가 나오면 위치 확인 셀을 실행하지 않은 것입니다.
「시작하기」로 올라가 그 셀을 실행합니다.
%pip install -q requests
토큰
텍스트를 인식하고 처리하는 가장 기본적인 문자 단위입니다.
인간이 글을 단어, 형태소, 또는 음절 단위로 읽는 것처럼, AI는 텍스트를 토큰이라는 조각으로 잘라서 숫자로 변환한 뒤 학습하고 이해한다.
환각
단순히 '모른다'고 말하는 것이 아니라, 틀린 정보를 마치 완벽한 사실인 것처럼 매우 그럴듯하고 확신에 찬 어조로 출력하는 형태를 띤다.
2 · LLM을 API로 호출한다
왜 필요한가
지금까지 만든 룰은 우리가 정한 조건에 맞는 로그만 찾았습니다.
조건에 없는 로그는 그대로 지나갑니다.
LLM은 로그 한 줄을 읽고 무슨 일인지 문장으로 설명합니다.
사람이 한 줄씩 읽던 일을 대신합니다.
LLM을 쓰는 방법은 새로 배우지 않습니다.
10/2에 배운 requests.post로 요청을 보내고 JSON 응답을 받습니다.
2교시에 사용하는 용어
용어 (읽는 법)뜻2교시의 예
| LLM (엘엘엠) | 다음에 올 토큰을 확률로 예측하는 언어 모델 | "model": "gemini-3.5-flash-lite" |
| 토큰 (token) | 모델이 글을 자르는 최소 단위. 사용량과 한도를 토큰으로 센다 | "total_tokens": 50 |
| 환각 (hallucination · 할루시네이션) | 사실이 아닌 것을 그럴듯하게 지어내는 것 | LLM SK텔레콤 ↔ 조회 API SamsungSDS Inc. |
2.1 LLM의 응답은 딕셔너리로 옵니다
「한 문장으로 답하세요. 피싱이란 무엇입니까?」라고 물었을 때 실제로 받은 응답입니다.
오늘 쓰지 않는 키는 줄였습니다.
data = {
"choices": [
{
"message": {
"role": "assistant",
"content": "피싱이란 신뢰할 수 있는 기관이나 사람으로 위장하여 타인의 개인정보나 금융정보를 속여 빼내는 전자 금융 사기 수법입니다."
}
}
],
"model": "gemini-3.5-flash-lite",
"usage": {
"prompt_tokens": 15,
"completion_tokens": 35,
"total_tokens": 50
}
}
print(data["choices"][0]["message"]["content"])
답 문장은 choices → 0 → message → content 순서로 들어가야 나옵니다.
choices는 리스트라서 숫자 0으로 꺼냅니다.
message와 content는 딕셔너리의 키라서 이름으로 꺼냅니다.
usage에는 토큰 수가 있습니다.
질문이 15토큰, 답이 35토큰, 합계가 50토큰입니다.
2.2 호출에는 주소 · 헤더 · 본문이 필요합니다
9/30과 10/2에 쓴 requests 그대로입니다.
넣는 값만 다릅니다.
import requests
api_key = "발급받은_키" # 실제로는 .env 에서 읽습니다
url = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"
headers = {
"Authorization": "Bearer " + api_key,
"Content-Type": "application/json"
}
body = {
"model": "gemini-3.5-flash-lite",
"messages": [
{
"role": "user",
"content": "한 문장으로 답하세요. 피싱이란 무엇입니까?"
}
]
}
response = requests.post(
url,
headers=headers,
json=body,
timeout=30
)
print(response.status_code) # 200
주소 — LLM 서비스가 요청을 받는 곳입니다.
헤더 — Authorization에 Bearer와 공백 한 칸, 그 뒤에 키를 붙입니다. 9/29 오후에 배운 인증 방식입니다.
본문 — model에는 모델 이름을, messages에는 질문을 넣습니다. 질문은 role이 "user"인 딕셔너리 하나입니다.
무료 등급은 1분에 15번까지 호출할 수 있습니다.
넘으면 상태코드 429가 오고, 1분쯤 기다리면 다시 됩니다.
🐍 문법 상자 · requests.post에 넣는 네 가지
response = requests.post(
url,
headers=headers,
json=body,
timeout=30
)
print(response.status_code) # 200 — 성공
쓰는 것뜻
| url | 보낼 주소 — 맨 앞에 이름 없이 쓴다 |
| headers=headers | 요청 헤더 — 키를 여기에 넣는다 |
| json=body | 본문 딕셔너리를 JSON으로 바꿔 보낸다 (9/30) |
| timeout=30 | 30초 안에 답이 없으면 기다리지 않는다 |
⚠ requests.post(url, body)처럼 이름 없이 쓰면 body가 json=이 아닌 다른 자리로 들어가 요청이 실패합니다.
json=을 꼭 붙입니다.
🐍 문법 상자 · 안으로 한 단계씩 꺼내기
data = {
"choices": [
{
"message": {
"content": "포트 스캔입니다."
}
}
]
}
print(data["choices"][0]["message"]["content"])
# 포트 스캔입니다.
쓰는 것꺼낸 것모양
| data["choices"] | 답 목록 | 리스트 |
| data["choices"][0] | 첫 번째 답 | 딕셔너리 |
| …[0]["message"] | 메시지 | 딕셔너리 |
| …["message"]["content"] | 답 문장 | 문자열 |
⚠ [0]을 빠뜨리면 리스트에 글자 키를 쓴 것이라 TypeError: list indices must be integers가 납니다.
2.3 LLM은 조회하지 않고 문장을 만듭니다
LLM은 어딘가를 찾아보고 답하지 않습니다.
그럴듯한 단어를 이어 붙여 문장을 만듭니다.
그래서 모르는 것도 아는 것처럼 답합니다.
이것을 환각이라고 합니다.
같은 질문 「IP 211.45.12.9는 어느 통신사의 주소입니까?」에 실제로 받은 답입니다.
누구에게 물었나받은 답
| LLM · 첫 번째 | LG Uplus |
| LLM · 두 번째 | SK텔레콤 |
| 조회 API ipwho.is (9/30에 쓴 서비스) | SamsungSDS Inc. |
LLM의 답은 물을 때마다 달라졌고, 둘 다 틀렸습니다.
문장은 자연스러워서 틀린 줄 알기 어렵습니다.
조회 API는 실제 등록 정보를 찾아서 반환합니다.
그래서 사실 확인은 조회 API와 로그로 하고, LLM에게는 요약과 설명을 맡깁니다.
최종 판단은 사람이 합니다.
2.4 되풀이되는 코드를 함수로 묶습니다 — post_to_llm
문제 2-2와 2-3의 셀에는 미리 채워 둔 줄이 열 줄쯤 있었습니다.
키를 읽고, 주소와 헤더를 만들고, 요청을 보내는 줄입니다.
LLM을 호출할 때마다 똑같이 되풀이됩니다.
되풀이되는 코드는 함수 하나로 묶습니다.
9/23 오후에 배운 방법입니다.
아래 셀의 함수 post_to_llm이 그 일을 합니다.
함수 안의 줄하는 일앞에서 직접 쓴 곳
| read_api_key() | .env 파일(ENV_PATH)에서 API 키를 읽는다 | 문제 2-2의 미리 채워 둔 여섯 줄 |
| headers = {"Authorization": "Bearer " + api_key, …} | 요청 헤더에 키를 넣는다 | 문제 2-2의 미리 채워 둔 줄 |
| requests.post(URL, headers=headers, json=body, timeout=30) | 요청을 보낸다 | 문제 2-2 |
| return | 받은 response를 돌려준다 | — |
쓰는 법은 한 줄입니다.
본문 body만 만들어서 넣으면 됩니다.
body = {
"model": "gemini-3.5-flash-lite",
"messages": [
{
"role": "user",
"content": "피싱이란 무엇입니까?"
}
]
}
response = post_to_llm(body)
print(response.status_code) # 200
함수가 대신 해 주는 것은 키 읽기 · 헤더 만들기 · 요청 보내기입니다.
본문 body는 우리가 만듭니다.
무엇을 물을지는 본문에 들어 있기 때문입니다.
돌려받은 response는 문제 2-2의 response와 같은 것입니다.
.status_code와 .json()을 그대로 씁니다.
이 아래 문제부터는 셀에 이 함수만 나옵니다.
셀이 짧아지고, 그 문제에서 새로 배우는 줄만 남습니다.
⚠ 아래 셀을 실행하지 않으면 뒤의 문제에서 NameError: name 'post_to_llm' is not defined가 나옵니다.
노트북을 다시 열었을 때도 아래 셀을 다시 실행합니다.
아래 셀을 실행합니다.
함수를 만들어 두기만 합니다.
LLM을 호출하지 않습니다.
import requests # 인터넷으로 요청을 보내는 도구 (9/30)
# Gemini 에 요청을 보낼 주소
URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"
def read_api_key(): # .env 에서 키를 읽어 돌려주는 함수
""".env 파일(ENV_PATH)에서 GEMINI_API_KEY 의 값을 읽어 반환합니다."""
api_key = None # 키를 못 찾으면 None 으로 남는다
with open(ENV_PATH, encoding="utf-8") as f: # .env 파일을 읽기로 연다
for line in f: # 파일을 한 줄씩 꺼낸다
parts = line.strip().split("=", 1) # 줄 끝 공백을 지우고 = 에서 둘로 나눈다
if parts[0] == "GEMINI_API_KEY": # = 앞부분이 키 이름이면
api_key = parts[1] # = 뒷부분이 키 값이다
return api_key # 읽은 키를 돌려준다
def post_to_llm(body, api_key=None): # 본문을 받아 Gemini 로 보내는 함수. 키를 주지 않으면 .env 의 키를 쓴다
"""body 를 Gemini API 로 보내고 response 를 반환합니다."""
if api_key is None: # 키를 따로 주지 않았으면
api_key = read_api_key() # .env 의 키를 쓴다
headers = {
"Authorization": "Bearer " + api_key,
"Content-Type": "application/json"
}
response = requests.post(
URL,
headers=headers,
json=body,
timeout=60
)
if response.status_code == 429: # 한도(1분에 15번)를 넘었으면
print("[한도 초과] 429 — 1분 기다린 뒤 이 셀을 다시 실행합니다")
return response # 응답을 그대로 돌려준다
print("post_to_llm 함수를 만들었습니다.")
한눈에
하려는 일쓰는 코드
| LLM에 요청을 보낸다 | requests.post(url, headers=headers, json=body, timeout=30) |
| 키를 싣는다 | {"Authorization": "Bearer " + api_key} |
| 위 두 줄을 함수로 줄여 쓴다 | response = post_to_llm(body) |
| 질문을 담는다 | "messages": [{"role": "user", "content": question}] |
| 답 문장을 꺼낸다 | data["choices"][0]["message"]["content"] |
| 쓴 토큰을 본다 | data["usage"]["total_tokens"] |
| 성공했는지 확인한다 | response.status_code == 200 |
LLM의 답은 문장이 자연스러워도 틀릴 수 있습니다.
사실 확인은 조회 API와 로그로 합니다.
프롬프트로 답의 모양을 정한다
프롬프트
LLM(대형 언어 모델)에게 좋은 답변을 받으려면 프롬프트를 구체적이고 구조적으로 작성해야 합니다.
프롬프트 엔지니어링의 핵심은 AI에게 명확한 '역할'과 '맥락'을 부여하는 것입니다.
temperature
생성형 AI 모델(대형 언어 모델 등)에서 temperature(온도)는 출력 텍스트의 무작위성과 창의성의 수준을 결정하는 하이퍼파라미터이다.
3 · 프롬프트로 답의 모양을 정한다
왜 필요한가
로그 한 줄만 보내면 LLM은 영어로, 항목을 여러 개 나열해 길게 답했습니다.
실제로 229토큰이 나왔습니다.
답의 길이와 모양이 매번 다르면, 뒤에 오는 파이썬 코드가 값을 꺼낼 수 없습니다.
LLM에게 보내는 글 전체를 프롬프트라고 합니다.
프롬프트에 역할 · 지시 · 출력 형식 · 예시를 적으면 답의 모양이 정해집니다.
3교시에 사용하는 용어
용어 (읽는 법)뜻3교시의 예
| 프롬프트 (prompt) | 모델에게 건네는 지시문 전체 | messages에 담는 글 |
| system · user (시스템 · 유저) | 역할 지시와 이번 질문을 나눠 담는 자리 | {"role": "system", ...} · {"role": "user", ...} |
| few-shot (퓨샷) | 본보기를 예시로 주는 방식 | 예시: {"severity": "high", "summary": "root 로그인 실패"} |
| temperature (템퍼러처) | 답을 얼마나 다양하게 낼지 정하는 값 | "temperature": 0 |
3.1 역할은 system에, 질문은 user에 적습니다
messages 리스트에 딕셔너리를 둘 넣습니다.
앞의 것이 역할, 뒤의 것이 이번 질문입니다.
messages = [
{
"role": "system",
"content": "당신은 보안관제 애널리스트입니다. 한 문장으로만 답합니다."
},
{
"role": "user",
"content": "2026-10-06 03:22:40 WARN failed login for ops.kim from 203.0.113.61"
}
]
body = {
"model": "gemini-3.5-flash-lite",
"messages": messages
}
response = post_to_llm(body)
같은 로그를 보냈을 때 실제로 받은 답입니다.
보낸 것받은 답토큰
| 로그만 | 영어로 항목 여러 개를 나열한 긴 설명 | 271 |
| 역할 + 로그 | 「203.0.113.61 IP에서 ops.kim 계정으로의 로그인 실패(WARN)를 나타내며 … 모니터링이 필요합니다.」 | 132 |
역할을 정해 주면 쓰는 단어와 판단 기준이 보안 직무 쪽으로 좁혀집니다.
「한 문장으로만」 같은 규칙도 system에 적습니다.
답이 짧아지면 토큰도 줄어듭니다.
3.2 지시와 출력 형식을 분명하게 적습니다
무엇을 할지(지시)와 어떤 모양으로 답할지(출력 형식)를 질문에 적습니다.
같은 로그 ops.kim에 실제로 받은 답입니다.
질문에 적은 것받은 답
| 「심각도를 high, medium, low 중 하나로만 답하세요.」 | low |
| 「JSON으로만 답하세요. 키는 severity와 summary 둘입니다.」 | 아래 |
{
"severity": "medium",
"summary": "IP 주소 203.0.113.61에서 계정 ops.kim으로 로그인 실패 발생"
}
- 고를 수 있는 값을 나열해 주면 그 안에서만 답합니다.
- JSON으로 받으면 뒤에 오는 코드가 severity 같은 키 이름으로 값을 꺼낼 수 있습니다.
- 받은 글의 앞뒤에 json``과 가 붙어 있습니다. 4교시에 이 표시를 지우고 읽습니다.
- 같은 로그인데 한 단어로 물으면 low, JSON으로 물으면 medium이 나왔습니다. LLM의 판단은 묻는 방식에 따라 달라집니다.
3.3 예시를 보여 주면 그 모양을 따라 합니다
본보기를 한두 개 보여 주는 방식을 few-shot이라고 합니다.
질문에 예시 한 줄을 넣었을 때 실제로 달라진 것입니다.
질문받은 summary
| 예시 없음 | IP 주소 203.0.113.61에서 사용자 계정 ops.kim으로의 로그인 시도가 실패했습니다. |
| 예시: {"severity": "high", "summary": "root 로그인 실패"}를 넣음 | ops.kim 계정 로그인 실패 |
예시가 짧으면 답도 짧아집니다.
길이를 글로 설명하는 것보다 예시 하나가 더 잘 통합니다.
예시의 키 이름과 순서, 쓰는 언어도 그대로 따라 합니다.
예시가 없으면 요약이 영어로 올 때가 있습니다.
예시를 넣어도 `````json`` 표시는 붙을 때도 있고 안 붙을 때도 있었습니다.
세 번 가운데 두 번 붙었습니다.
🐍 문법 상자 · 문자열을 이어 붙일 때는 공백도 직접 넣습니다
a = "JSON 으로만 답하세요."
b = "예시: high"
print(a + b)
print(a + " " + b)
# JSON 으로만 답하세요.예시: high
# JSON 으로만 답하세요. 예시: high
쓰는 것결과
| a + b | 두 문장이 붙어 버린다 |
| a + " " + b | 사이에 공백 한 칸 |
⚠ +는 공백을 넣어 주지 않습니다.
붙어 버린 질문은 LLM도 읽기 어렵습니다.
3.4 temperature를 0으로 두어도 답은 달라질 수 있습니다
본문에 "temperature"를 넣으면 답을 얼마나 다양하게 낼지를 정할 수 있습니다.
보안 자동화에서는 낮게 0으로 둡니다.
body = {
"model": "gemini-3.5-flash-lite",
"temperature": 0,
"messages": messages
}
그런데 0으로 두어도 답이 똑같지는 않았습니다.
「로그인 실패가 다섯 번 이어진 사용자에게 보낼 경고 문구를 한 줄로 쓰세요.」를 temperature 0으로 세 번 물었을 때 실제로 받은 답입니다.
몇 번째받은 답
| 1 | 비밀번호 오류 5회 초과로 계정이 임시 잠겼습니다. 본인 확인 후 잠금을 해제해 주세요. |
| 2 | 로그인 5회 실패로 보안을 위해 계정이 잠겼으니 비밀번호를 재설정해 주세요. |
| 3 | 비밀번호를 5회 연속 잘못 입력하여 계정이 일시 잠금되었습니다. |
같은 temperature 0으로 「심각도를 high, medium, low 중 하나로만 답하세요.」라고 세 번씩 물었을 때입니다.
로그받은 답 세 개
| ops.kim 로그인 실패 | medium · medium · medium |
| root 로그인 실패 | high · medium · high |
temperature를 0으로 두어도 답은 매번 달라질 수 있습니다.
문장도, 심각도 판단도 그렇습니다.
그래도 출력 형식을 좁히면 답이 세 단어 가운데 하나로 모입니다.
문장은 비교할 수 없지만 단어는 코드로 세고 비교할 수 있습니다.
같은 로그에 high와 medium이 섞여 나왔습니다.
그래서 LLM의 판단을 그대로 믿지 않고 사람이 최종 확인합니다.
temperature를 0으로 해도 환각은 사라지지 않습니다.
2교시의 통신사 질문은 0으로 물어도 KT, LG Uplus로 틀린 답이 나왔습니다.
🐍 문법 상자 · temperature는 본문 딕셔너리의 키입니다
body = {
"model": "gemini-3.5-flash-lite",
"temperature": 0,
"messages": []
}
print(list(body.keys()))
# ['model', 'temperature', 'messages']
키넣는 것
| model | 모델 이름 |
| temperature | 0 — 답을 매번 비슷하게 |
| messages | 역할 · 질문 리스트 |
⚠ temperature를 messages 안의 딕셔너리에 넣지 않습니다.
model · messages와 같은 자리입니다.
한눈에
프롬프트의 조각어디에 적나무엇이 달라지나
| 역할 | messages의 system | 쓰는 단어와 판단 기준 |
| 지시 | user의 content | 무엇을 할지 |
| 출력 형식 | user의 content | 답의 모양 (한 단어 · JSON) |
| 예시 | user의 content | 답의 길이와 키 이름 |
| temperature | 본문 body | 답의 다양함. 자동화에서는 0으로 둔다 |
temperature를 0으로 두어도 답은 달라질 수 있고 환각도 남습니다.
출력 형식을 좁히면 답이 달라져도 코드가 다룰 수 있는 값이 됩니다.
JSON 답을 안전하게 읽는다 — llm_client.py
파싱
컴퓨터가 이해할 수 없는 단순한 통글자(문자열)를 규칙에 따라 쪼개고 분석해서, 프로그램이 즉시 계산하거나 사용할 수 있는 의미 있는 데이터 형식으로 바꾸는 과정.
구조화된 출력
프로그램이 바로 읽고 처리할 수 있어서 데이터 연동과 자동화가 훨씬 안정적이다.
4 · JSON 답을 안전하게 읽는다
왜 필요한가
3교시에 JSON으로 답하라고 했더니, 답의 앞뒤에 json``과 가 붙어서 왔습니다.
세 번 물으면 세 번 다 붙었습니다.
그 문자열을 json.loads에 그대로 넣으면 JSONDecodeError로 프로그램이 멈춥니다.
로그 백 건을 처리하다가 한 건에서 멈추면 남은 아흔아홉 건을 잃습니다.
읽지 못한 한 건은 건너뛰고 계속 가야 합니다.
4교시에 사용하는 용어
용어 (읽는 법)뜻4교시의 예
| 파싱 (parsing) | 문자열을 프로그램이 쓰는 값으로 바꾸는 일 | json.loads(clean) |
| 코드 블록 표시 | 답의 앞뒤에 붙는 json``과 | text.replace("```json", "") |
| call_llm | 질문을 받아 LLM의 응답 문자열을 반환하는 함수 | call_llm(question) |
| parse_llm_json | 응답 문자열을 딕셔너리로 바꾸는 함수. 못 읽으면 None | parse_llm_json(text) |
4.1 표시를 지우고 나서 읽습니다
3교시에 실제로 받은 답은 이런 문자열이었습니다.
{
"severity": "medium",
"summary": "IP 주소 203.0.113.61에서 계정 ops.kim으로 로그인 실패 발생"
}
이 문자열을 그대로 json.loads에 넣으면 실제로 이런 오류가 납니다.
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
첫 글자가 {가 아니라 `이기 때문입니다.
그래서 표시를 먼저 지웁니다.
import json
text = '```json{"severity": "medium", "summary": "ops.kim 로그인 실패"}```'
clean = text.replace("```json", "")
clean = clean.replace("```", "")
clean = clean.strip()
result = json.loads(clean)
print(result["summary"]) # ops.kim 로그인 실패
replace는 9/23에, strip은 9/28에, json.loads는 9/29 오후에 배웠습니다.
새 문법은 없습니다.
json``을 먼저 지우고 을 나중에 지웁니다.
순서를 바꾸면 json 문자열이 남습니다.
strip은 앞뒤에 남은 공백과 줄바꿈을 지웁니다.
4.2 읽지 못해도 멈추지 않게 합니다
LLM이 JSON이 아닌 문장으로 답할 때가 있습니다.
그때 json.loads는 JSONDecodeError를 냅니다.
9/28에 배운 try · except로 감쌉니다.
import json
text = "이 로그는 분석할 수 없습니다."
try:
result = json.loads(text)
except json.JSONDecodeError:
result = None
print(result) # None
읽는 데 성공하면 result에 딕셔너리가 담깁니다.
실패하면 result에 None이 담기고, 프로그램은 다음 줄로 넘어갑니다.
부르는 쪽은 if result:로 확인합니다.
9/30의 call_with_retry와 같은 약속입니다.
🐍 문법 상자 · try · except — 읽지 못해도 멈추지 않게
import json
for s in ['{"a": 1}', "그럴듯한 문장"]:
try:
print(json.loads(s))
except json.JSONDecodeError:
print("읽지 못함")
# {'a': 1}
# 읽지 못함
들어온 글실행되는 곳
| JSON 모양 | try 안 — 딕셔너리가 된다 |
| 그냥 문장 | 에러가 나서 except 안으로 (9/28) |
⚠ except가 없으면 LLM이 문장으로 답한 순간 프로그램이 멈춥니다.
4.3 호출도 함수로 묶습니다 — call_llm
post_to_llm은 요청을 보내는 데까지만 합니다.
본문을 만들고, 상태코드를 확인하고, 답 문장을 꺼내는 줄은 3교시 내내 되풀이했습니다.
이 줄들도 함수 하나에 넣습니다.
질문을 주면 응답 문자열을 반환합니다.
def call_llm(question):
body = {
"model": "gemini-3.5-flash-lite",
"temperature": 0,
"messages": [
{
"role": "user",
"content": question
}
]
}
response = post_to_llm(body)
if response.status_code != 200:
print(
"[LLM 호출 실패] 상태 코드",
response.status_code
)
return ""
data = response.json()
return data["choices"][0]["message"]["content"]
실패하면 빈 문자열 ""를 반환합니다.
빈 문자열은 parse_llm_json이 읽지 못해서 None이 됩니다.
두 함수가 그렇게 이어집니다.
temperature는 0으로 고정합니다.
함수로 묶으면 부르는 쪽은 한 줄이면 됩니다.
call_llm("…")
🐍 문법 상자 · %%writefile은 셀 맨 첫 줄에
%%writefile llm_client.py
import json
...
할 일결과
| 셀 첫 줄에 %%writefile 파일이름 | 셀 내용이 그 파일로 저장된다 |
| 파일을 고친 뒤 | 이 셀을 다시 실행해야 파일이 바뀐다 |
⚠ %%writefile 위에 빈 줄이나 주석이 있으면 저장되지 않고 에러가 납니다.
4.4 만든 파일을 불러 씁니다
llm_client.py는 같은 폴더에 있으므로 import로 불러 씁니다.
9/23 오후에 배운 모듈 불러오기입니다.
from llm_client import call_llm, parse_llm_json
text = call_llm(
"JSON 으로만 답하세요. 키는 severity 하나입니다. 로그: …"
)
result = parse_llm_json(text)
if result:
print(result["severity"])
else:
print("읽지 못했습니다")
call_llm이 응답 문자열을 주고, parse_llm_json이 그 문자열을 딕셔너리로 바꿉니다.
result가 None일 수 있으므로 꺼내기 전에 if result:로 확인합니다.
llm_client.py를 고친 뒤에는 문제 4-4 셀을 다시 실행해 저장하고, 이 문제 셀을 다시 실행합니다.
🐍 문법 상자 · if result: — 값이 있을 때만
for result in [{"severity": "high"}, None]:
if result:
print("있음", result["severity"])
else:
print("없음")
# 있음 high
# 없음
resultif result:
| 딕셔너리 | 참 — 키로 꺼낸다 |
| None | 거짓 — else로 간다 |
⚠ 확인 없이 None["severity"]를 쓰면 TypeError: 'NoneType' object is not subscriptable입니다.
한눈에
하려는 일쓰는 코드
| 코드 블록 표시를 지운다 | text.replace("```json", "").replace("```", "").strip()을 세 줄로 나눠 쓴다 |
| 문자열을 딕셔너리로 바꾼다 | json.loads(clean) |
| 읽지 못해도 멈추지 않는다 | try: … except json.JSONDecodeError: … return None |
| 질문을 보내 응답 문자열을 받는다 | call_llm(question) |
| 응답 문자열을 딕셔너리로 바꾼다 | parse_llm_json(text) |
| 꺼내기 전에 확인한다 | if result: |
오전 산출물은 agent_core/llm_client.py입니다.
오후에는 이 파일의 두 함수로 AI 에이전트를 만듭니다.
AI 에이전트와 도구 호출
AI 에이전트
AI 에이전트(Agent)와 일반 챗봇의 가장 큰 차이점은 '독자적인 행동 능력'.
단순히 질문에 답을 하는 챗봇과 달리, AI 에이전트는 사용자가 목표를 주면 스스로 계획을 세우고 외부 도구를 사용해 업무를 직접 수행함.
도구 호출
LLM(대형 언어 모델)이 사용자의 컴퓨터에 있는 함수를 직접 실행하는 것은 아니다.
LLM이 수행하는 역할은 직접 실행이 아니라 어떤 함수를 어떤 인자(값)로 실행해야 할지 결정하고 요청서(JSON 형태)를 만들어주는 것.
5 · 도구를 만들고 LLM이 고르게 한다
왜 필요한가
오전의 LLM은 로그를 설명하기만 했습니다.
로그 파일을 세거나 IP를 조회하지는 못합니다.
2교시에 봤듯이 LLM은 조회하지 않고 지어냅니다.
사실은 우리 코드가 확인해야 합니다.
그래서 일을 나눕니다.
우리가 도구(함수)를 만들어 두고, LLM은 어느 도구를 쓸지 고르기만 합니다.
실행은 우리 코드가 합니다.
5교시에 사용하는 용어
용어 (읽는 법)뜻5교시의 예
| 에이전트 (agent) | 목표를 받아 스스로 순서를 정하고 도구를 쓰는 프로그램 | 목표 → 도구 고르기 → 실행 → 답 |
| 도구 (tool) | LLM이 고를 수 있게 우리가 만들어 둔 함수 | count_failed_logins(args) |
| 도구 호출 (tool calling) | 모델이 어떤 함수를 어떤 값으로 부를지 정하는 구조 | {"tool": "count_failed_logins", "args": {"user": "admin"}} |
| 도구 목록 | 쓸 수 있는 함수의 이름·설명·인자를 적은 글 | tools_text |
5.1 도구는 딕셔너리 하나를 받는 함수입니다
도구는 평범한 파이썬 함수입니다.
다만 인자를 딕셔너리 하나(args)로 받게 만듭니다.
LLM이 인자를 JSON으로 주기 때문입니다.
import json
def count_failed_logins(args):
with open("normalized_logs.json", encoding="utf-8") as f:
rows = json.load(f)
count = 0
for row in rows:
if (
row["user"] == args["user"]
and row["level"] == "WARN"
):
count = count + 1
return count
print(
count_failed_logins(
{"user": "kim.cs"}
)
)
# 1
args["user"]로 어느 계정을 셀지 받습니다.
9/29 오전의 룰 ①과 같은 코드입니다.
계정이 같고 등급이 WARN인 기록을 셉니다.
결과는 return으로 반환합니다.
화면에 출력하지 않습니다.
부른 쪽이 그 값을 다시 씁니다.
5.2 도구 목록을 보내면 LLM이 하나를 고릅니다
프롬프트에 도구 목록과 목표를 함께 넣습니다.
LLM은 어느 도구를 어떤 값으로 부를지를 JSON으로 답합니다.
tools_text = "쓸 수 있는 도구는 둘입니다. "
tools_text = tools_text + "1) count_failed_logins: 한 계정의 로그인 실패 횟수를 센다. 인자는 user. "
tools_text = tools_text + "2) lookup_ip: IP 주소의 나라와 통신사를 조회한다. 인자는 ip. "
tools_text = tools_text + "목표를 이루는 데 쓸 도구 하나를 골라 JSON 으로만 답하세요. "
tools_text = tools_text + '예시: {"tool": "count_failed_logins", "args": {"user": "kim01"}} '
text = call_llm(
tools_text
+ "목표: admin 계정이 로그인에 몇 번 실패했는지 알려 주세요."
)
실제로 받은 답입니다.
{
"tool": "count_failed_logins",
"args": {
"user": "admin"
}
}
- LLM은 고르기만 했습니다. 로그 파일을 열지도, 함수를 실행하지도 않았습니다.
- 고른 결과는 문자열입니다. 오전에 만든 parse_llm_json으로 딕셔너리로 바꿉니다.
- 실행은 우리 파이썬 코드가 합니다. 그래서 실행할지 말지를 우리가 정할 수 있습니다.
🐍 문법 상자 · 딕셔너리 안의 딕셔너리
choice = {
"tool": "count_failed_logins",
"args": {
"user": "kim.cs"
}
}
print(choice["tool"])
print(choice["args"]["user"])
# count_failed_logins
# kim.cs
쓰는 것결과
| choice["tool"] | 고른 도구 이름 — 문자열 |
| choice["args"] | 인자 — 딕셔너리 |
| choice["args"]["user"] | 그 안의 계정 이름 |
⚠ LLM이 고른 것은 아직 이름일 뿐입니다.
함수는 우리 코드가 실행합니다.
5.3 되풀이되는 줄을 함수 choose_tool로 묶습니다
문제 5-3에서 쓴 「질문을 만들어 보내고 → 답을 딕셔너리로 바꾸는」 줄은 오후 내내 되풀이됩니다.
매번 다시 쓰지 않도록 함수 하나로 묶습니다.
def choose_tool(goal):
text = call_llm(
tools_text + "목표: " + goal
)
if text == "":
print(
"LLM 의 답을 받지 못했습니다. "
"1분 기다린 뒤 이 셀을 다시 실행합니다 "
"(한도를 넘으면 429 가 옵니다)"
)
return parse_llm_json(text)
줄하는 일문제 5-3에서 쓴 줄
| def choose_tool(goal): | 목표 하나를 받는 함수를 만든다 | — |
| text = call_llm(tools_text + "목표: " + goal) | 도구 목록과 목표를 이어 붙여 LLM에 보낸다 | 1번 · 2번 |
| if text == "": | 답을 받지 못했으면 안내를 출력한다 | — |
| return parse_llm_json(text) | 답을 딕셔너리로 바꿔 반환한다 | 3번 |
쓰는 법은
choice = choose_tool(goal)
한 줄입니다.
문제 5-3의 네 줄이 한 줄이 됩니다.
반환값은
{
"tool": "…",
"args": {
…
}
}
모양의 딕셔너리입니다.
LLM의 답을 읽지 못하면 None입니다.
5.4 목표가 바뀌면 다른 도구를 고릅니다
같은 도구 목록에 목표만 바꿔 보냈을 때 실제로 받은 답입니다.
목표LLM이 고른 것
| admin 계정이 로그인에 몇 번 실패했는지 알려 주세요. | count_failed_logins · {"user": "admin"} |
| 185.220.101.34는 어느 나라의 주소인지 알려 주세요. | lookup_ip · {"ip": "185.220.101.34"} |
| admin 계정을 잠가 주세요. | count_failed_logins · {"user": "admin"} |
| 오늘 점심 메뉴를 추천해 주세요. | 「관련된 도구가 없습니다」라는 문장, 또는 {"tool": null, "args": {}} |
목표에 맞는 도구가 목록에 있으면 그것을 고릅니다.
목록에 없는 일(계정 잠금)을 시켰더니, 있는 도구 가운데 하나를 억지로 골랐습니다.
목표는 이뤄지지 않습니다.
도구와 상관없는 목표에는 JSON이 아닌 문장이 올 때도 있습니다.
parse_llm_json이 None을 반환하는 경우입니다.
그래서 LLM이 고른 것을 그대로 실행하지 않고 확인합니다.
6교시와 7교시가 그 확인입니다.
🐍 문법 상자 · if · elif — 갈래 고르기
tool = "lookup_ip"
if tool == "count_failed_logins":
print("세기")
elif tool == "lookup_ip":
print("조회")
# 조회
쓰는 것뜻
| if 조건: | 첫 갈래 |
| elif 조건: | 위가 거짓일 때 다음 갈래 |
⚠ 두 조건이 다 거짓이면 아무 줄도 실행되지 않습니다.
그 뒤에 result를 쓰면 NameError입니다.
5.5 실행 결과를 다시 LLM에 넘겨 답을 만듭니다
도구가 반환한 값은 숫자나 짧은 문자열입니다.
그 값을 다시 LLM에 넘겨 사람이 읽을 문장으로 만듭니다.
prompt2 = "목표: admin 계정이 로그인에 몇 번 실패했는지 알려 주세요. "
prompt2 = prompt2 + "도구 count_failed_logins 를 실행한 결과는 4 입니다. "
prompt2 = prompt2 + "이 결과로 목표에 한 문장으로 답하세요."
print(call_llm(prompt2))
# admin 계정의 로그인 실패 횟수는 총 4번입니다.
목표 → 도구 고르기 → 실행 → 결과로 답하기.
이 한 바퀴가 에이전트의 기본 흐름입니다.
숫자 4는 LLM이 지어낸 것이 아니라 우리 도구가 로그 파일에서 센 값입니다.
숫자를 문자열에 이어 붙일 때는 9/22에 배운 str()로 문자열로 바꿉니다.
🐍 문법 상자 · 숫자를 문자열에 붙일 때는 str()
result = 4
print(
"결과는 "
+ str(result)
+ "번"
)
# 결과는 4번
쓰는 것결과
| "결과는 " + str(result) | "결과는 4" |
| "결과는 " + result | TypeError: can only concatenate str (not "int") to str |
⚠ print("결과는", result)처럼 쉼표로 넘기면 str()이 없어도 됩니다.
이어 붙이는 +에서만 필요합니다.
한눈에
하려는 일쓰는 코드
| 도구를 만든다 | def count_failed_logins(args): … return count |
| LLM에게 고르게 한다 | call_llm(tools_text + "목표: " + goal) |
| 고른 것을 읽는다 | choice = parse_llm_json(text) · choice["tool"] · choice["args"] |
| 위 두 줄을 한 번에 | choice = choose_tool(goal) |
| 우리 코드로 실행한다 | if choice["tool"] == "…": result = 도구(choice["args"]) |
| 결과로 답을 만든다 | call_llm("목표: … 실행한 결과는 … 한 문장으로 답하세요.") |
LLM은 고르기만 합니다.
실행은 우리 코드가 합니다.
라우터 — 이름으로 함수를 찾아 실행한다
라우터
사용자가 요청한 URL(또는 네트워크 경로)을 그에 맞는 특정 코드나 화면(핸들러/컴포넌트)으로 이어 주는 일.
레지스트리
레지스트리(Registry)처럼 이름과 실제 대상을 매핑(짝짓기)해 두는 구조는 컴퓨터 시스템과 소프트웨어 아키텍처에서 매우 핵심적인 역할.
- 찾기과 관리가 쉬워짐(추상화)
- 대상이 바뀌어도 쓰는 사람은 알 필요 없음(느슨한 결합)
- 중복을 방지하고 한 곳에서 통제할 수 있음(중앙 집중 관리)
6 · 이름으로 함수를 찾아 실행한다
왜 필요한가
5교시에는 도구마다 if · elif를 한 갈래씩 썼습니다.
도구가 열 개면 갈래도 열 개입니다.
LLM이 목록에 없는 이름을 답하면 어느 갈래에도 걸리지 않고 조용히 지나갑니다.
왜 아무 일도 안 일어났는지 아무도 모릅니다.
그래서 이름과 함수를 짝지은 딕셔너리를 만들고, 이름으로 함수를 찾아 실행하는 코드를 한 곳에 둡니다.
없는 이름이면 그 자리에서 알립니다.
6교시에 사용하는 용어
용어 (읽는 법)뜻6교시의 예
| 레지스트리 (registry) | 이름과 함수를 짝지어 둔 딕셔너리 | tool_registry = {"lookup_ip": lookup_ip} |
| 라우터 (router) | 이름을 보고 실제 함수를 찾아 실행하는 코드 | route_tool_call(choice) |
6.1 함수도 딕셔너리의 값으로 담을 수 있습니다
함수 이름을 괄호 없이 쓰면 실행하지 않고 함수 자체를 가리킵니다.
그래서 딕셔너리의 값으로 담을 수 있습니다.
def say_ok(args):
return "ok " + args["name"]
registry = {
"ok": say_ok
}
func = registry.get("ok")
print(
func(
{"name": "deploy"}
)
)
# ok deploy
print(
registry.get(
"없는이름"
)
)
# None
say_ok는 함수 자체이고, say_ok(…)는 함수를 실행한 결과입니다.
10/2 오후 스케줄러에 함수를 넘길 때도 괄호 없이 썼습니다.
.get()은 9/22에 배웠습니다.
키가 없으면 오류를 내지 않고 None을 반환합니다.
도구가 늘어도 딕셔너리에 한 줄만 더하면 됩니다.
실행하는 코드는 바뀌지 않습니다.
🐍 문법 상자 · 함수를 딕셔너리에 담기 — 괄호 없이
def double(args):
return args["n"] * 2
registry = {}
registry["double"] = double
print(
registry["double"](
{"n": 5}
)
)
# 10
쓰는 것뜻
| double | 함수 자체 — 담아 둘 수 있다 |
| double({"n": 5}) | 함수를 실행한 결과 — 10 |
| registry["double"]({"n": 5}) | 꺼낸 함수를 실행한다 |
⚠ registry["double"] = double()처럼 괄호를 붙이면 담는 순간 실행되어 TypeError가 납니다.
6.2 등록되지 않은 이름은 무시하지 않습니다
5교시에 봤듯이 LLM은 목록에 없는 일을 시키면 엉뚱한 도구를 고르거나, 없는 이름을 답할 수 있습니다.
func = tool_registry.get(
"delete_all_logs"
)
# None
if func:
result = func(args)
else:
print(
"[오류] 등록되지 않은 도구: delete_all_logs"
)
레지스트리에 없는 이름이면 .get()이 None을 반환합니다.
if func:로 확인합니다.
없는 이름을 무시하지 않고 알립니다.
LLM이 도구를 지어냈거나 레지스트리를 잘못 만든 것이라, 어느 쪽이든 사람이 알아야 합니다.
조용히 넘어가면 「왜 조치가 안 됐는지」가 어디에도 남지 않습니다.
🐍 문법 상자 · .get() — 없는 키는 None
registry = {
"count_failed_logins": 1
}
print(
registry.get("count_failed_logins"),
registry.get("block_everything")
)
# 1 None
쓰는 것키가 없을 때
| registry[name] | KeyError로 멈춘다 |
| registry.get(name) | None — if func:로 확인할 수 있다 |
⚠ LLM이 목록에 없는 도구 이름을 보낼 수 있습니다.
그래서 []가 아니라 .get()으로 꺼냅니다.
🐍 문법 상자 · 도구를 더해도 라우터는 그대로
def count_by_ip(args):
return 3
registry = {}
registry["count_by_ip"] = count_by_ip
print(
list(
registry.keys()
)
)
# ['count_by_ip']
바꾸는 곳바꾸지 않는 곳
| 도구 함수 하나 · 등록 한 줄 | route_tool_call |
⚠ 라우터는 이름으로 찾을 뿐이라 도구가 늘어도 고칠 필요가 없습니다.
이름만 LLM에게 알려 준 것과 같아야 합니다.
한눈에
하려는 일쓰는 코드
| 도구를 등록한다 | tool_registry["lookup_ip"] = lookup_ip (괄호 없이) |
| 이름으로 함수를 꺼낸다 | func = tool_registry.get(name) |
| 꺼낸 함수를 실행한다 | func(choice["args"]) |
| 없는 이름을 알린다 | print("[오류] 등록되지 않은 도구:", name) |
| 한 줄로 실행한다 | route_tool_call(choice) |
오후 산출물 agent_core/tool_router.py가 생겼습니다.
도구가 늘어도 라우터는 고치지 않습니다.
승인 게이트 — 위험한 도구 앞에 사람을 둔다
사람 개입
AI나 자동화 흐름에서 AI가 처리하기 어렵거나 위험도가 높은 지점, 혹은 중요한 판단과 승인이 필요한 단계에 배치한다.
되돌릴 수 없는 조치
시스템 상태나 데이터를 이전으로 복구할 수 없는 '되돌릴 수 없는 조치'를 자동 실행하는 것은 비즈니스 연속성에 치명적인 위험을 초래할 수 있다.
7 · 위험한 도구 앞에 사람을 둔다
왜 필요한가
로그를 세거나 IP를 조회하는 도구는 틀려도 다시 하면 됩니다.
계정 잠금은 다릅니다.
잘못 잠그면 그 사람은 일을 못 하고, 되돌리는 데 사람 손이 듭니다.
5교시에 봤듯이 LLM은 엉뚱한 도구를 고를 수 있습니다.
그래서 되돌리기 어려운 도구 앞에는 사람의 확인을 둡니다.
이것을 승인 게이트라고 합니다.
7교시에 사용하는 용어
용어 (읽는 법)뜻7교시의 예
| 승인 게이트 (approval gate) | 위험한 실행 앞에 사람 확인을 두는 것 | if choice["tool"] in need_approval: |
| 사람 개입 (human-in-the-loop · 휴먼 인 더 루프) | 자동 흐름 중간에 사람이 끼는 설계 | 담당자의 답 answer = "y" |
7.1 승인이 필요한 도구를 목록으로 정해 둡니다
도구를 되돌릴 수 있는가로 나눕니다.
승인이 필요한 도구의 이름을 리스트에 적어 둡니다.
need_approval = [
"lock_account"
]
print(
"lock_account"
in need_approval
)
# True
print(
"lookup_ip"
in need_approval
)
# False
바로 실행해도 되는 도구승인을 받아야 하는 도구
| count_failed_logins · lookup_ip | lock_account |
| 틀려도 다시 하면 된다 | 잘못하면 사람이 일을 못 한다 |
in은 9/23에 배웠습니다.
리스트 안에 그 값이 있으면 True입니다.
승인 여부를 LLM이 정하게 하지 않습니다.
규칙은 코드에 적혀 있어야 언제나 같게 동작합니다.
오늘의 lock_account는 실습이라 화면에 출력만 합니다.
실제 계정을 잠그지 않습니다.
7.2 담당자의 답을 보고 실행할지 정합니다
승인이 필요한 도구는 담당자의 답을 확인한 뒤에 실행합니다.
실습에서는 그 답을 변수 answer에 직접 적습니다.
answer = "n" # 담당자의 답
if answer == "y":
print("실행합니다")
else:
print("보류합니다")
# 보류합니다
"y"일 때만 실행합니다.
"y"가 아닌 모든 답은 실행하지 않습니다.
잘못 적은 답이 실행으로 이어지면 안 됩니다.
보류했다는 것은 문자열 "held"로 남깁니다.
실제 서비스에서는 담당자의 답을 승인 화면이나 메신저에서 받습니다.
5과목에서 다룹니다.
오늘은 답을 확인하고 나서 실행한다는 순서만 만듭니다.
🐍 문법 상자 · in과 !=
need_approval = [
"lock_account",
"block_ip"
]
tool = "lock_account"
answer = "n"
print(
tool in need_approval,
answer != "y"
)
# True True
쓰는 것뜻
| tool in need_approval | 목록 안에 있으면 True (9/23) |
| answer != "y" | "y"가 아니면 True — 「같지 않다」 |
⚠ "Y"처럼 대문자로 답하면 != "y"가 True라서 보류됩니다.
그대로 두는 것이 안전한 쪽입니다.
7.3 무엇을 실행했는지 기록으로 남깁니다
에이전트가 한 일은 실행했든 보류했든 기록으로 남깁니다.
9/28 오후에 배운 json.dump로 agent_result.json에 저장합니다.
records = []
records.append(
{
"goal": "admin 계정이 몇 번 실패했나",
"tool": "count_failed_logins",
"result": 4
}
)
with open(
"agent_result.json",
"w",
encoding="utf-8"
) as f:
json.dump(
records,
f,
ensure_ascii=False,
indent=2
)
기록 한 건은 딕셔너리입니다.
목표 · 고른 도구 · 결과를 담습니다.
보류한 것도 "result": "held"로 남깁니다.
남기지 않으면 「왜 조치가 안 됐는지」를 설명할 수 없습니다.
ensure_ascii=False는 한글을 그대로 저장하고, indent=2는 두 칸 들여쓰기입니다.
🐍 문법 상자 · 기록을 리스트에 모아 JSON으로
import json
records = []
records.append(
{
"goal": "잠가 주세요",
"tool": "lock_account",
"result": "held"
}
)
print(
json.dumps(
records,
ensure_ascii=False,
indent=2
)
)
결과:
[
{
"goal": "잠가 주세요",
"tool": "lock_account",
"result": "held"
}
]
쓰는 것뜻
| records.append({…}) | 리스트 끝에 딕셔너리 하나를 더한다 (9/22) |
| ensure_ascii=False | 한글을 그대로 쓴다 |
| indent=2 | 두 칸 들여쓰기 — 사람이 읽기 좋게 |
| json.dump(records, f, …) | 같은 모양을 파일 f에 쓴다 |
⚠ ensure_ascii=False가 없으면 한글이 \uc7a0 같은 기호로 저장됩니다.
한눈에
하려는 일쓰는 코드
| 승인이 필요한 도구를 정한다 | need_approval = ["lock_account"] |
| 승인이 필요한지 확인한다 | if choice["tool"] in need_approval: |
| 담당자의 답을 확인한다 | if answer == "y": |
| 승인하지 않으면 보류한다 | if answer != "y": return "held" |
| 한 일을 기록한다 | records.append({"goal": …, "tool": …, "result": …}) |
| 파일로 남긴다 | json.dump(records, f, ensure_ascii=False, indent=2) |
오늘은 LLM과 함께한 즐거운 시간입ㄴㅣ다...^_^
이제 AI 자동화 기초가 얼마 남지 않았는데,, 떨리네요 다음주에는 또 어떤 내용이 시작될까,,,?
3일간의 연휴를 끝내고 오니 정말 정신없구 좋은 것 같아요.
오늘 점심으로는 집에서 불닭김밥을 싸왔는데, 가끔 불닭쌈이나 다양한 밥을 싸오는게 목표입니다.
다만,, 휴게실에서 라면이나 냄새가 너무 심한건 못먹어서 아쉽긴 한데 어쩔 수 없죠,,
그럼 빠잇 아됴,,,,
'공부 > SKT ALEPH' 카테고리의 다른 글
| AI·자동화 기초(에이전트·API·스크립팅) - 6 (0) | 2026.10.02 |
|---|---|
| AI·자동화 기초(에이전트·API·스크립팅) - 5 (0) | 2026.09.30 |
| AI·자동화 기초(에이전트·API·스크립팅) - 4 (0) | 2026.09.29 |
| AI·자동화 기초(에이전트·API·스크립팅) - 3 (0) | 2026.09.28 |
| AI·자동화 기초(에이전트·API·스크립팅) - 2 (0) | 2026.09.23 |


