Google AI Gemini 통합

Google AI Gemini 통합

LangChain4j에서 Google AI Gemini 모델을 쓰는 방법을 다룰게요. GoogleAiGeminiChatModel로 채팅, GoogleAiGeminiStreamingChatModel로 스트리밍, GoogleAiGeminiBatchChatModel로 배치 처리를 할 수 있고, 툴(함수 호출)·구조화된 출력·멀티모달·thinking·컨텍스트 캐싱 같은 Gemini 고유 기능도 파라미터로 제어할 수 있어요.

출처: 공식문서

목차

Maven 의존성

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-google-ai-gemini</artifactId>
    <version>1.20.0</version>
</dependency>

API 키

무료 API 키는 여기서 받을 수 있어요: https://ai.google.dev/gemini-api/docs/api-key .

사용 가능한 모델

문서에서 이용 가능한 모델 목록을 확인할 수 있어요.

  • gemini-3-pro-preview
  • gemini-2.5-pro
  • gemini-2.5-flash
  • gemini-2.5-flash-lite
  • gemini-2.0-flash
  • gemini-2.0-flash-lite

GoogleAiGeminiChatModel

일반적인 chat(...) 메서드를 사용할 수 있어요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    ...
    .build();

String response = gemini.chat("Hello Gemini!");

ChatResponse chat(ChatRequest req) 메서드도 있어요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .build();

ChatResponse chatResponse = gemini.chat(ChatRequest.builder()
    .messages(UserMessage.from(
        "How many R's are there in the word 'strawberry'?"))
    .build());

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

구성 (Configuring)

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .httpClientBuilder(...)
    .defaultRequestParameters(...)
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .baseUrl(...)
    .modelName("gemini-2.5-flash")
    .maxRetries(...)
    .temperature(1.0)
    .topP(0.95)
    .topK(64)
    .seed(42)
    .frequencyPenalty(...)
    .presencePenalty(...)
    .maxOutputTokens(8192)
    .timeout(Duration.ofSeconds(60))
    .responseFormat(ResponseFormat.JSON) // or .responseFormat(ResponseFormat.builder()...build()) 
    .stopSequences(List.of(...))
    .toolConfig(GeminiFunctionCallingConfig.builder()...build()) // or below
    .toolConfig(GeminiMode.ANY, List.of("fnOne", "fnTwo"))
    .allowCodeExecution(true)
    .includeCodeExecution(true)
    .logRequestsAndResponses(true)
    .safetySettings(List<GeminiSafetySetting> or Map<GeminiHarmCategory, GeminiHarmBlockThreshold>)
    .thinkingConfig(...)
    .returnThinking(true)
    .sendThinking(true)
    .responseLogprobs(...)
    .logprobs(...)
    .enableEnhancedCivicAnswers(...)
    .mediaResolution(GeminiMediaResolutionLevel.MEDIA_RESOLUTION_HIGH)
    .mediaResolutionPerPartEnabled(true)
    .listeners(...)
    .supportedCapabilities(...)
    .build();

기본 요청 파라미터 (Default Request Parameters)

위에 나온 개별 빌더 메서드 대신(또는 추가로) defaultRequestParameters(...)ChatRequestParameters 객체 하나를 넘길 수 있어요. 이 파라미터들은 개별 ChatRequest의 파라미터로 덮어쓰지 않는 한 모델이 내는 모든 요청에 적용돼요.

공통 ChatRequestParameters 또는 Gemini 전용 GoogleAiGeminiChatRequestParameters를 넘길 수 있어요. 후자는 aspectRatioimageSize 같은 Gemini 전용 옵션을 추가로 노출해요.

GoogleAiGeminiChatRequestParameters parameters = GoogleAiGeminiChatRequestParameters.builder()
    .modelName("gemini-2.5-flash")
    .temperature(1.0)
    .maxOutputTokens(8192)
    .aspectRatio("16:9") // Gemini-specific
    .imageSize("2K")     // Gemini-specific
    .build();

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .defaultRequestParameters(parameters)
    .build();

같은 파라미터를 defaultRequestParameters(...)와 개별 빌더 메서드(예: modelName(String)) 양쪽에 설정하면, 개별 빌더 메서드로 설정한 값이 우선해요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .defaultRequestParameters(GoogleAiGeminiChatRequestParameters.builder()
        .modelName("gemini-2.5-flash")
        .temperature(1.0)
        .build())
    .temperature(0.0) // overrides temperature from defaultRequestParameters
    .build();
// effective parameters: modelName=gemini-2.5-flash, temperature=0.0

GoogleAiGeminiStreamingChatModel

GoogleAiGeminiStreamingChatModel은 응답 텍스트를 토큰 단위로 스트리밍할 수 있게 해줘요. 응답은 StreamingChatResponseHandler로 처리해야 해요.

StreamingChatModel gemini = GoogleAiGeminiStreamingChatModel.builder()
        .apiKey(System.getenv("GEMINI_AI_KEY"))
        .modelName("gemini-2.5-flash")
        .build();

CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();

gemini.chat("Tell me a joke about Java", 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();

안전 설정과 안전 등급 (Safety Settings and Safety Ratings)

Gemini는 보내는 프롬프트와 생성하는 콘텐츠를 HARM_CATEGORY_HARASSMENTHARM_CATEGORY_DANGEROUS_CONTENT 같은 유해 카테고리 집합에 대해 검사해요.

검사가 얼마나 엄격할지는 모델 빌더의 safetySettings(...)로 제어해요.

ChatModel model = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .safetySettings(Map.of(
        GeminiHarmCategory.HARM_CATEGORY_HARASSMENT, GeminiHarmBlockThreshold.BLOCK_ONLY_HIGH,
        GeminiHarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, GeminiHarmBlockThreshold.BLOCK_LOW_AND_ABOVE))
    .build();

Gemini는 응답에 그 검사 결과를 보고해요. 읽으려면 ChatResponse.metadata()GoogleAiGeminiChatResponseMetadata로 캐스팅해요.

ChatResponse chatResponse = model.chat(ChatRequest.builder()
    .messages(UserMessage.from("Hello!"))
    .build());

var metadata = (GoogleAiGeminiChatResponseMetadata) chatResponse.metadata();

// how the generated content was rated
for (GeminiSafetyRating rating : metadata.safetyRatings()) {
    System.out.println(rating.category() + " -> " + rating.probability());
}

세 가지 정보를 얻을 수 있어요.

Method Meaning
safetyRatings() How the generated content was rated, one entry per harm category. Empty if none.
promptSafetyRatings() How your prompt was rated. Empty if none.
blockReason() Why Gemini refused the prompt outright, or null if it did not.

Gemini가 프롬프트를 거부하면 콘텐츠를 전혀 반환하지 않아요. 이 경우 blockReason()이 설정되고(예: "SAFETY", "PROHIBITED_CONTENT", "BLOCKLIST"), AiMessage는 텍스트가 없으며 finishReason()CONTENT_FILTER가 돼요.

var metadata = (GoogleAiGeminiChatResponseMetadata) chatResponse.metadata();

if (metadata.blockReason() != null) {
    System.out.println("Prompt was rejected: " + metadata.blockReason());
    metadata.promptSafetyRatings().forEach(rating ->
            System.out.println("  " + rating.category() + ": " + rating.probability()));
}

GeminiSafetyRating.category()probability()는 raw API 값을 담은 일반 String이라, 나중에 Google이 새로 도입하는 유해 카테고리도 애플리케이션을 깨지 않고 그대로 통과돼요.

같은 데이터는 GoogleAiGeminiStreamingChatModel(onCompleteResponse에 전달된 ChatResponse에서)과 GoogleAiGeminiBatchChatModel에서도 사용할 수 있어요.

툴 (Tools)

Tools(일명 Function Calling)는 병렬 호출을 포함해 지원돼요. 하나 이상의 ToolSpecification으로 구성할 수 있는 ChatRequest를 받는 chat(ChatRequest) 메서드를 쓰거나, LangChain4j의 AiServices로 정의할 수 있어요.

AiServices로 날씨 툴을 만드는 예시예요.

record WeatherForecast(
    String location,
    String forecast,
    int temperature) {}

class WeatherForecastService {
    @Tool("Get the weather forecast for a location")
    WeatherForecast getForecast(
        @P("Location to get the forecast for") String location) {
        if (location.equals("Paris")) {
            return new WeatherForecast("Paris", "sunny", 20);
        } else if (location.equals("London")) {
            return new WeatherForecast("London", "rainy", 15);
        } else if (location.equals("Tokyo")) {
            return new WeatherForecast("Tokyo", "warm", 32);
        } else {
            return new WeatherForecast("Unknown", "unknown", 0);
        }
    }
}

interface WeatherAssistant {
    String chat(String userMessage);
}

WeatherForecastService weatherForecastService =
    new WeatherForecastService();

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .temperature(0.0)
    .build();

WeatherAssistant weatherAssistant =
    AiServices.builder(WeatherAssistant.class)
        .chatModel(gemini)
        .tools(weatherForecastService)
        .build();

String tokyoWeather = weatherAssistant.chat(
        "What is the weather forecast for Tokyo?");

System.out.println("Gemini> " + tokyoWeather);
// Gemini> The weather forecast for Tokyo is warm
//         with a temperature of 32 degrees.

$ref, $defs 또는 raw JSON Schema를 쓰는 툴 파라미터

툴 파라미터는 보통 Gemini의 parameters 필드로 설명하는데, 이 필드는 고정된 스키마 키워드 집합을 이해해요. 표준 JSON Schema는 이것보다 더 나아가서, $ref$defs로 문서의 한 부분이 다른 부분을 가리킬 수 있고 parameters에는 자리가 없는 minimum·maximum 같은 키워드도 있어요.

툴 파라미터에 그런 것이 포함되면 LangChain4j는 대신 parametersJsonSchema(plain JSON Schema를 받는 Gemini 필드)로 보내서 스키마가 변경 없이 API에 도달해요. 설정하거나 켤 건 아무것도 없어요.

JsonObjectSchema priceRange = JsonObjectSchema.builder()
        .addNumberProperty("min")
        .addNumberProperty("max")
        .build();

ToolSpecification searchProducts = ToolSpecification.builder()
        .name("search_products")
        .description("Search the catalog")
        .parameters(JsonObjectSchema.builder()
                .definitions(Map.of("PriceRange", priceRange))
                .addStringProperty("query")
                // a reference to the definition above, resolved by Gemini
                .addProperty("retail_price", JsonReferenceSchema.builder()
                        .reference("PriceRange")
                        .build())
                // a fragment of JSON Schema, sent exactly as written
                .addProperty("max_results", JsonRawSchema.from(
                        "{\"type\":\"integer\",\"minimum\":1,\"maximum\":50}"))
                .required("query")
                .build())
        .build();

이건 손으로 쓰는 스키마에만 국한되지 않아요. LangChain4j가 대신 만들어 주는 툴도 포함해요. 파라미터 타입이 자기 자신을 참조하는 @Tool 메서드나, 스키마에 $ref를 쓰는 MCP 툴 같은 것요.

응답 스키마도 같은 방식으로 처리돼요. Raw Response Schema를 참고하세요.

:::note Gemini는 $schema 키워드를 거부해요. 스키마 생성기가 만들어낸 문서는 보통 "$schema": "https://json-schema.org/draft/2020-12/schema"로 시작하므로, 문서를 JsonRawSchema에 넘기기 전에 그 줄을 빼야 해요. 빼지 않으면 요청이 400으로 실패해요. :::

구조화된 출력 (Structured Outputs)

Structured Outputs에 대한 자세한 내용은 여기를 참고하세요.

자유 형식 텍스트에서 타입 안전한 데이터 추출

대규모 언어 모델은 비구조화 텍스트에서 구조화된 정보를 추출하는 데 탁월해요. 다음 예시에서는 AiServices 덕분에 날씨 예보 텍스트에서 타입 안전한 WeatherForecast 객체를 가져와요.

// A type-safe / strongly-typed object 
// representing the weather forecast

record WeatherForecast(
    @Description("minimum temperature")
    Integer minTemperature,
    @Description("maximum temperature")
    Integer maxTemperature,
    @Description("chances of rain")
    boolean rain
) { }

// An interface contract, to interact with Gemini

interface WeatherForecastAssistant {
    WeatherForecast extract(String forecast);
}

// Let's extract the data:

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // this is required to enable structured outputs feature
    .build();

WeatherForecastAssistant forecastAssistant =
    AiServices.builder(WeatherForecastAssistant.class)
        .chatModel(gemini)
        .build();

WeatherForecast forecast = forecastAssistant.extract("""
    Morning: The day dawns bright and clear in Osaka, with crisp
    autumn air and sunny skies. Expect temperatures to hover
    around 18°C (64°F) as you head out for your morning stroll
    through Namba.
    Afternoon: The sun continues to shine as the city buzzes with
    activity. Temperatures climb to a comfortable 22°C (72°F).
    Enjoy a leisurely lunch at one of Osaka's many outdoor cafes,
    or take a boat ride on the Okawa River to soak in the beautiful
    scenery.
    Evening: As the day fades, expect clear skies and a slight chill
    in the air. Temperatures drop to 15°C (59°F). A cozy dinner at a
    traditional Izakaya will be the perfect way to end your day in
    Osaka.
    Overall: A beautiful autumn day in Osaka awaits, perfect for
    exploring the city's vibrant streets, enjoying the local cuisine,
    and soaking in the sights.
    Don't forget: Pack a light jacket for the evening and wear
    comfortable shoes for all the walking you'll be doing.
    """);

Response Format / Response Schema

GoogleAiGeminiChatModel을 만들 때나 호출할 때 ResponseFormat을 지정할 수 있어요. 특히 Json 형식의 경우, 해당 자바 객체를 만들어 프로그래밍 방식으로 스키마를 정의하거나 raw json schema를 제공할 수 있어요.

Response Schema

GoogleAiGeminiChatModel을 만들 때 레시피용 JSON schema를 정의하는 예시를 볼게요. 이 예시에서는 JsonObjectSchema 클래스로 json schema를 선언해요.

ResponseFormat responseFormat = ResponseFormat.builder()
        .type(ResponseFormatType.JSON)
        .jsonSchema(JsonSchema.builder() // see [1] below
                .rootElement(JsonObjectSchema.builder()
                        .addStringProperty("title")
                        .addIntegerProperty("preparationTimeMinutes")
                        .addProperty("ingredients", JsonArraySchema.builder()
                                .items(new JsonStringSchema())
                                .build())
                        .addProperty("steps", JsonArraySchema.builder()
                                .items(new JsonStringSchema())
                                .build())
                        .build())
                .build())
        .build();

ChatModel gemini = GoogleAiGeminiChatModel.builder()
        .apiKey(System.getenv("GEMINI_AI_KEY"))
        .modelName("gemini-2.5-flash")
        .responseFormat(responseFormat)
        .build();

String recipeResponse = gemini.chat("Suggest a dessert recipe with strawberries");

System.out.println(recipeResponse);

참고:

  • [1] - JsonSchemaJsonSchemas.jsonSchemaFrom() 헬퍼 메서드로 클래스에서 자동 생성할 수 있어요.
JsonSchema jsonSchema = JsonSchemas.jsonSchemaFrom(TripItinerary.class).get();

GoogleAiGeminiChatModel을 호출할 때 레시피용 JSON schema를 정의하는 예시도 볼게요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
        .apiKey(System.getenv("GEMINI_AI_KEY"))
        .modelName("gemini-2.5-flash")
        .build();

ResponseFormat responseFormat = ...;

ChatRequest chatRequest = ChatRequest.builder()
        .messages(UserMessage.from("Suggest a dessert recipe with strawberries"))
        .responseFormat(responseFormat)
        .build();

ChatResponse chatResponse = gemini.chat(chatRequest);

System.out.println(chatResponse.aiMessage().text());

Raw Response Schema

또 다른 예시로, JsonRawSchema 클래스를 사용해 Gemini API의 responseJsonSchema로 raw JSON schema를 제공하는 방법을 볼 수 있어요. Gemini API의 지원 타입만 사용하도록 주의하세요. 응답 스키마 안 어디에 JsonRawSchemaJsonReferenceSchema가 있으면 같은 필드가 사용되므로, 여기서도 $ref$defs가 동작해요.

String rawSchema = """
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "birthDate": {
      "type": "string",
      "format": "date"
    },
    "preferredContactTime": {
      "type": "string",
      "format": "time"
      },
    "height": {
      "type": "number",
      "minimum": 1.83,
      "maximum": 1.88
    },
    "role": {
      "type": "string",
      "enum": ["developer", "maintainer", "researcher"]
    },
    "isAvailable": { "type": "boolean" },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "maxItems": 5
    },
    "address": {
      "type": "object",
      "properties": {
        "city": { "type": "string" },
        "streetName": { "type": "string" },
        "streetNumber": { "type": "string" }
      },
      "required": ["city", "streetName", "streetNumber"],
      "additionalProperties": true
    }
  },
  "required": ["name", "birthDate", "height", "role", "tags", "address"]
}
""";

JsonRawSchema jsonRawSchema = JsonRawSchema.builder().schema(rawSchema).build();
JsonSchema jsonSchema = JsonSchema.builder().rootElement(jsonRawSchema).build();
        
ResponseFormat responseFormat = ResponseFormat.builder()
        .type(ResponseFormatType.JSON)
        .jsonSchema(jsonSchema)
        .build();

GoogleAiGeminiChatModel gemini = GoogleAiGeminiChatModel.builder()
        .apiKey(GOOGLE_AI_GEMINI_API_KEY)
        .modelName("gemini-2.5-flash-lite")
        .logRequests(true)
        .logResponses(true)
        .responseFormat(responseFormat)
        .build();
        
UserMessage userMessage = UserMessage.from(
        """
           Tell me about a detective named Sherlock Holmes,
           who was born on November 28 1852 and sees the world over six feet from the ground.
           He is a trouble-seeker, an active volunteer and lives in London at 221B Baker Street.
           He plays the violin and he likes to conduct various physics and chemistry experiments.
           He accepts clients or prefers to be contacted at 09:00am.
           """);

ChatResponse response = gemini.chat(ChatRequest.builder()
        .messages(userMessage)
        .build());

JSON 모드

Gemini가 JSON으로 답하도록 강제할 수 있어요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .responseFormat(ResponseFormat.JSON)
    .build();

String roll = gemini.chat("Roll a 6-sided dice");

System.out.println(roll);
// {"roll": "3"}

시스템 프롬프트로 JSON 출력이 어떻게 생겨야 하는지 더 자세히 설명할 수도 있어요. Gemini는 보통 제안된 스키마를 따르지만 보장되지는 않아요. JSON schema 적용을 보장받으려면 이전 섹션에서 설명한 것처럼 response format을 정의해야 해요.

Python 코드 실행

함수 호출을 넘어, Google AI Gemini는 샌드박스 환경에서 Python 코드를 만들고 실행하게 해줘요. 더 고급 계산이나 로직이 필요한 상황에서 특히 흥미로워요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .allowCodeExecution(true)
    .includeCodeExecutionOutput(true)
    .build();

빌더 메서드가 2개 있어요.

  • allowCodeExecution(true) — Gemini가 Python 코딩을 할 수 있다는 걸 알려줘요.
  • includeCodeExecutionOutput(true) — Gemini가 생각해낸 실제 Python 스크립트와 그 실행 출력을 보고 싶을 때 써요.
ChatResponse mathQuizz = gemini.chat(
    SystemMessage.from("""
        You are an expert mathematician.
        When asked a math problem or logic problem,
        you can solve it by creating a Python program,
        and execute it to return the result.
        """),
    UserMessage.from("""
        Implement the Fibonacci and Ackermann functions.
        What is the result of `fibonacci(22)` - ackermann(3, 4)?
        """)
);

Gemini가 Python 스크립트를 만들고 서버에서 실행해 결과를 돌려줘요. 코드와 실행 출력을 보여달라고 했으므로, 답은 다음과 같이 생겨요.

Code executed:
```python
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

def ackermann(m, n):
    if m == 0:
        return n + 1
    elif n == 0:
        return ackermann(m - 1, 1)
    else:
        return ackermann(m - 1, ackermann(m, n - 1))

print(fibonacci(22) - ackermann(3, 4))
```
Output:
```
17586
```
The result of `fibonacci(22) - ackermann(3, 4)` is **17586**.

I implemented the Fibonacci and Ackermann functions in Python.
Then I called `fibonacci(22) - ackermann(3, 4)` and printed the result.

코드/출력을 요청하지 않았다면 다음 텍스트만 받았을 거예요.

The result of `fibonacci(22) - ackermann(3, 4)` is **17586**.

I implemented the Fibonacci and Ackermann functions in Python.
Then I called `fibonacci(22) - ackermann(3, 4)` and printed the result.

멀티모달 (Multimodality)

Gemini는 멀티모달 모델이라 텍스트 외에도 다양한 modality(양식)를 받고 생성할 수 있어요.

입력 Modalities

입력으로 Gemini는 다음을 받아요.

  • 사진(ImageContent)
  • 비디오(VideoContent)
  • 오디오 파일(AudioContent)
  • PDF 파일(PdfFileContent)

아래 예시는 텍스트 프롬프트와 이미지를 섞는 방법을 보여줘요.

// PNG of the cute colorful parrot mascot of the LangChain4j project
String base64Img = b64encoder.encodeToString(readBytes(
  "https://avatars.githubusercontent.com/u/132277850?v=4"));

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .build();

ChatResponse response = gemini.chat(
    UserMessage.from(
        ImageContent.from(base64Img, "image/png"),
        TextContent.from("""
            Do you think this logo fits well
            with the project description?
            """)
    )
);

이미지 생성 출력

일부 Gemini 모델(예: gemini-2.5-flash-image)은 응답의 일부로 이미지를 생성할 수 있어요. 이미지가 생성되면 AiMessage 속성에 저장되고 GeneratedImageHelper 유틸리티 클래스로 접근할 수 있어요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey("Your API Key")
    .modelName("gemini-2.5-flash-image")
    .build();

ChatResponse response = gemini.chat(UserMessage.from("A high-resolution, studio-lit product photograph of a minimalist ceramic coffee mug in matte black"));

// Extract generated images from the response
AiMessage aiMessage = response.aiMessage();
List<Image> generatedImages = GeneratedImageHelper.getGeneratedImages(aiMessage);

if (GeneratedImageHelper.hasGeneratedImages(aiMessage)) {
    System.out.println("Generated " + generatedImages.size() + " image(s)");
    System.out.println("Text response: " + aiMessage.text());

    for (Image image : generatedImages) {
        String base64Data = image.base64Data();
        String mimeType = image.mimeType();
        
        // You can now save the image, display it, or process it further
        // For example, save to file:
        byte[] imageBytes = Base64.getDecoder().decode(base64Data);
        Files.write(Paths.get("generated_image.png"), imageBytes);
    }
} else {
    System.out.println("Text response: " + aiMessage.text());
}

미디어 해상도 (Media Resolution)

모델로 보내는 미디어(이미지, 비디오, PDF)의 해상도를 제어할 수 있어요. 전역으로 또는 파트별(이미지별)로 할 수 있어요.

전역 미디어 해상도

요청의 모든 미디어 파트에 미디어 해상도를 설정하려면 .mediaResolution() 빌더 메서드를 써요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .mediaResolution(GeminiMediaResolutionLevel.MEDIA_RESOLUTION_LOW) // or MEDIUM, HIGH, ULTRA_HIGH, UNSPECIFIED
    .build();

파트별 미디어 해상도 (Gemini 3)

Gemini 3에서는 ImageContentDetailLevel로 개별 이미지의 해상도를 지정할 수 있어요. 먼저 빌더에서 이 기능을 켠 다음 ImageContent에 디테일 레벨을 설정해요.

ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-3-pro-preview")
    .mediaResolutionPerPartEnabled(true)
    .build();

ChatResponse response = gemini.chat(
    UserMessage.from(
        ImageContent.from(url1, ImageContent.DetailLevel.LOW),
        ImageContent.from(url2, ImageContent.DetailLevel.HIGH),
        TextContent.from("Compare these two images")
    )
);

지원되는 DetailLevel 값과 Gemini 해상도 레벨 매핑:

  • LOW -> MEDIA_RESOLUTION_LOW
  • MEDIUM -> MEDIA_RESOLUTION_MEDIUM
  • HIGH -> MEDIA_RESOLUTION_HIGH
  • ULTRA_HIGH -> MEDIA_RESOLUTION_ULTRA_HIGH (Highest token count, required for specific use cases such as computer use)
  • AUTO -> MEDIA_RESOLUTION_UNSPECIFIED

Thinking

GoogleAiGeminiChatModelGoogleAiGeminiStreamingChatModel 둘 다 thinking을 지원해요.

다음 파라미터도 thinking 동작을 제어해요.

  • GeminiThinkingConfig.includeThoughtsthinkingBudget — thinking을 켜요. 자세한 내용은 여기를 참고하세요.
  • returnThinking — thinking(있다면)을 AiMessage.thinking()에 반환할지, 그리고 GoogleAiGeminiStreamingChatModel을 쓸 때 StreamingChatResponseHandler.onPartialThinking()TokenStream.onPartialThinking() 콜백을 호출할지 제어해요. 기본 비활성. 켜면 thinking 시그니처도 AiMessage.attributes()에 저장돼 반환돼요.
  • sendThinkingAiMessage에 저장된 thinking과 시그니처를 후속 요청에서 LLM에 보낼지 제어해요. 기본 비활성.

:::note returnThinking이 설정되지 않고(null) thinkingConfig가 설정되면, thinking 텍스트가 AiMessage.text() 필드 안의 실제 응답 앞에 붙고, StreamingChatResponseHandler.onPartialThinking() 대신 StreamingChatResponseHandler.onPartialResponse()가 호출돼요. :::

thinking을 구성하는 예시예요.

GeminiThinkingConfig thinkingConfig = GeminiThinkingConfig.builder()
        .includeThoughts(true)
        .thinkingBudget(250)
        .build();

ChatModel model = GoogleAiGeminiChatModel.builder()
        .apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
        .modelName("gemini-2.5-flash")
        .thinkingConfig(thinkingConfig)
        .returnThinking(true)
        .sendThinking(true)
        .build();

Gemini 3 Pro

Gemini 3 Pro에서 thinking 구성은 _thinking level_을 도입하는데, "low" 또는 "high"(기본값 high)예요. thinking 구성 안에서 레벨을 설정할 수 있어요.

GoogleAiGeminiChatModel modelHigh = GoogleAiGeminiChatModel.builder()
        .modelName("gemini-3-pro-preview")
        .apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
        .thinkingConfig(GeminiThinkingConfig.builder()
                .thinkingLevel(LOW) // or HIGH
                .build())
        .sendThinking(true)
        .returnThinking(true)
        .build();

문자열 "high"/"low" 또는 GeminiThinkingConfig.GeminiThinkingLevel.HIGH/GeminiThinkingConfig.GeminiThinkingLevel.LOW enum 값을 넘길 수 있어요.

Gemini 3 Pro를 쓸 때는 thought signatures가 모델에 제대로 전달되도록 sendThinking()returnThinking()true로 구성하는 게 필수예요.

Gemini Files API

Gemini Files API는 Gemini 모델에 쓸 미디어 파일을 업로드하고 관리하게 해줘요. 총 요청 크기가 20MB를 넘을 때 특히 유용한데, 파일을 따로 업로드하고 콘텐츠 생성 요청에서 참조할 수 있기 때문이에요.

주요 기능

  • 멀티모달 지원: 이미지, 오디오, 비디오, 문서 업로드
  • 저장: 파일은 48시간 동안 저장돼요.
  • 용량: 프로젝트당 최대 20GB, 개별 파일당 최대 2GB
  • 무료: Files API는 요금이 없어요.

파일 업로드

두 가지 방법으로 파일을 업로드할 수 있어요.

파일 경로에서:

GeminiFiles filesApi = GeminiFiles.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .build();

// Upload from a file path
Path filePath = Paths.get("path/to/your/file.pdf");
GeminiFile uploadedFile = filesApi.uploadFile(filePath, "My Document");

System.out.println("File uploaded: " + uploadedFile.name());
System.out.println("File URI: " + uploadedFile.uri());

바이트 배열에서:

byte[] fileBytes = Files.readAllBytes(Paths.get("path/to/file.jpg"));
GeminiFile uploadedFile = filesApi.uploadFile(
    fileBytes,
    "image/jpeg",
    "My Image"
);

파일 관리

업로드된 파일 모두 나열:

List<GeminiFile> files = filesApi.listFiles();
for (GeminiFile file : files) {
    System.out.println("File: " + file.displayName() + " (" + file.name() + ")");
}

파일 메타데이터 가져오기:

GeminiFile file = filesApi.getMetadata("files/abc123");
System.out.println("File size: " + file.sizeBytes() + " bytes");
System.out.println("MIME type: " + file.mimeType());
System.out.println("Created: " + file.createTime());
System.out.println("Expires: " + file.expirationTime());

파일 삭제:

filesApi.deleteFile("files/abc123");
System.out.println("File deleted successfully");

파일 상태 (File States)

파일은 수명 주기 동안 다른 상태에 있을 수 있어요.

GeminiFile file = filesApi.getMetadata("files/abc123");

if (file.isActive()) {
    System.out.println("File is ready to use");
} else if (file.isProcessing()) {
    System.out.println("File is still being processed");
} else if (file.isFailed()) {
    System.out.println("File processing failed");
}

컨텍스트 캐싱 (Context Caching)

context caching API는 크고 자주 재사용되는 컨텍스트(시스템 인스트럭션, 긴 문서)를 Google 서버에 한 번 저장해서, 이후 요청이 재전송하는 대신 이름으로 참조하게 해 입력 토큰 비용과 지연 시간을 줄여줘요.

GeminiCaches가 캐시 수명 주기(create / get / list / delete)를 관리해요. 메시지는 채팅 모델이 쓰는 것과 같은 메시지 매핑으로 캐시되므로 LangChain4j ChatMessage 도메인에 머무를 수 있어요.

GeminiCaches caches = GeminiCaches.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .build();

// Cache a large, reusable context (a system instruction plus a long document)
GeminiCachedContent cache = caches.createCache(
    "gemini-2.5-flash",
    List.of(
        SystemMessage.from("You are a precise assistant answering questions about the attached document."),
        UserMessage.from(longDocumentText)),
    Duration.ofHours(1));

// Reuse it across many requests via cachedContentName
ChatModel gemini = GoogleAiGeminiChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .cachedContentName(cache.name())
    .build();

String answer = gemini.chat("Summarize the cached document in 3 bullet points.");

// Manage the cache lifecycle
caches.getCache(cache.name());
caches.listCaches();
caches.deleteCache(cache.name());

createCache는 만료에 세 가지 형태가 있어요. 만료 인자 없이 호출하면 API 기본값(현재 1시간), Duration을 주면 상대적 TTL, Instant를 주면 절대 만료 시각을 설정해요.

Note: 명시적 컨텍스트 캐싱은 유료 티어가 필요해요. 무료 티어에선 사용할 수 없어요.

배치 처리 (Batch Processing)

GoogleAiBatchChatModel

GoogleAiBatchChatModel은 많은 양의 채팅 요청을 표준 가격의 50%로 비동기 처리하는 인터페이스를 제공해요. 24시간 처리 SLO를 가진 긴급하지 않은 대규모 작업에 이상적이에요.

배치 작업 생성

인라인 배치 생성:

GoogleAiGeminiBatchChatModel batchModel = GoogleAiGeminiBatchChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .build();

// Create batch requests
List<ChatRequest> requests = List.of(
    ChatRequest.builder()
        .messages(UserMessage.from("What is the capital of France?"))
        .build(),
    ChatRequest.builder()
        .messages(UserMessage.from("What is the capital of Germany?"))
        .build(),
    ChatRequest.builder()
        .messages(UserMessage.from("What is the capital of Italy?"))
        .build()
);

// Submit the batch (generic API, no Gemini-specific options)
BatchResponse<ChatResponse> response = batchModel.submit(new BatchRequest<>(requests));

// Or, to set a Gemini-specific display name and priority, use GeminiBatchRequest:
BatchResponse<ChatResponse> response = batchModel.submit(GeminiBatchRequest.from(
    requests,
    "Geography Questions Batch", // display name
    0L                           // priority (optional, defaults to 0)
));

파일 기반 배치 생성:

더 큰 배치나 요청 형식을 더 제어해야 할 때 업로드한 파일로 배치를 만들 수 있어요.

// First, upload a file with batch requests
GeminiFiles filesApi = GeminiFiles.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .build();

GeminiFile uploadedFile = filesApi.uploadFile(
    Paths.get("batch_chat_requests.jsonl"),
    "Batch Chat Requests"
);

// Wait for file to be active
while (uploadedFile.isProcessing()) {
    Thread.sleep(1000);
    uploadedFile = filesApi.getMetadata(uploadedFile.name());
}

// Create batch from file
BatchResponse<ChatResponse> response = batchModel.submit("My Batch Job", uploadedFile);

배치 응답 처리

BatchResponse는 현재 state()와 함께 요청별 results(), 그리고 responses()/errors() 편의 뷰를 노출해요. state()로 분기해요(state().isTerminal()로 배치가 아직 진행 중인지 판별).

BatchResponse<ChatResponse> response = batchModel.submit(new BatchRequest<>(requests));

if (!response.state().isTerminal()) {
    System.out.println("Batch is " + response.state());
    System.out.println("Batch ID: " + response.batchId());
} else if (response.state() == BatchState.SUCCEEDED) {
    System.out.println("Batch completed successfully!");

    // Process successful responses
    for (ChatResponse chatResponse : response.responses()) {
        System.out.println(chatResponse.aiMessage().text());
    }

    // Check for individual request errors within the batch
    if (!response.errors().isEmpty()) {
        System.out.println("Some requests failed:");
        for (BatchError error : response.errors()) {
            System.err.println("Error code: " + error.code() + ", message: " + error.message());
        }
    }
} else {
    System.err.println("Batch " + response.state() + ": " + response.errors());
}

참고: state() == SUCCEEDED인 배치는 배치 작업이 완료됐다는 뜻이지만, 배치 안의 개별 요청은 실패했을 수 있어요. errors() 목록은 개별 요청 실패(예: 타임아웃, rate limit)를 담고, responses()는 성공한 응답을 담아요. 둘 다 편의 뷰라 절대 null이 아니므로(보고할 게 없으면 빈 상태), !responses().isEmpty()/!errors().isEmpty()를 확인해서 부분 실패를 우아하게 처리하세요.

결과와 요청 대응

responses()errors()는 어느 입력이 어느 결과를 만들었는지 추적을 잃는 평면(pflat) 뷰예요. 모든 결과를 원래 요청으로 매핑해야 한다면 results()를 쓰세요. 요청당 BatchItemResult 하나를 제출된 요청과 같은 순서로 돌려주므로 i번째 결과가 i번째 요청에 대응해요. 각 결과는 BatchItemResult.Success(response()를 담음) 또는 BatchItemResult.Failure(error()를 담음)예요.

BatchResponse<ChatResponse> result = batchModel.submit(new BatchRequest<>(requests));
// ... poll until terminal ...

List<BatchItemResult<ChatResponse>> results = result.results();
for (int i = 0; i < results.size(); i++) {
    BatchItemResult<ChatResponse> item = results.get(i);
    if (item.isSuccess()) {
        System.out.println("Request #" + i + " -> " + item.response().aiMessage().text());
    } else {
        BatchError error = item.error();
        System.err.println("Request #" + i + " failed: " + error.code() + " - " + error.message());
    }
}

결과 폴링

배치 처리는 비동기라 결과를 폴링해야 해요(결과는 최대 24시간 걸릴 수 있음).

BatchResponse<ChatResponse> result = batchModel.submit(new BatchRequest<>(requests));
String batchId = result.batchId();

// Poll until the batch reaches a terminal state
while (!result.state().isTerminal()) {
    Thread.sleep(5000); // Wait 5 seconds between polls
    result = batchModel.retrieve(batchId);
}

// Process final result
if (result.state() == BatchState.SUCCEEDED) {
    System.out.println("Successful responses: " + result.responses().size());
    for (ChatResponse chatResponse : result.responses()) {
        System.out.println(chatResponse.aiMessage().text());
    }

    // Handle any individual request failures
    if (!result.errors().isEmpty()) {
        System.out.println("Failed requests: " + result.errors().size());
        for (BatchError error : result.errors()) {
            System.err.println("Error: " + error.code() + " - " + error.message());
        }
    }
} else {
    System.err.println("Batch did not succeed: " + result.state());
}

배치 작업 관리

배치 작업 취소:

String batchId = // ... obtained from submit(...)

try {
    batchModel.cancel(batchId);
    System.out.println("Batch cancelled successfully");
} catch (HttpException e) {
    System.err.println("Failed to cancel batch: " + e.getMessage());
}

배치 작업 삭제:

batchModel.deleteBatchJob(batchId);
System.out.println("Batch deleted successfully");

배치 작업 나열:

// List first page of batch jobs
BatchPage<ChatResponse> page = batchModel.list(new BatchPagination(10, null));

for (BatchResponse<ChatResponse> batch : page.batches()) {
    System.out.println("Batch: " + batch);
}

// Get next page if available
if (page.nextPageToken() != null) {
    BatchPage<ChatResponse> nextPage = batchModel.list(new BatchPagination(10, page.nextPageToken()));
}

파일 기반 배치 처리

고급 사용 사례를 위해 배치 요청을 JSONL 파일로 써서 업로드할 수 있어요.

// Create a JSONL file with batch requests
Path batchFile = Files.createTempFile("batch", ".jsonl");

try (JsonLinesWriter writer = new StreamingJsonLinesWriter(batchFile)) {
    List<BatchFileRequest<ChatRequest>> fileRequests = List.of(
        new BatchFileRequest<>("request-1", ChatRequest.builder()
            .messages(UserMessage.from("Question 1"))
            .build()),
        new BatchFileRequest<>("request-2", ChatRequest.builder()
            .messages(UserMessage.from("Question 2"))
            .build())
    );
    
    batchModel.writeBatchToFile(writer, fileRequests);
}

// Upload the file
GeminiFiles filesApi = GeminiFiles.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .build();

GeminiFile uploadedFile = filesApi.uploadFile(batchFile, "Batch Chat Requests");

// Create batch from file
BatchResponse<ChatResponse> response = batchModel.submit("File-Based Chat Batch", uploadedFile);

배치 작업 상태

BatchState enum은 배치 작업의 가능한 상태를 나타내요.

  • PENDING: Batch is queued and waiting to be processed
  • RUNNING: Batch is currently being processed
  • SUCCEEDED: Batch completed successfully (terminal)
  • FAILED: Batch processing failed (terminal)
  • CANCELLED: Batch was cancelled by the user (terminal)
  • EXPIRED: Batch expired before completion (terminal)
  • UNSPECIFIED: State is unknown or not provided

BatchResponse.state()BatchState.isTerminal()을 써서 폴링을 언제 멈출지 감지해요.

배치 우선순위 설정

우선순위가 높은 배치가 낮은 것보다 먼저 처리돼요. GeminiBatchRequest로 우선순위를 설정해요.

// High priority batch
BatchResponse<ChatResponse> highPriority = batchModel.submit(GeminiBatchRequest.from(
    urgentRequests, "Urgent Batch", 100L));

// Low priority batch
BatchResponse<ChatResponse> lowPriority = batchModel.submit(GeminiBatchRequest.from(
    backgroundRequests, "Background Batch", -50L));

구성

GoogleAiGeminiBatchChatModelGoogleAiGeminiChatModel과 같은 구성 옵션을 지원해요.

GoogleAiGeminiBatchChatModel batchModel = GoogleAiGeminiBatchChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .temperature(0.7)
    .topP(0.95)
    .topK(40)
    .maxOutputTokens(2048)
    .maxRetries(3)
    .timeout(Duration.ofMinutes(5))
    .logRequestsAndResponses(true)
    .build();

중요한 제약 사항

  • 모델 일관성: 배치의 모든 요청은 같은 모델을 써야 해요.
  • 크기 제한: 인라인 API는 총 요청 크기 20MB 이하를 지원해요.
  • 비용: 배치 처리는 실시간 요청 대비 50% 비용 절감을 제공해요.
  • 처리 시간: 24시간 SLO지만 완료는 보통 훨씬 빨라요.
  • 사용 사례: 데이터 전처리나 평가 같은 대규모·비긴급 작업에 가장 좋아요.

예시: 전체 워크플로우

GoogleAiGeminiBatchChatModel batchModel = GoogleAiGeminiBatchChatModel.builder()
    .apiKey(System.getenv("GEMINI_AI_KEY"))
    .modelName("gemini-2.5-flash")
    .build();

// Prepare batch requests
List<ChatRequest> requests = new ArrayList<>();
for (int i = 0; i < 50; i++) {
    requests.add(ChatRequest.builder()
        .messages(UserMessage.from("Generate a creative story idea #" + i))
        .build());
}

// Submit batch
BatchResponse<ChatResponse> result = batchModel.submit(GeminiBatchRequest.from(
    requests, "Story Ideas Batch", 0L));
String batchId = result.batchId();

// Poll for completion
int attempts = 0;
int maxAttempts = 720; // 1 hour with 5-second intervals
while (!result.state().isTerminal()) {
    if (attempts++ >= maxAttempts) {
        throw new RuntimeException("Batch processing timeout");
    }
    Thread.sleep(5000);
    result = batchModel.retrieve(batchId);
    System.out.println("Status: " + result.state());
}

// Process results
if (result.state() == BatchState.SUCCEEDED) {
    System.out.println("Generated " + result.responses().size() + " stories");
    for (int i = 0; i < result.responses().size(); i++) {
        ChatResponse chatResponse = result.responses().get(i);
        System.out.println("Story #" + i + ": " + chatResponse.aiMessage().text());
    }

    // Report any failures
    if (!result.errors().isEmpty()) {
        System.err.println(result.errors().size() + " requests failed:");
        for (BatchError error : result.errors()) {
            System.err.println("  - Code " + error.code() + ": " + error.message());
        }
    }
} else {
    System.err.println("Batch did not succeed: " + result.state());
}

더 알아보기