LangGraphを使ってテックブログレビューエージェントを作ってみた

こんにちは、Insight EdgeでDeveloper兼テックブログ運営担当をしているMatsuzakiです。 今回は、私が担当している本テックブログ「Insight Edge Tech Blog」運営担当業務における業務効率化・高度化兼自己研鑽の一貫として現在テックブログレビューエージェントを試作中ですので、そちらの開発経緯や内容をお話ししていきたいと思います。

目次

開発背景

本テックブログ「Insight Edge Tech Blog」は、2022年10月に開設し、2025年2月現在で2年以上継続しています。(先日記事も100本を超えました!🎉) しかし、テックブログを安定して運営し続けるのは難しいですよね。そこで弊社では、「テックブログ運営チーム」(以下運営チーム)としてエンジニア2名、データサイエンティスト2名の計4名でチームを組み、スケジュール管理、進捗管理、記事レビュー、投稿などの業務を担当しています。

その中でも、特にコストがかかり、担当者によってばらつきが出やすいのが記事のレビュー作業です。 さらに、近年AIエージェントが盛り上がっている状況を踏まえ、技術のキャッチアップも兼ねて、「レビュープロセスをAIエージェントで自動化できないだろうか?」と考え、開発に至りました。

※現在は試作段階です。

システム構成

システムの構成は以下の通りです。

弊社のテックブログは記事をMarkdownファイルで執筆し、各々ブランチを作成してGitHub上で管理しています。 現在の運用では、作成されたプルリクエストに対してレビューを行っており、システム導入後も同様に、プルリクエスト単位でレビューを実施します。

レビューの流れ

以下に本システムのレビューの流れを示します。

  1. 執筆者がGitHubにプルリクエストを作成
  2. GitHub Actionsがプルリクエスト作成・更新をトリガーとして起動
  3. GitHub ActionsがLambdaを起動
  4. GitHubのAPIを使って記事内容を取得
  5. 記事内容とプロンプトを元にBedrockを通じてモデル(Claude 3.5 Sonnet)を呼び出す
  6. モデルのレスポンスを元にレビュー内容を作成
  7. GitHubのAPIを使ってレビュー内容をプルリクエストに投稿

後述しますが、5についてはモデルとの1回のやり取りでレビュー内容を作成するのではなく、レビュー観点ごとに段階的に呼び出しを行い最終的な結果を出力させます。 以上が一連の流れとなります。

開発内容

今回の開発の軸となるところは、上記の「レビューの流れ」のうち、5-6に値する生成AIを使ったレビュー処理フローの部分となります。 そのために、まずはレビュー観点を整理して洗い出し、その後処理フローへの落とし込み、最終的に実装へ繋げています。

レビュー観点の洗い出し

まず、テックブログレビューにおける重要な観点を整理しました。(現状の運用ではレビュー観点として整理されているものはなく、個々人に委ねられている部分があるため、これを機に体系的な整理を進めたいところです!)

一つ一つ見ると細かいですが、大きな観点としては3つで、「1.構造の正確さ、2.内容の適切さ、3.文章の品質」です。

1. 構造の正確さ
・見出しの整理
 ・見出し(h1, h2, h3 など)が階層的に整理されているか。
 ・見出しと内容が対応しているか。
・論理構成
 ・文章や論理構成に問題がないか。
2. 内容の適切さ
・技術的正確性
 ・専門用語の誤用がないか。
・独創性・独自性
 ・具体的な事例が含まれているか。
 ・他の記事と差別化できているか。
 ・本記事ならではの工夫ポイントがあるか。
・網羅性
 ・他に記述すべき観点がないか。
・内容の質
 ・簡単すぎる内容になっていないか。
 ・単なるマニュアルの模倣になっていないか。
 ・読者がすぐに見つけられるような内容だけではないか。
・倫理性・適切性
 ・他者に誤解や不快感を与える表現がないか。
 ・会社に不利益をもたらすような内部情報や公開すべきでない情報が含まれていないか。
3. 文章の品質
・誤字脱字
 ・誤字や脱字がないか。
・読みやすさ
 ・複雑な文構造を避け、平易な言葉になっているか。
・冗長表現の整理
 ・繰り返し表現や冗長な記述がないか。
・表記揺れ
 ・言葉の表記が統一されているか。
・スタイルの一貫性
 ・一人称と三人称が混在していないか。

洗い出してみると、結構多くの観点がありました。

処理フロー

上記で洗い出した観点、中でも大項目レベルの「1.構造の正確さ、2.内容の適切さ、3.文章の適切さ」を主軸とし、Agentic Workflowの手法を用いて処理フローを作成しました。
※ Agentic Workflowとは、「LLMの出力結果に基づいて、次の処理を分岐したり、分岐によってLLMを呼び分けたりする」手法です(LLMマルチエージェントのフローエンジニアリング(Agentic Workflow)実践ガイドより)。

一部の項目については、Web検索APIを組み合わせるなど、LLM単体ではなく他のアプローチを併用するのが適切な場合もあります。ただし、試作段階としてまずはシンプルに、プロンプト内で対応可能な範囲に絞った処理フローを作成しました。

実際の処理フロー(※現時点)は以下です。

※一つ一つの水色の四角はLangGraphのノードと対応しています。(後述)

処理の流れは以下の通りです。

  1. 「構造の正確さ」観点でのレビュー結果を生成(structure_check)
  2. 「内容の適切さ」観点でのレビュー結果を生成(content_check)
  3. 「文章の品質」観点でのレビュー結果を生成(quality_check)
  4. 1~3で生成されたレビュー結果を統合(review_integration)
  5. 統合後レビュー結果に対して、意図した結果となっているか評価(self_reflcection)
  6. 5の結果が'NG'であればリトライ数4回未満の範囲で'OK'になるまで4を繰り返す

観点ごとにプロンプトを分けて結果を生成し、更に統合結果に対してセルフリフレクションで評価・再生成ループを繰り返すことで一定の品質を維持できるようにしています。

実装

今回実装にあたっては、フローの可読性が高く、条件分岐・ループが実装しやすいLangGraphを使用しています。 上記処理フロー図内の水色の四角で表されている部分がそれぞれノードとして独立しています。

ここでは、LangGraphで実装したグラフの構造を中心に実装の一部を紹介します。

LangGraphでは、共通のステートを定義した上で、ノード(処理)とエッジ(処理の流れ)を組み合わせ、グラフを構築・実行します。

そのため、まずはワークフロー全体で利用するステートを定義します。 データ構造はPydanticのBaseModelクラスを用いて定義します。

ステートの定義

from pydantic import BaseModel, Field

class ReviewResults(BaseModel):
    structure_check: str = Field(default="", description="構造")
    content_check: str = Field(default="", description="記事内容")
    sentence_quality_check: str = Field(default="", description="文章の質")


class ReflectionResults(BaseModel):
    judgement: str = Field(default="", description="OK/NG判断")
    feedback: str = Field(default="", description="改善提案")
    retry_count: int = Field(default=0, description="リトライ数")


# State
class State(BaseModel):
    article: str = Field(default="", description="記事内容")
    review_results: ReviewResults = Field(
        default_factory=ReviewResults,
        description="レビュー結果"
    )
    review_summary: str = Field(default="", description="レビュー統合結果")
    reflection_result: ReflectionResults = Field(
        default_factory=ReflectionResults,
        description="内省結果"
    )

LangGraphにおけるステートとは、ワークフローで実行される各ノードによって更新された値を保存する仕組みです。ワークフロー内で読み書きするデータはステートとして定義しておきます。

グラフの定義

次に、グラフのインスタンスを生成し、グラフを定義します。 StateGraphはグラフ構造の定義のために使われるクラスであり、これに対してノードやエッジを追加することで処理を構成していきます。

workflow = StateGraph(State)

ノードの追加

次に、ノードを追加していきます。 それぞれのノードの中でLLM呼び出しを行い結果を生成します。

# ノードの追加
workflow.add_node(structure_check) # 構造観点のレビュー
workflow.add_node(content_check) # 内容観点のレビュー
workflow.add_node(quality_check) # 文章の質観点のレビュー
workflow.add_node(review_integration) # レビュー結果統合
workflow.add_node(self_reflection) # 統合結果の評価

ノードは以下のように定義します。
ここでは、content_checkノードとself_reflectionノードの実装例を記載しています。
※プロンプトは今後改善予定です

from typing import Any
from langchain_aws import ChatBedrock
from state.state import State
from langchain_core.output_parsers import StrOutputParser
from langchain.prompts import PromptTemplate
from settings import config

def content_check(state: State) -> dict[str, Any]:
    article = state.article
    llm = ChatBedrock(
        model_id={モデルIDを入力}, 
        region_name={リージョン名を入力},
    )

    prompt = PromptTemplate(
        input_variables=["article"],

        template="""
        あなたはプロフェッショナルな技術ブログの校正・校閲者です。
        あなたのタスクは記事内容について、以下の5つの観点に基づき、「評価ポイント」と「改善点」を出力する ことです。

        # レビュー観点
        1. **技術的正確性**
        - 専門用語の誤用がないか。

        2. **独創性・独自性**
        - 具体的な事例が含まれているか。
        - 他の記事との差別化ができているか。

        3. **網羅性**
        - 他に加えるべき観点がないか。

        4. **内容の質**
        - 一般的な情報のなぞりになっていないか。
        - 内容が簡単すぎないか。

        5. **倫理性・適切性**
        - 誤解や不快感を与える表現がないか。
        - 企業の機密情報やブランドを損なう内容がないか。

        # 留意事項
        - 「改善点」は具体的に記述してください
        - 改善点がない場合は「改善点」の欄に「なし」と明記してください
        - 文章は「です・ます」調で統一してください

        # 記事内容
        {article}

        # 出力フォーマット        
        **観点1: 技術的正確性**
        - **評価ポイント**: {{良い点を一文で記載}}
        - **改善点**:
        - {{改善点がある場合は記載し、ない場合は「なし」と記載}}

        **観点2: 独創性・独自性**
        - **評価ポイント**: {{良い点を一文で記載}}
        - **改善点**:
        - {{改善点がある場合は記載し、ない場合は「なし」と記載}}

        **観点3: 網羅性**
        - **評価ポイント**: {{良い点を一文で記載}}
        - **改善点**:
        - {{改善点がある場合は記載し、ない場合は「なし」と記載}}

        **観点4: 内容の質**
        - **評価ポイント**: {{良い点を一文で記載}}
        - **改善点**:
        - {{改善点がある場合は記載し、ない場合は「なし」と記載}}

        **観点5: 倫理性・適切性**
        - **評価ポイント**: {{良い点を一文で記載}}
        - **改善点**:
        - {{改善点がある場合は記載し、ない場合は「なし」と記載}}
    """,
    )
    parser = StrOutputParser()
    chain = prompt | llm | parser

    # 実行して結果を取得
    result = chain.invoke({"article": article})
    state.review_results.content_check = result

    return state
from typing import Any
from langchain_aws import ChatBedrock
from state.state import State
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser


def self_reflection(state: State) -> dict[str, Any]:
    """
    review_integrationの結果を評価し、フォーマットと改善点の抽出が適切か判断する

    Args:
        state (State): アプリケーションの状態

    Returns:
        dict[str, Any]: 評価結果を含む辞書
    """
    llm = ChatBedrock(
        model_id={モデルIDを入力},
        region_name={リージョン名を入力},
    )


    review_result = state.review_summary
    content_check_result = state.review_results.content_check
    sentence_quality_check_result = state.review_results.sentence_quality_check
    structure_check_result = state.review_results.structure_check

    base_prompt_template = """
    あなたはプロフェッショナルなレビュー統合結果を評価する品質管理者です。
    あなたのタスクは、レビュー結果を評価し、以下の評価観点が満たされているかを判定することです。

    # 前提
    レビュー結果は統合元レビュー結果を元に生成されています。

    # 評価観点
    1. フォーマットの統一性
    - 【内容について】【文章の品質について】【構造について】の3セクションがある
    - 観点は1から始まっている
    - 各セクションに「観点」と「改善点」が含まれている

    2.文章のスタイルが適切か
    - 「評価ポイント」に否定的な表現が含まれていない
    - 文章の文体が「です・ます」調で統一されている

    3. 改善点の抽出が適切か
    - 「なし」以外の改善点が漏れなく抽出されている
    - 改善点の内容が元のレビュー結果から変更されていない

    # 判定ルール
    - 全ての評価観点を満たしている場合 → "OK"
    - いずれかの評価観点を満たしていない場合 → "NG" + その理由と改善案

    # レビュー結果
    {review_result}

    # 統合元レビュー結果
    ### 構造について
    {structure_check_result}

    ### 内容について
    {content_check_result}

    ### 文章の品質について
    {sentence_quality_check_result} 

    # レビュー対象記事
    {article}

    # 出力フォーマット
    {{"judgement": "OK" or "NG", "feedback": "評価観点が満たされていない理由、改善案"}}

    """

    # プロンプトの作成と実行
    prompt = PromptTemplate(
        input_variables=["review_result"],
        template=base_prompt_template,
    )
    output_parser = JsonOutputParser()
    chain = prompt | llm | output_parser

    # 評価の実行
    result = chain.invoke(
        {
            "review_result": review_result,
            "content_check_result": content_check_result,
            "sentence_quality_check_result": sentence_quality_check_result,
            "structure_check_result": structure_check_result,
        }
    )

    # retry_countの更新
    current_retry_count = int(state.reflection_result.retry_count or 0)
    if result["judgement"] == "NG":
        current_retry_count += 1

    # 結果を状態に保存
    return {
        "reflection_result": state.reflection_result.model_copy(
            update={
                "judgement": result["judgement"],
                "feedback": result["feedback"],
                "retry_count": current_retry_count,  # 更新されたカウントを保存
            }
        )
    }

エントリーポイントの追加

次にエントリーポイント(最初の処理)を設定します。今回structure_checkノードから処理を始めるため、エントリーポイントにstructure_checkノードを追加します。

workflow.set_entry_point("structure_check")

エッジの追加

次に、処理同士を接続するためにエッジを追加します。

workflow.add_edge("structure_check", "content_check")
workflow.add_edge("content_check", "quality_check")
workflow.add_edge("quality_check", "review_integration")
workflow.add_edge("review_integration","self_reflection")

条件分岐が入る部分については、条件付きエッジを定義します。

from langgraph.graph import END
# 条件付きエッジの追加
workflow.add_conditional_edges(
    "self_reflection",
    branch_on_reflection,
    {"continue": "review_integration", "end": END},
)

ここでは、self_reflectionノードの処理の後、branch_on_reflection関数の戻り値によって次のノードを定義しています。 LangGraphでは組み込みでENDノードが用意されており、branch_on_reflectionノードの結果が"end"だった場合は処理を終了させます。 branch_on_reflection関数は以下の通りです。

def branch_on_reflection(state: State) -> str:
    """リフレクション結果に基づいてフローを分岐"""
    reflection_result = state.reflection_result
    if reflection_result.judgement == "NG" and reflection_result.retry_count < 4:
        return "continue"
    return "end"

コンパイルと実行

最後にここまでのワークフローをコンパイルして実行可能なインスタンスになります。

compiled = workflow.compile()

これを以下のようにinvoke関数で実行することで処理が走ります。

article_content = {GitHubから取得した記事内容}
initial_state = State(article=article_content)
result = compiled.invoke(initial_state)

※GitHubとのやりとり部分(記事内容の取得とコメントの投稿)は、グラフの実行前後に行っています。

成果物について

本システムを動かした結果が以下となります。 こちらは、本記事(初稿段階)のプルリクエストに対するレビューコメントです。

やや冗長ですが、、必要な観点は抑えられているのではないでしょうか。 日付など一部指摘に誤りが見られる点は課題ですが、特に表記揺れや構成など、自分で気づけていなかった誤りを適切に指摘してくれています。 実際に今回の記事はこのレビュー結果を取り入れつつ作成しました。

なお、レビューコメントの投稿には GitHub Apps で作成したtechblog-reviewを利用しています。 GitHub Apps を作成し、組織レベルでの管理とすることで、アクセス権を限定し、特定のユーザーに依存せずにシステムを運用することができます。

参考

GitHub Apps の作成や認証方法については、以下の記事を参考にしました。
組織アカウントで作成する場合は、各種 ID やシークレットをリポジトリオーナーに発行してもらう必要がある点にご留意ください。

今後の期待

今回まずは試作としてテックブログレビューエージェントを作成しましたが、まだまだ改善すべき点は多くあります。

個人的には、例えば以下の点についてアップデートをしていきたいと考えています。

  • 出力精度(品質)の向上
    • 処理フローの改善
      • 統合結果だけでなく各レビューに対してもセルフリフレクションを取り入れる
      • 記事内容に基づいて対象読者(ペルソナ)を作成し、ペルソナの視点からレビューを行う
      • 内容チェックでWeb検索を取り入れ、類似記事の参照や比較ができるようにする
      • レビューにSEO観点を取り入れ、検索流入を意識したキーワードやタイトルの提案を行う
    • プロンプトの改善
  • 出力の冗長さの改善
    • 「文章の品質」の部分に関しては全体コメントの中ではなく以下例のようなSuggestion機能を利用したコメントをできるようにする
    • プロンプトを改善し適切なフォーマットにする

おわりに

本記事では、テックブログレビューエージェントの開発過程を紹介しました。
成果物としてのレビューコメントには課題もあるものの、適切な指摘が多く含まれており、今後社内で活用していけそうだと感じました。
また、今回の開発を通じてLangGraphやGitHub Appsなどの新しい技術に触れることができ、技術的なキャッチアップとしても貴重な機会となりました。

現在、本テックブログレビューエージェントはまだ実運用を行っていないため、今後は社内での運用を開始し、得られたフィードバックを反映しながら、継続的に改善していきたいと思います!