Embabel Agent 프레임워크 가이드(1) - 개요

bebeis·2025년 11월 26일

embabel

목록 보기
5/5
post-thumbnail

https://docs.embabel.com/embabel-agent/guide/0.1.3/ 공식 문서를 바탕으로 작성된 글입니다.

1.1 용어 설명

  • 에이전트(Agent): 도메인 로직, AI 기능, 도구 사용을 번들로 묶어서 여러 @Action 메서드를 통해 특정 목표를 달성하는 자체 포함 컴포넌트
  • 도구(Tools): LLM이 외부 시스템과 상호작용할 수 있도록 하는 확장 기능. 단순 텍스트 생성을 넘어 데이터베이스 업데이트 또는 서비스 예약 같은 실제 작업을 수행할 수 있다.
  • MCP (Model Context Protocol): 모델과 런타임을 통해 도구를 호스팅하고 공유하는 표준화된 접근 방식. 시스템 전체 또는 런타임 도구 등록을 가능하게 한다.
  • DICE (Domain Integrated Context Engineering): 타이핑되지 않은 프롬프트가 아닌 타이핑된 도메인 객체에 LLM 입출력을 기반으로 하여 컨텍스트를 개선한다.

1.2. 에이전트 프레임워크가 필요한 이유

앞선 글을 참고하자.

1.3 Embabel의 차별점

1.3.1. 정교한 계획 수립

Finite State Machine을 넘어 비LLM AI 알고리즘을 사용한 계획 수립으로 새로운 단계 조합을 가능하게 한다.

1.3.2. 우수한 확장성

동적 계획 수립으로 기존 코드를 수정하지 않고 기능을 확장할 수 있습니다. 즉, 도메인 객체와 액션을 쉽게 추가할 수 있다.

1.3.3. 강한 타이핑(Strong Typing)과 객체 지향(Object Orientation)

액션, 목표, 조건이 모두 정적으로 타입이 정의되어 있어 리팩토링 시 IDE가 전체 흐름을 정확히 추적해준다.
문자열 기반의 “마법 같은 맵”을 제거하고(문자열 기반 매핑 제거), 안정적인 구조를 유지할 수 있다.

actions = {
    "search_product": search_product_action,
    "apply_discount": apply_discount_action,
    "checkout": checkout_action,
}

대신

@Action
fun findProduct(productId: ProductId): Product { ... }
  • IDE와 컴파일러가 전체 흐름을 추적할 수 있다.
  • 리팩토링이 완전하게 지원된다.
  • 타입 검증으로 안전성 확보가 가능하다.

1.3.4. 플랫폼 추상화

프로그래밍 모델과 실제 실행 플랫폼을 깔끔하게 분리해 어떤 환경에서도 유연하게 에이전트를 배포할 수 있다.

1.3.5. LLM 혼합

여러 종류의 LLM을 상황에 맞게 조합해 사용할 수 있으며, 비용 효율성과 개인정보 보호 측면에서 더 나은 구성을 만들 수 있다.

1.3.6. Spring 및 JVM 통합

Spring 기반으로 동작하기 때문에 엔터프라이즈 애플리케이션에서 사용하는 다양한 기능과 견고한 데이터 영속성 계층을 자연스럽게 활용할 수 있다.

1.3.7. 테스트 가능성

단위 테스트부터 엔드-투-엔드 에이전트 테스트까지 처음부터 고려된 구조로 설계되어 있어, 안정적인 테스트 환경을 쉽게 구축할 수 있다.

1.4 핵심 개념

에이전트 프레임워크는 하나의 큰 작업을 여러 개의 작은 상호작용 단위로 나누어 다루며, 이를 다음과 같은 요소로 모델링한다

  • 액션(Actions): 에이전트가 실제로 수행할 수 있는 개별 단계
  • 목표(Goals): 에이전트가 최종적으로 달성해야 하는 결과
  • 조건(Conditions): 각 액션을 실행한 뒤 다시 평가되는 제약이나 요구사항
  • 도메인 모델: 에이전트가 판단하고 흐름을 이어가는 데 필요한 정보를 담는 중심 데이터 구조

별자리 뉴스 finder

@Agent(description = "Find news based on a person's star sign")   // 1
public class StarNewsFinder {

    private final HoroscopeService horoscopeService;  // 2
    private final int storyCount;

    public StarNewsFinder(
            HoroscopeService horoscopeService,  // 3
            @Value("${star-news-finder.story.count:5}") int storyCount) {
        this.horoscopeService = horoscopeService;
        this.storyCount = storyCount;
    }

    @Action  // 4
    public StarPerson extractStarPerson(UserInput userInput, OperationContext context) {  // 5
        return context.ai()
            .withLlm(OpenAiModels.GPT_41)
            .createObject("""
                Create a person from this user input, extracting their name and star sign:
                %s""".formatted(userInput.getContent()), StarPerson.class);  // 6
    }

    @Action  // 7
    public Horoscope retrieveHoroscope(StarPerson starPerson) {  // 8
        // Uses regular injected Spring service - not LLM
        return new Horoscope(horoscopeService.dailyHoroscope(starPerson.sign()));  // 9
    }

    @Action(toolGroups = {CoreToolGroups.WEB}) // 10 
    public RelevantNewsStories findNewsStories(
            StarPerson person, Horoscope horoscope, OperationContext context) {  // 11
        var prompt = """
            %s is an astrology believer with the sign %s.
            Their horoscope for today is: %s
            Given this, use web tools to find %d relevant news stories.
            """.formatted(person.name(), person.sign(), horoscope.summary(), storyCount);

        return context.ai().withDefaultLlm().createObject(prompt, RelevantNewsStories.class);  // 12
    }

    @AchievesGoal(description = "Write an amusing writeup based on horoscope and news") // 13  
    @Action
    public Writeup writeup(
            StarPerson person, RelevantNewsStories stories, Horoscope horoscope,
            OperationContext context) {  // 14
        var llm = LlmOptions.fromCriteria(ModelSelectionCriteria.getAuto())
            .withTemperature(0.9);  // 15

        var prompt = """
            Write something amusing for %s based on their horoscope and these news stories.
            Format as Markdown with links.
            """.formatted(person.name());

        return context.ai().withLlm(llm).createObject(prompt, Writeup.class);  // 16
    }
}

이 조그만한 예시에서 우리는 JVM 기반의 Embabel 에이전트 프레임워크의 특징을 볼 수 있다.

  1. 완전한 타입 기반(Type-safe) 에이전트 설계
    • 액션의 입력과 출력이 모두 Kotlin/Java 클래스(StarPerson, Horoscope, RelevantNewsStories, Writeup) 로 명확히 정의되어 있다.
    • 즉,
      • 문자열 파싱 없음
      • JSON 스키마 수동 정의 없음
      • LLM 출력 → 바로 강타입 객체로 매핑
      • 리팩토링 시 IDE가 모든 연결을 추적
  2. JVM 서비스/도메인과의 자연스러운 통합
    • private final HoroscopeService horoscopeService;
    • horoscopeService는 기존 Spring/JVM 서비스다. 에이전트는 LLM이 아니라 기존 비즈니스 로직을 직접 호출한다.
    • 도메인 서비스를 호출하고, 엔터프라이즈 로직을 재사용할 수 있다. 엄청난 강점이다.
  3. 도구(Action) 탐색 및 연결 자동화
    • @Action, @AchievesGoal 같은 어노테이션만 붙이면 도구 등록, 액션 연결, 목표 달성 흐름이 자동으로 구성된다.
    • 어노테이션 기반의 선언형 스타일로 구성된 스프링 프레임워크에 익숙하다면, 쉽게 사용할 수 있다.
  4. Multi-LLM 기능이 코드 레벨에서 자연스럽게 지원된다.
    • 각 단계마다 어떤 모델을 사용할지 선택 가능하다.
    • context.ai().withLlm(OpenAiModels.GPT_41)
    • context.ai().withDefaultLlm()
    • 비용/성능 최적화를 코드 레벨에서 쉽게 처리할 수 있다.
  5. 도메인 모델 중심의 워크플로우
    • 에이전트의 상태 전이가 전부 도메인 객체로 구성되어 구조적으로 명확하다.
    1. UserInput → StarPerson
    2. StarPerson → Horoscope
    3. StarPerson + Horoscope → RelevantNewsStories
    4. 최종적으로 Writeup 생성

아래는 embabel 공식 문서에서 설명하고 있는 프레임워크 핵심 기능들이다.
1. Agent 선언
@Agent 애노테이션은 이 클래스가 여러 단계를 거치는 에이전트임을 정의한다.

  1. Spring 통합
    일반적인 Spring DI가 그대로 적용되며, LLM 기반 작업과 기존 비즈니스 로직을 자연스럽게 함께 사용할 수 있다.

  2. 서비스 주입
    HoroscopeService는 일반 Spring Bean처럼 주입되며, 에이전트는 AI 기능과 기존 서비스 호출을 자유롭게 조합할 수 있다.

  3. 액션 정의
    @Action이 붙은 메서드는 에이전트가 수행할 수 있는 하나의 단계로, 각 메서드는 에이전트가 가진 능력(capability)을 나타낸다.

  4. 입력 조건 추론
    extractStarPerson(UserInput userInput, …​) 시그니처는 다음을 의미한다:

    • 사전 조건: UserInput 객체가 있어야 함
    • 필요 데이터: 사용자 입력이 있어야 해당 액션 실행 가능
    • 기능 설명: 비정형 텍스트에서 구조화된 데이터를 추출하는 능력 제공
  5. 출력 조건 생성
    이 액션이 StarPerson을 반환하면 Embabel은 다음을 이해한다.

    • 사후 조건: StarPerson이 에이전트 상태에 추가됨
    • 데이터 활용: 이후 액션의 입력으로 자동 연결
    • 타입 안정성: 도메인 모델이 구조를 보장
  6. 비 LLM 액션
    모든 액션이 LLM을 사용하는 것은 아니다. 이 예시는 전통적인 서비스 로직과 AI 로직을 함께 사용할 수 있음을 보여준다.

  7. 데이터 흐름 연결
    retrieveHoroscope(StarPerson starPerson)는 다음을 의미한다:

    • 사전 조건: StarPerson이 먼저 존재해야 함
    • 의존성: extractStarPerson 이후 실행 가능
  8. 서비스 통합:

    • LLM 대신 기존 Spring 서비스 호출
    • 전통적인 비즈니스 로직 재사용 가능
  9. 도구 사용 요구사항
    toolGroups = {CoreToolGroups.WEB}은 이 액션이 웹 검색 도구에 접근해야 함을 나타낸다.

  10. 다중 입력 의존성
    findNewsStories()StarPersonHoroscope 두 데이터를 모두 필요로 하며, 복잡한 데이터 흐름을 자동으로 조합할 수 있음을 보여준다.

  11. 툴 지원 LLM 호출
    이 단계에서 LLM은 웹 도구를 활용하여 운세 관련 최신 뉴스를 검색할 수 있다.

  12. 목표 달성 표시
    @AchievesGoal은 이 메서드가 에이전트의 최종 목표를 이루는 종결 단계임을 나타낸다.

  13. 복합 데이터 요구사항
    마지막 액션은 StarPerson, RelevantNewsStories, Horoscope 세 데이터를 모두 입력으로 사용하며, 에이전트의 자동 오케스트레이션 능력을 보여준다.

  14. 창의적 출력 설정
    temperature가 0.9로 설정된 LLM은 더욱 창의적이고 재미있는 글을 생성하도록 최적화되어 있다. (0.9가 창의적이다? 라고 볼 수 있는지에 대해서는 잘 모르겠다 ㅎㅎ)

  15. 최종 출력
    이 액션은 Writeup을 반환하며, 에이전트의 전체 목표가 성공적으로 완료되었음을 의미한다.

위 예시의 모든 @Action 메서드에서 다음과 같은 부분이 반복됨을 알 수 있다.

  • 메서드 시그니처를 통해 자연스럽게 사전 조건을 나타내고, 반환 타입을 통해 출력 조건을 나타낸다.
  • 사전 조건과 출력 조건은 모두 도메인 모델을 활용함으로써 비즈니스 로직과 자연스럽게 통합될 수 있다.

1.4.2. 추론된 실행 계획

Embabel은 각 액션의 타입 시그니처(입력·출력 타입) 를 분석하여, 어떤 순서로 액션을 실행해야 목표를 달성할 수 있는지를 자동으로 계산한다.

예를 들어 Writeup을 만들기 위해 필요한 흐름을 Embabel은 다음과 같이 스스로 도출한다:
1. 최종 결과인 Writeup을 생성하려면 writeup() 액션이 필요하다.
2. writeup()StarPerson, RelevantNewsStories, Horoscope을 입력으로 받는다.
3. StarPerson을 얻기 위해 → extractStarPerson() 실행
4. Horoscope을 얻기 위해 → retrieveHoroscope() 실행 (입력: StarPerson)
5. RelevantNewsStories를 얻기 위해 → findNewsStories() 실행

결과적으로 자동 생성된 실행 순서:
UserInput
extractStarPerson()
StarPerson
retrieveHoroscope()
Horoscope
findNewsStories()
RelevantNewsStories
writeup()
Writeup

1.4.3. 타입 기반 흐름의 주요 이점

  • 자동 오케스트레이션: 액션 순서를 사람이 일일이 정의할 필요가 없다. Embabel이 타입 정보를 바탕으로 알아서 계획을 세운다.
  • 동적 재계획: 각 단계가 끝날 때마다 현재 얻어진 데이터를 다시 살펴보고, 가장 적절한 다음 액션을 자동으로 선택한다.
  • 타입 안전성: 데이터 흐름이 정적으로 보장되기 때문에 런타임 오류를 크게 줄일 수 있다.
  • 유연한 실행: 가능한 경로가 여러 개일 때, 에이전트가 상황과 컨텍스트에 맞춰 최선의 흐름을 결정할 수 있다.
profile
No Trying. Just Doing. 시도에 머무르지 않고, 개발이라는 '일'을 확실하게 해냅니다.

0개의 댓글