Ollama 통합

Ollama 통합

LangChain4j에서 Ollama를 쓰는 방법을 다룰게요. Ollama는 대규모 언어 모델을 로컬(CPU·GPU 모드)에서 쉽게 세팅하고 실행하게 해주는 AI 도구예요. Llama 2 같은 강력한 모델을 쓸 수 있고, 직접 모델을 커스터마이즈·생성할 수도 있어요. Ollama는 모델 가중치·구성·데이터를 Modelfile로 정의해 하나의 패키지에 묶고, GPU 사용을 포함한 세팅·구성 관련 세부사항을 최적화해 줘요.

Ollama에 대한 자세한 내용은 아래를 참고하세요.

출처: 공식문서

시작하기 (Get started)

시작하려면 프로젝트의 pom.xml에 다음 의존성을 추가해요.


<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama</artifactId>
    <version>1.20.0</version>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>ollama</artifactId>
    <version>1.19.1</version>
</dependency>

Ollama가 testcontainers에서 실행될 때 간단한 채팅 예시 코드를 시도해 볼게요.

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.model.Image;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.util.List;

public class OllamaChatExample {

  private static final Logger log = LoggerFactory.getLogger(OllamaChatExample.class);

  static final String OLLAMA_IMAGE = "ollama/ollama:latest";
  static final String TINY_DOLPHIN_MODEL = "tinydolphin";
  static final String DOCKER_IMAGE_NAME = "tc-ollama/ollama:latest-tinydolphin";

  public static void main(String[] args) {
    // Create and start the Ollama container
    DockerImageName dockerImageName = DockerImageName.parse(OLLAMA_IMAGE);
    DockerClient dockerClient = DockerClientFactory.instance().client();
    List<Image> images = dockerClient.listImagesCmd().withReferenceFilter(DOCKER_IMAGE_NAME).exec();
    OllamaContainer ollama;
    if (images.isEmpty()) {
        ollama = new OllamaContainer(dockerImageName);
    } else {
        ollama = new OllamaContainer(DockerImageName.parse(DOCKER_IMAGE_NAME).asCompatibleSubstituteFor(OLLAMA_IMAGE));
    }
    ollama.start();

    // Pull the model and create an image based on the selected model.
    try {
        log.info("Start pulling the '{}' model ... would take several minutes ...", TINY_DOLPHIN_MODEL);
        Container.ExecResult r = ollama.execInContainer("ollama", "pull", TINY_DOLPHIN_MODEL);
        log.info("Model pulling competed! {}", r);
    } catch (IOException | InterruptedException e) {
        throw new RuntimeException("Error pulling model", e);
    }
    ollama.commitToImage(DOCKER_IMAGE_NAME);

    // Build the ChatModel
    ChatModel model = OllamaChatModel.builder()
            .baseUrl(ollama.getEndpoint())
            .temperature(0.0)
            .logRequests(true)
            .logResponses(true)
            .modelName(TINY_DOLPHIN_MODEL)
            .build();

    // Example usage
    String answer = model.chat("Provide 3 short bullet points explaining why Java is awesome");
    System.out.println(answer);

    // Stop the Ollama container
    ollama.stop();
  }
}

Ollama가 로컬에서 실행된다면 아래 채팅 예시 코드도 시도해 볼 수 있어요.

class OllamaChatLocalModelTest {
  static String MODEL_NAME = "llama3.2"; // try other local ollama model names
  static String BASE_URL = "http://localhost:11434"; // local ollama base url

  public static void main(String[] args) {
      ChatModel model = OllamaChatModel.builder()
              .baseUrl(BASE_URL)
              .modelName(MODEL_NAME)
              .build();
      String answer = model.chat("List top 10 cites in China");
      System.out.println(answer);

      model = OllamaChatModel.builder()
              .baseUrl(BASE_URL)
              .modelName(MODEL_NAME)
              .responseFormat(JSON)
              .build();

      String json = model.chat("List top 10 cites in US");
      System.out.println(json);
    }
}

Ollama가 testcontainers에서 실행될 때 스트리밍 채팅 예시 코드를 시도해 볼게요.

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.model.Image;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
import dev.langchain4j.model.output.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class OllamaStreamingChatExample {

  private static final Logger log = LoggerFactory.getLogger(OllamaStreamingChatExample.class);

  static final String OLLAMA_IMAGE = "ollama/ollama:latest";
  static final String TINY_DOLPHIN_MODEL = "tinydolphin";
  static final String DOCKER_IMAGE_NAME = "tc-ollama/ollama:latest-tinydolphin";

  public static void main(String[] args) {
    DockerImageName dockerImageName = DockerImageName.parse(OLLAMA_IMAGE);
    DockerClient dockerClient = DockerClientFactory.instance().client();
    List<Image> images = dockerClient.listImagesCmd().withReferenceFilter(DOCKER_IMAGE_NAME).exec();
    OllamaContainer ollama;
    if (images.isEmpty()) {
        ollama = new OllamaContainer(dockerImageName);
    } else {
        ollama = new OllamaContainer(DockerImageName.parse(DOCKER_IMAGE_NAME).asCompatibleSubstituteFor(OLLAMA_IMAGE));
    }
    ollama.start();
    try {
        log.info("Start pulling the '{}' model ... would take several minutes ...", TINY_DOLPHIN_MODEL);
        Container.ExecResult r = ollama.execInContainer("ollama", "pull", TINY_DOLPHIN_MODEL);
        log.info("Model pulling competed! {}", r);
    } catch (IOException | InterruptedException e) {
        throw new RuntimeException("Error pulling model", e);
    }
    ollama.commitToImage(DOCKER_IMAGE_NAME);

    StreamingChatModel model = OllamaStreamingChatModel.builder()
            .baseUrl(ollama.getEndpoint())
            .temperature(0.0)
            .logRequests(true)
            .logResponses(true)
            .modelName(TINY_DOLPHIN_MODEL)
            .build();

    String userMessage = "Write a 100-word poem about Java and AI";

    CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();
    model.chat(userMessage, new StreamingChatResponseHandler() {

        @Override
        public void onPartialResponse(String partialResponse) {
            System.out.print(partialResponse);
        }

        @Override
        public void onCompleteResponse(ChatResponse completeResponse) {
            futureResponse.complete(completeResponse);
        }

        @Override
        public void onError(Throwable error) {
            futureResponse.completeExceptionally(error);
        }
    });

    futureResponse.join();
    ollama.stop();
  }
}

Ollama가 로컬에서 실행된다면 아래 스트리밍 채팅 예시 코드도 시도해 볼 수 있어요.

class OllamaStreamingChatLocalModelTest {
  static String MODEL_NAME = "llama3.2"; // try other local ollama model names
  static String BASE_URL = "http://localhost:11434"; // local ollama base url

  public static void main(String[] args) {
      StreamingChatModel model = OllamaStreamingChatModel.builder()
              .baseUrl(BASE_URL)
              .modelName(MODEL_NAME)
              .temperature(0.0)
              .build();
      String userMessage = "Write a 100-word poem about Java and AI";

      CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();
      model.chat(userMessage, new StreamingChatResponseHandler() {

          @Override
          public void onPartialResponse(String partialResponse) {
              System.out.print(partialResponse);
          }

          @Override
          public void onCompleteResponse(ChatResponse completeResponse) {
              futureResponse.complete(completeResponse);
          }

          @Override
          public void onError(Throwable error) {
              futureResponse.completeExceptionally(error);
          }
      });

      futureResponse.join();
  }
}

파라미터

OllamaChatModelOllamaStreamingChatModel 클래스는 빌더 패턴으로 다음 파라미터들로 인스턴스화할 수 있어요.

Parameter Description Type Example
httpClientBuilder See Customizable HTTP Client HttpClientBuilder
baseUrl The base URL of Ollama server. String http://localhost:11434
defaultRequestParameters ChatRequestParameters
modelName The name of the model to use from Ollama server. String
temperature Controls the randomness of the generated responses. Higher values (e.g., 1.0) result in more diverse output, while lower values (e.g., 0.2) produce more deterministic responses. Double
topK Specifies the number of highest probability tokens to consider for each step during generation. Integer
topP Controls the diversity of the generated responses by setting a threshold for the cumulative probability of top tokens. Double
mirostat Integer
mirostatEta Double
mirostatTau Double
repeatLastN Integer
repeatPenalty Penalizes the model for repeating similar tokens in the generated output. Double
seed Sets the random seed for reproducibility of generated responses. Integer
numPredict The number of predictions to generate for each input prompt. Integer
numCtx Integer
stop A list of strings that, if generated, will mark the end of the response. List<String>
minP Double
responseFormat The desired format for the generated output. TEXT or JSON with optional JSON Schema definition ResponseFormat
think Controls thinking. Boolean
truncate Controls what the server does with a prompt that does not fit the context window. See below. Boolean true (server default)
returnThinking Boolean
timeout The maximum time allowed for the API call to complete. Duration PT60S
customHeaders Custom HTTP headers. Map<String, String>
logRequests Boolean
logResponses Boolean
listeners See Chat Model Observability List<ChatModelListener>
supportedCapabilities Set of model capabilities used by AiServices API (only OllamaChatModel supported) Set<Capability> RESPONSE_FORMAT_JSON_SCHEMA
maxRetries The maximum number of retries in case of API call failure. Integer

이 표에서 눈여겨볼 점 하나는 baseUrl 기본값이 http://localhost:11434라는 것과, timeout 기본값이 PT60S라는 거예요. 또 truncate는 서버 기본값이 true라 컨텍스트 창에 안 맞는 프롬프트를 서버가 잘라낸다는 의미예요.

사용 예시

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3.1")
    .temperature(0.8)
    .timeout(Duration.ofSeconds(60))
    .build();

Spring Boot 사용 예시

Ollama용 Spring Boot starter를 가져와요.

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama-spring-boot4-starter</artifactId>
    <version>1.20.0-beta30</version>
</dependency>

:::note 이 starter는 Spring Boot 4가 필요해요. Spring Boot 3에서는 langchain4j-ollama-spring-boot-starter를 쓰면 되고, 자세한 건 Spring Boot Integration 문서를 참고하세요. :::

그런 다음 OllamaChatModel 빈을 구성해요.

langchain4j.ollama.chat-model.base-url=http://localhost:11434
langchain4j.ollama.chat-model.model-name=llama3.1
langchain4j.ollama.chat-model.temperature=0.8
langchain4j.ollama.chat-model.timeout=PT60S

JSON 모드

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3.1")
    .responseFormat(ResponseFormat.JSON)    
    .temperature(0.8)
    .timeout(Duration.ofSeconds(60))
    .build();

구조화된 출력 (Structured Outputs)

빌더로 JSON schema 정의

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3.1")
    .responseFormat(ResponseFormat.builder()
            .type(ResponseFormatType.JSON)
            .jsonSchema(JsonSchema.builder().rootElement(JsonObjectSchema.builder()
                            .addProperty("name", JsonStringSchema.builder().build())
                            .addProperty("capital", JsonStringSchema.builder().build())
                            .addProperty(
                                    "languages",
                                    JsonArraySchema.builder()
                                            .items(JsonStringSchema.builder().build())
                                            .build())
                            .required("name", "capital", "languages")
                            .build())
                    .build())
            .build())
    .temperature(0.8)
    .timeout(Duration.ofSeconds(60))
    .build();

ChatRequest API로 JSON Schema

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3.1")
    .build();

ChatResponse chatResponse = ollamaChatModel.chat(ChatRequest.builder()
        .messages(userMessage("Tell me about Canada."))
        .responseFormat(ResponseFormat.builder()
                .type(ResponseFormatType.JSON)
                .jsonSchema(JsonSchema.builder().rootElement(JsonObjectSchema.builder()
                                .addProperty("name", JsonStringSchema.builder().build())
                                .addProperty("capital", JsonStringSchema.builder().build())
                                .addProperty(
                                        "languages",
                                        JsonArraySchema.builder()
                                                .items(JsonStringSchema.builder().build())
                                                .build())
                                .required("name", "capital", "languages")
                                .build())
                        .build())
                .build())
        .build());

String jsonFormattedResponse = chatResponse.aiMessage().text();

/* jsonFormattedResponse value:

  {
    "capital" : "Ottawa",
    "languages" : [ "English", "French" ],
    "name" : "Canada"
  }

 */

AiServices로 Json Schema

OllamaChatModel을 지원 기능 RESPONSE_FORMAT_JSON_SCHEMA로 만들면, AIService가 인터페이스 반환 값에서 스키마를 자동 생성해요. 자세한 내용은 Structured Outputs를 참고하세요.

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("llama3.1")
    .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)    
    .build();

컨텍스트 창을 초과하는 프롬프트

기본적으로 Ollama는 컨텍스트 창에 안 맞는 프롬프트 부분을 버리고 나머지로 답해요. 응답은 일반 200이고, promptEvalCount는 트림 후의 프롬프트를 세므로 응답도 토큰 사용량도 입력이 손실됐다는 걸 보여주지 않아요.

대신 오류를 받으려면 truncatefalse로 설정해요. 그러면 서버가 HTTP 400으로 요청을 거부하고 프롬프트 크기와 컨텍스트 크기를 모두 보고해요.

ChatModel model = OllamaChatModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("llama3.1:8b")
        .numCtx(4096)
        .truncate(false)
        .build();

이건 멀티턴 에이전트 루프에서 유용한데, 툴 결과마다 대화가 자라고 조용히 줄어든 프롬프트가 실패한 요청보다 더 나쁘기 때문이에요. truncate를 설정하지 않으면 서버 기본값을 유지해요.

Thinking / Reasoning

thinking 기능이 지원되고 다음 파라미터로 제어돼요.

  • think — LLM이 생각하는지, 어떻게 하는지 제어해요.
    • true: LLM이 생각하고 그 생각을 별도의 thinking 필드에 반환해요.
    • false: LLM이 생각하지 않아요.
    • null(설정 안 함): reasoning LLM(예: DeepSeek R1)은 thinking response로 구분된 생각을 실제 응답 앞에 붙여요.
  • returnThinking — API 응답의 thinking 필드를 파싱해서 AiMessage.thinking()에 반환할지, 그리고 OllamaStreamingChatModel을 쓸 때 StreamingChatResponseHandler.onPartialThinking()TokenStream.onPartialThinking() 콜백을 호출할지 제어해요. 기본 비활성.

thinking을 구성하는 예시예요.

ChatModel model = OllamaChatModel.builder()
        .baseUrl("http://localhost:11434")
        .modelName("qwen3:0.6b")
        .think(true)
        .returnThinking(true)
        .build();

사용자 지정 메시지 (Custom Messages)

OllamaChatModelOllamaStreamingChatModel은 표준 채팅 메시지 타입 외에도 사용자 지정 채팅 메시지를 지원해요. 사용자 지정 메시지는 임의의 속성을 가진 메시지를 지정하는 데 쓸 수 있어요. 이건 Granite Guardian처럼 Retrieval-Augmented Generation(RAG)에 쓰이는 검색 컨텍스트를 평가하기 위해 비표준 메시지를 쓰는 모델에 유용해요.

임의 속성을 가진 메시지를 지정하기 위해 CustomMessage를 어떻게 쓰는지 볼게요.

OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
    .baseUrl("http://localhost:11434")
    .modelName("granite3-guardian")
    .build();
 
String retrievedContext = "One significant part of treaty making is that signing a treaty implies recognition that the other side is a sovereign state and that the agreement being considered is enforceable under international law. Hence, nations can be very careful about terming an agreement to be a treaty. For example, within the United States, agreements between states are compacts and agreements between states and the federal government or between agencies of the government are memoranda of understanding.";

List<ChatMessage> messages = List.of(
    SystemMessage.from("context_relevance"),
    UserMessage.from("What is the history of treaty making?"),
    CustomMessage.from(Map.of(
        "role", "context",
        "content", retrievedContext
    ))
);

ChatResponse chatResponse = ollamaChatModel.chat(ChatRequest.builder().messages(messages).build());

System.out.println(chatResponse.aiMessage().text()); // "Yes" (meaning risk detected by Granite Guardian)

더 알아보기