XV. 작은 AI Agent 프로그램(실제 LLM -OpenAI 사용)
#-------- 1. Tools
from pathlib import Path
def calculator(expr:str)->str:
"""(20*4)+15와 같은 간단한 산술 표현식을 계산합니다."""
allowed = set("0123456789+-*/().")
expr = expr.replace(" ", "")
if not expr or not set(expr) <=allowed:
return "오류: 숫자와 +-*/().만 허용됩니다."
try:
return str(eval(expr))
except (SyntaxError, ZeroDivisionError) as e:
return f"ERROR: {type(e).__name__}"
def read_file(path:str, max_chars:int=800)->str:
"""로컬 텍스트 파일을 읽습니다. URL에는 사용하지 마십시오."""
p=Path(path)
if not p.exists():
return f"오류: {path}라는 이름의 파일이 없습니다."
text=p.read_text(encoding="utf-8")
return text[:max_chars]
def lookup(topic:str)->str:
"""내부 지식 기반(facts)에서 사실을 조회합니다."""
facts={
"환불 정책":"배송후 30일 이내",
"단가":"$24",
"배송":"$500 이상 무료",
}
return facts.get(topic.lower(), f"오류: '{topic}'에 대해 알려진 정보가 없습니다.")
TOOLS = {"calculator":calculator, "read_file":read_file, "lookup":lookup}
# print(calculator("(25*4)+15"))
# print(lookup("단가"))
# print(read_file("missing.txt"))
#-------- 2. tool_description()
import inspect
def tool_description(tools:dict)->str:
lines=[]
for name, func in tools.items():
params=",".join(inspect.signature(func).parameters)
summary=(inspect.getdoc(func) or "").splitlines()[0]
lines.append(f"- {name}({params}):{summary}")
return "\n".join(lines)
def build_system_prompt(tools:dict)->str:
return f"""
당신은 단계적으로 작업하는 신중한 조력자입니다.(You are a careful assistant that works in steps.)
사용 가능한 도구(Tools available):
{tool_description(tools)}
JSON 객체 하나만 응답으로 보내주세요. 다른 내용은 포함하지 마세요.(Reply with ONE JSON object and nothing else):
{{"tool":"<tool name or finish>", "args": {{...}}}}
최종 답변을 얻으면 다음을 사용하세요.(When you have the final answer, use):
{{"tool":"finish", "args":{{"answer":"..."}}}}
작업 규칙:
1. 가격이나 비용을 계산해야 하는데 단가가 제공되지 않았다면, 먼저 lookup 도구를 사용하여 단가를 확인하세요.
2. 숫자 계산이 필요한 경우 직접 계산하지 말고, 반드시 calculator 도구를 사용하세요.
3. 파일의 내용을 확인해야 하는 경우, read_file 도구를 사용하세요.
4. Tool 실행 결과는 Observation에서 확인하세요.
5. Observation 결과를 이용하여 다음 작업을 결정하세요.
6. 충분한 정보를 얻으면 finish를 사용하세요.
"""
# print(build_system_prompt(TOOLS))
#-------- 3. Agent
import inspect
import json
import logging
import os
from datetime import datetime
from pathlib import Path
# 가. 추가
from openai import OpenAI
client = OpenAI()
logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s")
log = logging.getLogger("agent")
MAX_STEPS = int(os.environ.get("MAX_STEPS", "6"))
DATA_DIR = Path("agent_data")
# 나. LLM모델 대체하던 SCRIPT 삭제
# 다. llm호출하는, call_model 코드 새로 작성
def call_model(messages:list[dict])->dict:
try:
response = client.responses.create(
model="gpt-4o-mini",
input=messages,
)
return {"ok":True, "text":response.output_text, "error":None,}
except Exception as e:
return {"ok":False, "text":None, "error":f"{type(e).__name__}:{e}"}
# parsing
def parse_reply(text:str)->dict:
if not text or not text.strip():
return {"ok":False, "tool":None, "args":{}, "error":"empty reply"}
start, end = text.find("{"), text.rfind("}")
if start == -1 or end == -1:
return {"ok":False, "tool":None, "args":{}, "error":"no JSON found"}
try:
data = json.loads(text[start:end+1])
except json.JSONDecodeError as e:
return {"ok":False, "tool":None, "args":{}, "error":f"bad JSON:{e.msg}"}
return {
"ok":True,
"tool":data.get("tool"),
"args":data.get("args",{}),
"error":None,
}
# the agent
class Agent:
def __init__(self, tools:dict, max_steps:int=MAX_STEPS):
self.tools=tools
self.max_steps=max_steps
self.steps=0
self.history=[]
self.answer=None
def __repr__(self):
return f"Agent(steps={self.steps}/{self.max_steps}, msgs={len(self.history)})"
def add(self, role:str, content:str):
self.history.append({"role":role, "content":content})
def dispatch(self, name:str, args:dict)->str:
if name not in self.tools:
return f"ERROR:unknown tool '{name}'. Available:{','.join(self.tools)}"
if not isinstance(args, dict):
return "ERROR: args must be an object"
func = self.tools[name]
expected = set(inspect.signature(func).parameters)
unknown = set(args) - expected
if unknown:
return f"ERROR:unexpected argument(s): {sorted(unknown)}"
required = {name for name, args in inspect.signature(func).parameters.items() if args.default is inspect.Parameter.empty}
missing = required-set(args)
if missing:
return f"ERROR:missing required argument(s): {sorted(missing)}"
try:
return str(func(**args))
except TypeError as e:
return f"ERROR: bad arguments - {e}"
except Exception as e:
return f"ERROR: {type(e).__name__}:{e}"
def run(self, goal:str)->str:
self.add("system", build_system_prompt(self.tools))
self.add("user", goal)
log.info(f"goal: {goal}")
while self.answer is None and self.steps < self.max_steps:
self.steps +=1
reply = call_model(self.history)
if not reply["ok"]:
log.error(f"model call failed: {reply['error']}")
break
self.add("assistant", reply["text"])
parsed = parse_reply(reply["text"])
if not parsed["ok"]:
log.warning(f"step {self.steps}: {parsed['error']}")
self.add("user",f"That was not valid JSON ({parsed['error']}). Try again.")
continue
tool = parsed["tool"]
if tool == "finish":
self.answer = parsed["args"].get("answer","(no answer given)")
log.info(f"step {self.steps}: finished")
break
observation = self.dispatch(tool, parsed["args"])
log.info(f"step {self.steps}: {tool} -> {observation}")
self.add("user", f"Observation: {observation}")
if self.answer is None:
self.answer = f"Stopped after {self.steps} steps without an answer."
self.save()
return self.answer
def save(self):
DATA_DIR.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
path = DATA_DIR/f"run-{stamp}.json"
path.write_text(
json.dumps(
{"steps":self.steps, "answer":self.answer, "history":self.history},
indent=2,
ensure_ascii=False,
),
encoding="utf-8",
)
log.info(f"transcript saved to {path}")
if __name__=="__main__":
bot = Agent(TOOLS)
result = bot.run("7개의 가격은 얼마인가요?")
print("-"*50)
print("ANSWER: ", result)
print(bot)
# INFO goal: 7개의 가격은 얼마인가요?(단가는 lookup tool에서 찾고, 계산은 calculator tool로 해라.)
# INFO HTTP Request: POST https://api.openai.com/v1/responses "HTTP/1.1 200 OK"
# INFO step 1: lookup -> $24
# INFO HTTP Request: POST https://api.openai.com/v1/responses "HTTP/1.1 200 OK"
# INFO step 2: calculator -> 168
# INFO HTTP Request: POST https://api.openai.com/v1/responses "HTTP/1.1 200 OK"
# INFO step 3: finished
# INFO transcript saved to agent_data\run-20260826-184102.json
# --------------------------------------------------
# ANSWER: 7개의 가격은 168달러입니다.
# Agent(steps=3/10, msgs=7)
1. 이번 코드에서 가장 큰 변화
이전:
User
↓
Agent
↓
SCRIPT (가짜 모델)
↓
Tool
현재:
User
↓
Agent
↓
OpenAI API
↓
실제 GPT 판단
↓
Tool 선택
↓
Python Tool 실행
↓
Observation
↓
OpenAI API 재호출
↓
최종 Answer
즉, Agent의 뇌가 진짜 AI로 바뀌었습니다.
2. 전체 프로그램 구조
이번 코드를 크게 나누면 다음과 같습니다.
프로그램
│
├── 1. Tools
│ ├── calculator()
│ ├── read_file()
│ └── lookup()
│
├── 2. Tool Registry
│ └── TOOLS
│
├── 3. System Prompt
│ ├── tool_description()
│ └── build_system_prompt()
│
├── 4. LLM 연결
│ ├── OpenAI()
│ └── call_model()
│
├── 5. JSON Parser
│ └── parse_reply()
│
├── 6. Agent
│ ├── history
│ ├── dispatch()
│ ├── run()
│ └── save()
│
└── 7. main
└── bot.run()
이제 하나씩 보겠습니다.

3. Tools 부분
이 부분은 이전 코드와 거의 같지만 조금 개선되었습니다.
calculator()
def calculator(expr:str)->str:
이 함수는:
"24*7"
↓
calculator
↓
"168"
을 만드는 Tool입니다.
허용 문자
allowed = set("0123456789+-*/().")
허용되는 문자는:
숫자
+
-
*
/
(
)
.
입니다.
이번에 추가한 개선
expr = expr.replace(" ", "")
이전 코드에서는:
calculator("24 * 7")
이 실패할 수 있었습니다.
왜냐하면:
24 * 7
↑
공백
이 allowed에 없기 때문입니다.
그래서 지금은:
"24 * 7"
↓
replace(" ", "")
↓
"24*7"
이 됩니다.
이것은 좋은 개선입니다. 👍
4. calculator의 입력 검사
if not expr or not set(expr) <= allowed:
두 가지를 검사합니다.
① 빈 문자열인가?
expr = ""
이면:
not expr
가 True입니다.
② 이상한 문자가 있는가?
예:
expr = "24*hello"
그러면:
h
e
l
o
등이 허용 문자에 없습니다.
그래서 계산을 하지 않습니다.
5. eval()
return str(eval(expr))
예:
"24*7"
↓
eval()
↓
168
↓
str()
↓
"168"
입니다.
6. lookup()
def lookup(topic:str)->str:
현재 Agent의 작은 지식 DB입니다.
facts={
"환불 정책":"배송후 30일 이내",
"단가":"$24",
"배송":"$500 이상 무료",
}
그림으로 보면:
facts
│
├── "환불 정책"
│ ↓
│ "배송후 30일 이내"
│
├── "단가"
│ ↓
│ "$24"
│
└── "배송"
↓
"$500 이상 무료"
사용자가:
7개의 가격은 얼마인가요?
라고 물으면 모델은 계산을 바로 할 수도 있지만, System Prompt를 보고:
단가를 모른다
↓
lookup("단가")
↓
$24
↓
calculator("24*7")
이라는 순서를 판단하게 됩니다.
7. TOOLS Registry
TOOLS = {
"calculator":calculator,
"read_file":read_file,
"lookup":lookup
}
이 부분은 Agent의 도구 상자입니다.
TOOLS
│
├── calculator ───► calculator()
│
├── read_file ────► read_file()
│
└── lookup ───────► lookup()
LLM이:
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
라고 결정하면 Agent는:
TOOLS["lookup"]
을 찾아서:
lookup(topic="단가")
를 실행합니다.
8. tool_description()
def tool_description(tools:dict)->str:
이 함수는 매우 중요합니다.
Python 내부의 Tool 정보를 LLM이 읽을 수 있는 설명문으로 바꿉니다.
예를 들어:
def calculator(expr:str)->str:
을 inspect로 분석합니다.
inspect.signature(func)
↓
(expr: str) -> str
그리고 Docstring:
"""(20*4)+15와 같은 간단한 산술 표현식을 계산합니다."""
를 가져옵니다.
최종적으로:
- calculator(expr):(20*4)+15와 같은 간단한 산술 표현식을 계산합니다.
같은 문장을 만듭니다.
9. build_system_prompt()
def build_system_prompt(tools:dict)->str:
Agent의 작업 설명서를 만드는 함수입니다.
결과적으로 LLM에게 이런 내용을 보냅니다.
너는 단계적으로 작업하는 조력자다.
사용 가능한 도구:
- calculator(expr)
- read_file(path,max_chars)
- lookup(topic)
JSON 하나만 응답해라.
형식:
{
"tool":"도구 이름",
"args": {...}
}
즉:
Python 세계
↓
tool_description()
↓
자연어 설명
↓
System Prompt
↓
LLM
이라는 연결입니다.
10. 가장 중요한 변화: OpenAI 연결
이 부분입니다.
from openai import OpenAI
client = OpenAI()
이전에는:
Agent
↓
SCRIPT
였지만 지금은:
Agent
↓
client
↓
OpenAI API
입니다.
client = OpenAI()
이 코드는 OpenAI API와 통신할 수 있는 객체를 만듭니다.
개념적으로:
client
│
└── OpenAI와 대화할 수 있는 창구
라고 생각하면 됩니다.
보통 API Key는 환경변수에서 읽습니다.
예:
OPENAI_API_KEY=sk-....
그러면:
client = OpenAI()
가 자동으로 해당 환경변수를 이용합니다.
11. call_model()
이번 코드에서 가장 큰 변화입니다.
def call_model(messages:list[dict])->dict:
역할:
history
↓
OpenAI API
↓
GPT
↓
응답
↓
Agent가 사용할 Dictionary
API 호출
response = client.responses.create(
model="gpt-4o-mini",
input=messages,
)
구조를 보면:
messages
↓
client.responses.create()
↓
OpenAI 서버
↓
모델
↓
response
입니다.
messages
예를 들어 실제 history는 이런 구조가 됩니다.
[
{
"role":"system",
"content":"너는 단계적으로..."
},
{
"role":"user",
"content":"7개의 가격은 얼마인가요?"
}
]
이 전체가:
input=messages
로 들어갑니다.
즉 LLM은 단순히 마지막 질문만 보는 것이 아니라:
System Prompt
+
사용자 질문
+
이전 Tool 결과
+
이전 판단
을 보고 판단합니다.
이것이 Agent의 Memory 구조입니다.
12. response.output_text
response.output_text
예를 들어 GPT가:
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
라고 응답했다면:
response.output_text
에 그 문자열이 들어갑니다.
그리고:
return {
"ok":True,
"text":response.output_text,
"error":None,
}
로 Agent에게 전달합니다.
13. 왜 try / except가 필요한가?
try:
API 호출은 실패할 수 있습니다.
예:
인터넷 문제
API Key 문제
일시적인 서버 오류
Rate Limit
Timeout
그래서:
except Exception as e:
로 잡습니다.
그리고:
return {
"ok":False,
"text":None,
"error":f"{type(e).__name__}:{e}"
}
를 반환합니다.
Agent는:
if not reply["ok"]:
에서 실패를 확인할 수 있습니다.
14. parse_reply()
이 부분은 이전과 동일하지만 실제 모델을 사용하기 때문에 훨씬 중요해졌습니다.
def parse_reply(text:str)->dict:
LLM이 다음처럼 보내기를 기대합니다.
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
하지만 LLM은 가끔 이렇게 보낼 수 있습니다.
단가를 먼저 확인하겠습니다.
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
또는:
```json
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
그래서:
```python
start = text.find("{")
end = text.rfind("}")
로 JSON 부분만 잘라냅니다.
15. Agent Class
이제 전체 Agent를 다시 보겠습니다.
Agent
│
├── tools
│
├── max_steps
│
├── steps
│
├── history
│
├── answer
│
├── add()
│
├── dispatch()
│
├── run()
│
└── save()
Agent는 단순히 AI를 호출하는 객체가 아닙니다.
더 정확하게 말하면:
LLM의 판단과 Python Tool 실행을 관리하는 관리자
입니다.
16. dispatch()의 개선
이번에 중요한 개선이 있습니다.
이전에는 예상하지 않은 인자만 검사했습니다.
이번에는 필수 인자가 빠진 경우도 검사합니다.
먼저 Tool 확인
if name not in self.tools:
예:
{
"tool":"weather"
}
하지만 Registry에:
weather 없음
그러면:
ERROR:unknown tool 'weather'
를 반환합니다.
args가 Dictionary인지 확인
if not isinstance(args, dict):
예를 들어 모델이:
{
"tool":"lookup",
"args":"단가"
}
라고 하면 안 됩니다.
반드시:
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
여야 합니다.
17. expected
expected = set(inspect.signature(func).parameters)
예:
def lookup(topic:str):
이면:
expected = {"topic"}
입니다.
unknown 검사
unknown = set(args) - expected
예:
args = {
"topic":"단가",
"language":"ko"
}
이면:
args
=
{"topic", "language"}
expected
=
{"topic"}
따라서:
unknown
=
{"language"}
입니다.
18. 이번에 추가한 required 검사
required = {
name
for name, args in inspect.signature(func).parameters.items()
if args.default is inspect.Parameter.empty
}
조금 어렵지만 이전에 배운 내용을 실제 Agent에 적용한 것입니다.
예:
def read_file(path, max_chars=800):
이면:
path
↓
기본값 없음
↓
필수
max_chars
↓
기본값 있음
↓
선택
결과:
required = {"path"}
missing
missing = required - set(args)
예:
args = {}
이면:
required
=
{"path"}
args
=
{}
missing
=
{"path"}
그래서:
ERROR:missing required argument(s): ['path']
를 반환합니다.
19. 작은 오타 발견
여기:
return f"ERROR:missing required argement(s): {sorted(missing)}"
argement는 오타입니다.
올바른 것은:
argument
입니다.
따라서:
return f"ERROR:missing required argument(s): {sorted(missing)}"
가 좋습니다.
실행에는 큰 문제는 없지만 오류 메시지가 사용자에게 보이면 수정하는 것이 좋습니다.
20. run() — 진짜 Agent Loop
이제 가장 중요합니다.
def run(self, goal:str)->str:
사용자가:
bot.run("7개의 가격은 얼마인가요?")
하면 시작합니다.
Step 1: System Prompt 저장
self.add(
"system",
build_system_prompt(self.tools)
)
History:
[
system
]
Step 2: 사용자 질문 저장
self.add("user", goal)
History:
[
system,
user
]
Step 3: Agent Loop 시작
while self.answer is None and self.steps < self.max_steps:
조건은:
답이 없고
AND
최대 단계에 도달하지 않았다면
계속합니다.
실제 실행 흐름
질문:
7개의 가격은 얼마인가요?
단가는 lookup tool에서 찾고,
계산은 calculator tool로 해라.
이렇게 상세하게 줬기 때문에 모델이 잘 움직입니다.
STEP 1
History:
system
user: 질문
↓
reply = call_model(self.history)
↓
OpenAI API
↓
GPT 판단:
가격을 계산하려면 단가가 필요하다.
lookup Tool 사용
응답:
{
"tool":"lookup",
"args":{
"topic":"단가"
}
}
Agent가 이것을 History에 저장합니다.
self.add("assistant", reply["text"])
History:
system
user
assistant: {"tool":"lookup"...}
parse_reply()
parsed = parse_reply(reply["text"])
결과:
{
"ok":True,
"tool":"lookup",
"args":{
"topic":"단가"
},
"error":None
}
dispatch()
observation = self.dispatch(
"lookup",
{"topic":"단가"}
)
↓
lookup(topic="단가")
↓
"$24"
로그:
INFO step 1: lookup -> $24
Observation 저장
self.add(
"user",
"Observation: $24"
)
현재 History는:
[
system,
user: 7개의 가격은 얼마인가요?,
assistant: lookup 단가,
user: Observation: $24
]
입니다.
STEP 2
다시 모델 호출:
call_model(self.history)
이번에는 모델이 이전 결과를 봅니다.
질문: 7개의 가격?
Observation:
단가 = $24
따라서:
24 × 7
을 계산해야 합니다.
모델 응답:
{
"tool":"calculator",
"args":{
"expr":"24*7"
}
}
Tool 실행:
calculator("24*7")
↓
168
로그:
INFO step 2: calculator -> 168
STEP 3
History에 이제:
Observation: 168
이 들어 있습니다.
다시 GPT가 호출됩니다.
GPT는:
이제 답을 얻었다.
고 판단합니다.
응답:
{
"tool":"finish",
"args":{
"answer":"7개의 가격은 168달러입니다."
}
}
Agent:
if tool == "finish":
를 만납니다.
그리고:
self.answer = parsed["args"].get("answer")
↓
7개의 가격은 168달러입니다.
Loop 종료.
실제 로그를 해석하면
INFO goal: 7개의 가격은 얼마인가요?
Agent가 목표를 받았습니다.
INFO HTTP Request: POST ...
200 OK
OpenAI API 호출 성공.
INFO step 1: lookup -> $24
첫 번째 Tool 실행.
lookup("단가")
↓
$24
INFO step 2: calculator -> 168
두 번째 Tool 실행.
calculator("24*7")
↓
168
INFO step 3: finished
모델이:
{"tool":"finish"}
를 반환했습니다.
INFO transcript saved to agent_data\run-...
전체 실행 기록 저장 완료.
21. History가 실제로 어떻게 쌓였는가?
이번 실행의 History를 단순화하면:
history
│
├── [0]
│ system
│ └── Agent 규칙 + Tool 설명
│
├── [1]
│ user
│ └── 7개의 가격은 얼마인가요?
│
├── [2]
│ assistant
│ └── lookup 단가
│
├── [3]
│ user
│ └── Observation: $24
│
├── [4]
│ assistant
│ └── calculator 24*7
│
├── [5]
│ user
│ └── Observation: 168
│
└── [6]
assistant
└── finish
그래서:
msgs = 7
입니다.
로그:
Agent(steps=3/10, msgs=7)
와 정확히 연결됩니다.
22. 그런데 왜 질문을 상세하게 해야 잘 동작할까?
이게 지금 가장 중요한 질문입니다.
현재 System Prompt에는:
너는 신중한 조력자다.
Tool:
calculator
read_file
lookup
JSON으로 답해라.
정도만 있습니다.
하지만 사용자 질문:
7개의 가격은 얼마인가요?
를 보면 문제가 있습니다.
AI 입장에서는:
"가격"이 뭐지?
"7개"의 무엇이지?
단가는 어디에 있지?
lookup을 써야 하나?
라고 생각할 수 있습니다.
즉:
Tool은 알려줬지만, Tool을 언제 어떻게 사용해야 하는지 충분히 가르쳐주지 않았습니다.
그래서 사용자가:
단가는 lookup에서 찾아라.
계산은 calculator로 해라.
라고 직접 지시해야 했던 것입니다.
23. 이것이 현재 Agent의 가장 큰 부족한 부분
현재 System Prompt:
도구 목록은 알려줌
하지만 부족한 것:
언제 lookup을 사용해야 하는가?
언제 calculator를 사용해야 하는가?
여러 Tool을 어떤 순서로 사용해야 하는가?
사용자 질문의 어떤 표현이 어떤 Tool과 연결되는가?
Tool 결과를 받은 후 무엇을 해야 하는가?
입니다.
즉 현재 Agent는:
도구 설명서
는 가지고 있지만:
업무 매뉴얼
이 부족합니다.
24. 지금부터 개선할 수 있는 System Prompt
예를 들어 지금의 System Prompt에 이런 규칙을 추가할 수 있습니다.
작업 규칙:
1. 가격이나 비용을 계산해야 하는데 단가가 제공되지 않았다면
먼저 lookup 도구를 사용하여 단가를 확인하세요.
2. 숫자 계산이 필요한 경우 직접 계산하지 말고
반드시 calculator 도구를 사용하세요.
3. 파일의 내용을 확인해야 하는 경우
read_file 도구를 사용하세요.
4. Tool 실행 결과는 Observation에서 확인하세요.
5. Observation 결과를 이용하여 다음 작업을 결정하세요.
6. 충분한 정보를 얻으면 finish를 사용하세요.
그러면 사용자는 단순히:
7개의 가격은 얼마인가요?
라고 해도 Agent가 훨씬 잘 판단할 가능성이 높아집니다.
25. 현재 Agent의 수준을 평가하면
현재 Agent는:
Level 1
Tool을 실행할 수 있다
입니다.
조금 더 구체적으로:
현재
│
├── 실제 OpenAI API 연결 ✅
├── 실제 LLM 판단 ✅
├── Tool 선택 ✅
├── Tool 실행 ✅
├── 여러 단계 반복 ✅
├── 결과를 History에 저장 ✅
├── 최종 답변 생성 ✅
├── 파일 저장 ✅
│
├── Tool 사용 규칙 부족 ⚠️
├── Prompt 설계 부족 ⚠️
├── JSON 안정성 부족 ⚠️
├── Tool 결과 role 구조 개선 필요 ⚠️
├── API 오류별 처리 부족 ⚠️
└── Retry 미적용 ⚠️
입니다.
26. 특히 중요한 잠재적 문제점
문제 1. parse_reply()가 완벽하지 않음
현재:
start = text.find("{")
end = text.rfind("}")
입니다.
간단한 JSON에는 좋습니다.
하지만 LLM이:
설명:
{"tool":"lookup"}
추가 설명:
{"something":"else"}
라고 보내면:
첫 번째 {
~
마지막 }
를 통째로 잡아서 JSON 오류가 발생할 수 있습니다.
현재 단계에서는 괜찮지만 앞으로는:
- Structured Output
- JSON Schema
- 공식 Tool Calling
으로 발전시키는 것이 좋습니다.
문제 2. Tool 결과를 user로 저장
현재:
self.add("user", f"Observation: {observation}")
입니다.
학습용 Agent로는 충분합니다.
하지만 구조적으로는:
system
user
assistant
tool
assistant
tool
assistant
가 더 자연스럽습니다.
즉:
self.add("tool", observation)
같은 구조로 발전할 것입니다.
문제 3. 실제 Tool Calling이 아직 아님
현재 구조는:
GPT에게 Prompt로:
"JSON 형식으로 Tool 이름을 말해라"
라고 요청하고 있습니다.
그래서 GPT가:
{
"tool":"lookup"
}
을 텍스트로 생성하는 방식입니다.
하지만 앞으로 OpenAI의 정식 Tool Calling을 사용하면:
GPT
↓
공식 Tool Call 구조
↓
Python이 정확하게 Tool 실행
으로 더 안정적으로 만들 수 있습니다.
이것이 앞으로 아주 중요한 다음 단계입니다.
27. 이번 코드에서 가장 잘한 부분
제가 보기에는 이번 코드에서 특히 잘한 부분은 3가지입니다.
① SCRIPT를 실제 API로 교체
SCRIPT
↓
client.responses.create()
이것은 큰 전환점입니다.
② 필수 인자 검사 추가
required
missing
을 넣은 것은 매우 좋습니다.
단순 Agent에서 점점 견고한 Agent로 가는 과정입니다.
③ History 기반으로 실제 모델을 반복 호출
call_model(self.history)
이것이 진짜 Agent Loop의 핵심입니다.
단순 Chatbot:
질문
↓
답변
Agent:
질문
↓
판단
↓
Tool
↓
결과
↓
다시 판단
↓
Tool
↓
결과
↓
최종 답변
입니다.
지금 코드가 바로 이 구조를 실제로 구현하고 있습니다.