1. はじめに
技術資格の取得は、エンジニアとしての成長において大きな意味を持ちます。一方で、Qiita には日々大量の合格体験記が投稿されており、必要な情報を継続的に追いかけることは簡単ではありません。
私は社内で資格取得推進に関わっていたことから、
- 「どんな資格があるのか」
- 「どんな学習方法があるのか」
- 「取得するとどんなメリットがあるのか」
を、もっと気軽に知れる仕組みを作れないかと考えました。
そこで開発したのが、Qiita の資格関連記事を自動収集し、要約して Slack に発信するエージェントです。
構想~運用までの過程で以下のような課題に直面しました。
- 出力品質が安定しない
- 修正を重ねるほど崩壊する
- レビュー観点を増やすと逆にレビュー通過率が下がる
- 精度と速度・コストのトレードオフが発生する
いわゆる、「生成AIをプロダクトとして扱う難しさ」に直面しました。
本記事では、LangGraph を利用したワークフロー設計を通じて、LLM の出力揺れや品質担保にどう向き合ったのか、そして実際に運用を意識した際にどのような反省が残ったのかについて紹介します。
2. システム概要
システム全体は AWS Lambda 上で動作し、EventBridge による定期実行を行っています。
取得した Qiita 記事を LangGraph ワークフローに流し込み、
- 対象記事かどうかの判定(
judge_and_summarize) - 要約生成(
judge_and_summarize) - 品質レビュー(
review) - 修正(
revise_summary) - Slack通知(
notify) - 処理状態の保存(
save_db)
までを自動化しています。
発信されるデータのイメージ
シーケンス図・ワークフロー
Lambda 全体のシーケンス図
LangGraphの構成
State の肥大化や LangGraph の複雑化を防止する目的のもと、取得した技術記事分 LangGraph をFor文で実行する設計にしています。
3. なぜ LangGraph を採用したのか
今回の開発で LangGraph を採用した理由は、状態による分岐が複数回発生するためです。
品質向上を目的として、以下の分岐ロジックを取り入れることにしました。
- 発信済み判定
- 情報発信に適した技術記事かの判定
- 要約文のレビュー判定
- 以上の判定結果の状態管理
特に今回のシステムでは、
- 要約
- レビュー
- レビュー修正
を繰り返す構成を取っていたため、単純なチェーン型では制御が複雑化しやすくなります。
そのため、状態遷移を明示的に扱える LangGraph を採用しました。
LangGraph によって、
- retry制御
-
review/revise_summaryの循環 - 判定結果による状態遷移
を明確に管理できるようになり、LLMを組み込んだシステムとして整理しやすくなりました。
4. 最も苦労したこと:LLM出力の安定化
今回の開発で最も難しかったのは、要約精度そのものよりも、「出力の安定性」を維持することでした。
当初の設計
初期実装では、
- 要約生成
- レビュー
- NGなら再度要約生成
という構成を取っていました。レビュー指摘内容を次回要約プロンプトに追加し、再生成させる形です。
しかし、この方式には問題がありました。
レビュー指摘を増やすほど、
- 元の要約品質が崩れる
- システムプロンプトとレビュー指摘内容の間で矛盾が発生する
- 要約生成の品質が悪化する
- レビュー通過率がさらに低下する
という現象が発生しました。
つまり、
「レビューを繰り返せば品質が上がる」
わけではなかったのです。
5. 責務分離による改善
そこで途中から設計を見直し、
- 要約ノード (
judge_and_summarize) - レビュー修正ノード (
revise_summary)
のように、初回要約生成とレビュー修正の両方を担当していたノードを細分化し、要約ノードとレビュー修正ノードに細分化しました。
レビュー修正専用のノードの追加
再生成ではなく、「フィードバックに対応する最小限の修正のみ」を行う revise ノードを追加しました。
# レビュー修正ノードプロンプト構造
prompt_text = """
# 役割
あなたは、チームメンバーの成長を支援するために、技術記事の紹介文を洗練させるシニアエンジニアです。
以前作成した紹介文の各要素に対して、品質管理担当者からフィードバックを受けました。
元記事の内容を確認しながら、フィードバックを反映し、各項目を修正してください。
# 修正の指針
- **正確性の担保**: フィードバックに対応する際、元記事の内容と乖離がないよう注意してください。
- **最小限の修正**: フィードバックされた問題点のみを修正し、既存の優れた部分は維持してください。
- **制約遵守**: 「証明できるスキル」「学習のヒント」は**合わせて200文字以内**(1項目あたり80文字程度)に収めてください。
- **形式**: Slackのマークアップ形式を維持してください。
# 入力情報
- 元記事タイトル: {title}
- 元記事本文(抜粋): {body}
- 修正前の資格名: {prev_qualification_name}
- 修正前の「証明できるスキル」: {prev_what_is_qualification}
- 修正前の「学習のヒント」: {prev_learning_tips}
- 修正フィードバック: {feedback}
# 出力形式
{format_instructions}
"""
また、修正時には必ず元記事本文を再度コンテキストとして注入しました。
これは、修正を繰り返す中でハルシネーションが混ざることを防ぐためです。
結果として、
- システムプロンプトとレビュー修正内容の矛盾
- 要約品質劣化
- 不要な再生成
をかなり抑えられるようになりました。
6. 「レビュー項目を増やせば良い」は間違いだった
今回の発案で特に印象的だったのは、レビュー観点を細かくしすぎると、逆にシステム全体が不安定になったことです。
当初は、多くの観点をチェックしようとしていましたが、最終的には以下の4点に絞り込みました。
# src/app/graph/nodes/review.py
# 審査基準
1. **です・ます調の統一**:
- 文章が標準的な丁寧語(「です・ます」調)で統一されているか。
2. **文字量の遵守**:
- 「証明できるスキル」と「学習のヒント」の合計文字数が、**200文字以内**に収まっているか。
3. **非営利表現の徹底**:
- 技術記事と要約内容に転職を推進する内容が含まれていないか。
4. **情報の正確性**:
- 元記事の内容と照らし合わせ、矛盾する情報や架空の内容が含まれていないか。
また、使用するモデルについても運用コストと生成速度、今回のような定型的な修正タスクへの適性を考慮し、gemini-2.5-flash を採用しています。
7. スマホで読まれることを前提にした設計
本システムの対象ユーザーは自社内の社員となり、通勤中や勉強の合間にスマホで閲覧することをユースケースとして想定していました。
そのため今回は、要約精度だけでなく、「最後まで読まれるか」も重視しました。
特に意識したのは、画面をスクロールせずに読み切れる文字量です。
そのため、文章全体で200文字以内に制限するようにプロンプトと Pydantic モデルで定義しました。
# プロンプトによる出力制御
prompt = """
・・・
# 出力項目
以下の3つの内容を抽出・生成してください。紹介文(2と3)は**合わせて200文字以内**(1項目あたり80文字程度)に収めるようにしてください。
1. **資格名** (`qualification_name`): 記事の対象となっている資格の正式名称を抽出してください。
2. **証明できるスキル** (`what_is_qualification`):
単なる資格の説明ではなく、「この資格を得ることで、実務のどのような場面で役立つスキルを証明できるか」「市場価値やキャリアにどうプラスになるか」といった、読者の成長やメリットに焦点を当てて、客観的かつ簡潔に説明してください(80文字程度)。
3. **学習のヒント** (`learning_tips`):
記事の中で特に「ここが工夫されている」「この教材が効果的だった」というポイントを1点か2点に絞って紹介してください(80文字程度)。
**すべてを解説せず、詳細が気になるような「フック」を作ることが重要です。**
・・・
"""
# Pydanticによる出力制御
class JudgeAndSummarizeOutput(BaseModel):
is_matched: bool = Field(description="記事が『若手エンジニアの成長・市場価値向上』に資するIT資格の体験記か")
qualification_name: str = Field(description="対象資格の正式名称")
what_is_qualification: str = Field(description="証明できるスキルやメリット(50-80文字程度)")
learning_tips: str = Field(description="合格の鍵となった教材や方法のエッセンス(50-80文字程度)")
これは単なる文字数制限ではなく、「ユーザー体験」を考慮した設計でした。
※ 振り返って思うが、この辺はもう少し指示文の矛盾や重複を整理できたと思う
8. 意図的に一つのノードに2つの債務を持たせた設計
あえて、技術記事に発信したい内容が含まれているかの判定と、要約文の生成を同時に行うノードにしています。
prompot = """
・・・
# 判定基準 (`is_matched`)
以下の条件をすべて満たす場合に `True` としてください。
1. **資格学習・合格そのものがメインテーマである**: 記事の主目的が、特定のIT資格の取得に向けた学習プロセスの共有、または合格までの道のりの記録であること。
2. **具体的で有益な内容**: 他者がその資格を目指す際に直接参考にできる、具体的な学習方法や教材、気づきが含まれていること。
3. **非営利性**: 転職、勧誘、または特定の製品・サービスの宣伝が主目的ではないこと。
**※以下の場合は `False` としてください(不適合):**
- **資格を「手段」や「素材」として扱っている**: 別の技術、ツール、サービスの検証や紹介のために、資格試験を単なる題材やテストデータとして利用している記事。
- **学習プロセスが希薄**: 試験内容や学習法への言及が少なく、周辺技術の解説や開発日記、ポエムが中心となっているもの。
- **体験記ではない**: 資格の概要、ニュース、制度解説、またはおすすめ資格のまとめ(リストアップ)のみの内容。
# 出力項目
以下の3つの内容を抽出・生成してください。紹介文(2と3)は**合わせて200文字以内**(1項目あたり80文字程度)に収めるようにしてください。
1. **資格名** (`qualification_name`): 記事の対象となっている資格の正式名称を抽出してください。
2. **証明できるスキル** (`what_is_qualification`):
単なる資格の説明ではなく、「この資格を得ることで、実務のどのような場面で役立つスキルを証明できるか」「市場価値やキャリアにどうプラスになるか」といった、読者の成長やメリットに焦点を当てて、客観的かつ簡潔に説明してください(80文字程度)。
3. **学習のヒント** (`learning_tips`):
記事の中で特に「ここが工夫されている」「この教材が効果的だった」というポイントを1点か2点に絞って紹介してください(80文字程度)。
**すべてを解説せず、詳細が気になるような「フック」を作ることが重要です。**
・・・
"""
このような設計にした理由としては、
- 処理時間の短縮
- トークンの節約
となります。
LLM 生成回数に応じて処理時間とコストは増加するため、Lambdaの15分の制約内と限られたコスト内での運用を可能にする設計にしています。
9. 今振り返って感じる反省
今回の開発では、「LangGraph を使って自己修正ワークフローを作る」という技術的好奇心が先行していた部分もありました。
その結果、
- ユーザーからのフィードバックループの欠如
-
review/revise_summaryの多段構成による処理時間・トークンコストの増加
といった課題も残りました。
今回の経験を通じて、
「LLMの精度を上げること」
だけでなく、
「継続改善可能なシステムとして成立させること」
の重要性を強く実感しました。
10. まとめ
今回、LangGraph と Qiita APIを組み合わせた開発を通じて、
責務分離(要約・レビュー・修正)による品質安定化と、ユーザー視点でのプロダクト化の重要性を痛感しました。
特に、「LLMの品質問題はプロンプトだけで解決しようとせず、時には責務分離で対応する」というアプローチも有効である点が大きな学びだったと感じています。
