こんにちは、MNTSQでエンジニアをやっている平田です。 MNTSQでは 自然言語処理 を使って契約書を解析したり検索したりする機能を開発しています。 契約書解析には、次のようなタスクがあります。 秘密保持契約等の契約類型に分類 契約締結日や契約当事者等の基本情報を抽出 条項(第1条, 第2条, ...)単位で分解 本稿では、これらの契約書解析タスクをGPT-4oに解かせてどんな結果になるか見てみます。 ざっくりやり方 GPT-4oの API を呼び出すところ ここではAzure OpenAIのGPT-4oを使います。 Microsoft のサンプルコードほぼそのままですが、一応貼り付けておきます。 from openai import AzureOpenAI client = AzureOpenAI( api_version= "2023-05-15" , azure_endpoint=os.getenv( "AZURE_OPENAI_ENDPOINT" ), api_key=os.getenv( "AZURE_OPENAI_API_KEY" ), ) response = client.chat.completions.create( model= "gpt-4o" , messages=[{ "role" : "user" , "content" : "ここにプロンプトを入れる" }], temperature= 0 , ) completion = response.choices[ 0 ].message.content 解析結果を JSON 形式で出力させるプロンプトテンプレート 解析結果をソフトウェアで扱いやすくするために、 JSON 形式で出力させます。 JSON 形式で出力させるテクニックはいくつかありますが、ここではプロンプトで JSON スキーマ を渡します。 プロンプトのテンプレートは次のようなイメージです。 prompt_template = """ \ {content} Transform the above text into a JSON according to the following JSON schema. Output in a code block, and do not output anything else. ```json {json_schema} ``` """ content : 契約書本文 json_schema : 解析結果の JSON スキーマ Pydanticで JSON スキーマ を生成するところ Pydanticは JSON スキーマ の生成とバリデーションができるので、今回やりたいことにぴったりです。 docs.pydantic.dev Pydanticを使わずに JSON スキーマ を直接書いてもよいのですが、 JSON スキーマ は複雑すぎて少なくとも私は読みたくないですし、 JSON スキーマ 自体のバリデーションが必要になるので Python で書いてmypyに静的解析させるほうが楽だと思います。 Pydanticで次のようなデータモデルを定義します。 from pydantic import BaseModel, Field class ClauseResponse (BaseModel): number: str | None = Field(default= None , description= "条番号" ) heading: str | None = Field(default= None , description= "条見出し" ) class ContractResponse (BaseModel): document_name: str | None = Field(default= None , description= "ドキュメントのタイトル" ) is_contract: bool = Field(description= "契約書のテキストかどうか" ) execution_date: str | None = Field(default= None , description= "契約を締結した日" ) effective_date: str | None = Field(default= None , description= "契約の効力が発生した日" ) renewal_term: str | None = Field(default= None , description= "契約の自動更新期間" ) notice_to_terminate_renewal: str | None = Field(default= None , description= "契約の自動更新を終了するための条件" ) governing_law: str | None = Field(default= None , description= "契約の準拠法" ) parties: list [ str ] = Field(default_factory= list , description= "契約の当事者名リスト" ) clauses: list [ClauseResponse] = Field(default_factory= list , description= "契約の条項リスト" ) JSON スキーマ は ContractResponse.model_json_schema() で生成できます。 { "$defs": { "ClauseResponse": { "properties": { "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "条番号", "title": "Number" }, "heading": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "条見出し", "title": "Heading" } }, "title": "ClauseResponse", "type": "object" } }, "properties": { "document_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "ドキュメントのタイトル", "title": "Document Name" }, "is_contract": { "description": "契約書のテキストかどうか", "title": "Is Contract", "type": "boolean" }, "execution_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "契約を締結した日", "title": "Execution Date" }, "effective_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "契約の効力が発生した日", "title": "Effective Date" }, "renewal_term": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "契約の自動更新期間", "title": "Renewal Term" }, "notice_to_terminate_renewal": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "契約の自動更新を終了するための条件", "title": "Notice To Terminate Renewal" }, "governing_law": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "契約の準拠法", "title": "Governing Law" }, "parties": { "description": "契約の当事者名リスト", "items": { "type": "string" }, "title": "Parties", "type": "array" }, "clauses": { "description": "契約の条項リスト", "items": { "$ref": "#/$defs/ClauseResponse" }, "title": "Clauses", "type": "array" } }, "required": [ "is_contract" ], "title": "ContractResponse", "type": "object" } GPT-4oが出力した JSON 文字列を抽出する プロンプトで Output in a code block と指示しているので、GPT-4oはコードブロックで囲われた JSON を出力します。 ここでは 正規表現 を使ってコードブロックから JSON 文字列を抽出します。 import re json_pattern = re.compile( r"```json\n(.+?)\n```" , re.DOTALL) if m := json_pattern.search(completion): json_text = m.group( 1 ) Pydanticで JSON 文字列をバリデーションするところ Pydanticを使うと JSON 文字列が JSON スキーマ に従っているかバリデーションできます。 json_object = ContractResponse.model_validate_json(json_text) もしバリデーションでエラーになった場合は、次のようなテンプレートでエラーメッセージをプロンプトに追加するとうまく修正してくれることがあります。(まあGPT-4oだとほぼ完璧な JSON を返してくれるので使う機会のないテクニックですが...) """ \ The following errors occurred, fix it. ``` {errors} ``` """ 実際にやってみた MNTSQにたくさんある契約書サンプルの1つを使って実験してみます。 契約書サンプル(先頭と末尾のページ) 上記契約書から OCR で抽出したテキストを入力すると、GPT-4oから次のような出力が返ってきます。 ```json { "document_name": "金銭消費貸借契約書", "is_contract": true, "execution_date": "2021-04-20", "effective_date": "2021-04-30", "renewal_term": null, "notice_to_terminate_renewal": null, "governing_law": null, "parties": [ "株式会社フォイエルバッハ商事", "MNTSQ株式会社", "板谷隆平" ], "clauses": [ { "number": "1", "heading": "消費貸借" }, { "number": "2", "heading": "借入条件" }, { "number": "3", "heading": "連帯保証" }, { "number": "4", "heading": "期限の利益の喪失" }, { "number": "5", "heading": "届出義務" }, { "number": "6", "heading": "反社会的勢力の排除" }, { "number": "7", "heading": "公正証書の作成" }, { "number": "8", "heading": "費用負担" }, { "number": "9", "heading": "管轄" } ] } ``` GPT-4oの出力から JSON 文字列を抽出し、Pydanticのデータモデルでバリデーションしたところ、エラーなく読み込めました。 解析結果の値も完璧です 🚀 実は execution_date (締結日)と effective_date (発効日)って別物なのですが、これらをちゃんと区別できていますね。すごい! 感想 ちょっと前まで、契約書解析は次のような工程で開発していて、1機能リリースするのに数ヶ月かかることが通常でした。 アノテーション モデリング テスト リリース ChatGPTを使った開発では、 アノテーション と モデリング の代わりにプロンプトエンジニアリングを行います。 これにより次のような嬉しさ(開発のスケールアウト)があります。 数ヶ月かかっていた アノテーション と モデリング が数日になる MLエンジニアがいないとできなかった モデリング が非エンジニアでもできるようになる GPT-4でも同様の開発はできるのですが、 API の料金が高く選択肢に入りにくい状況でした。 一方、GPT-4oはGPT-4に比べて破壊的に安く、選択肢に入るビジネスモデルが大幅に増えたのではないかと思います。 Azure OpenAI Serviceの価格表 (執筆時点) こうやってできることが増えていくとワクワクしますね 🙌 残る課題 これでAIが仕事を奪ってくれて明日から遊べるぞ!!!!とはならないのが残念なのですが、実際に契約書解析のプロダクトでGPT-4oを使う上でどんな課題があるか、いくつか挙げてみます。主に契約書というデータが長過ぎることに起因するものです。 GPT-4oの トーク ン制限数 GPT-4oの入力 トーク ン数は最大128kです。やばいですね。一方、契約書の トーク ン数はだいたい1ページ500 トーク ンです。MNTSQには数百ページの契約書が入ってきたりするのですが、仮に300ページとすると150k トーク ン必要なのでそのままプロンプトに入力するにはちょっと足りないのです。 また、GPT-4oの出力 トーク ン数は最大4kです。条項数が100を超える契約書もあるので、本稿で紹介した解析結果を得るには心許ない数字です。 Lost in the Middle こちらの論文 にもあるように、プロンプトの中間にある情報は忘れられがちです。契約書は長いのでこの問題が結構クリティカルです。 GPT-4oの料金 いくら安くなったとはいえ、128k トーク ン全部使い切るようなプロンプトを入力すると1回のリク エス トで100円くらいかかるので、サービスのプライシングロジックに配慮した戦略が必要です。 GPT-4oのターンアラウンドタイム 爆速!と言われたGPT-4oですが、60ページくらいの契約書データを使ってプロンプト入力から JSON 出力の時間を測ると1分くらいでした。サービスで提供したい体験によっては許容できない可能性があります。 他にもありますが、MNTSQではこのような課題と向き合いながらLLMを活用したプロダクト開発を行っています。 もしこの記事に興味をお持ちいただいて、「他のデータだとどうなの?」とか「課題は解決したの?」とか聞いてみたい方がいらっしゃいましたらお気軽にDM *1 等でお問い合わせください。 この記事を書いた人 Takumi Hirata MNTSQの アルゴリズム エンジニア。流離の なんでも屋 。 *1 : https://x.com/_hrappuccino