Tool Search Tool

Tool Search Tool

AI 에이전트가 Slack, GitHub, Jira, MCP 서버 등 더 많은 서비스에 연결될수록 도구 라이브러리는 빠르게 커져요. 일반적인 다중 서버 구성에서는 대화가 시작되기 전에도 50개가 넘는 도구가 55,000+ 토큰을 소모할 수 있고, 모델이 30개 이상의 비슷한 이름의 도구를 마주하면 도구 선택 정확도도 떨어져요. ToolSearchToolCallingAdvisor는 기본 ToolCallingAdvisor를 점진적 도구 공개(progressive tool disclosure) 패턴 구현으로 교체해서 이 문제를 해결해요.

출처: 문서

본문

Tool Search Tool

AI 에이전트가 Slack, GitHub, Jira, MCP 서버와 같은 더 많은 서비스에 연결되면서 도구 라이브러리는 빠르게 커져요. 일반적인 다중 서버 구성은 대화가 시작되기 전에도 50개가 넘는 도구가 55,000+ 토큰을 쉽게 소모할 수 있어요. 모델이 30개 이상의 비슷한 이름의 도구를 마주하면 도구 선택 정확도도 떨어지고요.

ToolSearchToolCallingAdvisor는 기본 ToolCallingAdvisor를 점진적 도구 공개 패턴 구현으로 교체해 이 문제를 해결해요. 이 패턴에서는 도구 정의를 미리 보내는 대신, 모델에 온디맨드로 점진적으로 노출해요. OpenAI, Anthropic, Gemini에 걸친 벤치마크는 큰 도구 카탈로그에 대한 접근을 유지하면서 34-64%의 토큰 절감을 보여 줘요 — 측정 방법과 방법론은 Smart Tool Selection 블로그 포스트를 참고해 주세요.

이 페이지는 이 advisor와 그것의 ToolIndex 전략, 구성, 그리고 Spring Boot 자동 설정에 대한 참조 문서예요. 더 넓은 도구 호출 아키텍처에서 이 advisor의 개념적 위치는 Scaling to Hundreds of Tools를 참고해 주세요.

동작 원리 (How It Works)

ToolSearchToolCallingAdvisor는 ToolCallingAdvisor를 확장하고 루프의 초기화와 반복별 훅을 재정의해요. 런타임 흐름은 다음과 같아요:

  1. 인덱싱 (Indexing) — 세션 시작 시 모든 등록된 도구가 구성된 ToolIndex에 인덱싱돼요. 모델에는 어떤 도구 정의도 전송되지 않아요.
  2. 초기 요청 (Initial request) — LLM에 대한 첫 요청에는 내장된 toolSearchTool 정의만 포함돼요.
  3. 탐색 호출 (Discovery call) — 모델이 기능이 필요하면 자연어 질의로 toolSearchTool을 호출해요.
  4. 검색 및 확장 (Search & expand) — ToolIndex가 일치하는 도구를 찾고, 그 정의를 다음 반복을 위해 대화에 추가해요.
  5. 도구 호출 (Tool invocation) — 이제 관련 정의를 갖춘 모델이 일반적인 도구 호출을 실행해요.
  6. 도구 실행 (Tool execution) — ToolCallingManager가 발견된 도구를 실행하고 결과를 반환해요.
  7. 응답 (Response) — 모델이 도구 결과를 사용해 최종 답변을 생성해요.

인덱싱된 도구 집합은 세션별로 스코프돼요 (아래 Session Scoping 참고). 동시 대화는 서로 격리된 인덱스를 가져요.

언제 사용할까 (When to Use)

좋은 선택인 경우:

  • ChatClient에 10개 이상의 도구가 등록된 경우.
  • 도구 정의가 요청당 10K 토큰 이상 소모하는 경우.
  • 집계된 도구 카탈로그가 큰 다중 서버 MCP 구성.
  • 큰 도구 집합에서 도구 선택 정확도 문제가 나타나는 경우.

기본 ToolCallingAdvisor를 유지해야 할 때:

  • 도구 라이브러리가 작은 경우 (10개 미만).
  • 모든 도구를 매 세션에서 자주 사용하는 경우.
  • 도구 정의가 매우 컴팩트한 경우 (검색 왕복 비용이 토큰 절감을 초과할 때).

설치 (Installation)

가장 간단한 설정을 위해 Spring Boot starter를 사용해 주세요 (Lucene과 auto-configuration 포함):

  • Maven
  • Gradle
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-tool-search-advisor</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-tool-search-advisor'
}

또는 수동 구성을 위해 라이브러리를 직접 사용하세요:

  • Maven
  • Gradle
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tool-search-advisor</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.ai:spring-ai-tool-search-advisor'
}

빠른 시작 (Quick Start)

가장 빠른 경로는 자동 설정이에요 — 아래 Spring Boot Auto-Configuration을 참고하세요. 수동 연결은 다음과 같아요:

// 1. Configure a ToolIndex (semantic, keyword, or regex)
@Bean
ToolIndex toolIndex(VectorStore vectorStore) {
    return new VectorToolIndex(vectorStore);
}

// 2. Build the advisor
var toolSearchAdvisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .maxResults(5)
    .build();

// 3. Register with ChatClient — tools are indexed but NOT sent to the LLM up front
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(new MyTools())
    .defaultAdvisors(toolSearchAdvisor)
    .build();

// 4. Make a request — supply a session ID via the advisor context
String answer = chatClient.prompt("Help me plan what to wear today in Amsterdam")
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42-session"))
    .call()
    .content();

세션 스코프 (Session Scoping)

ToolSearchToolCallingAdvisor는 도구를 세션별로 인덱싱해요. 세션 ID가 어떤 요청이 어떤 도구 인덱스를 보는지 결정하므로, 멀티 테넌트와 멀티 대화 격리가 가능해져요.

호출자는 모든 요청에 세션 ID를 제공해야 해요. 기본적으로 advisor는 ChatMemory.CONVERSATION_ID 키 아래의 advisor 컨텍스트에서 세션 ID를 읽어요:

chatClient.prompt()
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42-session"))
    .user("...")
    .call()
    .content();

애플리케이션이 이미 다른 키(예: tenantId 또는 userId)로 세션 식별자를 전달한다면, sessionIdKeyName(…)(또는 해당 프로퍼티)으로 조회 키를 변경하세요:

var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .sessionIdKeyName("tenantId")
    .build();

참고: 대화 ID로 메모리 advisor를 구성했다면(MessageChatMemoryAdvisor의 표준 패턴), 동일한 키가 이미 컨텍스트에 있으므로 메모리 설정 덕분에 세션 스코핑을 "공짜로" 얻을 수 있어요.

검색 전략 (Search Strategies)

ToolIndex 인터페이스는 검색 구현을 추상화해요. 기본으로 세 가지 전략이 제공돼요:

전략 (Strategy) 구현 (Implementation) 가장 적합한 경우 (Best for)
시맨틱 (Semantic) VectorToolIndex 자연어 질의, 퍼지 매칭, 새로운 표현 — 호출자가 도구 이름을 말하는 대신 필요한 것을 설명할 때
키워드 (Keyword) LuceneToolIndex 정확한 용어 매칭, 빠른 검색, 알려진 어휘
정규식 (Regex) RegexToolIndex 도구 이름 패턴 (예: get_*_data); 의존성 없는 가벼운 기본값

VectorToolIndex (시맨틱)

임베딩 기반 유사도 검색을 사용해요. 호출자가 자연어로 필요한 것을 설명할 때 가장 적합해요.

@Bean
ToolIndex vectorToolIndex(VectorStore vectorStore) {
    return new VectorToolIndex(vectorStore);
}

VectorStore 빈이 필요해요 (예: spring-ai-starter-vector-store-pgvector 사용). 도구 이름과 설명은 인덱싱 시 임베딩되고, toolSearchTool의 질의도 임베딩되어 상위 K개 일치 항목이 반환돼요.

LuceneToolIndex (키워드)

키워드 기반 검색에 Apache Lucene을 사용해요. 빠르고 임베딩 모델이 필요 없어요.

@Bean
ToolIndex luceneToolIndex() {
    return new LuceneToolIndex();          // default minimum score 0.25
    // return new LuceneToolIndex(0.4f);   // custom minimum score threshold
}

최소 점수 임계값 미만의 히트는 조용히 버려져요. 임계값을 올리면 더 선별적이 되고, 낮추면 더 관대해져요.

RegexToolIndex (패턴)

도구 이름에 대한 정규식 패턴 매칭을 사용해요. 도구 이름이 엄격한 명명 규칙(예: get_*, database)을 따를 때 유용해요. 추가 의존성이 전혀 없어요.

@Bean
ToolIndex regexToolIndex() {
    return new RegexToolIndex();
}

명시적으로 tool-index-type을 구성하지 않았을 때의 기본 인덱스예요.

구성 (Configuration)

ToolSearchToolCallingAdvisor.Builder는 ToolCallingAdvisor.Builder를 확장하고 검색 관련 옵션을 추가해요. 상속된 설정은 ToolCallingAdvisor Builder Options를 참고하세요.

옵션 (Option) 설명 (Description) 기본값 (Default)
toolIndex(ToolIndex) 사용할 검색 구현. 필수 (Required)
maxResults(Integer) toolSearchTool 호출당 반환되는 최대 도구 참조 수. null이면 LLM이 결정해요 (내장 도구 설명은 5를 암시). null
systemMessageSuffix(String) toolSearchTool 사용법을 모델에 지시하기 위해 시스템 메시지에 추가되는 커스텀 프롬프트 접미사. 내장 템플릿 (DEFAULT_SYSTEM_PROMPT_SUFFIX.md 참고)
referenceToolNameAccumulation(boolean) true면 이전 모든 toolSearchTool 호출에서 발견된 도구 이름이 누적되어 주입돼요. false면 가장 최근 턴의 결과만 사용해요 (그 턴의 모든 병렬 toolSearchTool 호출 포함). true
sessionIdKeyName(String) 대화/세션 ID를 조회하는 데 쓰는 advisor 컨텍스트 키. ChatMemory.CONVERSATION_ID
evictionStrategy(ToolIndexEvictionStrategy) 세션 도구 인덱스가 언제 해제되는지 결정해요. Index Eviction 참고. LruEvictionStrategy(1000)

ToolIndex API

ToolIndex 인터페이스와 그 동반 타입들(ToolSearchRequest, ToolSearchResponse, ToolReference)은 spring-ai-tool-search-tool 모듈의 org.springframework.ai.tool.toolsearch 패키지에 있어요. 내장 구현들(LuceneToolIndex, VectorToolIndex, RegexToolIndex)도 이 모듈에 있죠.

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tool-search-tool</artifactId>
</dependency>
public interface ToolIndex {

    void indexTool(String sessionId, ToolReference toolReference);

    /** Default implementation loops over indexTool. */
    void indexTools(String sessionId, List<ToolReference> toolReferences);

    ToolSearchResponse search(ToolSearchRequest request);

    void clearIndex(String sessionId);
}

모든 작업은 sessionId로 스코프돼요. 커스텀 검색 전략이 필요할 때(예: 역할 기반 필터링이 있는 데이터베이스 기반 카탈로그, 또는 캐시된 원격 도구 레지스트리) ToolIndex를 직접 구현하세요.

인덱스 축출 (Index Eviction)

세션별 도구 인덱스는 메모리를 소모해요. ToolIndexEvictionStrategy는 언제 해제할지 결정해요.

기본적으로(LruEvictionStrategy(1000)) 최대 1,000개의 활성 세션이 유지되고, 한도 초과 시 가장 오래 사용되지 않은 세션이 축출돼요. advisor.evictSession(sessionId)을 호출하면 세션을 즉시 해제할 수 있어요 (예: 로그아웃 시).

축출은 각 요청 시 지연 평가(lazily)되므로, 백그라운드 스레드가 필요 없어요.

다섯 가지 내장 전략이 제공돼요:

전략 (Strategy) 동작 (Behavior)
LruEvictionStrategy(maxSessions) (기본값) 활성 세션 수가 maxSessions를 초과하면 가장 오래 사용되지 않은 세션을 축출해요.
NeverEvictStrategy.INSTANCE 자동으로 축출하지 않아요. evictSession()을 명시적으로 호출할 때까지 인덱스가 유지돼요.
AlwaysEvictStrategy.INSTANCE 매 요청 전에 세션의 인덱스를 지워서 매 턴 전체 재인덱싱을 강제해요. 테스트나 매 요청 도구 집합이 바뀌는 경우에 유용해요.
TtlEvictionStrategy(duration) 마지막 접근 시각이 주어진 TTL을 초과한 세션을 축출해요.
CompositeEvictionStrategy(strategies…) 여러 전략에 위임해요. 어떤 대리자라도 축출을 요청하면 세션을 축출해요.
// Default: LRU cap of 1000 sessions — no configuration needed
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .build();

// Never evict — manage session lifetime yourself
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .evictionStrategy(NeverEvictStrategy.INSTANCE)
    .build();

// Always evict — re-index every request (useful for testing)
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .evictionStrategy(AlwaysEvictStrategy.INSTANCE)
    .build();

// LRU with custom cap
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .evictionStrategy(new LruEvictionStrategy(200))
    .build();

// Evict sessions idle for more than 30 minutes
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .evictionStrategy(new TtlEvictionStrategy(Duration.ofMinutes(30)))
    .build();

// Combine: TTL + LRU cap
var advisor = ToolSearchToolCallingAdvisor.builder()
    .toolIndex(toolIndex)
    .evictionStrategy(new CompositeEvictionStrategy(
        new TtlEvictionStrategy(Duration.ofMinutes(30)),
        new LruEvictionStrategy(200)))
    .build();

Spring Boot 자동 설정 (Spring Boot Auto-Configuration)

spring-ai-starter-tool-search-advisor starter는 보일러플레이트 없는 설정을 제공해요. 단일 프로퍼티로 활성화하세요:

spring.ai.chat.client.tool-search-advisor.enabled=true

활성화되면 자동 설정은 다음을 수행해요:

  • ToolCallingAdvisor.Builder<?> 타입의 ToolSearchToolCallingAdvisor.Builder 빈을 등록해요. 기본 빌더의 @ConditionalOnMissingBean 가드 덕분에 이 빈이 기본 ToolCallingAdvisor를 투명하게 대체해요. ChatClient 코드를 바꿀 필요가 없어요. 기본 메커니즘은 Custom ToolAdvisor: Auto-Configuration Integration을 참고하세요.
  • 애플리케이션이 명시적으로 선언하지 않는 한 ToolIndex 빈을 자동 등록해요.

ToolIndex 자동 선택 (ToolIndex Auto-Selection)

구현을 선택하려면 spring.ai.chat.client.tool-search-advisor.tool-index-type을 설정하세요:

값 (Value) 구현 (Implementation) 요구사항 (Requirements)
regex (기본값) RegexToolIndex 추가 의존성 없음
lucene LuceneToolIndex 클래스패스에 org.apache.lucene:lucene-core (starter에 번들돼 있음)
vector VectorToolIndex 애플리케이션 컨텍스트에 VectorStore 빈

애플리케이션이 선언한 커스텀 ToolIndex 빈이 항상 우선해요 — @ConditionalOnMissingBean이 자동 구성된 것을 건너뛰게 해요.

구성 프로퍼티 참조 (Configuration Properties Reference)

Property Description Default
spring.ai.chat.client.tool-search-advisor.enabled advisor 활성화. true면 기본 ToolCallingAdvisor를 대체해요. false
spring.ai.chat.client.tool-search-advisor.tool-index-type ToolIndex 구현: regex, lucene, vector. regex
spring.ai.chat.client.tool-search-advisor.max-results 검색 호출당 반환되는 최대 도구 참조 수. null은 내장 기본값 사용. null
spring.ai.chat.client.tool-search-advisor.system-message-suffix 시스템 메시지에 추가되는 커스텀 프롬프트 접미사. null은 내장 템플릿 사용. null
spring.ai.chat.client.tool-search-advisor.reference-tool-name-accumulation true면 모든 검색 턴에 걸쳐 도구 이름을 누적하고, false면 가장 최근 턴만 유지 (그 안의 모든 병렬 호출 포함). true
spring.ai.chat.client.tool-search-advisor.session-id-key-name 대화/세션 ID를 담는 advisor 컨텍스트 키. chat_memory_conversation_id
spring.ai.chat.client.tool-search-advisor.advisor-order advisor 체인에서 이 advisor의 위치. HIGHEST_PRECEDENCE + 300
spring.ai.chat.client.tool-search-advisor.eviction.lru-max-sessions LRU 축출 전략이 유지하는 최대 활성 세션 수. 1000
spring.ai.chat.client.tool-search-advisor.eviction.ttl 유휴 세션에 대한 TTL. 설정하면 복합 LRU+TTL 전략이 사용돼요. java.time.Duration 문자열을 받아요 (예: 30m, 1h). null
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold 히트가 포함되기 위한 최소 Lucene 점수. tool-index-type=lucene일 때 적용돼요. 0.25

구성 예시 (Example Configurations)

커스텀 임계값과 TTL 축출을 쓰는 Lucene:

spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=lucene
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold=0.4
spring.ai.chat.client.tool-search-advisor.eviction.ttl=30m

벡터 검색 (VectorStore 빈 필요):

spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector

멀티 테넌트 배포용 커스텀 세션 ID 키:

spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector
spring.ai.chat.client.tool-search-advisor.session-id-key-name=tenantId

더 알아보기 (Learn more)