Ai logo

JetBrains AI

Supercharge your tools with AI-powered features inside many JetBrains products

Kotlin Tutorials

Kotlin으로 AI 에이전트 구축하기 – 4부: 위임과 서브 에이전트

Read this post in other languages:

이전 시리즈:

지난 글에서는 추적을 설정하는 과정을 살펴봤습니다. 이제 자연스럽게 두 가지 질문으로 이어집니다. 이 도구가 보여주는 인사이트를 바탕으로, 어떤 실험을 해야 할까요? 그리고 이 관찰 결과를 활용해 에이전트의 어떤 부분을 개선할 수 있을까요?

처음 떠올린 아이디어는 서브 에이전트를 활용한 실험, 구체적으로는 find 서브 에이전트를 사용하는 것입니다. 이를 통해 Koog가 서브 에이전트와 같은 일반 패턴 구현을 어떻게 더 수월하게 만들어주는지 살펴볼 수 있습니다. find 서브 에이전트가 전반적 비용을 줄이면서 성능을 유지하거나, 나아가 성능을 향상할 수 있다고 가설을 세워보겠습니다.

이러한 가설을 설정한 근거는 무엇일까요? 비용 증가의 핵심 요인은 컨텍스트 증가입니다. 각 LLM 요청에는 처음부터 끝까지 전체 컨텍스트가 포함되므로, 이후에 이어지는 각 요청은 이전 요청보다(적어도 입력 토큰 기준으로는) 비용이 늘어납니다. 에이전트 실행 초반에 특히 컨텍스트 증가를 제한할 수 있다면, 비용을 크게 줄일 수 있을 것입니다. 불필요하게 방만한 컨텍스트는 오히려 에이전트의 핵심 과제 수행을 방해할 수 있습니다. 따라서 컨텍스트를 좁히면, 예측은 쉽지 않지만 성능 개선이라는 긍정적인 효과를 기대할 수 있습니다.

특히 find 기능은 장기적 수준의 컨텍스트에서 제거할 때 적합합니다. 실제로 무언가를 찾는 과정에서는 목표와 무관한 파일들까지 여러 개 열어보는 경우가 흔합니다. 그러한 막다른 길을 기억하는 것은 유용하지 않습니다. 반대로, 실제로 발견한 결과를 기억해 두는 것은 가치가 있습니다. 이러한 접근 방식은 에이전트의 기록을 요약하는 매우 자연스러운 방법이기도 합니다(구체적인 압축 기법은 향후 글에서 자세히 다룰 예정입니다).

이 작업은 비교적 단순하기 때문에 서브 에이전트에 위임하기에 적합합니다. 단순한 작업인 만큼, 서브 에이전트가 메인 모델과 다른 별도의 LLM 모델을 사용하도록 구성할 수도 있습니다. 여기에서는 더 빠르고 싼 모델을 선택할 겁니다. 이렇게 하면 일반 압축으로는 얻기 어려운 유연성이 제공됩니다.

물론 기존 방식처럼 일반적인 절차적 도구로도 구현할 수 있습니다. 사실 RegexSearchTool이라는 도구를 하나 만들어 두었지만, 이번 실험의 목적상 이를 main 에이전트에 직접 넣는 대신 find 에이전트 내부에 배치했습니다. 이러한 접근 방식은 모델 선택의 유연성을 제공함과 동시에 에이전트의 지능 수준을 한 단계 더 높여줍니다.

find 에이전트

서브 에이전트 패턴을 구현하기 위해서는 기본적으로 두 번째 에이전트가 구성되어야 합니다. 에이전트 생성에 관한 상세한 내용은 시리즈 1부에서 이미 다루었으므로, 이번에는 긴 시간을 쓰지 않고 몇 가지 세부 사항만 짚고 넘어가겠습니다.

먼저 사소하지만 알아두실 내용은 이 서브 에이전트는 작업이 매우 단순해서 main 에이전트처럼 고성능 모델이 필요 없기 때문에 GPT4.1 Mini를 사용한다는 겁니다.

다음으로, 이 에이전트가 활용할 수 있는 도구를 살펴보는 것도 도움이 됩니다. main 에이전트와 마찬가지로 ListDirectoryToolReadFileTool에는 접근할 수 있지만, EditFileTool이나 ExecuteShellCommandTool에는 접근할 수 없습니다. 또한 앞서 언급한 새로운 절차적 검색 도구인 RegexSearchTool에도 접근 권한을 부여했습니다. 이 도구를 사용하면 하나의 폴더와 그 하위 폴더 전반에 걸쳐 정규식 패턴을 이용한 광범위한 파일 검색이 가능합니다.

ToolRegistry {
    tool(ListDirectoryTool(JVMFileSystemProvider.ReadOnly))
    tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
    tool(RegexSearchTool(JVMFileSystemProvider.ReadOnly))
}

전체 구현 코드가 궁금하시다면 여기에서 자세한 내용을 확인해 보세요.

find 서브 에이전트 구축

먼저 서브 에이전트란 무엇일까요? 서브 에이전트는 사실 매우 단순한 개념으로, 다른 에이전트에 의해 제어되는 에이전트를 의미합니다. 이 구체적인 사례에서는 에이전트를 도구로 사용하는(agent-as-a-tool) 서브 에이전트 패턴을 적용하고 있으며, 이때 서브 에이전트는 main 에이전트에 제공되는 하나의 도구 내부에서 실행됩니다.

서브 에이전트를 만드는 과정은 생각보다 간단합니다. 기본적으로 도구란 에이전트가 호출 시점과 방식을 판단하기 위해 읽을 수 있는 설명자와 쌍으로 묶인 함수입니다. 단순히 도구 하나를 정의하고, 그 도구의 .execute() 함수가 서브 에이전트를 호출하도록 만들 수도 있습니다. 하지만 Koog는 이러한 상용구조차 제거할 수 있는 도구를 제공합니다.

fun createFindAgentTool(): Tool {
    return AIAgentService
        .fromAgent(findAgent as GraphAIAgent)
        .createAgentTool(
            agentName = "__find_in_codebase_agent__",
            agentDescription = """
                
            """.trimIndent(),
            inputDescription = """
                
            """.trimIndent()
        )
}

개념적으로 대략 다음과 같습니다.

public class FindAgentTool(): Tool() {
   override val name: String = "__find_in_codebase_agent__"
   override val description: String = """
      
   """
   @Serializable
   public data class Args(
      @property: LLMDescription(
         """
            
         """
      )
      val input: String
   )
   @Serializable
   public data class Result(
	val output: String
   )
   override suspend fun execute(args: Args): Result = when {
      output = findAgent.run(args.input)
      Result(output)
   }
}

어느 쪽이든 해야 할 일은 다음과 같습니다. 

  1. 서브 에이전트를 생성합니다.
  2. agentName을 지정합니다.
  3. 서브 에이전트를 호출하는 시점agentDescription 프롬프트를 통해 지정합니다.
  4. 에이전트를 호출하는 방법inputDescription 프롬프트를 통해 지정합니다.

프롬프트는 아마도 가장 까다로운 부분일 것입니다. 세밀하게 조정할 여지는 충분히 있습니다. 다만 최신 LLM일수록 정밀하게 조정된 프롬프트가 덜 필요하다는 조사 결과도 있어, 완벽한 세부 조정이 시간 대비 가치가 있는지는 의문입니다. 이 주제는 여전히 탐구 중이며, 명확한 결론에 이르기까지는 추가적인 실험이 더 필요합니다.

프롬프트를 신중하게 설계하지 않으면 main 에이전트가 find 에이전트를 단순한 Ctrl+F/⌘F 기능으로 혼동해, 검색하려는 토큰만 보내는 경우가 있다는 점이 확인되었습니다. 이는 분명 최적의 방식이 아닙니다. 이렇게 맥락이 거의 없는 상태에서는 find 에이전트가 실제로 무엇을 찾아야 하는지 추론할 수 없습니다. 이를 해결하기 위해, 메인 에이전트가 어떤 정보를 찾고 있는지 그 이유를 명시하도록 요구하는 지침을 포함합니다. 이렇게 하면 find 에이전트가 자신의 인텔리전스를 최대한 활용하여 main 에이전트가 실제로 찾으려는 대상을 정확히 찾아낼 수 있습니다.

"""
이 도구는 코드 컨텍스트를 분석하고 이해하여 코드베이스에서 특정 요소를 찾아내는 지능형 마이크로 에이전트를 기반으로 작동합니다.
단순한 텍스트 검색(Ctrl+F / ⌘F)과 달리, 쿼리를 지능적으로 해석해 의도에 가장 잘 부합하는 클래스, 함수, 변수 또는 파일을 찾아냅니다.
이때 검색 대상, 이 정보가 필요한 이유, 검색 범위를 정의하는 절대 경로를 설명하는 상세한 쿼리가 필요합니다.
...
"""
강조 표시가 있는 쿼리(Ctrl+F /⌘F 방식이 아님) 강조 표시가 없는 쿼리(Ctrl+F /⌘F 방식이 아님)
불필요한 joins와 관련해 get_search_results의 변경 사항을 검색하여, 불필요한 joins에 대한 주석이나 로직이 있는지 확인합니다. get_search_results
저장소에서 SKLEARN_ALLOW 또는 이와 유사한 환경 변수의 사용 위치를 검색하여 check_build 우회 가능성을 확인합니다. SKLEARN_ALLOW

가끔 main 에이전트가 find 에이전트가 아닌 grep 명령어로 셸 도구를 호출하는 현상이 발견되었습니다. 이는 전용 서브 에이전트를 구축한 본래의 취지를 저해할 수 있습니다. 이러한 패턴을 피하기 위해, 이를 더 강하게 유도하도록 메인 시스템 프롬프트에 다음 섹션을 추가했습니다.

"""
...
코드 구성 요소나 기타 구문 탐색 시, 직접 검색하는 것보다 리소스 소모가 적은 지능형 find 마이크로 에이전트를 대신 사용할 수 있습니다. 모든 검색 작업에 대해 이 기능을 적극적으로 활용하세요. 검색 작업에 셸 실행을 사용하지 마세요.
...
"""

이것이 바로 앞서 언급한 ‘자연스러운 압축’의 핵심입니다. find 에이전트는 수많은 파일을 열고, 막다른 길을 따라가기도 하며, 코드베이스를 탐색합니다. 반면 main 에이전트에게는 연관성 높은 파일 경로와 스니펫, 설명이라는 결과만 전달됩니다. 모든 탐색 과정은 find 에이전트의 컨텍스트 내에 머물며, 결과 반환 후에는 사라집니다. 그 중 정말로 중요한 내용만이 main 에이전트의 컨텍스트에 추가됩니다.

득과 실

서브 에이전트를 사용하는 것에는 장점이 있지만, 단점 또한 있습니다. 이러한 변화는 과도한 단점 없이 기대하는 이점을 제공하는지 확인하기 위해 실험을 거칠 필요가 있습니다.

가장 먼저 고려해야 할 득과 실은 비용과 시간입니다. 메인 스레드의 컨텍스트를 줄이면 비용과 시간을 절감할 수 있지만, 서브 에이전트에서 여러 LLM 호출이 발생하면 비용과 대기 시간이 발생합니다. 총 비용과 소요 시간이 더 줄어들기를 기대하지만, 이는 main 에이전트가 서브 에이전트를 어떻게 사용하는지에 따라 달라집니다. 만약 작은 쿼리가 대량으로 발생하면 이점이 반감될 수 있습니다. 향후 섹션에서 벤치마크를 실행하면서 비용 측면을 다시 분석할 예정이며, 이때 비용과 시간은 긴밀하게 연결된 요소로 간주합니다.

실제로 일부 실행에서 이러한 현상을 발견하여 이 문제를 main 에이전트에게 설명하고 그러한 작은 쿼리의 대량 발생 빈도를 억제하도록 하는 세그먼트를 도구의 agentDescription에 추가했습니다.

"""
...
이 에이전트는 셸 명령어를 사용하는 방식보다 검색 실행 측면에서 훨씬 경제적이지만, 검색이 반복되는 과정에서 컨텍스트가 단절됩니다. 따라서 이 도구를 여러 번 호출하기보다는 유사한 검색을 한 번의 호출로 묶는 방식을 우선적으로 사용하세요.
...
"""

두 번째 고려해야 할 득과 실은, 이 접근 방식이 인간보다 컨텍스트 유지를 지나치게 이분법적으로 처리한다는 점입니다. 인간은 과거의 모든 일을 항상 기억하지는 않지만, 무슨 일이 있었는지에 대한 흐릿한 감각을 간직하고 있다가 필요할 때 추가 맥락을 검색할 수 있습니다. 이런 행동을 모델링하는 방식도 있지만, 이는 현재 에이전트의 반복 처리 범위를 넘어서는 더 심층적이고 복잡한 에이전트 메모리와 연관됩니다.

또 하나의 문제는 이 접근 방식이 추적의 복잡성을 높인다는 점입니다. Langfuse에서는 더 이상 하나의 에이전트 추적만 확인하면 되는 것이 아닙니다. 실제로는 전체 관점과 개별 에이전트의 관점이라는 여러 시각에서 동작을 살펴봐야 할 수도 있습니다.

더 넓은 관점에서 보기: 엔지니어링 팀에 비유해 보기

이러한 서브 에이전트 활용 기법은 find 에이전트와 같은 단순한 사례에만 국한되지 않습니다. 예를 들어, 분석, 구현, 테스트, 계획을 각각 다른 서브 에이전트에 할당하면 팀 구조에서의 담당 업무 분리를 그대로 재현할 수 있습니다. 

모든 기능을 하나의 에이전트에 집중시키는 방식이 여러 서브 에이전트로 나누는 시스템보다 성능이 더 나은지는 아직 결론이 나지 않았습니다. 다만, 분산 방식의 잠재적 이점은 쉽게 상상해볼 수 있습니다. 콘웨이의 법칙을 생각해 보면 이해가 쉽습니다. “조직은 내부 커뮤니케이션 구조를 반영한 시스템을 만들어 낸다.” 한 가지 해석을 해보자면, 이러한 커뮤니케이션 구조가 유지할 가치가 있는 효율적인 패턴을 발견하기 위해 진화해 왔다는 것입니다. 역 콘웨이 법칙은 이러한 구조가 바람직하다는 점을 시사하기까지 합니다.

그렇다면 역할 분배에 대해서도 동일한 논리가 적용될 수 있을까요? 어쩌면 소프트웨어 팀에서 전문 분야별로 업무가 분담되는 방식은 효율적 업무 방식을 발견하기 위한 진화의 산물일 수 있습니다. 그렇다면 LLM 역시 그로부터 이점을 얻을 수 있을지도 모릅니다.

다만 이는 보장된 것은 아닙니다. 이러한 효율성은 인간의 학습 과정을 분산시키는 데서 주로 비롯된 것일 수 있으며, 이는 LLM에는 그대로 적용되지 않을 수도 있습니다. Clean Code라는 책을 보면 다양한 입장, 즉 작성자 입장(창작자), 독자 입장(유지 관리자), 테스터 입장(테스터)을 고려하는 내용이 나옵니다. 핵심은 다른 입장의 관점에 방해받지 않고 하나의 역할에 집중하는 데 있습니다. 이는 작업 분담이 단순한 학습 효율성의 문제를 넘어서는 개념임을 시사하며, 따라서 LLM에도 실제로 적용 가능함을 의미합니다.

요컨대, 서브 에이전트를 훨씬 더 확장 적용할 수 있지만, 유의미한 접근 방식인지는 아직 입증되지 않았습니다. 현재로서는 여전히 하나의 기법이고, 엄밀히 과학은 아닙니다.

벤치마크 결과: 가설 검증

find 서브 에이전트를 제외한 버전은 총 비용이 약 814달러, 인스턴스당 약 1.63달러였고, 서브 에이전트를 포함한 버전은 총 비용이 약 733달러, 인스턴스당 약 1.47달러로 확인되었습니다. 이는 약 10%의 비용 절감에 해당하며, 충분히 주목할 만한 결과입니다.

또 하나 흥미로운 관찰 결과는, 서브 에이전트에 어떤 LLM을 선택하느냐에 따라 결과가 매우 크게 달라진다는 점입니다. 더 작은 규모의 실험에서, 서브 에이전트를 GPT-5 Codex에 연결한 상태를 유지해 보았으며, 그 결과 비용이 예시당 평균 3.30달러로 급격히 증가했습니다(50개 예시).

실험 성공률 인스턴스당 비용
Part 03(Langfuse) 56%(278/500) 1.63달러(814달러/500)
Part 04(서브 에이전트: GPT-4.1 mini) 58%(290/500) 1.47달러(733달러/500) 
Part 04(서브 에이전트: GPT-5 Codex) 58%(29/50) 3.30달러(165달러/50)

한편, 비용을 줄이기 위한 두 가지 방법을 가설로 설정했다는 점은 주목할 만합니다. 첫 번째는 작업 분배 과정에서 자연스러운 압축을 통해 컨텍스트 크기를 줄이는 것이었고, 두 번째는 작업을 더 저렴한 모델로 오프로딩하는 것이었습니다. 데이터에 따르면, 단순히 서브 에이전트를 분리하는 것만으로는(GPT-5 Codex 모델을 그대로 유지할 경우) 오히려 비용이 크게 증가하는 것으로 나타났습니다. 따라서 첫 번째 방법은 효과가 없는 것으로 보이며, 두 번째 방법인 저가 모델 활용이 실제로 효과를 내는 방식으로 보입니다. 다만 이는 엄밀한 증명이라고 보기는 어렵습니다.

성능 개선 측면에서는, 56%에서 58%로 소폭의 상승이 관찰되었습니다. 이는 통계적으로 허용 가능한 오차 범위 안에 있는 차이일 수도 있지만, 비용을 줄이는 과정에서도 성능이 최소한 일관되게 유지되었다는 점은 고무적입니다.

결론

서브 에이전트를 생성하는 것이 구현 측면에서는 단순하면서도, 잠재적으로는 매우 강력할 수 있다는 점을 확인했습니다. Koog는 이 과정을 한층 더 간소화할 수 있는 편리한 툴링을 제공하며, 사용자는 도구로서의 에이전트(agent-as-a-tool)를 위한 프롬프트만 정의하면 됩니다.

이 기법은 분명히 실질적인 비용 절감 효과를 제공합니다. 그 결과, 약 10%에 가까운 비용 절감을 달성했으며, 이는 수치로 입증되는 분명한 개선이라 할 수 있습니다. 성능에 미친 영향은 덜 명확하지만, 그럼에도 이 부분에서도 일정 수준의 개선이 있는 것으로 보입니다.

동시에, 이러한 유형의 평가는 비용이 많이 듭니다. 비용을 줄였음에도 불구하고, 이 벤치마크의 총비용은 여전히 730달러에 달했습니다. 그래서 다음 글에서는 비용을 낮추기 위한 또 다른 전략인 더 일반적인 압축 접근 방식을 자세히 살펴보겠습니다. 해당 글에서는 컨텍스트가 무한정 확장되어 비용도 함께 증가하는 문제를 방지하는 방법을 중점적으로 다룹니다.

게시물 원문 작성자

Bruno Lannoo

Bruno Lannoo

image description

Discover more