처음부터 AI 에이전트 구축하기: 80줄의 코드 리뷰 에이전트
요약
복잡한 프레임워크 없이 단 80줄의 코드로 작동하는 AI 에이전트의 핵심 원리를 설명합니다. LLM과 외부 도구 간의 통신 루프를 직접 구현하며 에이전트 아키텍처의 본질을 파악합니다.
핵심 포인트
- 에이전트는 LLM과 도구 간의 통신을 조율하는 상태 기반 루프임
- 프레임워크의 기능은 편의 기능일 뿐, 핵심은 ReAct 패턴의 구현
- 도구 정의, 프롬프트 엔지니어링, 오류 처리가 에이전트의 핵심 요소
- 코드 리뷰 에이전트 구현을 통해 실질적인 오케스트레이션 과정 학습
서론
AI 개발 커뮤니티에는 지능형 에이전트를 구축하려면 복잡한 프레임워크, 전문 지식, 그리고 수천 줄의 코드가 필요하다는 만연한 미신이 있습니다. LangChain, CrewAI, Mastra와 같은 프레임워크들은 에이전트 주변에 정교함이라는 아우라를 형성하여, 마치 에이전트가 마법 같은 존재인 것처럼 보이게 만들었습니다.
현실은 훨씬 더 단순합니다. 본질적으로 AI 에이전트는 언어 모델 (Language Model)과 외부 도구 (External Tools) 사이의 통신을 조율하는 루프 (Loop)에 불과합니다. 프레임워크가 제공하는 복잡성—대화 메모리 (Conversation Memory), 재시도 로직 (Retry Logic), 폴백 메커니즘 (Fallback Mechanisms)—은 편의 기능일 뿐 필수 사항은 아닙니다.
이 글은 기능적인 코드 리뷰 에이전트를 처음부터 직접 구축함으로써 그 신비감을 제거합니다. 전체 오케스트레이션 (Orchestration) 루프는 대략 80줄의 코드로 구성됩니다. 여러분은 내부에서 실제로 어떤 일이 일어나는지, 그리고 더 중요한 것은 언제 프레임워크를 사용할 가치가 있고 언제 그것이 불필요한 오버헤드 (Overhead)인지 배우게 될 것입니다.
섹션 1: 에이전트 루프 설명
AI 에이전트의 신비감을 없애주는 근본적인 통찰은 이것입니다: 에이전트는 상태 (State)를 가진 루프일 뿐이라는 점입니다. 모델은 특별한 방식으로 "생각"하거나 "추론"하지 않습니다. 모델은 프롬프트 (Prompt)를 기반으로 토큰 (Tokens)을 생성하며, 루프는 다음에 어떤 일이 일어날지를 조정합니다.
아키텍처는 다음과 같은 간단한 패턴을 따릅니다:
- 사용자의 프롬프트와 사용 가능한 도구를 LLM에 전송합니다.
- LLM은 텍스트 또는 도구 호출 (Tool Call) 요청으로 응답합니다.
- 도구 호출인 경우, 요청된 함수를 로컬에서 실행합니다.
- 도구의 결과를 모델에 다시 보냅니다.
- 모델이 최종 답변을 제공하거나 제한에 도달할 때까지 반복합니다.
이 패턴은 종종 ReAct (Reason + Act) 사이클이라고 불리지만, 특정 용어와 상관없이 그 원리는 동일하게 적용됩니다. LLM은 어떤 도구를 사용할지, 그리고 언제 끝낼지를 결정하며—그 외의 모든 것은 오케스트레이션입니다.
루프 구현
다음은 의사 코드 (Pseudocode)로 작성된 핵심 루프 개념입니다:
async function runAgent(userMessage, maxIterations = 10) {
let messages = [{ role: "user", content: userMessage }];
for (let i = 0; i < maxIterations; i++) {
const response = await llm.chat(messages, { tools: availableTools });
...
}
루프 자체는 사소합니다. 진짜 핵심적인 작업은 세 가지 영역에서 일어납니다:
도구 정의 (Tool definitions): 모델이 호출할 수 있는 함수를 노출함
프롬프트 엔지니어링 (Prompt engineering): 모델의 행동과 의사결정을 유도함
오류 처리 (Error handling): 실패, 재시도 및 예외 케이스를 관리함
섹션 2: 코드 리뷰 에이전트 구축하기
구체적인 구현 사례를 살펴보겠습니다. Git diff를 분석하고 15년 경력의 시니어 엔지니어 페르소나(persona)로 피드백을 제공하는 "Steve"라는 이름의 코드 리뷰 에이전트입니다.
도구 (The Tools)
에이전트가 코드 리뷰를 수행하려면 세 가지 기본적인 능력이 필요합니다:
getDiff: 현재 Git diff를 가져옴
getFile: 특정 파일의 내용을 읽음
listFiles: 저장소 구조를 탐색함
이 도구들은 LLM이 이해할 수 있는 스키마(schema)를 사용하여 정의되며, 일반적으로 함수의 이름, 설명 및 파라미터(parameters)를 기술하는 JSON 스키마 형식을 사용합니다.
const tools = [
{
name: "getDiff",
description: "Get the git diff of the current repository",
parameters: {
type: "OBJECT",
properties: {},
required: []
}
},
{
name: "getFile",
description: "Read a file from the repository",
parameters: {
type: "OBJECT",
properties: {
path: { type: "STRING", description: "Path relative to repo root" }
},
required: ["path"]
}
},
{
name: "listFiles",
description: "List files and directories at a given path",
parameters: {
type: "OBJECT",
properties: {
path: { type: "STRING", description: "Directory path" }
},
required: ["path"]
}
}
];
시스템 프롬프트 (The System Prompt)
시스템 프롬프트는 에이전트의 페르소나와 행동을 정의합니다. 코드 리뷰 에이전트의 경우 다음과 같습니다:
당신은 15년 경력의 시니어 소프트웨어 엔지니어 Steve입니다. 코드를 철저하게 리뷰하세요. 직설적으로 말하되, 적절할 때는 냉소적으로 대하십시오. 결함, 보안 문제 및 유지보수성 문제를 지적하는 것을 주저하지 마세요.
프롬프트 (Prompt)는 모델이 자신의 역할을 어떻게 해석하고 출력의 품질을 어떻게 결정하는지를 형성하기 때문에 매우 중요합니다. 잘 설계된 시스템 프롬프트 (System Prompt)는 코드를 전혀 변경하지 않고도 에이전트의 성능을 극적으로 향상시킬 수 있습니다.
대화 흐름 (The Conversation Flow)
에이전트의 작동은 다음과 같은 간단한 순서를 따릅니다:
1단계: 사용자가 “현재 git diff를 리뷰해 주세요.”와 같은 메시지를 보냅니다.
2단계: 모델은 메시지를 도구 정의 (Tool Definitions)와 함께 받습니다. 모델은 세 가지 방식으로 응답할 수 있습니다: 일반 텍스트 (최종 답변), 도구 호출 (Tool Call) 요청, 또는 텍스트와 도구 호출 모두.
3단계: 모델이 도구 호출을 요청하면, 애플리케이션은 이를 로컬에서 실행합니다. 예를 들어, git diff를 실행하고 그 출력을 캡처할 수 있습니다.
4단계: 도구 결과는 대화의 또 다른 메시지로서 모델에게 다시 전송됩니다.
5단계: 모델이 최종 답변을 제공하거나 반복 제한 (Iteration Limit)에 도달할 때까지 루프가 계속됩니다.
결정적으로, 전체 대화 기록 (Conversation History)이 매 반복마다 모델에게 다시 전송됩니다. 이를 통해 모델은 문맥 (Context)을 유지하고 다음에 어떤 도구를 호출할지에 대해 정보에 입각한 결정을 내릴 수 있습니다.
반복 제한 (Iteration Limits)
무한 반복과 통제 불능의 토큰 비용 발생을 방지하기 위해, 루프는 while 루프 대신 for 루프를 사용합니다. 일반적으로 10회의 반복 제한은 통제되지 않는 지출을 방지하면서도 복잡한 리뷰를 수행하기에 충분한 용량을 제공합니다.
섹션 3: 프로덕션 고려 사항 (Production Considerations)
80줄짜리 에이전트는 기능적으로 작동하지만, 프로덕션 환경에 적합한 (Production-ready) 에이전트가 되려면 추가적인 고려 사항이 필요합니다.
재시도 로직 및 에러 핸들링 (Retry Logic and Error Handling)
데모 구현에서는 과부하된 Gemini 모델로부터 503 에러가 발생하여 재시도 메커니즘이 필요했습니다. 프로덕션 시스템은 다음과 같은 사항을 구현해야 합니다:
지수 백오프 (Exponential backoff): 재시도 사이의 대기 시간을 점진적으로 늘림
지터 (Jitter): 천둥떼 문제 (thundering herd problems)를 방지하기 위한 무작위 변동성
폴백 모델 (Fallback models): 기본 모델이 실패할 때 대체 모델로 자동 전환
타임아웃 처리 (Timeout handling): 각 요청에 대한 최대 대기 시간
컨텍스트 윈도우 관리 (Context Window Management)
대화가 길어짐에 따라 컨텍스트 윈도우가 가득 차게 됩니다. 프로덕션 에이전트는 다음과 같은 전략을 구현합니다:
요약 (Summarization): 이전 상호작용을 압축
선택적 유지 (Selective retention): 관련 있는 컨텍스트만 유지
슬라이딩 윈도우 (Sliding windows): 가장 최근의 N개 교환 내용만 유지
도구 실행 안전성 (Tool Execution Safety)
도구는 코드를 실행하고 시스템과 상호작용합니다. 프로덕션 구현에는 다음 사항이 필요합니다:
샌드박싱 (Sandboxing): 격리된 실행 환경
권한 제어 (Permission controls): 도구가 접근할 수 있는 범위 제한
감사 추적 (Audit trails): 모든 도구 호출에 대한 로깅
롤백 기능 (Rollback capabilities): 필요 시 변경 사항 취소
관측 가능성 (Observability)
프로덕션 환경에서 에이전트의 동작을 이해하려면 다음이 필요합니다:
구조화된 로깅 (Structured logging): 결정 및 작업 추적
성능 지표 (Performance metrics): 지연 시간 (latency), 토큰 사용량, 성공률
트레이스 (Traces): 에이전트 실행의 엔드 투 엔드 (end-to-end) 가시성
회귀 테스트 (Regression testing): 동작 저하 포착
모범 사례 (Best Practices)
프레임워크 없이 시작하기
첫 번째 에이전트를 처음부터 직접 구축해 보세요. 이를 통해 메커니즘, 예외 케이스 (edge cases), 그리고 한계점을 이해할 수 있습니다. 이러한 기초적인 이해가 이루어진 후에 프레임워크 도입을 고려해야 합니다.
루프를 단순하게 유지하기
오케스트레이션 루프 (orchestration loop)는 직관적이고 가시적이어야 합니다. 복잡성은 오케스트레이션 계층이 아닌 도구 구현 단계에 존재해야 합니다.
도구를 신중하게 설계하기
도구는 에이전트가 세상과 상호작용하는 인터페이스입니다. 각 도구는 다음과 같아야 합니다:
명확하고 단일한 책임 (single responsibility)을 가질 것
철저한 문서를 포함할 것
구조화되고 예측 가능한 결과를 반환할 것
에러를 우아하게 처리할 것
반복 제한 구현하기
항상 최대 반복 횟수를 강제하세요. 이는 무한 루프와 비용 초과를 방지합니다.
가능한 경우 구조화된 출력 (Structured Output) 사용하기
모델이 구조화된 데이터(예: JSON)를 반환할 수 있을 때, 파싱(Parsing)이 더 신뢰할 수 있고 에러 처리도 더 쉬워집니다.
흔한 실수들
루프를 지나치게 복잡하게 만들기
많은 개발자가 루프 자체에 성급하게 복잡성을 추가합니다. 루프는 비즈니스 로직을 담는 컨테이너가 아니라, 얇은 조정자(Coordinator) 역할을 해야 합니다.
부실한 도구 (Tool) 설계
모호하거나, 문서화가 일관되지 않거나, 복잡한 파라미터(Parameter)를 요구하는 도구는 모델을 혼란스럽게 만들고 잘못된 결정으로 이어집니다.
에러 처리 소홀
LLM API는 실패할 수 있습니다. 도구도 실패합니다. 네트워크 호출도 타임아웃이 발생합니다. 프로덕션 환경의 에이전트는 이러한 실패를 우아하게 처리해야 합니다.
불충분한 프롬프트 엔지니어링 (Prompt Engineering)
시스템 프롬프트는 에이전트의 전체 페르소나와 행동을 정의합니다. 여기에 시간을 투자하는 것은 출력 품질 측면에서 엄청난 배당을 가져다줍니다.
관찰 가능성 (Observability) 생략
로깅(Logging)과 모니터링(Monitoring) 없이는 에이전트의 행동을 디버깅하는 것이 거의 불가능해집니다. 첫날부터 기본적인 관찰 가능성을 확보하며 시작하세요.
마치며
AI 에이전트 뒤에 숨겨진 추악한 비밀은, 그것들이 마케팅에서 말하는 것보다 훨씬 더 단순하다는 것입니다. 기능적인 에이전트는 기본적인 API 호출과 루프만을 사용하여 100줄 미만의 코드로 구축할 수 있습니다. 대화의 중심을 차지하는 프레임워크들은 편의를 위한 도구일 뿐, 필수 사항은 아닙니다.
이것이 프레임워크가 쓸모없다는 뜻은 아닙니다. 프레임워크는 프로덕션급 재시도(Retry), 폴백(Fallback) 메커니즘, 대화 메모리(Conversation Memory), 그리고 처음부터 구축하려면 상당한 시간이 걸릴 통합 기능들을 제공합니다. 하지만 프레임워크가 무엇을 하는지 이해하지 못한 채 사용하는 것은 문제가 발생했을 때 좌절을 불러오는 지름길입니다.
처음부터 직접 구축하며 시작하세요. 루프를 이해하고, 예외 상황(Edge cases)을 경험해 보세요. 그러고 나서 프레임워크가 당신의 삶을 단순화해 준다면, 프레임워크가 무엇을 처리하고 당신이 무엇을 직접 관리해야 하는지 정확히 아는 상태에서 자신 있게 도입하십시오.
에이전트 생태계는 빠르게 진화하고 있지만, 기본 원칙은 변하지 않습니다. 기초를 마스터한다면, 어떤 프레임워크나 패러다임이 등장하더라도 이를 헤쳐 나갈 능력을 갖추게 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기