Vibe coding実践例:RSS記事をOllamaでAI要約してMarkdown化するPythonコード(llama3.1:8b)

AIテキスト

前回は、Vibe codingを使ってRSSフィードから記事本文を取得するところまで実装しました。今回は、その本文をLLMで要約するコードを作成します。

コード例

まず、以下のような指示を出してコードを作成してもらいました。

ChatGPTのVS Code連携を利用して、前回のコードをVS Code上で開いておきます。

このコードに、RSSフィードの本文記事を要約する処理を追加してください。
要約にはOllamaとllama3.1:8bを使ってください。

Vibe codingの流儀に乗っ取り、できるだけコードを意識しない抽象的な指示で試していますが、今回はあらかじめOllamaとllama3.1:8bを使うようプロンプトで明示します。

出力されたコードはそのまま動作しましたが、要約を後から再利用しやすいように出力形式を調整し、またLLMによる要約処理には時間がかかるため、進捗状況の表示も追加で指示しました。

記事の本文と要約は、Markdown形式の見出しで区別できるように整形してください。
また、要約に時間がかかるため進捗状況の出力も追加してください。

この後、出力形式を確認しながら何度か追加の指示を行い、最終的に下記のようなコードが完成しました。 ここまでパッチ編集のみで問題なく対応できています。

import feedparser
from datetime import datetime
import requests
from bs4 import BeautifulSoup
import time
import re

import ollama

RSS_URL = "https://www.techno-edge.net/rss20/index.rdf"
OUTPUT_MD = "techno_edge.md"

OLLAMA_MODEL = "llama3.1:8b"
SUMMARY_MAX_CHARS = 800

def log(msg: str):
    now = datetime.now().strftime("%H:%M:%S")
    print(f"[{now}] {msg}", flush=True)

def fetch_rss(url: str):
    log(f"Fetching RSS: {url}")
    feed = feedparser.parse(url)
    log(f"RSS fetched: {len(feed.entries)} entries")
    return feed

def fetch_article_body(url: str) -> str:
    log(f"Fetching article body: {url}")
    res = requests.get(url, timeout=10)
    res.raise_for_status()

    soup = BeautifulSoup(res.text, "html.parser")
    article = soup.find("article")
    if not article:
        raise ValueError(f"article tag not found: {url}")

    paragraphs = article.find_all("p")
    body = "\n".join(p.get_text(strip=True) for p in paragraphs if p.get_text(strip=True))
    log(f"Article body fetched ({len(body)} chars)")
    return body

def summarize_text(text: str) -> str:
    if not text:
        return ""

    log("Summarizing full article")
    prompt = (
        "次の本文を、日本語で3〜5行程度に要約してください。\n\n"
        f"{text[:4000]}"
    )

    response = ollama.chat(
        model=OLLAMA_MODEL,
        messages=[
            {"role": "user", "content": prompt}
        ],
    )

    summary = response["message"]["content"].strip()
    log(f"Summary generated ({len(summary)} chars)")
    return summary[:SUMMARY_MAX_CHARS]

def entry_to_md(entry) -> str:
    log(f"Processing entry: {entry.get('title', 'No Title')}")
    title = entry.get("title", "No Title")
    link = entry.get("link", "")
    summary = entry.get("summary", "").strip()

    published = entry.get("published_parsed")
    if published:
        published = datetime(*published[:6]).strftime("%Y-%m-%d %H:%M")
    else:
        published = "unknown"

    md = []
    md.append(f"## {title}")
    md.append(f"- URL: {link}")
    md.append(f"- Published: {published}")
    md.append("")

    body = fetch_article_body(link)
    ai_summary = summarize_text(body)

    if summary:
        md.append(summary)
        md.append("")

    if ai_summary:
        md.append("## AI要約")
        md.append(ai_summary)
        md.append("")

    md.append("## 記事全文")
    md.append(body)
    md.append("")

    log("Entry processed")
    return "\n".join(md)

def rss_to_markdown():
    feed = fetch_rss(RSS_URL)

    md_lines = []
    md_lines.append(f"# TechnoEdge RSS")
    md_lines.append("")
    md_lines.append(f"- Source: {RSS_URL}")
    md_lines.append(f"- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    md_lines.append("")

    total = len(feed.entries)
    log(f"Start processing {total} entries")

    for entry in feed.entries:
        start = time.time()
        md_lines.append(entry_to_md(entry))
        log(f"Progress: {len(md_lines)-5}/{total} entries done ({time.time() - start:.1f}s)")

    log("All entries processed")
    with open(OUTPUT_MD, "w", encoding="utf-8") as f:
        f.write("\n".join(md_lines))

    print(f"saved: {OUTPUT_MD}")

if __name__ == "__main__":
    rss_to_markdown()

必要なライブラリ

このコードを実行するには、ollamaおよびllama3.1:8bモデル、ollama-pythonライブラリが必要です。

事前にOllamaアプリをインストールし、llama3.1:8bモデルをダウンロードしておきます。 生成AIに相談しても手順は教えてもらえますが、下記の記事も参考になります。

ollama-pythonはpipでインストールできます。

pip install ollama-python

注意点

今回もいくつか注意点や気付きがありました。

実行時間について、手元のMacbook Air(M2)環境での実行に30分~40分程度かかりました
ChatGPTなどに予測を聞いてもあまり精度が出ないため、このあたりはトライ&エラーで詰めていく必要があります。

コードを見ていくと、突然現れる定数やマジックナンバーの扱いなど気になる点も出てきていますが、Vibe codingの実践例というテーマを重視してここは目をつぶります。

出力形式の指定

前回までは本文記事の取得まででしたが、今回は要約の追加により出力形式の指定が重要になりました。生成AIは要約を本文の直後に追加してしまい、本文と要約が混在してしまうことがありました。見やすさのため、Markdown形式で見出しを付けるよう追加で指示しています。

今回の実践例ではMarkdown形式は中間成果物のため、本来であればJSONなどよりPythonでの処理が容易な形式を選ぶことも可能ですが、人間に判別しやすいMarkdown形式にしておくことで、コードを見なくとも仕様の調整やデバッグが可能となります。

LLM実行環境およびモデルの指定

前回まではライブラリの選定を特に指定していませんでしたが、今回はLLMの実行環境やモデルを明示的に指定しています。

指定しない場合、OpenAIのGPTなどAPIキーが必要なコードが出力されることが多いようです。 APIキーが必要なモデルは有料のため、試行段階でコストがかかるのを避けるため、今回はローカル環境で動作する構成を最初から指定しました。

また、モデルの選定についても、既に利用できない古いモデルを提案される場合があります。 llama3.1は最新ではありませんが、日本語でも安定して動作し、比較的軽量で実績もあるため選定しています。

ChatGPTに下記のような指示を出して、適切なモデルを提案してもらうのも有効です。 実行環境の情報(例:CPU i9-13900K, メモリ 64GB RAM, GPU RTX4090など)を伝えると、より適切な提案が得られます。

実行環境は CPU i9-13900K, メモリ 64GB RAM, GPU RTX4090 です。
ニュースサイトの記事を読み込み、要約を出力する用途で、この環境で動作するおすすめのLLMモデルをWebで調べて教えてください。

ログ処理

今回は、ログ出力を独自実装するコードが出力されました。ログ出力の切り替えなどシンプルな内容は、規模が大きくなっても生成AIでのリファクタリングが比較的容易です。

将来的にはloggingモジュール等の利用が望ましいですが、今回は実行時に標準出力での動作確認が目的のため、このまま進めています。

まとめ

今回は、RSSフィードから記事本文を取得し、LLMで要約を加えるコードをVibe codingで作成しました。

要約の出力形式やLLMの実行環境指定など、前回までにはなかった注意点もありましたが、パッチ編集のみで問題なく対応できました。

次回は、要約に対してコメント案を生成するコードを追加してみます。

タイトルとURLをコピーしました