gemma3:1bのStructured Outputを安定させる工夫

by
カテゴリ:
タグ:

Structured OutputはLLMの出力をプログラムで扱いやすい形式(JSONとか)に落としこむ機能です。 Googleが開発するオープンなLLMモデルのGemma 3が対応したとのことで試してみました。

https://developers.googleblog.com/en/introducing-gemma3/

出力を安定させるためにtemperature0にする、システムプロンプトに入力に忠実に構造化出力してと指示するなどの工夫が必要なものの、期待通りの結果を得ることができそうです。

準備

  • Ollamaをhttps://ollama.comの案内に従ってインストール
  • ollama pull gemma3:1bなどを実行して使いたいモデルを入手

ソース

名前は篤史、出生地は日本のような入力に対して、{"name": "篤史", "birthplace": "Japan"}のようなJSONを返すコードを作ってみました。安定性を確認するため10回ずつループさせます。

こういうとき、LangChainだと色んなモデルを統一的に扱えるので便利。

簡易的な作りですが、パラメータはモデル名システムプロンプトユーザープロンプトです。

uv --directory assets run main.py \
  "モデル名(例:gemma3:1b)" \
  "システムプロンプト" \
  "ユーザープロンプト"

ソースコードは記事内にも記載していますが、以下のURLからも参照できます。

https://github.com/atusy/blog/blob/882c6ec580ff4f58069e731548d325c156818e1f/content/post/2025/2025-04-28-gemma3-structured-output/assets

main.py

 # assets/main.py
 import asyncio
 import json
 import sys
 
 from langchain_core.messages import HumanMessage, SystemMessage
 from langchain_ollama import ChatOllama
 from pydantic import BaseModel, Field
 
 
 class X(BaseModel):
     name: str = Field(...)
     birthplace: str = Field(...)
 
 
 async def construct_x(client: ChatOllama, system_prompt: str, user_prompt: str) -> X:
     _ = SystemMessage
     result = await client.with_structured_output(X).ainvoke(
         [SystemMessage(content=system_prompt), HumanMessage(content=user_prompt)]
     )
 
     if not isinstance(result, X):
         raise TypeError
 
     print(json.dumps(result.model_dump(), ensure_ascii=False))
 
     return X
 
 
 async def main():
     _, model, system_prompt, user_prompt = sys.argv
     client = ChatOllama(model=model, temperature=0)
 
     await asyncio.gather(
         *[construct_x(client, system_prompt, user_prompt) for _ in range(10)]
     )
 
 
 asyncio.run(main())

pyproject.toml

 # assets/pyproject.toml
 [project]
 name = "assets"
 version = "0.1.0"
 description = "Add your description here"
 readme = "README.md"
 requires-python = ">=3.10"
 dependencies = [
     "langchain>=0.3.24",
     "langchain-ollama>=0.3.2",
     "pydantic>=2.11.3",
 ]

結果

Gemma3:1b

名前は篤史、出生地は日本

いい感じ

uv --directory assets run main.py gemma3:1b "" "名前は篤史、出生地は日本"
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}
#> {"name": "篤史", "birthplace": "日本"}

名前はatusy、出身地は日本

アルファベットを使ったハンドルネームのせいで、日本Japanに変換しやがります。ちなみに、モデルパラメータのtemperatureを高くすると、変換が発生したりしなかったり、atusyAtsushiAtusuに変えられたりします。不安定ですが、temperatureの性質を考えれば、それはそうといった感じですね。

uv --directory assets run main.py gemma3:1b "" "名前はatusy、出身地は日本"
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}
#> {"name": "Atusy", "birthplace": "Japan"}

この手の問題はシステムプロンプトで"入力に忠実に構造化出力して"と指示しておくとよさげ。

uv --directory assets run main.py gemma3:1b "入力に忠実に構造化出力して" "名前はatusy、出身地は日本"
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}
#> {"name": "atusy", "birthplace": "日本"}

Gemma2:2b

Gemma 2では以下のような記事を見かけて、Structured Outputするの大変なのかなーと思ってましたが、今回のソースコードで問題なく動くことを確認しました。

ローカルLLM(Gemma 2 2B)で「Structured Outputs(構造化出力)」っぽいことGuardrails AIで実現する
https://qiita.com/Hikakkun/items/f1813395a00b10222305

強いてGemma 2を使いつづける理由はあまりないと思いますが……。

名前はatusy、出身地は日本

uv --directory assets run main.py gemma2:2b "入力に忠実に構造化出力して" "名前はatusy、出身地は日本"
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}
#> {"name": "atusy", "birthplace": "Japan"}

ENJOY