01. Tool Calling
- -
Tool Calling(Tool: 함수)은 특정 시점을 기준으로 학습되서 닫힌 LLM이 외부 세계와 상호작용하거나 특정 작업을 수행할 수 있도록 하는 확장 기능으로 현재의 날씨를 가져오거나 계산을 수행하거나 이메일을 보내는 등의 부가적인 작업을 수행하게 하는 기능이다.
이 기능은 모델이 Tool 기능을 지원 해야 사용 가능하다. qwen3같은 경우는 Tool을 지원하지만 gemma3는 지원하지 않는다.
Overview
Pre-Trained 된 LLM의 한계
기본적으로 LLM은 특정 시점까지 훈련된 내용을 바탕으로 하기 때문에 현실감이 떨어질 수 있다. 따라서 LLM 외부 세계의 정보를 전달해서 현실감을 끌어올릴 필요가 있는데 이때 사용할 수 있는 기능 중 하나가 Tool Calling이다.
이런 경우 사용할 수 있는 기법이 Tool Calling이다. Tool Calling은 크게 2가지 용도로 사용된다.
- 정보 검색: 이 카테고리의 도구는 데이터베이스, 웹서비스, 파일 시스템 등 외부 소스에서 정보를 검색하는데 사용된다. 이를 통해 모델의 지식을 보강하여 원래는 처리할 수 없었던 질문에 대해 답변하게 할 수 있다. 즉 검색 증강 생성(RAG) 시나리오에서 사용된다. 예를 들어 특정 위치의 현재 날씨를 검색하거나 최신 뉴스 기사를 검색하는 경우 등이 이에 해당한다.
- 조치 취하기: 이 카테고리의 도구는 이메일 전송, 양식 제출등과 같은 작업을 수행하는데 사용된다. 즉 사람의 개입이나 명시적인 프로그래밍이 필요한 작업을 자동화 하는 것이 목표이다. 예를 들면 챗봇과 상호작용 하면서 항공편을 예약하거나 웹 페이지의 양식 작성 등의 작업이 가능하다.
기본 흐름
Spring AI는 일관된 방식으로 Tool을 정의, 확인, 실행할 수 있는 추상화 세트를 제공하여 Tool 호출을 지원한다.
- ChatRequest에는 제공하려는 Tool의 정의가 포함된다. Tool의 정의에는 name, description, input schema가 포함된다.
- Model이 도구를 호출하기로 결정하면 정의된 스키마에 따라 Tool의 name과 input parameter들이 포함된 모델의 응답을 애플리케이션에 전달한다.
- 애플리케이션은 Tool의 이름을 이용하여 Tool을 식별하고 input parameter를 이용해 Tool을 실행한다.
- Tool 호출의 결과가 애플리케이션에서 처리된다.
- 애플리케이션은 다시 Tool 호출 결과를 다시 Model에게 전달한다.
- Model은 Tool 호출 결과를 추가해서 최종 응답을 생성한다.
Tool의 정의 방식
Spring AI에서 Tool을 정의할 때는 Method 기반과 Function 기반을 사용할 수 있다.
먼저 메서드 기반의 Tool(Methods as Tools) 방식이 있는데 이는 Java 클래스 내의 메서드에 @Tool 애너테이션을 사용하여 Tool을 정의하는 방식이다.
@Component
public class WeatherService {
@Tool("get_current_weather")
public String getCurrentWeather(String location) {
// 실제 날씨 정보를 가져오는 로직 구현
return "현재 " + location + "의 날씨는 맑음입니다.";
}
}
두 번째 방식인 Functions as Tools는 Function, Supplier 등 함수형 인터페이스를 구현하는 객체를 사용해서 도구를 정의할 수도 있지만 @Tool을 활용하는 것이 매우 쉽다. 이 부분은 뒤에 다룬다.
// 함수 정의
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
public WeatherResponse apply(WeatherRequest request) {
return new WeatherResponse(30.0, Unit.C);
}
}
// 도구로 만들기
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
현재 시각을 기준으로 알람 설정하기(Methods as Tools 방식 활용)
정보 검색을 위한 Tool의 정의
먼저 현재 시각을 가져오는 Tool을 정의해보자. 앞서 살펴봤듯이 모델은 "오늘이 몇 일이지?"와 같은 기본적인 질문에 대해서 답할 수 없다. 학습한 적이 없기 때문이다.
이제 다음과 같이 Tool을 정의해보자. 필요한 정보를 반환하는 메서드를 만들고 @Tool을 지정해주면 된다.
public class DateTimeTools {
@Tool(description = "사용자의 timezone에 해당하는 현재의 날짜와 시간 정보를 가져온다.")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
@Tool은 다음과 같은 속성을 갖는다.
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Tool {
String name() default ""; // Tool의 이름으로 생략 시 메서드 이름
String description() default ""; // Tool에 대한 설명으로 생략 시 메서드의 이름
boolean returnDirect() default false; // 바로 클라이언트에게 반환할지, 모델로 돌려줄지 여부
// 결과에 대한 converter
Class<? extends ToolCallResultConverter> resultConverter() default DefaultToolCallResultConverter.class;
}
- name: name은 Tool의 이름으로 생략 시 메서드의 이름이 사용된다. AI 모델은 Tool을 구분할 때 이 이름을 사용하므로 이름은 반드시 unique 해야 한다.
- description: description을 Tool에 대한 설명이다. 다른 곳에서는 description이 약간 쓸데없는 내용인 경우가 많은데 Tool 설정에서는 매우 중요하다. 왜냐햐면 이 내용을 Model이 읽고 언제 어떻게 해당 툴을 호출할 것인지 이해하기 위한 항목이기 때문이다. 따라서 모델이 잘 이해할 수 있도록 상세히 써주는 것이 좋다. 생략하면 메서드 이름이 사용되기는 하지만 적절치 않은 방식이다.
- returnDirect: Tool의 동작 결과를 바로 클라이언트에게 반환할지, 모델에게 반환할지 결정
Tool 설치
먼저 ChatClient에 Tool을 설치하기 위해서는 tools 메서드를 사용할 수 있다.
private final BasicTools tools;
public String toolGeneration(String userInput) {
return ollamaQwenChatClient.prompt()
.user(userInput)
.advisors(SimpleLoggerAdvisor.builder().order(Ordered.LOWEST_PRECEDENCE - 1).build())
.tools(tools)
.call().content();
}
이제 모델은 DateTimeTools에 대한 내용을 읽고 시간을 가져오기 위해 어떤 메서드를 호출해야 하는지 알게 되었다. 물론 system이나 user 처럼 default 형태로 추가할 수도 있다.
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new DateTimeTools())
.build();
Tool을 이용한 검색 확인
이제 날짜를 가져오는 Tool을 장착한 ChatClient를 만나보자.
조취 취하기
사실 어떤 행동을 취하는 것도 크게 다르지 않다. 메서드를 작성하고 해당 메서드가 어떤 작업을 할 수 있는지 Tool에 잘 선언해두면 된다.
public class DateTimeTools {
. . .
@Tool(description = "ISO-8601 포멧으로 주어지는 시간에 알람을 설정한다.")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zonedAlarmTime = alarmTime.atZone(LocaleContextHolder.getTimeZone().toZoneId());
String osName = System.getProperty("os.name").toLowerCase();
// 실제는 알람을 호출하는 스크립트 호출
System.out.println(osName+"에서 " + zonedAlarmTime +"에 알람을 설정합니다.");
}
}
위 예에서 System.out.println 코드 대시 실제 알람을 호출하는 스크립트를 작성 후 호출해준다면 바로 알람 설정까지 가능하다.
위와 같이 파라미터를 처리하는 경우에는 파라미터에 대한 좀 더 자세한 설명을 할수도 있는데 이때는 @ToolParam을 사용한다.
@Target({ ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface ToolParam {
boolean required() default true; // 파라미터의 필수 여부
String description() default ""; // 파라미터에 대한 설명
}
역시 description 에 충분한 설명을 주는 부분이 중요하다.
Spring AI는 위와 같이 @Tool을 이용하는 방식 외에도 low level에서 FunctionToolCallback을 구현하는 프로그래밍 방식이나 런터임에 확인되는 @Bean으로 동적으로 도구를 지정하는 방식도 제공한다.
Function ToolCallback을 이용한 Tool 정의
Tool Function 정의
먼저 특정 요청에서 특정 응답을 생성할 tool function에 대한 구현체가 필요하다. 다음은 현재 날씨 정보를 가져오는 코드의 예이다. tool function은 Function, Supplier, Consumer, BiFunction 타입을 사용할 수 있다.
class WeatherService implements Function<WeatherRequest, WeatherResponse> {
public WeatherResponse apply(WeatherRequest request) {
if(request.location.equals("seoul")) {
return new WeatherResponse(30.0, Unit.C);
}else if(request.location.equals("newyork")) {
return new WeatherResponse(54.0, Unit.F);
}else {
//throw new RuntimeException("지원하지 않는 지역입니다."); 예외 발생보다는 뭐라고 응답을.
return null;
}
}
}
public enum Unit { C, F }
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}
주의사항! Function 기반으로 Tool을 정의할 때 아직 지원하는 input/output 타입이 한정되어있다. 기본형이나 Wrapper, Optional, Collection type(List, Map,Array...), 비동기 타입(Future, CompletableFuture), Reactive type(Flow, Mono, Flux)들은 지원하지 않는다.
ToolCallback 생성
작성된 tool function은 FunctionToolCallback.build()를 통해 ToolCallback 형태로 변환시킨다. 이때 필요한 내용은 @Tool을 이용했을 때와 다르지 않다. 단지 annotation 방식이 아니라 programming 방식일 뿐이다.
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService()) // 이름과 tool function 전달
.description("특정 위치의 날씨를 반환한다.") // 모델이 잘 알아먹도록 설명해주자.
.inputType(WeatherRequest.class) // 전달되는 파라미터의 타입(필수)
.build();
역시 모델에게 Tool이 할 수 있는 일을 잘 설명할 수 있도록 description을 잘 써주는 과정이 필요하다.
Tool Callback등록하기
Tool Callback을 등록하고 사용하는 과정은 @Tool로 Tool을 만들었을 때와 동일하게 tools() 를 사용하면 된다.
return this.advisedChatClient.prompt()
.system(c -> c.param("language", "Korean").param("character", "Chill한"))
.user(userInput)
.tools(toolCallback) // tool callback 등록
.call()
.content();
@Bean의 활용
마지막 Took 등록 방식으로는 @Bean을 이용할 수 있다. 이 과정은 Tool Callback을 구성하는 과정을 @Bean 기반의 애너테이션 선언으로 대체할 수 있다.
@Configuration
public class WeatherTools {
@Bean
@Description("특정 위치의 날씨를 반환한다.")
Function<WeatherRequest, WeatherResponse> currentWeather() {
return new WeatherService();
}
}
여기서는 빈의 이름이 tool의 이름이 된다. 이 툴을 ChatClient에 등록할 때에는 툴의 이름을 전달하면 된다.
return this.advisedChatClient.prompt()
.system(c -> c.param("language", "Korean").param("character", "Chill한"))
.user(userInput)
.tools("currentWeather") // 빈의 이름을 등록한다.
.call()
.content();
'spring-ai > 04.ToolCalling과 RAG' 카테고리의 다른 글
04.ETL Pipeline (3) | 2025.06.07 |
---|---|
03. RAG와 QuestionAnswerAdvisor (2) | 2025.06.06 |
02. Vector Database (2) | 2025.06.06 |
소중한 공감 감사합니다