Structured OutputはLLMの出力をプログラムで扱いやすい形式(JSONとか)に落としこむ機能です。 Googleが開発するオープンなLLMモデルのGemma 3が対応したとのことで試してみました。
出力を安定させるためにtemperature
を0
にする、システムプロンプトに入力に忠実に構造化出力して
と指示するなどの工夫が必要なものの、期待通りの結果を得ることができそうです。
準備
- Ollamaをhttps://ollama.comの案内に従ってインストール
ollama pull gemma3:1b
などを実行して使いたいモデルを入手
ソース
名前は篤史、出生地は日本
のような入力に対して、{"name": "篤史", "birthplace": "Japan"}
のようなJSONを返すコードを作ってみました。安定性を確認するため10回ずつループさせます。
こういうとき、LangChainだと色んなモデルを統一的に扱えるので便利。
簡易的な作りですが、パラメータはモデル名
、システムプロンプト
、ユーザープロンプト
です。
uv --directory assets run main.py \
"モデル名(例:gemma3:1b)" \
"システムプロンプト" \
"ユーザープロンプト"
ソースコードは記事内にも記載していますが、以下のURLからも参照できます。
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
を高くすると、変換が発生したりしなかったり、atusy
がAtsushi
、Atusu
に変えられたりします。不安定ですが、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