AI 에이전트 만들기, 프레임워크 없이 5단계 구축법
AI 에이전트, 왜 직접 만들어 봐야 할까
AI 에이전트 만들기에 관심이 있다면, 가장 먼저 LangChain이나 CrewAI 같은 프레임워크를 떠올리게 됩니다. 이런 도구들은 분명 개발 속도를 높여 줍니다. 하지만 프레임워크의 편리한 추상화 뒤에 숨겨진 핵심 원리를 모르면, 에이전트가 예상과 다르게 동작할 때 원인을 찾기 어렵고 커스터마이징에도 벽에 부딪힙니다. 자동차를 운전하기 전에 엔진의 기본 원리를 이해하는 것과 같습니다. 내부를 한 번이라도 들여다본 사람은 어떤 차를 몰든 더 유연하게 대처할 수 있습니다.
2026년 여름 현재, AI 에이전트 시장은 폭발적으로 성장하고 있습니다. 단순한 챗봇을 넘어 업무를 자율적으로 처리하는 에이전트 시스템이 개발 현장과 비즈니스 전반에 빠르게 확산되고 있습니다. 이런 흐름에서 에이전트의 작동 원리를 근본부터 이해하는 것은 개발자에게 강력한 차별화 포인트가 됩니다. 프레임워크가 해 주는 일의 실체를 알아야, 프레임워크를 넘어서는 시스템도 설계할 수 있기 때문입니다.
이 글에서는 별도의 에이전트 프레임워크 없이, Python과 LLM API 호출만으로 작동하는 AI 에이전트를 처음부터 구현하는 5단계 과정을 상세히 안내합니다. 각 단계마다 핵심 개념과 실제 구현 코드를 함께 다루므로, 글을 끝까지 따라가면 에이전트의 내부 구조를 완전히 이해하고 직접 작동하는 에이전트를 완성할 수 있습니다.
AI 에이전트의 핵심 구조 이해하기
에이전트 루프 — 관찰, 사고, 행동의 반복
모든 AI 에이전트의 중심에는 에이전트 루프(Agent Loop)라는 단순하면서도 강력한 구조가 있습니다. 에이전트가 하는 모든 일은 결국 이 루프의 반복입니다. 루프는 세 단계로 구성됩니다.
- 관찰(Observe): 사용자 입력, 도구 실행 결과, 환경 정보 등 새로운 데이터를 수집합니다. 에이전트가 현재 상황을 파악하는 감각 기관에 해당합니다.
- 사고(Think): 수집한 정보를 바탕으로 LLM이 현재 상황을 분석합니다. 목표 달성을 위해 다음에 어떤 행동을 취해야 하는지, 어떤 도구를 어떤 인자로 호출해야 하는지 결정합니다. 사고 단계에서 LLM은 단순 텍스트 생성이 아니라, 구조화된 판단을 내립니다.
- 행동(Act): 사고 결과에 따라 실제 행동을 수행합니다. 도구를 호출하거나, 추가 정보를 요청하거나, 작업이 완료됐다고 판단되면 사용자에게 최종 답변을 반환합니다.
이 관찰-사고-행동 사이클이 목표 달성까지 반복되면서, 에이전트는 단일 프롬프트로는 해결할 수 없는 복잡한 작업도 단계별로 풀어냅니다. 2022년 구글 리서치와 프린스턴 대학이 발표한 ReAct(Reasoning and Acting) 논문에서 이 패턴이 체계적으로 정리됐으며, 현재 거의 모든 에이전트 프레임워크가 이 구조를 기반으로 동작합니다.

에이전트와 일반 챗봇의 결정적 차이
일반 챗봇과 에이전트의 가장 큰 차이는 자율적 행동 능력입니다. 챗봇은 사용자의 메시지를 받으면 바로 텍스트 응답을 생성합니다. 입력 한 번에 출력 한 번, 그것으로 끝입니다. 매번 사용자가 다음 단계를 지시해야만 대화가 진행됩니다.
반면 에이전트는 사용자의 요청을 분석한 뒤, 스스로 다음 행동을 결정합니다. 필요하다고 판단하면 웹 검색을 실행하고, 그 결과가 불충분하면 다른 도구를 호출하고, 최종적으로 얻은 정보를 종합해서 답변합니다. 이 전체 과정이 사용자의 추가 입력 없이 자동으로 진행됩니다.
코드로 보면 이 차이가 선명합니다. 챗봇의 핵심은 단일 호출입니다.
response = llm.generate(user_message) print(response)
에이전트의 핵심은 반복 루프입니다.
messages = [system_prompt, user_message]
while True:
response = llm.generate(messages)
if response.has_tool_call:
result = execute_tool(response.tool_call)
messages.append(result)
else:
print(response.text)
break
이 구조적 차이가 AI 에이전트의 본질입니다. 루프가 있기에 에이전트는 복잡한 문제를 스스로 분해하고, 여러 단계에 걸쳐 해결할 수 있습니다. 이제부터 이 루프를 직접 구현해 보겠습니다.
1단계 — LLM 연결과 시스템 프롬프트 설계
기본 클라이언트 구성
에이전트를 만드는 첫 단계는 LLM과의 통신 채널을 여는 것입니다. 대부분의 LLM 제공업체는 OpenAI 호환 API 형식을 지원하므로, 하나의 클라이언트 코드로 다양한 모델에 연결할 수 있습니다. Python에서는 openai 라이브러리나 httpx를 사용해 직접 HTTP 요청을 보내는 방식 중 하나를 선택합니다.
핵심은 모든 메시지를 리스트로 관리하고, LLM에 보낼 때는 이 리스트 전체를 전달한다는 점입니다. LLM은 상태를 기억하지 않으므로, 대화의 맥락은 전적으로 우리 코드가 관리해야 합니다.
import openai
client = openai.OpenAI(
api_key="your-api-key",
base_url="https://api.your-provider.com/v1"
)
def call_llm(messages, tools=None):
params = {
"model": "your-model-name",
"messages": messages,
"temperature": 0.1
}
if tools:
params["tools"] = tools
return client.chat.completions.create(**params)
call_llm 함수가 에이전트의 두뇌와 연결하는 통로입니다. messages 파라미터에 전체 대화 이력을 넘기고, tools 파라미터에 사용 가능한 도구 목록을 전달합니다. 이 함수 하나로 에이전트의 모든 사고 과정이 이뤄집니다.
에이전트 역할을 정의하는 시스템 프롬프트
시스템 프롬프트는 에이전트의 성격과 행동 규칙을 정의하는 설계도입니다. 프레임워크 없이 에이전트를 만들 때, 시스템 프롬프트의 품질이 에이전트의 성능을 크게 좌우합니다. 좋은 시스템 프롬프트에는 다음 네 가지 요소가 포함됩니다.
- 역할 정의: 에이전트가 어떤 역할을 수행하는지 명확하게 서술합니다. 예를 들어 “당신은 날씨 정보와 일정 관리를 도와주는 개인 비서입니다.”
- 사용 가능한 도구 안내: 어떤 도구가 있고, 각각 어떤 상황에서 써야 하는지 설명합니다. LLM이 도구를 적절히 선택하려면 이 안내가 구체적이어야 합니다.
- 행동 원칙: 도구 호출 전 사용자에게 확인을 받아야 하는 상황, 민감한 정보 처리 방침 등의 규칙을 정합니다.
- 응답 스타일: 답변의 길이, 톤, 형식 등을 지정합니다. 에이전트가 일관된 사용자 경험을 제공하는 데 중요합니다.
SYSTEM_PROMPT = """ 당신은 사용자의 업무를 돕는 AI 비서입니다. ## 사용 가능한 도구 - get_weather: 특정 도시의 현재 날씨를 조회합니다. - search_web: 웹에서 최신 정보를 검색합니다. - create_reminder: 사용자에게 알림을 설정합니다. ## 행동 원칙 1. 사용자의 질문에 답하기 위해 필요한 도구가 있으면 적극 사용하세요. 2. 한 번에 하나의 도구만 호출하고, 결과를 확인한 뒤 다음 행동을 결정하세요. 3. 도구 호출 없이 답할 수 있는 질문은 바로 답하세요. 4. 확실하지 않은 정보는 추측하지 말고 검색 도구를 사용하세요. """
시스템 프롬프트에서 “한 번에 하나의 도구만 호출”이라는 원칙을 넣은 이유가 있습니다. 에이전트가 여러 도구를 병렬 호출하면 디버깅이 어려워지고, 오류 발생 시 복구 로직도 복잡해집니다. 기본 구현에서는 순차 실행으로 시작하고, 나중에 필요에 따라 병렬 호출을 추가하는 것이 안전합니다.
2단계 — 도구 정의와 함수 호출 구현
도구 스키마 작성법
AI 에이전트가 외부 세계와 상호작용하려면 도구가 필요합니다. 도구는 에이전트의 손과 발이 됩니다. LLM에게 사용 가능한 도구를 알려주려면, 각 도구의 이름, 설명, 파라미터를 구조화된 스키마로 정의해야 합니다. OpenAI의 Function Calling 사양이 사실상의 표준이 되어 대부분의 LLM이 이 형식을 지원합니다.
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "특정 도시의 현재 날씨 정보를 조회합니다. 기온, 습도, 날씨 상태를 반환합니다.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "날씨를 조회할 도시 이름 (예: 서울, 부산)"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "웹에서 최신 정보를 검색합니다. 실시간 데이터나 최신 뉴스가 필요할 때 사용합니다.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색할 키워드 또는 질문"
}
},
"required": ["query"]
}
}
}
]
도구 스키마를 작성할 때 가장 중요한 것은 description 필드의 품질입니다. LLM은 이 설명을 읽고 어떤 상황에서 어떤 도구를 호출할지 결정합니다. 설명이 모호하면 엉뚱한 도구를 선택하거나 필요할 때 도구를 호출하지 않는 문제가 생깁니다. “날씨를 조회합니다”보다 “특정 도시의 현재 날씨 정보를 조회합니다. 기온, 습도, 날씨 상태를 반환합니다.”처럼 구체적으로 쓰는 것이 좋습니다.
파라미터의 description도 마찬가지입니다. 예시를 포함하면 LLM이 올바른 형식으로 인자를 전달할 확률이 높아집니다. “도시 이름”보다 “날씨를 조회할 도시 이름 (예: 서울, 부산)”이 더 효과적입니다.
LLM의 도구 호출 요청 처리
도구를 정의했으면, LLM이 도구 호출을 요청할 때 실제로 함수를 실행하는 로직이 필요합니다. LLM은 텍스트 대신 구조화된 도구 호출 요청을 반환하고, 우리 코드가 그 요청을 받아 해당 함수를 실행한 뒤 결과를 다시 LLM에 돌려줍니다.
import json
def get_weather(city):
# 실제로는 기상청 API를 호출합니다
weather_data = {
"서울": {"temp": 32, "humidity": 75, "condition": "맑음"},
"부산": {"temp": 29, "humidity": 82, "condition": "구름 많음"}
}
return weather_data.get(city, {"error": f"{city}의 날씨 정보를 찾을 수 없습니다"})
def search_web(query):
# 실제로는 검색 API를 호출합니다
return {"results": [f"{query}에 대한 검색 결과입니다."]}
# 도구 이름과 실제 함수를 매핑하는 레지스트리
TOOL_REGISTRY = {
"get_weather": get_weather,
"search_web": search_web
}
def execute_tool(tool_name, arguments):
if tool_name not in TOOL_REGISTRY:
return {"error": f"알 수 없는 도구: {tool_name}"}
func = TOOL_REGISTRY[tool_name]
args = json.loads(arguments) if isinstance(arguments, str) else arguments
return func(**args)
TOOL_REGISTRY 딕셔너리가 핵심입니다. LLM이 “get_weather를 호출해 줘”라고 요청하면, 이 레지스트리에서 해당 함수를 찾아 실행합니다. 이 패턴은 모든 에이전트 프레임워크가 내부에서 사용하는 동일한 메커니즘입니다. 프레임워크가 데코레이터나 클래스로 포장해 놓은 것일 뿐, 근본은 함수 이름을 키로 하는 매핑입니다.

도구 실행 결과는 반드시 직렬화 가능한 형태(딕셔너리, 문자열 등)로 반환해야 합니다. LLM은 이 결과를 텍스트로 받아 다음 사고 단계에서 활용하기 때문입니다. 이미지나 바이너리 데이터를 다뤄야 한다면, 텍스트 설명이나 URL로 변환해서 전달하는 방식을 사용합니다.
3단계 — AI 에이전트 만들기의 핵심, 에이전트 루프 완성
앞서 설명한 개념들을 하나로 엮는 단계입니다. LLM 호출, 도구 호출 판단, 도구 실행, 결과 피드백 — 이 네 가지를 루프로 묶으면 에이전트가 완성됩니다. 이것이 AI 에이전트 만들기의 가장 핵심적인 부분입니다.
def run_agent(user_input):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input}
]
max_iterations = 10 # 무한 루프 방지
for i in range(max_iterations):
response = call_llm(messages, tools=tools)
choice = response.choices[0]
message = choice.message
# LLM 응답을 대화 이력에 추가
messages.append(message.to_dict())
# 도구 호출이 없으면 최종 답변
if not message.tool_calls:
return message.content
# 도구 호출 실행
for tool_call in message.tool_calls:
result = execute_tool(
tool_call.function.name,
tool_call.function.arguments
)
# 도구 결과를 대화 이력에 추가
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "작업이 최대 반복 횟수에 도달했습니다."
이 코드가 에이전트의 전부입니다. 놀랍도록 간단하지만, 이 안에 에이전트의 핵심 메커니즘이 모두 들어 있습니다. 한 줄씩 살펴보겠습니다.
먼저, messages 리스트가 에이전트의 기억입니다. 시스템 프롬프트와 사용자 입력으로 시작해서, 루프를 돌 때마다 LLM의 응답과 도구 실행 결과가 누적됩니다. LLM은 이 전체 기록을 매번 참고해서 다음 행동을 결정합니다.
max_iterations는 안전장치입니다. LLM이 도구를 반복 호출하며 루프에 빠지는 것을 방지합니다. 실전에서는 작업의 복잡도에 맞게 이 값을 조정합니다. 단순한 질문 응답 에이전트라면 5회면 충분하고, 복잡한 리서치 에이전트라면 20회 이상이 필요할 수 있습니다.
루프의 핵심 분기점은 message.tool_calls의 존재 여부입니다. LLM이 도구를 호출하겠다고 판단하면 tool_calls 배열이 채워져 오고, 최종 답변을 내놓을 준비가 되면 일반 텍스트 응답(message.content)을 반환합니다. 이 분기 하나가 “계속 일할 것인가, 멈출 것인가”를 결정합니다.
도구 실행 결과를 다시 messages에 추가하는 부분이 루프를 “닫는” 핵심입니다. 결과를 돌려주지 않으면 LLM은 도구가 실행됐는지조차 모릅니다. 결과를 tool 역할(role)로 추가해야 LLM이 이전 호출의 결과임을 인식하고, 그 정보를 바탕으로 다음 판단을 내립니다.
실제로 이 에이전트를 실행해 보면 어떤 일이 벌어지는지 구체적으로 살펴보겠습니다. 사용자가 “서울 날씨 알려주고, 비 오면 우산 가져가라고 알려줘”라고 입력하면 다음과 같은 과정이 진행됩니다.
- 1회차: LLM이 메시지를 분석합니다. 날씨 정보가 필요하다고 판단하고,
get_weather(city="서울")도구 호출을 요청합니다. - 도구 실행: 코드가 실제
get_weather함수를 실행하고, 결과({"temp": 32, "condition": "맑음"})를 대화 이력에 추가합니다. - 2회차: LLM이 날씨 결과를 확인합니다. 맑음이므로 비가 오지 않습니다. 알림 설정은 불필요하다고 판단하고, “서울은 현재 32도, 맑은 날씨입니다. 비 소식이 없으니 우산은 필요 없습니다.”라는 최종 답변을 반환합니다.
LLM이 상황을 판단해서 불필요한 도구 호출을 건너뛴 것이 핵심입니다. 비가 왔다면 create_reminder 도구까지 호출했을 것입니다. 이런 조건부 판단이 에이전트의 진짜 가치이며, 하드코딩된 분기문이 아니라 LLM의 추론 능력에 의해 동적으로 결정됩니다.
4단계 — 대화 이력과 상태 관리
에이전트 루프가 기본적으로 동작하게 됐다면, 이제 실전에서 반드시 필요한 상태 관리 문제를 다뤄야 합니다. 단일 질문-응답이 아니라 여러 턴에 걸친 대화를 처리하려면, 대화 이력을 체계적으로 관리하는 구조가 필요합니다.
대화 이력 관리의 핵심 과제
LLM에는 컨텍스트 윈도우라는 입력 길이 제한이 있습니다. 대화가 길어지면 초기 메시지가 잘리거나, 토큰 비용이 급격히 증가합니다. 따라서 대화 이력을 무한정 쌓기만 해서는 안 됩니다. 핵심 정보는 유지하면서 불필요한 내용은 정리하는 전략이 필요합니다.
class ConversationManager:
def __init__(self, system_prompt, max_history=50):
self.system_prompt = system_prompt
self.max_history = max_history
self.messages = [
{"role": "system", "content": system_prompt}
]
def add_message(self, role, content, **kwargs):
msg = {"role": role, "content": content}
msg.update(kwargs)
self.messages.append(msg)
self._trim_if_needed()
def _trim_if_needed(self):
# 시스템 프롬프트는 항상 유지
if len(self.messages) > self.max_history:
system = self.messages[0]
# 오래된 메시지 중 도구 호출 관련은 쌍으로 제거
recent = self.messages[-(self.max_history - 1):]
self.messages = [system] + recent
def get_messages(self):
return self.messages.copy()
ConversationManager는 단순하지만 중요한 역할을 합니다. 대화 이력의 최대 크기를 제한하면서도, 시스템 프롬프트는 항상 보존합니다. 시스템 프롬프트가 잘리면 에이전트의 성격과 규칙이 사라지므로, 이를 보호하는 것이 핵심입니다.
세션 기반 상태 관리
여러 사용자를 동시에 지원하는 에이전트를 만든다면, 사용자별로 독립된 대화 상태를 관리해야 합니다. 세션 개념을 도입하면 됩니다.
class AgentSession:
def __init__(self, session_id, system_prompt):
self.session_id = session_id
self.conversation = ConversationManager(system_prompt)
self.metadata = {} # 세션별 추가 상태
self.created_at = time.time()
def set_context(self, key, value):
"""세션별 컨텍스트 저장 (예: 사용자 위치, 선호 설정)"""
self.metadata[key] = value
세션에는 대화 이력 외에도 metadata를 둘 수 있습니다. 사용자의 위치 정보, 이전 작업 결과, 선호 설정 등 에이전트가 참고해야 할 상태 정보를 여기에 저장합니다. 예를 들어, 사용자가 “서울 날씨 알려줘”라고 물었다면 위치를 서울로 기억해 두고, 다음에 “내일 날씨는?”이라고 물으면 별도 확인 없이 서울 날씨를 조회할 수 있습니다.
상태 관리에서 흔히 저지르는 실수가 있습니다. 도구 호출 메시지와 결과 메시지는 반드시 쌍으로 유지해야 합니다. LLM이 도구 호출을 요청한 메시지가 있는데 그 결과가 없으면, 다음 API 호출에서 오류가 발생합니다. 이력을 정리할 때 도구 호출 관련 메시지가 분리되지 않도록 주의해야 합니다.
또 하나의 중요한 고려 사항은 직렬화입니다. 에이전트가 재시작돼도 대화를 이어가려면, 세션 상태를 파일이나 데이터베이스에 저장할 수 있어야 합니다. messages 리스트와 metadata 딕셔너리는 모두 JSON으로 직렬화할 수 있으므로, 필요할 때 쉽게 영속화할 수 있습니다.
5단계 — 오류 처리와 안전한 종료 조건
실전에서 에이전트를 운용하면 예상 밖의 상황이 반드시 발생합니다. LLM API가 일시적으로 응답하지 않거나, 도구가 실패하거나, LLM이 잘못된 형식의 도구 호출을 생성할 수 있습니다. 이런 상황에서 에이전트가 조용히 멈추거나 무한 루프에 빠지지 않도록, 체계적인 오류 처리가 필요합니다.
세 가지 핵심 오류 유형
에이전트에서 발생하는 오류는 크게 세 가지 유형으로 나뉩니다.
- LLM 호출 오류: 네트워크 장애, 요금 한도 초과, 모델 서비스 장애 등. 일시적인 경우가 많으므로 재시도 로직으로 대응합니다.
- 도구 실행 오류: 외부 API 응답 실패, 잘못된 파라미터, 타임아웃 등. 오류 메시지를 LLM에 돌려주면 LLM이 대안을 스스로 찾을 수 있습니다.
- 구조 오류: LLM이 존재하지 않는 도구를 호출하거나, 파라미터 형식이 스키마와 맞지 않는 경우. 오류 정보를 LLM에 피드백해서 재시도를 유도합니다.
import time
def safe_call_llm(messages, tools=None, max_retries=3):
for attempt in range(max_retries):
try:
return call_llm(messages, tools)
except Exception as e:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt # 1초, 2초, 4초 지수 백오프
time.sleep(wait)
def safe_execute_tool(tool_name, arguments):
try:
result = execute_tool(tool_name, arguments)
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "message": str(e)}
safe_call_llm은 지수 백오프(exponential backoff) 방식으로 재시도합니다. 첫 번째 실패 후 1초, 두 번째 후 2초, 세 번째 후 4초를 기다립니다. 대부분의 일시적 장애는 이 패턴으로 복구됩니다.
safe_execute_tool은 도구 오류를 포착해서 구조화된 형태로 반환합니다. 중요한 것은, 도구 오류가 발생해도 에이전트 루프 자체가 멈추지 않는다는 점입니다. 오류 메시지를 LLM에 돌려주면, LLM은 “그 도구가 실패했으니 다른 방법을 시도하겠습니다”와 같이 자율적으로 대처할 수 있습니다. 이것이 에이전트의 강점입니다. 전통적인 프로그래밍에서는 개발자가 모든 오류 분기를 미리 코딩해야 하지만, 에이전트에서는 LLM이 상황에 맞는 대응을 즉석에서 결정합니다.
안전한 종료 조건 설계
에이전트가 언제 멈춰야 하는지 명확하게 정의하는 것도 중요합니다. 종료 조건이 없는 에이전트는 토큰과 비용을 무한히 소비할 수 있습니다.
- 정상 종료: LLM이 도구 호출 없이 텍스트 응답을 반환하면, 작업이 완료된 것으로 판단합니다.
- 반복 횟수 초과:
max_iterations에 도달하면 강제로 멈추고, 현재까지의 진행 상황을 사용자에게 보고합니다. - 연속 오류 한도: 같은 도구가 연속 3회 이상 실패하면, 해당 도구를 사용 불가로 표시하고 LLM에게 알립니다.
- 비용 한도: 누적 토큰 사용량이 임계값을 넘으면 경고 후 종료합니다.
def run_agent_safe(user_input, max_iterations=10, max_consecutive_errors=3):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input}
]
consecutive_errors = 0
total_tokens = 0
for i in range(max_iterations):
response = safe_call_llm(messages, tools=tools)
choice = response.choices[0]
message = choice.message
total_tokens += response.usage.total_tokens
messages.append(message.to_dict())
if not message.tool_calls:
return {"answer": message.content, "tokens_used": total_tokens, "steps": i + 1}
for tool_call in message.tool_calls:
result = safe_execute_tool(tool_call.function.name, tool_call.function.arguments)
if result["status"] == "error":
consecutive_errors += 1
else:
consecutive_errors = 0
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
if consecutive_errors >= max_consecutive_errors:
messages.append({"role": "user", "content": "도구 호출이 반복 실패하고 있습니다. 현재까지의 정보로 최선의 답변을 제공해 주세요."})
return {"answer": "최대 반복 횟수에 도달했습니다.", "tokens_used": total_tokens, "steps": max_iterations}
이 버전의 에이전트 루프는 세 가지 안전장치를 모두 갖추고 있습니다. 반복 횟수 제한, 연속 오류 감지, 그리고 토큰 사용량 추적입니다. 연속 오류가 임계값에 도달하면, 에이전트를 강제로 멈추는 대신 LLM에게 “현재까지의 정보로 답변하라”고 안내합니다. 이렇게 하면 에이전트가 완벽한 답은 못 주더라도, 부분적인 결과라도 사용자에게 전달할 수 있습니다.
전체 흐름을 엮은 완성형 에이전트 코드
지금까지 다룬 5단계를 하나로 합치면, 독립적으로 작동하는 완전한 에이전트가 됩니다. 전체 구조를 한눈에 파악할 수 있도록 클래스로 정리하겠습니다.
class SimpleAgent:
def __init__(self, system_prompt, tools_schema, tool_registry):
self.conversation = ConversationManager(system_prompt)
self.tools_schema = tools_schema
self.tool_registry = tool_registry
def chat(self, user_input, max_iterations=10):
self.conversation.add_message("user", user_input)
for i in range(max_iterations):
response = safe_call_llm(
self.conversation.get_messages(),
tools=self.tools_schema
)
message = response.choices[0].message
self.conversation.add_message(
"assistant", message.content,
tool_calls=message.tool_calls
)
if not message.tool_calls:
return message.content
for tc in message.tool_calls:
result = safe_execute_tool(tc.function.name, tc.function.arguments)
self.conversation.add_message(
"tool", json.dumps(result, ensure_ascii=False),
tool_call_id=tc.id
)
return "작업이 최대 반복 횟수에 도달했습니다."
# 사용 예시
agent = SimpleAgent(
system_prompt=SYSTEM_PROMPT,
tools_schema=tools,
tool_registry=TOOL_REGISTRY
)
print(agent.chat("서울 날씨 어때?"))
print(agent.chat("그럼 부산은?")) # 대화 맥락 유지
100줄이 채 안 되는 코드로 작동하는 AI 에이전트가 완성됐습니다. 이 에이전트는 사용자와 대화하면서 필요할 때 도구를 자율적으로 호출하고, 결과를 종합해서 답변합니다. 대화 이력을 유지하므로 맥락이 이어지고, 오류가 발생해도 안전하게 대응합니다. 프레임워크가 해 주는 일의 본질이 바로 이것입니다.

물론 프로덕션 환경에서는 비동기 처리, 스트리밍 응답, 인증, 로깅, 모니터링 등 추가 인프라가 필요합니다. 하지만 에이전트의 지적 능력, 즉 판단하고 행동하고 결과를 반영하는 핵심 루프는 위 코드가 전부입니다. 나머지는 이 핵심을 감싸는 운영 레이어입니다.
직접 구축과 프레임워크, 상황별 선택 기준
에이전트의 내부를 직접 만들어 봤으니, 이제 현실적인 질문이 남습니다. 실제 프로젝트에서는 직접 구축과 프레임워크 사용 중 어떤 것을 선택해야 할까요? 상황에 따라 정답이 다릅니다.
직접 구축이 적합한 경우
- 요구사항이 명확하고 단순할 때: 도구 2-3개와 단일 루프로 충분한 에이전트라면, 프레임워크의 학습 비용이 더 클 수 있습니다.
- 성능 최적화가 중요할 때: 프레임워크의 추상화 레이어는 오버헤드를 추가합니다. 응답 지연이 민감한 서비스에서는 불필요한 레이어를 줄이는 것이 유리합니다.
- 프레임워크 종속을 피하고 싶을 때: 에이전트 프레임워크 생태계는 빠르게 변화합니다. 특정 프레임워크에 깊게 결합하면 마이그레이션이 어려워질 수 있습니다.
- 학습 목적일 때: 에이전트의 원리를 깊이 이해하고 싶다면, 직접 구현해 보는 것이 가장 효과적인 방법입니다.
프레임워크가 적합한 경우
- 복잡한 다중 에이전트 시스템: 여러 에이전트가 협업하거나, 복잡한 분기와 병합이 필요한 워크플로우에서는 LangGraph나 CrewAI 같은 프레임워크가 개발 생산성을 크게 높여 줍니다.
- 옵저버빌리티와 디버깅: 대부분의 프레임워크는 에이전트의 사고 과정을 추적하고 시각화하는 도구를 내장하고 있습니다. 직접 구현하면 이런 도구도 만들어야 합니다.
- RAG, 메모리, 가드레일 통합: 벡터 검색, 장기 기억, 안전 필터 같은 고급 기능이 필요하면, 프레임워크의 기성 컴포넌트를 활용하는 것이 효율적입니다.
- 팀 규모가 크고 표준이 필요할 때: 여러 개발자가 협업한다면, 프레임워크가 제공하는 구조와 컨벤션이 코드 일관성을 유지하는 데 도움이 됩니다.

가장 이상적인 접근법은 양쪽을 모두 경험하는 것입니다. 먼저 직접 구현으로 원리를 이해하고, 이후 프로젝트 규모와 복잡도에 따라 프레임워크를 선택적으로 도입합니다. 원리를 아는 상태에서 프레임워크를 쓰면, 프레임워크가 뭘 해 주고 뭘 안 해 주는지 정확히 알 수 있어서 디버깅도 훨씬 빠릅니다.
마무리 — 에이전트의 원리를 아는 개발자의 힘
이 글에서 다룬 5단계를 정리하면 이렇습니다. LLM 연결로 시작해서, 도구 정의로 에이전트의 손발을 만들고, 에이전트 루프로 자율적 판단 능력을 부여하고, 대화 이력으로 기억을 추가하고, 오류 처리로 안정성을 확보했습니다. 각 단계는 독립적이면서도 유기적으로 연결되어, 하나의 완성된 에이전트 시스템을 이룹니다.
AI 에이전트 만들기의 핵심은 결국 하나의 루프로 귀결됩니다. LLM에게 상황을 보여 주고, 판단을 받고, 행동을 실행하고, 결과를 다시 LLM에게 돌려주는 순환. 이 단순한 패턴이 아무리 복잡한 에이전트 시스템에서도 변하지 않는 근본 원리입니다.
직접 만들어 본 경험은 앞으로 어떤 에이전트 프레임워크를 접하더라도, 어떤 새로운 패턴이 등장하더라도 흔들리지 않는 기반이 됩니다. 프레임워크의 화려한 API 뒤에서 실제로 무슨 일이 벌어지는지 아는 개발자는, 문제가 발생했을 때 프레임워크의 소스 코드를 열어볼 수 있고, 필요하면 프레임워크를 넘어서는 해법을 직접 만들 수 있습니다.
이 글의 코드를 기반으로 실제 API를 연결하고, 자신만의 도구를 추가해 보세요. 날씨 API, 캘린더 API, 데이터베이스 조회 등 실제 서비스를 연동하면서, 에이전트가 자율적으로 문제를 해결하는 과정을 직접 관찰하는 것이 가장 좋은 학습입니다. 한 번 만들어 본 사람은, 에이전트의 가능성과 한계를 동시에 이해하게 됩니다.
Photo by Jesús Esteban San José on Pexels
[…] AI 에이전트 만들기, 프레임워크 없이 5단계 구축법 […]
[…] AI 에이전트 만들기, 프레임워크 없이 5단계 구축법 […]