[翻訳] MLflowチュートリアル: ChatModelを使い始める

[Tutorial: Getting Started with ChatModel](https://mlflow.org/docs/latest/llms/chat-model-intro/index.html)の翻訳です。

:::note warn
本書は著者が手動で翻訳したものであり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
:::

MLflowの[ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)クラスは、プロダクションレディの会話型AIモデルを作成するための標準化された手段を提供します。結果として得られるモデルは、MLflowのトラッキング、評価、ライフサイクル管理機能と完全にインテグレーションされます。これらは、MLflowモデルレジストリで他の人と共有したり、REST APIとしてデプロイしたり、インタラクティブに使うためにノートブックにロードすることができます。さらに、これらは広く導入されているOpenAI chat APIの仕様に互換性があるので、他のAIシステムやツールとの連携が容易になります。

すでに[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)に慣れているのであれば、なぜ[ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)が必要なのかと不思議に思うかもしれません。生成AIアプリケーションはどんどん複雑になっており、カスタムの`PythonModel`を用いた入出力、パラメーターのマッピングは困難になることがあります。`ChatModel`は、会話型AIモデルのために、構造化され、OpenAIと互換性のあるスキーマを提供することで、これをシンプルにします。

|  | ChatModel | PythonModel |
|:--|:--|:--|
|使用するケース   |OpenAIの仕様と互換性のある**標準**チャットスキーマで会話型モデルを開発、デプロイする際に使用。   | モデルのインタフェースに対する**フルコントロール**を必要とする、あるいはモデルの挙動の全ての側面をカスタマイズしたい際に使用。  |
|インタフェース   | OpenAIのチャットスキーマに**固定**。  | モデルの入出力スキーマに対する**フルコントロール**。  |
|セットアップ   | **クイック**。定義済みのモデルシグネチャと入力例を用いて、会話型のアプリケーションですぐに利用可能。  |  **カスタム**。自分でモデルのシグネチャと入力サンプルを定義する必要がありる。 |
|複雑度   | **低い**。標準化されたインタフェースがモデルのデプロイとインテグレーションをシンプルに。  | **高い**。カスタムのPythonModelのデプロイとインテグレーションはわかりにくくなる場合がある。例えば、MLflowがPythonModelに入力データを渡す前に、入力データをPandasデータフレームに変換するので、モデルはPandasデータフレームを取り扱う必要がある。  |

# 学ぶこと

このガイドでは、カスタムの会話型AIモデルを定義するための基本的なChatModel APIの使い方をウォークスルーします。特に、以下のことを学びます:

1. あなたのアプリケーションロジックを`ChatModel`の入力/出力スキーマにマッピングする方法。
1. ChatModelでサポートされる定義済みの推論パラメータの使い方。
1. `custom_inputs`を用いて、ChatModelにカスタムのパラメータを渡す方法。
1. カスタムチャットモデルを定義する際の[ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)と[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)の比較。

これらのポイントを説明するために、このガイドでは例としてローカルにホストされたOllamaモデルを用いて、カスタムの`ChatModel`の構築をウォークスルーします。ビルトインのOllamaモデルフレーバーは存在しないので、MLflowのさまざまなトラッキング、評価、ライフサイクル管理機能をOllamaモデルで使うために、カスタムの`ChatModel`を作成します。

# 前提条件

- MLflowのロギングAPIと生成AIの概念に対する習熟度。
- [ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)を使うためにMLflowバージョン2.17.0以降がインストールされていること。

# ChatModelを理解する: 入力/出力のマッピング

[mlflow\.pyfunc\.ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)インタフェースは、あなたのアプリケーションとMLflowのエコシステムの間に存在し、あなたのアプリケーションとMLflowの他の機能との連携や、アクセス可能かつプロダクションレディなフォーマットを用いたモデルのデプロイを容易にする、標準化レイヤーを提供します。

そのためには、カスタムの`ChatModel`を定義する際のキーとなるタスクとして、`ChatModel`の標準化されたインタフェースにあなたのアプリケーションのロジックをマッピングする必要があります。*このマッピングの練習は、カスタムの`ChatModel`を作成する際の重要な部分となります。*

![](https://mlflow.org/docs/latest/_images/interface_1.png)

カスタムChatModelを使用する際、`predict`メソッドは以下のように標準化された入力を期待します:

```py
input = {
    "messages": [{"role": "user", "content": "What is MLflow?"}],
    "max_tokens": 25,
}
```

ここには、メッセージのリストを含む`messages`キー、`max_tokens`、`temperature`、`top_p`、`stop`のようなオプションの推論パラメータが含まれています。[こちら](https://mlflow.org/docs/latest/python_api/mlflow.types.html#mlflow.types.llm.ChatCompletionRequest)で完全なchat requestオブジェクトの詳細を確認することができます。

また、出力も以下のように標準化されたフォーマットで返却されます:

```py
{
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "MLflow is an open-source platform for machine learning (ML) and artificial intelligence (AI). It's designed to manage,",
            },
            "finish_reason": "stop",
        }
    ],
    "model": "llama3.2:1b",
    "object": "chat.completion",
    "created": 1729190863,
}
```

[こちら](https://mlflow.org/docs/latest/python_api/mlflow.types.html#mlflow.types.llm.ChatCompletionResponse)で完全なchat responseオブジェクトの詳細を確認することができます。

これらの入力/出力スキーマは、広く受け入れられているOpenAIの仕様と互換性があるので、いろいろな文脈での`ChatModel`の活用が用意となります。

このマッピングプロセスをデモンストレーションするために、ネイティブなMLflowフレーバーを持たないOllama llmクライアントを通じて、MetaのLlama 3.2 1Bモデルを記録するために、[mlflow\.pyfunc\.ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)の使い方を説明します。

# 初めてのChatModelの構築

このセクションでは、ローカルにホストされたOllamaモデルを`ChatModel`インタフェースでラッピングします。入出力の取り扱い方を説明するためにシンプルなバージョンを構築し、`max_tokens`や`temperature`のような推論パラメータの取り扱い方法を説明します。

## セットアップ: Ollamaのインストールとモデルのダウンロード

1. [こちら](https://ollama.com/)からOllamaをインストールします。
1. Ollamaがインストールされ稼働したら、`ollama pull llama3.2:1b`を実行してLlama 3.2 1Bモデルをダウンロードします。

`ollama run llama3.2:1b`でモデルがダウンロードされ、システムで利用できることを検証できます。

```
> ollama run llama3.2:1b

>>> Hello world!
Hello! It's great to see you're starting the day with a cheerful greeting. How can I assist you today?
>>> Send a message (/? for help)
```

Ollamaモデルのインタフェースとして、`ollama-python`ライブラリを使います。`pip install ollama`で環境にインストールします。また、`pip install mlflow`で`mlflow`をインストールします。

## Ollama Pythonライブラリの使用

ChatModelの入出力スキーマにOllamaの入出力スキーマをマッピングするには、はじめにOllamaモデルが期待、返却する入力と出力の種類を理解する必要があります。シンプルなプロンプトでこのモデルにクエリーする方法を示します:

```py
import ollama
from ollama import Options
from rich import print

response = ollama.chat(
    model="llama3.2:1b",
    messages=[
        {
            "role": "user",
            "content": "What is MLflow Tracking?",
        }
    ],
    options=Options({"num_predict": 25}),
)

print(response)
```

以下の出力が返却されます:

```py
{
    'model': 'llama3.2:1b',
    'created_at': '2024-11-04T12:47:53.075714Z',
    'message': {
        'role': 'assistant',
        'content': 'MLflow Tracking is an open-source platform for managing, monitoring, and deploying machine learning (ML) models. It provides a'
    },
    'done_reason': 'length',
    'done': True,
    'total_duration': 1201354125,
    'load_duration': 819609167,
    'prompt_eval_count': 31,
    'prompt_eval_duration': 41812000,
    'eval_count': 25,
    'eval_duration': 337872000
}
```

Ollamaの入出力に関して注意すべきことがいくつかあります:

- `ollama.chat`メソッドによって期待される引数`messages`は、`role`と`content`のキーを持つディクショナリーのリストです。ChatModel APIによって期待される`ChatMessage`オブジェクトのリストをディクショナリーのリストに変換する必要があります。
- パラメータのディクショナリーである引数`options`を通じて、Ollamaに推論パラメータが渡されます。さらに、`num_predict`にあるように、パラメータ名はChatModelで期待されるパラメータ名と異なっています。ChatModelの推論パラメータをOllamaのオプションにまっっピングする必要があります。
- 出力の構造は`ChatModel`の出力スキーマと異なっています。これをChatModelの出力スキーマにマッピングする必要があります。

## Ollama ChatModelバージョン1: チャットのみ

推論パラメータには対応しませんが、入力/出力のメッセージに対応するシンプルなバージョンのカスタム`ChatModel`からスタートしましょう。これを達成するには以下が必要となります:

1. [mlflow\.pyfunc\.ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)を拡張するクラスを定義します。
1. Ollamaクライアントの初期化を取り扱う`load_context`メソッドを実装します。
1. 入出力マッピングを取り扱う`predict`メソッドを実装します。

少なくともこのシンプルなバージョンのカスタマイズの大部分は、`predict`メソッドで行われます。`predict`メソッドを実装する際には、以下の標準化された入力を活用します:

- `messages`: `ChatMessage`オブジェクトのリスト
- `params`: 推論パラメータを含む`ChatParams`オブジェクト

そして、`ChatChoice`オブジェクトのリストと(オプションの)利用データや他のメタデータから構成されるデータクラスである`ChatCompletionResponse`オブジェクトを返却する必要があります。

これらがOllamaの入出力にマッピングしなくてはならないものです。今のところは、入出力メッセージのみを取り扱うシンプルなバージョンとなります:

```py
# if you are using a jupyter notebook
# %%writefile ollama_model.py
from mlflow.pyfunc import ChatModel
from mlflow.types.llm import ChatMessage, ChatCompletionResponse, ChatChoice
from mlflow.models import set_model
import ollama


class SimpleOllamaModel(ChatModel):
    def __init__(self):
        self.model_name = "llama3.2:1b"
        self.client = None

    def load_context(self, context):
        self.client = ollama.Client()

    def predict(self, context, messages, params=None):
        # Prepare the messages for Ollama
        ollama_messages = [msg.to_dict() for msg in messages]

        # Call Ollama
        response = self.client.chat(model=self.model_name, messages=ollama_messages)

        # Prepare and return the ChatCompletionResponse
        return ChatCompletionResponse(
            choices=[{"index": 0, "message": response["message"]}],
            model=self.model_name,
        )


set_model(SimpleOllamaModel())
```

上のコードでは、Ollamaの入力に`ChatModel`の入力、Ollamaの出力を`ChatModel`の出力スキーマにマッピングしました。特に:

- `ChatModel`の入力スキーマにおける`messages`キーは、`ChatMessage`オブジェクトのリストとなります。これをOllamaで期待される入力である`role`と`content`キーのディクショナリーのリストに変換しました。
- `predict`メソッドが返却する`ChatCompletionResponse`は、`ChatCompletionResponse`データクラスを用いて作成する必要がありますが、期待されるスキーマにマッチするディクショナリーとして、ネストされたメッセージやチョイスデータを提供することもできます。MLflowは自動的にこれらのディクショナリーを適切なデータクラスオブジェクトに変換します。我々のケースでは、`ChatCompletionResponse`を作成しましたが、ディクショナリーとしてチョイスとメッセージを提供しちえます。

ノートブックの環境では、`%%writefile`マジックコマンドで`ollama_model.py`と呼ばれるファイルにモデルを保存し、`set_model(SimpleOllamaModel())`を呼び出すことができます。これは、[こちら](https://mlflow.org/docs/latest/model/models-from-code.html)で詳細を確認できる「models from code」のモデルロギングのアプローチです。

これで、以下のように作成したモデル定義を含むファイルへのパスを指定することで、MLflowにこのモデルを記録することができます:

```py
import mlflow

mlflow.set_experiment("chatmodel-quickstart")
code_path = "ollama_model.py"

with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        "ollama_model",
        python_model=code_path,
        input_example={
            "messages": [{"role": "user", "content": "Hello, how are you?"}]
        },
    )
```

繰り返しになりますが、モデルを記録するためにmodels-from-codeアプローチを採用しているので、`python_model`パラメータにモデル定義を含むファイルへのパスを指定しています。これで、モデルをロードして試すことができます:

```py
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)

result = loaded_model.predict(
    data={
        "messages": [{"role": "user", "content": "What is MLflow?"}],
        "max_tokens": 25,
    }
)
print(result)
```

```py
{
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "MLflow is an open-source platform for model deployment, monitoring, and tracking. It was created by Databricks, a cloud-based data analytics company, in collaboration with The Data Science Experience (TDEE), a non-profit organization that focuses on providing high-quality, free machine learning resources.\n\nMLflow allows users to build, train, and deploy machine learning models in various frameworks, such as TensorFlow, PyTorch, and scikit-learn. It provides a unified platform for model development, deployment, and tracking across different environments, including local machines, cloud platforms (e.g., AWS), and edge devices.\n\nSome key features of MLflow include:\n\n1. **Model versioning**: Each time a model is trained or deployed, it generates a unique version number. This allows users to track changes, identify conflicts, and manage multiple versions.\n2. **Model deployment**: MLflow provides tools for deploying models in various environments, including Docker containers, Kubernetes, and cloud platforms (e.g., AWS).\n3. **Monitoring and logging**: The platform includes built-in monitoring and logging capabilities to track model performance, errors, and other metrics.\n4. **Integration with popular frameworks**: MLflow integrates with popular machine learning frameworks, making it easy to incorporate the platform into existing workflows.\n5. **Collaboration and sharing**: MLflow allows multiple users to collaborate on models and tracks changes in real-time.\n\nMLflow has several benefits, including:\n\n1. **Improved model management**: The platform provides a centralized view of all models, allowing for better model tracking and management.\n2. **Increased collaboration**: MLflow enables team members to work together on machine learning projects more effectively.\n3. **Better model performance monitoring**: The platform offers real-time insights into model performance, helping users identify issues quickly.\n4. **Simplified model deployment**: MLflow makes it easy to deploy models in various environments, reducing the complexity of model deployment.\n\nOverall, MLflow is a powerful tool for managing and deploying machine learning models, providing a comprehensive platform for model development, tracking, and collaboration.",
            },
            "finish_reason": "stop",
        }
    ],
    "model": "llama3.2:1b",
    "object": "chat.completion",
    "created": 1730739510,
}
```

これで、標準化され、OpenAIと互換性のあるフォーマットでチャットのレスポンスを受け取りました。しかし、何かおかしいです: `max_tokens`を25に設定したのに、レスポンスは25トークン以上となっています！なぜでしょうか？

我々のカスタムChatModelでは、まだ推論パラメータに対応していません: ChatModelとOllamaフォーマット間での入力/出力のメッセージのマッピングに加え、二つのフォーマット間の推論パラメータをマッピングする必要があります。カスタムChatModelの次のバージョンではこれに対応します。

# 推論パラメーターを受け付けるChatModelの構築

多くのLLMでは、レスポンスに含めるトークン数を制限する`max_tokens`やレスポンスの「創造性」を調整する`temperature`のように、レスポンスの生成方法を制御する推論パラメータをサポートしています。ChatModel APIでは、一般的に使用される推論パラメータの多くに対するビルトインのサポートが提供されており、このセクションではどのように設定、活用するのかを設営します。

## ChatModelへのパラメータの引き渡し

ChatModelを使う際、入力のメッセージと共にパラメータが引き渡されます:

```py
result = model.predict(
    {
        "messages": [{"role": "user", "content": "Write a story"}],
        "max_tokens": 100,
        "temperature": 0.7,
    }
)
```

[こちら](https://mlflow.org/docs/latest/python_api/mlflow.types.html#mlflow.types.llm.ChatParams)でサポートされるパラメータの完全なリストを確認できます。さらに、次のセクションでカバーする`custom_inputs`キーを通じて、ChatModelに任意の追加パラメータを渡すことができます。

## カスタムPyFuncモデルにおけるパラメータの取り扱いとの比較

[PyFuncモデル](https://mlflow.org/blog/custom-pyfunc#parameterizing-the-custom-model)での推論パラメータに慣れているのであれば、ChatModelにおけるパラメータの取り扱いにはいくつかのキーとなる違いがあることに気づくことでしょう:

| ChatModel | PyFunc |
|:--|:--|
| パラメータは`predict`に引き渡される`messages`キーを含む`data`ディクショナリーの一部となります。  | パラメータは`params`キーワード引数として`predict`に引き渡されます。  |
| ChatModelクラスでは、一般的に使われるチャットモデルパラメータ(`max_tokens`、`temperature`、`top_p`など)は事前に定義されています。  | 開発者によってパラメータは選択、設定されます。  |
| 一般的なチャットモデルパラメータをサポートするために、モデルシグネチャは自動で設定されます。  | モデルシグネチャでパラメータを明示的に定義する必要があります。  |

まとめると、ChatModelによって推論パラメータの設定と利用が簡単になることに加えて、ある程度の柔軟性と引き換えに、標準化され、OpenAIと互換性がある出力フォーマットが提供されます。

それでは、推論パラメータを取り扱うカスタムChatModelを設定しましょう。

## Ollama ChatModelバージョン2: 推論パラメータを用いたチャット

推論パラメータを持つChatModelのセットアップはわかりやすいものとなっています: 入力メッセージのように、Ollamaクライアントで期待されるフォーマットに推論パラメータをマッピングする必要があります。Ollamaクライアントでは、推論パラメータは`options`ディクショナリーとしてモデルに渡されます。カスタムChatModelを定義する際に、`params`キーワード引数を通じて、`predict`に渡される推論パラメータにアクセスすることができます。ここでの仕事は、Ollamaクライアントの`options`ディクショナリーにpredictメソッドの`params`ディクショナリーをマッピングすることです。Ollamaでサポートされているオプションのリストは[こちら](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-request-with-options)で確認することができます。

```py
# if you are using a jupyter notebook
# %%writefile ollama_model.py

import mlflow
from mlflow.pyfunc import ChatModel
from mlflow.types.llm import ChatMessage, ChatCompletionResponse, ChatChoice
from mlflow.models import set_model
import ollama
from ollama import Options


class OllamaModelWithMetadata(ChatModel):
    def __init__(self):
        self.model_name = None
        self.client = None

    def load_context(self, context):
        self.model_name = "llama3.2:1b"
        self.client = ollama.Client()

    def _prepare_options(self, params):
        # Prepare options from params
        options = {}
        if params:
            if params.max_tokens is not None:
                options["num_predict"] = params.max_tokens
            if params.temperature is not None:
                options["temperature"] = params.temperature
            if params.top_p is not None:
                options["top_p"] = params.top_p
            if params.stop is not None:
                options["stop"] = params.stop

            if params.custom_inputs is not None:
                options["seed"] = int(params.custom_inputs.get("seed", None))

        return Options(options)

    def predict(self, context, messages, params=None):
        ollama_messages = [
            {"role": msg.role, "content": msg.content} for msg in messages
        ]
        options = self._prepare_options(params)

        # Call Ollama
        response = self.client.chat(
            model=self.model_name, messages=ollama_messages, options=options
        )

        # Prepare the ChatCompletionResponse
        return ChatCompletionResponse(
            choices=[{"index": 0, "message": response["message"]}],
            model=self.model_name,
        )


set_model(OllamaModelWithMetadata())
```

以前のバージョンからの変更点を示します:

- `params`ディクショナリーから`max_tokens`、`temperature`、`top_p`、`stop`をOllamaクライアントの`options`ディクショナリーの`num_predict`、`temperature`、`top_p`、`stop`にマッピングしました(Ollamaでは`max_tokens`で期待されるパラメータ名が異なることに注意してください)。
- Ollamaクライアントの`chat`メソッドに`options`ディクショナリーを引き渡しました。`params`から`options`へのマッピングを取り扱う新たなプライベートメソッド`_prepare_options`を作成していることに注意してください。カスタムロジックに対応しつつも、コードをきれいで整理した状態に保つためにカスタムの`ChatModel`にメソッドを追加することができます。
- `seed`の値が`params`ディクショナリーにあるかどうか`custom_inputs`をチェックしています。これは次のセクションで詳細を説明します。

これで、以前と同じように、MLflowにこのモデルを記録し、ロードし、試すことができます:

```py
code_path = "ollama_model.py"

with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        "ollama_model",
        python_model=code_path,
        input_example={
            "messages": [{"role": "user", "content": "Hello, how are you?"}]
        },
    )

loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)

result = loaded_model.predict(
    data={
        "messages": [{"role": "user", "content": "What is MLflow?"}],
        "max_tokens": 25,
    }
)
print(result)
```

以下が返却されます:

```py
{
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "MLflow is an open-source platform that provides a set of tools for managing and tracking machine learning (ML) model deployments,",
            },
            "finish_reason": "stop",
        }
    ],
    "model": "llama3.2:1b",
    "object": "chat.completion",
    "created": 1730724514,
}
```

これで、ChatModelの入力スキーマから`max_tokens`をOllamaクライアントの`num_predict`パラメーターに適切にマッピングし、期待したトークン数のレスポンスを受け取っています。

## カスタムパラメータの引き渡し

ビルトインの推論パラメータに含まれていないカスタムパラメータを引き渡したい場合にはどうしたらいいのでしょうか？ChatModel APIはそのままのモデルに引き渡されるキーバリューペアのディクショナリーを受け付ける`custom_inputs`キーを通じてそれを行う方法を提供します。キーとバリューの両方は文字列である必要があるので、`predict`メソッドで型変換に対応する必要が出てくることがあります。上の例では、`custom_inputs`ディクショナリーに`seed`キーを追加することで、カスタムの`seed`の値をOllamaモデルが使えるように設定しました:

```py
if params.custom_inputs is not None:
    options["seed"] = int(params.custom_inputs.get("seed", None))
```

これを含めたことで、`predict`メソッドの`custom_inputs`キーを通じて、`seed`の値を引き渡すことができます。同じシード値で複数回`predict`メソッドをコールすると、常に同じレスポンスを受け取ります。

```py
result = loaded_model.predict(
    data={
        "messages": [{"role": "user", "content": "What is MLflow?"}],
        "max_tokens": 25,
        "custom_inputs": {"seed": "321"},
    }
)

print(result)
```

以下が返却されます:

```py
{
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "MLflow is an open-source software framework used for machine learning model management, monitoring, and deployment. It's designed to provide",
            },
            "finish_reason": "stop",
        }
    ],
    "model": "llama3.2:1b",
    "object": "chat.completion",
    "created": 1730724533,
}
```

:::note info
**ティップ**

ChatModelの使用 vs. 定義

ChatModelを*使う*際にデータを引き渡す方法と、ChatModelを*定義する*際にデータにアクセスする方法の間には重要な違いがあります。

インスタンスが作成されたChatModelを*使う*際には、メッセージ、パラメータなどのすべての引数は単一のディクショナリーとして`predict`メソッドに引き渡されます。

```py
model.predict({"messages": [{"role": "user", "content": "Hello"}], "temperature": 0.7})
```

一方、カスタムChatModelの`predict`メソッドを*定義する*際には、別々の`messages`と`params`引数を通じてデータにアクセスします。ここで、`messages`は`ChatMessage`オブジェクトのリストであり、`params`は`ChatParams`オブジェクトとなります。この違いを理解すること、すなわちユーザーには統合された入力、開発者には構造化されたアクセス、はChatModelを効果的に取り扱うためには重要なこととなります。
:::

# PyFuncとの比較

`ChatModel` APIを通じてチャットモデルをセットアップすることと、`PythonModel` APIを用いることのいくつかのメリットとトレードオフを説明するために、`PythonModel`として上述のモデルを実装するとどうなるのかを見てみましょう。

## Ollamaモデルバージョン3: カスタムPyFuncモデル

```py
# if you are using a jupyter notebook
# %%writefile ollama_pyfunc_model.py

import mlflow
from mlflow.pyfunc import PythonModel
from mlflow.types.llm import (
    ChatCompletionRequest,
    ChatCompletionResponse,
    ChatMessage,
    ChatChoice,
)
from mlflow.models import set_model
import ollama
from ollama import Options
import pandas as pd
from typing import List, Dict


class OllamaPyfunc(PythonModel):
    def __init__(self):
        self.model_name = None
        self.client = None

    def load_context(self, context):
        self.model_name = "llama3.2:1b"
        self.client = ollama.Client()

    def _prepare_options(self, params):
        options = {}
        if params:
            if "max_tokens" in params:
                options["num_predict"] = params["max_tokens"]
            if "temperature" in params:
                options["temperature"] = params["temperature"]
            if "top_p" in params:
                options["top_p"] = params["top_p"]
            if "stop" in params:
                options["stop"] = params["stop"]
            if "seed" in params:
                options["seed"] = params["seed"]

        return Options(options)

    def predict(self, context, model_input, params=None):
        if isinstance(model_input, (pd.DataFrame, pd.Series)):
            messages = model_input.to_dict(orient="records")[0]["messages"]
        else:
            messages = model_input.get("messages", [])

        options = self._prepare_options(params)
        ollama_messages = [
            {"role": msg["role"], "content": msg["content"]} for msg in messages
        ]

        response = self.client.chat(
            model=self.model_name, messages=ollama_messages, options=options
        )

        chat_response = ChatCompletionResponse(
            choices=[
                ChatChoice(
                    index=0,
                    message=ChatMessage(
                        role="assistant", content=response["message"]["content"]
                    ),
                )
            ],
            model=self.model_name,
        )

        return chat_response.to_dict()


set_model(OllamaPyfunc())
```

これは、上で`ChatModel`を定義した方法と非常に似ており、実際のところ、同じOllamaモデルをサービングするのにこの`PythonModel`を使うことができます。しかし、いくつかの重要な違いが存在します:

- 最終的には入力は単なるメッセージのリストであるにもかかわらず、入力データをpandasデータフレームとして取り扱う必要がありました。
- 事前に定義されている`ChatParams`オブジェクトとして推論パラメータを受け取るのではなく、`params`ディクショナリーを受け取っています。この結果の一つは、他の推論パラメータとは個別に`seed`を取り扱う必要がないということでした: これらは`PythonModel` APIにおいては、*すべてが*カスタムのパラメータとなります。
- `ChatCompletionResponse`ではなく、`ChatCompletionResponse`をディクショナリーに変換するために`chat_response.to_dict()`を呼び出す必要がありました。これは、`ChatModel`では自動的に対応されます。

モデルを記録する際に最大の違いのいくつかに直面します:

```py
code_path = "ollama_pyfunc_model.py"

params = {
    "max_tokens": 25,
    "temperature": 0.5,
    "top_p": 0.5,
    "stop": ["\n"],
    "seed": 123,
}
request = {"messages": [{"role": "user", "content": "What is MLflow?"}]}

with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        "ollama_pyfunc_model",
        python_model=code_path,
        input_example=(request, params),
    )
```

カスタムの[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)では、入力サンプルを用いてモデルシグネチャが推定できるように、入力サンプルを手動で定義する必要があります。これは、標準的なOpenAI互換の入力/出力/パラメータのスキーマに準拠するように自動的にシグネチャを設定するChatModel APIとの大きな違いとなります。入力サンプルに基づいたモデルシグネチャの自動推定に関しては、[GenAI model signature example](https://mlflow.org/docs/latest/model/signatures.html#genai-model-signature-example)をご覧ください。

また、モデルの`predict`メソッドの呼び出し方法にも特筆すべき違いがあります: メッセージを含むディクショナリーではなく、`params`キーワード引数を通じて、ディクショナリーとしてパラメータが引き渡されます。

```py
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)

result = loaded_model.predict(
    data={"messages": [{"role": "user", "content": "What is MLflow?"}]},
    params={"max_tokens": 25, "seed": 42},
)
print(result)
```

これは以下を返却します:

```py
{
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "MLflow is an open-source platform for machine learning (ML) and deep learning (DL) model management, monitoring, and",
            },
            "finish_reason": "stop",
        }
    ],
    "model": "llama3.2:1b",
    "object": "chat.completion",
    "created": 1731000733,
}
```

まとめると、`ChatModel`は標準化され、OpenAI互換の入出力にフォーカスすることで、カスタムチャットモデルの定義に対してより構造化されたアプローチを提供します。`ChatModel`のスキーマと螺っぽいんぐするアプリケーション間での入力/出力スキーマをマッピングするためのちょっとしたセットアップの手間はありますが、入力/出力/パラメータのスキーマの定義が多くの場合困難になるタスクに対応しなくてはならない完全にカスタムの[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)よりも取り扱いが簡単になる場合があります。一方、[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)のアプローチは、最大の柔軟性を提供しますが、開発者は入力/出力/パラメータのマッピングロジックのすべてを手動で対応する必要があります。

# まとめ

このガイドでは、以下のことを学びました:

- あなたのアプリケーションロジックを`ChatModel`の入力/出力スキーマにマッピングする方法。
- ChatModelでサポートされる定義済みの推論パラメータの使い方。
- `custom_inputs`を用いて、ChatModelにカスタムのパラメータを渡す方法。
- カスタムチャットモデルを定義する際の[ChatModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatModel)と[PythonModel](https://mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)の比較。

これで、ChatModel APIが何であって、カスタムチャットモデルを定義する際にどのように使えるのかについて感触がつかめたかと思います。

`ChatModel`には、この入門ガイドでカバーしていない以下のようなその他の機能があります:

- 特にモデルが複数のコンポーネントやLLMのAPIを呼び出すモデルにおいて、あなたのチャットモデルをデバッグ、モニタリングする際に有用なMLflow Tracingのネイティブサポート。
- 外部設定ファイルを用いたモデル設定のカスタマイズのサポート。

これらの機能や、ChatModel APIのその他の高度な機能を学ぶには[こちらのガイド](https://mlflow.org/docs/latest/llms/chat-model-guide/index.html)をご覧ください。