Cholate
← 全部文章

2026-06-28 · 11 min⚑ 長文

Token、Context Window、RAG 一次看懂

別人聊 AI 你只能微笑點頭?這篇把最常見的幾個 AI 名詞用日常比喻一次講清楚。

為什麼#

你一定遇過這個場景:朋友聊到最近很紅的 AI 應用,「這個模型的 context window 有 100K token 誒」、「他們後端接了 RAG」、「用 embedding 做語意搜尋」——你在旁邊只能點頭微笑,心裡想著這幾個詞到底是什麼意思。這些名詞聽起來很硬,但拆開來看,每一個背後的概念其實都跟你熟悉的生活經驗有對應。這篇就是要把它們一次講清楚,讓你下次聊天可以真的接得上話,甚至能反問對方一句「所以你們是怎麼切 chunk 的?」。

我自己一開始也是被這些縮寫嚇到的人。後來發現,只要照順序理解——先搞懂 token、再搞懂 context window、再搞懂 embedding、最後才是 RAG——每一個新名詞都是建立在前一個名詞上,完全不需要死背,理解一次就忘不掉。這篇文章就照這個順序走,中間會穿插幾段真的可以跑的程式碼,讓抽象的概念有個具體的落地方式。

怎麼做#

Token:模型讀文字的最小單位#

先從最基礎的 token 開始。你可能以為語言模型是一個字一個字讀文章,但實際上模型讀的單位不是「字」,也不是「英文單字」,而是 token——一種介於字元跟單字之間、由演算法(常見的是 BPE,Byte Pair Encoding)切出來的片段。中文的一個字通常會被切成 1 到 2 個 token,英文的一個常見單字(例如 theis)常常剛好是 1 個 token,但比較少見的單字(例如 tokenization)可能被切成好幾個 tokentokenization)。

這件事之所以重要,是因為幾乎所有語言模型服務的計費、限制,都是用 token 數計算,不是用「字數」或「字元數」計算。用 Python 大概估算一段文字的 token 數會長這樣:

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")
text = "把 RAG 講成人話"
tokens = enc.encode(text)
print(len(tokens))  # 中英混合文字,token 數通常比字數多
print(enc.decode(tokens[:3]))  # 可以反解回去看每個 token 對應到什麼片段

一個粗略的經驗法則:英文大約 4 個字元約等於 1 個 token;中文因為資訊密度高,通常 1 個字約等於 1 到 2 個 token。這也是為什麼中文對話「感覺沒打幾個字」,token 用量卻常常比預期高——同一句話,中文版的 token 數經常比看起來對應的英文翻譯還多。

實務上這會直接影響你的荷包:多數 API 供應商是按輸入 token 數加輸出 token 數計費,同一個問題,你的提示詞(prompt)寫得又臭又長,帳單就會跟著變厚。所以壓縮提示詞、刪掉不必要的重複說明,其實是實際能省錢的工程技巧,不是只有「工程師潔癖」而已。

Context Window:模型一次能看多少#

如果 token 是模型讀文字的最小單位,context window(上下文視窗)就是模型「一次能同時看多少個 token」的上限。你可以把它想像成一張很長、但終究有邊界的辦公桌——你可以把資料、對話紀錄、文件內容都攤在桌上讓模型參考,但攤過頭、超出桌面範圍的部分,模型就看不到了,就像資料掉到桌子底下一樣。

不同模型的 context window 大小差很多,從幾千到幾十萬 token 都有,而且這個數字一直在往上長。context window 越大,模型能一次讀進去的資料越多(例如整份合約、整份程式碼庫、一整本書),但也不是無限大就是好——塞進去的內容越多,模型要在裡面「找重點」的難度也越高,回答速度跟成本也會跟著上升,有時候還會出現「中間內容被忽略」的現象(業界戲稱 lost in the middle)。

這裡的成本差異可以用一個具體的數字感受一下:假設你把一整份 5 萬字的公司規章塞進 context window,每次同事問一個問題,你就要重新把這 5 萬字連同問題一起送給模型,付一次對應的 token 費用;如果只塞真正相關的那一兩段(大概幾百字),同樣的問題、同樣的答案品質,token 用量可能只剩原本的百分之一。這也是為什麼「該塞多少進 context window」從來不是「越多越保險」,而是一個要精算的工程決策——多塞的每一個 token,都是真金白銀的成本,也是拖慢回應速度的重量。

實務上常見的策略,是把長對話「摘要」或「截斷」,只留下最相關的部分塞進 context window,而不是把所有歷史紀錄都無腦塞進去。舉個簡單的例子,一個聊天機器人通常會這樣管理對話歷史:

function buildContext(history: Message[], maxTokens: number): Message[] {
  const kept: Message[] = [];
  let used = 0;
  // 從最新的訊息往回塞,超過上限就停止,較舊的訊息改用摘要代替
  for (let i = history.length - 1; i >= 0; i--) {
    const cost = estimateTokens(history[i].content);
    if (used + cost > maxTokens) break;
    kept.unshift(history[i]);
    used += cost;
  }
  return kept;
}

Embedding:把語意變成一串數字#

接下來這個概念,是理解 RAG 的關鍵鑰匙。embedding(嵌入向量)是把一段文字,轉換成一串固定長度的數字(向量),而且這串數字有一個很有用的性質:語意相近的文字,轉出來的向量在數學空間裡也會相近。

舉個例子,「貓」跟「狗」的 embedding 向量距離,會比「貓」跟「發票」的向量距離近很多——即使這三個詞完全沒有共同的字。這代表你可以用數學方法(通常是計算 cosine similarity,餘弦相似度)去衡量兩段文字「意思有多接近」,而不需要文字完全一樣、也不需要關鍵字完全命中。這正是傳統關鍵字搜尋做不到、但語意搜尋做得到的事。

from openai import OpenAI
import numpy as np

client = OpenAI()

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(model="text-embedding-3-small", input=text)
    return resp.data[0].embedding

def cosine_similarity(a, b):
    a, b = np.array(a), np.array(b)
    return a.dot(b) / (np.linalg.norm(a) * np.linalg.norm(b))

cat = embed("貓喜歡曬太陽")
dog = embed("狗喜歡追球")
invoice = embed("這張發票需要重開")

print(cosine_similarity(cat, dog))       # 數值較高,語意較接近
print(cosine_similarity(cat, invoice))   # 數值較低,語意差比較遠

有了這個工具,你就可以把大量文件(例如公司內部知識庫、產品文件、客服 QA)都轉成 embedding,存進一個專門存向量的資料庫(vector database,例如 pgvector、Pinecone、Qdrant),之後只要把使用者的問題也轉成 embedding,就能用數學方法快速找出「意思最接近」的幾份文件,而不用整份文件庫都塞進 context window,也不用文件跟問題用詞完全一致。

RAG:先查資料,再回答問題#

有了 tokencontext windowembedding 三個底層概念,RAG(Retrieval-Augmented Generation,檢索增強生成)就很好理解了:它是一種「先查資料、再交給模型回答」的架構,用來解決兩個大問題——模型不知道你的私有資料,以及模型的記憶(訓練資料)有時間截止點,可能過時、也可能編造答案(也就是常聽到的「幻覺」,hallucination)。

RAG 的流程大致是這樣:

使用者問題


把問題轉成 embedding


在向量資料庫裡找出最相關的幾段文件(Top-K)


把「使用者問題」加上「找到的文件片段」一起塞進 context window


模型根據「有憑有據」的文件內容回答,而不是純靠記憶亂猜

用一段簡化的程式碼串起來,大概會長這樣:

async function askWithRAG(question: string) {
  const questionEmbedding = await embed(question);
  const topChunks = await vectorDb.search(questionEmbedding, { topK: 3 });

  const context = topChunks.map((c) => c.text).join('\n---\n');
  const prompt = `根據以下資料回答問題,若資料不足請明說:\n\n${context}\n\n問題:${question}`;

  return llm.complete(prompt);
}

這也解釋了為什麼很多產品會強調自己「有接 RAG」——它讓模型的回答有真實文件當依據,而不是憑空生成,同時不用把整個知識庫塞進昂貴又有限的 context window。實務上還有一個常被忽略的細節:文件要先切成適當大小的片段(chunk),切太大會讓 embedding 抓不到重點、切太小又會失去上下文,這個切法本身就是一門要調參的工程,沒有標準答案,通常要拿實際資料反覆測試才知道多大的 chunk 效果最好。

Chunk:切法才是 RAG 真正難的地方#

前面提到的 chunk(片段),聽起來只是把長文件「切一切」,但實際做起來比想像中細碎。切太大,一個 chunk 裡混雜太多主題,embedding 抓出來的向量會變成好幾個意思的平均值,語意搜尋準確度反而下降;切太小,一個 chunk 可能只剩半句話,缺乏上下文,模型讀到這個片段時完全不知道它在講什麼。常見的折衷做法,是按固定字數切、但在段落與段落之間保留一小段「重疊」(overlap),避免關鍵資訊剛好被切在兩個 chunk 的邊界上:

def chunk_text(text: str, size: int = 500, overlap: int = 50) -> list[str]:
    chunks = []
    start = 0
    while start < len(text):
        end = start + size
        chunks.append(text[start:end])
        start = end - overlap  # 讓下一段跟上一段有重疊,避免切斷關鍵句子
    return chunks

有了 chunk 之後,實際呼叫 embedding API 通常長這樣(這裡直接用 curl 示範,讓你看到底層其實就是一個普通的 HTTP 請求,沒有想像中神秘):

curl https://api.openai.com/v1/embeddings \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-small",
    "input": "把長文件切成 chunk 之後,逐段轉成 embedding"
  }'

我自己在實作 RAG 的時候,踩過的最大一個坑,是以為切 chunk 這件事「隨便切都可以」,結果實測發現:用同一份文件、只是把 chunk 大小從 300 字調成 800 字,語意搜尋回來的相關文件品質差了一大截。後來才理解,chunk 大小其實跟你的內容型態高度相關——技術文件(步驟清楚、段落獨立)適合切小一點,敘事型內容(前後文互相依賴)就得切大一點、重疊也要留多一點,沒有一組數字可以套用在所有專案上,得拿自己的資料實測,比對搜尋結果的相關度才知道哪個切法對自己的內容最有效。

混合搜尋:embedding 不是萬能#

講到這裡你可能會覺得,既然 embedding 這麼神奇,那乾脆全面改用語意搜尋,把舊的關鍵字搜尋整套換掉不就好了?實務上沒這麼簡單。embedding 擅長抓「意思接近」,但對「精確符號」反而不太行——例如商品編號 SKU-8891、錯誤代碼 ERR_TIMEOUT、或是使用者輸入的完整人名,這些字串本身沒有太多「語意」可言,向量距離不見得能準確反映「使用者就是要找這一筆」。這種情況下,傳統的關鍵字搜尋(例如 BM25 演算法)反而比向量搜尋更準,因為它就是直接比對字串是否出現,不需要猜語意。

所以正式環境的 RAG 系統,很少只用單一種搜尋方式,而是把「向量搜尋」跟「關鍵字搜尋」的結果合併,取兩邊排名都不錯的文件,業界稱這種做法叫 hybrid search(混合搜尋):

def hybrid_search(query: str, top_k: int = 5):
    vector_hits = vector_search(embed(query), top_k=top_k * 2)
    keyword_hits = bm25_search(query, top_k=top_k * 2)
    # 把兩邊分數正規化後加權合併,再取前 top_k 筆
    merged = merge_and_rerank(vector_hits, keyword_hits)
    return merged[:top_k]

我自己一開始沒意識到這件事,只做了純向量搜尋的 RAG,結果使用者輸入一組明確的訂單編號,系統卻回了一堆「語意相關但答非所問」的文件,後來補上關鍵字搜尋那一路,準確度才明顯改善。這也是這幾個名詞學完之後,我覺得最值得記住的一課:每個技術都有它的強項與死角,tokencontext windowembeddingRAG 沒有一個是「裝上去就萬事OK」的萬靈丹,理解它們各自解決什麼問題、又在哪裡會失手,才是真正把這些名詞「學懂」而不是「背起來」的差別。

把這四個概念串起來看一次:你先知道模型是用 token 計費、計量,所以會想辦法把提示詞寫精簡;接著知道 context window 有上限,所以設計對話系統時會做摘要或截斷,不會把所有歷史紀錄無腦塞進去;再來知道 embedding 能把文字變成可以計算距離的向量,所以想到可以拿它做語意搜尋,取代死板的關鍵字比對;最後知道純語意搜尋在精確符號上會失手,所以 RAG 系統實務上會混合關鍵字與向量搜尋。每一個決策,都是上一個概念的自然延伸——這也是為什麼我建議照順序理解,而不是把四個名詞當成四張互不相干的字卡死背,那樣背完很快就會忘記彼此之間的關聯,遇到實際問題時也不知道該從哪個概念下手排查。

重點回顧#

  • token 是模型讀寫文字的最小單位,計費與限制都以它計算,中文的 token 用量通常比字數看起來高,壓縮提示詞是真的能省錢的工程技巧。
  • context window 是模型一次能參考的 token 上限,越大不代表越好,塞太滿反而增加成本與找重點的難度,實務上常靠摘要或截斷來管理。
  • embedding 把文字轉成向量,讓「語意相近」變成可以用數學(cosine similarity)計算的距離,這是語意搜尋取代關鍵字搜尋的關鍵。
  • RAG 用「先查資料、再回答」的架構,讓模型的回答有文件依據,減少幻覺,也不用把整個知識庫塞進 context window
  • 這幾個名詞環環相扣:先懂 token 才懂 context window 的限制,先懂 embedding 才懂 RAG 怎麼找資料,順著這個順序理解,比死背名詞有效得多。

延伸閱讀#


#ai#llm#rag