AI Agent는 무엇인가? Java 예제와 함께한 상세 설명
1. 개요
“AI Agent”는 곳곳에서 나타나는 키워드이지만 정의는 종종 모호합니다. 모든 챗봇을 agent라고 부르는 사람도 있고, 복잡한 다중 단계 시스템만 진정한 agent라고 보는 사람도 있습니다. 이 글에서는 AI Agent가 무엇인지, 반드시 있어야 하는 구성 요소가 무엇인지, 그리고 그 뒤에 있는 루프 메커니즘을 설명합니다. 이 글을 읽은 후, 당신은 순수 Java로 최소한의 agent를 직접 만들어 “내부 구조”가 어떻게 작동하는지 이해할 수 있을 것이고, 나중에 Spring AI를 사용한 실제 LLM 버전으로 업그레이드할 수 있습니다.
2. AI Agent는 무엇인가?
간단한 정의: AI Agent는 LLM을 두뇌로 사용하여 목표를 달성하기 위해 자동으로 행동을 결정하는 시스템으로, 단순히 질문에 답하고 멈추는 것이 아닙니다.
일반 챗봇과의 차이:
| 특징 | 일반 챗봇 | AI Agent |
|---|---|---|
| 처리 횟수 | 1회: 질문 → 답변 | 여러 단계: 관찰 → 추론 → 행동 → 반복 |
| Tool/API 호출 여부 | 보통 없음 | 있음, 자동으로 어떤 tool을 호출할지 결정 |
| 장기 목표 여부 | 없음 | 있음, agent가 자동으로 작업을 분해하여 목표 달성 |
| 예시 | “서울 날씨는?” → 텍스트 설명 반환 | “내일 서울에서 부산 가는 가장 저렴한 항공권 예약” → 자동으로 항공권 찾기, 가격 비교, 예약, 확인 |
다르게 말하면, LLM은 “언어 뇌”일 뿐 텍스트 생성을 알고 있습니다. Agent는 그 뇌를 행동할 수 있는 존재로 변환합니다(API 호출, 파일 읽기, 데이터베이스 쿼리) 그리고 반복해서 추론 과정을 실행합니다 작업이 완료될 때까지.
3. AI Agent의 핵심 구성 요소
최소한의 agent는 4가지 구성 요소가 필요합니다. 이 중 하나라도 빠지면 시스템은 그저 “agent 옷을 입은” 챗봇에 불과합니다.
3.1. LLM – 추론 뇌
LLM은 프롬프트(목표, 이력, 사용 가능한 tool 목록 포함)를 받고 다음 결정을 생성합니다: 직접 답할지 또는 어떤 tool을 호출할지, 어떤 매개변수로.
3.2. Tool (function calling) – agent의 팔다리
Tool은 agent가 호출할 수 있는 구체적인 함수입니다: 주문 조회, 날씨 API 호출, 데이터베이스 쿼리. 이것이 agent를 “말하는 것”에서 “행동하는 것”으로 변환하는 부분입니다.
public interface Tool {
String name();
boolean canHandle(String input);
String execute(String input);
}
canHandle은 이 tool이 현재 입력을 처리할 수 있는지 확인합니다; execute는 행동을 실행하고 결과(observation)를 반환합니다.
3.3. Memory – 메모리
Agent는 이전 단계에서 한 일을 기억해야 합니다(대화 이력, 호출한 tool의 결과) 불필요한 반복을 피하기 위해. Memory는 단순히 이전 단계의 로그를 저장하는 List<String>일 수도 있고, 관련 정보를 찾기 위한 vector store일 수도 있습니다.
3.4. Control loop – 제어 루프
이것은 전체 과정을 실행하는 “엔진” 부분입니다: Thought → Action → Observation 단계를 반복하여 목표에 도달하거나 허용된 단계 수를 초과할 때까지. 다음 섹션에서 이 메커니즘을 자세히 설명할 것입니다.
4. ReAct 루프: agent가 “생각하고 행동하는” 방식
ReAct (Reasoning + Acting)는 agent를 위한 가장 일반적인 루프 모델입니다:
- Thought: LLM이 추론합니다 “다음에 뭘 해야 할까?”
- Action: tool을 선택하고 적절한 매개변수로 호출합니다.
- Observation: tool에서 결과를 받고 context에 다시 넣습니다.
- LLM이 충분한 정보를 얻었다고 결정하여 최종 답변을 할 때까지 1단계부터 반복하거나
maxSteps에 도달합니다.
이 메커니즘을 실제 LLM을 호출하지 않고 이해하기 위해(API 비용 절약), Java 순수 코드로 작은 agent를 직접 만듭니다. 실제 LLM 대신 if-else를 사용합니다.
public class SimpleAgent {
private final List<Tool> tools;
private final int maxSteps;
public SimpleAgent(List<Tool> tools, int maxSteps) {
this.tools = tools;
this.maxSteps = maxSteps;
}
public String run(String goal) {
String observation = goal;
for (int step = 1; step <= maxSteps; step++) {
Tool chosen = tools.stream()
.filter(t -> t.canHandle(observation))
.findFirst()
.orElse(null);
if (chosen == null) {
return "처리할 수 있는 tool이 없습니다: " + observation;
}
System.out.printf("스텝 %d - Thought: '%s' tool을 사용해야 함%n", step, chosen.name());
String result = chosen.execute(observation);
System.out.printf("스텝 %d - Observation: %s%n", step, result);
return result; // 간단한 예제: 1 단계 후 완료
}
return "허용된 단계 수(" + maxSteps + ")를 초과했지만 아직 작업을 완료하지 못했습니다.";
}
}
maxSteps는 루프의 “예산”입니다 — agent가 절대 목표에 도달하지 못해 무한 실행되지 않도록 필수입니다. for 루프는 Thought(tool 선택), Action(execute), Observation(결과를 콘솔에 출력)을 모의합니다.
세 가지 예제 tool:
public class WeatherTool implements Tool {
@Override
public String name() {
return "WeatherTool";
}
@Override
public boolean canHandle(String input) {
return input.toLowerCase().contains("날씨");
}
@Override
public String execute(String input) {
return "서울 오늘 28°C, 구름 많음.";
}
}
public class OrderStatusTool implements Tool {
@Override
public String name() {
return "OrderStatusTool";
}
@Override
public boolean canHandle(String input) {
return input.toLowerCase().contains("주문");
}
@Override
public String execute(String input) {
return "주문 #1023 배송 중입니다. 예상 배송일: 2일 후.";
}
}
public class CalculatorTool implements Tool {
@Override
public String name() {
return "CalculatorTool";
}
@Override
public boolean canHandle(String input) {
return input.matches(".*\\d+\\s*\\+\\s*\\d+.*");
}
@Override
public String execute(String input) {
String digits = input.replaceAll("[^0-9+]", "");
String[] parts = digits.split("\\+");
int sum = Integer.parseInt(parts[0]) + Integer.parseInt(parts[1]);
return "결과: " + sum;
}
}
시험해보기:
SimpleAgent agent = new SimpleAgent(
List.of(new CalculatorTool(), new WeatherTool(), new OrderStatusTool()),
3
);
String result = agent.run("서울 오늘 날씨를 알려주세요");
System.out.println(result);
결과:
스텝 1 - Thought: 'WeatherTool' tool을 사용해야 함
스텝 1 - Observation: 서울 오늘 28°C, 구름 많음.
서울 오늘 28°C, 구름 많음.
올바른 tool을 선택하고 적절한 tool이 없는 경우를 처리하는지 확인하기 위해 테스트를 작성합니다:
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SimpleAgentTest {
@Test
void shouldPickWeatherToolWhenGoalMentionsWeather() {
SimpleAgent agent = new SimpleAgent(
List.of(new CalculatorTool(), new WeatherTool(), new OrderStatusTool()),
3
);
String result = agent.run("서울 오늘 날씨를 알려주세요");
assertThat(result).contains("서울");
}
@Test
void shouldReturnErrorWhenNoToolMatches() {
SimpleAgent agent = new SimpleAgent(List.of(new CalculatorTool()), 3);
String result = agent.run("나한테 노래 불러줘");
assertThat(result).startsWith("처리할 수 있는 tool이 없습니다");
}
}
이 예제는 실제 LLM 대신 if-else를 사용하므로 “추론”은 매우 경직되어 있습니다. 실제 agent는 다릅니다: 경직된 canHandle 대신 LLM이 직접 tool 설명을 읽고 어떤 tool을 호출할지 결정하고, 매개변수는 LLM이 자연어 질문에서 자동으로 추출합니다.
5. Spring AI와 tool calling으로 실제 agent 구축
실제 LLM의 결정 과정을 보기 위해 Spring AI를 사용합니다 — Spring Boot에 AI 모델을 통합하는 프레임워크로 tool calling을 기본 지원합니다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
API 키와 모델 구성:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
@Tool annotation으로 tool을 선언합니다. 이것은 이전 예제의 OrderStatusTool의 역할과 같지만 이번에는 실제 LLM이 호출할 것입니다:
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
@Component
public class OrderTools {
@Tool(description = "주문 ID로 주문 상태 조회")
public String getOrderStatus(String orderId) {
// 실제로는 여기서 OrderService / 데이터베이스를 호출합니다
return "주문 " + orderId + " 배송 중입니다. 예상 배송일: 2일 후.";
}
}
description은 극도로 중요합니다: LLM은 이 설명을 기반으로 이 tool을 호출할지 말지 결정하고 어떤 매개변수를 전달할지를 결정합니다.
ChatClient를 사용하여 agent에 tool을 연결하는 service:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class OrderAssistantService {
private final ChatClient chatClient;
public OrderAssistantService(ChatClient.Builder builder, OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
public String ask(String question) {
return chatClient.prompt(question).call().content();
}
}
ask("내 주문 ORD-1023은 어디에 있나요?")를 호출하면, 내부적으로 Spring AI는 다음을 수행합니다:
- tool
getOrderStatus의 설명과 함께 질문을 모델에 전송합니다. - 모델은 직접 답변하는 대신 “
getOrderStatus를orderId="ORD-1023"으로 호출하세요”라는 결정을 반환합니다. - Spring AI는 자동으로 해당 Java 메서드를 호출하고 결과를 얻습니다.
- 그 결과를 모델에 다시 전송하여 모델이 최종 자연어 답변을 작성하도록 합니다.
당신의 주문 ORD-1023은 현재 배송 중이며 앞으로 2일 내에 배달될 예정입니다.
위의 4 단계는 완전한 ReAct 루프입니다. 유일한 차이는 Spring AI가 for 루프를 숨겼다는 것입니다. 섹션 4의 순수 Java 예제처럼 control loop을 직접 작성할 필요가 없습니다.
참고:
spring-ai-starter-model-openai버전 및 artifact 이름은 시간이 지남에 따라 변경될 수 있습니다. 사용 중인 Spring Boot 버전과 일치하는 BOM을 항상 확인한 후 dependency를 추가하세요.
6. 일반적인 Agent 유형
- Single-agent ReAct: 단일 agent가 Thought-Action-Observation을 반복합니다. 중간 정도의 작업에 적합합니다. 위의 두 예제처럼.
- Plan-and-Execute: agent가 먼저 상세한 계획(단계 목록)을 세운 다음 각 단계를 실행하고 ReAct처럼 계속 추론하지 않습니다. 구조화된 다단계 작업에 적합합니다 (예: “5개 부분으로 보고서 작성”).
- Multi-agent: 여러 전문 agent가 협력합니다. 보통 여러 “Worker” agent를 조율하는 “Supervisor” agent가 있습니다 (예: 검색 agent, 작문 agent, 검증 agent). 결과는 다시 합칩니다.
어떤 유형을 선택할지는 작업의 복잡도에 따라 결정됩니다: 간단한 작업은 single-agent를 사용하고, 여러 영역(검색 + 작문 + 검증)을 포함하는 작업은 각 부분을 쉽게 제어하고 디버깅하기 위해 multi-agent로 분리해야 합니다.
7. 흔한 오류 & agent를 사용하지 말아야 할 때
- 무한 루프, token 소모:
maxSteps또는 token 예산을 설정하지 않으면 agent가 계속 반복할 수 있습니다. LLM이 “아직 완료되지 않았다”고 생각하기 때문입니다. 항상 섹션 4의SimpleAgent처럼 단계 수의 명확한 제한을 설정하세요. - 모호한 tool 설명: tool의
description이 명확하지 않으면 LLM은 잘못된 tool을 호출하거나 잘못된 매개변수 유형을 전달할 가능성이 높습니다. 구체적인 설명을 작성하고 예상되는 매개변수의 형식을 명확히 하세요. - 위험한 작업에 대한 guardrail 부재: agent가 데이터 삭제, 돈 전송 등의 tool을 호출할 수 있는데 확인 단계가 없다면 큰 위험입니다. 항상 실행 취소할 수 없는 작업을 실행하기 전에 검증 레이어(human-in-the-loop 또는 규칙 기반 확인)를 추가하세요.
- 간단한 작업에 과도한 복잡성: 질문에 LLM을 한 번 호출하여 답변을 얻으면 되는 경우(예: 문장 번역), agent 루프와 tool calling을 구축하지 마세요. 추가 지연 시간만 발생하고 불필요한 모델 호출로 비용도 증가합니다.
참고: 시스템이 외부 작업을 호출할 필요가 없고 텍스트를 한 번 생성하기만 하면 되는 경우, 그것은 일반 LLM 작업입니다 — agent가 아닙니다. 불필요한 복잡성을 추가하지 마세요.
8. 결론
AI Agent는 일반 챗봇과 달리 자동으로 행동을 결정하고 목표에 도달할 때까지 추론 과정을 반복할 수 있다는 점에서 다릅니다. 이는 네 가지 구성 요소인 LLM, tool, memory 및 control loop에 기반합니다. ReAct 루프(Thought-Action-Observation)는 현재 대부분의 agent를 지원하는 일반적인 메커니즘이며, production으로 넘어가기 전에 Spring AI 같은 프레임워크를 사용하기 전에 순수 Java로 직접 모의하여 본질을 이해할 수 있습니다. 가장 중요한 것은: 항상 단계 수를 제한하고, tool 설명을 명확하게 작성하며, 실행 취소할 수 없는 작업에 guardrail을 추가하세요 — agent는 강력하지만 제어가 부족하면 “과하게” 할 수 있습니다.