見出し画像

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 を計算してください
    ユーザーの期待すること: 12345678の掛け算の結果を出力し、その答えが約数であることを期待しています。
    回答するためのアプローチ: 計算を実行して、12345678の直接の掛け算を行います。
    収集した情報:《計算結果》:12345678の掛け算の結果は、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です。'

いいなと思ったら応援しよう!