{"notes":[{"id":"retrieval-augmented-generation/cag","category":"retrieval-augmented-generation","legacyPath":"","legacySlug":"","legacyPaths":[],"title":"CAG","subtitle":"背多分","author":"Z. Yuan","paperDate":"2024-12-20","publishedOn":"2026-07-19","paperTitle":"Don't Do RAG: When Cache-Augmented Generation is All You Need for Knowledge Tasks","paperUrl":"https://arxiv.org/abs/2412.15605","bibliography":{"title":"Don't Do RAG: When Cache-Augmented Generation is All You Need for Knowledge Tasks","authors":["Brian J Chan","Chao-Ting Chen","Jui-Hung Cheng","Hen-Hsen Huang"],"url":"https://arxiv.org/abs/2412.15605","pdfUrl":"https://arxiv.org/pdf/2412.15605v2","codeUrl":"https://github.com/hhhuang/CAG","preprint":{"id":"2412.15605","submittedOn":"2024-12-20","url":"https://arxiv.org/abs/2412.15605"},"publication":{"venue":"WWW Companion","year":2025,"kind":"conference","url":"https://doi.org/10.1145/3701716.3715490"},"citation":"@misc{retrievalaugmentedgenerationcag2024,\n  title = {{Don't Do RAG: When Cache-Augmented Generation is All You Need for Knowledge Tasks}},\n  author = {Brian J Chan and Chao-Ting Chen and Jui-Hung Cheng and Hen-Hsen Huang},\n  year = {2024},\n  eprint = {2412.15605},\n  archivePrefix = {arXiv},\n  url = {https://arxiv.org/abs/2412.15605}\n}\n","citationPath":"/content/papers/citations/retrieval-augmented-generation/cag.52cd8e1fb14c.bib"},"thumbnail":{"src":"/content/papers/assets/1ccb714a2b9e40fe07cb712c508763c4447c40b66c78a8c364a030957c65a789.png","width":694,"height":575},"locale":"zh-hans","path":"/content/papers/zh-hans/retrieval-augmented-generation/cag.9b923dc653e1.md","headings":[{"id":"定义问题","text":"定义问题","depth":2},{"id":"解决问题","text":"解决问题","depth":2},{"id":"模型读完后留下什么","text":"模型读完后留下什么？","depth":3},{"id":"三个步骤","text":"三个步骤","depth":3},{"id":"拿实际尺寸算一次","text":"拿实际尺寸算一次","depth":3},{"id":"为什么要整包放","text":"为什么要整包放？","depth":3},{"id":"讨论","text":"讨论","depth":2},{"id":"实验怎么比","text":"实验怎么比？","depth":3},{"id":"准确度大部分领先但不是全部","text":"准确度：大部分领先，但不是全部","depth":3},{"id":"速度省下的是重读","text":"速度：省下的是重读","depth":3},{"id":"这篇没有回答的事","text":"这篇没有回答的事","depth":3},{"id":"结论","text":"结论","depth":2}],"anchorAliases":{},"guide":null,"relatedReading":[]}],"text":"这篇论文的标题很有个性：「**不要做 RAG**」。\n\nRAG（Retrieval-Augmented Generation，检索增强生成）是这几年替语言模型接上外部数据的标准做法：用户问一个问题，系统先去知识库找出几段相关文本，贴在问题前面，再让模型照着回答。公司内部文档、产品 FAQ、客服记录，很多都是这样接进聊天机器人的。\n\n这套流程好用，但每一关都可能出事。\n\n检索器找错段落，模型就只能拿错的数据作答；找对了但只拿到一半，答案也会缺一块。整条流水线要切文档、建索引、调 top-k、选 embedding 模型，维护起来并不轻松。\n\n到了 2024 年，开源模型的上下文长度已经拉到十几万 token。于是作者问了一个很直接的问题：\n\n- **如果整个知识库本来就塞得进去，为什么还要先检索？**\n\n\n> [!TIP]\n> \n> **作者是谁？**\n> \n> 三位共同第一作者 Brian J Chan、Chao-Ting Chen、Jui-Hung Cheng 来自国立政治大学资讯科学系，通讯作者黄瀚萱（Hen-Hsen Huang）来自中研院资讯科学研究所。论文发表于 WWW Companion '25，本文依 arXiv v2 阅读，代码公开在 [**GitHub**](https://github.com/hhhuang/CAG)。\n\n\n## 定义问题\n\n把整个知识库贴进提示词，这件事本身没有什么新意。问题在于代价。\n\n假设我们的知识库有 85,000 个 token。每来一个问题，就把这 85,000 个 token 加上问题一起送进模型。模型得从第一个 token 开始读，读完整份数据，才能开始写答案的第一个字。\n\n下一个问题进来，数据一个字都没变，模型还是得从头再读一次。\n\n这段「读提示词」的过程叫做 **prefill**。提示词越长，prefill 越久，用户等第一个字出现的时间（TTFT，time to first token）也越长。\n\n所以作者真正要处理的问题是：\n\n- **数据不变，能不能只读一次？**\n\n要回答这个问题，得先知道模型「读完」之后，手上到底留下了什么。\n\n## 解决问题\n\n### 模型读完后留下什么？\n\n现在的语言模型大多是 decoder-only 的 Transformer。每一层的自注意力，会把每个 token 投影成 Query、Key、Value 三组向量。\n\n产生下一个 token 时，新的 Query 要和**前面所有 token 的 Key** 计算相似度，再把**前面所有 token 的 Value** 加权起来。\n\n这里有个关键：前面那些 token 的 Key 和 Value，一旦算出来就不会再变。\n\n因此实现上会把它们存起来，下一步直接拿来用，这就是 **KV cache**。平常模型逐字生成时，本来就靠它避免重算。\n\n\n> [!TIP]\n> \n> **为什么前面的 Key 和 Value 不会变？**\n> \n> 因为 decoder 使用因果掩码（causal mask），每个 token 只能看自己和前面的 token。后面接上新的文本，不会回头改变前面 token 的计算结果。\n> \n> 反过来说，如果前面的文本改了一个字，后面所有 token 的 Key 和 Value 都得重算。这点后面讲限制时会再回来。\n\n\nCAG（Cache-Augmented Generation，缓存增强生成）的想法，就是把这件事从「同一题内重用」扩大成「所有题目共用」：\n\n- **先让模型把整份知识库读一遍，把产生的 KV cache 存起来；之后每一题只要接着读问题就好。**\n\n下图是论文对两种流程的比较，粉红色是每次推理时才会动到的部分：\n\n[![RAG 与 CAG 的流程比较：RAG 在推理时经过 IR Model 检索知识；CAG 则离线预载成 Context Cache，推理时只处理问题](/content/papers/assets/1ccb714a2b9e40fe07cb712c508763c4447c40b66c78a8c364a030957c65a789.png)](/content/papers/assets/1ccb714a2b9e40fe07cb712c508763c4447c40b66c78a8c364a030957c65a789.png)\n\n*原论文 Figure 1，取自 [**v2 PDF 第 2 页**](https://arxiv.org/pdf/2412.15605v2#page=2)。上半为 RAG，下半为 CAG；粉红色组件在即时推理时运作。*\n\n上半部的 RAG，每一题都要经过 IR Model（检索模型）去知识库找数据，再把找到的 Context 和问题 $q$ 一起交给 LLM。\n\n下半部的 CAG 把知识库通过「Offline Preloading」变成 Context Cache。推理时只有问题 $q$ 是粉红色，检索器整个不见了。\n\n### 三个步骤\n\n作者把流程写成三步。\n\n**第一步：预载知识**。把文档集合 $\\mathcal{D}=\\{d_1,d_2,\\dots\\}$ 整理好，让模型读一次，得到 KV cache：\n\n$$\nC_{KV}=\\text{KV-Encode}(\\mathcal{D})\n$$\n\n这份缓存可以存在内存，也可以写到硬盘。读文档的成本只付一次，不管后面来多少问题。\n\n**第二步：推理**。加载 $C_{KV}$，接上用户的问题 $q$：\n\n$$\nr=\\mathcal{M}(\\mathcal{D}\\oplus q)=\\mathcal{M}(q\\mid C_{KV})\n$$\n\n等号左边是「把文档和问题串起来给模型」，右边是「模型带着缓存，只读问题」。两者的计算结果相同，差在文档那一段不用重读。\n\n**第三步：重置缓存**。回答第一题时，问题和答案的 token 也会被追加进 KV cache。第二题进来前，得把这些多出来的部分砍掉，否则第二题会看到第一题的对话。\n\nKV cache 是只往后追加的，所以只要记住知识库读完时的长度，把后面多出来的位置截掉就好，不用从硬盘重新加载。\n\n### 拿实际尺寸算一次\n\n光讲「把 KV cache 存起来」有点抽象，我们用论文使用的 Llama 3.1 8B 实际算一下这份缓存长什么样子。\n\n按 [**Llama 3 论文**](https://arxiv.org/abs/2407.21783) Table 3，8B 模型有 32 层，隐藏维度 4096，32 个 attention head，其中 Key/Value 只有 8 个 head（grouped-query attention），每个 head 是 $4096/32=128$ 维。\n\n所以，每一层、每个 token 要存的 Key 张量形状是：\n\n$$\n[\\text{batch},\\ \\text{kv\\_heads},\\ \\text{seq\\_len},\\ \\text{head\\_dim}]=[1,\\ 8,\\ 1,\\ 128]\n$$\n\nValue 也一样。一个 token 在所有层要存的数字有：\n\n$$\n2\\ (\\text{K 和 V})\\times 32\\ (\\text{层})\\times 8\\ (\\text{head})\\times 128\\ (\\text{维})=65{,}536\n$$\n\n若每个数字用 16 比特（2 bytes）存，一个 token 就是 128 KiB。\n\n论文最大的 HotPotQA 测试组约有 85k token，乘下去：\n\n$$\n85{,}000\\times 128\\ \\text{KiB}\\approx 11.1\\ \\text{GB}\n$$\n\n模型权重本身以 16 比特存放约 16 GB，这份缓存已经接近它的七成。\n\n\n> [!NOTE]\n> \n> 这是依模型设置的估算，假设缓存以 16 比特存放；论文没有报告实际的缓存大小与内存用量。官方代码默认以 `torch.float16` 加载模型，另有 4-bit 量化选项。\n\n\n有了形状，官方代码里的重置步骤就很好懂了：\n\n```python\ndef clean_up(kv: DynamicCache, origin_len: int):\n    \"\"\"\n    Truncate the KV Cache to the original length.\n    \"\"\"\n    for i in range(len(kv.key_cache)):\n        kv.key_cache[i] = kv.key_cache[i][:, :, :origin_len, :]\n        kv.value_cache[i] = kv.value_cache[i][:, :, :origin_len, :]\n```\n\n*取自 [**hhhuang/CAG**](https://github.com/hhhuang/CAG/blob/5c0d8ed6e28f34c72bb7208068f55cb3147d0c8c/kvcache.py#L120-L126) 的 `kvcache.py`，MIT 授权。*\n\n`kv.key_cache[i]` 是第 $i$ 层的 Key，形状 `[1, 8, seq_len, 128]`。`[:, :, :origin_len, :]` 沿着第三个维度，也就是序列长度，只保留知识库那一段。\n\n`origin_len` 在预载完成时记下：知识库读完后缓存有多长，之后每一题开始前就切回这个长度。整个「重置」就是 32 层、每层两次的切片。\n\n### 为什么要整包放？\n\n读到这里，你可能会想：既然缓存这么好用，为什么不把每份文档**各自**算好缓存，检索到哪几份就拼哪几份？\n\n问题出在刚才那个提示框。每个 token 的 Key 和 Value，取决于它前面所有的文本，也取决于它在序列中的位置。文档 B 单独计算时，它不知道前面接的是文档 A；位置编号也从头开始。直接拼起来，和真的让模型连着读 A、B 的结果不一样。\n\n作者在前言提到，TurboRAG 预先计算各段落的 KV cache，就得额外重排位置 ID 才能正常运作。CAG 的做法比较粗暴：**知识库固定，就整份一起读**。检索不见了，拼接的问题也一起不见了。\n\n代价是，知识库里任何一个字改了，那个位置之后的缓存都得重算。\n\n\n> [!TIP]\n> \n> **这不就是 prompt caching 吗？**\n> \n> GitHub 上也有人[**问过类似的问题**](https://github.com/hhhuang/CAG/issues/18)：把整份文档放进 system prompt，结果不是一样吗？\n> \n> 项目贡献者 brian030128 的回复是：输出确实相同，差别在于提示词转成 KV cache 是很吃运算的步骤；CAG 把它预先算好，省掉的是 TTFT。之后也有人追问，这和 API 供应商提供的 context caching 有什么不同。\n> \n> 就机制来说，两者非常接近。现在 [**Claude 的 prompt caching**](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) 和 [**Gemini 的 context caching**](https://ai.google.dev/gemini-api/docs/caching) 都能重用重复的长提示词。这篇论文的重点，比较是把「整份知识库预载加缓存」和 RAG 放在同一组问答题上比较。\n\n\n## 讨论\n\n### 实验怎么比？\n\n作者用两个问答数据集：\n\n- **SQuAD 1.0**：答案是文章某个段落里的一段文本，偏向单段定位。\n- **HotPotQA**：答案需要结合多份文档，属于多跳（multi-hop）推理。\n\n为了看知识库变大时会发生什么事，每个数据集各切出小、中、大三组。以 HotPotQA small 为例，作者从文档集抽出 16 份文档，拼成一份长参考文本，测试题目只选和这 16 份文档相关的题目。\n\n| 数据集 | 组别 | 文档数 | token 数 | 题数 |\n| --- | --- | ---: | ---: | ---: |\n| HotPotQA | Small | 16 | 21k | 1,392 |\n| HotPotQA | Medium | 32 | 43k | 1,056 |\n| HotPotQA | Large | 64 | 85k | 1,344 |\n| SQuAD | Small | 3 | 21k | 500 |\n| SQuAD | Medium | 4 | 32k | 500 |\n| SQuAD | Large | 7 | 50k | 500 |\n\n*整理自原论文 Table 1。*\n\n所有系统都用 Llama 3.1 8B 回答，跑在 8 张 Tesla V100 32GB 上。对照组是两种用 LlamaIndex 建的 RAG：\n\n- **Sparse RAG**：用 BM25，靠关键字比对与词频排序。\n- **Dense RAG**：用 OpenAI 的 embedding 索引，靠语义相似度排序。\n\n两者都试了取回 top-1、3、5、10 段。\n\n评分用 **BERTScore**，比较模型答案和标准答案的语义相似程度。\n\n\n> [!TIP]\n> \n> **BERTScore 是什么？**\n> \n> 它把两段文本都丢进 BERT 类模型，取出每个 token 的向量，再逐一配对计算相似度。好处是「巴黎」和「法国首都巴黎」不会像逐字比对那样被判成完全不同。\n> \n> 但它衡量的是「像不像」，不是「对不对」。两个答案差一个数字，BERTScore 可能还是很高。读下面的表时，小数第三位的差距要保守看待。\n\n\n### 准确度：大部分领先，但不是全部\n\n[![原论文 Table 2：Sparse RAG、Dense RAG 在 top-1、3、5、10 与 CAG 于 HotPotQA、SQuAD 小中大三组的 BERTScore](/content/papers/assets/13f5f729ad0290d70a0afcceaf6ec535895cb1017a05906e8dcfa0a9e91cae31.png)](/content/papers/assets/13f5f729ad0290d70a0afcceaf6ec535895cb1017a05906e8dcfa0a9e91cae31.png)\n\n*原论文 Table 2，取自 [**v2 PDF 第 4 页**](https://arxiv.org/pdf/2412.15605v2#page=4)。左列 HotPotQA、右列 SQuAD，粗体为该组最高分。*\n\n先看每组最底下的「CAG (Ours)」那一行。\n\nSmall 和 Medium 两组，CAG 在两个数据集都拿下最高分。例如 HotPotQA small，CAG 是 0.7951，最好的 RAG 是 Sparse top-5 的 0.7676。\n\n再往下看 Large。SQuAD 那列，CAG 的 0.7734 仍然最高；但 HotPotQA 那列的粗体不在 CAG，而在 Sparse RAG top-5 的 **0.7535**。CAG 只有 0.7407，比 Sparse top-3 的 0.7463 低，和 Dense top-3 的 0.7409 几乎一样。\n\n作者在正文也写到，数据变大后 CAG 和 RAG 的差距缩小，并引用长上下文模型处理很长输入时会退化的研究。\n\n这里可以再多看一个细节。作者在前言引用 RULER 的评测，说 Llama 3.1 8B 的**有效上下文长度是 32K**；HotPotQA large 却有约 85k token，远超过这个范围。CAG 在这组落后，可能和这有关。不过论文没有拆开分析，这是我们读表时的推测。\n\n表上还有一个现象：六组里有五组，分数最高的 RAG 设置是 BM25，只有 SQuAD medium 由 Dense top-10 以 0.0009 小胜。作者自己的解读是，这两个数据集可能不够难，关键字比对就能找到大部分需要的信息。换句话说，这里的 RAG 对照组，并没有遇到语义检索才能解决的难题。\n\n### 速度：省下的是重读\n\n再看时间。\n\n[![原论文 Table 3：HotPotQA 小中大三组中，各 RAG 设置、CAG 与 In-Context Learning 的检索时间与生成时间](/content/papers/assets/fbdd7c78239b53372fb8d168a05e1cb9ffda9cfdc3c6301abb36dd2913389490.png)](/content/papers/assets/fbdd7c78239b53372fb8d168a05e1cb9ffda9cfdc3c6301abb36dd2913389490.png)\n\n*原论文 Table 3，取自 [**v2 PDF 第 4 页**](https://arxiv.org/pdf/2412.15605v2#page=4)。单位为秒；Retrieval 为检索时间，Generation 为生成时间。*\n\n表中多了一个对照：**In-Context Learning**。它把整份参考文本和问题直接送进模型，每题都即时计算 KV cache，也就是我们在「定义问题」那节说的「每题都从头读一遍」。\n\n这一行和 CAG 的比较，才是预载缓存真正省下的东西：\n\n| HotPotQA 组别 | 每题重读 | CAG | 倍数 |\n| --- | ---: | ---: | ---: |\n| Small（21k） | 9.32 秒 | 0.85 秒 | 约 11 倍 |\n| Medium（43k） | 26.37 秒 | 1.41 秒 | 约 19 倍 |\n| Large（85k） | 92.08 秒 | 2.26 秒 | 约 41 倍 |\n\n知识库越大，重读越贵，预载的效益也越明显。\n\n但如果拿 CAG 和 Sparse RAG 比，事情就不一样了。Large 组的 BM25 top-3 检索只花 0.0008 秒，生成 0.67 秒，总共不到一秒；CAG 是 2.26 秒。BM25 只把三段短文本交给模型，模型要处理的上下文本来就短。\n\n而且 CAG 的时间也随着知识库变大而增加，从 0.85 秒、1.41 秒到 2.26 秒。缓存省掉了重读，但每生成一个字，注意力仍然要扫过整份 85k 的缓存。\n\n还有一个表上没有的数字：**创建缓存要多久**。Table 3 只计算预载后的单题推理时间。官方代码会记录预载花的时间，但论文没有报告。对一个每天更新的知识库来说，这个数字很重要。\n\n### 这篇没有回答的事\n\n作者在 Limitations 一节写得很直接：这个方法需要把所有相关文档放进模型的上下文，适合小公司的内部知识库、FAQ、客服中心这类规模可控的场景；数据集大很多时就不实际。\n\n从前面的推导，还可以多列几个工程上会遇到的情况：\n\n1. **文档常常更新**。改动位置之后的缓存都得重算，论文没有量测更新成本。\n2. **不同用户能看的文档不同**。一份缓存包含整个知识库；若权限不同，可能得替不同的文档组合各存一份。这是本文的推断，论文没有讨论。\n3. **缓存绑定模型**。KV cache 是某个模型、某个版本算出来的中间状态，换模型就得重建。\n\n## 结论\n\n这篇论文提出的 CAG，做法可以浓缩成一句话：**知识库固定且放得进上下文时，先让模型读一次，把 KV cache 存起来，之后每题只读问题。**\n\n在论文的实验设置里：\n\n- **和每题重读相比，速度差很多**。HotPotQA large 组从 92.08 秒降到 2.26 秒，约 41 倍。\n- **准确度大多与 RAG 相当或更好**。Small、Medium 两组 CAG 都最高；Large 组的 SQuAD 仍最高，HotPotQA 则输给 BM25 top-5。\n- **和轻量的 BM25 相比，CAG 不一定比较快**。只取三段短文本的 RAG，总时间不到一秒。\n\n这些结果来自单一 8B 模型、最多约 85k token 的知识库，以及 BERTScore 这个语义相似度指标。\n\n标题说「不要做 RAG」，正文给的答案其实更温和：知识库小、稳定、会被反复查询时，可以考虑不检索，直接背起来。\n\n至于知识库大到背不下的时候，还是乖乖去查吧。\n","imageSizes":{"/content/papers/assets/1ccb714a2b9e40fe07cb712c508763c4447c40b66c78a8c364a030957c65a789.png":{"width":694,"height":575},"/content/papers/assets/13f5f729ad0290d70a0afcceaf6ec535895cb1017a05906e8dcfa0a9e91cae31.png":{"width":694,"height":1028},"/content/papers/assets/fbdd7c78239b53372fb8d168a05e1cb9ffda9cfdc3c6301abb36dd2913389490.png":{"width":689,"height":717}}}
