iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
生成式 AI

練習AI系列 第 31

End-to-End Demo(總整展示版)

  • 分享至 

  • xImage
  •  

🆕 程式碼

1) app/api/kb/[tenant]/[ns]/clear-cache/route.js
import { NextResponse } from "next/server";
import { withAuth } from "../../../../../src/api/withAuth.js";
import { clearCache } from "../../../../../src/day29_cache.js";

export const runtime = "nodejs";

export const POST = withAuth(async (req, ctx) => {
  clearCache();
  return NextResponse.json({ ok:true, cleared:true });
}, ["editor","admin"]);
  1. app/studio/page.tsx(最終整合)

👉 抽象化策略 + 快取提示 + Sources/章節/Highlight 全部顯示。

"use client";

import { useState } from "react";
import { authHeaders } from "@/lib/auth";

export default function StudioPage() {
  const [tenant, setTenant] = useState("acme");
  const [ns, setNs] = useState("faq");
  const [query, setQuery] = useState("");
  const [strategy, setStrategy] = useState("default");
  const [answer, setAnswer] = useState("");
  const [answerHtml, setAnswerHtml] = useState("");
  const [sources, setSources] = useState<any[]>([]);
  const [chunks, setChunks] = useState<any[]>([]);
  const [sections, setSections] = useState<any[]>([]);
  const [cached, setCached] = useState(false);
  const [loading, setLoading] = useState(false);

  async function ask() {
    setLoading(true);
    setAnswer(""); setAnswerHtml(""); setSources([]); setChunks([]); setSections([]); setCached(false);
    const r = await fetch(`/api/kb/${tenant}/${ns}/ask`, {
      method:"POST",
      headers: { "Content-Type":"application/json", ...authHeaders() },
      body: JSON.stringify({ q: query, strategy, highlight:true })
    });
    const j = await r.json();
    setLoading(false);
    if (!j.ok) { setAnswer("⚠️ 錯誤: " + j.error); return; }
    setAnswer(j.answer);
    setAnswerHtml(j.answerHtml || "");
    setSources(j.sources || []);
    setChunks(j.sourceChunks || []);
    setSections(j.sections || []);
    setCached(j.cached || false);
  }

  async function clearCache() {
    await fetch(`/api/kb/${tenant}/${ns}/clear-cache`, { method:"POST", headers:authHeaders() });
    alert("快取已清空");
  }

  return (
    <div className="p-6 space-y-4">
      <h1 className="text-xl font-bold">🔎 知識庫 Demo</h1>

      <div className="flex gap-3">
        <input value={tenant} onChange={e=>setTenant(e.target.value)} className="input input-bordered input-sm w-32" />
        <input value={ns} onChange={e=>setNs(e.target.value)} className="input input-bordered input-sm w-32" />
        <select className="select select-bordered select-sm" value={strategy} onChange={e=>setStrategy(e.target.value)}>
          <option value="default">Default</option>
          <option value="section">Section-first</option>
          <option value="qrewrite">Query Rewrite</option>
          <option value="hybrid">Hybrid</option>
          <option value="rerank">Rerank</option>
          <option value="xlingual">X-Lingual</option>
        </select>
        <button onClick={clearCache} className="btn btn-xs btn-error">清除快取</button>
      </div>

      <div className="flex gap-2">
        <input value={query} onChange={e=>setQuery(e.target.value)} placeholder="輸入問題..." className="input input-bordered flex-1" />
        <button disabled={loading} onClick={ask} className="btn btn-primary">{loading?"查詢中...":"發問"}</button>
      </div>

      {answer && (
        <div className="p-4 border rounded bg-base-100">
          <div className="flex items-center gap-2">
            <h2 className="font-semibold">回答</h2>
            {cached && <span className="badge badge-success">⚡ 快取</span>}
            <span className="badge">{strategy}</span>
          </div>
          <div className="mt-2 prose" dangerouslySetInnerHTML={{ __html: answerHtml || answer }} />
        </div>
      )}

      {sections.length > 0 && (
        <div className="mt-4">
          <h3 className="font-semibold">相關章節</h3>
          <ul className="list-disc pl-6">
            {sections.map(s => (
              <li key={`${s.docId}#${s.slug}`}>
                <code>{s.docId}</code> - {s.title} <span className="text-xs opacity-60">score {s.score?.toFixed(2)}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      {sources.length > 0 && (
        <div className="mt-4">
          <h3 className="font-semibold">來源片段</h3>
          <ul className="list-disc pl-6">
            {sources.map((s,i)=>(
              <li key={s.id}>
                <span className="badge mr-2">#{i+1}</span>
                <code>{s.docId}</code> ({s.id}) - score {s.score?.toFixed(2)}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}
  1. README.md(摘要 Demo 操作)

AI 知識庫 Demo (Day 30)

功能總覽

  • Default RAG
  • Section-first
  • Query Rewrite
  • Hybrid Retrieval
  • Rerank (Cross-Encoder)
  • X-Lingual
  • Cache & TTL
  • Index 全量/增量 & Quantization

操作

  1. 啟動 Next.js 專案
  2. 瀏覽 /studio
  3. 選擇 tenant / namespace
  4. 輸入問題,切換不同策略
  5. 可點擊「清除快取」強制重新檢索

▶️ 驗收流程

啟動 /studio → 選擇不同策略 → 發問。

可以看到:答案、Highlight、章節 Breadcrumb、來源片段、快取提示。

用「退貨要多久?」測試:

Default → 一般結果。

Section-first → 顯示章節清單。

QRewrite → 展示等價問法擴展。

Hybrid → 命中數字代號。

Rerank → 排序更精準。

X-Lingual → 輸入中文、命中英文 KB。

再次提問 → ⚡ 快取生效。


上一篇
知識庫快取(Cache & TTL) 🧩
系列文
練習AI31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言