2025年4月25日金曜日

これでボクがいなくなってもボクっぽいツイートは生き続けるでしょう

これでボクがいなくなってもボクっぽいツイートは生き続けるでしょう

便利な時代になったもんだ

アカウント

現在は1日1回ツイートするようにしています

仕組み

ローカルの M2 Mac で動かしています
開発当初は Fine-Tuning方式 (LoRA) にしようかなと思ったのですが面倒なのと精度も微妙なのでやめました
たぶん ollama が動作すればいいのでスペックはそこまで必要ではないかもしれません

概要

仕組みは非常に簡単で過去の自分のツイートを LLM にコンテキストとして与えてそれを元に新しいツイートを生成してもらっているだけです

前処理も含めて流れの全体は以下です

  1. ツイートのアーカイブ (tweets.js) からランダムに500件抽出
  2. 500 件のうち 20 件をランダムに抽出し LLM のコンテキストとして与える
  3. ツイートを生成してもらう
  4. ツイートする

というかなり簡単な流れになっています

LLM モデル

特に理由はないですが日本語に対応していて可能な限り軽量な LLM を採用しています

これを ollama で起動して API をコールしてツイートを生成しています

  • ollama run openhermes

ツイートを抽出するスクリプト

さすがに全ツイートを食わせるのは無理なので先に何件か抽出します
とりあえず今は500件抽出しています
ここの抽出条件をもっと自分っぽいツイートだけに絞ったら文章も良くなるのかもしれないです

ここで生成されたツイートを LLM に食わせます

import json
import random

with open(
    "/path/to/twitter_archive/tweets.js",
    "r",
    encoding="utf-8",
) as f:
    raw = f.read()
    # 最初の JavaScript 部分を除去
    json_data = json.loads(raw.replace("window.YTD.tweets.part0 = ", ""))
    # "http://" または "https://" を含まないツイートだけに絞る
    filtered_tweets = [
        t["tweet"]["full_text"]
        for t in json_data
        if "http://" not in t["tweet"]["full_text"]
        and "https://" not in t["tweet"]["full_text"]
    ]
    tweets = random.sample(filtered_tweets, min(500, len(filtered_tweets)))

with open("tweets_500.json", "w", encoding="utf-8") as f:
    json.dump(tweets, f, ensure_ascii=False, indent=2)

LLM によるツイート生成

先ほど作成したツイートファイルを展開しプロンプトを作成します

ここのプロンプトを変えるだけで生成されるツイートが大きく変わります (これが世に言う「プロンプト最適化」)

以下が現状使っているプロンプトですが Openhermes だと「URL 含めないで」と言っても平気で含めてくるので別途正規表現で削除する処理を追加しています

Twitter の API は v2 でないとダメなのでご注意ください

import json
import os
import random
import re

import requests
import tweepy

# 過去ツイートの読み込み
with open("tweets_500.json", "r", encoding="utf-8") as f:
    past_tweets = json.load(f)

# プロンプト作成
# openhermes の場合は可能な限り質問を具体的にしないと自然な文章にならない
# chatgpt はだいぶ自然なツイートを作成してくれるから本当は OpenAI API を使いたい
examples = "\n".join(random.sample(past_tweets, 20))
prompt = f"""以下は過去のツイートです:

----ここから過去のツイート----
{examples}
----ここまで過去のツイート----

これらの文体や話題を参考にして、あなたらしい新しいツイートを1つ生成してください。
日本語でかつ文章の意味がわかるような自然なツイートにしてください。
ツイートに「https://」から始まるようなURLは絶対に含めないでください。
ツイートの長さは50文字以内にしてください。
ツイートを作成した理由などは説明しないください、ツイートのみ生成してください。
"""

# Ollama API にリクエストを送信
# stream: false にしないと一文字ずつ処理しないといけないので false にする
response = requests.post(
    "http://localhost:11434/api/generate",
    json={"model": "openhermes", "prompt": prompt, "stream": False},
)

# 出力を整える
tweet = response.json().get("response", "(ツイート生成失敗)").strip()
# URL が含まれる場合があるのでここで削除、ドメインが追加されたら随時対応
tweet = re.sub(r"(https?://\S+|pic\.twitter\.com/\S+|t\.co/\S+)", "", tweet).strip()
print("自動生成されたツイート:")
print(tweet)

# Twitter (X) にツイート投稿
# 各種キーとトークン(環境変数などから取得するのが安全)
# OAuth 2.0 User Context 用クレデンシャル(環境変数から取得)
bearer_token = os.getenv("TWITTER_BEARER_TOKEN")
consumer_key = os.getenv("TWITTER_CONSUMER_KEY")
consumer_secret = os.getenv("TWITTER_CONSUMER_SECRET")
access_token = os.getenv("TWITTER_ACCESS_TOKEN")
access_token_secret = os.getenv("TWITTER_ACCESS_SECRET")

if all(
    [bearer_token, consumer_key, consumer_secret, access_token, access_token_secret]
):
    client = tweepy.Client(
        bearer_token=bearer_token,
        consumer_key=consumer_key,
        consumer_secret=consumer_secret,
        access_token=access_token,
        access_token_secret=access_token_secret,
    )
    try:
        response = client.create_tweet(text=tweet)
        print("ツイートを投稿しました")
        print(f"ツイートURL: https://twitter.com/user/status/{response.data['id']}")
    except Exception as e:
        print(f"ツイート投稿に失敗しました: {e}")
else:
    print("Twitter API キーが設定されていません。環境変数を確認してください。")

定期実行

Mac なので launchctl で行っています

生成されるツイートについて

詳細はアカウントを直接見てください
結果としては50点くらいです

課題

以下で改善点を上げます

文章が自然ではない

  • モデルが悪いかも
    • 日本語特化なモデル (Elyza や LLM-jp など) に変更すると良くなるかも
    • 試した中で一番自然だったのは OpenAI API だったので本当はそれがベスト
  • プロンプトが悪いかも
    • URL はいらないと命令しても URL が含まれたりする
    • 説明はいらないと命令しても説明が含まれたりする
    • プロンプトを構造的にして質問したほうがいいのかもしれない
  • そもそもコンテキストとして渡しているツイート自体が自然な文章ではないのが原因の可能性はある
    • つぶやきは文章ではない
    • そこから意図や感情を読み取るのは LLM でも難しいのかも
    • 渡すツイート情報を変更するのはあり
    • そもそもツイートを渡すのではなく「こういうツイートをして」とかにしたほうがいいのかも
    • そうなると自分がどういうツイートをしているのかを分析する必要があるが
  • コンテキストとして渡すツイートの精査をする
    • ランダムではなく「それっぽい」ツイートだけを抽出するようにする
    • その抽出作業自体を LLM に任せればいいのかも

なぞのURLとハッシュタグを含んでしまう

  • URL は正規表現で別途対応
  • ハッシュタグは含まないように命令したほうがいいのかも

リファクタリング

  • しません

定期実行が止まったらわからない

  • 通知機能をつけてもいいかも
  • あとはローカル実行ではなく適当なクラウドサーバに移行してもいいかも
  • Twitter の API が v2 から v3 になったり非互換になったりトークンが期限切れになった止まる

変なツイートしないか心配

  • Fワード系
  • 個人情報系
  • 急に誰かにメンションするとか

その他思うこと

LLM について思うことをつらつら書いていきます
ちなみに過去に思ったことはこちらです

Fine-tuning やら RAG やら MCP やらあるが正直オーバーエンジニアリングな気はする

  • LLM 自体がかなり賢くなっている
  • トークンの制限さえなくなれば「プロンプト最適化」が一強になるはず

AI を使うために AI を使う開発をするのは今後必然になる気がするが本末転倒な気がしなくもない

  • 例えば dspy や LangChain、MCP Server といったツールを使って LLM を操作する技術は今後必須になってくる
  • しかし結構大変なイメージ
  • 特に Fine-tuning や RAG や MCP は作るのも大変 (それすら AI 先生に助けてもらえるが)
  • やりたいことは AI のための開発ではなくあくまでもプロダクトの開発
  • そしてせっかく作ったものが必ずしも開発の役に立つ、手助けしてくれるレベルになるとは限らない

完全に AI のみで済むような世界観は実はまだまだ先の未来なのかもしれない

  • とは言え開発に LLM がないという状況は今後考えにくい
  • LLM はあくまでも「お手伝いロボット」で生成してくれたコードなどは結局自分たちでテスト、検証しなければならない
  • 本当はそこまで全部やってほしい
  • 極論「新機能でこういう機能がほしいから実装しておいて」とか「既存のコードでここにバグがあるから直しておいて」と言えば全部勝手にやってくるのが最高だがその未来は遠そう
    • Agent など実現するためのツールは揃っているがそれらを使って実現するスキルが自分には足りない
    • 結局「生成してもらう」->「コピペ」->「テスト」のループを繰り返すのが一番効率がいいのかもしれない

エージェントについて

  • 本当にいるのだろうかと思っている
  • もちろん自分の分身レベルでエージェントが勝手に動いてくれるならほしい
  • でもそこまでのエージェントを作るのはかなりの労力と技術がいると思っている
  • 現状のエージェントはいわゆる「作業の自動化」でそれは別にエージェントを使わなくても問題ない
  • これまで通りやっていた自動化を LLM を使って実現すればいいだけの話になる
  • つまりエージェントレベルではいらない (それをエージェントと呼ぶのであれば話は別だが)
  • 強化学習でいうところのエージェント+LLM の世界観
  • エージェントには意思があると思う

最後に

AI 先生は中身がブラックボックスすぎて何をしているのかわからなさすぎて怖いです
だがそれがいい

0 件のコメント:

コメントを投稿