AI Agent의 5가지 패턴 - 자세한 설명과 Java 예제
1. 개요
LLM을 몇 줄의 코드와 연결하여 “자동으로 실행”하면, 기초적인 AI Agent를 갖게 됩니다. 하지만 이런 식의 agent는 보통 질문에만 답하고, 자체적으로 오류를 수정할 수 없으며, 도구를 호출할 수 없고, 작업을 분담할 수 없습니다. 이 글에서는 agent를 “더 똑똑하게” 만드는 5가지 디자인 패턴을 설명합니다: Reflection, Tool Use, Planning, Multi-Agent, ReAct. 각 패턴은 간단하고 실행 가능한 Java (Spring AI) 코드 예제와 함께 언제 사용해야 하고 하지 말아야 하는지에 대한 설명을 포함합니다. 이 글을 다 읽고 나면, 거대한 프롬프트에 모든 것을 집어넣는 대신 자신의 문제에 맞는 패턴을 선택할 수 있게 됩니다.
하단의 예제를 위해서는 Spring AI 의존성이 필요합니다:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
주의: 이 글의 코드는 패턴 구조를 보여주기 위한 것이며, 프로덕션 제품이 아닙니다 — 실제 배포 시에는 재시도, 타임아웃, 로깅을 추가해야 합니다.
2. Reflection 패턴: 답변을 자체적으로 비판하고 수정하기
Reflection은 가장 간단한 패턴입니다: agent가 답변을 생성하고, 같은 LLM (또는 다른 LLM)을 사용하여 그 답변을 평가/비판한 후, 다시 수정합니다. 이 루프는 초안을 작성하고 보내기 전에 다시 한 번 읽어보는 것과 같습니다.
2.1. 동작 흐름
- Generate: agent가 처음 답변을 생성합니다.
- Critique: agent (또는 다른 agent)가 “검수자” 역할을 하며 오류를 지적합니다.
- Revise: agent가 비판에 기반하여 수정하며, 품질이 충분할 때까지 2–3단계를 반복할 수 있습니다.
@Service
public class ReflectionAgent {
private final ChatClient chatClient;
public ReflectionAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String answerWithReflection(String question, int maxRounds) {
String draft = chatClient.prompt()
.user("다음 질문에 간단히 답변해주세요: " + question)
.call()
.content();
for (int round = 0; round < maxRounds; round++) {
String critique = chatClient.prompt()
.user("""
당신은 까다로운 검수자입니다. 다음 답변의 오류, 누락, 문제점을 지적해주세요.
이미 좋다면 "OK"를 반환해주세요.
질문: %s
답변: %s
""".formatted(question, draft))
.call()
.content();
if (critique.strip().equalsIgnoreCase("OK")) {
break;
}
draft = chatClient.prompt()
.user("다음 의견을 바탕으로 답변을 수정해주세요: " + critique
+ "\n이전 답변: " + draft)
.call()
.content();
}
return draft;
}
}
answerWithReflection은 처음 초안을 생성하고, maxRounds번까지 반복합니다: LLM을 검수자 역할로 호출하여 평가하고, 검수자가 “OK”를 반환하면 멈추고, 그렇지 않으면 수정 버전을 생성합니다. 결과적으로 LLM을 한 번만 호출한 것보다 더 정확한 답변을 얻는 경향이 있습니다.
주의: Reflection은 일반적인 방식보다 LLM 호출 비용을 2–3배 증가시킵니다. 높은 정확성이 필요한 작업 (코드 검수, 계산, 계약서 작성)에만 사용하고, 일반적인 채팅에는 사용하지 않는 것이 좋습니다. 왜냐하면 느리고 비용이 많이 들기 때문입니다.
3. Tool Use 패턴: agent가 외부 도구를 호출하도록 하기
LLM은 현재 시간을 모르고, 데이터베이스에 접근할 수 없으며, 큰 수를 정확히 계산할 수 없습니다. Tool Use 패턴은 agent에게 필요할 때 “함수를 호출”할 수 있도록 합니다 — LLM이 어떤 도구를 호출할지 결정하고, 코드가 그 도구를 실행한 후 LLM에 결과를 반환하여 계속 추론하도록 합니다.
@Component
public class OrderTools {
private final OrderRepository orderRepository;
public OrderTools(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Tool(description = "주문번호로 주문 상태를 조회합니다")
public String getOrderStatus(String orderId) {
return orderRepository.findById(orderId)
.map(Order::getStatus)
.orElse("주문번호 " + orderId + "를 찾을 수 없습니다");
}
@Tool(description = "10% 세금을 포함한 주문의 총액을 계산합니다")
public double calculateTotal(double subtotal) {
return subtotal * 1.1;
}
}
@Service
public class CustomerSupportAgent {
private final ChatClient chatClient;
public CustomerSupportAgent(ChatClient.Builder builder, OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
public String handle(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
@Tool은 LLM이 호출할 수 있는 함수로 메서드를 표시하고, description은 모델이 언제 사용해야 하는지 이해하도록 도와줍니다. defaultTools(orderTools)가 등록되면, Spring AI는 자동으로: LLM에 도구 스키마를 전송하고, 도구 호출 요청 (JSON 형식)을 받으며, 해당하는 Java 메서드를 실행하고, 결과를 LLM에 다시 전송하여 답변을 완성합니다.
예를 들어 사용자가 “주문 ORD-001은 지금 어디에 있나요?“라고 묻으면, agent는 자동으로 getOrderStatus("ORD-001")을 호출하기로 결정합니다. 의도를 수동으로 파싱하는 로직을 작성할 필요가 없습니다.
사용자: 주문 ORD-001은 지금 어디에 있나요?
Agent: 주문 ORD-001은 현재 "배송 중" 상태입니다.
JUnit 5 + Mockito로 간단한 테스트를 작성하여 LLM을 실제로 호출하지 않고도 도구가 제대로 작동하는지 확인할 수 있습니다:
class OrderToolsTest {
private final OrderRepository repository = mock(OrderRepository.class);
private final OrderTools tools = new OrderTools(repository);
@Test
void shouldReturnStatusWhenOrderExists() {
when(repository.findById("ORD-001"))
.thenReturn(Optional.of(new Order("ORD-001", "배송 중")));
String result = tools.getOrderStatus("ORD-001");
assertThat(result).isEqualTo("배송 중");
}
}
주의: 도구가 실제 데이터베이스 또는 API에 접근하기 전에 항상 입력을 검증하세요 — LLM이 프롬프트가 명확하지 않으면 잘못된 형식의 매개변수를 전달할 수 있습니다 (예: 빈 orderId).
4. Planning 패턴: 큰 일을 작은 단계로 나누기
복잡한 요청이 있을 때 (“3개 공급업체를 비교하는 보고서를 작성해주세요. 가격 표와 권장사항 포함”), LLM에 한 번에 모든 것을 하도록 요청하면 단계를 놓치기 쉽습니다. Planning 패턴은 두 가지 역할로 나눕니다: Planner가 단계 목록을 작성하고, Executor가 각 단계를 순차적으로 실행합니다.
public record PlanStep(int order, String description) {}
@Service
public class PlannerAgent {
private final ChatClient chatClient;
public PlannerAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public List<PlanStep> createPlan(String goal) {
String json = chatClient.prompt()
.user("""
다음 목표를 최대 5단계로 나누어 실행 계획을 세우세요.
JSON 배열 형식 [{"order":1,"description":"..."}]으로 반환해주세요:
%s
""".formatted(goal))
.call()
.content();
return parseSteps(json);
}
}
@Service
public class ExecutorAgent {
private final ChatClient chatClient;
public ExecutorAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String runPlan(String goal, List<PlanStep> steps) {
StringBuilder context = new StringBuilder();
for (PlanStep step : steps) {
String result = chatClient.prompt()
.user("목표: " + goal
+ "\n이미 완료한 내용: " + context
+ "\n다음 단계를 실행해주세요: " + step.description())
.call()
.content();
context.append("단계 ").append(step.order())
.append(": ").append(result).append("\n");
}
return context.toString();
}
}
PlannerAgent는 단계 목록을 JSON 형식으로 생성하는 일만 하고, ExecutorAgent는 각 단계를 실행하며 context를 누적하여 다음 단계가 이전 단계에서 무엇을 했는지 알 수 있도록 합니다. planner/executor를 분리하면 디버깅이 쉬워집니다: 결과가 잘못되면 먼저 계획을 확인하고 (합리적인가), 그 다음 각 단계의 실행을 확인합니다.
"3개 공급업체를 비교하는 보고서 작성" 계획:
1. 각 공급업체의 가격과 기능 정보 수집
2. 비교 표 작성
3. 장단점 분석
4. 권장사항 제시
5. 경영진 요약(executive summary) 작성
주의: Planning 패턴은 여러 단계가 서로 의존적일 때 가장 효과적입니다. 한 단계로 해결되는 작업 (예: “이 문단 요약해주세요”)에는 planner를 추가하면 불필요하게 속도만 낮추고 토큰을 낭비할 뿐입니다.
5. Multi-Agent 패턴: 여러 전문 agent의 협력
“만능 agent” 하나 대신, Multi-Agent 패턴은 각 전문 분야의 agent들에게 작업을 분담합니다: 연구 agent, 작성 agent, 검수 agent… 그리고 한 명의 Orchestrator가 이들 사이의 흐름을 조정합니다. 마치 작은 팀처럼, 각 사람이 한 가지를 잘 합니다.
@Service
public class ResearchAgent {
private final ChatClient chatClient;
public ResearchAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String research(String topic) {
return chatClient.prompt()
.user("다음 주제에 대해 5가지 중요한 정보를 나열해주세요: " + topic)
.call()
.content();
}
}
@Service
public class WriterAgent {
private final ChatClient chatClient;
public WriterAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String write(String topic, String researchNotes) {
return chatClient.prompt()
.user("다음 데이터를 바탕으로 '" + topic
+ "'에 대해 200단어로 글을 작성해주세요: " + researchNotes)
.call()
.content();
}
}
@Service
public class OrchestratorAgent {
private final ResearchAgent researchAgent;
private final WriterAgent writerAgent;
public OrchestratorAgent(ResearchAgent researchAgent, WriterAgent writerAgent) {
this.researchAgent = researchAgent;
this.writerAgent = writerAgent;
}
public String produceArticle(String topic) {
String notes = researchAgent.research(topic);
return writerAgent.write(topic, notes);
}
}
OrchestratorAgent는 LLM을 직접 호출하지 않고 조정만 합니다: ResearchAgent를 호출하여 원본 데이터를 얻고, WriterAgent에 전달하여 완성된 글을 작성합니다. 각 agent는 자신의 프롬프트를 가지고 있으며, 한 가지 일에 최적화되어 있으므로, 각 부분의 품질은 보통 긴 프롬프트 하나에 모든 것을 집어넣은 경우보다 낫습니다.
조정 흐름이 올바른지 확인하기 위해 mock으로 테스트하고, LLM에 의존하지 않습니다:
class OrchestratorAgentTest {
@Test
void shouldPassResearchNotesToWriter() {
ResearchAgent research = mock(ResearchAgent.class);
WriterAgent writer = mock(WriterAgent.class);
when(research.research("Spring AI")).thenReturn("note-A, note-B");
when(writer.write("Spring AI", "note-A, note-B")).thenReturn("완성된 글");
OrchestratorAgent orchestrator = new OrchestratorAgent(research, writer);
String article = orchestrator.produceArticle("Spring AI");
assertThat(article).isEqualTo("완성된 글");
verify(writer).write("Spring AI", "note-A, note-B");
}
}
주의: Multi-Agent는 지연 시간 (여러 LLM 호출이 순차적으로 실행)과 시스템 복잡도를 증가시킵니다. 각 agent가 프롬프트나 문맥에서 실제로 뚜렷한 차이가 있을 때만 agent를 분리하세요 — 너무 작게 나누면 (각 agent마다 1문장) 필요 없는 오버헤드만 생깁니다.
6. ReAct 패턴: 추론과 행동의 순환
ReAct (Reasoning + Acting)는 Tool Use와 추론 루프를 결합한 패턴입니다: agent는 생각하고 (Thought) → 행동하며 (Action, 예: 도구 호출) → 결과를 관찰합니다 (Observation) → 충분한 정보가 있을 때까지 반복합니다. 단순 Tool Use (도구 1개를 호출하고 답변)와 달리, ReAct는 agent가 여러 도구를 유연한 순서로 호출하고, 이전 observation 결과에 따라 다음 단계를 자체적으로 결정할 수 있습니다.
@Service
public class ReActAgent {
private final ChatClient chatClient;
private final List<Object> tools;
public ReActAgent(ChatClient.Builder builder, WeatherTool weatherTool, SearchTool searchTool) {
this.tools = List.of(weatherTool, searchTool);
this.chatClient = builder.defaultTools(tools.toArray()).build();
}
public String solve(String question) {
return chatClient.prompt()
.system("""
실제 데이터가 필요하면 적절한 도구를 먼저 호출한 후 답변하세요.
단계별로 추론해주세요: Thought -> Action -> Observation -> Thought...
충분한 정보를 수집한 후에만 최종 답변을 제시하세요.
""")
.user(question)
.call()
.content();
}
}
“이번 주말 다낭에 가는데 레인코트를 가져가야 할까요?“라는 질문에 agent는 자체적으로 추론할 수 있습니다: weatherTool.forecast("다낭")을 호출하여 날씨 예보를 얻고, “비 70%” 결과를 관찰한 후, 그제서야 답변합니다 — 이 전체 Thought/Action/Observation 시퀀스는 프레임워크 내에서 자동으로 진행되며, 도구를 여러 번 호출하는 루프를 수동으로 작성할 필요가 없습니다. Spring AI가 자동으로 처리합니다.
Thought: 다낭의 날씨를 알아야 합니다.
Action: forecast("다낭")
Observation: 비 70%, 18-22°C
Thought: 레인코트를 가져가야겠습니다.
Answer: 다낭의 주말 비 예보가 70%이므로 레인코트를 가져가시는 것이 좋습니다.
주의: ReAct는 agent가 계속 도구를 호출하기만 하고 답변에 수렴하지 않으면 “무한 루프”에 빠질 수 있습니다. 항상 도구 호출의 최대 횟수를 제한하거나 (
maxToolCalls또는 타임아웃으로) 통제할 수 없는 API 비용을 피하세요.
7. 5가지 패턴 빠른 비교
| 패턴 | 해결하는 문제 | 비용 (LLM 호출 수) | 사용 시점 |
|---|---|---|---|
| Reflection | 답변 품질이 낮음 | 높음 (2–3배) | 정확도가 중요한 작업, 자체 검사 가능 |
| Tool Use | LLM이 데이터/시스템에 접근할 수 없음 | 낮음–중간 | 조회, 계산, API 호출 필요 |
| Planning | 많은 단계의 작업, 단계 누락 위험 | 중간 | 복잡한 작업, 구조가 명확함 |
| Multi-Agent | 한 agent로는 부족한 역할 분담 | 높음 | 역할이 명확한 파이프라인 (연구/작성/검수) |
| ReAct | 도구 필요하지만 단계 수 미정 | 변동적 | 정보 수집이 유연해야 하는 작업 |
실제로는 이 5가지 패턴이 상호 배타적이지 않습니다 — 고객 지원 agent는 Tool Use로 주문을 조회하고, Planning으로 복잡한 요청을 처리하며, Reflection으로 민감한 답변을 발송 전에 다시 확인할 수 있습니다.
8. 결론
위의 5가지 패턴 — Reflection, Tool Use, Planning, Multi-Agent, ReAct — 은 스스로 오류를 수정하고, 실제 시스템과 상호작용하며, 복잡한 작업을 처리하고, 정보를 유연하게 수집할 수 있는 AI Agent를 구축하기 위한 기본 빌딩 블록입니다. 모든 문제에 5가지를 다 사용할 필요는 없습니다. Tool Use로 시작하세요 (대부분의 agent가 필요함). 작업이 더 복잡해지면 Planning 또는 ReAct를 추가하세요. 추가 비용이 품질 향상으로 충분히 보상될 때만 Reflection/Multi-Agent를 사용하세요. 모든 것을 단일 agent에 우겨 넣는 대신 상황에 맞는 패턴을 선택하는 것이 더 중요합니다.