加载中...

泠落の小屋
首页项目归档照片墙音乐杂谈工具箱关于
文章

Day4

2026-08-09 21:53
更新于 2026-08-09 22:12
# 记录

Day4

RAG 基础 + 进阶 + 多轮 + SSE

目标:跑通最小 RAG 链路,理解检索增强生成全流程; 提升 RAG 质量,把 RAG 接进 agent,支持多轮对话和流式输出。



RAG 链路(不分语言):

文档加载 → 切分(chunking) → 向量化 → 存向量库 → 检索 → 重排序 → 生成



  • 理解 chunk size / overlap 对检索质量的影响。RAG 优化 + 多轮(不分语言)

  • 检索质量优化:query 改写、多 query 检索、重排序(rerank)

  • 多轮对话上下文融合:历史消息拼进 prompt

  • 把 RAG 作为一个 tool 注册给 agent(agent 自主决定查不查知识库) SSE 流式输出 · 二选一

Python 路线 Node 路线
流式 FastAPI StreamingResponse + openai stream NestJS SSE(@Sse 装饰器) / Vercel AI SDK streamText





Embark AI 知识库

Image

文档加载:把 PDF、Word、Markdown、网页等不同格式的文档解析成纯文本。这一步解决的是"资料进得来"的问题。

切分(chunking):文档太长,没法整篇向量化/塞进 prompt,所以要按段落或语义切成块。块(chunk)就是检索和回答的最小单元,这一步直接影响后面的质量——块太大,信息混杂;块太小,语义被切碎。

向量化(embedding):把每个文本块用 embedding 模型转成一个向量(一串几百维的数字),让语义相近的文本向量距离更近。这是"机器能理解语义"的基础,和关键词匹配是两回事。

存向量库:把"向量 + 原文 + 元数据(标题、来源、位置)"存起来。最小实现甚至不需要专门的向量数据库,内存数组或 SQLite 表都行,检索时全量算一遍相似度即可。

检索(retrieval):把用户问题也转成向量,在库里找最相似的 top-k 个块。最小版本就是"余弦相似度 + 取前几名"。

重排序(rerank):召回是"广撒网",top-k 里可能混着不太相关的块;重排序用更强的模型或规则再精排一次。最小链路可以跳过这一步,属于质量优化。

生成(generation):把检索到的块拼成一个带引用编号的上下文,连同用户问题一起发给 LLM,让它"只能基于检索结果回答",并标注 [ref_1] 之类的引用来源。







RAG 是什么?

一句话:RAG(Retrieval-Augmented Generation,检索增强生成)

就是"先查资料、再写答案"——回答前先从外部知识库检索相关内容,拼进 prompt 让 LLM 基于资料作答。

为什么需要 RAG(三大痛点):

  • 幻觉:LLM 不知道的会编。RAG 提供事实依据,要求"没查到就说没查到"。

  • 知识时效性:训练数据有截止日期,RAG 的知识库可随时更新。

  • 私有/领域知识:公司内部文档不可能进训练集,RAG 是"外挂记忆"。

流程(索引 + 检索生成两阶段):

索引阶段:文档加载 → 切分 → 向量化 → 存向量库
推理阶段:query 向量化 → 检索 top-k → (rerank)→ 拼 prompt → 生成

与微调的对比(常考):

演进:Naive RAG(基础五步)→ Advanced RAG(query 改写、混合检索、rerank、上下文压缩)→ Modular RAG(检索/记忆/路由等模块自由组合)。

面试追问:RAG 检索不到怎么办?答:阈值兜底(minScore)、明确告知无依据、触发重写 query 或换检索策略。你项目里 applyMinScoreThreshold 的 fallback top1 就是典型实现。


Chunking 怎么做?

一句话:把长文档按语义或结构切成固定大小的文本块,块是"向量化的输入单位 + 检索的最小粒度 + 拼 prompt 的基本单元"。

四种常见策略:

三个关键参数:

  • chunk size:影响 embedding 语义质量。太小→语义割裂、上下文不足;太大→语义被稀释、噪声多、占上下文预算。经验值几百到一千 token,按内容定。

  • overlap:相邻块重叠一部分,防止跨块上下文丢失。代价是重复内容占预算、召回重复——所以检索后要做去重(MMR)和拼 prompt 时去重叠。

  • 切分边界:优先在段落、句号、标题处切,比硬切质量高很多。

进阶技巧(面试加分):父子块(parent-child)——用小块检索、把父块拼进上下文;邻近块扩展——命中块补前后文。你项目里 context-expander 就是后者。

你项目里的实现:semantic-splitter.ts 用 TextTiling 山谷检测(局部相似度极小值 + 谷深阈值),失败回退 text-splitter.ts 的段落优先机械切分(默认 2000 字符 / 200 重叠,表格感知)。

面试追问:chunk 太大/太小分别有什么影响?

答:太大 embedding 平均化、检索命中不精准、prompt 塞不下;太小检索准但缺上下文,要靠合并/扩展补救。


向量检索原理?

一句话:把文本映射成向量(embedding),用向量距离衡量语义相似度,检索就是"查与 query 向量最接近的 top-k 个向量"。

核心步骤:

文本 → embedding 模型 → 向量(如 1024 维)
query → 同模型 → query 向量
相似度 = cosine(query_vec, chunk_vec)   → 取 top-k

相似度度量:

  • 余弦相似度:cos(θ) = A·B / (|A||B|),只看方向不看长度,最常用。归一化后 = 内积。

  • 内积:适合已归一化的向量,速度快。

  • 欧氏距离:越小越相似,注意与余弦方向相反。

为什么 embedding 能懂语义:训练目标让"语义相近的文本向量距离近",所以"苹果"和"iPhone"的向量比"苹果"和"香蕉"更近,能解决关键词匹配解决不了的同义词问题。

暴力检索 vs ANN(常考):

  • 暴力检索:全量算一遍相似度,精确但 O(N),小数据量(几万以内)完全够用。你项目就是 SQLite 存向量 + 内存暴力扫描。

  • ANN(近似最近邻):牺牲一点精度换速度。常用算法:

    • HNSW:多层图结构,上层稀疏图"跳远",下层密集图精查,导航式搜索,工业界最常用。

    • IVF:先聚类,只在最近的几个簇里搜,倒排索引思想。

    • PQ:向量压缩成短码,省内存。

    • LSH:局部敏感哈希,相似项落在同一桶。

为什么还要混合检索(高频追问):

向量检索对同义词好,但对专有名词、精确 ID、新词、文件名不敏感;

BM25 关键词检索恰好互补。

融合用 RRF(Reciprocal Rank Fusion):score = Σ 1/(k + rank),只依赖排名不依赖分数,避免不同检索器分数不可比。

你项目 RRF k=60,融合向量 + BM25 + 精确词三路。



面试追问:向量检索缺点?

答:冷启动(新词没语义)、依赖 embedding 质量、维度灾难、ANN 召回可能漏(近似)、存储和计算成本。


Transformer 基础?

Transformer 是"全注意力"架构——不递归、不卷积,用自注意力直接建模序列里任意两个位置的关系,可并行计算,是 GPT/LLaMA 等大模型的基础。

核心公式:

Attention(Q, K, V) = softmax(QKᵀ / √d_k) · V
  • Q(Query):我要找什么;K(Key):我有什么;V(Value):实际内容。

  • QKᵀ 算两两相似度 → softmax 归一化成权重 → 加权求和 V。

  • 除以 √d_k 是缩放:维度大时点积数值会很大,softmax 进入饱和区、梯度消失,缩放保证分布温和。

关键组件:

Mask 的作用:Decoder 自回归生成时只能用"当前及之前"的 token,用 causal mask 把未来位置遮住;BERT 训练用 mask language model 遮住随机词。

并行与复杂度:

  • 注意力是矩阵乘法,训练可并行(这是它取代 RNN 的核心原因);RNN 必须串行。

  • 复杂度 O(n²),序列越长越贵,所以有大模型长度限制和稀疏注意力/FlashAttention 优化。

  • 推理时自回归生成仍需逐步来,但用 KV Cache 缓存历史 K/V,避免每步重算,这是流式输出快的工程基础。

面试追问:

  • Transformer vs RNN:并行 vs 串行、长距离依赖(注意力一步到位,RNN 要逐步传递)vs 梯度衰减。

  • GPT 为什么只用 Decoder?答:Decoder 单向注意力天然适合"根据前文预测下一个 token"的自回归生成。

  • 现在主流位置编码?答:RoPE(旋转位置编码),把位置信息编码进 Q/K 的相对角度,外推性好,LLaMA 系列在用。


SSE 细节?

一句话:SSE(Server-Sent Events,服务器推送事件)是基于 HTTP 的单向流式通信,服务端把数据按固定格式一块块推给客户端,天然适合 LLM token 流式输出。

协议格式(Content-Type: text/event-stream):

event: token
data: {"text": "你好"}
id: 42
retry: 3000

event: done
data: {"ok": true}
  • 事件之间用空行分隔。

  • data: 可以有多行,会被拼接成一条(中间用换行)。

  • event: 命名事件类型;不写则默认事件名 message。

  • id: + retry: 用于断线重连:客户端带 Last-Event-ID 续传,retry 指定重连间隔(毫秒)。

  • 服务端要定期发心跳(注释行 : ping 或空事件)防止中间代理超时掐断连接。

关键特性:

  • 单向:服务器 → 客户端。客户端要发消息用普通 HTTP 请求(这也是 AI 场景够用的原因)。

  • 基于 HTTP,兼容性好:普通负载均衡、代理、防火墙都能过;HTTP/2 下多个 SSE 共享一条连接。

  • 浏览器两种消费方式:

    • EventSource:自动重连、自动处理 Last-Event-ID,但只支持 GET(发不了 body)。

    • fetch + ReadableStream:可 POST、可带 body/headers(API Key 放 Header),但要自己解析、自己重连。LLM 应用几乎都用这种。

  • 只能传文本(UTF-8),传 JSON 需自定格式。

实现要点与坑(流式输出必考细节):

  1. 响应头:Content-Type: text/event-stream; charset=utf-8、Cache-Control: no-cache、Connection: keep-alive,有些环境还要 X-Accel-Buffering: no(Nginx)和关掉代理缓冲,否则 token 被攒住不实时。

  2. 编码:TextEncoder 把 event: xxx\ndata: JSON\n\n 编码进 ReadableStream。

  3. 解析:按 \n\n 切事件,读 event: 和 data: 行,JSON.parse 数据。注意 TCP 粘包,缓冲区要留尾巴。

  4. 中断:客户端 AbortController.abort() → 服务端 request.signal 触发,停止生成、关流。

  5. 结束:发 event: done 后关流;LLM 侧收到 [DONE] 表示完成。

  6. 中文:确保 charset=utf-8,解码用 TextDecoder 且要处理流式边界({stream: true})。

你项目里的实现:createChatSseEmitter 统一编码 event: <name>\ndata: <JSON>\n\n;事件有 meta / status / trace / rag-summary / citations / token / knowledge-files / error / done;前端 readSseStream 按 \n\n 切分解析,token 进打字队列,AbortController 支持停止。


SSE vs WebSocket?

一句话:SSE 是"HTTP 上的单向流",WebSocket 是"独立协议上的全双工长连接";LLM 流式输出选 SSE,双向实时交互选 WebSocket。

对比表(背这个就够):

怎么选(面试答法):

  • 需求是"服务器持续推、客户端偶尔发一下"→ SSE:更简单、可自动重连、兼容性好。LLM 对话就是典型:用户发一条请求,服务端推几百个 token。

  • 需求是"双方高频互推、有二进制数据、低延迟双向"→ WebSocket:如在线聊天、游戏、白板协作。

  • 纯 LLM 流式输出几乎不用 WebSocket:多一次协议复杂度,却没有收益——token 本来就是单向的,重发、续传这些 SSE 还更好。聊天产品里用 WS,通常是因为消息收发本身也要走同一条双向通道。

面试追问:SSE 能发 POST 吗?答:EventSource 不行,但 fetch 流式读取可以,所以 AI 项目用 fetch 方案。WebSocket 丢包/断线怎么办?答:自己实现心跳(ping/pong)与重连、消息带序号做去重和补拉。


一句话总结串讲(面试可直接背)

RAG 解决 LLM 幻觉和私有知识问题,核心是"索引 + 检索 + 生成";chunking 决定检索质量,向量检索用余弦相似度找语义近邻,Transformer 用自注意力并行建模长距离依赖;LLM 生成是自回归逐 token 输出,所以用基于 HTTP 的单向 SSE 流式推送最合适——简单、可重连、免代理配置,而 WebSocket 留给需要双向实时通信的场景。

需要的话,我可以把这 6 题整理成一页面试速记卡(含每题的 30 秒口头答法),方便你考前快速过。

avatar

泠落

街喧闹,人过往,且记曾相识,不为少年留

Recent Records

定心猿而驯木母

2026-08-06 02:34

你好,世界

2026-08-05 16:58

The first memo

2026-08-05 02:59

Table of Contents