AIアプリ開発の教科書 | Edbase[キューベス]
RAG完成版3 ~見やすく・使いやすくする——UI/UXをやさしく磨く | SkillhubAI(スキルハブエーアイ)

RAG完成版3 ~見やすく・使いやすくする——UI/UXをやさしく磨く

レッスン3:見やすく・使いやすくする

UI/UXの“ねらい”を理解してから実装する(初心者向け)

前回(レッスン2)で「質問 → 答え+根拠」は動きました。 ここからは“使う人の気持ち”に寄り添って、UIを迷わず・気持ちよく整えます。まずはなぜこのUIにするのか(ねらい)をやさしく理解し、そのあとどこに何を書けばよいかを迷わない手順で実装します。最後に差分完成版コードを載せます。


なぜ UI を整えるの?(ユーザーの気持ちから逆算)

最初の画面で、いきなり空の入力欄だけだと……👇

┌─────────────── 以前 ───────────────┐
│ [          何を書けばいいの?         ] │ ← ブランクの壁
└──────────────────────────────┘

初心者はここで固まりがちです。 だから、導き(ガイド)と安心感を足します。

┌───────────────────────────────────── 今回 ─────────────────────────────────────┐
│  [質問]   プリセット(簡潔/バランス/じっくり)   🧹クリア                                  │
│   └─ ヒント(例)ボタン:・要点だけ ・結論と根拠 ・前提と制約                                 │
│                                                                                     │
│  🧠 回答カード:最初に3行で要点 → その後に本文(落ち着いて読める)                           │
│  🔎 参照した文脈:折りたたみ+先頭プレビュー+軽いハイライト(根拠の所在が一目で)            │
└──────────────────────────────────────────────────────────────────────────────┘

UIの“ねらい”と嬉しさ

  • プリセット(簡潔/バランス/じっくり) 初心者に難しい「top_k / トークン量 / temperature」を3択に凝縮。 → “とりあえず選べる” → 手戻りが減る。

  • ヒントボタン 「聞き方が分からない」を1クリックで解消。 → “ゼロから考えなくていい” → 迷いが消える。

  • 回答カードを「要点 → 本文」 最初に3行以内の要点を先出し→その後に本文。 → “流し見→読み込む”の自然な順番で疲れない。

  • 参照(根拠)は折りたたみ+プレビュー+ハイライト いきなり長文を見せず、先頭だけ太字で視線誘導。 → “どこが根拠?”が一目でわかる。信頼できる。

  • 🧹クリア(小リセット) 入れ替えや試行錯誤のとき、ワンボタンで状態を掃除。 → “試しやすい・戻しやすい”。


置き場所マップ(どこに書く?)

すべて 1ファイル:app.py に追記・置換します。 レッスン2の完成版をベースに、以下の手順どおりに入れてください。

  1. 先頭の import 群import re を追加(ハイライトに使う)
  2. build_messages() 内の user_text を置換(要点→本文の書式に)
  3. answer_query() より上preview() / highlight() 関数を新規追加
  4. page_ask_my_pdf()を手直し
  • プリセット → ヒントボタン → (初期値つき)質問欄 → クリア → 回答生成 → 参照表示
    1. Streamlitの再実行API差を吸収するため、force_rerun() を追加して st.rerun() / 旧APIを両対応

ここからはハンズオンで、各ステップを「どこに書くか」つきで示します。


ハンズオン

ステップ1|import re を追加

場所app.py 先頭の import 群

import re  # ← これを追加(太字ハイライトに使う)


ステップ2|回答カードの書式(要点→本文)に変更

場所def build_messages(query, contexts): の中 やることuser_text = ( から始まるブロックを丸ごと置換

user_text = (
    f"次の文脈に基づいて日本語で丁寧に回答してください。\n"
    f"出力形式:\n"
    f"1) 最初に3行以内で要点(箇条書き、記号は「・」)\n"
    f"2) 続いて本文(必要なら段落)\n"
    f"注意: 文脈にないことは書かない。分からない場合は「分かりません」。\n\n"
    f"[文脈]\n{context_text}\n\n"
    f"[質問]\n{query}\n\n"
    f"[回答]\n"
)


ステップ3|参照を“のぞきやすく”する関数を追加

場所def answer_query(...): より(ヘルパー群の近く)

def preview(text: str, chars=240):
    text = text.replace("\n", " ")
    return text[:chars] + ("…" if len(text) > chars else "")

def highlight(text: str, query: str):
    # 記号除去後、3文字以上の語を太字に(超かんたんハイライト)
    words = [w for w in re.split(r"[^\wぁ-んァ-ヶ一-龠ー]+", query) if len(w) >= 3]
    out = text
    for w in set(words):
        out = re.sub(fr"({re.escape(w)})", r"**\1**", out, flags=re.IGNORECASE)
    return out


ステップ4|Streamlit の再実行(rerun)を両対応に

場所st.set_page_config(...)直後あたり(どこでもOK)

def force_rerun():
    """Streamlit のバージョン差を吸収して再実行する."""
    if hasattr(st, "rerun"):
        st.rerun()                # 新API
    else:
        st.experimental_rerun()   # 旧API

こうしておくと、あなたの requirements.txtstreamlit>=1.28.0)でも壊れません


ステップ5|質問ページ page_ask_my_pdf() を整える

場所def page_ask_my_pdf():中身 やること:以下のパーツをこの順番で入れていきます。

  1. プリセット(簡潔/バランス/じっくり → top_k/トークン量/temperature を自動セット)
  2. ヒントボタン(クリックで例文が質問欄に入る)
  3. 質問欄(初期値はヒントの内容を入れる)
  4. 🧹クリアsession_state を掃除 → force_rerun()
  5. 回答生成ボタン → 回答カード表示
  6. 参照表示preview()highlight() で見やすく)

具体的なコードは、すぐ下の「差分」と「完成版」にそのまま載せています。 置き場所に迷ったら、完成版をコピペで上書きしてOKです。


動作チェック(小さなチェックリスト)

  • [ ] PDFをアップロード → 「🚀 処理を開始」で保存できる
  • [ ] 「Ask My PDF(s)」でプリセットを選べる
  • [ ] ヒントを押すと質問欄に文言が入る(画面がサッと更新される)
  • [ ] 💬 回答を生成で「要点→本文」の形で答えが出る
  • [ ] 参照(根拠)を開くと、先頭プレビュー太字ハイライトで見やすい
  • [ ] 🧹 クリアで状態がリセットされる(ヒントや質問が消える)

差分(レッスン2 → レッスン3)

既存の app.py への最小変更だけを示します。手で直すときの道しるべにどうぞ。

--- a/app.py
+++ b/app.py
@@
-import os
-import io
-import uuid
-import hashlib
+import os
+import io
+import uuid
+import hashlib
+import re

@@
 st.set_page_config(page_title="PDFに質問しよう", page_icon="📄", layout="wide")

+# ---- Streamlit 互換ラッパ(rerun) ----
+def force_rerun():
+    """Streamlit のバージョン差を吸収して再実行する."""
+    if hasattr(st, "rerun"):
+        st.rerun()
+    else:
+        st.experimental_rerun()
+

@@
 def build_messages(query, contexts):
@@
-    user_text = (
-        f"次の文脈に基づいて質問に答えてください。\n\n"
-        f"[文脈]\n{context_text}\n\n"
-        f"[質問]\n{query}\n\n"
-        f"[回答](日本語で簡潔に)"
-    )
+    user_text = (
+        f"次の文脈に基づいて日本語で丁寧に回答してください。\n"
+        f"出力形式:\n"
+        f"1) 最初に3行以内で要点(箇条書き、記号は「・」)\n"
+        f"2) 続いて本文(必要なら段落)\n"
+        f"注意: 文脈にないことは書かない。分からない場合は「分かりません」。\n\n"
+        f"[文脈]\n{context_text}\n\n"
+        f"[質問]\n{query}\n\n"
+        f"[回答]\n"
+    )

@@
+def preview(text: str, chars=240):
+    text = text.replace("\n", " ")
+    return text[:chars] + ("…" if len(text) > chars else "")
+
+def highlight(text: str, query: str):
+    # 超かんたんハイライト(3文字以上の語を対象)
+    words = [w for w in re.split(r"[^\wぁ-んァ-ヶ一-龠ー]+", query) if len(w) >= 3]
+    out = text
+    for w in set(words):
+        out = re.sub(fr"({re.escape(w)})", r"**\1**", out, flags=re.IGNORECASE)
+    return out
@@
 def page_ask_my_pdf():
-    st.title("🤔 Ask My PDF(s)")
-    st.markdown("アップロードしたPDFの内容に基づいて質問できます。")
+    st.title("🤔 Ask My PDF(s)")
+    st.markdown("アップロードしたPDFの内容に基づいて質問できます。使い方に迷わないよう、プリセットとヒントを用意しました。")

@@
-    st.markdown("---")
-    query = st.text_input("質問を入力してください", placeholder="例:この資料の売上の要点は?")
-    c1, c2, c3 = st.columns([1, 1, 1])
-    with c1:
-        top_k = st.slider("検索チャンク数(top_k)", 1, 10, 5)
-    with c2:
-        max_ctx = st.slider("文脈の最大トークン", 500, 4000, 2000, step=100)
-    with c3:
-        temp = st.slider("創造性(temperature)", 0.0, 1.0, 0.2, step=0.1)
+    st.markdown("---")
+    # ① プリセット(初心者向け)
+    preset = st.radio("回答スタイル", ["簡潔", "バランス", "じっくり"], horizontal=True)
+    if preset == "簡潔":
+        top_k, max_ctx, temp = 3, 1200, 0.1
+    elif preset == "じっくり":
+        top_k, max_ctx, temp = 7, 2600, 0.3
+    else:
+        top_k, max_ctx, temp = 5, 2000, 0.2
+
+    # ② ヒント(例文ボタン)
+    st.markdown("**ヒント:こんな聞き方も**")
+    cols = st.columns(3)
+    examples = ["要点だけ教えて", "結論と数値の根拠を教えて", "この資料の前提と制約は?"]
+    for c, ex in zip(cols, examples):
+        if c.button(f"💡 {ex}"):
+            st.session_state["last_query"] = ex
+            force_rerun()
+
+    # ③ 質問欄(ヒントの反映を初期値に)
+    query_default = st.session_state.get("last_query", "")
+    query = st.text_input("質問を入力してください", value=query_default, placeholder="例:この資料の売上の要点は?")
+
+    # ④ クリア(小さなリセット)
+    reset_cols = st.columns([1,3,1])
+    with reset_cols[2]:
+        if st.button("🧹 クリア(質問と表示をリセット)"):
+            for k in ["chat", "last_query"]:
+                if k in st.session_state:
+                    del st.session_state[k]
+            force_rerun()

@@
-                with st.expander(f"チャンク {i}(類似度: {r['score']:.3f})"):
-                    st.write(r["text"])
+                with st.expander(f"チャンク {i}(類似度: {r['score']:.3f})"):
+                    pv = preview(r["text"])
+                    st.markdown(highlight(pv, query))

@@
+    # (任意)詳細設定:慣れてきた人向け
+    with st.expander("⚙️ 詳細設定(上級者向け・任意)"):
+        top_k = st.slider("検索チャンク数(top_k)", 1, 10, top_k, key="adv_topk")
+        max_ctx = st.slider("文脈の最大トークン", 500, 4000, max_ctx, step=100, key="adv_ctx")
+        temp = st.slider("創造性(temperature)", 0.0, 1.0, temp, step=0.1, key="adv_temp")


これで、UIの意図がわかり、どこに何を書くかが迷わず進められます。 次はレッスン4で、「取りすぎず・足りなすぎず」を実現する検索品質のチューニングに進みましょう。