Spring AI ChatModel 도구 호출

Spring AI ChatModel 도구 호출 (ChatModel Tool Calling)

ChatModel 은 채팅 프로바이더에 대한 저수준 요청/응답 인터페이스예요. 도구 정의를 받아 모델에 보내고 모델의 응답을 반환합니다. 그 응답의 도구 호출은 자동으로 실행되지 않습니다 — 그건 호출자의 책임이에요.

대부분의 애플리케이션에서는 ChatClient 가 권장 진입점입니다. ToolCallingAdvisor 를 통해 도구 호출 루프를 처리하고, 메모리·관측 가능성 advisor와 결합하며, 자동 구성 확장 지점을 지원하니까요. 이 페이지는 의도적으로 저수준 경로를 원하는 사용자를 위한 것입니다.

ChatModel 을 직접 써야 할 때

ChatModel 은 다음과 같은 경우에 적합합니다:

  • advisor 체인이 필요 없을 때 (메모리 advisor, 관측 가능성 advisor, ToolCallingAdvisor 없음).
  • 도구 호출을 커스텀 오케스트레이터(도메인 특화 워크플로 엔진, 비-Spring 에이전트 프레임워크)에 통합하는 경우 — 루프를 직접 소유하니까요.
  • Spring AI 위에 인프라를 구축하는 경우 — 예: 커스텀 ChatClient 구현, 또는 자체 고수준 API를 노출하는 라이브러리.

그 외의 모든 것 — 채팅 애플리케이션, RAG, 에이전틱 워크플로, 도구 중심 assistant — 은 ChatClient 를 사용하세요.

내부 도구 실행 없음

ChatModel 은 도구 호출을 실행하지 않습니다. ChatModel.call(prompt)ChatModel.stream(prompt) 는 도구 호출 요청을 포함한 모델의 원시 응답을 실행 없이 반환합니다. 요청된 도구를 실행하고 결과로 모델을 다시 호출하는 것은 호출자의 책임입니다 — 수동 루프 구동 참고.

Spring AI 1.x에서 각 ChatModel 이 내부 도구 실행 루프를 돌렸던 것과 다릅니다.

ChatModel 에 도구 전달

도구는 ToolCallingChatOptions.toolCallbacks(...) 로 전달됩니다. 이 옵션은 List<ToolCallback> 또는 ToolCallback[] 을 받아요. ToolCallbacks.from(...)@Tool 애노테이션 객체를 콜백으로 변환합니다.

요청별 도구

ChatModel chatModel = ...
ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools());

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .build();

Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
ChatResponse response = chatModel.call(prompt);

이러면 도구 정의가 모델로 전송됩니다. 모델이 도구 호출을 결정하면 응답에 호출 요청이 들어 있고, 직접 실행합니다 (수동 루프 구동 참고).

기본 도구 (모델에 구성)

일부 ChatModel 빌더는 기본 옵션을 받습니다. 기본 옵션에 설정된 도구는 오버라이드하지 않는 한 모든 요청에 적용됩니다:

ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());

ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .options(ToolCallingChatOptions.builder()
        .toolCallbacks(dateTimeTools)
        .build())
    .build();

기본 도구는 이 ChatModel 인스턴스를 통해 발행한 모든 요청에서 모델로 보내집니다. 항상 사용 가능해야 하는 도구에는 편리하지만, 위험할 수도 있어요 — 위험 등급과 파괴적 도구는 기본값이 아니라 요청별로 추가하는 게 일반적입니다.

오버라이드 의미

ChatModel 의 기본 옵션과 요청별 옵션 양쪽에 도구를 설정하면 요청별 도구 목록이 기본값을 완전히 대체합니다:

ChatModel chatModel = OllamaChatModel.builder()
    .options(ToolCallingChatOptions.builder()
        .toolCallbacks(defaultTools)  // 5 default tools
        .build())
    .build();

ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(otherTool)          // 1 tool
    .build();

ChatResponse response = chatModel.call(new Prompt("...", runtimeOptions));
// The model sees ONLY otherTool — defaultTools were replaced, not appended.

단일 요청에서 기본값에 추가 도구를 쓰려면 런타임 옵션에 기본값을 명시적으로 포함하세요:

List<ToolCallback> combined = new ArrayList<>(Arrays.asList(defaultTools));
combined.add(otherTool);

ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(combined)
    .build();

이 오버라이드 동작은 ChatModel API에만 해당됩니다. ChatClient 를 쓰면 호출별 .tools(...).defaultTools(...)추가됩니다 — 두 계층이 자연스럽게 결합되죠.

수동으로 루프 구동

모델이 도구 호출이 있는 응답을 반환하면, 도구를 실행하고 그 결과로 모델을 다시 호출합니다. 이것이 ToolCallingAdvisorChatClient 에서 자동으로 돌리는 루프예요. ChatModel 에서는 직접 작성합니다.

블로킹

ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .build();

Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);
ChatResponse response = chatModel.call(prompt);

while (response.hasToolCalls()) {
    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);

    if (result.returnDirect()) {
        // Tool's returnDirect=true — break out without sending back to the model
        return result.conversationHistory();
    }

    prompt = new Prompt(result.conversationHistory(), chatOptions);
    response = chatModel.call(prompt);
}

String finalAnswer = response.getResult().getOutput().getText();

핵심 구성 요소:

  • ToolCallingManager — 도구 호출을 실행합니다. 기본 DefaultToolCallingManager 는 Spring Boot가 자동 구성합니다. 자동 구성을 안 쓸 때는 ToolCallingManager.builder().build() 로 직접 만드세요.
  • response.hasToolCalls() — 모델이 도구를 하나 이상 요청했으면 true.
  • toolCallingManager.executeToolCalls(prompt, response) — 요청된 각 도구를 찾아 실행하고, 갱신된 대화 기록을 담은 ToolExecutionResult 를 반환합니다.
  • result.returnDirect() — 호출된 도구 전부returnDirect = truetrue.
  • result.conversationHistory() — 원래 메시지에 assistant의 도구 호출 요청과 도구 응답을 더한 것. 다음 프롬프트의 메시지로 사용하세요.

스트리밍

스트리밍 변형은 도구 호출을 확인하기 전에 ChatClientMessageAggregator 로 각 반복의 청크를 집계합니다. 집계하면서 원시 청크 스트림을 다운스트림 구독자(예: SSE 엔드포인트)에 전달할 수 있어요:

ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .build();

Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);

while (true) {
    AtomicReference<ChatResponse> aggregated = new AtomicReference<>();

    new MessageAggregator().aggregate(
        chatModel.stream(prompt).doOnNext(chunk -> forwardToSse(chunk)),
        aggregated::set
    ).blockLast();

    ChatResponse response = aggregated.get();
    if (!response.hasToolCalls()) {
        break;
    }

    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);
    if (result.returnDirect()) {
        break;
    }
    prompt = new Prompt(result.conversationHistory(), chatOptions);
}

완전한 advisor 구성을 갖춘 ChatClient 구동 스트리밍은 ToolCallingAdvisor 문서를 참고하세요.

도구 컨텍스트

도구 메서드에 전달되는 비모델 데이터인 도구 컨텍스트는 ChatModel 에서 ChatClient 와 같은 방식으로 동작합니다. ToolCallingChatOptions 로 설정하세요:

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(ToolCallbacks.from(new CustomerTools()))
    .toolContext(Map.of("tenantId", "acme"))
    .build();

Prompt prompt = new Prompt("Tell me about customer 42", chatOptions);
chatModel.call(prompt);

기본과 런타임 toolContext 둘 다 설정하면 결과 컨텍스트는 둘의 병합입니다 (toolCallbacks 가 대체되는 것과 달라요) — 런타임 항목이 일치하는 키의 기본값보다 우선합니다.

Return Direct

returnDirect 플래그는 ToolCallingManager.executeToolCalls(...) 에서 존중됩니다. 실행 후 ToolExecutionResult.returnDirect() 를 확인하세요:

ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);

if (result.returnDirect()) {
    // Skip the next model call — return the tool result to the caller
    return result.conversationHistory();
}

모델이 단일 반복에서 여러 도구 호출을 요청하면 returnDirect() 는 호출된 도구 전부returnDirect = true 일 때만 true 입니다.

더 보기