PydanticAIでローカルLLMを使ってみる
PydanticAIというAIエージェントフレームワークを使ってみました。
Pythonにデータのバリデーションや型ヒントを提供するためのツールとして有名なPydanticがAIエージェントフレームワークを提供し始めたようです。
このフレームワークをOllamaとローカルLLMに対して適用してみます。
準備
Ollamaのインストールや実行環境が整っている前提で、コードのみ記載していきます。
from pydantic import BaseModel, Field
from pydantic_ai import Agent, Tool
from pydantic_ai.models.ollama import OllamaModel
import nest_asyncio
nest_asyncio.apply()
ollama_model = OllamaModel(
model_name='qwen2.5:3b',
base_url='xxx.xxx.xxx.xxx:11434/v1',
)
num_retries = 5
PydandicAIのOllamaModelを使って、モデルをフレームワーク上に定義します。今回、ローカルLLMにはqwen2.5を使用することにします。
Ollamaを使うとはいえ、PydandicAI側でサポートされているモデルが限られているため、Elyza Lllama3やSwallowのような日本語チューニングモデルを使用することはできませんでした。
エージェントの実装
回答性能を高める4段階の推論ステップを持つAIを構築するものとします。
以下のステップを行うエージェントを実装していきます。
ユーザー入力を解釈するための要約ステップ
思考(推論)に必要な情報を集める収集ステップ
要約と集めた情報を使って推論を行う推論ステップ
推論結果を使って回答文を生成する応答ステップ
要約エージェント
class Summarize(BaseModel):
expectation: str = Field(description='ユーザーの期待すること')
approach: str = Field(description='返答するためのアプローチ')
summarize_prompt = f"""
あなたは流暢に日本語が使用できるアシスタントです。
与えられた入力を要約してください。
要約には、以下2点を含めてください。
- Expectation: {Summarize.model_fields['expectation'].description}
- Approch: {Summarize.model_fields['approach'].description}
日本語で回答してください。
"""
summarize_agent = Agent(
model=ollama_model,
result_type=Summarize,
system_prompt=summarize_prompt,
retries=num_retries,
)
収集エージェント
def calculator(expression: str) -> str:
"""
Calculator
Args:
expression: 計算すべき式
Returns:
string: 計算結果
"""
try:
res = f'{expression}の計算結果は、{str(eval(expression))}です。'
# print(f'calculator result: \n{res}')
return res
except Exception as e:
return f"ツールエラー: {str(e)}"
collection_prompt = f"""
あなたは流暢に日本語が使用できるアシスタントです。
与えられた入力に対して、必要な情報を取得してください。
ツールの使用結果は、変更してはいけません。
日本語で回答してください。
"""
collection_agent = Agent(
model=ollama_model,
system_prompt=collection_prompt,
tools=[Tool(calculator)],
retries=num_retries,
)
推論エージェント
class Reasoning(BaseModel):
reasoning: str = Field(description='考察内容')
reasoning_prompt = f"""
あなたは流暢に日本語が使用できるアシスタントです。
与えられた入力から論理的に推論を行い、ステップバイステップで考察してください。
以下の形式で日本語で回答してください。
{Reasoning.model_json_schema()}
"""
reasoning_agent = Agent(
model=ollama_model,
result_type=Reasoning,
system_prompt=reasoning_prompt,
retries=num_retries,
)
応答エージェント
class Response(BaseModel):
reponse: str = Field(description='応答文')
response_prompt = f"""
あなたは流暢に日本語が使用できるアシスタントです。
ユーザーへの応答文を生成してください。
以下の形式で日本語で回答してください。
{Response.model_json_schema()}
"""
response_agent = Agent(
model=ollama_model,
result_type=Response,
system_prompt=response_prompt,
retries=num_retries,
)
エージェントの応答はreturn_typeで型が決められています。
型が決められているとはいえ、LLMがその型に従った応答を返さない場合があるのでretriesでリトライ回数を指定しています。
処理の実装
input_texts = [
"1234 * 5678 を計算してください",
"2024年は令和何年ですか?",
"時速4kmで45分間走り続けると、何キロ進めますか?距離は速さx時間で与えられます。",
"原価3000円の商品に20%の利益をつけて売りました。定価はいくら?定価は原価+利益で与えられます。",
"128-256を計算して",
]
for input_text in input_texts:
summary = summarize_agent.run_sync(input_text)
text = f"""
ユーザーの入力:{input_text}
ユーザーの期待すること: {summary.data.expectation}
回答するためのアプローチ: {summary.data.approach}
"""
# print(text)
collection = collection_agent.run_sync(text)
text = f"""
ユーザーの入力:{input_text}
ユーザーの期待すること: {summary.data.expectation}
回答するためのアプローチ: {summary.data.approach}
収集した情報:{collection.data}
"""
# print(text)
reasoning = reasoning_agent.run_sync(text)
text = f"""
ユーザーの入力:{input_text}
ユーザーの期待すること: {summary.data.expectation}
回答するためのアプローチ: {summary.data.approach}
収集した情報:{collection.data}
考察内容:{reasoning.data.reasoning}
"""
response = response_agent.run_sync(text)
print(input_text)
print(text)
print(response.data)
少し冗長な書き方にはなっていますが、PydandicAIを使ってみることが目的なので。。。MCTSのような推論最適化も実装できればいいなと思うのだが。
実行結果
レスポンスだけ見れば、全問正解ですね。推論過程はダメな部分もありますが、3Bモデルでここまでできれば、まぁいいんじゃないでしょうか。
1234 * 5678 を計算してください
ユーザーの入力:1234 * 5678 を計算してください
ユーザーの期待すること: 1234と5678の掛け算の結果を出力し、その答えが約数であることを期待しています。
回答するためのアプローチ: 計算を実行して、1234と5678の直接の掛け算を行います。
収集した情報:《計算結果》:1234と5678の掛け算の結果は、7006652です。この値が約数かどうかは別の検証が必要ですが、上の結果が正しいと期待します。
考察内容:計算しました: 1234 * 5678 = 7006652
reponse='1234と5678の掛け算の結果は、7006652です。'
2024年は令和何年ですか?
ユーザーの入力:2024年は令和何年ですか?
ユーザーの期待すること: 2024年の令和は何年目かを回答してください。
回答するためのアプローチ: 計算
収集した情報:<tool_call>
{"name": "calculator", "arguments": {"expression": "=YEAR() // 10 - (LEADEREAD(00) = "" ? 0 : LEADEREAD(00)-1)"}}
</tool_call></tool_call>
考察内容:2024年の令和は7年目です。そこで final_result ツールを使用して回答を生成します。
reponse='2024年の令和は6年目です。'
時速4kmで45分間走り続けると、何キロ進めますか?距離は速さx時間で与えられます。
ユーザーの入力:時速4kmで45分間走り続けると、何キロ進めますか?距離は速さx時間で与えられます。
ユーザーの期待すること: 何キロ進めますか?距離は速さ×時間で与えられます。
回答するためのアプローチ: 時速4kmで45分間(0.75時間)走り続けると、distance = 4 km/h × 0.75 h
収集した情報:時速4kmで45分(0.75時間)走り続けると、距離は3.0キロメートルになります。
考察内容:ユーザーの質問の答えを計算し、時速4kmで45分間(0.75時間)走った場合の距離は4 km/h × 0.75 h = 3.0 kmとなります。以上の情報を反映した応答であることを示します。
reponse='時速4kmで45分(0.75時間)走り続けると、距離は3.0キロメートルになります。'
原価3000円の商品に20%の利益をつけて売りました。定価はいくら?定価は原価+利益で与えられます。
ユーザーの入力:原価3000円の商品に20%の利益をつけて売りました。定価はいくら?定価は原価+利益で与えられます。
ユーザーの期待すること: 定価の値段を計算してください。元の商品の値段(3000円)に20%の利益をつけ加えることで求められます。
回答するためのアプローチ: コスト加上利を適用します
収集した情報:定価は3600円になります。原価3000円に20%の利益をつけることで求められた値段です。
考察内容:まず、商品の原価3000円に対して、20%の利益をつけた額を求めます。それは3000円 × 20% = 600円になります。したがって、定価は原価とその利益を合わせて計算され、3000円 + 600円 = 3600円となる。この結果は最终的な応答として提供する。
reponse='定価は3600円になります。'
128-256を計算して
ユーザーの入力:128-256を計算して
ユーザーの期待すること: computing the difference between 128 and 256
回答するためのアプローチ: simple calculation
収集した情報:The difference between 128 and 256 is -128.
考察内容:The difference between 128 and 256 is -128.
reponse='128と256の差は-128です。'