Agents and Agentic AI
Agents and Agentic AI
langchain4j-agentic 모듈로 에이전틱 AI(agentic AI) 애플리케이션을 만드는 방법을 알아봐요. 이 모듈 전체는 실험적이고 향후 릴리스에서 변경될 수 있어요.
에이전틱 시스템
AI 에이전트에 대한 보편적으로 합의된 정의는 없지만, 여러 AI 서비스의 능력을 조정·결합해서 더 복잡한 작업을 수행하는 AI 애플리케이션을 만드는 몇 가지 떠오르는 패턴이 있어요. 이런 패턴을 "에이전틱 시스템" 또는 "에이전틱 AI"라고 부르죠. 보통 작업 실행을 오케스트레이션하고, 도구 사용을 관리하며, 상호작용 전반에 걸쳐 컨텍스트를 유지하기 위해 대형 언어 모델(LLM)을 사용해요.
Anthropic 연구자들이 발표한 최근 글에 따르면, 에이전틱 시스템 아키텍처는 workflows와 pure agents 두 가지 주요 범주로 나눌 수 있어요.
이 튜토리얼에서 다루는 langchain4j-agentic 모듈은 workflow와 pure 에이전틱 AI 애플리케이션을 만드는 데 도움이 되는 추상화와 유틸리티 세트를 제공해요. 서로 다른 LLM과의 상호작용에 걸쳐 워크플로를 정의하고, 도구 사용을 관리하며, 컨텍스트를 유지할 수 있게 해주죠.
LangChain4j의 에이전트
LangChain4j의 에이전트는 LLM을 사용해 특정 작업 또는 작업 세트를 수행해요. 일반 AI service와 비슷하게 단일 메서드가 있는 인터페이스로 정의하고, 메서드에 @Agent 애노테이션만 추가하면 돼요.
public interface CreativeWriter {
@UserMessage("""
You are a creative writer.
Generate a draft of a story no more than
3 sentences long around the given topic.
Return only the story and nothing else.
The topic is {{topic}}.
""")
@Agent("Generates a story based on the given topic")
String generateStory(@V("topic") String topic);
}
이 애노테이션과 함께 에이전트의 목적에 대한 짧은 설명을 제공하는 게 좋아요. 특히 순수 에이전틱 패턴에서 쓰일 때는요. 다른 에이전트들이 이 에이전트의 능력을 알아야 언제 어떻게 쓸지 정보에 입각한 결정을 내릴 수 있으니까요. 이 설명은 에이전트를 빌드할 때 에이전트 빌더의 description 메서드로 프로그래매틱하게 제공할 수도 있어요.
에이전트는 에이전틱 시스템 안에서 고유하게 식별하는 이름도 있어야 해요. 이 이름은 @Agent 애노테이션에서 또는 에이전트 빌더의 name 메서드로 프로그래매틱하게 지정할 수 있어요. 지정하지 않으면 @Agent가 붙은 메서드 이름에서 가져와요.
AgenticServices.agentBuilder() 메서드로 이 에이전트의 인스턴스를 빌드할 수 있어요, 인터페이스와 사용할 채팅 모델을 지정하면서요.
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(myChatModel)
.outputKey("story")
.build();
본질적으로 에이전트는 평범한 AI service로, 같은 기능을 제공하면서 다른 에이전트와 결합해 더 복잡한 워크플로와 에이전틱 시스템을 만들 수 있는 능력이 추가된 거예요.
AI service와의 또 다른 주요 차이는 outputKey 파라미터가 있다는 점이에요. 이 파라미터는 에이전트 호출 결과가 저장될 공유 변수의 이름을 지정해서, 같은 에이전틱 시스템에서 다른 에이전트가 사용할 수 있게 해줘요. 또는 출력 이름을 프로그래매틱하게 지정하는 대신 @Agent 애노테이션에서 직접 선언할 수도 있어요:
@Agent(outputKey = "story", description = "Generates a story based on the given topic")
AgenticServices 클래스는 langchain4j-agentic 프레임워크에서 사용할 수 있는 모든 종류의 에이전트를 만들고 정의하는 정적 팩토리 메서드 세트를 제공해요.
AgenticScope 소개
langchain4j-agentic 모듈은 에이전틱 시스템에 참여하는 에이전트들 사이에 공유되는 데이터 모음인 AgenticScope라는 개념을 도입해요. AgenticScope는 공유 변수를 저장하는 데 쓰이며, 에이전트가 생산한 결과를 쓰고 다른 에이전트가 작업을 수행하는 데 필요한 정보를 조합하기 위해 읽을 수 있어요. 이를 통해 에이전트들이 필요할 때 정보와 결과를 공유하며 효과적으로 협력할 수 있어요.
AgenticScope는 모든 에이전트의 호출 순서와 응답 같은 다른 관련 정보도 자동으로 등록해요. 에이전틱 시스템의 메인 에이전트가 호출될 때 자동으로 생성되고, 필요할 때 콜백을 통해 프로그래매틱하게 제공돼요.
워크플로 패턴
langchain4j-agentic 모듈은 여러 에이전트를 프로그래매틱하게 오케스트레이션하고 에이전틱 워크플로 패턴을 만드는 추상화 세트를 제공해요. 이 패턴들을 결합해서 더 복잡한 워크플로를 만들 수 있어요.
순차 워크플로
순차 워크플로는 여러 에이전트가 하나씩 순서대로 호출되고, 각 에이전트의 출력이 다음 에이전트의 입력으로 전달되는 가장 단순한 패턴이에요. 특정 순서로 수행해야 하는 일련의 작업이 있을 때 유용하죠.
앞서 정의한 CreativeWriter 에이전트를, 생성된 스토리를 특정 독자층에 맞게 편집하는 AudienceEditor 에이전트로 보완하는 게 좋겠어요.
public interface AudienceEditor {
@UserMessage("""
You are a professional editor.
Analyze and rewrite the following story to better align
with the target audience of {{audience}}.
Return only the story and nothing else.
The story is "{{story}}".
""")
@Agent("Edits a story to better fit a given audience")
String editStory(@V("story") String story, @V("audience") String audience);
}
세 에이전트를 결합한 순차 워크플로를 만들 수 있어요. CreativeWriter의 출력이 AudienceEditor와 StyleEditor 둘 다의 입력으로 전달되고, 최종 출력이 편집된 스토리예요:
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
AudienceEditor audienceEditor = AgenticServices
.agentBuilder(AudienceEditor.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
StyleEditor styleEditor = AgenticServices
.agentBuilder(StyleEditor.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
UntypedAgent novelCreator = AgenticServices
.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
Map<String, Object> input = Map.of(
"topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"
);
String story = (String) novelCreator.invoke(input);
여기서 novelCreator 에이전트는 세 개의 하위 에이전트를 하나씩 호출하는 순차 워크플로를 구현하는 에이전틱 시스템이에요. 이 에이전트 정의가 타입 인터페이스와 함께 제공되지 않았으므로, 시퀀스 에이전트 빌더는 입력 맵으로 호출할 수 있는 제네릭 에이전트인 UntypedAgent 인스턴스를 반환해요.
public interface UntypedAgent {
@Agent
Object invoke(Map<String, Object> input);
}
입력 맵의 값들은 AgenticScope 공유 변수로 복사되어 하위 에이전트가 접근할 수 있어요. novelCreator 에이전트의 출력도 이전에 모든 다른 에이전트들이 소설 생성·편집 워크플로 실행 중 다시 쓴 "story"라는 이름의 AgenticScope 공유 변수에서 가져와요.
루프 워크플로
LLM의 능력을 더 잘 활용하는 흔한 방법은 특정 조건이 충족될 때까지 에이전트를 반복 호출하면서 스토리 같은 텍스트를 반복적으로 다듬는 거예요. 루프 워크플로 패턴으로 에이전트를 여러 번 호출해서 이걸 이뤄요.
관측성(Observability)
모니터링
AgentListener 인터페이스가 제공하는 관측성 기능을 활용해 langchain4j-agentic 모듈은 이 인터페이스의 내장 구현(모든 하위 에이전트가 상속하도록 구성된 AgentMonitor)도 제공해요. 목표는 모든 에이전트 호출을 인메모리 트리 구조로 기록하는 거예요.
AgentMonitor는 모든 에이전트 호출을 트리 구조로 기록하는데, 각 에이전트 호출의 시작 시간, 종료 시간, 지속 시간, 토큰, 입력, 출력도 추적해요. 이때 모니터에서 기록된 실행을 검색해서 콘솔에 출력하는 등 검사할 수 있어요.
HtmlReportGenerator 클래스의 정적 generateReport 메서드를 쓰면 에이전틱 시스템의 토폴로지와 기록된 실행 둘 다에 대해 AgentMonitor가 수집한 데이터의 시각적 HTML 리포트를 생성할 수도 있어요.
HtmlReportGenerator.generateReport(monitor, Path.of("review-loop.html"));
기본적으로 AgentMonitor는 결과(성공·실패 각각)당 최대 100개 세션(고유 메모리 ID)을 유지해요. 한도를 초과하면 가장 오래된 세션부터 자동으로 제거돼요. 유지 한도는 setMaxRetainedSessions로 바꿀 수 있고, 0으로 설정하면 유지를 완전히 비활성화해요. clear() 메서드로 유지된 세션을 모두 제거할 수 있어요.
순수 에이전틱 AI
langchain4j-agentic 모듈은 즉시 사용 가능한 슈퍼바이저 에이전트를 제공해요. 하위 에이전트 세트를 제공받아 자율적으로 계획을 생성하고, 다음에 어떤 에이전트를 호출할지 또는 할당된 작업이 완료됐는지 결정할 수 있죠.
커스텀 에이전틱 패턴
지금까지 논의한 에이전틱 패턴은 langchain4j-agentic 모듈이 즉시 제공하는 것들이에요. 하지만 그중 어느 것도 애플리케이션의 특정 요구에 맞지 않으면 어떡할까요? 커스텀 에이전틱 패턴을 정의할 수 있어요. 이 인터페이스에는 init, firstAction, nextAction 세 메서드가 있어요. init 메서드는 실행 시작 시 한 번 호출되어 플래너가 필요한 상태나 데이터 구조를 초기화하는 데 쓰여요. plannerBuilder() 메서드는 다른 모든 에이전트 빌더와 비슷한데, 이 에이전틱 시스템에서 사용할 특정 플래너의 새 인스턴스를 반환하는 Supplier<Planner>를 요구하는 점만 달라요.
비AI 에이전트
Human-in-the-loop
langchain4j-agentic 모듈이 즉시 제공하는 HumanInTheLoop 에이전트는 이 함수와 에이전트 설명, 사용자 응답이 기록될 출력 변수를 함께 정의하게 해줘요.
메모리와 컨텍스트 엔지니어링
AgenticScope 등록·영속화
AgenticScope는 에이전틱 시스템 실행 중 생성·사용되는 일시적 데이터 구조예요. 에이전틱 시스템마다·사용자마다 하나의 AgenticScope가 있어요.
Agents Registry
langchain4j-agentic 모듈은 AgentsRegistry용 SPI(서비스 제공자 인터페이스)를 제공해요. 외부 제공자가 이름으로 발견되고 어떤 에이전틱 패턴에도 연결될 수 있는 에이전트를 등록하게 해주죠.
MCP 기반 도구 에이전트
추가 langchain4j-agentic-mcp 모듈은 에이전틱 시스템에서 단일 MCP 도구를 비AI 에이전트로 감싸게 해줘요. LLM을 사용하는 일반 에이전트와 달리, MCP 도구 에이전트는 MCP 도구를 직접 실행하고 그 결과를 반환해요. 이렇게 하면 도구 실행 자체에 LLM을 끼우지 않고도 더 큰 에이전틱 시스템에서 MCP 도구를 다른 에이전트와 결합할 수 있어요.
MCP 도구 에이전트를 만들려면 McpClient 인스턴스를 제공해서 McpAgent.builder()를 써요. 빌더가 MCP 서버에 도구 명세(이름, 설명, 입력 스키마)를 쿼리하고 호출을 그 도구로 전달하는 에이전트를 만들어요. 예를 들어 MCP 서버가 generate_story 도구를 노출하면 이를 untyped 에이전트로 감쌀 수 있어요:
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(myMcpTransport)
.build();
UntypedAgent storyGenerator = McpAgent.builder(mcpClient)
.toolName("generate_story")
.inputKeys("topic")
.outputKey("story")
.build();
String story = (String) storyGenerator.invoke(Map.of("topic", "dragons and wizards"));
타입 인터페이스로도 MCP 도구 에이전트를 정의할 수 있어요:
public interface StoryGenerator {
@Agent
String generateStory(@V("topic") String topic);
}
StoryGenerator storyGenerator = McpAgent.builder(mcpClient, StoryGenerator.class)
.toolName("generate_story")
.outputKey("story")
.build();
String story = storyGenerator.generateStory("dragons and wizards");
마지막으로, MCP 도구 에이전트는 @McpClientAgent 애노테이션으로 선언적으로도 정의할 수 있어요. @McpClientSupplier 애노테이션은 McpClient 인스턴스를 제공하는 정적 메서드를 표시해요.