IT 関連の情報収集に RSS フィードアプリを使い始めたところ、すぐに手放せないものとなりました。
自分が気になるメディアやトピックを登録して、フィードを追いかければ、興味のある分野の記事が一気に集まってきます。
摂取する情報密度が大幅に上がり、今では RSS アプリがない生活は考えられません。
ただ、気になる記事の中には英語のものが少なくありません。
そこで英文記事の URL を ChatGPT に投げ、「要約して」「これ何がポイント?」と聞いていくスタイルで読むようにしました。
生成 AI の力で言語の壁も文字数の壁も乗り越えられる!と快適なニュース生活を送っていたところ、
ある日「自分オリジナルの News Reader」を作ってみたくなる事件が起こりました。
そうしてできたのが、自分の目標とその進捗を踏まえながら、3 人のペルソナが独自視点で記事を語ってくれる個人ツール News Prism です。
1. きっかけ
ChatGPT に記事を解説してもらう運用は、しばらく快適に回っていました。
「要約して」「これ何がポイント?」「新しい機能はある?」
気になった記事の URL と一緒にこうしたメッセージを投げていけば、サクサク情報を消化できます。
そんなある日、事件は起きました。
通勤中に、ランサムウェア関連の記事をいくつか眺めていました。
そして「ランサム多いねー」というメッセージと、ある記事の URL を ChatGPT に投げました。
すると、その記事とは全く関係のないランサムウェアについて解説を始めたのです。
そんなこと書いてあったっけ?」と思って聞いてみました。
(私)「ちゃんと記事読んだ?」
(AI)「すいません、読んでませんでした。」
これまで便利だと思っていた AI アプリによる要約は、記事の内容を元に答えている保証はなく、常にハルシネーションのリスクが付きまとっていたのだと実感したのでした。
ツールの裏側を理解していないと、ハルシネーションに振り回され続けるかもしれない、、
そう感じて、自分好みの要約システムを作ってしまおうと思い立ちました。
欲しいと思っていた機能は次の 3 つです。
- 記事を 必ず 取得した上で要約してくれる
- 自分の目標や進捗を入れたファイルを動的に参照してくれる
- それを踏まえて複数の切り口で語ってくれる
そうして News Prism が誕生しました。
2. News Prism の概要
ざっくり言うと、こんなツールです。
- スマホの Safari から記事の URL を投げる
- Amazon Bedrock で 4 並列の解析が走る (中立要約 + 3 つのペルソナ)
- 構造化された JSON が返ってきて、整形された画面で読める
- 履歴は DynamoDB に蓄積されていく
3 つのペルソナの役割は、自身の業務や興味に合わせて以下のように設定しました。
- セキュリティ標準設計の専門家:アーキテクチャや統制、その運用の観点でコメントする
- サプライチェーンセキュリティの専門家:第三者経由のリスク観点でコメントする
- ブロガー:発信ネタとしての切り口でコメントする
返ってくる JSON はこんな構造です。
実画面のスクショは 5 章でご紹介しますので、ここでは「何が返るか」のイメージとしてご覧ください。
{
"url": "...",
"title": "記事のタイトル",
"neutral_summary": "事実だけを淡々と要約した中立要約",
"perspectives": [
{ "persona": "セキュリティ標準設計の専門家", "comment": "..." },
{ "persona": "サプライチェーンセキュリティの専門家", "comment": "..." },
{ "persona": "ブロガー", "comment": "..." }
],
"action_items": ["やってみたいことの具体メモ"],
"relevant_okr_refs": ["関連する自分の目標"]
}
ちなみに ChatGPT には、固定プロンプトを仕込める Project という機能と、ChatGPT Codex Connector という連携機能を組み合わせれば、近いことは実現できます。
ただ、結局ハルシネーションと付き合っていかなければならないことは、構造上変わらないと実感することになりました。
それはまた別の記事で書きたいと思います。
3. アーキテクチャ
News Prism は、自分の AWS アカウントを軸とし、外部依存は自分が管理する GitHub private repo (context.md) のみの構成です。
主なコンポーネントの役割は次の通りです。
| コンポーネント | 役割 |
|---|---|
| CloudFront | Web UI 配信 + API Gateway へのルーティング |
| API Gateway (REST) | API Key + Usage Plan による呼び出し制限 |
| Lambda (Python 3.12) | 記事 fetch + Bedrock 呼び出し + DynamoDB 書き込み |
| Bedrock | 中立要約 + 3 ペルソナの見解 |
| DynamoDB | 履歴の永続化 |
| Secrets Manager | GitHub の PAT 保管 |
設計の時に判断したことを 3 つだけ紹介します。
REST API + API Key を選んだ
HTTP API も検討しましたが、Usage Plan (利用量制限) がネイティブでサポートされている REST API を選びました。
個人ツールなので、想定外の呼び出しが走るとそのままコスト直撃になります。
お財布を痛めないためにも Usage Plan で上限を引いておきたいという事情です。
Lambda は zip + pip --platform でビルドした
ローカルが macOS なので、Lambda 用の Linux パッケージを揃えるのに最初は Docker container Lambda を検討していました。
ただビルド環境を別途用意するのが少し重く、pip --platform manylinux2014_x86_64 ... で Mac から直接 Linux 用パッケージを引いて zip するスタイルに落ち着きました。
Docker を起動せずに済ませる軽量スタイルです。
Secrets Manager は枠だけ Terraform 管理にした
Lambda から GitHub を読めるように、GitHub の PAT (Personal Access Token) を Secrets Manager に置いています。
PAT は fine-grained タイプで、対象リポジトリの Contents: Read only に絞ったものです。
ただし PAT そのものは Terraform で管理していません。
Terraform で値まで管理しようとすると local state に平文で残ってしまうためです。
Secrets Manager のリソース定義 (aws_secretsmanager_secret) は Terraform で管理し、値そのものはマネジメントコンソールから手で投入する構成にしています。
4. こだわりポイント
News Prism の中身を支えている設計判断を 3 つ紹介します。
自分の目標と進捗を動的に取得する
GitHub のプライベートリポジトリに context.md というファイルを置いていて、これを Lambda が毎回 fetch します。
ファイルの中身は、自分の目標とその進捗です。
これは別の仕組みなのですが、PC 端末で Claude Code に目標を確認して進捗を報告する度に、中身を更新して GitHub にプッシュしてもらっています。
ポイントは、これを prompt の <context> ブロックに 背景情報 (background information) として渡していることです。
そうすることで、日常でどんどん更新されていくファイルを、News Prism が読んで把握してくれることになります。
ニュースを解説するだけでなく、自分の目標や進捗に合わせて、「こう活かしたら良いのではないか?」と提案してくれるアプリになりました。
自分のことを理解してコメントしてくれていて嬉しくなります。
ペルソナごとに並列で語ってもらう
Bedrock で、記事の要約と 3 つのペルソナによるコメント生成を 4 並列で行っています。
要約と各ペルソナを独立した呼び出しにすることで、待ち時間を減らせると同時に、ペルソナ同士の視点が引きずられにくくなる意図もあります。
実装当初は 1 つの呼び出しで複数の視点をまとめて生成しており、1 記事の処理に約 80 秒かかっていました。
「1 記事あたり 80 秒待たされたら、たいして記事を読めずに通勤が終わってしまう、、!」とショックを受けましたが、
4 並列の処理に辿り着いたことで wall avg 12 秒に着地しています。
レイテンシのさらなる改善は、また別の機会に書きたいテーマです。
また「要約 + 3 視点で書いて」と 1 つのリクエストで頼むのと、4 つの独立した呼び出しで書くのとでは、構造上の意味が違ってきます。
1 つのリクエストでは前半の視点が後半に影響しがちですが、独立したリクエストでは互いが見えない状態で回答が生成されます。
Prompt Caching で入力コストを抑える
3 人のペルソナのシステムプロンプトと <context> (自分の目標と進捗) は、毎回ほぼ同じ内容です。
そこで Bedrock の Prompt Caching を使って、共通部分を cache 境界に置き、ペルソナごとの差分だけを後ろに付ける構成にしました。
これにより、記事を 8 連投した時の計測で、Bedrock に渡す入力側のコストを 57.9% 削減できることを確認しました。
5. デモ画面
実際の画面と数字をいくつか紹介します。
入力フォームはこんな感じです。スマホの Safari から URL を貼って待つだけです。
処理が完了すると、まず完了表示と記事のメタ情報、続いて要約が現れます。
下にスクロールすると、3 人のペルソナがそれぞれの視点で語ってくれます。
さらに下にはアクション提案と、関連する自分の目標 (KR) が並びます。
本番の Lambda で 8 連投したときの数値はこんな感じでした。
| 項目 | 値 |
|---|---|
| キャッシュヒットトークン | 13,909 × 7 (1 回目で書き込み、2-8 回目で全ヒット) |
| 入力側コスト削減 (出力分除く) | 57.9% |
| 失敗 | 0 / 8 |
試作段階で観測した Prompt Caching 設計が AWS 本番でもそのまま再現したのは、嬉しい一次情報でした。
レイテンシは現状 wall avg 12 秒ほどです。
スマホから URL を投げて、12 秒待って結果を読む、というリズムは個人ツールとしては許容範囲ですが、ここはまだ改善余地が大きいテーマです。
別記事「レイテンシ改善編」として、後日改めて書こうと思います。
まとめ
生成 AI に「読んでませんでした」と返された日から、自分オリジナルの News Reader を作った話でした。
満足な機能を実現できただけでなく、並列実行や Prompt Caching、レイテンシ問題への直面など、生成 AI アプリ実装のリアルを肌で感じることができました。
少しだけ裏側を知って愛着が湧き、生成 AI とより一層仲良くなりたいと思うのでした。
今日も小さな学びを。
Bedrock 関連記事