Gemini 2.5 Previewの"ny"問題を調べてみた
"ny"問題とは?
私がそう呼んでいるだけです。
2025年4月8日執筆現在、gemini-2.5-pro-preview-03-25
でStructured Outputをすると、割と高確率でレスポンスの先頭にny
が付き、Pydanticモデルにパースできなくなってしまう問題をそう呼んでいます。
Pydanticにできないことにより後続の処理が全部落ちますから、大問題ですね。
なぜこんなStructured Outputが不安定なのか、気になったのでPythonのgoogle.genaiライブラリを少し調べてみました。
※ 素人調査なので、間違っている可能性があります。
まずは正常のレスポンス
今回は下記のコードを実行しました。
from dotenv import load_dotenv
from google import genai
from pydantic import BaseModel
load_dotenv("../.env")
client = genai.Client()
class CountryInfo(BaseModel):
name: str
population: int
capital: str
continent: str
major_cities: list[str]
gdp: int
official_language: str
total_area_sq_mi: int
def main():
response = client.models.generate_content(
model="gemini-2.5-pro-preview-03-25",
contents="日本の情報を教えて",
config={
"response_mime_type": "application/json",
"response_schema": CountryInfo,
"temperature": 1.0,
},
)
print(f"{response=}")
if __name__ == "__main__":
main()
うまくいった場合、下記のようにresponse
のparsed
に、Pydanticモデルのレスポンスが返ってきます。
response=GenerateContentResponse(
candidates=[
Candidate(
content=Content(
parts=[
Part(
video_metadata=None,
thought=None,
code_execution_result=None,
executable_code=None,
file_data=None,
function_call=None,
function_response=None,
inline_data=None,
text='{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kyoto"],\n "gdp": 4231141000000,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}'
)
],
role='model'),
citation_metadata=None,
finish_message=None,
token_count=None,
avg_logprobs=None,
finish_reason=<FinishReason.STOP: 'STOP'>,
grounding_metadata=None,
index=0,
logprobs_result=None,
safety_ratings=None)
],
model_version='gemini-2.5-pro-preview-03-25',
prompt_feedback=None,
usage_metadata=GenerateContentResponseUsageMetadata(cached_content_token_count=None,
candidates_token_count=483,
prompt_token_count=4,
total_token_count=487),
automatic_function_calling_history=[],
parsed=CountryInfo(
name='Japan',
population=125120000,
capital='Tokyo',
continent='Asia',
major_cities=['Tokyo', 'Yokohama', 'Osaka', 'Nagoya', 'Sapporo', 'Fukuoka', 'Kyoto'],
gdp=4231141000000,
official_language='Japanese',
total_area_sq_mi=145937
)
)
ny
問題では、parsed=None
で返ってきてしまいます。
nyが入っているレスポンス
確率で下記が返ってきます。
response = GenerateContentResponse(
candidates=[
Candidate(
content=Content(
parts=[
Part(
video_metadata=None,
thought=None,
code_execution_result=None,
executable_code=None,
file_data=None,
function_call=None,
function_response=None,
inline_data=None,
text='ny\n```json\n{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kobe", "Kyoto"],\n "gdp": 4231141221114,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}\n```'
)
],
role='model'
),
citation_metadata=None,
finish_message=None,
token_count=None,
avg_logprobs=None,
finish_reason=<FinishReason.STOP: 'STOP'>,
grounding_metadata=None,
index=0,
logprobs_result=None,
safety_ratings=None
)
],
model_version='gemini-2.5-pro-preview-03-25',
prompt_feedback=None,
usage_metadata=GenerateContentResponseUsageMetadata(
cached_content_token_count=None,
candidates_token_count=487,
prompt_token_count=4,
total_token_count=491
),
automatic_function_calling_history=[],
parsed=None
)
textは、下記となっていました。先頭にny
が追加され、その後コードブロックになってします。
そして、parsed=None
が返ってきました。
text='ny\n```json\n{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kobe", "Kyoto"],\n "gdp": 4231141221114,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}\n```'
ちなみに、正常なレスポンスのtextは下記です。
text='{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kyoto"],\n "gdp": 4231141000000,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}'
なぜparsed = Noneとなるのか
結論、それはそうだろという話ですが、Google側が意図してなかったレスポンス形式で返ってきているためです。
少し、genaiのライブラリをいじり、レスポンスの形を強制的に変更して実験してみました。
下記レスポンスがapiから返ってきた直後に、とても雑に変更しています。
操作内容としては、'ny'文字列と、コードブロックを取り除くだけの作業です。
response_dict = self._api_client.request(
'post', path, request_dict, http_options
)
print(f"{request_dict=}")
print(f"{response_dict=}")
response_dict["candidates"][0]["content"]["parts"][0]["text"] = response_dict["candidates"][0]["content"]["parts"][0]["text"][11:-4]
print(f"{response_dict=}")
すると、問題なくparsed
含むレスポンスが帰ってきました。
request_dict = {
"contents": [{"parts": [{"text": "日本の情報を教えて"}], "role": "user"}],
"generationConfig": {
"responseMimeType": "application/json",
"responseSchema": {
"propertyOrdering": [
"name",
"population",
"capital",
"continent",
"major_cities",
"gdp",
"official_language",
"total_area_sq_mi",
],
"type": <Type.OBJECT: 'OBJECT'>,
"properties": {
"name": {"type": <Type.STRING: 'STRING'>},
"population": {"type": <Type.INTEGER: 'INTEGER'>},
"capital": {"type": <Type.STRING: 'STRING'>},
"continent": {"type": <Type.STRING: 'STRING'>},
"major_cities": {
"type": <Type.ARRAY: 'ARRAY'>,
"items": {"type": <Type.STRING: 'STRING'>},
},
"gdp": {"type": <Type.INTEGER: 'INTEGER'>},
"official_language": {"type": <Type.STRING: 'STRING'>},
"total_area_sq_mi": {"type": <Type.INTEGER: 'INTEGER'>},
},
"required": [
"name",
"population",
"capital",
"continent",
"major_cities",
"gdp",
"official_language",
"total_area_sq_mi",
],
},
},
}
response_dict = {
"candidates": [
{
"content": {
"parts": [
{
"text": 'ny\n```json\n{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kobe", "Kyoto"],\n "gdp": 4231141221114,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}\n```'
}
],
"role": "model",
},
"finishReason": "STOP",
"index": 0,
}
],
"usageMetadata": {
"promptTokenCount": 4,
"candidatesTokenCount": 487,
"totalTokenCount": 491,
"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 4}],
"thoughtsTokenCount": 351,
},
"modelVersion": "gemini-2.5-pro-preview-03-25",
}
response_dict = {
"candidates": [
{
"content": {
"parts": [
{
"text": '{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kobe", "Kyoto"],\n "gdp": 4231141221114,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}'
}
],
"role": "model",
},
"finishReason": "STOP",
"index": 0,
}
],
"usageMetadata": {
"promptTokenCount": 4,
"candidatesTokenCount": 487,
"totalTokenCount": 491,
"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 4}],
"thoughtsTokenCount": 351,
},
"modelVersion": "gemini-2.5-pro-preview-03-25",
}
response = GenerateContentResponse(
candidates=[
Candidate(
content=Content(
parts=[
Part(
video_metadata=None,
thought=None,
code_execution_result=None,
executable_code=None,
file_data=None,
function_call=None,
function_response=None,
inline_data=None,
text='{\n "name": "Japan",\n "population": 125120000,\n "capital": "Tokyo",\n "continent": "Asia",\n "major_cities": ["Tokyo", "Yokohama", "Osaka", "Nagoya", "Sapporo", "Fukuoka", "Kobe", "Kyoto"],\n "gdp": 4231141221114,\n "official_language": "Japanese",\n "total_area_sq_mi": 145937\n}'
)
],
role='model'
),
citation_metadata=None,
finish_message=None,
token_count=None,
avg_logprobs=None,
finish_reason=<FinishReason.STOP: 'STOP'>,
grounding_metadata=None,
index=0,
logprobs_result=None,
safety_ratings=None
)
],
model_version='gemini-2.5-pro-preview-03-25',
prompt_feedback=None,
usage_metadata=GenerateContentResponseUsageMetadata(
cached_content_token_count=None,
candidates_token_count=487,
prompt_token_count=4,
total_token_count=491
),
automatic_function_calling_history=[],
parsed=CountryInfo(
name='Japan',
population=125120000,
capital='Tokyo',
continent='Asia',
major_cities=['Tokyo', 'Yokohama', 'Osaka', 'Nagoya', 'Sapporo', 'Fukuoka', 'Kobe', 'Kyoto'],
gdp=4231141221114,
official_language='Japanese',
total_area_sq_mi=145937
)
)
やはり、Json文字列のみが返ってくることを期待しているように見えますね。
え、、Json以外の文字列がはいってると即アウトなの?
パッと見た感じ、その様になってます。
Pydanticにパースしてる部分は、下記になります。
テキストをそのままPydanticに型変更してるように見えます。
モデル内部でStructured Outputように学習されているのか、裏で問題ない様になる(はずの)仕組みがあるのかわからないですが、今回は失敗してしまいましたね。
出力は確率に左右されるので、仕方ないかなぁとは思いつつ、少し困っちゃいますね!
対策
これはgeminiのモデルの問題なので、モデルが改善されるまで待ちましょう。
まだpreviewですからね。
nyってなに?
わかりませんが、トークンカウントしてみたところ1トークンでした。
なので、トークナイザーを学習させたときに、nyが登録されたのでしょう。
また、temperature
を0.0にするとny
の出現頻度が上がります。
種類にもよるとは思いますが、Structured Outputするときには、次トークン予測でny
が選ばれる確率が高いのでしょう。
Discussion