효과적인 에이전트 만들기

효과적인 에이전트 만들기

LLM 에이전트를 "잘" 만드는 방법은 뭘까요. 최근 연구 논문 Building Effective Agents(Anthropic)가 중요한 통찰을 줬습니다. 핵심은 복잡한 프레임워크보다 **단순함과 조합성(composability)**에 있다는 거예요. 이 페이지에서는 그 원칙을 Spring AI로 어떻게 실전에 옮길 수 있는지 살펴볼게요.

출처: 공식문서

패턴 설명과 다이어그램은 Anthropic 원문에서 가져온 것이고, 여기서는 그 패턴들을 Spring AI의 모델 포팅과 구조화된 출력 기능으로 구현하는 데 집중합니다. 원문을 먼저 읽어 두면 더 좋아요.

이어지는 예제의 전체 코드는 spring-ai-examples 저장소의 agentic-patterns 디렉토리에 있습니다.

에이전트 시스템의 두 종류

연구 논문은 에이전트 시스템을 크게 두 가지로 나눕니다.

  1. Workflows: LLM과 도구가 미리 정의된 코드 경로로 오케스트레이션되는 시스템(예: 규칙 기반 시스템)
  2. 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 애플리케이션을 만들 수 있어요.

핵심은 때로 가장 단순한 해결책이 가장 효과적이라는 점을 기억하는 겁니다. 기본 패턴에서 시작하고 사용 사례를 철저히 이해한 다음, 시스템 성능이나 능력을 실질적으로 향상시킬 때만 복잡도를 더하세요.

더 알아보기