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();
}
}
파라미터
OllamaChatModel과 OllamaStreamingChatModel 클래스는 빌더 패턴으로 다음 파라미터들로 인스턴스화할 수 있어요.
| 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는 트림 후의 프롬프트를 세므로 응답도 토큰 사용량도 입력이 손실됐다는 걸 보여주지 않아요.
대신 오류를 받으려면 truncate를 false로 설정해요. 그러면 서버가 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)
OllamaChatModel과 OllamaStreamingChatModel은 표준 채팅 메시지 타입 외에도 사용자 지정 채팅 메시지를 지원해요. 사용자 지정 메시지는 임의의 속성을 가진 메시지를 지정하는 데 쓸 수 있어요. 이건 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)