日曜日, 7月 13, 2025
日曜日, 7月 13, 2025
- Advertisment -
ホームニューステックニュース生成 AI で国会議事録を要約して提供する Web サイトをリリースした

生成 AI で国会議事録を要約して提供する Web サイトをリリースした


国会議事録を生成 AI で要約したものを提供する Web サイト 『ポリ徹 (Politetsu)』 を公開しました。

ポリ徹 -政治家徹底解剖

せんきょけん

本サイトは主に 2 人の開発者で開発したもので、私はその一人です。
友人から「AI で国会議事録を要約して掲載する」というアイデアでアプリケーション開発に誘われたので、それに乗っかる形で開発に携わりました。
その友人が以下の記事でこの Politetsu についての機能紹介や開発の経緯などを詳しく解説してくれています。

Go×Next.jsで半年かけて『ポリ徹』を個人開発してみた – 生成AI×国会議事録要約アプリ #Docker – Qiita

こちらの記事では、主に私が担当したところで、生成 AI サービスの API を使ったデータ生成について解説します。

Politetsu では主に以下のコンテンツのデータを生成 AI で生成しています。

  • 国会の議事録をもとに生成した議事録要約
  • Web 情報(Web グラウンディング)をもとにした政治家個人の経歴情報
  • GPT-4o の画像生成で作成した政治家のイラスト

このようなデータを生成 AI で作成する場合は、チャットボットの利用などとは異なり、複数のソースデータに対して同じプロンプトで処理してそれぞれのデータを作成することになります。
生成 AI のランダム性に左右されない一貫した出力を得るためには、このプロンプトの最適化(いわゆるプロンプトエンジニアリング)やデータ生成のパイプラインの構築など様々な工夫が必要になります。
以下では国会議事録要約に用いた手法について紹介します。

国会議事録 API から取得した議事録データを LLM で要約するパイプラインを構築するには、生成 AI サービスが提供する API を利用する必要があります。
この API 利用の場合は基本的に利用した分だけ課金される従量課金制のサービスが一般的です。

当初は以前利用した経験のあった OpenAI の API を利用し、料金も考慮して GPT-4o-mini 等のモデルでの議事録要約を検討していました。
しかし、最初のうちはプロンプトも未熟だったこともあり、出力するたびに要約の仕方が大きく異なったり、指示に従わないケースが多かったりと、思ったような精度が出せず不安定でした。

そんな折に Google による gemini-2.0-flash モデルが発表され、以下の記事を見つけました。

gemini-2.0-flashが賢くてコスパがよすぎる件

簡単に説明すると、当時最新でコスパの良いモデルであった gemini-2.0-flash を使い、構造化出力を駆使して Chain of Thought (CoT) を誘導することで、文章の要約などのあらかじめ決められたタスクを精度良く行う手法について解説されています。
こちらの記事に倣って出力スキーマを定義して議事録要約の成果物を出力させると、かなり精度が良くなることを実感しました。
また、Gemini のモデルには無料枠が設定されており、個人開発での試行錯誤を気兼ねなくできる上に、運用も当面は無料枠内で可能であると見積もれたので、早急に LLM モデルを Gemini に乗り換えました。

今回の議事録要約のような、ある程度再現性のある安定した出力が欲しい繰り返しタスクの場合は、この Gemini の安価モデル + 構造化出力の構成が適しているケースが多いかと思います。
一方でチャットやエージェントなどのあらかじめすべきことが決まっていないタスクには向かないので、用途によってモデルや手法を使い分ける必要があります。
(個人的に、これは今後モデルが進化しても変わらないと予想しています。いわゆるノーフリーランチ定理のようなイメージです。)

Gemini の API 利用にはモデルごとにレート制限があり、以下のいずれかの制限を超えると 429 エラーが返ります。

  • 1 分あたりのリクエスト数(RPM
  • 1 日あたりのリクエスト数(RPD
  • 1 分あたりのトークン数(入力)(TPM

レート制限  |  Gemini API  |  Google AI for Developers
無料枠での実行はリクエスト数(RPM, RPD)がボトルネックになるケースが多く、トークンは割と気にせずにたくさん使うことができます。
そのため、トークンをたくさん消費してでも 1 回のリクエストにタスクを詰め込む方法が有効です。
そういった意味でも前述の構造化出力によってステップバイステップの処理を一つのリクエストに詰め込む手法が適しているといえます。
(レート制限以外にも一回のリクエストでのモデルの入出力のトークン制限があるので、これを超えないように注意しましょう。)
また、レート制限はモデルごとに別々に設定されるので、パイプライン内に複数の LLM タスクがある場合は、簡単なタスクに軽量なモデルを使うなどしてうまく分散させることで、レート制限をうまく回避できるかもしれません。

Politetsu の議事録要約データは以下のような手順で生成されています。
データの生成のために以下の手順をパイプライン化し、新しい国会議事録に対して自動的に要約データが生成されるようになっています。

  • 国会議事録 API から議事録データを取得する
  • 議事録内の各発言を質問・回答・その他に分類する
  • 分類を元に一つの会議の議事録を一連の質問ごとに分割する
  • 一連の質問に対して質問と回答の要約を行う
  • 要約した議事録からキーワードを抽出する

以下ではこれらのうち、生成 AI を用いたタスクについて解説します。いくつか実装例のコードを載せていますが、これらのコードには Google Gen AI Python SDK (google-genai) を用いています。
実装の一部のみを記載しているので、詳しい使い方はドキュメント Google Gen AI SDK documentation を参照してください。

議事録内の各発言を分類する

Politetsu の議事録要約は質疑応答に着目したものであり、質問と回答の形式で要約を提供しています。
国会の議事録には質疑応答以外にも議長による進行の発言などの質疑に関係ない発言が含まれていますが、
元の議事録データには質疑応答がどの発言から始まってどこで終わったかなどの情報はありません。
そのため、質疑応答部分をうまく要約するには、その前処理として各発言を質問・回答・その他に分類し、
関係のない発言を取り除き、一人の質問者による一連の質問に分割するのが良いと考えました。

当初は発言者を元にした単純なアルゴリズムによってこの分類を行うことを考えましたが、
質疑応答以外の発言をうまく取り除くことができなかったり、アルゴリズムが複雑になってしまったりといった問題があり、
結局 LLM (gemini-2.0-flash) に発言内容を読んで分類してもらうことにしました。

この分類タスクは最近の LLM にとってはそれほど難しくなく、適切に指示すれば軽量モデルでも人間と同程度の精度が出ると思います。
分類タスクを API で投げる場合は Enum 型で出力形式を指定しましょう。
また、複数の発言をまとめて投げて書く発言に対して分類させることで、リクエスト数を削減できます。

以下のようなコードで発言の分類を行うことができます。

from enum import Enum
from textwrap import dedent

from google.genai.client import Client
from google.genai.types import GenerateContentConfig, Schema, Type


class SpeechCategories(Enum):
    QUESTION = "question"
    ANSWER = "answer"
    OTHER = "other"




schema = Schema(
    type=Type.ARRAY,
    items=Schema(
        type=Type.OBJECT,
        properties={
            "speech_id": Schema(
                description="対象 SpeechRecord の SpeechID",
                type=Type.INTEGER,
            ),
            "category": Schema(
                description="対象 SpeechRecord の カテゴリー",
                type=Type.STRING,
                enum=list(SpeechCategories.__members__),
            ),
        },
    ),
)


prompt = dedent(
    """\
    Content はとある国会の議事録の一部です。
    Content は発言ごとに複数の SpeechRecord に分けられています。
    SpeechRecord には発言を識別する識別子である SpeechID と発言内容である Speech を含んでいます。
    Speech には議長による会の進行や質疑応答が含まれます。
    あなたの仕事はこの議事録中の各発言を 3 つのカテゴリーのいずれかに分類することです。
    各 SpeechRecord の Speech の全文を確認し,その中に相手への質問や何かしらの質問を受けて意見を述べる回答が含まれているかを判定して
    以下のように分類してください。
    全ての SpeechRecord に対して、その SpeechID と分類した Speech のカテゴリーを回答してください

    - QUESTION: 質疑応答に含まれる発言のうち,質疑を行っている発言
    - ANSWER: 質疑応答に含まれる発言のうち,質問に対して何らかの主張や根拠を含む回答を行っているもの
    - OTHER: 主張を含まないやり取りや議長による会の進行など,質疑応答以外の発言。質疑や異議の有無の確認などもこれに含まれる

    
      {}
    
    """
).format(speech_contents)  



client = Client(api_key="")
response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents=prompt,
    config=GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=schema,
    ),
)

result: list[dict] = response.parsed

議事録を要約する

一連のパイプラインの中で最も重要かつ難易度の高い処理になります。
そのため、先述の記事の内容に従い、構造化出力のスキーマをしっかり組むことで、
LLM の思考にレールを敷いてコントロールし、成果物である要約の内容や文体が大きくブレないようにしています。
例えば、ハルシネーション(幻覚)や国会の議事録の要約として問題のある出力を防ぐために、構造化出力の中に
複数の視点で自身の出力の内容を自己評価させる、いわゆる LLM-as-a-Judge のようなものを含めています。


claim_validations_schema = Schema(
    description="speech_summaries を踏まえて question と answer を検証し直す",
    type=Type.ARRAY,
    items=Schema(
        type=Type.OBJECT,
        properties={
            "question_claim": Schema(
                description="検証する対象の質問事項を短くまとめる",
                type=Type.STRING,
            ),
            "answer_claim": Schema(
                description="質問事項に対する回答内容を短くまとめる",
                type=Type.STRING,
            ),
            "summary_relevance": Schema(
                description="speech_summaries との関連性を述べる",
                type=Type.STRING,
            ),
            "is_question_relevant": Schema(
                description="この質疑の主題に沿った質問か",
                type=Type.BOOLEAN,
            ),
            "is_answer_exists": Schema(
                description="この質問に対する回答が存在するか",
                type=Type.BOOLEAN,
            ),
            "is_answer_relevant": Schema(
                description="この質疑の質問に沿った回答か",
                type=Type.BOOLEAN,
            ),
            ...
        },
    ),
)

こんな感じで成果物を作成する上で LLM に確認して欲しいことを羅列します。
これは、人間が作業をするときにあらかじめチェックシートを用意しておくようなもので、
ただプロンプトに「この点に注意せよ」と指示を書くよりも忘れにくく、指示を無視しづらいといったことが期待できます。

また、プロンプトの指示にどうしても従わないような出力に対する微調整を行うこともできます。
例えば、今回の要約では「〇〇氏が〜〜と述べた。」といった文体ではなく、自然な会話の形式になるようにしています。
しかし、国会の質疑応答の質問者の発言が「〇〇大臣に伺います。」といった呼びかけから始まることがよくありますが、
そのような発言を要約した際に「〇〇大臣、〜〜」といったように呼びかけを含んだ要約文になることが多く、
プロンプトに「呼びかけは省略せよ」といった指示を書いても従わないことがありました。
これに対し、以下のようにスキーマで段階的に調整することで、力技で指示に従わせることができました。


question_summaries_schema = Schema(
    type=Type.OBJECT,
    properties={
        "untrimmed_question": Schema(
            description="質問を 150 文字程度の日本語で要約し,回答者との対話の形式で記載する",
            type=Type.STRING,
        ),
        "question": Schema(
            description="untrimmed_question に相手の名前や役職の呼びかけを含んでいる場合は,その部分を取り除く",
            type=Type.STRING,
        ),    
    },
)

このように、構造化出力のスキーマをうまく使って、LLM の思考をコントロールしたり、成果物を調整したりすることができます。

要約からキーワードを抽出する

最後に要約からキーワードを抽出します。単純そうに見えますが、議事録の中で重要なキーワードがどれかを考えるのは人間にとってもそう容易ではなく、どうしてその単語をキーワードとして選んだのかといった基準を言語化するのは難しいです。
今回は基準を作成し、まずキーワードになりうる単語を可能な限り抽出させた上で、各基準について 5 段階で評価させ、その合計が大きいものから順に幾つかの単語をキーワードとして設定するという手法で抽出しました。


schema = Schema(
    type=Type.OBJECT,
    properties={
        "keyword": Schema(
            description="検証する対象のキーワード",
            type=Type.STRING,
        ),
        "relevance": Schema(
            description="議事録との関連性の高さを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "specificity": Schema(
            description="キーワードの表現が曖昧でなく具体的であるかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "representativeness": Schema(
            description="キーワードが議論を象徴する代表的なものかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "uniqueness": Schema(
            description="キーワードがその議事録に特有で,他の国会の議事録から特定の議事録を特徴づけるものかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "discriminative": Schema(
            description="キーワードが他の国会の議事録に対して差別化できるものであるかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "objectivity": Schema(
            description="キーワードが中立的かつ客観的であるかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
        "searchability": Schema(
            description="キーワードが一般ユーザにとって直感的で検索しやすいかどうかを1~5の5段階で評価する",
            type=Type.NUMBER,
        ),
    },
)

このような評価基準を言語化するのはなかなか難しいですが、この評価基準は ChatGPT に協力してもらって作成しました。
LLM には自然言語で指示を出す必要があるため、人間には自分の要求をうまく言語化することが要求されますが、その指示の作成を LLM に協力してもらうというメタ的な使い方をしています。
繰り返しタスクのためのプロンプトは使い捨てのプロンプトよりも正確な指示を言語化する必要があるため,そのようなプロンプトの作成のために別の LLM を使って推敲するのが有効だと思います。

生成 AI を使ったアプリケーションのアイデアを個人開発で実現するには、いくつか障壁があります。
一つはコストの問題で、生成 AI モデルの利用には結構なコストが必要になります。
個人開発の場合は OpenAI や Gemini などの API サービスの利用がほとんど前提になりますが、大抵は従量課金制で、
使えば使うほどコストがかかります。
例えばユーザの入力に対してモデルを動かして出力を行うようなアプリケーションの場合、そのモデルのコストを誰が負担するかが問題になり、
ユーザが際限なくモデルを動かして運用者が破産するといったことを避ける必要があります。
そのために、ユーザが AI サービスの API キーを発行して自ら負担したり、利用制限を課した上でサブスクリプションで提供したりといった方法が考えられます(後者を個人開発で行うのは難しいですが)。
一方で、この Politetsu の場合は生成 AI を利用するのはデータ生成の部分であり、ユーザが直接モデルに入力を行うことはありません。
そのため、利用者がどれだけ増えようが生成 AI 利用のコストが増えることはありません。
さらに、一定の期間で生じる国会の議事録は限られており、(プロンプトの工夫によって)Gemini の無料枠でまかなえる範囲で要約可能なため、
運用コストをサーバの利用料などを除いてほぼゼロに抑えることができ、
ユーザの数に関わらず持続可能なアプリケーションにすることができました
(とはいえ、API 利用料以外のコストはかかっているので、ユーザは増えて欲しいですが)。

この先収益が出れば嬉しいですが、趣味の範囲で生成 AI を使った開発を経験できたので、個人的には満足しています。

この記事の投稿は 2025 年の参院選の真っ只中に行いました。
ほんとは公示日前後に投稿する予定でしたが、ダラダラ書いているうちに遅れてしまいました。
期日前投票はすでに始まっていますが、これから投票する人はぜひ Politetsu を覗いてみてください。

ポリ徹 -政治家徹底解剖

Politetsu を読んで選挙に行こう!🗳️



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -