理解现代 LLM 系统 · 从 RAG 到全景
03
CHAPTER 03

第三章 · 深入专题:四个最核心的技术机制

3.1 Embedding(嵌入):机器怎么"理解"语言的意思

先讲为什么需要它:计算机本质上只会算数字,不懂"猫"和"狗"这两个字有什么关系。但如果我们能把每个词、每句话都变成一串数字(比如1024个数字组成的一个"坐标"),并且让"意思相近的词句"在这串数字所代表的空间里"离得近"、"意思不相关的词句"离得远,那么计算机就能通过算两个坐标之间的距离,间接判断出"这两句话讲的是不是同一件事"。这串数字就叫做"嵌入向量"(embedding vector),这个转换过程就叫"嵌入"。

一个比喻:想象一个超大的图书馆,馆员不是按书名字母排序,而是把主题相近的书摆在相邻的书架上——讲猫的书和讲狗的书因为都属于"宠物"大类,会摆得比较近;讲猫的书和讲汽车维修的书,会摆得很远。你想找"跟这本书类似的书",只需要看它旁边摆了什么。嵌入做的就是这件事,只不过它不是把书摆在三维的书架上,而是摆在一个有几百上千个维度的抽象空间里(人脑没法直接想象"1024维空间"长什么样子,但数学上完全可以计算)。

这串数字是怎么"学"出来的:不是有人手动规定"猫=[0.2, 0.8, ...]",而是通过一种叫"对比学习"(contrastive learning)的训练方法——给模型看大量"这两句话意思相关"和"这两句话意思无关"的例子,每次模型判断错了就微调一下,让"相关的"在空间里拉得更近、"无关的"推得更远。这样重复几百万次之后,模型就自己"摸索"出一套坐标系统,使得"离得近=意思相近"这个规律成立。没有任何一个维度是人特意设计成"代表某个具体含义"的——意思是分散在全部维度共同组成的"位置"里的,就像一个人的性格不是由单独一项特质决定,而是由很多特质共同构成的整体印象。

具体技术流程(分两步)

  1. 分词(Tokenizer):先把文字切成模型能处理的小单位,叫"token"。要注意 token 不是"词"也不是"字",而是按出现频率划分的片段:常用算法(BPE、SentencePiece)先把文字拆到最小单位,再反复把"最常一起出现的一对"合并,直到凑够一个词表(通常 5 万到 15 万个片段)。所以高频词可能整个是一个 token,生僻词则被拆成几个子词片段。

这样设计有个好处:再罕见、再新造的词,也总能拆成已知的子词甚至单个字节来表示,不会出现"这个词不认识、无法处理"的情况——这是早期分词方法的一个常见失败点。

还有一点对成本很实际:同样一段话,不同语言占用的 token 数差别很大。英文大约 4 个字符算一个 token,中文通常一到两个字就是一个 token。由于模型的收费和"一次能处理多长"都按 token 数算而非字数,同样篇幅的中文内容,占用的 token 往往比英文多。 2. 编码(Transformer 神经网络):把切好的token序列,喂进一个叫"transformer"的神经网络,它会给每个token算出一个向量,然后通过"池化"(把很多token的向量合并压缩成一个代表整句话的向量)和"归一化"(把向量的长度统一调整为1,方便后面用固定方法比较相似度),最终得到一个能代表整句话意思的向量。这个向量有多少个数字(即"维度"),2026年常见的范围是384到4096,通用场景一般从768或1024起步,再根据检索效果和存储成本调整。

补充:早期的嵌入模型多用 BERT 这类"编码器"结构,而目前效果最好的一批(如 NV-Embed、E5-mistral、GritLM)改为以大语言模型为起点,在其上加装池化层并做对比微调。两条路线产出的都是句子向量,用法完全一样,读者不必深究区别。

一个必须知道的限制:嵌入模型一次能读的文字有上限。 每个嵌入模型都规定了单次输入的最长token数,超过的部分会被直接截掉,而且不会有任何报错提示——一段被切得过大的文本,后半截可能根本没有进入向量,检索时自然永远找不到。这正是第一章"离线索引"要先把长文档切块的硬性原因之一:切块的大小必须落在所用嵌入模型的长度上限之内。

一个容易踩坑但很重要的规则:你事先把资料库里的文档转成向量存起来(这叫"索引"),跟用户提问时把问题转成向量(这叫"查询"),必须用同一个嵌入模型来做。原因很简单:不同模型"学出来的坐标系统"是不一样的,就像两份地图如果用了不同的比例尺和不同的坐标原点,你没法直接拿A地图上的坐标去B地图上找对应位置。所以如果要换一个嵌入模型,不能只换查询那一端,必须把整个数据库的向量重新算一遍(叫 re-embed),而且库里的向量和查询向量维度必须一致。

上面说"维度必须一致",指的是存进库里的那些向量和提问时算出来的向量,长度必须一样——不然根本没法拿来比较。但这不等于维度本身是固定死的。现在的主流模型普遍支持一种叫 Matryoshka(套娃) 的技术:同一个模型算出的长向量,可以直接砍掉后面一截来用,不必重新训练——比如把1024维只保留前256维,检索效果通常只下降百分之几,存储和检索成本却大幅降低。想这样省成本完全可以,只要存进库里的和提问时算出的都砍到同样的长度就行。

3.2 Retrieval(检索):怎么把该找的都找出来

第一章说过,真实系统里的检索是一个"先宽后窄的漏斗"。这一节讲清楚这个漏斗为什么要这样设计——每一层各自弥补上一层的什么缺陷。

先说底层能力:向量检索能做什么

3.1 讲过,意思相近的文字,向量会离得近。所以"检索"最基础的做法就是:把问题也转成向量,去库里找位置最接近的若干条。至于"接近"具体怎么算,工程上通常得到一个 0 到 1 之间的分数,越接近 1 表示意思越贴近——你在系统里看到的 "score 0.82" 就是这个分数。原理上它算的是两个向量的夹角,方向越一致分数越高,细节不必深究。

这个做法最大的好处是不依赖字面。用户问"怎么把老数据搬到新版本",文档里写的是"历史记录迁移方案",两句话没有一个词相同,向量检索照样能对上。这正是它相对传统关键词搜索的根本优势。

第一层缺陷:数据量一大,逐条比对就慢得无法接受

如果库里有一千万条向量,每次提问都要跟全部比一遍再排序,实时问答根本扛不住。解决办法是 ANN(近似最近邻):不追求百分之百找到最相似的那几条,而是靠预先建好的索引结构,只比对其中一小部分,用极小的精度损失换来几百倍的速度。

这个"精度"有个标准的衡量方式,叫 recall@k(前 k 条召回率):真正最相似的前 k 条里,实际被找回来了几条。它是一个可以调节的旋钮,而不是固定档位——而且越往高处代价越陡:从 0.8 调到 0.95,延迟大约增加三成,尚可接受;从 0.95 再往 0.99 推,延迟可能涨到三到五倍。多数生产系统会停在 0.95 附近。

具体的索引结构主要有三种,选哪个取决于数据规模:

  • HNSW(分层可导航小世界图):把所有向量预先连成一张"邻居关系图",查询时像玩"六度分隔"游戏,从一个起点出发,每次跳到离目标更近的邻居,几步就能逼近目标区域。2026 年多数生产系统的默认选择,召回率高、支持随时新增数据。它唯一的硬限制是整张图必须放进内存,因此单机实用上限大约在一到两亿条向量。
  • IVF(倒排文件索引):先用 k-means 把所有向量粗略分成若干个群,查询时只在最相关的几个群里细找。内存占用比 HNSW 小,但同等条件下召回率也更低,适合数据量大、内容基本不变、内存吃紧的场景。
  • DiskANN:把完整索引放在固态硬盘上,内存里只留压缩后的向量和导航结构。十亿级数据量下的标准答案——单节点可做到十亿条向量、95% 召回、5 毫秒延迟,内存占用比纯内存方案低约九成。

这些方法的实现可见于 FAISS(一个开源的向量检索函数库)以及各类商用向量数据库。

第二层缺陷:向量检索会漏掉"必须一字不差"的东西

向量的长处是理解语义,短处也正在于此——它对字面不敏感。用户问"X-200 型号支持不支持这个功能",向量检索很可能找回一堆讲产品规格的段落,却漏掉真正写着 X-200 那一行。产品型号、错误代码、人名、法条编号、版本号这类内容,差一个字符就是另一回事,语义相似度对它们几乎无能为力。

所以生产系统通常同时跑两路检索:向量检索负责"意思对得上",关键词检索(业界常用的算法叫 BM25)负责"字面对得上",再把两边的结果合并。这就是混合检索(hybrid search),也是第一章说的"两条路一起找"。合并时因为两套分数不在同一个尺度上,需要一个换算规则,常见做法叫 RRF(倒数排名融合)——不比较分数本身,只看每条结果在各自榜单里排第几,排名靠前的综合得分就高。

第三层缺陷:找回来的东西,用户未必都有权限看

检索结果直接进 prompt 是危险的:不同用户能看的文档不同,客户不该看到内部版本的文档,外部合作伙伴不该看到内部定价明细。所以在切块入库时,每一块都要带上它来自哪份文档、那份文档谁能看(即 ACL 权限信息),检索时按提问者的身份实时过滤。同理还有时效性过滤——已作废的旧版本文档不该被翻出来当依据。

这一步在工程上比听起来棘手:先过滤再搜索会破坏 ANN 索引的结构(那张邻居关系图是按全量数据建的,抽掉一部分点,路就断了);先搜索再过滤则可能出现搜回来 50 条、过滤完只剩 2 条甚至一条不剩的情况。成熟的向量数据库为此提供了专门的过滤检索机制,但这仍是实际部署中最容易出性能问题的环节之一。

第四层:粗筛出来的几十条,还要再精选

前面几层的目标是"别漏"——宁可多拿回一些。但塞进 prompt 的资料不能太多:既有长度限制,也因为无关内容会干扰模型判断。所以最后需要一轮重排(rerank):用一个更精细但更慢的模型,把粗筛出的几十条逐条与问题比对,重新打分排序,只留最切题的几条。

重排模型和嵌入模型的工作方式不同:嵌入模型是把问题和文档各自转成向量再比距离,快但粗糙;重排模型是把问题和文档放在一起读一遍再打分,慢但准得多。正因为慢,它只能用在候选已被缩小到几十条之后——这也解释了整个漏斗为什么必须是"先宽后窄":先用快而粗的方法把范围从百万缩到几十,再用慢而准的方法从几十里挑出几条。

3.3 LLM Serving(推理服务):模型怎么同时应付很多人

前面两节讲的是"资料怎么找出来",这一节讲资料交给模型之后的事。注意这里的关键词是服务(serving)——重点不只是模型内部怎么算,而是一台服务器要同时应付成百上千个用户时,是怎么组织的。

一次推理分两步

模型生成文字的方式叫自回归:一次只吐出一个 token,把它接到已有文字后面,再基于"到目前为止的全部文字"猜下一个,直到猜出表示结束的符号。

由此产生两个阶段,运算特性正好相反:

  • Prefill(预填充):把整段输入(系统指令 + 检索到的资料 + 用户问题)一次性并行读完,为其中每个 token 都算好两样东西(K 和 V,下面解释),存进一块叫 KV cache 的显存暂存区。这一步吃的是算力。
  • Decode(解码):从第一个字开始,逐个、串行地生成。每写一个新字,都要回头参考前面全部内容。这一步吃的是内存读取带宽——瓶颈不在算得快不快,而在能多快把 KV cache 读进来。

KV cache 为什么必须存在:decode 每写一个字都要回看前文,如果每次都把前面所有字重新算一遍,写到第 100 个字时就要重算前 99 个字——这种重复计算随文字变长呈平方级膨胀,长文本根本没法用。把算过的结果存下来复用,计算量就降到接近线性。用显存换掉重复计算,这就是 KV cache 的全部意义。

那么 K 和 V 是什么?模型处理每个字时会算出三个向量:Q(这个字带着的问题:"前面哪些字跟我相关")、K(每个字举的牌子,写着"我是讲什么的")、V(这个字真正携带的信息)。当前字拿自己的 Q 去和前面每个字的 K 比对,谁匹配度高就给谁更高权重,再把各字的 V 按权重加权平均——这就是"注意力"。之所以只缓存 K 和 V 不缓存 Q,是因为 K 和 V 会被后面每一个新字反复用到,值得存;而 Q 只在当下这一步用一次,用完即弃。

衡量快慢的两个指标

这两个阶段各自对应一个业界通用指标,看懂它们就能读懂大部分推理性能讨论:

  • TTFT(Time To First Token,首字延迟):从发出请求到看到第一个字的时间,由 prefill 决定。
  • TPOT / ITL(每个输出 token 的间隔):开始输出后,字与字之间的节奏,由 decode 决定。

值得注意的是,总等待时间通常由 decode 主导。一个 500 字左右的回答,若每字间隔 80 毫秒,光 decode 就要花掉约 40 秒;相比之下首字延迟可能只有两百毫秒。所以"回答越长越慢"是线性累加的,感受非常直接。

服务器怎么同时应付很多人:连续批处理

如果服务器一次只处理一个请求,处理完再接下一个,GPU 大部分时间都在空转——因为 decode 阶段算力用不满,浪费严重。

现代推理框架(vLLM、SGLang 等)采用的做法叫连续批处理(continuous batching):每一轮迭代,让当前所有活跃请求各自前进一个 token,然后进入下一轮;有请求结束就退出批次,有新请求进来就随时加入,不必等整批做完。这是把 GPU 喂饱的关键,也是它能同时服务几十上百个用户的原因。

这个机制还解释了一个大家都遇到过的现象:回答写到一半突然卡顿几百毫秒。因为新请求的 prefill 挤进批次时,需要占用大量算力,正在逐字输出的请求只能等它算完——用户看到的就是文字流突然停住。

几条实用的成本规律

  • 输入越长,prefill 成本上升得比想象中快。注意力计算的复杂度是输入长度的平方级,所以参考资料从 2000 字加到 4000 字,这部分开销不是翻倍,而是接近四倍。这是"塞更多资料进 prompt"要付的隐性代价。
  • 答案越长,decode 时间线性增加,且它通常主导用户的总等待时间。
  • KV cache 很占显存,并且随着上下文变长而增长——它是决定一台机器能同时服务多少人的主要限制。为此业界发展出了 PagedAttention 这类优化:像操作系统管理内存分页那样管理 KV cache,避免大块显存被闲置浪费,开源推理框架 vLLM 用的就是这个方法。

一个针对 prefill 的重要优化:提示缓存(prompt caching)。 RAG 系统的每次请求,开头往往是完全相同的——同一段系统指令,有时还有同一份长文档。既然内容一样,算出来的 KV cache 也一样,那就没必要每次重算。所以云端服务商普遍提供跨请求的前缀缓存:把这部分 KV cache 保留一段时间(通常几分钟到几小时),后续请求命中相同开头时直接复用,省下的正是 prefill 那笔平方级的开销。

这个机制有一个容易被忽略的含义:KV cache 并不像通常以为的那样"算完即丢、只存在于当次请求"。开启缓存后,一部分内容会在服务商的基础设施里短暂驻留——第五章讨论合规时会再回到这一点。

3.4 谁跟谁说话:一次提问里系统各部件怎么分工

核心规则:整个系统里,只有"后端/编排层"会主动发起动作。向量数据库、模型服务这些部件都是被动的——有人来问才回应,而且它们彼此之间从不直接对话

用第一章的秘书处来说,后端就是主办这件事的那位秘书。去哪个档案室调卷、调不到怎么办、材料够不够、要不要再补一轮、稿子能不能上报——每一个决定都是他做的,出了岔子也是他担责。档案室只负责"有人来调就给",笔杆子只负责"接到材料就写",两边从不直接打交道,甚至不知道对方存在。所以后端不是个传话筒,而是这件事唯一的负责人。

完整的通信步骤

  1. 前端 → 后端(对应第一章 ❶ 接收问题):用户的问题被送到后端。
  2. 后端 → 嵌入服务(对应 ❷ 嵌入理解):把问题文字送过去,请它转换成查询向量。
  3. 后端 → 向量数据库(对应 ❸ 检索的前半段):拿查询向量做相似度检索,同时按提问者的身份做权限过滤(ACL,Access Control List,访问控制列表),取回最相关的若干条(叫 top-k)。3.2 讲的关键词检索通常也在这一步完成——多数向量数据库自带这个能力,一次调用就能拿回两路结果并合并;只有当关键词索引是另一套独立系统(比如单独部署的 Elasticsearch)时,这里才会变成两次调用,再由后端把两边结果合并。
  4. 后端 → 重排服务(对应 ❸ 检索的后半段):把粗筛出的几十条连同问题一起送过去,请它逐条打分排序,只留最切题的几条。重排模型和嵌入模型同属一类——都是跑在 GPU 上的独立模型服务,可以自建,也可以调用现成的 API。
  5. 后端(自己内部完成,不调用任何人)(对应 ❹ 增强):把系统指令、检索到的原文、对话历史、用户问题拼成完整的 Prompt。
  6. 后端 → 生成模型服务(对应 ❺ 生成):把完整 Prompt 送过去,请它生成答案。
  7. 后端 → 前端(对应 ❻ 守门与 ❼ 回应引用):一边接收模型吐出的文字一边检查,通过的部分连同引用来源一起推送给界面。
  8. 后端 → 日志系统(对应 ❽ 记录):把这次问答的全过程写一份记录留底。

两套"八步"为什么对不齐:第一章按"做了什么动作"划分,这一节按"谁给谁发消息"划分,两种分法本来就不是一一对应的。❸ 检索是一个动作,却要发出两到三次通信;❻ 守门和 ❼ 回应引用是两个动作,却共用同一次回传。同一个系统,换个角度看,切分方式自然不同。

关于第 7 步:答案是"流"过去的,不是等写完才"传"过去的

3.3 讲过模型是逐字生成的。后端并不会等整段答案生成完再回传,而是一边接收一边往前端推,这就是文字一个个蹦出来的原因(技术上通常用 SSE 或 WebSocket 实现)。

这带来一个真实的工程矛盾:流式输出和守门是冲突的。字都已经推到用户屏幕上了,怎么还能"检查完再放行"?实践中的折中办法是边推边检测,一旦发现问题立即中断,把已显示的内容撤回或替换掉。这也是为什么有时会看到 AI 的回答写到一半突然消失,变成一句"抱歉,我无法回答这个问题"。

四个值得记住的重点

  1. 各个下游部件彼此互不知道对方存在。向量数据库不知道有模型服务,重排服务不知道有日志系统——是后端把前一个部件的产出,翻译成下一个部件看得懂的格式再递过去。部件越多这一点越明显:上面八步里后端要打交道的对象有六个,而它们相互之间的连线是零。

  2. 一切判断和把关只能发生在后端。只有它看得到全貌,而且它处在公司自己能控制的范围内(模型服务很可能是别家公司的)。这就是为什么合规和安全审查逻辑必须放在这一层,第五章会详细展开。

  3. 后端还负责所有的"出岔子怎么办"。模型服务限流了要不要排队重试、检索超时了是降级回答还是直接报错、某一步失败后回退到哪里——这些判断只有后端做得了。真实系统里,处理异常的代码往往比处理正常流程的还多,这也是"负责人"和"传话筒"的区别所在。

  4. 代理式 RAG 并没有改变这张图(第四章详细讲)。它只是让后端把第 2 到第 4 步多跑几轮,而且轮数由模型临场决定。分工结构、谁能跟谁说话,一点没变。


理解现代 LLM 系统 · 从 RAG 到全景  —  通俗扩充版 · 简体中文