这是孔子角色扮演问答系统开发笔记的一部分。在完成 Embedding、向量检索、分块和数据库选型后,我们把焦点转向更核心的问题:如何科学地评价检索系统好不好?调参有什么可复用的经验?以及,Transformer 究竟是怎么"读懂"一句话的?
项目背景:基于 DeepSeek 大模型的孔子对话应用,后端用 BGE 嵌入 + ChromaDB 检索论语章句。
一、RAG 评估与调优:用指标代替感觉
1.1 前提:一个标注过的测试集
要量化评估检索质量,首先得有几组"标准答案"。例如:
text
查询“什么是仁” → 预期相关:[学而篇_0, 颜渊篇_0, 颜渊篇_1, ...]
查询“怎么学习” → 预期相关:[学而篇_0, 为政篇_10, ...]
...
有了这样的标注集,才能算指标。目前本项目还没有(仅测试了功能"不崩溃"),这在学习阶段是正常的,也是下一步要补齐的事情。
1.2 两个核心指标
Hit Rate@K(命中率)
在返回的 Top-K 个结果里,至少有一条是相关的,算一次命中。
-
10 个查询中,8 个命中 → Hit Rate@5 = 80%
-
回答的问题是:"东西捞到了没有?"
-
它对 K 的大小敏感:K 越大,命中率自然越高,但同时输入 LLM 的 token 成本也越高。
-
只看"有/没有",不关心排在第几位。
MRR(Mean Reciprocal Rank,平均倒数排名)
第一个相关的结果排在第几位?取它的倒数,再对所有查询取平均。
公式:MRR = 平均(1 / 第一个相关结果的排名)
举个例子:
-
问题 1:第一个相关结果排在第 1 位 → 1/1 = 1.0
-
问题 2:排在第 3 位 → 1/3 = 0.33
-
问题 3:完全没命中 → 0
-
MRR = (1.0 + 0.33 + 0) / 3 ≈ 0.44
MRR 回答的问题是:"捞到之后,最好的那个排在最前面吗?" 它对排序精度极其敏感------排第一和排第五,分数差 5 倍。
Hit Rate 90% 但 MRR 只有 0.42,翻译成人话就是:"东西确实在袋子里,但每次都得翻到最底下才能摸到。"
1.3 两个指标的互补关系
| Hit Rate | MRR | |
|---|---|---|
| 问什么 | 有没有捞到? | 第一个命中的排第几? |
| 低了怎么办 | 加大 K / 混合检索 / 优化分块 | 加 Reranker / 混合检索 / 调相似度权重 |
| 局限 | 不管排序质量 | 只看第一个命中,后续的全部忽略 |
两个指标要一起看。只追求 Hit Rate 可能把 K 调得巨大,往 LLM 里塞一堆无关上下文;只追求 MRR 可能在"确定性高"的查询上过度优化,忽略那些召回本身就困难的边缘问题。
1.4 改进路线图
-
混合检索(向量 + 关键词)
这是解决"白话问句 vs 古文章句"语义鸿沟的直接手段。比如用户问"仁是个什么玩意儿",向量检索可能效果一般,但"仁"这个字可以用传统的倒排索引精确匹配。RRF(Reciprocal Rank Fusion)把两套排序结果加权融合,往往能同时提升 Hit Rate 和 MRR。
-
重排序(Reranker)
先用向量检索粗筛 Top-20(快速),再用一个更精确的 Reranker 模型精排 Top-5(慢但准)。比喻很形象:HR 初筛 100 份简历,技术主管再从中精挑 5 份进面试。
1.5 项目现状与下一步
当前项目在 test_edge_cases.py 第 204 行只验证了 assert len(results) == 3------测的是功能不崩,不测检索质量。检索链路也是纯向量检索,没有混合检索,没有 Reranker。下一步可以手动标注 10 个问题及预期论语章句,写个脚本把 Hit Rate 和 MRR 跑起来,从此调优有据可依。
二、实践:调 Top-K、调温度、对比效果
2.1 当前参数总览
| 参数 | 当前值 | 位置 |
|---|---|---|
| RAG Top-K | 5 | chat.py 第 68、95 行 |
| 闲聊 temperature | 1.1 | client.py 第 49 行 |
| RAG temperature | 0.8 | chat.py 第 80、108 行 |
| 意图分类 temperature | 0.0 | chat.py 第 35 行 |
2.2 Top-K 怎么调
-
增大(5→10):召回更多上下文,Hit Rate 提升,但 token 消耗增加,且 LLM 可能"每条引一点",把回答答成流水账。
-
减小(5→3):更聚焦,但有可能漏掉最相关的那条。
-
原则:不要一次性翻倍,阶段性调整(3→6→8),每个档位验证效果。如果 K 已经调到 8 以上效果仍然不理想,根因通常不在 K,而要回头检查分块策略或 Embedding 质量。
2.3 Temperature 怎么调
RAG Temperature 控制回答在多大程度上忠于检索到的原文。
-
降低(0.8→0.3):回答更加忠实于论语原文,但也可能变成复读机,丧失解释力。
-
升高(0.8→1.2):解释力更强,语气更生动,但也可能脱离原文开始"编造"论语内容。
-
保守区间 0.8-1.0,生动区间 1.1-1.3,不建议超过 1.5。
闲聊 Temperature 是另一个维度。古文角色扮演需要在"灵活"和"守规矩"之间找平衡,1.1 是当前项目的经验值,并非绝对标准。
2.4 常见问题速查
| 用户抱怨 | 可能原因 | 先试什么 |
|---|---|---|
| 回复和问题不搭 | Top-K 太小,最相关条文没进来 | 加大 Top-K |
| 照抄原文,没有解释 | Temperature 太低 | 提高 temperature |
| 编造论语原文 | Temperature 太高 / 上下文里混进了不相关条文 | 降低 temperature,同时检查检索质量 |
| 又死板又离谱,同时出现 | 两个参数都不在最优区 | 组合调:提高 temperature + 加入 top_p 来裁掉低概率噪音 |
2.5 验证闭环
调参最忌讳"改了以后感觉好像好了一点"。一个简单的验证闭环:
-
准备 5-10 个覆盖不同主题的测试问题。
-
改参前跑一遍,人工记录回复质量(是否引用原文、是否有解释、是否贴合问题)。
-
改参后用同一组问题再跑一遍,对比。
-
如果效果不变------问题就不在参数上,需要回头检查分块或检索链路。
三、Transformer 与注意力机制:词语如何"看见"彼此
3.1 问题的起点:为什么同一个"仁"字向量不同?
BGE 和 LLM 底层都是 Transformer 架构。它们真正强大的地方在于,同一个字在不同句子里,最终的向量表示完全不同。"杏仁"和"仁爱"的"仁",在 Transformer 眼里不是同一个东西。这是怎么做到的?核心就在注意力机制。
3.2 注意力:词与词之间互相"看"
处理一句话时,每个词都要做一件事:看看周围的词,谁对我理解当前这句话有帮助?
-
"杏仁好吃" → "仁"发现旁边是"杏"和"好吃" → 我是"杏仁"的仁
-
"仁者爱人也" → "仁"发现旁边是"者"和"爱人" → 我是"仁爱"的仁
这种"互相看"的机制,被抽象成三个矩阵:Q、K、V。
3.3 Q、K、V:三个关键角色
| 角色 | 比喻 | |
|---|---|---|
| Q(Query,查询) | "我想知道谁对我重要?" | 举手提问的学生 |
| K(Key,键) | "我这里有什么信息?" | 每个学生胸前的标签 |
| V(Value,值) | "我的实际含义是什么?" | 每个学生手里的讲义 |
以"什么是仁"为例,整个过程分四步:
-
每个词通过训练好的矩阵乘法,生成自己的 Q、K、V。
-
"仁"的 Q₃ 去和所有词的 K 做点积,算出匹配分数:Q₃·K₁("什么")=0.85,Q₃·K₂("是")=0.10,Q₃·K₃("仁")=0.05。
-
用这些匹配分数作为权重,对所有词的 V 做加权求和:新的"仁" = 0.85×V₁ + 0.10×V₂ + 0.05×V₃。
-
新的"仁"向量融入了"什么"的信息------它知道自己正在被提问。
注意:每个词都会这样更新一次,但侧重不同。最终每个词都带上整句话的语境。
3.4 多头注意力:12 个角度同时看
BGE 用的是 12 头注意力。这意味着同一层里,有 12 组独立的 Q、K、V,从 12 个不同角度观察句子:
-
一个头关注语法关联(哪个词在修饰我?)
-
一个头关注情感色彩(褒还是贬?)
-
一个头关注条件限定(经常?偶尔?)
12 个头的输出最后拼接在一起,经过线性变换融合成一个更丰富的新向量。
一个很好的比喻:房间里装了 12 个不同角度的摄像头。单一角度看可能被遮挡或误解,12 个画面一合成,全貌就清晰了。另一个是"盲人摸象"的比喻------每个头只摸到象的一部分(鼻子、腿、身体),但合成起来就能大致还原大象的外形。
3.5 12 层堆叠:从字面到语义的层层抽象
| 层 | 理解深度 | "学而时习之"的处理 |
|---|---|---|
| Layer 1 | 直接邻居 | "学"发现"而"在身旁 |
| Layer 2 | 视野扩大 | "学"通过"而"间接看到"时" |
| Layer 4 | 短语理解 | 识别出"学习并时常复习" |
| Layer 8 | 语义抽象 | 判断"学"是动词,不是"学校" |
| Layer 12 | 完整语境 | 每个词的向量都已包含整句话的信息 |
12 层 Transformer 就像 12 轮讨论会。从字面到短语再到语义,每一层都让词的表示更加抽象和完整。继续用盲人摸象的比喻:多层堆叠就是不断抽象的过程------Layer 1 发现"这个动物很大,鼻子很长,会喷气",到 Layer N 抽象出"体型庞大,有长鼻子,皮肤粗糙",最终概括为"大象"。
3.6 为什么"学而时习之"和"温故而知新"向量接近?
经过 12 层注意力之后:
-
"学而时习之"里,"学"融入了"习"和"时","习"也融入了"学"和"时",整体语义向"复习 + 经常"收敛。
-
"温故而知新"里,"温"融入了"故"和"知新","故"融入了"温"和"知新",整体语义向"复习 + 新知"收敛。
两个句向量都含有很强的"复习"语义方向,所以在向量空间中夹角很小,余弦相似度高。但"经常"和"知新"的侧重点不同,又保证它们不会完全重合------相似但不同一。
最后一步 Mean Pooling 把所有词的最终向量取平均,得到代表整个句子的 1024 维向量。这个平均不是原始词向量的平均,而是被语境更新了 12 轮的词向量的平均。
3.7 与项目的关系
在我们项目里,这套 Q-K-V 机制被用在两处:
-
BGE 模型 (
embedder.py):12 层 Transformer,用注意力把句子编码成 1024 维向量。 -
DeepSeek 大模型 (
client.py):远更深的 Transformer(几十层),同样用 Q-K-V 注意力机制,但任务是预测下一个 token------一个负责编码语义,一个负责生成文本。原理同源,用途各异。
以上三个主题,从 RAG 的评估指标到 Top-K 与 temperature 的调参实战,再到 Transformer 内部那套精妙的注意力机制,构成了 LLM 应用开发者必须穿透的三个层次:衡量效果、控制行为、理解模型。希望这份整理也能为你的项目提供一些清晰而可落地的参考。