生成AIでブログをレビューする

この記事は技術広報 Advent Calendar 2024の1日目の記事です。 estieのブログレビューに生成AIを活用する可能性を探り、その試みの一部を共有します。

estieでソフトウェアエンジニアをしている松本(@matsu7874)です。estieでは、社外の皆様に様々な情報をお届けするために、estie inside blog というブログを運営しています。estieのカルチャー入社エントリーの記事が多く、技術知見資金調達の記事なども公開しています。 平均すると週2本 のペースで公開できています。

estie のブログのレビューフロー

こちらの記事で紹介しているように、estieでは記事の公開前にレビュープロセスを挟んでいます。せっかく書いてもらったブログを読みやすい形で発信する目的と、会社として発信の品質を担保する目的があります。 レビューの観点のうち、表記の誤り(プロダクト名やメンバーの名前、カッコが閉じているかなど)は下記のツールを使って機械的に検出しています。

gecko655/proofreading-tool proofreading-toolは文書校正ツールで、ルールファイルに従って校正した結果をGUIで表示してくれるツールです。ソフトウェアエンジニアではないインターンメンバーにも作業をお願いしているため、GUIで操作できることが大きなメリットです。

生成AIを活用できそうな部分

一方で、その単語で伝わる?ちょっとミスリーディングな表現かもというタイプの指摘は熟練のメンバーが対応せざるを得ず、ボトルネックになりやすい部分でした。そこで、生成AIを活用して、この部分を効率化できないかと考えました。

これまでレビュープロセスの一部を生成AIにやってもらえないかとプロンプトを試行錯誤していたのですが、複数回にリクエストを分けてやるとかなり汎用的で可能性を感じる(話が通じる)レビュープログラムが作れたので、シェアします。※実際に本番運用できているものではありませんが、PoCの成果物としてお楽しみください。

実装

プログラムの構造としては、複数のレビュアーにそれぞれ得意な領域の意見をもらいながら、1人の編集者が修正の有り無しを判断して修正していく形です。 人間がレビューのプロセスで行っていること(指摘とリライト)を一部自動化しようというものです。

from typing import List, Dict, Tuple
from openai import OpenAI
import os
import json

# OpenAI API クライアントの初期化
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def main() -> None:
    """
    ブログを読み込み、各観点のレビューと修正提案を適用し、最終結果を保存する。
    """
    input_file: str = "blog.md"

    # 1. ブログの読み込み
    original_content: str = read_file(input_file)

    # 2. 会話履歴の初期化
    messages: List[Dict[str, str]] = [
        {
            "role": "system",
            "content": "あなたはIT企業のブログを担当する非常に優れた編集者です。レビュアーからの修正提案を受け取り、適切に活用してください。",
        },
    ]

    # 3. 各観点のレビュー取得・修正判断・修正適用
    review_points: List[Tuple[str, str]] = [
        ("ベテランのソフトウェアエンジニア", "技術用語の使い方に違和感がないか"),
        (
            "日本語の専門家",
            "文法・敬語の使い方に誤りがないか。特に余計な謙譲語がないか",
        ),
        ("ブログ編集の専門家", "箇条書きや段落の構成が適切か"),
    ]
    updated_content: str = original_content
    for i in range(3):
        is_any_review_applied: bool = False

        for reviewer_role, review_point in review_points:
            is_updated, updated_content = review_and_apply_suggestions_with_context(
                updated_content, reviewer_role, review_point, messages
            )
            is_any_review_applied = is_any_review_applied or is_updated

        # 4. 更新後のブログを保存
        output_file: str = f"revise_{i}.md"
        write_file(output_file, updated_content)
        print(f"更新後のブログを {output_file} に保存しました")

        if not is_any_review_applied:
            print("修正の必要がないため、終了します")
            break
def review_and_apply_suggestions_with_context(
    content: str, reviewer_role: str, review_point: str, messages: List[Dict[str, str]]
) -> Tuple[bool, str]:
    """
    会話履歴を利用して特定の観点について修正提案を取得し、適用する。

    Args:
        content (str): 修正対象のブログ内容。
        reviewer_role (str): レビューを行う役割(例: "編集者", "ソフトウェアエンジニア")。
        review_point (str): 修正提案を依頼する観点。
        messages (List[Dict[str, str]]): 会話履歴を保持するリスト。

    Returns:
        str: 修正が適用された後のブログ内容。
    """
    is_updated: bool = False
    print(f"レビュー観点: {review_point}")

    # 修正提案を取得
    suggestions = generate_revision_suggestions(content, reviewer_role, review_point)
    messages.append(
        {
            "role": "assistant",
            "content": json.dumps({"suggestions": suggestions}, ensure_ascii=False),
        }
    )

    print(f"{review_point}に関する修正提案:")

    # 提案を適用して差分を表示
    updated_content: str = content
    for suggestion in suggestions:
        print(f"- 修正の観点: {suggestion.get("修正の観点")}")
        print(f"  修正箇所: {suggestion.get("修正箇所")}")
        print(f"  修正案: {suggestion.get("修正案")}")
        print(f"  修正理由: {suggestion.get("修正理由")}")

        # 修正を適用するか判断
        messages.append(
            {
                "role": "user",
                "content": (
                    f"以下の修正提案を反映してUPDATEすべきかIGNOREすべきか判断してください。\n\n"
                    f"修正箇所: {suggestion.get("修正箇所")}\n"
                    f"修正案: {suggestion.get("修正案")}\n"
                    f"修正理由: {suggestion.get("修正理由")}\n"
                    "指摘内容があまりに細かすぎる、反映しても改善が見込めない場合は、IGNOREしても構いません。"
                ),
            }
        )
        response = client.chat.completions.create(
            model="gpt-4o-mini", messages=messages
        )
        assistant_message = response.choices[0].message
        messages.append(assistant_message)

        decision = assistant_message.content.strip().upper()
        apply = "UPDATE" in decision
        print(f"LLMの判断: {'UPDATE' if apply else 'ignore'}\n")

        if apply:
            is_updated = True
            # LLMに修正させる
            messages.append(
                {
                    "role": "user",
                    "content": (
                        f"以下のブログ内容に修正を適用してください。\n\n"
                        f"元のブログ内容:\n{updated_content}\n\n"
                        f"修正箇所: {suggestion.get("修正箇所")}\n"
                        f"修正案: {suggestion.get("修正案")}\n"
                        f"修正理由: {suggestion.get("修正理由")}\n\n"
                        "修正後のブログ内容のみを、そのまま返してください。他のコメントや補足説明は不要です。"
                    ),
                },
            )
            messages.append(
                {
                    "role": "user",
                    "content": (f"ブログの全文を出力してください"),
                }
            )
            response = client.chat.completions.create(
                model="gpt-4o-mini", messages=messages
            )
            assistant_message = response.choices[0].message
            messages.append(assistant_message)
            updated_content = assistant_message.content

    return is_updated, updated_content
def generate_revision_suggestions(
    content: str,
    reviewer_role: str = "ブログ編集の専門家",
    review_point: str = "前後の文脈がおかしい点がないか",
) -> List[Dict[str, str]]:
    """
    LLMに修正提案を依頼し、[{修正の観点, 修正箇所, 修正案, 修正理由}]形式で結果を取得する。

    Args:
        content (str): 修正対象のブログ内容。
        reviewer_role (str): レビューを行う役割(例: "編集者", "ソフトウェアエンジニア")。
        review_point (str): 修正提案を依頼する観点。

    Returns:
        List[Dict[str, str]]: 修正提案のリスト。
    """
    function_definitions: List[Dict] = [
        {
            "name": "generate_suggestions",
            "description": "ブログの修正提案を生成します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "suggestions": {
                        "type": "array",
                        "description": "修正提案のリスト",
                        "items": {
                            "type": "object",
                            "properties": {
                                "修正の観点": {
                                    "type": "string",
                                    "description": "修正の観点",
                                },
                                "修正箇所": {
                                    "type": "string",
                                    "description": "修正が必要な具体敵な箇所",
                                },
                                "修正案": {
                                    "type": "string",
                                    "description": "修正後の文案",
                                },
                                "修正理由": {
                                    "type": "string",
                                    "description": "なぜ修正が必要かの理由",
                                },
                            },
                            "required": [
                                "修正の観点",
                                "修正箇所",
                                "修正案",
                                "修正理由",
                            ],
                        },
                    }
                },
                "required": ["suggestions"],
            },
        }
    ]

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": f"あなたは{reviewer_role}です。"},
            {
                "role": "user",
                "content": (
                    f"以下の観点でブログ内容をレビューし、修正の観点・修正内容・修正理由をリスト形式で提案してください。\n\n"
                    f"レビューの観点: {review_point}\n"
                    f"ブログ内容:\n{content}"
                ),
            },
        ],
        functions=function_definitions,
        function_call={"name": "generate_suggestions"},
    )

    return json.loads(response.choices[0].message.function_call.arguments)[
        "suggestions"
    ]

動作例

プログラムを実行すると下記のような表示が行われ、イテレーションが進むたびにrevise_X.mdとして保存されます。

- 修正の観点: 技術用語の理解度
  修正箇所: 「PoC」
  修正案: 「Proof of Concept(PoC)」
  修正理由: 「PoC」の略語を説明することで、業界外の読者にも理解できるようにするため。
LLMの判断: UPDATE

- 修正の観点: 用語の正確性
  修正箇所: 「バリバリ本番運用できている」
  修正案: 「実際に本番運用している」
  修正理由: 「バリバリ」という言葉はカジュアルすぎるため、より正式な表現にすることで信頼感を高めるため。
LLMの判断: UPDATE

まとめ

生成AIを活用することで、ブログのレビュープロセスの一部を自動化することができました。レビューの観点が違う各会話が同じフォーマットで理由を伝えてくれる部分に使いやすさを感じました。今後は、各観点のプロンプトのブラッシュアップを中心に、生成AIを活用したブログのレビュープロセスの改善を進めていきたいと考えています。

それ、助けてやるぜ!という方がいらっしゃいましたら下記カジュアル面談のフォームからご連絡ください。

hrmos.co

© 2019- estie, inc.