Perplexity: AI原生搜尋設計

Perplexity如何在AI搜尋中建立信任:引用優先設計、漸進式披露、串流回應和查詢優化。包含TypeScript和CSS實作模式。

5 分鐘閱讀 181 字
Perplexity: AI原生搜尋設計 screenshot

Perplexity

「人們希望盡快獲得資訊,並且信任這些資訊。從這個角度來看,這其實是一個相當簡單的設計問題。」

設計理念

Perplexity 代表了一種新典範:AI 原生搜尋。傳統搜尋引擎回傳連結清單,聊天機器人回傳對話式回應,而 Perplexity 則將多個來源的資訊整合成單一、附有引用來源的答案。這個設計體現了徹底的透明度——每一項聲明都可追溯到其來源。

團隊的核心洞察:搜尋不是對話,而是一項資訊探索任務。UI 設計反映了這一點,呈現的是來源和答案,而非聊天記錄和角色設定。


核心要點

  1. 引用是建立 AI 信任的必要條件 - 每項事實陳述都連結到其來源;來源面板保持可見,讓使用者無需離開答案頁面即可驗證
  2. 熟悉的介面降低使用門檻 - 搜尋框看起來像 Google,而非聊天提示框;使用者可以輸入關鍵字或完整問題,無需學習對話模式
  3. 預測後續問題 - 多數使用者不知道接下來該問什麼;建議與上下文相關的後續問題可維持使用者的參與度
  4. 展示過程,而非僅呈現結果 - 「搜尋中 → 閱讀中 → 撰寫中」的階段顯示透過透明度建立信任,並降低使用者感知的等待時間
  5. 將品質不佳的結果視為失敗狀態 - 當查詢可能產生不足的結果時,應請求澄清,而非回傳平庸的答案

模式庫

引用優先設計

Perplexity 普及了 AI 回應中的行內引用,從根本上改變了使用者對生成內容的信任方式。每項事實陳述都連結到其來源。

interface Citation {
  index: number;
  url: string;
  title: string;
  favicon: string;
  snippet: string;
  domain: string;
}

interface AnswerBlock {
  text: string;
  citations: number[];  // Indices into citation array
}

function CitedAnswer({ blocks, citations }: {
  blocks: AnswerBlock[];
  citations: Citation[];
}) {
  return (
    <article className="answer">
      {blocks.map((block, i) => (
        <p key={i}>
          {block.text}
          {block.citations.map(citationIndex => (
            <CitationMarker
              key={citationIndex}
              citation={citations[citationIndex]}
              index={citationIndex + 1}
            />
          ))}
        </p>
      ))}

      {/* Source panel always visible */}
      <aside className="sources-panel">
        <h3>Sources</h3>
        {citations.map((citation, i) => (
          <SourceCard key={i} citation={citation} index={i + 1} />
        ))}
      </aside>
    </article>
  );
}

function CitationMarker({ citation, index }: {
  citation: Citation;
  index: number;
}) {
  const [expanded, setExpanded] = useState(false);

  return (
    <span className="citation-wrapper">
      <sup
        className="citation-marker"
        onMouseEnter={() => setExpanded(true)}
        onMouseLeave={() => setExpanded(false)}
      >
        [{index}]
      </sup>

      {/* Expandable snippet preview */}
      {expanded && (
        <div className="citation-preview">
          <img src={citation.favicon} alt="" className="citation-favicon" />
          <span className="citation-domain">{citation.domain}</span>
          <p className="citation-snippet">{citation.snippet}</p>
        </div>
      )}
    </span>
  );
}

設計洞察:引用並非可選項,而是嵌入每一次互動中的必要元素。來源面板保持可見,讓使用者無需離開答案視圖即可驗證各項聲明。


熟悉的搜尋介面

Perplexity 的輸入框看起來像傳統搜尋框,而非聊天提示。這種熟悉的設計降低了不習慣對話式 AI 的使用者的使用門檻。

function SearchInput({ onSubmit }: { onSubmit: (query: string) => void }) {
  const [query, setQuery] = useState('');

  return (
    <div className="search-container">
      {/* Intentionally looks like Google/traditional search */}
      <div className="search-box">
        <SearchIcon className="search-icon" />

        <input
          type="text"
          className="search-input"
          placeholder="Ask anything..."
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && onSubmit(query)}
        />

        {query && (
          <button
            className="clear-button"
            onClick={() => setQuery('')}
          >
            <XIcon />
          </button>
        )}

        <button
          className="submit-button"
          onClick={() => onSubmit(query)}
        >
          <ArrowRightIcon />
        </button>
      </div>

      {/* Optional context chips */}
      <div className="focus-chips">
        <Chip icon={<GlobeIcon />}>All</Chip>
        <Chip icon={<AcademicIcon />}>Academic</Chip>
        <Chip icon={<CodeIcon />}>Code</Chip>
        <Chip icon={<VideoIcon />}>Video</Chip>
      </div>
    </div>
  );
}

設計洞察:即使只有幾個關鍵字也能運作。使用者不需要精心設計複雜的提示詞。這個介面同時接受簡單查詢和複雜問題。


透過後續建議實現漸進式揭露

Perplexity 不期待使用者提出好的後續問題,而是主動預測並建議問題。預測性後續建議解決了大多數使用者不知道接下來該問什麼的現實問題。

interface FollowUpSuggestion {
  question: string;
  reasoning: string;  // Why this might be relevant
}

function FollowUpSuggestions({
  suggestions,
  onSelect
}: {
  suggestions: FollowUpSuggestion[];
  onSelect: (question: string) => void;
}) {
  return (
    <div className="follow-ups">
      <h4>Related</h4>

      {/* Show one at a time - progressive disclosure */}
      {suggestions.slice(0, 4).map((suggestion, i) => (
        <button
          key={i}
          className="follow-up-chip"
          onClick={() => onSelect(suggestion.question)}
        >
          <span className="follow-up-text">{suggestion.question}</span>
          <ArrowRightIcon className="follow-up-arrow" />
        </button>
      ))}
    </div>
  );
}

// Predict follow-ups based on query context
function generateFollowUps(query: string, answer: string): FollowUpSuggestion[] {
  // AI generates contextually relevant next questions
  return [
    { question: "How does this compare to alternatives?", reasoning: "comparison" },
    { question: "What are the limitations?", reasoning: "critical analysis" },
    { question: "Can you provide specific examples?", reasoning: "concrete details" },
    { question: "What's the historical context?", reasoning: "background" },
  ];
}

設計洞察:就像很少有人在主題演講結束時提問一樣,大多數使用者也不擅長提出後續問題。預測他們可能想知道的內容。


串流回應與漸進式渲染

Perplexity 使用 Server-Sent Events (SSE) 來串流回應,而 UI 則以自然的方式漸進式呈現內容。

function StreamingAnswer({ query }: { query: string }) {
  const [sources, setSources] = useState<Citation[]>([]);
  const [answer, setAnswer] = useState<string>('');
  const [phase, setPhase] = useState<'searching' | 'reading' | 'writing'>('searching');

  useEffect(() => {
    const eventSource = new EventSource(`/api/search?q=${encodeURIComponent(query)}`);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case 'sources':
          setPhase('reading');
          setSources(data.sources);
          break;
        case 'chunk':
          setPhase('writing');
          setAnswer(prev => prev + data.text);
          break;
        case 'done':
          eventSource.close();
          break;
      }
    };

    return () => eventSource.close();
  }, [query]);

  return (
    <div className="streaming-answer">
      {/* 階段指示器 */}
      <PhaseIndicator phase={phase} />

      {/* 來源優先顯示 */}
      {sources.length > 0 && (
        <SourceCards sources={sources} />
      )}

      {/* 答案串流輸入 */}
      <div className="answer-content">
        <TypewriterText text={answer} />
        {phase === 'writing' && <BlinkingCursor />}
      </div>
    </div>
  );
}

function PhaseIndicator({ phase }: { phase: 'searching' | 'reading' | 'writing' }) {
  const phases = {
    searching: { icon: <SearchIcon />, text: 'Searching the web...' },
    reading: { icon: <BookIcon />, text: 'Reading sources...' },
    writing: { icon: <PenIcon />, text: 'Writing answer...' },
  };

  return (
    <div className="phase-indicator">
      {phases[phase].icon}
      <span>{phases[phase].text}</span>
      <LoadingDots />
    </div>
  );
}

設計洞察:展示過程,而非僅呈現結果。使用者先看到來源出現(建立信任),然後觀看答案逐步撰寫。透明度可減少等待時的焦慮感。


透過精煉預防錯誤

當查詢過於廣泛時,Perplexity 會要求澄清,而非回傳品質不佳的結果。系統將不足的結果視為失敗狀態。

interface ClarificationRequest {
  type: 'ambiguous' | 'too_broad' | 'missing_context';
  suggestions: string[];
  originalQuery: string;
}

function QueryRefinement({ request, onRefine }: {
  request: ClarificationRequest;
  onRefine: (refinedQuery: string) => void;
}) {
  const messages = {
    ambiguous: "I found multiple meanings. Which one did you mean?",
    too_broad: "This topic is quite broad. Can you be more specific?",
    missing_context: "I need a bit more context to give you a useful answer.",
  };

  return (
    <div className="refinement-prompt">
      <p className="refinement-message">{messages[request.type]}</p>

      <div className="refinement-suggestions">
        {request.suggestions.map((suggestion, i) => (
          <button
            key={i}
            className="refinement-option"
            onClick={() => onRefine(suggestion)}
          >
            {suggestion}
          </button>
        ))}
      </div>

      <div className="refinement-custom">
        <input
          type="text"
          placeholder="Or type your own refinement..."
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              onRefine((e.target as HTMLInputElement).value);
            }
          }}
        />
      </div>
    </div>
  );
}

設計洞察:其他搜尋服務回傳品質不佳的結果,然後期望使用者自行改進查詢。Perplexity 則主動引導使用者建構更精確的查詢。


Collections 與 Spaces

對於結構化的研究工作,Perplexity 提供了 Spaces 功能——這是專屬的資料集合空間,使用者可以在其中整理查詢、釘選結果,並上傳參考資料。

interface Space {
  id: string;
  name: string;
  threads: Thread[];
  documents: Document[];
  createdAt: Date;
}

interface Thread {
  id: string;
  query: string;
  answer: Answer;
  citations: Citation[];
  pinned: boolean;
}

function SpacesSidebar({ spaces, activeSpace, onSelect }: {
  spaces: Space[];
  activeSpace: string;
  onSelect: (id: string) => void;
}) {
  return (
    <aside className="spaces-sidebar">
      <header>
        <h2>Spaces</h2>
        <button className="new-space">
          <PlusIcon /> New Space
        </button>
      </header>

      <nav className="space-list">
        {spaces.map(space => (
          <button
            key={space.id}
            className={`space-item ${space.id === activeSpace ? 'active' : ''}`}
            onClick={() => onSelect(space.id)}
          >
            <FolderIcon />
            <span className="space-name">{space.name}</span>
            <span className="thread-count">{space.threads.length}</span>
          </button>
        ))}
      </nav>
    </aside>
  );
}

function SpaceView({ space }: { space: Space }) {
  return (
    <div className="space-view">
      <header className="space-header">
        <h1>{space.name}</h1>
        <div className="space-actions">
          <button><ShareIcon /> Share</button>
          <button><UploadIcon /> Add Files</button>
        </div>
      </header>

      {/* 釘選的對話串 */}
      <section className="pinned-threads">
        <h3>Pinned</h3>
        {space.threads.filter(t => t.pinned).map(thread => (
          <ThreadCard key={thread.id} thread={thread} />
        ))}
      </section>

      {/* 所有對話串 */}
      <section className="all-threads">
        <h3>All Threads</h3>
        {space.threads.map(thread => (
          <ThreadCard key={thread.id} thread={thread} />
        ))}
      </section>

      {/* Embedded documents */}
      {space.documents.length > 0 && (
        <section className="space-documents">
          <h3>Reference Materials</h3>
          {space.documents.map(doc => (
            <DocumentCard key={doc.id} document={doc} />
          ))}
        </section>
      )}
    </div>
  );
}

設計洞察:Collections 模仿了學者和記者過去需要手動維護的分頁研究資料堆疊,現在直接嵌入產品中。


視覺設計系統

字型排版:清晰優先於個性

Perplexity 使用中性、高度可讀的字型系統。內容才是主角,而非介面本身。

:root {
  /* System font stack for fastest load */
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'SF Mono', Monaco, 'Courier New', monospace;

  /* Type scale - optimized for reading */
  --text-sm: 13px;
  --text-base: 15px;
  --text-lg: 17px;
  --text-xl: 20px;
  --text-2xl: 24px;

  /* Line heights - generous for readability */
  --leading-tight: 1.3;
  --leading-normal: 1.6;
  --leading-relaxed: 1.8;
}

/* Answer text - optimized for long-form reading */
.answer-content {
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: 680px;  /* Optimal reading width */
}

/* Citations - smaller, unobtrusive */
.citation-marker {
  font-size: var(--text-sm);
  color: var(--accent-primary);
  cursor: pointer;
  vertical-align: super;
}

/* Source cards - scannable metadata */
.source-card {
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
}

.source-domain {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

色彩系統:以克制建立信任

色彩的使用相當節制,主要用於引用標記、狀態指示和焦點狀態。

:root {
  /* Neutral foundation */
  --bg-primary: #ffffff;
  --bg-secondary: #f7f7f8;
  --bg-tertiary: #ededef;

  /* Text hierarchy */
  --text-primary: #1a1a1b;
  --text-secondary: #57575a;
  --text-tertiary: #8e8e93;

  /* Citation accent - trustworthy blue */
  --accent-citation: #0066cc;
  --accent-citation-hover: #0052a3;

  /* Focus states */
  --focus-ring: 0 0 0 2px var(--accent-citation);

  /* Phase indicators */
  --phase-searching: #f59e0b;
  --phase-reading: #3b82f6;
  --phase-writing: #10b981;
}

/* Clean, ad-free interface */
.search-results {
  background: var(--bg-primary);
  padding: 24px;
  border-radius: 12px;
}

/* Citation hover state */
.citation-marker:hover {
  background: var(--accent-citation);
  color: white;
  border-radius: 2px;
}

/* Source card - subtle boundary */
.source-card {
  border: 1px solid var(--bg-tertiary);
  border-radius: 8px;
  padding: 12px;
  transition: border-color 0.15s ease;
}

.source-card:hover {
  border-color: var(--accent-citation);
}

動畫模式

階段轉換

搜尋 → 閱讀 → 撰寫各階段之間的流暢轉換,讓使用者隨時掌握進度。

/* Phase indicator */
.phase-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--bg-secondary);
  border-radius: 20px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Loading dots animation */
.loading-dots {
  display: flex;
  gap: 4px;
}

.loading-dots span {
  width: 4px;
  height: 4px;
  background: var(--text-tertiary);
  border-radius: 50%;
  animation: dot-pulse 1.4s infinite ease-in-out;
}

.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }

@keyframes dot-pulse {
  0%, 80%, 100% {
    transform: scale(0.6);
    opacity: 0.5;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}

/* Sources fade in as they're found */
.source-card {
  animation: fade-in-up 0.3s ease-out;
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Stagger source cards */
.source-card:nth-child(1) { animation-delay: 0ms; }
.source-card:nth-child(2) { animation-delay: 100ms; }
.source-card:nth-child(3) { animation-delay: 200ms; }
.source-card:nth-child(4) { animation-delay: 300ms; }

串流文字

答案以逐字方式呈現,但會以群組區塊的形式顯示以提升可讀性。

function TypewriterText({ text }: { text: string }) {
  const [displayedText, setDisplayedText] = useState('');

  useEffect(() => {
    // Text already arrived from SSE - just display it
    setDisplayedText(text);
  }, [text]);

  return (
    <div className="typewriter">
      {displayedText}
    </div>
  );
}

function BlinkingCursor() {
  return (
    <span className="cursor" aria-hidden="true">|</span>
  );
}
.cursor {
  animation: blink 1s step-end infinite;
  color: var(--text-secondary);
}

@keyframes blink {
  50% { opacity: 0; }
}

經驗總結

1. 引用來源是建立 AI 信任的必要條件

在 AI 介面中,每個事實陳述都應該可追溯來源。Perplexity 將引用的準確性融入每次互動中,這不是可選功能,也不會被隱藏。

2. 搜尋框優於聊天提示

熟悉的搜尋介面降低了使用門檻。使用者可以輸入關鍵字或完整問句,兩者都能運作。不要強迫使用者採用對話式模式。

3. 預測後續問題

大多數使用者不知道接下來該問什麼。建議與上下文相關的後續問題,而不是期望使用者主導對話。

4. 展示處理過程

串流提供的是透明度,而非僅僅是速度。展示「搜尋中 → 閱讀中 → 撰寫中」的階段可以建立信任,並減少感知等待時間。

5. 將薄弱結果視為失敗

當查詢可能產生不足的結果時,應該請求使用者提供更多細節,而不是回傳品質不佳的答案。引導使用者提出更好的問題。

6. 簡潔介面 = 信任訊號

無廣告的極簡介面傳達了一個訊息:產品優先考慮資訊品質,而非廣告變現。


常見問題

Perplexity 的引用系統如何運作?

Perplexity 答案中的每個事實陳述都包含行內編號引用,連結到來源網址。來源面板會與答案並列顯示,呈現 favicon、網域名稱和摘要預覽。將滑鼠懸停在引用編號上會展開顯示特定的來源上下文。這使驗證變得即時,不需要使用者費力尋找。

為什麼 Perplexity 看起來像搜尋引擎而不是聊天機器人?

團隊發現搜尋不是對話——而是尋找資訊的任務。熟悉的搜尋框介面同時接受簡單關鍵字和複雜問題,不需要使用者學習對話式提示模式。這降低了對 AI 聊天介面感到不自在的使用者的使用門檻。

Perplexity 的串流回應方法是什麼?

Perplexity 使用 Server-Sent Events(SSE)分三個階段逐步呈現內容:「搜尋中」(尋找來源)、「閱讀中」(分析來源)和「撰寫中」(整合答案)。來源會先出現以建立信任,然後答案才開始串流。這種透明度減少了等待時的焦慮感。

Perplexity 如何處理模糊或過於廣泛的查詢?

當查詢可能產生薄弱結果時,Perplexity 會要求澄清,而不是回傳平庸的答案。系統會識別模糊的術語、過於廣泛的主題或缺少的上下文,然後建議具體的改進方向。使用者可以點擊建議或輸入自己的補充說明。

什麼是 Perplexity Spaces,如何使用?

Spaces 是用於結構化研究的專屬集合,使用者可以在其中整理相關查詢、釘選重要結果並上傳參考文件。它們模擬了學術研究者和記者手動維護的分頁研究堆疊,現在直接嵌入產品中,用於持續進行的研究專案。


參考資料