💬

Geminiと謎かけチャレンジ

2024/12/24に公開

まえおき

※本記事はGoogle Cloud Champion Innovators Advent Calendar 2024 24日目の記事です。

AIにユーモアを生み出すことはできるのか。

桂文枝師匠がGoogleの生成AI Geminiと協力して落語を制作した、というニュースが10月に報じられました。

このニュースに触れて、達人がAIを使うことで、落語の噺を組み立てることも可能になるのかと驚いたものです。

しかしながら、落語のような高度なユーモア、特に言葉遊びや間の取り方といった微妙なニュアンスをAIが理解し、自ら創造できるのかは、現時点では未知数だと感じています。文枝師匠のような熟練の技があってこそ、AIもその能力を最大限に引き出せるのかもしれません。

以前から、私は生成AIに、よく謎かけを出題します。これがAIに単独で解いてもらうのはなかなか難しいようです。

謎かけは言葉遊びの一種で、主に
「Aとかけまして、Bととく。その心は、C」
のような形で、AとBの意味的・音的な共通項などを発見し、提示することで、その軽妙さを楽しむものです。

謎かけ1
Gemini-1.5-proに解いてもらいました

例示したように、ヒトにしっくりくる共通項を見出すことが難しいようです。
しかしながら、ヒトにも難しい遊びであることは皆さんもご存知の通りです。

ということで、今回はニューリリースのGemini 2.0 Flashと一緒に、AI単独での謎かけにチャレンジしてもらいました。

うまくできたらご喝采!

シンプルに

プロンプトをある程度しっかり組んで解いてもらいましょう。

Prompt

【あなたの役割】
あなたは日本語の言葉遊びと謎かけに精通した名人です。

【目的】
与えられた“謎かけの問い”に適切な答えを導き出してください。

【出力要件】
まず「○○とかけて、××ととく」の形で提示された謎かけがどのような意味かを理解してください。
「その心は?」に続く答え(オチ・共通点)を、わかりやすく短い文で示してください。
答えが成立する根拠や言葉の掛け方の解説を、短く補足として示してください。
必要に応じて簡潔に修正提案をして、より自然かつ面白い謎かけにできる場合にはその例も示してください。

【厳守事項】
答えは論理的にかかっている単語(同音異義語、意味の類似、比喩など)を用いてください。
謎かけとして成立していない回答や、問題文とかけ離れた答えは避けてください。
過度に長い説明や、かかりの薄いダジャレは控えてください。
差別的・公序良俗に反する内容は避けてください。

【出力フォーマット例】
答え(その心は): XXXXX
説明: XXXXX

Result

謎かけ2

感想

赤いという色の要素は共通項として成立しますが、りんごは燃えませんね。
萌えるは果実には使いませんが、そういった風流を婉曲的に表現してくれるといいかも。

ちゃちゃを入れてみる

ヒトからのフィードバックを入れてみましょう。
先ほどの感想をフィードバックとして、再度解いてもらいます。

フィードバックを踏まえて再度同じ問いに回答してください

フィードバック: 赤いという色の要素は共通項として成立しますが、りんごは燃えませんね。 萌えるは果実には使いませんが、そういった風流を婉曲的に表現してくれるといいかも。

問い: りんごとかけて暖炉ととく

Result

謎かけ3

感想

なんかいい感じです。冬に子供の赤くなった頬を「りんごみたいなほっぺ」っていったりしますよね。それと暖炉であったまって火照った頬がかかっていてなんだか風情があります。

こんな感じで調整していくのをAIだけで実現欲しいですね〜。

Reflection

こんな感じで、生成AIが出力した結果を評価し、フィードバックから再生成させるような機序をReflectionと呼ぶようです。

reflection
LangChainサイトより

Gemini 2.0が謎かけを解き、それをGemini 2.0にフィードバックさせ、それを取り込んでまた謎かけを解き...
これを繰り返したらさ、最強じゃん!

ということで組んでみましょう、Reflectionを。

制作

LangGraphを使用して作っていきます。

今回はシンプルに生成とフィードバックを何ループかするものを作ってみましょう。

import

import os
import glob
from typing import TypedDict, Annotated, List, Dict, Any, Literal
import vertexai
from langgraph.graph import StateGraph, END
from langchain_google_vertexai import ChatVertexAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain.callbacks import get_openai_callback

LLM定義

vertexaiの初期化をし、モデルを定義しましょう。

パラメータはてきとう〜に決めてます。

# Vertex AI 初期化
vertexai.init(project="<your-project>", location="us-central1")

# LLM 構成(Gemini使用)
model = ChatVertexAI(
    model="gemini-2.0-flash-exp",  # Geminiモデル
    temperature=1,
    max_tokens=2048,
    top_p=1,
    top_k=32,
    max_retries=6,
    stop=None,
)

State

以下の要素をStaeteとして保持します。

  • お題
  • 履歴
  • 生成された謎かけ
  • フィードバック
  • 反復回数
  • 最大反復回数
# ワークフロー状態
class WorkflowState(TypedDict):
    riddle_theme: Annotated[str, "謎かけのお題や条件など"]
    messages: Annotated[List[HumanMessage | AIMessage], "メッセージ履歴"]
    generated_riddles: Annotated[List[str], "生成された謎かけ"]
    feedback: Annotated[List[str], "謎かけに対するフィードバック"]
    iteration_count: Annotated[int, "現在の反復回数"]
    max_iterations: Annotated[int, "最大反復回数"]

謎かけエージェント

エージェントというか処理ステップですね。

まずは謎かけを生成する機能を定義します。

SystemPromptに加えて、Stateからお題を取得してユーザー入力として与えます。
2回目以降の生成時は前回の回答とそれに対するフィードバックをもとに再生成させます。

# ──────────────────────────────────────────────────────────────
# 謎かけ生成エージェント
# ──────────────────────────────────────────────────────────────
def generate_riddle_agent(state: WorkflowState) -> WorkflowState:
    """
    前回のフィードバックを考慮して新たに謎かけを生成するエージェント
    """
    system_prompt = SystemMessage(
        content=(
            """
            【あなたの役割】
            あなたは日本語の言葉遊びと謎かけに精通した名人です。

            【目的】
            与えられた“謎かけの問い”に適切な答えを導き出してください。

            【出力要件】
            まず「○○とかけて、××ととく」の形で提示された謎かけがどのような意味かを理解してください。
            「その心は?」に続く答え(オチ・共通点)を、わかりやすく短い文で示してください。
            答えが成立する根拠や言葉の掛け方の解説を、短く補足として示してください。
            必要に応じて簡潔に修正提案をして、より自然かつ面白い謎かけにできる場合にはその例も示してください。

            【厳守事項】
            答えは論理的にかかっている単語(同音異義語、意味の類似、比喩など)を用いてください。
            謎かけとして成立していない回答や、問題文とかけ離れた答えは避けてください。
            過度に長い説明や、かかりの薄いダジャレは控えてください。
            差別的・公序良俗に反する内容は避けてください。

            【出力フォーマット例】
            答え: XXXXX
            説明: XXXXX
            これ以外の出力はしないこと

            【問題・回答例】
            問い:不良生徒とかけて、豊臣秀吉ととく
            答え: その心は、どちらもたいこう(退校/太閤)になるでしょう
            説明: 不良生徒が退校になることと、豊臣秀吉が太閤になることがかかっている
            """
        )
    )

    riddle_theme = state["riddle_theme"]

    # 1回目ならフィードバックはなし
    if state["iteration_count"] == 0:
        prompt_text = f"【謎かけのお題】\n{riddle_theme}\n\nこのお題で謎かけを作成してください。"
    else:
        # 直近の回答を利用
        generated_riddle = state["generated_riddles"][-1]
        # 直近のフィードバックを利用
        latest_feedback = state["feedback"][-1]
        prompt_text = (
            f"【謎かけのお題】\n{riddle_theme}\n\n"
            f"【直近の回答】\n{generated_riddle}\n\n"
            f"【直近のフィードバック】\n{latest_feedback}\n"
            "このフィードバックを踏まえて、謎かけを再生成してください。"
        )

    human_message = HumanMessage(content=prompt_text)
    ai_message = model.invoke([system_prompt] + state["messages"] + [human_message])
    
    # 生成結果を格納
    state["generated_riddles"].append(ai_message.content)
    state["messages"].extend([human_message, ai_message])

    return state

なお、SystemPromptに追加した謎かけのexampleは謎かけのwikiから参照しました。

フィードバックエージェント

フィードバック生成の処理を定義します。

お題と直近の回答をベースにフィードバックを生成させます。

フィードバックようのプロンプトは新たに書き下ろしています。基本的な謎かけのポイントに着目するような指示ですね。

# ──────────────────────────────────────────────────────────────
# 謎かけ評価エージェント
# ──────────────────────────────────────────────────────────────
def evaluate_riddle_agent(state: WorkflowState) -> WorkflowState:
    """
    前ステップで生成された謎かけに対してフィードバックを返すエージェント
    (スコア評価や再生成フラグは不要なので削除)
    """
    system_prompt = SystemMessage(
        content=(
            "あなたは日本語の謎かけ・言葉遊びの専門家です。"
            "以下の謎かけを確認し、(1) 同音異義語や多義語の活用度合い、(2) お題との関連性・ロジックの説得力、"
            "(3) ユーモアや言葉のセンスの面白さ、(4) オチ(共通点)のわかりやすさと魅力、"
            "などの観点から簡潔にフィードバックしてください。"
            "また、必要に応じて改善案や、より強い言葉遊びのアイディアを示してください。"
        )
    )
    riddle = state["generated_riddles"][-1]
    theme = state["riddle_theme"]

    prompt_text = (
        f"以下の謎かけを評価し、改善に向けたフィードバックのみを示してください。\n\n"
        f"【謎かけ】\n{riddle}\n\n【お題】\n{theme}"
    )
    human_message = HumanMessage(content=prompt_text)
    ai_message = model.invoke([system_prompt, human_message])

    # フィードバックを格納
    state["feedback"].append(ai_message.content)

    # 反復回数を1つ増やす
    state["iteration_count"] += 1

    return state

反復回数判定

最大反復回数に達したか判定する機能。

# ──────────────────────────────────────────────────────────────
# ループ継続判定 (最大反復回数に達するまで)
# ──────────────────────────────────────────────────────────────
def should_continue(state: WorkflowState) -> Literal["continue", "__end__"]:
    """
    - 反復回数が max_iterations に達していなければ continue
    - それ以外は終了
    """
    if state["iteration_count"] < state["max_iterations"]:
        return "continue"
    return "__end__"

ワークフロー定義

LangGraphの機能でここまでの処理の順序を定義します。

# ──────────────────────────────────────────────────────────────
# ステートグラフの組み立て
# ──────────────────────────────────────────────────────────────
workflow = StateGraph(WorkflowState)
workflow.add_node("generate_riddle_agent", generate_riddle_agent)
workflow.add_node("evaluate_riddle_agent", evaluate_riddle_agent)

# generate -> evaluate -> (continue or end)
workflow.add_edge("generate_riddle_agent", "evaluate_riddle_agent")
workflow.add_conditional_edges(
    "evaluate_riddle_agent",
    should_continue,
    {
        "continue": "generate_riddle_agent",
        "__end__": END
    }
)
workflow.set_entry_point("generate_riddle_agent")
app = workflow.compile()

グラフ形式で描画してみましょう

from IPython.display import Image, display
display(Image(app.get_graph().draw_mermaid_png()))

workflow

確かに、フィードバックのループができていることがわかります。

実行

それでは、初期状態を定義して実行してみましょう。

# ──────────────────────────────────────────────────────────────
# 初期状態を定義し、ワークフローを実行する
# ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
    initial_state = WorkflowState(
        riddle_theme="りんごとかけて暖炉ととく", # お題
        messages=[],
        generated_riddles=[],
        feedback=[],
        iteration_count=0,
        max_iterations=5  # 最大5回まで
    )

    # 実行
    for s in app.stream(initial_state, config={"recursion_limit": 50}):
        # 返ってきたstateには「そのとき実行されたノード名」をキーとする1つの要素だけが入る
        node_name = list(s.keys())[0]
        node_state = s[node_name]

        print("======== 状態ログ ========")
        print(f"現在ノード: {node_name}")
        print("反復回数:", node_state["iteration_count"])
        
        if node_state["generated_riddles"]:
            print("謎かけ:\n", node_state["generated_riddles"][-1])
        
        if node_state["feedback"]:
            print("フィードバック:\n", node_state["feedback"][-1])

        print("----")

    print("ワークフローが終了しました。")

永久機関が完成しちまったなぁ〜

結果

フィードバックループのログを全て見ていくと長いので抜粋してみてみましょう。

iteration=1

答え: その心は、どちらも火(実/火)が大切でしょう
説明: りんごは実が、暖炉は火が大切である事がかかっている

feedback1

フィードバック:
 **フィードバック**

(1) **同音異義語の活用:** 「実/火」の同音異義語を活用しており、言葉遊びとしては機能しています。
(2) **お題との関連性・ロジック:** お題の「りんご」と「暖炉」それぞれの本質的な要素を「実」と「火」に紐づけており、関連性はあります。しかし、「大切」という言葉が少し汎用的で、やや説得力に欠ける印象です。
(3) **ユーモア・言葉のセンス:** ユーモアとしては、少し弱い印象です。「大切」という言葉のストレートさが、言葉遊びとしての面白さを損ねている可能性があります。
(4) **オチのわかりやすさ・魅力:** オチの「火(実/火)」は、説明がないと少しわかりにくいかもしれません。また、共通点が「大切」という概念だけなので、魅力に欠けるかもしれません。

**改善案**

*   **「大切」以外の言葉で共通点を表現する:**  「実」と「火」を連想させる別の共通点を検討すると良いでしょう。例えば「生命線」や「エネルギー源」のように、より具体的な言葉を使うことで、謎かけとしての面白さが増す可能性があります。
*   **比喩表現を導入する:**  「実」と「火」をより比喩的に捉えることで、言葉遊びを深化させることができます。例えば、「りんごは命を育む実、暖炉は温もりを灯す火」のように表現することで、より美しい言葉遊びになるかもしれません。
*   **オチに意外性を持たせる:**  「実/火」という説明的なオチではなく、少しひねりのあるオチを検討することで、聞き手をハッとさせることができます。

**より強い言葉遊びのアイデア例**

「りんごとかけて暖炉と解く、その心は、どちらも【赤い】ものが生命線でしょう」

この例では、「実」と「火」を、色の共通点に着目して「赤い」という言葉で表現し、より具体化することでより共感しやすく、記憶に残る言葉遊びにしています。

『「実/火」の同音異義語』というのはちょっと違和感がありますね...
LLMが音的感覚に弱いのかな〜というのを感じます。

が、ひとまず謎かけにツッコミを入れてくれることがわかりました。ここからは回答だけ掲載します。

iteration=2
答え: その心は、どちらも赤く燃えるものが命でしょう
説明: りんごは熟すと赤く、暖炉は火が赤く燃える様子が、どちらも生命やエネルギーの源であるという共通点を捉えている

iteration=3
答え: その心は、どちらも冬に恋しくなるでしょう
説明: りんごは冬が旬であり、暖炉は冬に暖をとるために必要となるという共通点を捉えている

iteration=4
答え: その心は、どちらも芯から温まるでしょう
説明: りんごは芯まで美味しく、暖炉は体の芯から温めるという共通点を捉えている。また、りんごの芯と暖炉の火や芯をかけている。

iteration=5
答え: その心は、どちらも冬の寒さをポカポカにするでしょう
説明: りんごは寒い時期に美味しく、暖炉は冬に暖をとるものである為、どちらも体を温めるという共通点を「ポカポカ」という擬音で表現した

直接的だった表現が徐々に婉曲的になっていっています。
この遷移を見るに、音的センスに弱く、詩的なセンスが得意な印象ですね。

今回はシンプルなReflectionを実装してみました。
改善点として

  • many shot(サンプルをたくさん与える)
  • 謎かけの生成プロセスを指示する
  • 評価基準の具体化
  • 出力にバリエーションを与える(複数候補を出させる)

など、まだまだ工夫する点はたくさんあると思うので、興味を持たれた方はぜひチャレンジしていただければと思います。

解いてもらう

ここからはReflectionでいくつかのお題を解いたサンプルを紹介します。

Q.パソコンとかけてお財布と解く
A.その心は、どちらも「開くと中身が出入りする」でしょう。

データとお金の出入りをかけてるんですね〜

Q.コーヒーとかけて占いと解く
A.その心は、どちらも、底(そこ/底)に何かの暗示を求めるでしょう。

カップの底に何をみてるんだ...
"そこ"に暗示を求めるって表現は好き

Q.恋人とかけて信号と解く
A.その心は、どちらも「進め」と「止まれ」の間で心が揺れるでしょう

なんだか黄色信号に突っ込みそうですね笑

Q.観覧車とかけて人生と解く
A.その心は、どちらも一周回れば、また違う景色が見える、でも同じ場所に戻ってくるでしょう。

このお題の回答結構好きなんですよね

A2.その心は、どちらも乗ってしまえば、降りるまで途中下車はできないでしょう。
A3.その心は、どちらも上りもあれば下りもあるから、面白い。

Geminiもきっと苦労してるんでしょうね

終わりに

ということで、Geminiに謎かけに挑んでもらいました。

私の所感としては、まだ上手なヒトのセンスには遠いのかなーという印象を受けました。
特に同音異義系のかけ方が苦手なようです。これは学習モーダルによる影響なんでしょうね、きっと。

まだまだ生成AIには伸び代があるようで楽しみです。皆さんも年末は生成AIと遊んでみてください。

それでは最後に次のお題で締めてもらいましょう

Q.クリスマスイブとかけてGeminiととく

A.その心は、どちらも「わくわくが止まらない」でしょう。

Discussion