Gunosyデータ分析ブログ

Gunosyで働くデータエンジニアが知見を共有するブログです。

GPT から Claude 3 への移行ガイド

こんにちは。Gunosy R&D チームの森田です。

GPT-4o が発表されたこのタイミングで!?という向きもあるかとおもいますが、LLMの世界は一ヶ月もすればまったく違う状況になっているのが常なので、いずれは GPT-4o を超えるモデルが発表される時も来るでしょう。 Claude 3 Opus は一時期 GPT-4 のスコアを超え、 Claude 3 Haiku では GPT-3.5-Turbo のトークン当たりで約半額とコストパフォーマンスに優れていますし、 AWS Bedrock 経由で安定して利用できることもあり、Claude 3 は乗り換え先の候補の一つです。

Claude 3 への乗り換えには、点々とつまづくポイントがあるので、引っかかった所と回避方法をご紹介します。 今回紹介する内容はClaude 3に限らないものもありますので、ローカルLLM や他のLLM への乗り換えでも参考になるかと思います。

Claude 3 の紹介

Claude 3 は Anthropic が 2024年3月に提供を開始した比較的新しい LLM です。GPT-3.5 や GPT-4 と比べた場合に特にウリとなるのは以下の3点かと思います。

コストパフォーマンス

特に Haiku では GPT-3.5-Turbo と比べても半額程度の設定になっており、Llama 3 (8B) よりも入力コストは下となります。LLM コストを削減したい場合にはかなり魅力的な価格です。

Model Price per 1,000 input tokens Price per 1,000 output tokens
Mistral 7B 0.00015 USD 0.0002 USD
Claude 3 Haiku 0.00025 USD 0.00125 USD
Llama 3 Instruct (8B) 0.0004 USD 0.0006 USD
gpt-3.5-Turbo 0.0005 USD 0.0015 USD
Command R 0.0005 USD 0.0015 USD
Claude 3 Sonnet 0.003 USD 0.015 USD
gpt-4o-2024-05-13 0.005 USD 0.015 USD
Claude 3 Opus 0.015 USD 0.075 USD

(出典: Amazon Bedrock の料金、Pricing| OpenAI )

Vision への対応

安価ながらも vision に対応しているため、気軽に画像を入力として扱うことができます。 条件にもよりますが、画像 1枚あたり約 200 token 程度 (約 $0.00005) の換算になり、ほぼコストを気にすること無く使えます。

コンテクスト長が長い

200k トークンまで受け付けることができるため、通常の使い方ではまず不足することはないでしょう。 最近のLLM はどれも受け付けるコンテクスト長は長くなってきていますが、今のところ実用可能なモデルの中では最長です。

GPT から乗り換える時のつまづきポイント

プロンプトの書き方

GPTからの乗り換えで主に考える必要があるのは、XMLタグを使うことと、Prefill と呼ばれる回答形式を強制させるテクニックです。GPTのプロンプトそのままでは、特に出力の制御が難しくなります。

XMLタグで構造を明示する

GPT ではおおよそ人が分かりやすいフォーマットであれば、マークダウンやタグ、章立てや、果ては Lisp 形式までかなり自由度が高い記述ができました。Claude 3 では、フォーマットとして xml のみが推奨されています。

XML といっても、<?xml version="1.0" encoding="UTF-8" ?> から始めるような厳密なフォーマットがあるわけではなく、タグ名もあらかじめ決まったものはありません。 <instruction>や <input> など中身が何かが読んで分かるタグ名を考えて利用することができます。

タグのネストにも対応してくれますが、パーサと違って深いネストは LLM では扱いにくいようです。公式ドキュメントによると、おおよそ 5 階層までが目安とされています。 また、テンプレートやフォーマットの指定などで、どこにどんな内容を入力・出力させるかを指定する場合には {{VARIABLE}} のような二重ブラケットで内容を指定したプレースホルダをつくることができます。

XMLタグを使ったプロンプトの例

User:

<instruction>
つぎの doc から、人名を抽出し、 <name></name> タグで囲んで output format に従い出力してください
</instruction>

<output format>
<xml>
  <name>{{PERSON NAME}}</name>
  <name>{{PERSON NAME}}</name>
</xml>
</output format>

<doc>
英語圏で,ジョン・スミス (John Smith) は架空の名称として使われます.
</doc>

Assistant:

<xml>
  <name>ジョン・スミス</name>
  <name>John Smith</name>
</xml>

Prefill で回答形式を固定する

最近の GPT では出力フォーマットを指定するとかなり素直に従ってくれるのですが、Claude 3 はまだじゃじゃ馬感が強いようです。 特に、回答が難しい場合に、指定したフォーマットに加えて一言コメントを追加してしまうことが多く観測されます。 例えば、先程の例で doc として人名の含まれていない文章を渡すと、フォーマットを無視して「人名が含まれていません」のようなコメントを返してしまうことがあります。 そういった場合も、出力をXMLタグに囲ませておけばある程度コメントとそうでない部分を分けてリカバリはしやすいものの、無駄な出力は避けたいですし、予期せぬメッセージはエラーの原因にもなります。

Prefill は LLM にフォーマットをある程度強制させるためのテクニックの1つで、LLM が出力する出だしをあらかじめ指定しておく方法です。

Claude 3 へプロンプトを渡す際には、メッセージのロールを指定することができます。 ロールは通常の user のほかに、systemとして出力には含めたくない情報の他、LLM自身の出力を表す assistant を指定することができます(これはGPT でも同様)。 prefill は、プロンプトの末尾に出力の先頭部分を assistant をロールとして含めます。

prefill の例

Claude 3 への入力

user: <instruction>つぎの doc から、人名を抽出し……</instruction>

assistant: <xml>

Claude 3 からの出力

assistant: <name>...</name> </xml>

Claude 3 に出力させたい、<xml> タグの先頭をあらかじめ指定しておくことで、xml を必ず出力させるように強制しています。先頭の <xml> タグは入力側に含めたため、出力には含まれないことに注意が必要です(パースの際は自分で補う必要があります)。

Prefill を使ったとしても、確信度が低い場合に </xml> の後にコメントしてしまうなど、余分な出力を出してしまうことは依然として残るため、</xml> タグ以降を削除するなど、後処理自体は必要となることに注意してください。

癖が強い Claude 3 の出力

JSON モード非対応

Claude 3 自体は JSON を出力させることができるものの、Bedrock 経由で利用する場合には、必ずパースできる JSON を返すよう強制する方法は今のところありません。Anthropic の API を直接使う場合には、 tool use の機能を使うことで(仮想のAPIへの入力として)パースできるJSONを強制させることができますが、残念ながら bedrock では未実装です(参考: https://docs.anthropic.com/claude/docs/legacy-tool-use#when-will-the-new-tool-use-format-come-to-vertex-ai-or-amazon-bedrock) 。

対策は後述します。

文字の正規化

地味に厄介な仕様で、Claude 3 は事実上一部の文字を出力できないようです。さらに全角記号は高い確率で半角に置き換えられてしまう傾向があります。入力の一部を抽出するような利用ケースでは、一部の文字が置き換えられてしまう可能性を考慮しなければいけません。この振る舞いは Claude 2 でも共通のようなので、Claude シリーズの癖として受け入れる必要があるようです。

置き換えられてしまう文字 置き換え先 備考
” U+201D Right Double Quotation Mark " U+0022 Quotation Mark 100% 置換 or 削除
“ U+201C Left Double Quotation Mark " U+0022 Quotation Mark 100% 置換 or 削除
‘ Left Single Quotation Mark ' U+0027 Apostrophe 100% 置換 or 削除
’ U+2019 Right Single Quotation Mark ' U+0027 Apostrophe 100% 置換 or 削除
!(全角記号) !(半角記号) 文脈次第で起こる
“ “(全角空白) 削除 文脈次第で起こる
「」()など 削除 文脈次第で起こる

ユーザの入力チェックに使う場合など原文に忠実である必要がある場合、元の文章から変化している箇所を検知して修正する必要がありますが、 ルールベースで書き戻すには、変化してしまう文字が多い上に確率的なので厄介です。 原文の一部を抜き出すケースでは、出力に類似した文字列を原文から探し出し置き換えてしまうこともできますが、文字列の始点や終点にあたる文字の置き換えが起こる可能性もあり、正しい範囲を抜き出すことは難しいです。 たとえば、原文が「ジョン・スミス(John Smith)」であるときに、LLMの出力した文字列が「ジョン・スミスJohn Smith」であった時、通常は終端の「)」を含めないほうが類似度が高くなってしまいます。

このようなアラインメントをするには、編集距離を使うのが便利です。(参考: https://www.cs.t-kougei.ac.jp/SSys/LevenS.htm) 原文との上記のLLMが起こしがちな変換を許す(コストを小さく or 0 とする)少し特殊な編集距離を計算し、編集距離の計算で出てくる編集系列をバックトレースすることで、LLMの出力を原文にもとづいた修正をすることができます。 一般的な編集距離では挿入・削除がコスト1、置換をコスト2とすることが多いですが、全角・半角の変換やクオーテーションの変換、記号の削除コストを小さく設定することで、LLMの癖は許容しつつハルシネーション的な 勝手な内容の追加・削除は見逃さない文字単位のアラインメントができます。

例:ダブルクォーテーションを削除する例

この例では、横が原文で縦がLLMの出力を表しています。右下から左上まで経路を逆にたどることで、AとBの間の「“」が削除されていることがわかり、経路で取った編集内容からLLMの出力をどう修正すれば原文に戻せるかがわかります。

APIの出力生成に関する仕様の違い

GPT では1つの入力に対して複数の出力を一度に生成して返すことができましたが、今のところ Claude 3 ではその機能は無いようです。複数の解を出力させて、voting で性能を上げようとする時には、入力からやり直す必要があります。同様に出力の確率値(GPTにおけるlogprob)も出せないため、選択問題のどの出力で確信度が高くなるか、のような評価は難しくなります。

JSON モード非対応への対策

複合的なタスクを解く場合や、コードの中で回答内の情報を利用する場合など、JSON のような構造化された出力はとても便利で、必須といっても過言ではありません。しかし、Claude 3 には前述の通り JSON モードが現状なく、正しい JSON 形式で出力されることが保証されないため、複雑度の高い出力や、特に任意の文字列が含まれる場合に厄介な問題を引き起こします。

取りうる戦略は、出力させたい内容によって変わってきます。

リトライ戦略

単純な JSON 形式や、文字列が JSON 内に含まれない場合にはほとんどの場合問題なく出力することができるため、もしパースできないJSONが出力された時には正しい JSON になるまでリトライするのが簡単でいい戦略だと思います。 戦略といっても、出力させる JSON を <json></json> タグで囲ませ、取り出した JSON のパースに失敗したらリトライ、を繰り返すだけです。 指定したキーの有無、数値のはずが文字列になっているなど、微妙なフォーマット違いは良く起こるため、後処理で修正するか、できるだけ細かくチェックしてリトライするなど対策をしておくのが望ましいです。

しかし、任意の文字列が含まれる場合には、リトライ戦略だけではうまくいきません。Claude 3 が出力する JSON は、文字列中に「”」が含まれても正しくエスケープしてくれず、何度リトライしても同じエラーを起こしてしまいます。 加えて Claude が文字の正規化を行い、全角の引用符なども「”」に変換されてしまうため、予期せぬところでエラーになる場合があります。

後述する戦略を使う場合もフォーマットのチェック、リトライなどは組み合わせる方が良いでしょう。

GPT にフォーマットを修正させる戦略

複雑度が比較的低い場合、JSONの出力が長すぎない場合に有効です。Claude 3 の出力したエラーを含むJSON形式を、GPT の JSON モードを使って正しいJSON形式に修正させます。汎用的に使えますが、コストがかかってしまうことと、複数のLLMを同時に扱う手間、JSONとしては正しくとも意図どおりに修正されている保証は結局ないところがデメリットです。

XMLで出力させて変換する戦略

JSON の代わりに XML で出力させる方法もあります。こちらも Claude 3 では エスケープを正しく扱ってくれないところはありますが、比較的対処がしやすいです。 XMLというフォーマット自体が厄介なところはあるものの、構造化した出力をさせる方法では今回紹介した3つの中で一番安定させることができます。 デメリットは、JSONに比べ冗長になりがちであり、深いネストでは性能が落ちることです。

XMLで出力させる場合、LLMの出力に対する前処理として一部の記号を & などの参照文字に置き換える必要があります。 Claude 3 は自分ではエスケープしてくれず、パース時にエラーになるためです。基本的には置換するだけで済みますし、XMLパーザーを使わず正規表現で必要な情報を取り出す際には不要です。

パースできるXMLが得られれば、xmltodict (python の場合)で dict 形式に変換して利用することができます。 ただし、注意する点が一つあり、XMLではフォーマット上要素が1つの場合にリストかどうかを判別することができないため、配列になるタグを force_list 引数であらかじめ指定する必要があります。(参考: Pythonライブラリxmltodict、XMLを辞書に変換してくれるなんて最高じゃん!🤗 触ってみたに使いこなしtipsを添えて )

XMLの例

<xml>
  <item> item 1 </item>
</xml>

force_list に何も指定しない場合に変換される dict

{"xml": 
  {"item": 
    "item 1"
  }
}

force_list に item を指定した場合に変換される dict

{"xml": 
  {"item": 
    ["item 1"] 
  }
}

まとめ

Claude 3 に乗り換える際に障壁になりやすい、プロンプトの書き方の違い、構造化された出力の扱い方についてまとめました。 GPT 以外のモデルに乗り換える際には、ある程度似た問題が生じるので、Llama 3など別のモデルに乗り換える際にも参考になれば幸いです。