はじめに
litellmやlangchainで、pydanticを使って構造化出力する方法をよく忘れるのでメモしました。
バッチ処理を合わせて使うことも多いので、そのやり方もメモしました。
環境
- macOS
- python 3.12
- uvを使用
% uv run pip list | grep langchain
langchain-core 0.3.72
langchain-litellm 0.2.2
langchain-openai 0.3.28
準備
- 環境変数OPENAI_API_KEYを設定
mkdir test
cd test
uv init
uv add litellm langchain-openai langchain-litellm
litellm
pydantic.BaseModelを継承したスキーマクラスを定義して、litellm.completionのresponse_formatに与えます。
レスポンスからcontentを取得します。
このcontentはjsonの文字列なので、BaseModel.model_validate_jsonでスキーマ型にできます。
import litellm
from pydantic import BaseModel, Field
class Person(BaseModel):
"""Person Information"""
name: str = Field(..., description="name of the person.")
age: int = Field(..., description="age of the person.")
def generate_person() -> Person:
response = litellm.completion(
messages=[
{"role": "user", "content": "人の情報を生成して"}
],
model="gpt-4.1-mini",
response_format=Person
)
content = response['choices'][0]['message']['content']
person = Person.model_validate_json(content)
return person
if __name__ == "__main__":
person = generate_person()
print(person)
% uv run litellm_structured_output.py
name='佐藤太郎' age=28
バッチ処理をしたい場合はbatch_completionを使います。
import litellm
from pydantic import BaseModel, Field
class Person(BaseModel):
"""Person Information"""
name: str = Field(..., description="name of the person.")
age: int = Field(..., description="age of the person.")
def generate_persons() -> list[Person]:
messages_list = []
messages_list.append([
{"role": "user", "content": "男性の情報を生成して"}
])
messages_list.append([
{"role": "user", "content": "女性の情報を生成して"}
])
response = litellm.batch_completion(
messages=messages_list,
model="gpt-4.1-mini",
response_format=Person
)
persons = []
for response in response:
content = response['choices'][0]['message']['content']
person = Person.model_validate_json(content)
persons.append(person)
return persons
if __name__ == "__main__":
person = generate_persons()
print(person)
% uv run litellm_structured_output_batch.py
[Person(name='拓海', age=30), Person(name='佐藤 花子', age=29)]
langchain
langchainでも試してみます。
litellm同様に、モデル名を簡単に入れ替えられるようにChatLiteLLMを使います。
OpenAI APIしか使う予定がないのであればfrom langchain_openai import ChatOpenAI
を代わりに使ってもよいです。
langchainを使う場合のポイントは以下です。
- チャットモデル宣言時にwith_structured_outputでスキーマを紐づけ
- messagesがlist[dict]ではなくlist[tuple]
- llmの呼び出しがcompletionではなくinvoke
- 戻り値は(なにもしなくても)スキーマ型
from langchain_litellm import ChatLiteLLM
from pydantic import BaseModel, Field
class Person(BaseModel):
"""Person Information"""
name: str = Field(..., description="name of the person.")
age: int = Field(..., description="age of the person.")
def generate_person() -> Person:
chat = ChatLiteLLM(model="gpt-4.1-mini").with_structured_output(Person)
messages = [
("user", "人を生成して"),
]
response = chat.invoke(messages)
return response
if __name__ == "__main__":
person = generate_person()
print(person)
% uv run langchain_structured_output.py
name='太郎' age=30
バッチ処理をしたい場合はChatLiteLLM.batchにmessagesのリストを入力します。
from langchain_litellm import ChatLiteLLM
from pydantic import BaseModel, Field
class Person(BaseModel):
"""Person Information"""
name: str = Field(..., description="name of the person.")
age: int = Field(..., description="age of the person.")
def generate_persons() -> list[Person]:
chat = ChatLiteLLM(model="gpt-4.1-mini").with_structured_output(Person)
messages_list = []
messages_list.append([
("user", "男性の情報を生成して")
])
messages_list.append([
("user", "女性の情報を生成して")
])
response = chat.batch(messages_list)
return response
if __name__ == "__main__":
person = generate_persons()
print(person)
[Person(name='山田太郎', age=30), Person(name='美咲', age=28)]
おわりに
litellmとlangchainの使い分けは好みです。
litellmのほうがopenaiライブラリの仕様に準拠しており覚えることが少ないですが、なれると、スキーマ型を直接返せるlangchainも魅力です。
個人的には、単体で使うときはlitellmにすることが多く、チェインの機能を使いたい場合はlanchainも使うと思います。