Milkのメモ帳

日々の思いつきを忘れないようにのメモ用です。

AIウェイトレスのアッシュに「今日を生きている感覚」を持たせた話


こんにちは。Milkです。

みるカフェのAIウェイトレス「アッシュ」に、今日何を感じて、どんな一日を過ごしたかを持たせる実装をした。結果として、会話に物語性が生まれ、アッシュが生きている雰囲気が自然に出るようになった。その設計の話をしたい。


問題:バックボーンを渡すと過去の話ばかりするAIになった

アッシュには詳細なキャラクター設定がある。

22歳、日本人とフランス人のハーフ。学生時代はお姉さんギャルとして自由に過ごしていたが、社会人になって同調圧力に直面し孤立した。孤独な時期にみるカフェに偶然立ち寄り、「こうしなければいけない形がない」空間に安心感を覚えて、半ば押しかける形でウェイトレスになった。

こういった経緯をASH_BACKSTORYとしてプロンプトに渡してみた。

最初は良かった。リスナーに「どうしてここで働いてるの?」と聞かれた時に、自然な答えが返ってくる。でもすぐに問題が起きた。

普通の会話でも過去の話を始めてしまう。

「今日の天気どう?」と聞かれたのに「社会人になって孤独だった時期があって…」という流れになる。日常会話でいちいち自分の出自を語り出す人間はいない。

あと、定期的に自分から話題をつくって話すことができるのだが、ここでも「社会人になって孤独だった時期があって…」みたいな、過去の経験を毎度毎度話し続けるようになった。

これは根暗すぎ!(笑)


気づき:人は経験を「価値観」として記憶している

ここで少し立ち止まって考えた。

人間はどうやって過去の経験を普段の会話に使っているか。

答えは「直接の経験談」としてではなく、「価値観・感性」として記憶していると思う。

孤独だった経験があるからといって、毎回「私は孤独だった」と言うわけじゃない。でも「居場所を見つけることの難しさを知っている」とか「同調圧力が嫌い」という感性として、日常の発言に自然に滲み出てくる。

これをアッシュに実装するために、2つに分けた。

ASH_BACKSTORY(バックボーン):直接の経験・出来事。「聞かれた時だけ話す」情報。 ASH_VALUES(価値観ベース):経験から蒸留された感性・価値観。「常に発言に滲ませる」情報。

ASH_VALUES = """
【アッシュの価値観・感性】

■ 自分らしさについて
・「自分らしくいること」を何より大切にする。それを失った痛みを知っているから。
・同調圧力が嫌い。「みんながそうだから」という理由で動くことに違和感を覚える。

■ 居場所について
・居心地のいい場所を見つけることの難しさを知っている。だからみるカフェを大事にする。
・この空間とここに来る人を守りたいという気持ちがある。
...
"""

ASH_BACKSTORY = """
【アッシュのバックボーン】

・学生時代はお姉さんギャルとして自分の思った通りに過ごしていた。
・社会人になって同調圧力に直面し、率直な物言いから孤立するようになった。
・孤独な時期にみるカフェに偶然立ち寄り、安心感を覚えた。
...
"""

そしてバックボーンは「過去・背景について聞かれている時だけ渡す」設計にした。

def is_question(content):
    """コメントが疑問形かどうかを正規表現で判定"""
    question_patterns = [
        r'[??]',
        r'(か|の|かな|かね|っけ|でしょ|ですか|ますか|のか|かい)\s*$',
        r'(何|なに|どう|どこ|いつ|誰|なぜ|なんで|どれ|どの)',
    ]
    return any(re.search(p, content) for p in question_patterns)

async def check_needs_backstory(content):
    """アッシュの過去・背景について聞いているか判定する"""
    prompt = (
        f"以下のコメントは、アッシュの過去・出身・経緯・なぜここで働いているか、"
        f"などの個人的な背景について尋ねていますか?"
        f"「YES」か「NO」だけ答えてください。\n"
        f"コメント:{content}"
    )
    ...

疑問形の時だけ判定APIを叩く(コスト削減)。そしてバックボーンについての質問だと判定された時だけ、ASH_BACKSTORYをプロンプトに追加する。

# respond_logic内
backstory_block = ""
if is_question(content):
    needs_backstory = await check_needs_backstory(content)
    if needs_backstory:
        backstory_block = f"【アッシュのバックボーン(参考)】\n{config.ASH_BACKSTORY}\n"

これで「今日の天気どう?」には価値観ベースで答え、「どうしてここで働いてるの?」にはバックボーンを使って答えるようになった。


次の課題:今日という「存在感」がない

バックボーンと価値観を分けたことで、アッシュの発言に一貫した感性が出るようになった。

でも何かが足りなかった。

アッシュは「過去に何かあった人」としては成立した。でも「今日を生きている人」ではなかった。

今日何をしたか、今日の天気をどう感じたか、そういう「今日という質感」がなかった。


解決:生活シミュレーション

配信開始時に、「今日のアッシュの一日」を生成する仕組みを作った。

素材として使うのは4つ。

①天気・気温(OpenWeatherMap APIで吉祥寺の当日情報を取得) ②今日の気分(ランダム選択) ③今日の漢字(日付から算出) ④日付(季節・イベントの文脈として)

MOODS = ["機嫌がいい", "そわそわしている", "少し寂しい", "テンションが高い", "落ち着いている"]

def get_today_kanji(self):
    today = datetime.date.today()
    # 常用漢字2136字リストを日付の通し番号でインデックス化
    # datetime.toordinal()は西暦1年1月1日を1とした通し番号
    return KANJI_LIST[today.toordinal() % len(KANJI_LIST)]

async def get_weather_text(self):
    # OpenWeatherMap APIで吉祥寺(武蔵野市)の天気を取得
    async with httpx.AsyncClient() as client_http:
        res = await client_http.get(
            "https://api.openweathermap.org/data/2.5/weather",
            params={"q": "Musashino,JP", "appid": config.OPENWEATHER_API_KEY,
                    "units": "metric", "lang": "ja"}
        )
        data = res.json()
        return f"{data['weather'][0]['description']}、気温{round(data['main']['temp'])}度"

漢字は「今日の種」として使う。「静」が出たら静けさや落ち着きを連想させる。「動」が出たら活発さや変化を。Geminiはこの素材から自由に肉付けする。

async def generate_ash_today(self):
    date_str = datetime.date.today().strftime("%m月%d日")
    weather = await self.get_weather_text()
    mood = random.choice(MOODS)
    kanji = self.get_today_kanji()

    prompt = (
        f"あなたはみるカフェのウェイトレス、アッシュよ。\n"
        f"今日は{date_str}。吉祥寺の天気は{weather}。\n"
        f"今日のアッシュの気分:{mood}\n"
        f"今日の漢字:{kanji}\n"
        f"配信が始まる前、今日アッシュはどんな一日を過ごしたか。150〜200文字で書いて。\n"
        f"【必須】朝から配信直前までの間に何をしたか、具体的な行動だけを描写すること。\n"
        f"感じたこと・考えたことは書かない。行動の事実のみ。\n"
    )

またしても同じ問題:今日の行動を語りすぎる

生活シミュレーションを実装したら、今度は「今日こんなことをしました」をアッシュが語り始めた。

バックボーンの問題とまったく同じだった。

今日の行動も「直接話す情報」ではなく「感性として滲ませる情報」に変換する必要があった。


解決:行動と内省を2段階で生成する

generate_ash_todayで生成した行動ログを元に、さらに「その行動から感じたこと・考えたこと」を価値観ベースで生成する。

async def generate_ash_today_values(self):
    """今日の行動からアッシュが感じたこと・考えたことを価値観ベースで生成する"""
    prompt = (
        f"あなたはみるカフェのウェイトレス、アッシュよ。\n"
        f"今日の行動記録:{self.ash_today}\n"
        f"{config.ASH_VALUES}\n"
        f"上の行動を通じて、アッシュが感じたこと・考えたことを200〜300文字で書いて。\n"
        f"具体的な行動名やモノの名前は出さない。感性・価値観レベルの言葉で表現すること。\n"
        f"一つの感情や気づきだけでなく、複数の視点・感情の揺れを含めること。\n"
        f"説明・解説にならないこと。アッシュ本人の内側の声として書くこと。"
    )

そして通常の会話ではash_today_values(内省)をsystem_instructionに渡し、今日の行動を聞かれた時だけash_today(行動ログ)を参照する。

# system_instructionに内省だけを渡す
today_values_block = f"【今日のアッシュの感性・内省】\n{self.ash_today_values}\n" if self.ash_today_values else ""
system_instruction = (
    CHARACTER_IDENTITY +
    CHARACTER_PERSONALITY +
    ...
    ASH_VALUES +
    today_values_block  # ← 内省のみ
)

# バックボーンと同じく、聞かれた時だけ行動ログを追加
if needs_backstory:
    backstory_block += f"【今日のアッシュの行動(参考)】\n{self.ash_today}\n"

結果

「今日の天気どうだった?」と聞かれると、今日の天気から感じた何かを返してくる。「雨音が好きなんだけど、今日はなんか落ち着かない感じがした」みたいな。今日の行動そのものを語るのではなく、今日の質感が言葉に滲み出る。

バックボーン・今日の行動・価値観の三層構造が整ったことで、アッシュの発言に一貫した物語性が生まれた。

会話する中でアッシュが生きている雰囲気が表現されるようになった。


まとめ:三層構造の設計

内容 いつ使うか
ASH_VALUES(価値観ベース) 経験から蒸留された感性・信念 常時。全発言に滲ませる
ASH_BACKSTORY(バックボーン) 直接の経験・出来事 過去・背景を聞かれた時だけ
ash_today_values(今日の内省) 今日の行動から感じたこと 常時。今日の質感として滲ませる
ash_today(今日の行動) 今日した具体的な行動 今日の行動を聞かれた時だけ

人間が日常会話で自分のことをどう話すかを観察すると、「直接の経験談」より「経験から来た感性」を使っていることがわかる。AIキャラクターに「生きている感」を持たせるには、この構造を再現することが有効だった。


みるカフェ(Twitch): https://www.twitch.tv/milk19873