효과적인 에이전트 만들기
효과적인 에이전트 만들기
LLM 에이전트를 "잘" 만드는 방법은 뭘까요. 최근 연구 논문 Building Effective Agents(Anthropic)가 중요한 통찰을 줬습니다. 핵심은 복잡한 프레임워크보다 **단순함과 조합성(composability)**에 있다는 거예요. 이 페이지에서는 그 원칙을 Spring AI로 어떻게 실전에 옮길 수 있는지 살펴볼게요.
출처: 공식문서
패턴 설명과 다이어그램은 Anthropic 원문에서 가져온 것이고, 여기서는 그 패턴들을 Spring AI의 모델 포팅과 구조화된 출력 기능으로 구현하는 데 집중합니다. 원문을 먼저 읽어 두면 더 좋아요.
이어지는 예제의 전체 코드는 spring-ai-examples 저장소의 agentic-patterns 디렉토리에 있습니다.
에이전트 시스템의 두 종류
연구 논문은 에이전트 시스템을 크게 두 가지로 나눕니다.
- Workflows: LLM과 도구가 미리 정의된 코드 경로로 오케스트레이션되는 시스템(예: 규칙 기반 시스템)
- Agents: LLM이 자신의 프로세스와 도구 사용을 동적으로 주도하는 시스템
핵심 통찰은 이렇습니다. 완전 자율 에이전트가 매력적으로 보일 수 있지만, 잘 정의된 작업에서는 대개 워크플로우가 더 나은 예측 가능성과 일관성을 제공해요. 이는 안정성과 유지보수성이 중요한 엔터프라이즈 요구와 정확히 맞물립니다.
Spring AI가 이 개념을 다섯 가지 근본 패턴으로 어떻게 구현하는지 볼게요. 각 패턴은 특정 사용 사례를 다룹니다.
1. Chain Workflow
Chain Workflow 패턴은 복잡한 작업을 더 단순하고 관리 가능한 단계로 쪼개는 원칙을 보여줍니다.
언제 쓰나:
- 단계가 명확한 순서로 이어지는 작업
- 지연 시간을 조금 희생해도 정확도를 높이고 싶을 때
- 각 단계가 이전 단계의 출력 위에 쌓이는 경우
Spring AI 구현의 실전 예시입니다.
public class ChainWorkflow {
private final ChatClient chatClient;
private final String[] systemPrompts;
public String chain(String userInput) {
String response = userInput;
for (String prompt : systemPrompts) {
String input = String.format("{%s}\n {%s}", prompt, response);
response = chatClient.prompt(input).call().content();
}
return response;
}
}
이 구현은 몇 가지 핵심 원칙을 보여줍니다.
- 각 단계는 집중된 책임을 가진다
- 한 단계의 출력이 다음 단계의 입력이 된다
- 체인은 쉽게 확장하고 유지보수할 수 있다
2. Parallelization Workflow
LLM은 작업을 동시에 처리하고, 그 결과를 프로그램적으로 합칠 수 있어요.
언제 쓰나:
- 유사하지만 독립적인 항목을 대량 처리할 때
- 여러 독립적인 관점이 필요한 작업
- 처리 시간이 중요하고 작업을 병렬화할 수 있을 때
List<String> parallelResponse = new ParallelizationWorkflow(chatClient)
.parallel(
"Analyze how market changes will impact this stakeholder group.",
List.of(
"Customers: ...",
"Employees: ...",
"Investors: ...",
"Suppliers: ..."
),
4
);
3. Routing Workflow
Routing 패턴은 지능적인 작업 분배를 구현해서, 입력 유형에 따라 각각 전문적인 처리를 하게 합니다.
언제 쓰나:
- 입력 범주가 뚜렷하게 나뉘는 복잡한 작업
- 서로 다른 입력이 전문 처리를 요구할 때
- 분류를 정확히 수행할 수 있을 때
@Autowired
private ChatClient chatClient;
RoutingWorkflow workflow = new RoutingWorkflow(chatClient);
Map<String, String> routes = Map.of(
"billing", "You are a billing specialist. Help resolve billing issues...",
"technical", "You are a technical support engineer. Help solve technical problems...",
"general", "You are a customer service representative. Help with general inquiries..."
);
String input = "My account was charged twice last week";
String response = workflow.route(input, routes);
4. Orchestrator-Workers
언제 쓰나:
- 하위 작업을 미리 예측할 수 없는 복잡한 작업
- 서로 다른 접근 방식이나 관점이 필요한 작업
- 적응형 문제 해결이 필요한 상황
public class OrchestratorWorkersWorkflow {
public WorkerResponse process(String taskDescription) {
// 1. Orchestrator analyzes task and determines subtasks
OrchestratorResponse orchestratorResponse = // ...
// 2. Workers process subtasks in parallel
List<String> workerResponses = // ...
// 3. Results are combined into final response
return new WorkerResponse(/*...*/);
}
}
사용 예시:
ChatClient chatClient = // ... initialize chat client
OrchestratorWorkersWorkflow workflow = new OrchestratorWorkersWorkflow(chatClient);
WorkerResponse response = workflow.process(
"Generate both technical and user-friendly documentation for a REST API endpoint"
);
System.out.println("Analysis: " + response.analysis());
System.out.println("Worker Outputs: " + response.workerResponses());
5. Evaluator-Optimizer
언제 쓰나:
- 평가 기준이 명확하게 존재할 때
- 반복적 개선이 측정 가능한 가치를 제공할 때
- 여러 차례의 비평이 도움이 되는 작업
public class EvaluatorOptimizerWorkflow {
public RefinedResponse loop(String task) {
Generation generation = generate(task, context);
EvaluationResponse evaluation = evaluate(generation.response(), task);
return new RefinedResponse(finalSolution, chainOfThought);
}
}
사용 예시:
ChatClient chatClient = // ... initialize chat client
EvaluatorOptimizerWorkflow workflow = new EvaluatorOptimizerWorkflow(chatClient);
RefinedResponse response = workflow.loop(
"Create a Java class implementing a thread-safe counter"
);
System.out.println("Final Solution: " + response.solution());
System.out.println("Evolution: " + response.chainOfThought());
Spring AI 구현의 장점
Spring AI가 이 패턴들을 구현할 때 제공하는 이점은 Anthropic의 권장 사항과 정확히 맞아떨어집니다.
모델 포팅 (Model Portability)
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
구조화된 출력 (Structured Output)
EvaluationResponse response = chatClient.prompt(prompt)
.call()
.entity(EvaluationResponse.class);
일관된 API (Consistent API)
- 서로 다른 LLM 프로바이더에서도 동일한 인터페이스
- 내장된 오류 처리와 재시도
- 유연한 프롬프트 관리
모범 사례와 권장 사항
-
단순하게 시작하세요
- 복잡도를 더하기 전에 기본 워크플로우부터 시작
- 요구 사항을 만족하는 가장 단순한 패턴을 사용
- 필요할 때만 정교함을 더하기
-
안정성을 설계하세요
- 명확한 오류 처리 구현
- 가능하면 타입 안전한 응답 사용
- 각 단계에 검증 구축
-
트레이드오프를 고려하세요
- 지연 시간과 정확도의 균형
- 병렬 처리를 쓸 시점 평가
- 고정 워크플로우와 동적 에이전트 사이의 선택
향후 작업
이 가이드들은 이런 기반 패턴과 정교한 기능을 결합한 더 고급 에이전트를 만드는 방법으로 확장될 예정입니다.
패턴 조합 (Pattern Composition)
- 여러 패턴을 결합해 더 강력한 워크플로우 구성
- 각 패턴의 강점을 살리는 하이브리드 시스템 구축
- 변화하는 요구에 적응하는 유연한 아키텍처 설계
고급 에이전트 메모리 관리
- 대화에 걸친 영속 메모리 구현
- 컨텍스트 윈도우를 효율적으로 관리
- 장기 지식 보존 전략 개발
도구와 MCP(Model-Context Protocol) 통합
- 표준화된 인터페이스로 외부 도구 활용
- 향상된 모델 상호작용을 위한 MCP 구현
- 확장 가능한 에이전트 아키텍처 구축
결론
Anthropic의 연구 통찰과 Spring AI의 실전 구현이 결합되면, 효과적인 LLM 기반 시스템을 만드는 강력한 프레임워크가 됩니다.
이 패턴과 원칙을 따르면 개발자는 불필요한 복잡도를 피하면서도 실제 가치를 주는 견고하고 유지보수 가능한 AI 애플리케이션을 만들 수 있어요.
핵심은 때로 가장 단순한 해결책이 가장 효과적이라는 점을 기억하는 겁니다. 기본 패턴에서 시작하고 사용 사례를 철저히 이해한 다음, 시스템 성능이나 능력을 실질적으로 향상시킬 때만 복잡도를 더하세요.
더 알아보기
- MCP (Model Context Protocol) — 외부 도구·서비스 연동
- Tools — 함수 호출/도구
- Observability — 에이전트 운영 관측