💡

Gemini API でグラウンディング×JSON 出力を実現する実践テクニック

2025/02/20に公開

1. はじめに

株式会社 Hogetic Lab のエンジニア 古川です。

生成 AI を使ってデータを整形し、JSON 形式のデータを生成してシステムに組み込みたい、と考えるのはエンジニアにとっては自然なことです。

各社の LLM でも提供されているように、Gemini API でも出力形式として JSON を指定することができます。
さらに、Gemini API にはグラウンディング(AI の回答を信頼できる外部ソースの情報に基づいて生成する機能)のオプションがあり、このオプションを有効にすることで Google 検索を使って最新の情報を参照して回答を生成するため、ハルシネーション(AI が誤った、または事実に基づかない情報を生成すること)を削減することができます。

しかし、この二つのオプションは同時に設定することができません。
試しに Vertex AI Studio で実行しようとすると、以下のようなエラーが発生します。

プロンプトを送信できませんでした
エラー メッセージ: 「Unable to submit request because Controlled generation is not supported with Google_search tool.. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
ステータス: Undefined parameter - status エラーコード: 3

※ 2025/2/17時点

gemini-2.0-flash-exp が使えるようになった直後、しばらくは同時に設定してもエラーにはなりませんでしたが、現在は同時に設定できなくなりました。しかし、Google 検索でのグラウンディングを使った信頼性の高い回答は魅力的です。

何か良い方法はないでしょうか?

今回は、Gemini API でグラウンディングを適用した JSON 形式のデータを取得する方法を紹介します。

2. 実践方法とアプローチ

といっても、特別な設定は不要で、プロンプトを少し工夫するだけです。

具体例と共に紹介します。

前提条件と目標設定

過去、いくつかの記事[1]でも紹介しましたが、私はクラフトビールが好きで、飲んだビールを記録しています。

その記録の検索や分析で困ることの一つはブルワリー(醸造所)の名前のブレです。
飲食店のメニューには必ずしも正式名が載っていないことも多く、そもそも商品パッケージ上でも統一されていないことすらあります。

そこで、ブルワリーのマスターデータ作成と推定に機械学習や生成 AI を活用して取り組んでいます。

今回は、 Gemini API を使用してブルワリーのマスターデータを作成することを目標に設定します。

コードの実装

コードの抜粋を記載します。

Gemini API の回答テキストおよびJSON形式データを取得するコード
from google import genai
from google.genai import types
import prompts
import os,sys
import json
from dotenv import load_dotenv

load_dotenv()

def generate(text, id):
    client = genai.Client(
        vertexai=True,
        project=os.getenv("PROJECT_ID"),
        location=os.getenv("LOCATION")
    )

    textsi_1 = "{{プロンプトを設定}}"

    model = "gemini-2.0-flash-001"
    contents = [
        types.Content(
        role="user",
        parts=[
            types.Part.from_text(text=text)
        ]
        )
    ]

    tools = [
        types.Tool(google_search=types.GoogleSearch())
    ]

    # https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/content-generation-parameters
    generate_content_config = types.GenerateContentConfig(
        temperature = 0,
        top_p = 0.5,
        max_output_tokens = 8000,
        response_modalities = ["TEXT"],
        safety_settings = [types.SafetySetting(
        category="HARM_CATEGORY_HATE_SPEECH",
        threshold="OFF"
        ),types.SafetySetting(
        category="HARM_CATEGORY_DANGEROUS_CONTENT", 
        threshold="OFF"
        ),types.SafetySetting(
        category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
        threshold="OFF"
        ),types.SafetySetting(
        category="HARM_CATEGORY_HARASSMENT",
        threshold="OFF"
        )],
        tools = tools,
        ## JSON出力を設定する場合、以下を有効にする。ただし google_search tool は使用できない。
        # response_mime_type = "application/json",
        # response_schema = {"type":"OBJECT","properties":{"response":{"type":"STRING"}}},
        system_instruction=[types.Part.from_text(text=textsi_1)],
    )

    result = client.models.generate_content(
        model = model,
        contents = contents,
        config = generate_content_config,
    )

    # レスポンスは Markdown 形式で parts として分割されている。
    response_text = "" # 全てを結合して text として保持
    response_json = {} # JSON パートがあれば抜き出してJSONに変換して保持

    for part in result.candidates[0].content.parts:
        response_text += part.text
        if part.text.startswith("```json"):
            # ```jsonで始まるテキストから、最初の```jsonと最後の```を削除
            json_text = part.text.replace("```json", "").replace("```", "").strip()
            response_json = json.loads(json_text)
    if hasattr(result.candidates[0], "grounding_metadata") and result.candidates[0].grounding_metadata.grounding_chunks:
        chunks = result.candidates[0].grounding_metadata.grounding_chunks
        idx=0
        response_text += "\n\n**Grounded References**\n"
        for chunk in chunks:
            response_text += f"[{idx}] {chunk.web.title}\n"
            response_text += f"    {chunk.web.uri}\n"
            idx += 1

    return {"text":response_text, "json":response_json}

コードでは、JSON形式データだけでなく、回答文面全てとグラウンディングの参照先(タイトル、URL)をテキストとして取得するようにしています。

なお、このコードは Vertex AI Studio で「コードを取得」で表示されるものベースにしています。
Vertex AI Studio
Vertex AI Studio で取得したコードを流用

プロンプトの設計

プロンプトに記載する内容のポイントは以下のとおりです。

  • JSON形式でまとめることを要件として明記する
  • 「取得する情報」と JSON のキーを記載する
ブルワリー情報をまとめるプロンプト
あなたはクラフトビール好きでブルワリーの情報を収集しています。ブルワリー名の表記はブレが大きいため、インターネット上の信頼性の高い情報から必要な情報を取得したいと考えています。
与えられたキーワード(ブルワリー名)を元に以下を参照し、取得した情報を整理して出力してください。

## 要件
- ブルワリー名は通称とする。例:「麒麟麦酒株式会社」は「キリンビール」、「株式会社ヤッホーブルーイング」は「ヤッホーブルーイング」など。
- 住所は、複数の拠点がある場合、本社の住所を採用する。
- 国名コードは ISO 3166-1 alpha-3 (ラテン文字3文字) とする。例: 日本はJPN、米国はUSA。
- facebookアカウントは、公式アカウントのURL(https://www.facebook.com/...)を採用する。
- instagramアカウントは、公式アカウントのURL(https://www.instagram.com/...)を採用する。
- Xアカウントは、公式アカウントのURL(https://x.com/... or https://twitter.com/...)を採用する。
- JSON形式でまとめる。
- 情報が存在しない場合は null とする。

## 参照する情報の優先順位
1. クラビ連サイト: https://0423craft.beer/expo2025/?id=brewery で探す
2. 日本産ホップ推進委員会サイト: https://japanhop.jp/brewery/ で探す
3. 公式ホームページを探す
4. facebook または instagram の公式アカウントを探す
5. Wikipedia で探す

## 取得する情報とJSON key
- ブルワリー名: beer_brewery_name
- 企業名: company_name
- 住所: address
- 国名: country
- 国名(英語): country_en
- 国名コード: country_code
- 公式ホームページURL: url
- facebookアカウント: facebook_account
- instagramアカウント: instagram_account
- Xアカウント: x_account
- ブルワリー名(英語): beer_brewery_name_en
- 企業名(英語): company_name_en

実行結果と検証

上のプロンプトを使って「箕面ビール」を問い合わせた結果はがこちらです。

{
  "beer_brewery_name": "箕面ビール",
  "company_name": "株式会社箕面ビール",
  "address": "大阪府箕面市牧落3-19-11",
  "country": "日本",
  "country_en": "Japan",
  "country_code": "JPN",
  "url": "https://www.minoh-beer.jp/",
  "facebook_account": "https://www.facebook.com/minohbeer/",
  "instagram_account": "https://www.instagram.com/minohbeer/",
  "x_account": "https://twitter.com/minohbeer",
  "beer_brewery_name_en": null,
  "company_name_en": null
}

グラウンディングの参照情報: wikipedia.org

各種 SNS のアカウントも含め、おおよそ正確な情報を取得することができました。

3. 実践から得られた知見

この方法を実践して得られた知見を共有します。

データ品質と信頼性

  • グラウンディングを使っても常に正しい情報が取得できるわけではありません。Facebook や Instagram、X(旧Twitter) の URL については存在しない場合も多かったです。また、公式ではない個人のアカウントであることもありました。
  • 似たような名前のブルワリーがあると、複数の要素を持つ配列が回答されることがありました。その場合、例えば JSON は配列の1つ目を採用し、 text はログとして記録する、など後続処理に合わせて工夫が必要です。
  • 必ずしも JSON 形式でまとめられず、リストで回答されることがありました。その場合でも「取得する情報」に記載した項目名でリストになっていたので、text の内容をパースして JSON を作ることでデータが得られないケースを減らすことができました。(これが、回答全文を全てtextで保管した主な理由です。)
パース用の関数
def parse_brewery_text_to_json(text: str) -> dict:
    """
    collect_brewery_info() で取得したテキスト内容からブルワリー情報を
    JSON形式の辞書に変換する関数です。
    """
    mapping = {
        "ブルワリー名": "beer_brewery_name",
        "企業名": "company_name",
        "住所": "address",
        "国名": "country",
        "国名(英語)": "country_en",
        "国名コード": "country_code",
        "公式ホームページURL": "url",
        "facebookアカウント": "facebook_account",
        "instagramアカウント": "instagram_account",
        "Xアカウント": "x_account",
        "ブルワリー名(英語)": "beer_brewery_name_en",
        "企業名(英語)": "company_name_en"
    }
    data = {}
    for line in text.splitlines():
        stripped_line = line.strip()
        # Grounded References 以下はパース対象外とする
        if stripped_line.startswith("**Grounded References**"):
            break
        # 行頭がアスタリスクの場合のみ処理する
        if stripped_line.startswith("*"):
            # 行頭のアスタリスクと不要な空白を除去
            line_content = stripped_line.lstrip("*").strip()
            if ':' in line_content:
                key, value = line_content.split(":", 1)
                key = key.strip()
                value = value.strip()
                if value.lower() == "null":
                    value = None
                # マッピング辞書を利用して英語のキーに変換する
                if key in mapping:
                    data[mapping[key]] = value
    return data

システムの安定性

  • gemini-2.0-flash-exp の時は回答が不安定で、時々、不思議なポエムが生成されることがありました。しかし、gemini-2.0-flash-001 になってからは、そのようなことはなくなり、出力内容もかなり安定しています。
  • gemini-2.0-flash-exp の時は HTTP Status 429 (Too Many Requests) が発生しやすい時がありましたが gemini-2.0-flash-001 になってからは発生していません。ただ、一時期 HTTP Status 500 (Internal Server Error) が発生して1日程度使えなかったことはありました。

運用上の注意点

  • Gemini のモデルは日々更新されているのか、回答が変わることがあります。以前は「JSON 形式でまとめること」という要件を入れなくても JSON 形式で回答されたのですが、ある時から、この要件を入れないとリストで回答されるようになりました。今後も適宜チューニングが必要になると思われます。

4. まとめ

今回は Google 検索をグラウンディングとして使用して Gemini API で JSON 形式データを取得する方法について解説しました。

信頼性の点では、システム間連携の API として組み込むには難しいと思いますが、マスターデータのベースを作るようなユースケースには適しているように感じました。

他にも Browser Use など、インターネットの情報を効率的に取得して整理する方法はありますので、利用目的に応じて色々と試してみるのが良いと思います。

※ データ分析や AI 活用に関するご相談は、以下よりお気軽にお問い合わせください。
お問い合わせフォーム

5. 参考情報

以前は Google AI Studio と Vertex AI Studio で SDK が分かれていましたが、Gemini 2.0 のリリースに合わせて統合されました。統合後の SDK への変更については以下の公式ページをご確認ください。

統合前の SDK についてはこちらの記事が参考になります。

脚注
  1. ビールをネタにした過去の記事 : 実践 Vertex AI Search:フィルタを使ってみる(基本) / 実践 Vertex AI Search:自然言語フィルタを使ってみる / 実践 Vertex AI Search:「関連性」をマスターする ↩︎

Hogetic Lab

Discussion