내장 도구와 함수 호출 결합

내장 도구와 함수 호출 결합

Gemini는 도구 호출의 컨텍스트 히스토리를 보존하고 노출함으로써 google_search 같은 내장 도구와 함수 호출(일명 커스텀 도구)을 단일 생성에서 결합할 수 있어요. 내장 도구와 커스텀 도구 조합은 예를 들어 모델이 특정 비즈니스 로직을 호출하기 전에 실시간 웹 데이터에 기반을 둘 수 있는 복잡한 에이전트형 워크플로를 가능하게 해요.

다음은 google_search와 커스텀 함수 getWeather로 내장 및 커스텀 도구 조합을 활성화하는 예시예요.

from google import genai
from google.genai import types

client = genai.Client()

getWeather = {
    "name": "getWeather",
    "description": "Gets the weather for a requested city.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "The city and state, e.g. Utqiaġvik, Alaska",
            },
        },
        "required": ["city"],
    },
}

# Turn 1: Initial request with Google Search (built-in) and getWeather (custom) tools enabled
response = client.models.generate_content(
    model="gemini-3.8-flash",
    contents="What is the northernmost city in the United States? What's the weather like there today?",
    config=types.GenerateContentConfig(
        tools=[
            types.Tool(
                google_search=types.GoogleSearch(),  # Built-in tool
                function_declarations=[getWeather],  # Custom tool
            ),
        ],
        tool_config=types.ToolConfig(
            include_server_side_tool_invocations=True
        )
    ),
)
function_call_id = None
for part in response.candidates[0].content.parts:
    if part.function_call:
        print(f"Function call: {part.function_call.name} (ID: {part.function_call.id})")
        function_call_id = part.function_call.id

# Turn 2: Manually build history to circulate both tool and function context
history = [
    types.Content(
        role="user",
        parts=[types.Part(text="What is the northernmost city in the United States? What's the weather like there today?")]
    ),
    # Response from Turn 1 includes tool_call, tool_response, and thought_signatures
    response.candidates[0].content,
    # Return the function_response
    types.Content(
        role="user",
        parts=[types.Part(
            function_response=types.FunctionResponse(
                name="getWeather",
                response={"response": "Very cold. 22 degrees Fahrenheit."},
                id=function_call_id # Match the ID from the function_call
            )
        )]
    )
]

response_2 = client.models.generate_content(
    model="gemini-3.8-flash",
    contents=history,
    config=types.GenerateContentConfig(
        tools=[
            types.Tool(
                google_search=types.GoogleSearch(),
                function_declarations=[getWeather]
            ),
        ],
        # This flag needs to be enabled for built-in tool context circulation and tool combination
        tool_config=types.ToolConfig(
            include_server_side_tool_invocations=True
        )
    ),
)

for part in response_2.candidates[0].content.parts:
    if part.text:
        print(part.text)
import { GoogleGenAI } from '@google/genai';

const client = new GoogleGenAI({});

const getWeather = {
    name: "getWeather",
    description: "Get the weather in a given location",
    parameters: {
        type: "OBJECT",
        properties: {
            location: {
                type: "STRING",
                description: "The city and state, e.g. San Francisco, CA"
            }
        },
        required: ["location"]
    }
};

async function run() {
    const tools = [
      { googleSearch: {} },
      { functionDeclarations: [getWeather] }
    ];
    // This flag needs to be enabled for built-in tool context circulation and tool combination
    const toolConfig = { includeServerSideToolInvocations: true };

    // Turn 1: Initial request with Google Search (built-in) and getWeather (custom) tools enabled
    const response1 = await client.models.generateContent({
        model: "gemini-3.8-flash",
        contents: [{role: "user", parts: [{text: "What is the northernmost city in the United States? What's the weather like there today?"}]}],
        config: {
            tools: tools,
            toolConfig: toolConfig,
        },
    });

    for (const part of response1.candidates[0].content.parts) {
        if (part.functionCall) {
            console.log(`Function call: ${part.functionCall.name} (ID: ${part.functionCall.id})`);
        }
    }

   const functionCallId = response1.candidates[0].content.parts.find(p => p.functionCall)?.functionCall?.id;

    // Turn 2: Manually build history to circulate both tool and function context
    const history = [
        {
            role: "user",
            parts:[{text: "What is the northernmost city in the United States? What's the weather like there today?"}]
        },
        // Response from Turn 1 includes tool_call, tool_response, and thought_signatures
        response1.candidates[0].content,
        // Return the function_response
        {
            role: "user",
            parts: [{
                functionResponse: {
                    name: "getWeather",
                    response: {response: "Very cold. 22 degrees Fahrenheit."},
                    id: functionCallId // Match the ID from the function_call
                }
            }]
        }
    ];

    const response2 = await client.models.generateContent({
        model: "gemini-3.8-flash",
        contents: history,
        config: {
            tools: tools,
            toolConfig: toolConfig,
        },
    });

    for (const part of response2.candidates[0].content.parts) {
        if (part.text) {
            console.log(part.text);
        }
    }
}

run();
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/google/generative-ai-go/genai"
    "google.golang.org/api/option"
)

func main() {
    ctx := context.Background()
    client, err := genai.NewClient(ctx, option.WithAPIKey(os.Getenv("GEMINI_API_KEY")))
    if err != nil {
        log.Exit(err)
    }
    defer client.Close()

    getWeather := &genai.FunctionDeclaration{
        Name:        "getWeather",
        Description: "Get the weather in a given location",
        Parameters: &genai.Schema{
            Type: genai.Object,
            Properties: map[string]*genai.Schema{
                "location": {
                    Type:        genai.String,
                    Description: "The city and state, e.g. San Francisco, CA",
                },
            },
            Required: []string{"location"},
        },
    }

    model := client.GenerativeModel("gemini-3.8-flash")
    model.Tools = []*genai.Tool{
        {GoogleSearch: &genai.GoogleSearch{}}, // Built-in tool
        {FunctionDeclarations: []*genai.FunctionDeclaration{getWeather}}, // Custom tool
    }
    ist := true
    model.ToolConfig = &genai.ToolConfig{
        IncludeServerSideToolInvocations: &ist, // This flag needs to be enabled for built-in tool context circulation and tool combination
    }

    chat := model.StartChat()

    // Turn 1: Initial request with Google Search (built-in) and getWeather (custom) tools enabled
    prompt := genai.Text("What is the northernmost city in the United States? What's the weather like there today?")
    resp1, err := chat.SendMessage(ctx, prompt)
    if err != nil {
        log.Exitf("SendMessage failed: %v", err)
    }

    if resp1 == nil || len(resp1.Candidates) == 0 || resp1.Candidates[0].Content == nil {
        log.Exit("empty response from model")
    }

    var functionCallID string
    for _, part := range resp1.Candidates[0].Content.Parts {
        switch p := part.(type) {
        case genai.FunctionCall:
            fmt.Printf("Function call: %s (ID: %s)\n", p.Name, p.ID)
            if p.Name == "getWeather" {
                functionCallID = p.ID
            }
        }
    }

    if functionCallID == "" {
        log.Exit("no getWeather function call in response")
    }

    // Turn 2: Provide function result back to model.
    // Chat history automatically includes tool_call, tool_response, and thought_signatures from Turn 1.
    fr := genai.FunctionResponse{
        Name: "getWeather",
        ID:   functionCallID,
        Response: map[string]any{
            "response": "Very cold. 22 degrees Fahrenheit.",
        },
    }

    resp2, err := chat.SendMessage(ctx, fr)
    if err != nil {
        log.Exitf("SendMessage for turn 2 failed: %v", err)
    }

    if resp2 == nil || len(resp2.Candidates) == 0 || resp2.Candidates[0].Content == nil {
        log.Exit("empty response from model in turn 2")
    }

    for _, part := range resp2.Candidates[0].Content.Parts {
        if txt, ok := part.(genai.Text); ok {
            fmt.Println(string(txt))
        }
    }
}
# Turn 1: Initial request with Google Search (built-in) and getWeather (custom) tools enabled
curl -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: *** \
-d '{
  "contents": [{
    "role": "user",
    "parts": [{
      "text": "What is the northernmost city in the United States? What'\''s the weather like there today?"
    }]
  }],
  "tools": [{
    "googleSearch": {}
  }, {
    "functionDeclarations": [{
      "name": "getWeather",
      "description": "Get the weather in a given location",
      "parameters": {
          "type": "OBJECT",
          "properties": {
              "location": {
                  "type": "STRING",
                  "description": "The city and state, e.g. San Francisco, CA"
              }
          },
          "required": ["location"]
      }
    }]
  }],
  "toolConfig": {
    "includeServerSideToolInvocations": true
  }
}'

# Turn 2: Manually build history to circulate both tool and function context
# The following request assumes you have captured candidates[0].content from Turn 1 response,
# and extracted function_call.id for getWeather.
# Replace FUNCTION_CALL_ID and insert candidate content from turn 1.
curl -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: *** \
-d '{
  "contents": [
    {
      "role": "user",
      "parts": [{"text": "What is the northernmost city in the United States? What'\''s the weather like there today?"}]
    },
    YOUR_CANDIDATE_CONTENT_FROM_TURN_1_RESPONSE,
    {
      "role": "user",
      "parts": [{
        "functionResponse": {
          "name": "getWeather",
          "id": "FUNCTION_CALL_ID",
          "response": {"response": "Very cold. 22 degrees Fahrenheit."}
        }
      }]
    }
  ],
  "tools": [{
    "googleSearch": {}
  }, {
    "functionDeclarations": [{
      "name": "getWeather",
      "description": "Get the weather in a given location",
      "parameters": {
          "type": "OBJECT",
          "properties": {
              "location": {
                  "type": "STRING",
                  "description": "The city and state, e.g. San Francisco, CA"
              }
          },
          "required": ["location"]
      }
    }]
  }],
  "toolConfig": {
    "includeServerSideToolInvocations": true
  }
}'

출처: 원문

본문

작동 방식

Gemini 3 모델은 *도구 컨텍스트 순환(tool context circulation)*을 사용해 내장 및 커스텀 도구 조합을 가능하게 해요. 도구 컨텍스트 순환은 내장 도구의 컨텍스트를 보존·노출하고 같은 호출에서 턴마다 커스텀 도구와 공유할 수 있게 해요.

도구 조합 활성화

  • 도구 컨텍스트 순환을 활성화하려면 include_server_side_tool_invocations 플래그를 true로 설정해야 해요.
  • 사용하려는 내장 도구와 함께 function_declarations를 포함해 조합 동작을 트리거해요. function_declarations를 포함하지 않아도 플래그가 설정되어 있다면 도구 컨텍스트 순환은 포함된 내장 도구에 대해 여전히 동작해요.

API가 반환하는 파트

단일 응답에서 API는 내장 도구 호출에 대한 toolCall 및 toolResponse 파트를 반환해요. 함수(커스텀 도구) 호출의 경우 API는 functionCall 호출 파트를 반환하고, 사용자는 다음 턴에 functionResponse 파트를 제공해요.

  • toolCall 및 toolResponse: 이 파트들은 서버 측에서 실행되는 도구와 실행 결과의 컨텍스트를 다음 턴을 위해 보존하도록 반환돼요.
  • functionCall 및 functionResponse: API가 함수 호출을 사용자에게 보내 채우게 하고, 사용자는 결과를 함수 응답으로 다시 보내요(이 파트들은 Gemini API의 모든 함수 호출에 표준이며 도구 조합 기능에만 고유한 것이 아니에요).
  • (코드 실행 도구 전용) executableCode 및 codeExecutionResult: 코드 실행 도구를 사용할 때 functionCall 및 functionResponse 대신 API는 executableCode(실행될 모델이 생성한 코드)와 codeExecutionResult(실행 코드의 결과)를 반환해요.

매 턴마다 포함된 모든 필드를 포함한 모든 파트를 모델에 다시 반환해야 컨텍스트를 유지하고 도구 조합을 가능하게 할 수 있어요.

반환된 파트의 핵심 필드

API가 반환하는 특정 파트에는 id, tool_type, thought_signature 필드가 포함돼요. 이 필드는 도구 컨텍스트를 유지하는 데 중요하며(따라서 도구 조합에 중요), 후속 요청에서 응답에 주어진 그대로 모든 파트를 반환해야 해요.

  • id: 호출을 응답에 매핑하는 고유 식별자예요. id는 도구 컨텍스트 순환 여부와 무관하게 모든 함수 호출 응답에 설정돼요. API가 함수 호출에서 제공하는 것과 동일한 id를 함수 응답에서 반드시 제공해야 해요. 내장 도구는 도구 호출과 도구 응답 간에 id를 자동으로 공유해요. 모든 도구 관련 파트(toolCall, toolResponse, functionCall, functionResponse, executableCode, codeExecutionResult)에서 찾을 수 있어요.
  • tool_type: 사용 중인 특정 도구를 식별해요. 리터럴 내장 도구(예: URL_CONTEXT) 또는 함수(예: getWeather) 이름. toolCall 및 toolResponse 파트에서 찾을 수 있어요.
  • thought_signature: API가 반환하는 각 파트에 내장된 실제 암호화된 컨텍스트예요. thought 서명 없이는 컨텍스트를 재구성할 수 없어요. 매 턴마다 모든 파트의 thought 서명을 반환하지 않으면 모델이 오류를 내요. 모든 파트에서 찾을 수 있어요.

도구별 데이터

일부 내장 도구는 도구 유형에 특정한 사용자에게 보이는 데이터 인자를 반환해요.

도구 사용자에게 보이는 도구 호출 인자(있는 경우) 사용자에게 보이는 도구 응답(있는 경우)
GOOGLE_SEARCH queries search_suggestions
GOOGLE_MAPS queries places google_maps_widget_context_token
URL_CONTEXT urls 탐색할 URL urls_metadata retrieved_url : 탐색된 URL url_retrieval_status : 탐색 상태
FILE_SEARCH 없음 없음

예시 도구 조합 요청 구조

다음 요청 구조는 "미국에서 가장 북쪽에 있는 도시는 어디인가요? 오늘 날씨는 어떤가요?"라는 프롬프트의 요청 구조를 보여줘요. 내장 Gemini 도구 google_search와 code_execution, 그리고 커스텀 함수 get_weather라는 세 가지 도구를 결합해요.

{
  "model": "models/gemini-3.8-flash",
  "contents": [{
    "parts": [{
      "text": "What is the northernmost city in the United States? What's the weather like there today?"
    }],
    "role": "user"
  }, {
    "parts": [{
      "thoughtSignature": "...",
      "toolCall": {
        "toolType": "GOOGLE_SEARCH_WEB",
        "args": {
          "queries": ["northernmost city in the United States"]
        },
        "id": "a7b3k9p2"
      }
    }, {
      "thoughtSignature": "...",
      "toolResponse": {
        "toolType": "GOOGLE_SEARCH_WEB",
        "response": {
          "search_suggestions": "..."
        },
        "id": "a7b3k9p2"
      }
    }, {
      "functionCall": {
        "name": "getWeather",
        "args": {
          "city": "Utqiaġvik, Alaska"
        },
        "id": "m4q8z1v6"
      },
      "thoughtSignature": "..."
    }],
    "role": "model"
  }, {
    "parts": [{
      "functionResponse": {
        "name": "getWeather",
        "response": {
          "response": "Very cold. 22 degrees Fahrenheit."
        },
        "id": "m4q8z1v6"
      }
    }],
    "role": "user"
  }],
  "tools": [{
    "functionDeclarations": [{
      "name": "getWeather"
    }]
  }, {
    "googleSearch": {
    }
  }, {
    "codeExecution": {
    }
  }],
  "toolConfig": {
    "includeServerSideToolInvocations": true
  }
}

토큰 및 가격

요청의 toolCall 및 toolResponse 파트는 prompt_token_count에 포함돼요. 이 중간 도구 단계가 이제 보이고 반환되므로 대화 히스토리의 일부가 되기 때문이에요. 이는 요청에만 해당되며 응답에는 해당되지 않아요.

Google 검색 도구는 이 규칙의 예외예요. Google 검색은 이미 쿼리 수준에서 자체 가격 모델을 적용하므로 토큰이 이중 청구되지 않아요(가격 페이지 참조).

자세한 내용은 토큰 페이지를 읽어보세요.

제한 사항

  • include_server_side_tool_invocations 플래그를 활성화하면 기본적으로 VALIDATED 모드(AUTO 모드는 지원되지 않음)를 사용해요.
  • google_search 같은 내장 도구는 위치와 현재 시간 정보에 의존하므로 system_instruction이나 function_declaration.description에 위치·시간 정보가 충돌하면 도구 조합 기능이 잘 작동하지 않을 수 있어요.

지원 도구

표준 도구 컨텍스트 순환은 서버 측(내장) 도구에 적용돼요. 코드 실행도 서버 측 도구이지만 컨텍스트 순환을 위한 자체 내장 솔루션이 있어요. 컴퓨터 사용과 함수 호출은 클라이언트 측 도구이며 역시 컨텍스트 순환을 위한 내장 솔루션이 있어요.

도구 실행 측 컨텍스트 순환 지원
Google 검색 서버 측 지원
Google 지도 서버 측 지원
URL 컨텍스트 서버 측 지원
파일 검색 서버 측 지원
코드 실행 서버 측 지원(내장, executableCode 및 codeExecutionResult 파트 사용)
컴퓨터 사용 클라이언트 측 지원(내장, functionCall 및 functionResponse 파트 사용)
커스텀 함수 클라이언트 측 지원(내장, functionCall 및 functionResponse 파트 사용)

다음 단계

  • Gemini API의 함수 호출에 대해 자세히 알아보세요.
  • 지원 도구 탐색: Google Search / Google Maps / URL Context / File Search.

더 알아보기 (Learn more)