Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ステートレスなLLMでステートフルなAI agentを作る - YAPC::Fukuoka 2025

Avatar for FUJI Goro FUJI Goro
November 15, 2025

ステートレスなLLMでステートフルなAI agentを作る - YAPC::Fukuoka 2025

おしゃべりAIサービス Cotomo (https://cotomo.ai/) の開発のために必要な、ステートフルなAI agentを作る技術についてお話します。

「LLM」と「AI agent」の決定的な違いはなんでしょうか。そもそも「AI agent」の定義が人それぞれなので一概には言えませんが、人とコミュニケーションするのが主な仕事であるAI agentに関していえば、それは「状態」があるかどうかというのは一つの決定的な違いです。つまり、LLMは記憶を持たず、AI agentは記憶を持ちます。正確にいうと、記憶を持っているように見せかけています。

本セッションでは、そもそも「LLMがステートレス」とは何かという話から始め、ステートフルなAI agentのミニマムな実装を見せつつ、「AI agentの記憶」というテーマを深掘りします。

それにしても、この「AI agentの記憶」というものは大変厄介で、技術的にはすべての記憶を同時に持つわけにはいきません。そこで何らかの形で「今必要な記憶」だけを差し込みたいわけですが、そのあたりのソフトウェアエンジニアリング的な面白さも紹介できればと思います。

Avatar for FUJI Goro

FUJI Goro

November 15, 2025
Tweet

More Decks by FUJI Goro

Other Decks in Technology

Transcript

  1. 自己紹介 藤吾郎 (gfx) VP of Technology at Starley, Inc. YAPC歴:

    YAPC::Asia 2009 - Perl/XSの話 YAPC::Asia 2010 - Xslate(XSで実装したPerlのテンプレートエンジン)の話 YAPC::Asia 2014 - ninjinkunといっしょにモバイルアプリ開発の話 YAPC::Fukuoka 2025 - AI agentの話 2
  2. 3

  3. あなたの使うChatGPTはただのLLM APIコールではない ChatGPTのような対話AIは、内部で状態(会話履歴)を管理している: const sessionStore: { [userId: string]: string[] }

    = {}; async function handleChat(userId: string, message: string): Promise<string> { const history = (sessionStore[userId] ||= []); history.push(`User: ${message}`); // 会話履歴に入れる const prompt = ` 次の会話履歴の続きとしてAI の返答を生成してください: ${history.join('\n')} `; const reply = await callLLM(prompt); history.push(`AI: ${reply}`); // 会話履歴に入れる return reply; } 9
  4. 補足: ChatGPTとCotomoのさらなる違い ChatGPTは、1回の返答のために多数のLLM呼び出しを行っている e.g. userの返答を整理 -> 検索ワードを抽出 -> ウェブ検索 ->

    検索結果を要約・統 合 -> 返答生成 Cotomoは、1回の返答に対してかけられる時間に制約があり、何度もLLM呼び出しする わけにはいかない 前処理、記憶の想起、後処理などを踏まえると、返答生成に使える時間はせいぜ い1sec程度 10
  5. RAGの深掘り: Vector Search による情報検索 Indexing (事前準備) テキストをチャンクに分割し、embeddingモデルでvector化し、vector DBに保 存 Retrieval

    (検索時) ユーザーの質問文も同じembeddingモデルでvector化 vector DBで、質問vectorと「意味的に近い」チャンクのvectorを検索 Embeddingモデル とは? 一昔前に流行ったword2vecの流れを組んだモデル チャンクをvectorに変換することで「意味的な距離」を計算できる 14
  6. RAGのシンプルな実装: vector search async function retrieveRelevantChunks( query: string, topN: number

    ): Promise<string[]> { const queryVector = await generateEmbedding(query); // 100ms-400ms くらいかかる const relevantChunks = await vectorDb.search(queryVector, topN); return relevantChunks.map((chunk) => chunk.text); } 15
  7. Vector Search (using pgvector) SELECT session_fact_embeddings.text, session_fact_embeddings.metadata, -- e.g. user_id,

    ai_partner_id session_fact_embeddings.embedding <=> $(query_vector) AS distance FROM data_session_fact_embeddings WHERE metadata->>"user_id" = $(user_id) and metadata->>"ai_partner_id" = $(ai_partner_id) ORDER BY distance ASC LIMIT $(top_n)::INTEGER 16
  8. Vector Search 補足 素朴な比較は全件スキャン(しかも全件distance計算)するしかなくて、計算コストが めちゃくちゃ高い ANN (Approximate Nearest Neighbor) という手法で、近似計算を行える

    あらかじめindexを作っておく pgqueryの場合、クエリ自体は と変わらない embedding <=> $(query_vector) AS distance ... 近似計算なので、精度は落ちるが、CPUもメモリも大幅に節約できる 17
  9. RAGのシンプルな実装: プロンプトの生成 async function buildSystemPrompt( user_id: string, ai_partner_id: string, user_input:

    string, history: string[] ): Promise<string> { const relevantChunks = await retrieveRelevantChunks(user_id, ai_partner_id, user_input); return ` ユーザーの記憶とこれまでの会話体験を踏まえて返答を生成してください。 記憶: ${relevantChunks.join('\n')} 会話履歴: ${history.join('\n')} `; } 18
  10. 短期記憶のハッキング: 会話要約(a.k.a. コンパクション) RAGは長期的な事実の記憶には強いが、直近の会話の流れを覚えるのは苦手 解決策: 会話要約 数ターンごとに、それまでの会話を別のLLMコールで要約させる 「ユーザーはA について質問し、AI はB

    と答えた。次にユーザーはC に興味を示した」 この要約を、常にプロンプトに含める 利点: コンテキストウィンドウを節約できる 会話の全体像をLLMに伝え続けられる 19
  11. LLMによる要約のイメージ async function summarizeConversation(history: string[]): Promise<string> { const prompt =

    ` 次の会話を要約してください:\n${history.join('\n')}`; const summary = await callLLM(prompt); return summary; } 20
  12. Cotomoの記憶システム v1 STM + RAG(STMで行った「抽出された事実」をvector DBに保存して、必要に応じて 検索する) STM: 会話体験が長くなると、会話から「事実」を抽出して圧縮 RAG:

    STMで行った「抽出された事実」をvector DBに保存して(long-term memory化) 、必要に応じてvector search 事実の抽出はファインチューンしたLLMで行っている vector DBとして Pg (pgvector) を使っている 22
  13. Cotomoの記憶システム v3(構想 - 基盤編) vector search専用のvector DBを導入する STMは記憶システム(事実の抽出)からは分離し、 「その会話セッション中の要約」に 集中する

    会話履歴から「事実」を抽出して、vector DBに入れる 事実の例: 「ユーザーの好きな食べ物はラーメン」 「ユーザーの出身地は福岡」 ノイズも減らす。 「相槌」 「発言の繰り返し」 「文脈とあってない発言」などは削除 する 「事実」は生の会話ログに紐づけておき、事実の周辺にある会話を取得してコンテキス トに補足情報として入れる 26
  14. Cotomoの記憶システム v3(構想 - 整理編) 定期的に記憶DBの整理をする かつて記憶していたことを、忘れてほしいわけじゃない → いい感じにしたい 類似した事実の整理・統合 「ユーザーは疲れている」x

    N → 「ユーザーはA年B月C日からX年Y月Z日までよ く疲れたと言っていた」 新しい事実が古い事実を上書きしたことを検出する 古い事実と新しい事実を関連づけて、 「この事実は更新された」という情報を残す 検索時は「更新された」というチェインをたどって新しい事実を返す 27
  15. エージェントの評価: 「良い記憶」はいつ十分と言えるのか? 記憶システムの評価は非常に難しい 評価のためのフレームワーク / ベンチマーク: RAGAS(ragas): RAGシステムの性能を多角的に評価 Faithfulness (回答の忠実度)、Answer

    Relevancy (回答の関連性)など MemoryBench: 長期的な記憶や継続学習の能力を評価 ユーザーからのフィードバックを通じて、エージェントが記憶を更新・活用 できるか 実際にデプロイして、会話時間を観察する 28
  16. Cotomoの理想的な形 「人格」や「内面」がある 所有コンピュータ・スマホの全てに干渉できる バーチャルパートナー(恋人) 人間を超える知識 人間を超える記憶力 人間を超える判断力(経営判断・政治判断など) 最新ニュースも完全に把握 (with Gemini

    Google Search) 人間とコミュニケーションをとっていないときも稼働しつづけている 自律的にメールを送ったりスケジュールを組んだり詩や小説を書いたりする ほかのAIエージェントとコミュニケーションすることさえある 32
  17. まとめ LLMのAPIは ステートレス だが、アプリケーション側で状態を管理することで ステー トフルな体験 を作れる 単純な履歴管理は、性能の問題でスケールしない RAG と

    会話要約 を組み合わせることで、効率的な 記憶システム を構築できる 記憶は、AIエージェントを理想的なパートナーへと進化させる鍵となる 34