에이전트는 코드에서 직접 호출할 수도 있고, 사용자 입력을 통해 호출할 수도 있다.
프로그램적으로 호출
“Invoking Embabel Agents” 문서를 참고하면, 코드에서 에이전트를 호출하는 다양한 방법을 볼 수 있다.
이 경우 보통 단순 문자열이 아닌, 구조화된 타입(도메인 객체 등) 을 주고받는다.
사용자 입력 기반 호출
사용자 입력으로 호출할 때는, LLM이 Autonomy 클래스를 통해 적절한 에이전트를 고른다. 동작 방식은 설정 모드에 따라 달라진다.
에이전트를 호출하면 Embabel은 고유한 ID를 가진 AgentProcess 를 만들고,
이 객체가 전체 실행 라이프사이클을 관리한다.
AgentProcess는 실행 내내 상태를 유지하며 다음과 같은 상태로 전이된다.
프로세스 상태들:
NOT_STARTED : 아직 시작되지 않음RUNNING : 문제 없이 실행 중COMPLETED : 정상적으로 완료됨FAILED : 실패하여 더 이상 진행 불가TERMINATED : 조기 종료 정책에 의해 강제 종료됨KILLED : 사용자 또는 플랫폼에 의해 종료됨STUCK : 더 이상 진행할 계획을 세우지 못하는 상태(일시적일 수도 있음)WAITING : 사용자 입력 또는 외부 이벤트를 기다리는 중PAUSED : 스케줄링 정책에 의해 일시 정지됨실행 메서드:
tick() :run() :이 메서드들은 사용자 코드에서 직접 호출하는 경우는 드물고, 보통 프레임워크가 실행 흐름을 제어할 때 내부적으로 사용한다.
각 AgentProcess가 유지하는 정보:
각 액션이 실행된 직후, Embabel은 GOAP(Goal-Oriented Action Planning)를 이용해 다시 계획을 세운다.
플래닝 과정:
1. 현재 상태 분석
블랙보드와 월드 상태를 보고 지금 어떤 데이터와 조건이 충족되어 있는지 확인한다.
실행 가능한 액션 탐색
각 액션의 precondition 을 보고 지금 상태에서 실행 가능한 액션을 모두 찾는다.
액션 시퀀스 탐색
A* 알고리즘을 사용해 목표를 달성할 수 있는 최적의 액션 시퀀스 를 찾는다.
최적 계획 선택
비용(cost)과 성공 가능성을 고려해 가장 좋은 계획을 선택한다.
다음 액션 실행 → 다시 계획
계획에서 첫 번째 액션을 실행한 뒤, 새로운 상태를 기준으로 다시 계획(planning)을 수행한다.
이 과정은 전형적인 OODA 루프(Observe–Orient–Decide–Act) 를 구현한다.
이 재계획(replanning) 접근 덕분에 에이전트는:
Blackboard 는 에이전트 프로세스 실행 동안 상태를 유지하는 공유 메모리 시스템이다. KB에서 말하는 블랙보드 패턴을 구현한 것이다.
일반적으로 사용자 코드는 블랙보드를 직접 다룰 필요가 거의 없다. Embabel이 다음을 자동으로 처리하기 때문이다.
주요 특징
핵심 연산 예시 (Kotlin 스타일):
// 객체 추가
blackboard += person
blackboard["result"] = analysis
// 타입으로 조회
val person = blackboard.last<Person>()
val allPersons = blackboard.all<Person>()
// 조건 설정 및 조회
blackboard.setCondition("userVerified", true)
val verified = blackboard.getCondition("userVerified")
**데이터 흐름:**
1. **입력 처리**: 초기 사용자 입력이 블랙보드에 올라간다.
2. **액션 실행**: 각 액션은 블랙보드에서 입력을 읽어 결과를 추가한다.
3. **상태 진화**: 블랙보드는 시간에 따라 "상태의 히스토리"를 축적한다.
4. **플래닝 입력**: 현재 블랙보드 상태가 다음 플래닝의 입력이 된다.
5. **결과 반환**: 최종 결과는 블랙보드에서 꺼내서 사용자/호출자에게 전달된다.
**Blackboard가 제공하는 이점:**
기본적으로 블랙보드의 항목은 **타입 기준**으로 매칭된다. 여러 후보가 있을 경우 **가장 최근에 추가된 객체**가 사용된다.
또한 블랙보드 항목에 **이름(key)** 를 줄 수도 있다.
@Action
public Person extractPerson(UserInput userInput, OperationContext context) {
PersonImpl maybeAPerson =
context.promptRunner()
.withLlm(LlmOptions.fromModel(OpenAiModels.GPT_41))
.createObjectIfPossible(
"""
Create a person from this user input, extracting their name:
%s""".formatted(userInput.getContent()),
PersonImpl.class
);
if (maybeAPerson != null) {
context.bind("user", maybeAPerson);
}
return maybeAPerson;
}
context.bind("user", maybeAPerson): 블랙보드의 PersonImpl에 “user”라는 이름을 부여한다.it 이다.Embabel에서 에이전트는 크게 세 가지 개념으로 모델링된다.
이들은 모두 어노테이션(@AchievesGoal, @Action, @Condition) 기반으로 정의된다. 자세한 내용은 아래에서 다시 다룬다.
Embabel에서 도메인 객체는 단순한 DTO를 넘어선다. **상태 + 행동(Behavior)** 을 함께 가지며, 이 행동 일부를 LLM에 도구로 노출할 수 있다.
도메인 객체는 비즈니스 로직을 포함하고, 그 일부를 @Tool 을 통해 LLM이 호출할 수 있게 한다.
@Entity
public class Customer {
private String name;
private LoyaltyLevel loyaltyLevel;
private List orders;
@Tool(description = "Calculate the customer's loyalty discount percentage")
public BigDecimal getLoyaltyDiscount() {
return loyaltyLevel.calculateDiscount(orders.size());
}
@Tool(description = "Check if customer is eligible for premium service")
public boolean isPremiumEligible() {
return orders.stream()
.mapToDouble(Order::getTotal)
.sum() > 1000.0;
}
*// LLM에는 노출되지 않는 내부 로직*
private void updateLoyaltyLevel() {
*// ...*
}
}
@Tool 애노테이션을 사용하면, 도메인 객체의 메서드 중 **어떤 것만 LLM에 노출할지 선택**할 수 있다.
도메인 객체는 액션 메서드에서 자연스럽게 사용되며, LLM과 전통적인 OOP 코드를 섞어 사용하는 중심이 된다.
@Action
public Recommendation generateRecommendation(Customer customer, OperationContext context) {
*// LLM은 customer.getLoyaltyDiscount(), isPremiumEligible() 등을 툴로 사용할 수 있다.*
String prompt = String.format(
"Generate a personalized recommendation for %s based on their profile",
customer.getName()
);
return context.ai()
.withDefaultLlm()
.createObject(prompt, Recommendation.class);
}
Rod Johnson이 DICE(Domain-Integrated Context Engineering) 개념에서 강조하듯, **도메인 이해는 좋은 컨텍스트 엔지니어링의 핵심**이다.
도메인 객체는 다음을 연결하는 다리 역할을 한다.
@Tool 메서드를 추가하는 것만으로 에이전트 능력을 확장할 수 있다.지금까지 다룬 내용을 그림으로 그려보면 다음과 같다.
