源内の拡張RAGアプリに会話継続機能を実装して、源内Webの「会話を続ける」ボタンと連携させてみた

源内の拡張RAGアプリに会話継続機能を実装して、源内Webの「会話を続ける」ボタンと連携させてみた

源内のクエリ拡張 RAG は 1 問 1 答で会話が続かないので、会話履歴を受け取ってプロンプトに渡すよう改修し、源内 Web の「会話を続ける」ボタンで実際に文脈が引き継がれるところまで確認しました。
2026.09.08

いわさです。

デジタル庁が OSS 公開しているガバメント AI「源内(GENAI)」について、以前 2 本の記事を書きました。

https://dev.classmethod.jp/articles/digital-genai-oss/

https://dev.classmethod.jp/articles/digital-genai-exapp/

源内は大きく分けて 2 つのシステムで構成されています。

1 つが利用者が直接さわる Web アプリケーションの源内 Web(genai-web)、もう 1 つが生成 AI を活用したマイクロサービス群である源内 AI アプリ(genai-ai-api)です。
源内 Web には ExApp(外部 AI アプリ連携)という仕組みがあって、REST API のプロトコルに準拠した外部アプリを源内の UI から呼び出せます。
源内 AI アプリのリポジトリには、この ExApp に登録できる行政実務用のテンプレートが公開されていて、AWS 向けには Bedrock Knowledge Base を使ったクエリ拡張 RAG(Query Expansion RAG)が用意されています。
前回はこのクエリ拡張 RAG をデプロイして源内 Web に登録するところまでやりました。

前回の検証では拡張 RAG への質問は 1 問 1 答で、前の会話を踏まえたやり取りは試していませんでした。
源内 Web の AI アプリ API 仕様を読んでみると、実は会話履歴を扱う仕組みが用意されています。

会話履歴は、API 定義の JSON に conversation_history というキーを追加することで利用できます。

https://github.com/digital-go-jp/genai-web/blob/main/docs/AIアプリAPI仕様.md

このキーを追加すると実行結果と履歴に「会話を続ける」ボタンが表示され、過去のやり取りを保持したまま質問できるようになります。
ただボタンが表示されはするのですが、実際に会話を継続させるためにはもうちょい手を入れる必要がありまして、そのあたりを今回やってみました。

会話を続けるボタンの仕組み

先に「会話を続ける」ボタンについて整理しておきます。
AI アプリ登録時の API リクエスト形式(JSON)に conversation_history キーを追加すると、源内 Web はアプリ実行結果の下に「会話を続ける」ボタンを表示します。

{
  "question": {
    "title": "質問",
    "desc": "質問したい内容を入力してください。",
    "type": "text",
    "required": true
  },
  "conversation_history": {
    "title": "会話履歴",
    "desc": "過去の会話履歴を入力することで、その内容を参照した回答を生成できます",
    "type": "textarea"
  }
}

源内 Web のチーム管理でアプリを登録する際、この JSON を「API リクエストのデータ形式」に設定します。

A0104B54-85ED-454A-A099-DAB3B8F0592F_1_105_c.jpeg

このボタンを押すと、直前のやり取りが会話履歴として次のリクエストに引き継がれます。
あとは AI アプリ側がその会話履歴を使って回答すれば、チャットのように会話を続けられます。

実際に確認してみる

前回の記事と同様に拡張 RAG テンプレートをデプロイ済みの環境を使います。

改修前は会話が継続されない

まず改修前の状態を確認します。
源内 Web から「会話を続ける」ボタンで会話履歴を送っても、次のように前の質問内容を踏まえた回答にはなりませんでした。

35A74A52-D00F-490E-862D-E4F10BAE2338.png

ボタン自体は表示されて会話履歴も送信されているのに、それが回答に反映されていません。

API に直接リクエストを送っても同じでした。会話履歴を渡した上で「それ」を含む質問をしてみます。

curl -X POST "$API_ENDPOINT" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -d '{
    "inputs": {
      "question": "それについてもっと詳しく教えてください。",
      "conversation_history": "ユーザー: フレックスタイム制について教えてください。\nアシスタント: フレックスタイム制は労働者が始業・終業時刻を自由に決められる制度です。",
      "n_queries": 3
    }
  }'

申し訳ございませんが、ご質問の「それ」が何を指しているのか特定できません。

会話履歴を送信しているのに「それ」を理解できていません。

源内 AI アプリ側のリポジトリのコードを確認してみたのですが、Lambda 関数のエントリポイント aws/query-expansion-rag/lib/constructs/rag-lambda/invokeModel/app.pyparse_input() では、リクエストから question しか取り出しておらず、conversation_history は使われていないっぽかったです。
ということで、実装上デフォルトのクエリ拡張 RAG は 1 問 1 答の設計になっているようでした。

会話履歴の処理を追加する

ということで会話履歴を処理できるようにアプリケーションを修正してみましょう。
aws/query-expansion-rag/lib/constructs/rag-lambda/invokeModel/ 配下のコードをいくつか修正します。

まず app.py で会話履歴を受け取れるようにします。
源内 Web の実装(packages/web/src/features/exapp/invoke/utils/buildPayload.ts)を確認したところ、「会話を続ける」ボタンで送信されるキーは conversation_histories(複数形・配列型)でした。
API 仕様ドキュメントに書かれている conversation_history(単数形・textarea 型)とは別物です。
両方に対応しておきます。

app.py
     # output_in_detailの取得
     output_in_detail = inputs.get("output_in_detail", False)
     logger.debug(f"output_in_detail: {output_in_detail}")

+    # 会話履歴の取得(源内Webは conversation_histories 配列で送ってくる)
+    conversation_history = ""
+    conversation_histories = inputs.get("conversation_histories", [])
+    if conversation_histories and isinstance(conversation_histories, list):
+        history_parts = []
+        for entry in conversation_histories:
+            if isinstance(entry, dict):
+                input_text = entry.get("input", "")
+                output_text = entry.get("output", "")
+                if input_text:
+                    # inputがJSON文字列の場合、questionだけ抽出
+                    try:
+                        parsed_input = json.loads(input_text) if isinstance(input_text, str) else input_text
+                        if isinstance(parsed_input, dict) and "question" in parsed_input:
+                            input_text = parsed_input["question"]
+                    except (json.JSONDecodeError, TypeError):
+                        pass
+                    history_parts.append(f"ユーザー: {input_text}")
+                if output_text:
+                    history_parts.append(f"アシスタント: {output_text[:500]}")
+        conversation_history = "\n".join(history_parts)
+    else:
+        # テキスト形式の conversation_history も受け付ける
+        conversation_history = inputs.get("conversation_history", "")

送られてくる conversation_histories は以下のような配列で、input が JSON 文字列になっています。
ここから question を取り出してテキストに整形しています。

[
  {
    "input": "{\"question\": \"テレワークのルールについて教えてください。\"}",
    "output": "テレワークは事前に上長の承認を得た上で...",
    "createdDate": "1787658275125"
  }
]

整形した会話履歴を後続の処理に渡します。クエリ拡張(core/query_expansion.py)では、質問の前に会話履歴を差し込んでからクエリを生成します。

core/query_expansion.py
     n_queries: int = 3,
     file_content_blocks: list[dict[str, Any]] | None = None,
     usage_tracker: BedrockUsageTracker | None = None,
+    conversation_history: str = "",
 ) -> list[str]:
     ...
-    prompt = replacePlaceholders(system_prompt, {"question": question, "n_queries": str(n_queries)})
+    placeholder_values = {"question": question, "n_queries": str(n_queries)}
+    if conversation_history:
+        question_with_context = f"以下は過去の会話履歴です:\n{conversation_history}\n\n現在の質問:{question}"
+        placeholder_values["question"] = question_with_context
+    prompt = replacePlaceholders(system_prompt, placeholder_values)

回答生成(core/answer_generation.py)でも同様に、プロンプトに会話履歴を含めます。

core/answer_generation.py
     file_content_blocks: list[dict[str, Any]] | None = None,
     system_prompt_override: str | None = None,
     usage_tracker: BedrockUsageTracker | None = None,
+    conversation_history: str = "",
 ) -> str:
     ...
+    if conversation_history:
+        placeholder_values["question"] = (
+            f"以下は過去の会話履歴です:\n{conversation_history}\n\n現在の質問:{user_question}"
+        )

なお、クエリ拡張と回答生成のあいだには関連性評価のステップもあるようだったのですが、今回はそこには履歴を渡していません。
会話履歴を反映するのに最低限の実装だけ今回はしてます。

改修後の動作を確認する

改修後の API に会話履歴付きでリクエストを送ってみます。
なお今回は Knowledge Base にドキュメントを入れていないので、RAG 検索の中身ではなく「それ」という代名詞を会話履歴から解決できるかどうかで会話継続性を見ています。

curl -X POST "$API_ENDPOINT" \
  -H "x-api-key: $API_KEY" \
  -d '{
    "inputs": {
      "question": "それは週に何日まで可能ですか?",
      "conversation_history": "ユーザー: テレワークのルールについて教えてください。\nアシスタント: テレワークは事前に上長の承認を得た上で実施可能です。"
    }
  }'

テレワークの実施可能日数に関する具体的な情報が現在のシステムに登録されていません。週に何日まで可能かについては、直属の上長または人事部門にお問い合わせいただくことをお勧めします。

「それ」が「テレワーク」を指していると理解してますね。パラメータをちゃんと処理できていそうです。

これが会話履歴なしで同じ質問を送ると、こうなります。

「それ」が何を指しているのか...具体的にどの機能やサービスについてのご質問かが明確でないため、正確な回答ができません。

会話履歴なしだと相変わらず「それ」を特定できないので、うまくうごいていそうです。

源内 Web から会話を継続する

改修した拡張 RAG アプリを源内 Web に登録し直して、ブラウザからも使ってみます。
登録後、まず適当な会話を開始すると、回答の下に「会話を続ける」ボタンが表示されます。良いですね。

43506BD1-1BCE-45BE-BDF2-5B09D83B0526_1_105_c.jpeg

このボタンを押すと同じアプリの実行画面に遷移し、「会話履歴」フィールドに前回のやり取りが入った状態になります。
ここで先ほどの内容について聞いてみましょう。どうかな...

335D5AC1-88DB-41BD-99FE-79116100DE42.png

先ほどのご質問は「こんにちは」というご挨拶でした。

前回何を聞いたかを踏まえた回答になりました。良いですね!!

さらに「会話を続ける」を押すと、履歴がどんどん蓄積されていきます。

BCB8A913-2747-48ED-A79B-85B7BC064445_1_105_c.jpeg

さいごに

本日はクエリ拡張 RAG に会話継続機能を実装して、源内 Web の「会話を続ける」ボタンと連携させてみました。

デフォルトのテンプレートは会話を保持しませんが、ちょっと改修することで対応できました。
源内 Web 側は「会話を続ける」の UI やパラメータ渡しの仕組みは用意されているので、AI アプリ側が会話履歴を受け取って処理するだけどうにかいけます。

API 仕様ドキュメントには conversation_history(単数形・textarea 型)と書かれているのですが、最初この通りに実装したら源内 Web からの会話履歴が届きませんでした。
ログを見たら実際に送られてくるキーは conversation_histories(複数形・配列型)で、フロントエンドの実装とドキュメントがずれていたような気がしますね。

今回は源内 Web が会話履歴を毎回まるごと送ってくる方式に合わせて、AI アプリ側はプロンプトに埋め込むだけにしましたが、この方式だと会話が長くなるほど毎回のトークン量が増えていくので、チャットのように長く使うなら少し工夫が必要そうです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事