Post

06 Agent 패턴

06 Agent 패턴

1. Note

  • 어우 복잡해
    • 기본적으로 reAct는 사용하는 것으로 보는게 유리할듯
    • 어차피 반복적으로 사용하는게 기본 베이스
    • 이떄 이전에 질문한걸 활용해야하니 어차피 reAct패턴과 흡사
  • 결과적으로는 그냥 Agentic을 구현하는 방법론
    • 해석하고 판단하는걸 LLM에게 한번에 맡기지말고
    • 순차적으로 판단할 수 있도록 유도하는 방법

2. Agent와 Agentic

1. Memo

  • 무언가를 대신 수행하는 프로그램 단위가 Agent
  • LLM이 어떤 순서로 판단하고 행동할지를 우리가 설계한 것이 Agentic한것
  • 그러나 본질적으로는 LLM을 호출하는 것

1. Agentic

  • “Agent”는 구조(개체),
  • Agent가 가진 요소
    • 목표 (Goal)
    • 입력 (User 요청, 환경 데이터)
    • 판단 (LLM 추론)
    • 행동 (툴 호출, API 호출 등)

2. Agentic

  • “Agentic”은 행동 방식(성향/패턴)
  • Agentic이 가진 요소
    • 스스로 계획 세움 (Planning)
    • 여러 단계로 문제 해결 (Multi-step reasoning)
    • 상황에 따라 행동 수정 (Adaptive)
    • 반복 실행 (Iteration / Loop)

3. Agentic 패턴

패턴핵심 특징언제 쓰냐흐름주도
ReAct생각+행동 반복Tool 기반 작업LLM이 매 스텝 결정
Plan-and-Execute계획 먼저단계 명확할 때LLM이 초반에 전체 결정
Self-Reflection자기 검증품질 중요할 때LLM이 자기 자신을 검토
Multi-Agent역할 분리복잡한 작업여러 LLM이 역할 분담

3. 도구(Tools) 사용 & 기본 패턴

1.ReAct Agent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String solveWithAgent(String goal) {
    log.info("Standard ReAct Agent 실행: {}", goal);

    String systemPrompt = """
        당신은 자율적인 AI 에이전트입니다. 목표 달성을 위해 다음 단계를 반복하세요:
        1. Thought: 현재 상황을 분석하고 필요한 행동을 생각합니다.
        2. Action: 적절한 도구를 선택하여 실행합니다.
        3. Observation: 도구의 결과를 확인하고 지식을 업데이트합니다.
        4. Answer: 모든 정보가 모이면 최종 답변을 작성합니다.
        """;

    return chatClientBuilder.build().prompt()
            .system(systemPrompt)
            .functions("getWeather", "calculator", "getCurrentTime") // 사용할 기능 등록
            .user(goal)
            .call()
            .content();
}

2. Plan-and-Execute (계획 후 실행)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String planAndExecute(String goal) {
    log.info("Plan-and-Execute 시작: {}", goal);

    // 1. 계획 수립 (Planner)
    String plan = chatClientBuilder.build().prompt()
            .system("당신은 복잡한 목표를 위한 논리적인 단계를 수립하는 전략가입니다.")
            .user("목표: " + goal + "\n이 목표를 달성하기 위한 상세 계획을 번호를 매겨 세워주세요.")
            .call()
            .content();

    log.info("생성된 계획: \n{}", plan);

    // 2. 계획 실행 (Executor)
    return chatClientBuilder.build().prompt()
            .system("당신은 주어진 계획을 정확히 이행하는 실행 전문가입니다. 제공된 도구를 활용하세요.")
            .functions("getWeather", "calculator", "getCurrentTime")
            .user("수립된 계획: " + plan + "\n위 계획을 실행하고 최종 결과를 보고하세요.")
            .call()
            .content();
}

3. Self-Reflection(자기 반성)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public String solveWithReflection(String problem, int maxIterations) {
  log.info("Self-Reflection Agent 시작: {}", problem);
  
  ChatClient client = chatClientBuilder.build();
  String currentResponse = "";
  String feedback = "초기 시도";

  for (int i = 0; i < maxIterations; i++) {
      log.info("반복 단계: {}/{}", i + 1, maxIterations);

      // 1. 해결 시도 또는 개선
      currentResponse = client.prompt()
              .system("도구를 사용하여 문제를 해결하세요. 이전 피드백이 있다면 반영하여 답변을 개선하세요.")
              .functions("getWeather", "calculator", "getCurrentTime")
              .user("문제: " + problem + "\n이전 피드백: " + feedback)
              .call()
              .content();

      // 2. 검증 (Critique)
      feedback = client.prompt()
              .system("제시된 답변의 정확성과 논리적 결함을 검토하세요. 완벽하면 'APPROVED'라고 답하세요.")
              .user("검토 대상 답변: " + currentResponse)
              .call()
              .content();

      if (feedback.contains("APPROVED")) {
          log.info("답변 최종 승인됨");
          break;
      }
      log.warn("개선 필요 사항 발견: {}", feedback);
  }
  return currentResponse;
}

4. 도구제한

1
2
3
4
5
6
7
8
public String solveWithLimitedTools(String goal, String... toolNames) {
    return chatClientBuilder.build().prompt()
            .system("허용된 도구만 사용하여 문제를 해결하세요.")
            .functions(toolNames)
            .user(goal)
            .call()
            .content();
}

4. reAct 패턴

1. ReAct 패턴

  • ReAct = Reason(생각) + Act(행동)을 반복하는 루프 구조

2. 주요 특징

  • Thought (추론): 현재 상황을 분석하고, 목표 달성을 위해 ‘무엇을 해야 할지’ 스스로 판단
  • Action (행동): 판단에 따라 특정 도구(검색, DB 조회, Python 코드 실행 등)를 선택하고 실행
  • Observation (관찰): 도구의 실행 결과(데이터)를 확인하고 학습
  • Final Answer (최종 답변): 모든 정보가 수집되면 사용자에게 최종 결과를 전달

3. 흐름

1
2
3
4
5
6
 ※ 정해진 루프가 있는 상태에서 
 1. LLM 호출 
 2. LLM 호출 외 비즈니스 작업 
 3. 엔드포인트인지 확인 (for문 횟수 확인, 자기 검증, 엔티티 완성 등)
   - 종료포인트 O : 작업 종료 
   - 종료포인트 X : 해당 답변과 user 질문을 합쳐서 다시 LLM호출(1번 작업)

4. 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public String run(String userQuestion) {

    StringBuilder context = new StringBuilder(userQuestion).append("\n\n");

    for (int step = 0; step < MAX_STEPS; step++) {
        log.info("=== {}바퀴 ===", step + 1);

        String response = chatClient.prompt()
                .user(context.toString())
                .call()
                .content();

        log.info("[LLM 생성]\n{}", response);
        context.append(response).append("\n");

        // Final Answer 체크
        if (response.contains("Final Answer:")) {
            return extractValue(response, "Final Answer:");
        }

        // Action Input 파싱 → 툴 실행 → Observation 주입
        if (response.contains("Action Input:")) {
            String actionInput = extractValue(response, "Action Input:");
            String observation = searchTool.search(actionInput);

            log.info("[툴 실행] {}", observation);
            context.append("Observation: ").append(observation).append("\n\n");
        }
    }

    throw new IllegalStateException("MAX_STEPS(%d) 초과".formatted(MAX_STEPS));
}

5. 전략적 계획 및 단계별 실행 (Plan-and-Execute)

1. 프롬프트 상수/ private

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//프롬프트 상수화
private static final String PLANNER_SYSTEM_PROMPT = """
  당신은 효율적인 계획을 수립하는 전문가입니다. 목표 달성을 위해 구체적이고 실행 가능한 계획을 세우세요.
  계획 작성 형식:
  1. [단계명]: [작업 내용] - 도구: [도구명]
  2. [단계명]: [작업 내용] - 도구: [도구명]
  
  사용 가능 도구: getWeather, calculator, getCurrentTime
  각 단계는 이전 단계의 결과를 활용하도록 설계하세요.
  """;
  
// 계획 수립  
private String createPlan(String goal) {
  return chatClientBuilder.build().prompt()
          .system(PLANNER_SYSTEM_PROMPT)
          .user(goal)
          .call()
          .content();
}

// 스탭 추출
private List<String> parseSteps(String plan) {
  return plan.lines()
          .map(String::trim)
          .filter(line -> line.matches("^\\d+\\..*")) // "1. 단계명" 형태만 추출
          .collect(Collectors.toList());
}

//결과 취합
private String summarizeResults(String goal, List<StepResult> stepResults) {
  String combined = stepResults.stream()
          .map(s -> "단계 " + s.stepNumber() + ": " + s.result())
          .collect(Collectors.joining("\n"));

  return chatClientBuilder.build().prompt()
          .system("실행 결과들을 종합하여 사용자에게 최종 보고서를 작성하세요.")
          .user("목표: " + goal + "\n결과들:\n" + combined)
          .call()
          .content();
}

2. 기본 Plan-and-Execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String executeWithPlanning(String goal) {
    log.info("=== Basic Plan-and-Execute 시작: {} ===", goal);

    String plan = createPlan(goal);
    log.info("수립된 계획:\n{}", plan);

    String executionPrompt = """
        다음 계획을 단계별로 실행하고 최종 결과를 요약하세요.
        계획: %s
        원래 목표: %s
        """.formatted(plan, goal);

    return chatClient.build().prompt()
            .system("당신은 계획을 정확하게 실행하는 AI 에이전트입니다.")
            .functions("getWeather", "calculator", "getCurrentTime")
            .user(executionPrompt)
            .call()
            .content();
}

3. 상세 추적형 실행 (Detailed Tracking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public ExecutionResult executeWithDetailedTracking(String goal) {
    log.info("=== Detailed Tracking Agent 시작 ===");

    String plan = createPlan(goal);
    List<String> steps = parseSteps(plan);
    List<StepResult> stepResults = new ArrayList<>();
    String context = ""; // 이전 단계들의 누적 결과

    for (int i = 0; i < steps.size(); i++) {
        String currentStep = steps.get(i);
        int stepNum = i + 1;
        log.info("단계 {}/{} 실행: {}", stepNum, steps.size(), currentStep);

        String stepResult = chatClient.build().prompt()
                .system("당신은 주어진 단계를 실행하는 전문가입니다. 이전 문맥을 참고하세요.")
                .functions("getWeather", "calculator", "getCurrentTime")
                .user("전체목표: %s\n현재단계: %s\n이전까지의 문맥: %s".formatted(goal, currentStep, context))
                .call()
                .content();

        stepResults.add(new StepResult(stepNum, currentStep, stepResult));
        context += "\n[단계 " + stepNum + " 결과]: " + stepResult;
    }

    String finalSummary = summarizeResults(goal, stepResults);
    return new ExecutionResult(goal, plan, stepResults, finalSummary);
}

4. 적응형 계획

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public String executeWithAdaptivePlanning(String goal, int maxReplanning) {
    log.info("=== Adaptive Planning Agent 시작 ===");
    
    String currentPlan = createPlan(goal);
    String lastResult = "";

    for (int i = 0; i <= maxReplanning; i++) {
        lastResult = executeWithPlanning(currentPlan);
        
        // 결과 검증
        String validation = chatClient.build().prompt()
                .system("결과가 목표를 달성했는지 평가하세요. 달성했다면 'SUCCESS', 아니면 'INCOMPLETE: 이유'라고 답하세요.")
                .user("목표: %s\n결과: %s".formatted(goal, lastResult))
                .call().content();

        if (validation.startsWith("SUCCESS")) {
            log.info("목표 달성 성공 (시도: {})", i + 1);
            return lastResult;
        }

        log.warn("계획 재수립 필요 (사유: {})", validation);
        currentPlan = chatClientBuilder.build().prompt()
                .system("피드백을 바탕으로 계획을 수정하세요.")
                .user("기존계획: %s\n실패결과: %s\n피드백: %s".formatted(currentPlan, lastResult, validation))
                .call().content();
    }
    return lastResult;
}

6. 협업형 멀티 에이전트

1. 상수 / 개별 프롬프트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 연구원 에이전트
private static final String RESEARCHER_PROMPT = """
    당신은 정보 수집 전문 연구원입니다. 도구(getWeather, getCurrentTime 등)를 활용하여 
    문제 해결에 필요한 팩트와 데이터를 체계적으로 수집하세요.
    """;

  private String executeResearcher(String input) {
      return chatClientBuilder.build().prompt()
              .system(RESEARCHER_PROMPT)
              .functions("getWeather", "getCurrentTime") // 연구용 도구
              .user(input)
              .call().content();
  }

// 데이터분석가 에이전트
private static final String ANALYST_PROMPT = """
    당신은 데이터 분석 전문가입니다. 수집된 정보를 바탕으로 인사이트를 도출하세요.
    수치 계산이 필요하면 calculator 도구를 사용하고, 논리적인 인과관계를 분석하세요.
    """;

private String executeAnalyst(String input) {
    return chatClientBuilder.build().prompt()
            .system(ANALYST_PROMPT)
            .functions("calculator") // 분석용 도구
            .user(input)
            .call().content();
}

// 리포트 작성 에이전트
private static final String WRITER_PROMPT = """
    당신은 전문 리포트 작성자입니다. 분석 내용을 바탕으로 구조화된 보고서를 작성하세요.
    구성: 1.요약, 2.주요발견, 3.상세분석, 4.결론
    """;
    
private String executeWriter(String input) {
    return chatClientBuilder.build().prompt()
            .system(WRITER_PROMPT)
            .user(input)
            .call().content();
}

2. 순차적 협업 (Sequential Chain)

1
2
3
4
5
6
7
8
public String solveSequential(String problem) {
  log.info("순차적 협업 시작: {}", problem);

  //순서대로 한번씩 호출해서 사용함.
  String research = executeResearcher(problem);
  String analysis = executeAnalyst(research);
  return executeWriter(analysis);
}

3. 동적 파이프라인 (Dynamic Orchestration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public String solveWithDynamicAgents(String problem) {
    String problemType = analyzeProblemType(problem);
    log.info("분석된 문제 유형: {}", problemType);

    //문제 타입에 따른 프롬프트 조합 선택
    List<AgentRole> pipeline = switch (problemType) {
        case "CALCULATION" -> List.of(AgentRole.ANALYST, AgentRole.WRITER);
        case "DATA_COLLECTION" -> List.of(AgentRole.RESEARCHER, AgentRole.WRITER);
        default -> List.of(AgentRole.RESEARCHER, AgentRole.ANALYST, AgentRole.WRITER);
    };

    //프롬프트 조합별 작업 진행
    String currentInput = problem;
    for (AgentRole role : pipeline) {
        currentInput = switch (role) {
            case RESEARCHER -> executeResearcher(currentInput);
            case ANALYST -> executeAnalyst(currentInput);
            case WRITER -> executeWriter(currentInput);
        };
    }
    return currentInput;
}

private String analyzeProblemType(String problem) {
    return chatClientBuilder.build().prompt()
            .system("문제 유형을 하나만 선택하세요: CALCULATION, DATA_COLLECTION, ANALYSIS")
            .user(problem)
            .call().content().trim();
}

4. 피드백 루프 (Iterative Review)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String solveWithFeedback(String problem, int maxIterations) {
    String research = executeResearcher(problem);
    String result = "";

    for (int i = 0; i < maxIterations; i++) {
        String analysis = executeAnalyst(research);
        
        // 검토자(Reviewer) 에이전트 호출
        String feedback = chatClientBuilder.build().prompt()
                .system("분석 결과가 충분한지 평가하세요. 완벽하면 'APPROVED', 부족하면 피드백을 주세요.")
                .user("문제: " + problem + "\n분석결과: " + analysis)
                .call().content();

        if (feedback.contains("APPROVED")) {
            return executeWriter(analysis);
        }
        
        log.warn("품질 미달로 재연구 수행 (반복: {}): {}", i + 1, feedback);
        research = executeResearcher("이전 연구: " + research + "\n피드백 반영: " + feedback);
    }
    return result;
}
This post is licensed under CC BY 4.0 by the author.