OllamaでStructured Outputを試してみた

## はじめに
企業内の機密データをインターネットに出さず、生成AIソリューションを構築するツールの1つとして、Ollamaがあります。
このツールは、インターネット接続なしでオンプレミス上のサーバやローカルマシン上で多様なオープンソースの生成AIを実行することができます。
今回、Ollamaにおいて2024年12月6日にサポートされたStructured Outputを試してみました。

## Structured Outputとは
Structured Outputは、生成AIの出力を予め定義された形式で構造化するための機能です。
この機能には以下のような特徴があります：

### 主な特徴
- 出力フォーマットの制御
  - スキーマに従った一貫性のある出力を生成
  - 複数回の生成でも同じ構造を維持
- システム統合の容易さ
  - 構造化されたデータを直接プログラムで扱える
  - 後続の処理フローへの連携が容易
  - バッチ処理との親和性が高い

このStructured Outputにより、会話などの非構造化データを構造化データとして変換でき、
業務システムやワークフローに確実に組み込むことが期待できます。

### 検証の動機
現在、オンプレミス上で音声会話データから重要な情報を抽出・分類するシステムを構築しており、会話内容の自動分析結果に応じて適切なワークフローを選択できる仕組みの実装を進めています。

この音声会話データを生成AIで処理するプロセスにおいて、以下の課題がありました：
- プロンプトで指示したJSONスキーマに従わない
- 非構造化データ抽出、分類

これらの課題に対し、Structured Outputが有効な解決策になるかを確認します。

### 期待する結果
Structured Outputを利用することで、以下のような構造化データを得られることを期待し確認を進めます：

### 入力内容
```
「商品返品に関する品質クレームが発生しました。緊急対応が必要ですので、品質管理部門に通知し、24時間以内に対応をお願いします。」
```

### 出力結果
```json
{
    "keywords": ["商品返品", "品質クレーム", "緊急対応"],
    "category": "クレーム対応",
    "priority": "高",
    "next_action": "品質管理部門への通知",
    "required_response_time": "24時間以内"
}
```

## ためしてみる
まずは、Ollama公式のドキュメントに従い、試してみます。

1. 最新バージョンであることを確認する
```
ollama -v
ollama version is 0.5.3
```

2. ローカル上で、curlコマンドを実行する
```
curl -s -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{        "model": "llama3.1",
  "messages": [{"role": "user", "content": "Tell me about Canada."}],
  "stream": false,
  "format": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string"
      },
      "capital": {
        "type": "string"
      },
      "languages": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "name",
      "capital",
      "languages"
    ]
  }
}' | jq .message
```

3. requiredで指定したプロパティが出力されたことを確認できました
```
{
  "role": "assistant",
  "content": "{ \"name\": \"Canada\", \"capital\": \"Ottawa\", \"languages\": [\"English\", \"French\", \"First Nations languages\", \"Indigenous sign languages\", \"others including Punjabi, Spanish and many others as well.\" ]\n}\n  \t\t\t    \t\t\t"
}
```


## 実環境を想定したプロンプト
ここまでStructured Output動作が確認できたので、次は実際の利用を想定した
コールセンターの会話のやりとり（ダミーデータ）をプロンプトに入力してみます。

```
curl -s -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{        "model": "llama3.2:3b",
  "messages": [{"role": "user", "content": "ユーザーID: U12345、名前: 山田です。料金プランについて教えてください。現在のプランは何ですか？スマホのデータプランです。データ容量はどのくらい必要ですか？10GBあれば 十分です。10GBのプランは月額4,000円です。変更しますか？はい、お願いします。手続きが完了しました。ありがとうございます。お困りのことがあればいつでもお電話ください。"}],
  "stream": false,
  "temperature": 0.0,
  "format": {
    "type": "object",
    "properties": {
      "userId": {
        "type": "string"
      },
      "userName": {
        "type": "string"
      },
      "phoneNumber": {
        "type": "string"
      },
      "serviceName": {
        "type": "string"
      },
      "content": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "userId",
      "userName",
      "phoneNumber",
      "serviceName",
      "content"
    ]
  }
}' | jq .message
```


#### 出力結果
以下が出力結果になります。
会話内容から、指定したJSONスキーマに従い、出力されていることがわかります。
また、指定したプロパティで、ユーザID(userId)、名前(userName)、サービス(serviceName)に関する内容が会話内容からうまく抽出できています。一方、内容(content)については、もう一工夫必要に見えます。複数回実行したところ、一意に決まるようなプロパティの内容については、大きな揺れはありませんでした。しかし内容のような複数個出力が想定されるものは、回答数の変化や揺れが見られました。
```
{
  "role": "assistant",
  "content": "{\"userId\": \"U12345\", \"userName\": \"山田さん\", \"phoneNumber\": \"03-1234-5678\", \"serviceName\": \"スマホデータプラン\", \"content\": [\"データ容量: 10GB\", \"月額料金: 4000円\", \"変更しました\"]}"
}
```

## まとめ
今回、Ollama Structured Outputがサポートされたため、
音声会話データをプロンプトに入力しその動作を確認しました。
一意に決まるものについては、有用であることが確認できました。
複数回答があるものについては、もう一工夫が必要なことを確認できました。