使用 Next.js 進行提示工程:生產環境中的流式 RAG 與快取
我在過去的十八個月裡一直在為從法律科技初創公司到企業知識庫等各種客戶的 Next.js 應用程式中構建 AI 功能。在酷炫的 RAG 演示和能夠處理真實流量而不會虧損的生產系統之間存在巨大差距。本文涵蓋了慘痛的經驗教訓 -- 如何開發保持一致的提示詞、在不破壞你的 UI 的情況下流式傳輸回應、透過智能快取將成本降低 60-80%,以及部署不會在星期五凌晨 2 點崩潰的 RAG 管道。
目錄
- 為什麼 2026 年在 Next.js 中使用 RAG 有意義
- RAG 的提示詞工程基礎
- 在 Next.js 中設置流式 RAG
- 快取層:你節省真實成本的地方
- 生產架構模式
- 監控、可觀測性和調試
- 性能基準和成本分析
- 常見問題

為什麼 2026 年在 Next.js 中使用 RAG 有意義
Next.js 已經成為 AI 驅動的網絡應用的預設選擇,這不僅僅是炒作。App Router、Server Actions、Route Handlers 和 React Server Components 的組合為 RAG 管道提供了真正優秀的架構。你可以將嵌入邏輯保持在伺服器端、透過 Route Handlers 流式傳輸回應,並在多個層面進行積極快取。
Vercel AI SDK(現在為 v4.x 版本)已經成熟了許多。它本機處理流式傳輸、工具調用和結構化輸出。但 SDK 只是管道 -- 真正的挑戰在於周邊的一切:提示詞設計、檢索質量、快取策略和錯誤處理。
以下是 Next.js 中典型生產 RAG 流程的樣子:
- 使用者提交查詢
- 查詢被嵌入(或命中嵌入快取)
- 向量搜索檢索相關的區塊
- 區塊被排序和過濾
- 一個精心設計的提示詞組合上下文
- LLM 流式傳輸回應
- 回應被快取以供類似的未來查詢使用
每個步驟都有失敗模式。讓我們深入研究每一個。
RAG 的提示詞工程基礎
RAG 的提示詞工程與純 LLM 互動的提示詞工程從根本上不同。你不是在問模型一個問題 -- 你是在給它一個特定的上下文視窗,並要求它在當其與訓練資料衝突時忽略訓練資料,從該上下文綜合一個答案。
系統提示詞架構
我已經開發出一個三部分的系統提示詞結構,在不同的領域都運作良好:
const buildSystemPrompt = (config: RAGConfig) => `
You are ${config.assistantName}, a ${config.role} for ${config.company}.
## CONTEXT RULES
- Answer ONLY based on the provided context documents
- If the context doesn't contain enough information, say so explicitly
- Never fabricate citations or reference numbers
- When multiple context documents conflict, note the discrepancy

## RESPONSE FORMAT
- Use markdown formatting for readability
- Cite sources using [Source: document_id] notation
- Keep responses under ${config.maxResponseTokens} tokens unless the user asks for detail
- Use ${config.tone} tone
## DOMAIN RULES
${config.domainRules.join('\n')}
`;
關鍵見解:領域規則是你放置真正讓你的 RAG 有用的東西的地方。對於法律客戶,那可能是「總是注意法規適用的司法管轄區。」對於醫療知識庫,「永遠不要提供劑量建議;始終指向醫療保健提供者。」
上下文視窗管理
使用 GPT-4o 運行在 128k 上下文和 Claude 3.5 在 200k,很容易被誘惑只是把一切都塞進去。別這樣做。更多的上下文並不意味著更好的答案 -- 它通常意味著更糟的。
我使用了一個分層的方法:
const assembleContext = async (
query: string,
retrievedChunks: Chunk[]
): Promise<string> => {
// Tier 1: Top 3 chunks by cosine similarity (always included)
const primary = retrievedChunks.slice(0, 3);
// Tier 2: Next 5 chunks, but only if similarity > threshold
const secondary = retrievedChunks
.slice(3, 8)
.filter(c => c.similarity > 0.78);
// Tier 3: Metadata-enriched summaries of remaining relevant docs
const tertiary = retrievedChunks
.slice(8)
.filter(c => c.similarity > 0.72)
.map(c => c.summary); // Pre-computed summaries, not full text
return formatContextTiers(primary, secondary, tertiary);
};
這通常導致 3,000-8,000 個上下文令牌而不是 30,000+。回應質量上升,延遲下降,你的 API 帳單也會減少。
提示詞版本控制
這是在部落格文章中幾乎沒有人談論但每個人都在生產中需要的東西。你的提示詞會改變。你需要追蹤這些變化。
// prompts/v2.3.ts
export const RAG_PROMPT_V2_3 = {
version: '2.3',
createdAt: '2026-03-15',
changelog: 'Added conflict resolution instruction, reduced hallucination on legal queries by 23%',
system: `...`,
userTemplate: (query: string, context: string) => `...`,
};
我們將提示詞版本存儲在程式碼中,而不是在資料庫中。它們在 PR 中被審查,就像任何其他程式碼變更一樣。當生產環境出現問題時,你可以將其追蹤回特定的提示詞版本。
在 Next.js 中設置流式 RAG
使用 Vercel AI SDK 的 Route Handler
以下是一個生產就緒的流式 RAG 端點:
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { retrieveContext } from '@/lib/rag/retriever';
import { buildPrompt } from '@/lib/rag/prompts';
import { checkCache, setCache } from '@/lib/rag/cache';
import { rateLimiter } from '@/lib/middleware/rate-limit';
export const runtime = 'nodejs'; // Not edge -- you need Node for most vector DBs
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, sessionId } = await req.json();
const lastMessage = messages[messages.length - 1].content;
// Rate limiting
const allowed = await rateLimiter.check(sessionId);
if (!allowed) {
return new Response('Rate limited', { status: 429 });
}
// Check semantic cache first
const cached = await checkCache(lastMessage);
if (cached) {
return new Response(cached.response, {
headers: { 'X-Cache': 'HIT', 'Content-Type': 'text/plain' },
});
}
// Retrieve context
const context = await retrieveContext(lastMessage, {
topK: 10,
minSimilarity: 0.72,
namespace: 'production',
});
// Build the prompt
const systemPrompt = buildPrompt(context);
// Stream the response
const result = streamText({
model: openai('gpt-4o'),
system: systemPrompt,
messages,
temperature: 0.3, // Low temp for factual RAG
maxTokens: 1500,
onFinish: async ({ text }) => {
// Cache the completed response
await setCache(lastMessage, text, context.chunks.map(c => c.id));
},
});
return result.toDataStreamResponse();
}
客戶端流式傳輸 UI
在前端,useChat 鉤子處理流式傳輸很好:
// components/ChatInterface.tsx
'use client';
import { useChat } from 'ai/react';
import { useRef, useEffect } from 'react';
export function ChatInterface() {
const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
useChat({
api: '/api/chat',
body: { sessionId: getSessionId() },
onError: (err) => {
// Don't just console.log -- show the user something useful
toast.error('Something went wrong. Try rephrasing your question.');
},
});
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((m) => (
<div key={m.id} className={m.role === 'user' ? 'text-right' : ''}>
<div className="prose prose-sm max-w-none">
<Markdown>{m.content}</Markdown>
</div>
</div>
))}
{isLoading && <TypingIndicator />}
<div ref={scrollRef} />
</div>
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask a question..."
className="w-full p-3 rounded-lg border"
disabled={isLoading}
/>
</form>
</div>
);
}
處理流式傳輸邊界情況
生產中的流式傳輸意味著處理在演示中永遠不會發生的事情:
- 連接在流式傳輸中期中斷:使用指數退避實現重試邏輯。AI SDK 的
onError回調是你的好朋友。 - 超過令牌限制:監控令牌使用情況並在模型之前實現硬切斷(它的切斷很醜陋)。
- 緩慢的檢索:在向量資料庫查詢上設置超時。如果檢索花費 > 2 秒,退而使用較小的上下文或快取的類似查詢。
快取層:你節省真實成本的地方
快取是你可以對生產 RAG 系統進行的最具影響力的優化。有三個值得實現的層。
層 1:嵌入快取
每個查詢都需要一個嵌入。使用 text-embedding-3-small 每 1K 令牌 $0.00002 的成本,它每個查詢都很便宜,但它會累積 -- 更重要的是 -- 增加延遲。
import { Redis } from '@upstash/redis';
import { createHash } from 'crypto';
const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL!, token: process.env.UPSTASH_REDIS_TOKEN! });
export async function getEmbedding(text: string): Promise<number[]> {
const hash = createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
// Check cache
const cached = await redis.get<number[]>(`emb:${hash}`);
if (cached) return cached;
// Generate embedding
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
const vector = embedding.data[0].embedding;
// Cache for 7 days
await redis.set(`emb:${hash}`, vector, { ex: 604800 });
return vector;
}
層 2:語義快取
這是大的。如果有人問「你的退款政策是什麼?」而另一個人問「我怎樣才能獲得退款?」,他們應該得到相同的快取回應。
export async function checkSemanticCache(query: string): Promise<CacheResult | null> {
const embedding = await getEmbedding(query);
// Search the cache index (separate from your content index)
const results = await pinecone.index('cache').query({
vector: embedding,
topK: 1,
includeMetadata: true,
});
if (results.matches[0]?.score > 0.95) {
return {
response: results.matches[0].metadata.response as string,
originalQuery: results.matches[0].metadata.query as string,
cachedAt: results.matches[0].metadata.cachedAt as string,
};
}
return null;
}
0.95 閾值很重要。太低你會提供錯誤的答案。太高你不會得到快取命中。從 0.95 開始並根據你的領域進行調整。
層 3:回應片段快取
對於結構化回應(如產品規格或政策摘要),快取個別片段:
| 快取層 | 命中率(典型) | 延遲節省 | 成本節省 |
|---|---|---|---|
| 嵌入快取 | 40-60% | 每個查詢 50-100ms | 在 100K 查詢中約 $50/月 |
| 語義快取 | 15-35% | 每個查詢 1-3 秒 | 在 100K 查詢中約 $300-800/月 |
| 片段快取 | 20-40% | 每個查詢 500ms-1s | 在 100K 查詢中約 $100-200/月 |
| 組合 | 60-80% | 平均 1-3 秒 | 在 100K 查詢中約 $500-1200/月 |
快取失效
經典的難題。對於 RAG,我使用了一個兩管齊下的方法:
- 基於 TTL:所有快取都在 24-72 小時後過期,具體取決於你的源資料更改的頻率。
- 基於事件:當源文件更新時,使任何引用這些文件 ID 的快取條目失效(這就是為什麼我們在快取元資料中儲存區塊 ID)。
生產架構模式
完整堆棧
以下是我們用於大多數生產 RAG 部署的架構:
使用者 → Next.js App Router → Route Handler
↓
速率限制器 (Upstash)
↓
語義快取檢查 (Pinecone + Redis)
↓ (未命中)
嵌入生成 (OpenAI / 快取)
↓
向量搜索 (Pinecone / Weaviate / pgvector)
↓
重新排序 (Cohere Rerank / 自訂)
↓
提示詞組合
↓
LLM 流式傳輸 (OpenAI / Anthropic)
↓
回應 → 快取寫入 → 使用者
選擇你的向量資料庫
| 資料庫 | 最適合 | 定價 (2026) | Next.js 整合 |
|---|---|---|---|
| Pinecone | 託管、零操作 | 免費層 → $70/月起 | 優秀 (REST API) |
| Weaviate Cloud | 混合搜索(向量 + 關鍵詞) | $25/月起 | 良好 (JS 客戶端) |
| pgvector (Supabase) | 已在使用 Postgres | 免費層 → $25/月 | 很好 (Supabase SDK) |
| Qdrant Cloud | 高效能、過濾 | 免費層 → $30/月 | 良好 (JS 客戶端) |
| Turbopuffer | 成本優化、S3 支援 | 約 $0.04/GB 儲存 | 還不錯 (REST API) |
對於大多數 Next.js 專案,如果你已經在該生態系中,我會從 Supabase 上的 pgvector 開始,或者如果你想要零操作開銷,就選擇 Pinecone。我們在包括 Astro 和 Next.js 專案中已經使用過所有這些,其中 CMS 內容饋送 RAG 管道。
錯誤處理和回退
生產 RAG 需要優雅的降級:
export async function handleRAGQuery(query: string) {
try {
// Primary path: full RAG
return await fullRAGPipeline(query);
} catch (error) {
if (error instanceof VectorDBError) {
// Fallback 1: Use cached similar queries
const fallback = await getFallbackFromCache(query);
if (fallback) return { ...fallback, degraded: true };
}
if (error instanceof LLMError) {
// Fallback 2: Try a different model
return await fullRAGPipeline(query, { model: 'claude-3-5-sonnet' });
}
// Fallback 3: Return relevant raw chunks without LLM synthesis
const chunks = await retrieveContext(query);
return {
response: 'I couldn\'t generate a full answer, but here are relevant excerpts:',
chunks: chunks.slice(0, 3),
degraded: true,
};
}
}
監控、可觀測性和調試
你不能改進你無法測量的東西。以下是要追蹤的內容:
關鍵指標
- 檢索質量:前 K 個區塊是否實際相關?記錄相似度分數並每週進行抽查。
- 回應延遲 (p50/p95/p99):流式傳輸 TTFB(首位元組時間)和總完成時間。
- 快取命中率:按層。如果你的語義快取命中率低於 10%,你的閾值可能太高了。
- 每個查詢的令牌使用:平均和 p99。監視提示詞注入試圖增加上下文的情況。
- 使用者反饋信號:豎起大拇指/向下,複製事件,後續問題(表示第一個答案不夠好)。
工具
對於 LLM 可觀測性,我對以下工具取得了不錯的結果:
- Langfuse:開源、可自託管、卓越的追蹤可視化。免費層很慷慨。
- Helicone:基於代理的日誌記錄,非常適合成本追蹤。一行整合。
- Braintrust:很好地用於評估和提示詞迭代。評估框架很紮實。
// 範例:Langfuse 與 Vercel AI SDK 整合
import { Langfuse } from 'langfuse';
const langfuse = new Langfuse();
export async function POST(req: Request) {
const trace = langfuse.trace({ name: 'rag-query' });
const retrievalSpan = trace.span({ name: 'retrieval' });
const context = await retrieveContext(query);
retrievalSpan.end({ output: { chunkCount: context.length, topScore: context[0]?.similarity } });
const generationSpan = trace.generation({
name: 'llm-response',
model: 'gpt-4o',
input: messages,
});
// ... 流式傳輸回應
generationSpan.end({ output: completedText });
await langfuse.flushAsync();
}
性能基準和成本分析
來自生產部署的真實數字(法律知識庫,約 50K 個文件,約 2M 個區塊):
| 指標 | 無優化 | 完整優化 |
|---|---|---|
| 中位數延遲 (TTFB) | 2.1 秒 | 340 毫秒 |
| P95 延遲 (TTFB) | 4.8 秒 | 1.2 秒 |
| 月度 LLM 成本 (50K 查詢) | $2,400 | $680 |
| 月度嵌入成本 | $180 | $45 |
| 月度向量資料庫成本 | $70 | $70(不變) |
| 快取命中率 | 0% | 67% |
| 使用者滿意度(豎起大拇指%) | 71% | 89% |
滿意度的改進主要來自更好的提示詞和重新排序,而不是快取。但成本和延遲的改進幾乎完全來自快取。
每個查詢的成本分解
在 50K 查詢/月使用 GPT-4o 的情況下:
- 未快取查詢:約 $0.048(嵌入 + 檢索 + 生成)
- 語義快取命中:約 $0.0004(嵌入查閱 + 快取讀取)
- 嵌入快取命中 + 語義未命中:約 $0.035(跳過嵌入,仍然生成)
如果你正在構建這樣的東西並需要架構方面的幫助,我們會定期進行這類工作。
常見問題
2026 年生產 RAG 的最佳 LLM 是什麼? 對於大多數用例,GPT-4o 在質量、速度和成本之間達到完美平衡。Claude 3.5 Sonnet 在需要更長上下文處理或更細緻推理時非常出色。對於成本敏感的應用程式和更簡單的查詢,GPT-4o-mini 或 Claude 3.5 Haiku 的表現令人驚訝地好 -- 當檢索質量良好時,我們已經看到它們在簡單 Q&A 上與 GPT-4o 相匹配。模型的重要性不如檢索質量和提示詞工程。
我應該為 RAG Route Handlers 使用 Edge Runtime 還是 Node.js Runtime? 幾乎總是 Node.js。Edge Runtime 對大多數向量資料庫和 ORM 的連接限制使得使用它很困難。Edge 的冷啟動優勢對於流式傳輸端點可以忽略不計,因為使用者已經在等待檢索 + 生成。對簡單代理端點或非 RAG 路由使用 Edge。
我如何防止 RAG 回應中的幻覺? 三個實際起效的策略:(1) 明確指示模型在上下文不足時說「我沒有足夠的信息」-- 並在提示詞中包括示例。(2) 為事實查詢使用低溫度 (0.1-0.3)。(3) 實現引用要求 -- 當模型必須引用特定區塊時,它很難幻覺。事後驗證(檢查聲稱是否出現在源區塊中)增加了另一層安全性。
我應該為 RAG 上下文檢索多少個區塊? 檢索比你使用的更多。我通常檢索 15-20 個區塊,重新排序它們,然後將前 3-5 個用作主要上下文,並包括接下來 3-5 個的摘要。將 20 個完整區塊轉儲到上下文視窗中會降低質量。即使相關信息存在,模型也會被無關信息搞糊塗。質量永遠勝過數量。
pgvector 對生產來說足夠好嗎,還是我需要專用的向量資料庫? 對於最多 ~1M 向量,Supabase 或 Neon 上具有 HNSW 索引的 pgvector 絕對可用於生產。查詢性能很好,你可以獲得留在現有 Postgres 生態系中的好處。超過 1M 向量或如果你需要使用向量搜索進行高級過濾,Pinecone 或 Qdrant 等專用選項開始脫穎而出。我們在幾個 Astro 和 Next.js 專案中在生產中運行了 pgvector,沒有問題。
我如何在 React 中使用載入狀態處理流式回應?
Vercel AI SDK 的 useChat 鉤子為你提供了一個 isLoading 布林值,但比這更細緻。鉤子經歷不同狀態:閒置 → 等待(無令牌)→ 流式傳輸(令牌到達)→ 閒置。為了獲得最佳 UX,在等待階段顯示輸入指示器,在流式傳輸期間逐步呈現 markdown。使用一個處理不完整 markdown 優雅的 markdown 渲染器 -- react-markdown 有效但可能閃爍;考慮在呈現前緩衝幾個令牌。
在 RAG 中處理多輪對話的最佳方式是什麼? 不要在每條消息上重新檢索。使用對話歷史來確定新消息是後續(需要相同上下文)還是主題轉換(需要新檢索)。一個簡單的分類器 -- 甚至基於正則表達式的檢查代詞和引用的 -- 可以為你節省許多不必要的向量搜索。當你重新檢索時,在檢索查詢中包括對話摘要,而不是最後一條消息。
我應該多久為 RAG 重新索引一次我的文件? 取決於你的源資料更改的頻率。對於靜態知識庫,每週完整重新索引就很好。對於動態內容(CMS 驅動的網站、每天更新的文件),設置由 webhook 觸發的增量索引。關鍵是有一個管道,可以更新個別區塊而無需重新索引所有內容。我們將其構建到我們的 headless CMS 整合中 -- 當內容在 Sanity 或 Contentful 中更新時,受影響的區塊會自動重新嵌入並更新插入。