LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

生成AI×Pythonによる契約レビューの自動化が自作できるか試してみた。

こちらは Livesense Advent Calendar 2025 DAY 25 の記事です。

 

 

自己紹介

こんにちは。株式会社リブセンスの経営推進部に所属するtoshiyukiと申します。

私は、普段は技術系の話とは全く関係のない法務の仕事をしている非エンジニアの会社員です。ただ、技術系のテーマが大好きで、簡単な業務ツールなどを自作したりしながら、かれこれ10年以上技術と戯れています。

最近は、特に法務オペレーションに関連する各種SaaSのAPIをつないだワークフローを作ったり、生成AIを使って法務オペレーションを楽にする方法がないか日々研究しています。

 

 

この記事で書くこと

この記事では、Geminiを使って契約レビュー業務を自動化できないか試してみた取り組みについて書きます。

この記事を読んでる人に僕と同じ法務をしている人がどのくらいいるかわかりませんが(超レアだと思う)、これまでシステムで自動化できなかった事務を生成AIを使ってDXすることに興味を持っている方に読んで頂ければ嬉しいです。

 

 

きっかけ

契約書とはロジックの塊を書いた文書であり、プログラムにも通じる構造をしています。一方、プログラムはコーディングの現場において誰でも生成AIを活用してゴリゴリ書くのが当たり前の時代になっているにもかかわらず、契約書は人手で書いたり直したりするのがいまだ主体と自動化に関する状況に大きな差があります。

確かに、契約レビュー業務を構成する「読む」「考える」「書く」という作業のうち、「読む」「考える」作業をプログラム実装するのは非常に難しいと思います。

例えば、「読む」作業については、プログラミング言語で書かれたコードと違って、契約書は自然言語で書かれており、表現方法に決まった型がなく、時にファジーさ(場合によっては文法的な誤り)があっても人間が意味解釈可能ならば許容される現実があるため、AIが登場する前はプログラムで契約書の意味を的確に読解することは困難でした。

また、「考える」作業についても、案件の様々な要因(状況、相手方、力関係等)に応じて最適解が異なり、柔軟にアプローチや結論を変えていくための「思考」をAIが登場する前はプログラムで実装することは困難でした。

しかし、これらの作業はいくらレビュー業務に慣れていても、時間がかかるし集中するので、結構すり減る作業で、生産性も上がりにくいです。(今でいう)バイブコーディングとまでいかなくても、何とか一定自動化できないのか?ということをもやもや感じておりました。

ところが、ここ数年で生成AIが登場し、自然言語処理や思考といったこれまで難しかったタスクをかなりの精度でAIにさせることができるようになりました。完全でなくても下準備程度でも自動化できたら「法務」の世界が変わるかも。その可能性を探りたいと思ったことから、今回の取組みを始めました。

 

 

ゴール

ふわっとした図ですが、契約書を入力すると生成AIによるレビューが行われて、その結果が修正履歴&コメント付きWordファイルなり、実務ですぐ使える形で入手できる。こういう仕組みを作れるか、実験を通じて探ってみるのが今回の取組みのゴールです。

契約レビュー業務は、四角四面に画一的な回答を常に返せればよい業務ではありません。先に触れた通り、状況、相手方、力関係等様々な要因によって常に最適解は異なるため、品質の高い結果を出すためには、関係者の利害や都合を織り込みながら高度で柔軟な調整(匙加減)が欠かせません。

この調整は、目下人間にしかできないことですが、先に触れた「読む」「考える」「書く」に多くの人手と時間が取られてしまうため、ややもすると品質最適化のために必要な調整業務に手が回らず、振り返ってみると改善の余地が多くあったと反省する案件が少なくありません。

仮に、下準備レベルでも自動化ができると、「読む」「考える」「書く」に割かれていた人手と時間を大幅に圧縮することができ、品質最適化のための業務に人間は集中することができるようになります。

今回の実験を通じて、こういう姿を生成AIを使って目指すことができるかを探ります。

 

 

環境

今回は、次の理由から、APIを利用した独自アプリケーションの開発ではなく、Enterprise環境のWeb UI(Gemini for Workspace)で実現できる範囲で実験しました。

① Gemini3(思考モード) の推論能力とコストのバランス

契約レビューには高度な文脈理解と論理的思考が求められます。Gemini3 (思考モード)の推論能力を最大限活用しつつ、長い契約書を読み込ませる際のトークンコストや利用制限を気にせず試行錯誤を繰り返したかったため、Enterprise環境のサブスクリプション内で利用できるWeb UIを実験環境に選びました。

また、長いプレイブックを扱う際に、Lost in the Middle現象(長いプロンプトを取扱う際にプロンプトの中間部分をAIモデルが無視してしまう現象)等の影響になるべく強いモデルを使いたかったという理由もあり、ロングコンテキスト性能に優れていると噂のGemini3(思考モード)を使いたいと思っていました。

② エンタープライズ環境でのセキュリティ担保

法務という機密性の高い情報を扱う特性上、実験段階とはいえ、入力データに関する守秘義務や学習に利用されないことが保証されていることが必要なため、Enterprise環境のサブスクリプション内で利用できるWeb UIを実験環境に選びました。
(※)今回の実験では、もちろんダミー用に作った契約書を使っています。

 

なお、一方で、GeminiをWeb UIだけ使うだけでは出力結果をワークフロー的に連結する処理につなぐことが(人手によるコピー&ペーストをしなくてはならなかったり)煩雑になります。

この点は、Native Message APIを利用したChrome拡張機能でPythonにつなげてテキスト出力やWordファイル書き出しをする仕組みを作り、動線を改善することでクリアしましたが、細かく書くと長くなってしまうので、今回の記事からは割愛し、別途の機会に記事で紹介できればと思います。

 

 

実装の全体像と工夫した点

実装にあたっては、契約レビューの実務を踏まえ、いくつか工夫しました。

① 工夫した点: AIだけでは難しい「法務特有の最適解の匙加減」

工夫した1つ目の点は、プレイブックなどで自社の暗黙知をパターン化したとしても、前述の最適解を探る匙加減(状況、相手方、力関係、社内から求められている事項などの影響を受けて同じ内容の契約書であっても、対応粒度が常に変わる)を出来る余地を織り込む必要があるということです。

いかにGeminiと言えども、事細かに状況等をプロンプティングしない限りは、そのような事情を配慮した回答を作れません。一方、この種の粒度加減は多分に人間が状況を読みながら毎回細かい調整を加えていくものです。この柔軟性をどうワークフローの動線に入れるか。

このことについては、ワークフロー内で「読む」AIと「考える」AIの間に人間がレビュー作業を行える動線を用意することにしました。

「読む」AIがリスクと方針を網羅的にまとめたテキストファイルを出力し、人間が状況を読みながら、必要な方針調整をテキスト上で行い、それを「考える」AIに渡す仕様にすれば、レビュー作業に必要な柔軟性を実現できますし、人手がかかることになっても、ゼロベースで「読む」「考える」をすることに比べれば、労力は遥かに少なくて済みます。(クリックして拡大して下さい)

② 工夫した点: AIだけでは難しい「Wordファイル等への修正・書き込み」

工夫した2つ目の点は、Geminiと、非AIコード(Python)の連携です。

Geminiは「読む」「考える」は得意でも、Wordファイルに修正履歴を入れたり、コメントを入れたりする「書く」作業はできません。一方、Geminiからいくら的確な修正案やコメントを貰ったとしても、それを人手でWordファイルに書き込むのでは、使い勝手は大幅に減少してしまいます。

そこで、Geminiに全て任せるのではなく、「書く」作業を別途Pythonに分業し、これらを接続しました。

Pythonには、python-docxやpywin32といったWordファイル操作に使えるライブラリが用意されていますから、修正に関する情報さえ事前に用意できればWordファイルの編集や操作はそこまでハードルが高くありません。一方、GeminiにとってもWordファイルを作ったり直したりは難しくても、どのように修正すればよいか指示する情報をjsonフォーマットで出力することは可能です。

このことを活かしてGeminiとPythonがお互いに取り扱えるjson形式の中間ファイルで両者を橋渡しすることで、Wordファイルに自然な修正履歴と綿密なコメントを入れられるワークフローを作ることができました。

今回は時間がなくて試しませんでしたが、この「中間ファイル→PythonでWord化」処理について、少し手を入れてパターンを作り分ければ、例えば、次のようなことなことも可能になるのではないかと思います。

修正可能な契約書 元ファイルに修正履歴&コメントを入れる。

修正不能な契約書
(PDFや利用規約等)

元ファイルとは別途、覚書を作成&コメントを入れる。

③ 最終的なワークフローの形

最終的にワークフローの全体像は以下のようになりました。

 

 

「読む」機能の実装

Geminiがインプットとして与えられた契約書ファイルを読み、プレイブック上の各ルールに沿ってリスクと対策案を網羅的に指摘した方針txtを出力するのが「読む」機能の中身です。

従って、「読む」機能の実装はGeminiに与えるプロンプトが中心になりますが、主に以下の点に留意してプロンプティングしました。

① 双方向走査

出力結果の網羅性(リスクの見落としがないこと)を担保するのが重要なため、契約書とプレイブックの双方向から抜け漏れがないか走査するようにしました。

  • 契約書 > プレイブック方向の走査(ポジティブ走査)
     契約書の各条文が、自社のプレイブック(判断基準)のどのルールに抵触するかを総当たりで確認。

  • プレイブック > 契約書方向の走査(ネガティブ走査)
    プレイブック側を起点とし、契約書の中に「本来あるべき条文(SLAや損害賠償制限など)」が欠落していないかを確認。

② 疑わしきは指摘させる

当初、Geminiがルールに適合するものの「大きな問題なし」と勝手に判断して指摘をスルーしたり、指摘事項を分かりやすく一定数内にまとめようと丸めてしまう動きがみられました。

ただ、今回の取組みでは、最適解を出すための匙加減は人間が行うため、Geminiには些末な事項であっても一旦リスクとして指摘する必要があります。

そのため、プロンプトに「些末な事項であっても、疑わしきはすべて指摘せよ」との指令を組み込んだり、「最低でも*個以上の指摘事項を挙げることを目標とすること」「指摘事項が少なすぎる場合は、指摘漏れを疑い見直すこと」等の自己監査を定義したりことで、回答粒度の安定性・網羅性を高める工夫をしました。

③ 出力フォーマットの強制

後続処理(人間による方針レビュー)を毎回安定的に行えるようには、常に決まった仕様に沿って方針txtを出力する必要があります。

そのため、プロンプトで出力フォーマットが強制されるよう工夫しました。

  • 厳格フォーマット定義
    回答に含まれがちな無用な挨拶・コメントやMarkdown装飾等があると、人間が方針調整を行う際、削除したりする手間が増えるため、これら装飾の使用を禁止し、プレーンテキストでの出力が徹底されるよう工夫をしました。

  • 指摘項目の分かりやすさ
    後段で人間が方針調整を行う際に読みやすく、かつ、読み疲れないようにするため、指摘項目を【リスク】【修正案】【代替案】の3点セットで各最大100文字程度の文字数までにまとめさせるよう工夫をしました。

 

 

「考える」機能の実装

人間が調整を行った方針txtを元に、Wordに付ける具体的な修正内容やコメント内容をプレイブックに沿って考え、後段の「書く」処理に渡す中間ファイル(json)として作成するのが「考える」機能の中身です。

従って、「考える」機能の実装もGeminiに与えるプロンプトが中心になりますが、主に以下の点に留意してプロンプティングしました。

① 修正要否の判定

指摘項目ごとに、方針txtに含まれる【修正】タグの有無等から、当該指摘項目が修正案の作成を必要とするものか、それともコメントの付記だけでよいかを的確に判断できるよう工夫しました。

② カウンターコメントの生成

修正案だけではなく、相手方に修正を依頼する際の「依頼文言(カウンター文言)」も、プレイブックに沿って、実際の文面で使われている文言等に違和感なく溶け込ませる形でGeminiに作って貰えるよう工夫しました。

なお、プレイブックには、交渉時のラリーを極力少なくできるよう過去経験から修正を希望する理由を相手方にご理解頂きやすい的確なフレーズを体系化しました。

③ 修正箇所の厳格な特定

Geminiが考えた修正案・コメントを、Wordファイル上の的確な位置にズレなく挿入するため、Word上の修正箇所が正確に特定できるよう工夫をしました。

  • ターゲットとなる文字列の一字一句違わぬ抽出

  • 出現インデックスのカウント
    同一の文字列が複数回出現する場合に備え、文脈から何番目の出現箇所かをカウントして、間違えた箇所の修正・コメントと後続処理に誤認されるリスクを排除しました。

④ 出力フォーマットの強制

後段でPythonによる「書く」処理を的確に行うためには、常に決まった仕様に沿ってjsonファイルを出力する必要があります。

そのため、プロンプトで以下の出力フォーマットが強制されるよう工夫しました。

[
    {
        "action": 修正が必要なら"r", コメント付記のみなら"c",
        "target": 修正前の文字列,
        "index": 文書全体でのtargetの出現順番,
        "replacement": 修正後の文字列(アクションが"r"なら空白),
        "comment": コメント案
    },
    ... 以後、対応項目毎に以下のレコードを追加した配列にする。
]

 

 

「書く」機能の実装

最後に「書く」機能の実装です。

「書く」機能は、Geminiでは実現できませんので、Pythonで実装しました。使った主なライブラリ等は以下です。

Python 3.12.9
python-docx 1.1.2
pywin32 308

① 文言の修正

契約書内には「本契約」「当社」といった言葉が何度も登場するため、単純な一括置換では誤った箇所を書き換えてしまうリスクがあります。そのため、修正対象となる文言の出現順番(index)も使って、正確に編集を行うロジックを実装しました。

from docx import Document

def replace_nth_occurrence(text, target, replacement, n):
    """
    文章の中に同じ言葉が複数ある場合、そのうちの「n番目」だけを入れ替えます。

    Args:
        text (str): 元の文章全体
        target (str): 置き換えたい対象の文字列
        replacement (str): 新しく挿入する文字列
        n (int): 何番目の出現箇所を置換するか(0から数え始めます)
    """
    parts = text.split(target)
    if len(parts) <= n + 1:
        return text
    
    # 指定箇所の前後を分割し、新しい言葉を挟んで再結合する
    left_part = target.join(parts[:n+1])
    right_part = target.join(parts[n+1:])
    return left_part + replacement + right_part

def create_revised_file_pydocx(docx_path, temp_path, patches):
    """
    Wordファイルを開き、指定された箇所の文字を書き換えて保存します。

    Args:
        docx_path (str): 元になるWordファイルのパス
        temp_path (str): 保存先(一時ファイル)のパス
        patches (list): 置換指示のリスト。actionが"replace"のものを処理。
    """   
    # python-docxのDocumentオブジェクトとしてファイルを読み込む
    doc = Document(docx_path)
    
    for patch in patches:
        if patch.get("action") != "r":
            continue

        target = patch["target"]
        replacement = patch["replacement"]
        target_idx = patch["index"] # 全体で何番目の出現箇所か
        current_match_count = 0 

        # 文書内の各段落(para)をループで走査
        for para in doc.paragraphs:
            count_in_para = para.text.count(target)
            if count_in_para > 0:
                # 探している「n番目」がこの段落内に含まれるか判定
                if current_match_count <= target_idx < (current_match_count + count_in_para):
                    local_index = target_idx - current_match_count
                    # 段落のテキストをスタイルを維持しつつ書き換える
                    para.text = replace_nth_occurrence(para.text, target, replacement, local_index)
                    break 
                current_match_count += count_in_para

    # 変更を一時ファイルに保存
    doc.save(temp_path)

(※)patchesは、「考える」AIから出力された中間ファイル(json)をパースしたlist型データです。

(※)修正箇所のフォントや書式の操作に関するコードは割愛する等、コードを実際のものより一部簡略にしています。

② コメントの付記

コメントを入れる作業は、Win32APIを使ってローカルPC上のWindowsを操作できるライブラリであるpywin32を利用して実装しました。なお、コメントを入れる段落かを判定する際の文字列検索において、改行等の文字列を削除しないと、なぜか上手くヒットしなかったため、文字列の正規化をしてから比較するヘルパー関数を挟んでいます(しっかり時間をかければ、もっと上手く書けるかもです)

本来は、python-docxだけで実装したかったのですが、思ったようにコメントを入れられず調べると.docxファイルの内部実体であるXML構造(Open Office XML)に直接タグを注入するコードを書く必要があるっぽかったので、今回はパスしました。

そのため、WordがインストールされているローカルのWindowsPCでコードを動かす場合のみに使えるコードになってしまいましたが、法務メンバーのPCは通常WordがインストールされているWindows PCのため、今回は実験でもあり、それでよしとしました。

(※)将来、APIを使ったサーバーサイドで動かすアプリケーションとして実装し直す場合は、このあたりをどうするかが課題になりそうです。

import win32com.client
import os
import re

def normalize_text(text):
    """
    照合用に文字列を正規化します。
    改行コードや空白の混在によるマッチング失敗を防ぎます。 
    """
    if not text: return ""
    return re.sub(r'[\r\n\t\u3000 ]+', '', str(text))

def apply_comments_win32(file_path, patches):
    """
    Wordの標準機能(COMオブジェクト)を呼び出し、指定された箇所にコメントを挿入します。

    Args:
        file_path (str): 操作対象となるWordファイルのパス
        patches (list): 実行する指示のリスト。
    """
    abs_path = os.path.abspath(file_path)
    word = win32com.client.Dispatch("Word.Application")
    word.Visible = False
    
    try:
        # win32comを介してWordのDocumentオブジェクトを取得
        doc = word.Documents.Open(abs_path)
        
        for patch in patches:
            normalized_target = normalize_text(patch["target"])
            # 文書を1段落(Paragraph)ずつ読み込み、正規化したテキストで照合を行う
            for para in doc.Paragraphs:
                if normalized_target in normalize_text(para.Range.Text):
                    # 一致した段落に対してコメントを挿入
                    doc.Comments.Add(Range=para.Range, Text=patch["comment"])
                    break
        doc.Save()
    finally:
        word.Quit()

(※)patchesは、「考える」AIから出力された中間ファイル(json)をパースしたlist型データです。

(※)修正箇所のフォントや書式の操作に関するコードは割愛する等、コードを実際のものより一部簡略にしています。

③ 修正履歴の作成

修正履歴の作成を細かく行うのもpython-docxでは難しかったため、pywin32からWordの文書比較機能を使って実装しました。文言修正&コメント付記後のファイルを、修正前のファイルと別に保存した上、修正前のファイルと修正後のファイルをWordの比較機能を使って突合し、文言修正(修正履歴付き)&コメントが含まれた最終アウトプットを作成しました。

import win32com.client
import os

def compare_and_save(original_file, revised_temp_file, final_output_file):
    """
    元のファイルと書き換え後のファイルを比較し、修正履歴(赤字)が付いた状態のファイルを保存します。

    Args:
        original_file (str): 修正前のオリジナルファイルのパス
        revised_temp_file (str): 文字列を書き換えた後の一時ファイルのパス
        final_output_file (str): 修正履歴を反映して保存する最終的なファイル名
    """
    abs_orig = os.path.abspath(original_file)
    abs_rev = os.path.abspath(revised_temp_file)
    abs_out = os.path.abspath(final_output_file)

    word = win32com.client.Dispatch("Word.Application")
    try:
        # 修正前と修正後の2つの文書を開く
        doc_orig = word.Documents.Open(abs_orig)
        doc_rev = word.Documents.Open(abs_rev)

        # 2つの文書を比較し、元の文書(doc_orig)に修正履歴として統合
        word.CompareDocuments(
            OriginalDocument=doc_orig,
            RevisedDocument=doc_rev,
            Destination=0, # wdCompareDestinationOriginal
            Granularity=1, # wdGranularityWordLevel(単語単位で精緻に比較)
            CompareFormatting=False, # 書式の違いは無視
            CompareCaseChanges=True,
            CompareWhitespace=True
        )
        
        # 修正履歴が付与されたファイルを最終出力として保存
        doc_orig.SaveAs2(abs_out)
    finally:
        word.Quit()

(※)コードを実際のものより一部簡略にしています。

 

 

終わりに

結論、実験としては思いのほか、しっかりした契約レビュー済みのWordファイルを作ることができました!

以下のスクショがGemini主体の処理で修正させた契約書条文と社内依頼者/相手方向けのコメント(Wordファイル)です。

 

(※)コメントへの色入れ等はコード省略箇所にて行っています

 

もちろん、テスト用のダミー契約書を使った実験なので、多種多様な契約実務でどこまで使えるかは全く未知数です。

ただ、詳細省略しましたがリブセンス法務で日々重視しているポイントを中心に40ルールほど設定した本格的なプレイブックを使って、多くのルールの中から実際に的確な対応をこなせることは確認できましたし、自力でも生成AIを使って契約書の修正案(アウトプット)を作れるかもと分かったことは、十分な収穫と言えそうです。

また、APIを使ってUI/UXを工夫すれば、よりスムーズな動線・体験を実現する実践的なアプリケーションに出来そうなので、今後の法務DXの発想が果てしなく膨らみ、楽しみになりました。