はじめに
私は広告会社でデータマーケティングをしています。
日々のプランニング業務やデータ分析業務において、MMMや因果推論、状態空間モデルなどの最新トレンドをキャッチアップすることは非常に重要です。
しかし、毎日更新される学術論文に目を通したり、難解な数式から「実務にどう活かせるか」を読み解く時間はありません。
そこで今回は、「毎朝07:00に最新の関連論文を検索し、ビジネスパーソン向けにわかりやすく要約してLINEに届ける」という完全自動化のパイプラインをPythonとGitHub Actionsで構築しました。本記事では、その設計思想(なぜこの構成にしたのか)と実装の工夫を備忘録としてまとめます。
なぜ作ったのか?(開発の背景)
学術論文は素晴らしい知見の宝庫ですが、そのままでは「ビジネス実務との距離が遠い」という課題がありました。
「結局、この数式は自社のマーケティングROI改善にどう使えるのか?」という翻訳作業に人間のリソースを割くのは非効率です。
そこで、「専門的で難解な一次情報を、通勤時間中のスマートフォンで直感的に理解でき、かつ明日の業務のアイデアの種になるレベルまでLLMに咀嚼させる」ことを目的として、このボットの開発に着手しました。
技術選定の背景(Why this Tech Stack?)
開発にあたり、いくつかの技術的な選定を行いました。
1. なぜ論文ソースに「arXiv」を選んだのか?
当初は「引用数」という定量的な注目度データを持つSemantic Scholar APIの利用も検討しました。しかし、論文が発表されてから引用されるまでには数ヶ月〜数年のタイムラグが発生します。
今回は「毎朝の最新速報」が目的であるため、一次ソースであるarXivから最新論文(引用数ゼロ)を取得し、トレンドの評価はLLMの文脈理解力に委ねる設計にしました。
2. なぜ「Gemini 2.5 Flash」なのか?
より高度な推論ができるProモデルもありますが、毎日の定期実行ボットにおいて最優先すべきは「圧倒的な生成スピード」と「無料枠で回せるコストパフォーマンス」です。後述するプロンプトエンジニアリングで出力の「型」をガチガチに固めることで、Flashモデルでも驚くほど質の高いビジネス要約が生成できます。
3. なぜ「LINE」なのか?
Slackやメールへの通知も簡単ですが、「強制的に目に入り、サクッと消費できる」というUX(ユーザー体験)を重視しました。通勤電車の中でニュースアプリを見るような感覚で論文をインプットするには、モバイル最適化されたLINEがベストだと判断しました。
4. なぜ「GitHub Actions」なのか?
毎朝07:00に実行するためだけにサーバー(EC2など)を立ち上げておくのはコストの無駄です。GitHub Actionsのcron機能を活用すれば、サーバー維持費ゼロの完全なクラウドネイティブ運用が可能になります。
実装とコードの解説
保守性を高めるため、実行ロジック(main.py)と設定値(config.py)をファイルを分けて実装しています。これにより、「今週はLLMのマーケティング応用に絞ろう」といった検索条件の変更がノーコードで安全に行えます。
1. 設定とプロンプトエンジニアリング(config.py)
最もこだわったのがプロンプトです。AI特有の直訳調(「〜が示唆された」等)を禁止し、NewsPicksのようなビジネス記事風の出力を強制しています。
config.py
# 大カテゴリと小カテゴリを分けて定義(後で掛け合わせ検索にします)
MAJOR_KEYWORDS = ["Marketing Mix Modeling", "Media Planning", "Advertising", "Marketing Data"]
MINOR_KEYWORDS = ["State Space Models", "Gradient Boosting", "Bayesian", "Time Series", "Causal Inference"]
NUM_PAPERS = 3
# AIへの指示書(プロンプト)
PROMPT_TEMPLATE = """
あなたは最新のデータサイエンスやマーケティングの学術論文を、最前線で活躍するビジネスパーソン向けに解説するプロの編集者です。
以下の論文(タイトルと要約)を読み、LINEでサクッと読めるビジネス記事風のフォーマットで要約を作成してください。
・タイトル: {title}
・内容: {abstract}
【翻訳・要約のルール】
1. 冒頭で「要するに何の論文なのか」を1文でズバリ言い切ること。
2. 直訳調(「〜が示唆された」「本論文は〜を提案する」)を排除し、経済メディアのような洗練された文体で記述すること。
3. 抽象的な表現は極力避け、具体的で実務解像度の高い言葉を選ぶこと。
4. 前置きや挨拶は一切不要。
【出力フォーマット】
🔥【10秒でわかる本論文のコア】
・
📌【従来手法の限界と本研究の狙い】
・
⚙️【課題を突破する新たなアプローチ】
・
📈【実証されたパフォーマンスと成果】
・
💡【明日の実務への応用ヒント】
・(「自社のデータ分析にこう組み込めるかも」というアイデアを1〜2文で提示)
プロンプトの中に {title} と {abstract} というプレースホルダ(空箱)を用意しています。
後でPythonの format() メソッドを使って、取得した論文データをここにカチッと当てはめます。
2. メインロジック(main.py)
APIを叩いてデータを取得し、AIに投げてLINEへ送る中核部分です。
import os
import time
import requests
import random
import xml.etree.ElementTree as ET
from google import genai
import config
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
LINE_CHANNEL_ACCESS_TOKEN = os.getenv('LINE_CHANNEL_ACCESS_TOKEN')
LINE_USER_ID = os.getenv('LINE_USER_ID')
def build_arxiv_query(major_kws, minor_kws):
"""大・小のキーワード群からAND検索のクエリ文字列を組み立てる"""
major_query = "(" + " OR ".join([f'all:"{k}"' for k in major_kws]) + ")"
minor_query = "(" + " OR ".join([f'all:"{k}"' for k in minor_kws]) + ")"
return f"{major_query} AND {minor_query}"
def fetch_arxiv_papers(query, num_papers):
"""arXivから論文データをXML形式で取得する"""
url = "https://export.arxiv.org/api/query"
params = {
"search_query": query,
"start": random.randint(0, 10), # 毎回同じ論文にならないよう少しランダムにずらす
"max_results": num_papers,
"sortBy": "submittedDate",
"sortOrder": "descending"
}
# arXivの仕様に合わせてUser-Agentを設定(これがないと弾かれることがあります)
headers = {"User-Agent": "DailyArxivBot/1.0"}
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status() # エラーがあればここで処理を止める
# XMLデータをパース(解析)してPythonの辞書リストに変換する
root = ET.fromstring(response.text)
ns = {'atom': 'http://www.w3.org/2005/Atom'}
papers = []
for entry in root.findall('atom:entry', ns):
papers.append({
'title': entry.find('atom:title', ns).text.replace('\n', ' ').strip(),
'abstract': entry.find('atom:summary', ns).text.replace('\n', ' ').strip(),
'year': entry.find('atom:published', ns).text[:10],
'id': entry.find('atom:id', ns).text
})
return papers
def summarize_paper(paper_data, client):
"""Gemini APIを呼び出して要約を生成する"""
prompt = config.PROMPT_TEMPLATE.format(
title=paper_data['title'],
abstract=paper_data['abstract']
)
response = client.models.generate_content(model='gemini-2.5-flash', contents=prompt)
return response.text
def send_to_line(message):
"""LINE Messaging APIを叩いてメッセージを送る"""
url = "https://api.line.me/v2/bot/message/push"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}"}
payload = {"to": LINE_USER_ID, "messages": [{"type": "text", "text": message}]}
requests.post(url, headers=headers, json=payload)
def main():
client = genai.Client(api_key=GEMINI_API_KEY)
# 1. 検索クエリの作成とデータ取得
query = build_arxiv_query(config.MAJOR_KEYWORDS, config.MINOR_KEYWORDS)
papers = fetch_arxiv_papers(query, config.NUM_PAPERS)
# 2. 取得した論文を1件ずつループ処理
for i, paper in enumerate(papers):
summary = summarize_paper(paper, client)
# LINE用の見やすいフォーマットに整形
msg = (f"📚 論文速報 ({i+1}/{config.NUM_PAPERS})\n━━━━━━━━━━━━\n"
f"💡 {paper['title']}\n📅 {paper['year']}\n━━━━━━━━━━━━\n\n"
f"{summary}\n\n🔗 原文リンク\n{paper['id']}")
send_to_line(msg)
time.sleep(5) # APIのレート制限(連続アクセス)を回避するための待機時間
if __name__ == "__main__":
main()
xml.etree.ElementTree:arXivのAPIはJSONではなくXMLでデータが返ってきます。そのため、要素ツリーをたどって必要なタグ(title や summary)だけを抽出し、Pythonで扱いやすい辞書型(Dictionary)に変換する処理を挟んでいます。
time.sleep(5):APIをループで連続して叩く際、サーバー側に負荷をかけすぎるとブロックされる(Rate Limitエラー)ため、意図的に5秒の待機時間を設けています。自動化スクリプトを組む際の鉄則です。
3. 自動化ワークフロー
(.github/workflows/ally_research.yml)
作成したPythonスクリプトを、毎朝07:00(JST)に実行するための設定ファイルです。
YAML
name: Daily arXiv Research Bot
on:
schedule:
# JSTの07:00は、UTC(世界標準時)の22:00になります
- cron: '0 22 * * *'
workflow_dispatch: # 手動実行用ボタンを表示する設定
env:
# GitHub Actionsの仕様変更(Node.js 24への強制移行)による警告エラーを防ぐためのおまじない
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Python Script
run: python main.py
env: # GitHubのSecretsに登録した機密情報を環境変数として渡す
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
LINE_CHANNEL_ACCESS_TOKEN: ${{ secrets.LINE_CHANNEL_ACCESS_TOKEN }}
LINE_USER_ID: ${{ secrets.LINE_USER_ID }}
おわりに
このボットを導入してから、通勤電車の中でスマホを開くだけで、実務に直結する分析手法のアイデアを効率よくインプットできるようになりました。
APIを利用した業務効率化や、LLMをシステムに組み込む際の参考になれば幸いです!
もし「こういう機能も追加すると面白いよ!」といったアイデアがあれば、ぜひコメントで教えてください。