(2024年版)AIの出力形式が安定しない？そんなときはFunction Calling機能を使おう

## 出力が安定しなくて泣きそう、そんな経験ありませんか？

自前のアプリでChatGPT等の自然言語系のAIから返ってくる値を利用したい！だけど…

- 同じプロンプトにもかかわらず、出力が安定せずに上手くパース出来ない
- 「以下のJSON形式で」って言っているのに、謎なコメントを挟んできたりしてパース出来ない
- 例外的なパータンでAIが気を利かせすぎて予想外の出力をしちゃう
- 配列を返して欲しいのに、配列じゃないものを返してくる
- どんなに頑張っても数十件に1件くらい、変な出力が混ざってくる
- プロンプトに色々細かい条件を付け加えて行くも、結局解決せずドツボにハマる

そんな貴方にオススメなのが「Function Calling」、滅茶苦茶出力が安定します。

## Function Callingを使おう

2024年7月時点でChatGPTの場合はベータ版ですが、アシスタント(AIからの出力)に対するFunction Callingという機能が出てきています。出力する形式を型など含めて細かく指定することが可能になっています。

https://platform.openai.com/docs/assistants/tools/function-calling/quickstart

Claude3の場合もtool-use(Function Calling)が使えます。

https://docs.anthropic.com/en/docs/build-with-claude/tool-use

他のAIは調べていません。ごめんなさい


## 実際に使ってみた(Claude3)

今回はClaude3をAWS Bedrock経由、Rubyアプリケーションで「カレー店のアンケートサンプル生成器」を作ってみました。

```ruby:ツール設定
tool_name = "curry_shop_questionnaire_generator"
description = "あなたはカレー店で食事をした人です。食後に自由記載のアンケートを求められました。"

tool_definition = {
  "name": tool_name,
  "description": description,
  "input_schema": {
    "type": "object",
    "properties": {
      "results": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "curry_score": {"type": "integer", "description": "カレーの味に関しての5段階評価、1~5の自然数、5が良い、1が悪い"},
            "curry_comment": {"type": "string",   "description": "カレーの味に関してのフリーコメント"},
            "others_score": {"type": "integer", "description": "味以外の事に関しての5段階評価、1~5の自然数、5が良い、1が悪い"},
            "others_comment": {"type": "string",   "description": "味以外の事に関してのフリーコメント"},
          },
          "require": ["curry_score", "curry_comment", "others_score", "others_comment"]
        },
      },
    },
    "required": ["results"],
  },
}
```

ポイントは、アンケートの形式をJSONで書ける事。今回はカレーの味とその他全般に関して、5段階評価＋フリーコメントの合計4設問のアンケートを設定してみました。


```ruby:AIへの入力
bedrock_client = Aws::BedrockRuntime::Client.new(region: 'us-east-1', credentials: Aws::ECSCredentials.new)

target_text = "アンケートのポジティブな回答を5件分、適当に作ってください。"


prompt = "<text>#{target_text}</text>#{tool_name}ツールのみを利用すること"

messages = [
    {
        "role": "user",
        "content": [{"type":"text", "text": prompt}],
    }
]

body_hash = {
  "anthropic_version": "bedrock-2023-05-31",
  "max_tokens": 3000,
  "messages": messages,
  "tools": [tool_definition],
  "tool_choice": {type: "tool", "name": tool_name}
}

body = body_hash.to_json

response = bedrock_client.invoke_model({
  accept: "application/json",
  content_type: "application/json",
  body: body,
  model_id: "anthropic.claude-3-haiku-20240307-v1:0"
})


answer_hash = JSON.parse(response.body.string)
```

という出力を投げた所、以下のような結果が返ってきました。

```ruby:AIからの出力
{"id"=>"msg_bdrk_xxxxxxxxxxxxxxxxx",
 "type"=>"message",
 "role"=>"assistant",
 "model"=>"claude-3-haiku-20240307",
 "content"=>
  [{"type"=>"tool_use",
    "id"=>"toolu_bdrk_xxxxxxxxxxxxxxxxx",
    "name"=>"curry_shop_questionnaire_generator",
    "input"=>
     {"results"=>
       [{"curry_score"=>5, "curry_comment"=>"とてもおいしかったです。店の雰囲気もよく、スタッフの接客も素晴らしかったです", "others_score"=>4, "others_comment"=>"店内が清潔で落ち着いた雰囲気でした。また来たいと思います。"},
        {"curry_score"=>4, "curry_comment"=>"美味しかったです。辛さもちょうど良く、量も丁度良かったです。", "others_score"=>5, "others_comment"=>"スタッフの対応がとても良く、安心して食事ができました。"},
        {"curry_score"=>5, "curry_comment"=>"とてもおいしかったです。スパイシーな味付けが印象的で、また来たくなりました。", "others_score"=>4, "others_comment"=>"店内が清潔で過ごしやすかったです。また利用したいと思います。"},
        {"curry_score"=>4, "curry_comment"=>"美味しい料理を楽しめました。スタッフの方も親切で丁寧な接客でした。", "others_score"=>5, "others_comment"=>"落ち着いた雰囲気の中、ゆっくりと食事ができました。また来たいと思います。"},
        {"curry_score"=>5, "curry_comment"=>"とてもおいしかったです。香り豊かな味わいに満足しました。店の雰囲気も良かったです。", "others_score"=>4, "others_comment"=>"店内が清潔で快適でした。また利用したいと思います。"}]}}],
 "stop_reason"=>"tool_use",
 "stop_sequence"=>nil,
 "usage"=>{"input_tokens"=>1062, "output_tokens"=>559}}
```

きっちり指定したJSONの結果で返ってきています。これなら、出力結果を簡単にパースして色々使えます。


試しに、ネガティブなアンケートに変えて5件出力させてみました。

```ruby:ネガティブなAI出力
{"id"=>"msg_bdrk_xxxxxxxxxxxxxxxxx",
 "type"=>"message",
 "role"=>"assistant",
 "model"=>"claude-3-haiku-20240307",
 "content"=>
  [{"type"=>"tool_use",
    "id"=>"toolu_bdrk_xxxxxxxxxxxxxxxxxx",
    "name"=>"curry_shop_questionnaire_generator",
    "input"=>
     {"results"=>
       [{"curry_score"=>2, "curry_comment"=>"スパイシーすぎて辛かった。もっと控えめな味付けがいいと思う。", "others_score"=>3, "others_comment"=>"店内の雰囲気が暗く、居心地が悪かった。"},
        {"curry_score"=>3, "curry_comment"=>"具材がバランス良くなかった。肉が少なく、野菜が多すぎた。", "others_score"=>2, "others_comment"=>"注文から提供まで時間がかかりすぎた。効率が悪いと感じた。"},
        {"curry_score"=>2, "curry_comment"=>"カレーの味付けが濃すぎて飽きてきた。もっさりした感じがした。", "others_score"=>3, "others_comment"=>"店員の対応が冷たく、サービスが悪かった。"},
        {"curry_score"=>3, "curry_comment"=>"ルーの質感がぼそぼそしていて好みではなかった。", "others_score"=>2, "others_comment"=>"店内の清潔さが気になった。ちょっと汚かった感じがした。"},
        {"curry_score"=>2, "curry_comment"=>"カレーの味が全体的に薄く、物足りなかった。", "others_score"=>3, "others_comment"=>"店の広さに比べてテーブルが狭く、ゆったり食べられなかった。"}]}}],
 "stop_reason"=>"tool_use",
 "stop_sequence"=>nil,
 "usage"=>{"input_tokens"=>1062, "output_tokens"=>520}}
```

全く同じ形式で出力されるので、機械的な処理でエラーが出ること無くなりそうです！