この記事では「総務省新着情報AI (@micsummary@mastodon.kotet.jp)」というMastodon Botを作成した経験について書く。 このBotは総務省の新着情報RSSフィードを監視し、新着情報に添付されたPDFを自動で要約してMastodonに投稿するものである。 たとえば以下のような投稿を行う。最近ニュースになっている海底ケーブルに関する会議について要約している。
「コストをかけない、がんばらない」という方針で進めたため非常に時間がかかってしまったが、同様のBotを作成したい人のために面白かった点を書いていく。
なぜBotを作ろうと思ったのか
総務省のホームページは新着情報をRSSフィードとして提供している。 基本的には総務省から出る報道資料や、新たな制度を作るにあたっての会議の情報とそこで出た資料や議事録が掲載される。 ただこの新着情報、ほとんどPDFで提供されており、内容を把握するにはPDFを開いて読む必要がある。
特に報道資料はひどくて、以下の画像のように本文が1文のみで1枚のPDFが添付されているだけのものもある。 報道資料なので凝ったデザインにするより速報性を大事にするのはわかるが、であればPDFから卒業してさらなる効率化を図ってほしいところだ。 公文書は誰でも同じ内容が印刷できるようでなくてはならない、みたいな法的な制約があるのだろうか?
自分は政府がうまいこと運営されている様子がうかがい知れて面白いので新着情報を見ているのだが、PDFを閲覧できる大きな端末の前にじっと座ってPDFを読むほど興味があるわけではない。 そこでLLMを使ってPDFの内容を要約し、Mastodonに投稿するBotを作ることにした。 MastodonアカウントはRSSフィードとしても利用できるため、RSSリーダーに登録しておけば、スマホからでも見やすい形式で総務省新着情報をチェックできる。
Botの構成
このBotでは、LLMを使っている箇所が大きく分けて2つある。
- そのページが「要約する価値のあるページかどうか」を判定する部分
- 実際にPDFやページ内容を読み込み、要約を生成する部分
この記事ではより複雑な要約生成の部分にフォーカスして説明する。判定部分についても、基本的な考え方はほぼ同じとなる。 以下、書きたいことをあまり整理せずに羅列していく。
開発に使用した技術
自分はRaspberry Pi Zero Wを持っていて、自宅で常時稼働させている。 このRaspberry Piの上ではGoで作られたモノレポ風のサーバーアプリケーションが既に動いているため、Botはその一部として組み込めるようにGoのライブラリとして実装した。
Botの作成にあたってはGemini Code Assistを利用した。 ただ、当時の性能、かつ無料枠ではコーディング性能が十分ではなく、リポジトリの規模が大きくなってくるとほとんど前に進まなくなってしまった。 そのため体感7割ほどは自分で書いている。
LLMサービス、モデルの選定
Botの要約生成には、GoogleのGeminiを利用した。 GoogleのAPIには無料枠が多い。GeminiもちょっとしたBotを作るくらいなら十分すぎるくらいの無料枠がある。
Botを作り始めた時点ではgemini-2.5-proも使えた。 gemini-2.5-flashでも内容を適切に反映した要約を生成することはできる。 しかしgemini-2.5-proにモデルを切り替えたところ、全体の要約をするだけだったところから一歩進んで「面白い」部分をうまくピックアップしてくれるようになった。
無料枠(無料Tierのレートリミット)は安定しているわけではなく、急に制限がかかることもある。 Botを作り始めた時点ではAPIのレートリミットはドキュメントに記載されていたが、頻繁に変更するためにドキュメントからは削除され、現在はダッシュボードでのみ確認できるようになっているようだ。 まあ使えるだけありがたいと思っている。
gemini-2.5-proを要約に使っていたが、急に無料枠が廃止されてResourceExhaustedエラーが出るようになって動かなくなってしまった。 また、要約対象スクリーニングに使っていたgemini-2.0-flashも廃止されたようだ。 現在はgemini-2.5-flash/gemini-2.5-flash-liteの組み合わせで動かしている。 LLMの性能向上が止まらないうちは無料枠が縮小してもそのぶん性能が上がってダメージは少ないかもしれない。 ただ、無料枠での運用をあきらめたり、無料枠のモデルの性能が下がっていったときのために、モデルの賢さに依存しすぎない設計にしておくことも重要だと思う。
構造化出力
構造化出力とは
LLMの出力には構造化出力 (structured output) を使っている。 構造化出力はLLMの応答をスキーマで定義した形にしてもらう機能である。 単純にプロンプトでそのように指示するのとは異なり、応答生成の過程で働くことでスキーマに合わない出力につながるようなトークンの生成確率がゼロになる。 そのため、たとえば素の状態ではValidなJSONを出力することすら怪しいLLMでも、構造化出力機能を使えばスキーマに沿って項目を過不足なく含んだJSONを必ず生成できる。
もちろんスキーマで表現できていない部分のクオリティはLLMの性能に依存するため、得られた出力が価値のある物かどうかは別問題である。 しかし少なくとも出力の形式が保証されるため、自動化との親和性が非常に高い。
この構造化出力でChain-of-Thoughtの流れを作ることで、自分が行っている作業の流れを記憶して制御するメタ能力の無いような性能の低いモデルにもChain-of-Thoughtをさせることができる、という話を聞いたので試してみている。
要約生成スキーマ
以下のコードは実際にBotで使っている要約生成のための構造化出力のスキーマ定義である。 大きく分けて5つの部分から構成されている。
documents: 各ドキュメントごとの要約とキーポイントを抽出させるfirst_summary: 上の内容を踏まえてとりあえず初稿を作らせるomissibles: 初稿から省いても良い内容を列挙させるmissed_items: 初稿に入れ忘れた重要な内容を列挙させるfinal_summary: 上記のすべてをもとに改めて要約を作成する
段階的に要約を出力しているような出力をこのように強制することで、LLMがより短く洗練された要約を生成することを期待している。
modelConfig := &genai.GenerateContentConfig{
Temperature: new(float32), // 0
ResponseMIMEType: "application/json",
ResponseSchema: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"documents": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"metadata": {
Type: genai.TypeString,
},
"keyPoints": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeString,
},
},
"summary": {
Type: genai.TypeString,
},
},
PropertyOrdering: []string{"metadata", "keyPoints", "summary"},
},
},
"first_summary": {
Type: genai.TypeString,
},
"omissibles": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeString,
},
},
"missed_items": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeString,
},
},
"final_summary": {
Type: genai.TypeString,
},
},
PropertyOrdering: []string{"documents", "first_summary", "omissibles", "missed_items", "final_summary"},
Required: []string{"final_summary"},
},
}
ちなみに入力プロンプトは普通の自然言語で書くことができる。 以下はその抜粋である。 指示と出力の部位が対応するようにキー名をそのまま使って箇条書きにしているが、もしかしたら十分に賢いモデルならそんな工夫すら不要かもしれない。
【ドキュメント要約出力形式】
- metadata: タイトル、日時、出席者、開催場所、議題などのメタ情報
- keyPoints: 重要な発言や決定事項を3~5項目列挙
- summary: 文書の要点を日本語で簡潔にまとめる
【一次要約出力形式】
- first_summary: 会議の特に重要な部分を取り上げ、だ/である調、3~5文、全体で200文字程度の日本語にまとめる
【改善点出力形式】
- omissibles: タイトルに含まれている等で要約に含める必要のない情報、一次要約内で重複していて1回にまとめられる情報を列挙
- missed_items: 一次要約に含まれていないが重要な事実や決定事項に関するキーワードを列挙
【最終要約出力形式】
- final_summary: 会議の特に重要な部分を取り上げ、だ/である調、3~5文、全体で200文字程度の日本語にまとめる。短縮した結果余裕がある場合、missed_itemsに基づき重要な情報を追加して充実させる
結果
結果として以下のような出力が得られる。 一定以上の長さのデータを人に読ませるときは人間の解釈を添えておくべきであると考えているため軽く解説する。
FirstSummaryはDocumentsを出力した直後に生成される要約であり、結果として、提出意見を踏まえた案の修正は行われなかった等のもっとも重要な部分を要約できてはいる。
しかし「どんな意見が寄せられたのか」「それに対して総務省はどう考えたのか」といった個別のトピックを拾えていない。
MissedItemsでは一次要約に含まれていないトピックとして都度課金型ゲームやスーパーチャットに関する内容が挙げられている。
意見の中でも新しい概念を含むものであり、「スーパーチャットは消費者物価指数の対象外」みたいなニュースの見出しになりそうな面白味のある内容だと思う。
FinalSummaryではその内容がうまく盛り込まれており、より充実した要約になっている。
構造化出力による段階的な要約生成の効果が出ているように思う。
micsummarybot.SummarizeResult{
Documents:[]micsummarybot.DocumentSummary{
micsummarybot.DocumentSummary{
Summary:"総務省は、令和7年7月31日から9月5日に実施した「消費者物価指数2025年基準改定計画(案)」に関する意見募集の結果を公表した。11件の意見が寄せられ、それらに対する総務省の考え方が示された。今後のスケジュールとして、本年中に2025年基準改定計画を決定し、令和8年(2026年)7月分の消費者物価指数公表時に2025年基準指数への切り替えを予定している。",
Metadata:"報道資料 「消費者物価指数2025年基準改定計画(案)」についての意見募集の結果 令和7年10月16日",
KeyPoints:[]string{"総務省は「消費者物価指数2025年基準改定計画(案)」に対する意見募集結果を公表した。", "令和7年7月31日から9月5日まで意見募集を実施し、11件の意見を受領した。", "提出された意見とそれに対する総務省の考え方を公表した。", "2025年基準改定計画は本年中に決定され、令和8年(2026年)7月分の消費者物価指数公表時に2025年基準指数へ切り替え予定である。"}
},
micsummarybot.DocumentSummary{
Summary:"「消費者物価指数2025年基準改定計画(案)」に対する意見募集で寄せられた11件の意見と、それに対する総務省の考え方が示された。意見は品目選定、調査方法、名称変更など多岐にわたるが、総務省は既存の選定基準や統計作成上の基本方針に基づき、各意見への対応を説明。新たな品目追加の要望については、基準適合状況に応じて検討する方針を示し、都度課金型ゲームやスーパーチャットは対象外とした。家賃の品質調整は今後も検討を進める。結果として、提出意見を踏まえた案の修正は行われなかった。",
Metadata:"「消費者物価指数2025年基準改定計画(案)」に対して提出された御意見及び総務省の考え方 (令和7年7月31日~同年9月5日意見募集) 別紙",
KeyPoints:[]string{"意見募集で寄せられた11件の意見に対し、総務省は既存の品目選定基準や調査方針に基づき回答した。", "「充電料」「充填料」やフードデリバリー、有線イヤホンなど、新たな品目追加の要望は、今後の基準適合状況に応じて検討される。", "都度課金型ゲームやスーパーチャットは、品質継続調査の困難さや所得移転の性格から消費者物価指数の対象外とされた。", "「ノンアルコールビール」の名称変更や「サッカー観戦料」等の廃止は、品目概念の明確化や選定基準に基づき適切と判断された。", "家賃の品質調整については、重要性を鑑み、今後も実証的な研究分析と検討を進める方針である。", "提出された意見を踏まえた案の修正は行われなかった。"}
}
},
FirstSummary:"総務省は意見募集結果を公表した。11件の意見が寄せられたが、総務省は既存の品目選定基準や統計作成方針に基づき回答し、案の修正は行われなかった。本年中に改定計画を決定し、2026年7月分の消費者物価指数公表時に2025年基準指数へ切り替える予定。",
Omissibles:[]string{"「消費者物価指数2025年基準改定計画(案)」についての意見募集の結果", "令和7年10月16日", "意見募集期間", "意見数", "総務省が意見募集を実施したこと"},
MissedItems:[]string{"都度課金型ゲームやスーパーチャットが消費者物価指数の対象外である理由(品質継続調査の困難さ、所得移転の性格)", "家賃の品質調整に関する今後の検討方針"},
FinalSummary:"総務省は意見募集結果を公表。寄せられた11件の意見に対し、既存の品目選定基準や統計作成方針に基づき回答し、案の修正は行われなかった。都度課金型ゲームやスーパーチャットは品質継続調査の困難さや所得移転の性格から対象外とされた。家賃の品質調整は今後も実証的な研究分析と検討を進める。本年中に改定計画を決定し、2026年7月分の消費者物価指数公表時に2025年基準指数へ切り替える予定。"
}
API呼び出しテスト
テストとして、実際のAPIキーを使ったモデル呼び出しを含んだユニットテストを書いた。 スクリーニングに関しては実際に要約対象の記事と要約対象でない記事を用意して、正しく判定されるかを確認している。 要約生成に関しては機械的なチェックが難しいので、生成された要約を目視で確認する形にしている。 途中から導入したが、やはりこのように手軽にテスト実行できるのは便利だった。
$ make test-full
go test -v -tags=integration ./...
// 中略...
=== RUN TestSummarizeDocument
=== RUN TestSummarizeDocument/Summarize_page_with_one_PDF
time=2025-12-13T16:29:20.496+09:00 level=INFO msg="Starting document summarization process"
time=2025-12-13T16:29:20.496+09:00 level=INFO msg="Processing documents for download" count=1
time=2025-12-13T16:29:20.496+09:00 level=INFO msg="Downloading PDF file" url=../resources/example_for_summerize/001034183.pdf local_path=/tmp/TestSummarizeDocument3573160111/001/b89950ed-ecd4-4962-ab06-a2f0751ca245.pdf
time=2025-12-13T16:29:23.412+09:00 level=INFO msg="Starting Gemini API calls with retry" max_retry=4
summarizer_integration_test.go:120: Generated Summary: micsummarybot.SummarizeResult{Documents:[]micsummarybot.DocumentSummary{micsummarybot.DocumentSummary{Summary:"総務省は、令和7年7月31日から9月5日に実施した「消費者物価指数2025年基準改定計画(案)」に関する意見募集の結果を公表した。11件の意見が寄せられ、それらに対する総務省の考え方が示された。今後のスケジュールとして、本年中に2025年基準改定計画を決定し、令和8年(2026年)7月分の消費者物価指数公表時に2025年基準指数への切り替えを予定している。", Metadata:"報道資料 「消費者物価指数2025年基準改定計画(案)」についての意見募集の結果 令和7年10月16日", KeyPoints:[]string{"総務省は「消費者物価指数2025年基準改定計画(案)」に対する意見募集結果を公表した。", "令和7年7月31日から9月5日まで意見募集を実施し、11件の意見を受領した。", "提出された意見とそれに対する総務省の考え方を公表した。", "2025年基準改定計画は本年中に決定され、令和8年(2026年)7月分の消費者物価指数公表時に2025年基準指数へ切り替え予定である。"}}, micsummarybot.DocumentSummary{Summary:"「消費者物価指数2025年基準改定計画(案)」に対する意見募集で寄せられた11件の意見と、それに対する総務省の考え方が示された。意見は品目選定、調査方法、名称変更など多岐にわたるが、総務省は既存の選定基準や統計作成上の基本方針に基づき、各意見への対応を説明。新たな品目追加の要望については、基準適合状況に応じて検討する方針を示し、都度課金型ゲームやスーパーチャットは対象外とした。家賃の品質調整は今後も検討を進める。結果として、提出意見を踏まえた案の修正は行われなかった。", Metadata:"「消費者物価指数2025年基準改定計画(案)」に対して提出された御意見及び総務省の考え方 (令和7年7月31日~同年9月5日意見募集) 別紙", KeyPoints:[]string{"意見募集で寄せられた11件の意見に対し、総務省は既存の品目選定基準や調査方針に基づき回答した。", "「充電料」「充填料」やフードデリバリー、有線イヤホンなど、新たな品目追加の要望は、今後の基準適合状況に応じて検討される。", "都度課金型ゲームやスーパーチャットは、品質継続調査の困難さや所得移転の性格から消費者物価指数の対象外とされた。", "「ノンアルコールビール」の名称変更や「サッカー観戦料」等の廃止は、品目概念の明確化や選定基準に基づき適切と判断された。", "家賃の品質調整については、重要性を鑑み、今後も実証的な研究分析と検討を進める方針である。", "提出された意見を踏まえた案の修正は行われなかった。"}}}, FirstSummary:"総務省は意見募集結果を公表した。11件の意見が寄せられたが、総務省は既存の品目選定基準や統計作成方針に基づき回答し、案の修正は行われなかった。本年中に改定計画を決定し、2026年7月分の消費者物価指数公表時に2025年基準指数へ切り替える予定。", Omissibles:[]string{"「消費者物価指数2025年基準改定計画(案)」についての意見募集の結果", "令和7年10月16日", "意見募集期間", "意見数", "総務省が意見募集を実施したこと"}, MissedItems:[]string{"都度課金型ゲームやスーパーチャットが消費者物価指数の対象外である理由(品質継続調査の困難さ、所得移転の性格)", "家賃の品質調整に関する今後の検討方針"}, FinalSummary:"総務省は意見募集結果を公表。寄せられた11件の意見に対し、既存の品目選定基準や統計作成方針に基づき回答し、案の修正は行われなかった。都度課金型ゲームやスーパーチャットは品質継続調査の困難さや所得移転の性格から対象外とされた。家賃の品質調整は今後も実証的な研究分析と検討を進める。本年中に改定計画を決定し、2026年7月分の消費者物価指数公表時に2025年基準指数へ切り替える予定。"}
--- PASS: TestSummarizeDocument (39.22s)
--- PASS: TestSummarizeDocument/Summarize_page_with_one_PDF (39.22s)
PASS
ok github.com/kotet/mic-summary-bot/mic_summary_bot 47.154s
おわりに
Botを作って運用しブログ記事を書こうという構想はずっと前からあったが、かなり時間がかかってしまった。 「内容がわかりにくいRSSフィードを要約する」という用途は総務省に限らず様々な場面で応用できると思う。 そのようなBotがこのようにノーコストで運用できる。 各々の無料枠を活用して、ぜひ色々な情報を要約するBotを作ってみてほしい。

