diff --git a/notes/大模型应用.md b/notes/大模型应用.md
index 381e9a27..65cd936f 100644
--- a/notes/大模型应用.md
+++ b/notes/大模型应用.md
@@ -85,6 +85,9 @@
* [A28 Spring AI 的对话记忆与工具调用](#a28-spring-ai-的对话记忆与工具调用)
* [A29 HNSW 的距离计算次数与知识库规模](#a29-hnsw-的距离计算次数与知识库规模)
* [A30 混合检索与重排的召回率](#a30-混合检索与重排的召回率)
+ * [A31 注意力分数的缩放](#a31-注意力分数的缩放)
+ * [A32 语义缓存的相似度](#a32-语义缓存的相似度)
+ * [A33 模型生成 SQL 的权限限制](#a33-模型生成-sql-的权限限制)
* [参考资料](#参考资料)
@@ -132,7 +135,9 @@ Token 数决定了两件事:
2. 向量依次经过几十个结构相同的层,Qwen2.5-7B 有 28 层。每层先用注意力从前文收集信息,再用前馈网络(Feed-Forward Network, FFN)对每个位置单独加工;
3. 最后一个位置的向量经过线性层,得到词表中每个 Token 的分数(logits),再经过 softmax 转为概率。
-每个子层都带有残差连接和归一化。残差连接把子层的输入直接加到输出上,使几十层的网络也能稳定训练;归一化把每个向量的数值缩放到稳定的范围。这里用的是 LayerNorm 一类的方法,只用一个 Token 自己向量的各个维度计算统计量,而不像 BatchNorm 那样依赖同一批中的其它样本,所以不受批大小和序列长度影响,训练和推理时的计算也相同。最初的 Transformer 在残差相加之后做归一化,目前的大模型大多改为在每个子层之前做(Pre-Norm),残差的主干上不再经过归一化,几十层叠加时训练更稳定,并使用只按均方根缩放、计算更简单的 RMSNorm,Qwen2 系列就是这样。
+每个子层都带有残差连接和归一化。残差连接把子层的输入直接加到输出上,使几十层的网络也能稳定训练;归一化把每个向量的数值缩放到稳定的范围。这里用的是 LayerNorm 一类的方法,只用一个 Token 自己向量的各个维度计算统计量,而不像 BatchNorm 那样依赖同一批中的其它样本,所以不受批大小和序列长度影响,训练和推理时的计算也相同。
+
+最初的 Transformer 在残差相加之后做归一化(Post-Norm),目前的大模型大多改为在每个子层之前做(Pre-Norm),残差的主干上不再经过归一化,最后一层之后再做一次归一化。这样训练开始时各层的梯度更平稳,可以不用或少用学习率预热。这些模型还使用计算更简单的 RMSNorm,Qwen2 系列就是这样。RMSNorm 的论文认为 LayerNorm 起作用的主要是缩放,而不是减去均值,所以只把向量除以均方根 √(mean(x²) + ε) 再乘以增益,少算一次均值,论文的实验中效果与 LayerNorm 相当。
三种结构的注意力有两处不同。编码器中每个 Token 都能看到全部 Token;解码器的自注意力带有因果掩码,只能看到前面的 Token,见下文的注意力机制。编码器-解码器结构的解码器每层还多一个交叉注意力(Cross-Attention):Q 来自解码器,K 和 V 来自编码器最后一层的输出,生成每个词时都可以查阅整句原文。
@@ -146,7 +151,7 @@ Transformer 取代循环神经网络(Recurrent Neural Network, RNN)和 LSTM
-- 前馈网络先把向量从 3584 维放大到 18944 维,再压回 3584 维,三个矩阵占了全部参数的 75%。Qwen2.5 的前馈网络使用 SwiGLU:gate_proj 的输出经过激活函数后与 up_proj 的输出逐元素相乘,再交给 down_proj;
+- 前馈网络先把向量从 3584 维放大到 18944 维,再压回 3584 维,三个矩阵占了全部参数的 75%。Qwen2.5 的前馈网络使用 SwiGLU,即 down_proj(SiLU(gate_proj(x)) ⊙ up_proj(x)):gate_proj 的输出经过激活函数后与 up_proj 的输出逐元素相乘,再交给 down_proj。它比原始的前馈网络多一个矩阵,SwiGLU 的论文和 LLaMA 把中间维度取为 4d 的 2/3,使参数量不变;Qwen2.5-7B 的 18944 是 3584 的约 5.29 倍,参数量约为原始前馈网络的两倍;
- 注意力的 k_proj、v_proj 只有 q_proj 的七分之一,这是因为 28 个查询头共享 4 组 K、V,见下文的 GQA;
- 加起来共 7,615,616,512 个参数,与模型权重文件中记录的大小(15,231,233,024 字节,每个参数 2 字节)一致(见[附录 A16](#a16-qwen25-7b-与-qwen3-30b-a3b-的参数量))。
@@ -166,7 +171,9 @@ MoE 与参数量相同的 Dense 模型相比:
- 计算量取决于激活的参数,生成一个 Token 的计算量接近 3B 的 Dense 模型;
- 显存取决于总参数,所有专家都要加载,Qwen3-30B-A3B 的 BF16 权重约 61 GB,是 Qwen2.5-7B 的 4 倍;
-- 训练时需要负载均衡:路由器可能总是选中少数几个专家,其它专家得不到训练。Switch Transformer 等模型在损失函数中加一项辅助损失,各专家处理的 Token 数越不均匀,这一项越大;DeepSeek-V3 改为给每个专家的路由分数加一个只用于选择专家的偏置,某个专家负载过高时调低它的偏置,辅助损失只保留一项权重极小的,防止单个序列内极度不均衡。
+- 训练时需要负载均衡:路由器可能总是选中少数几个专家,其它专家得不到训练;专家分布在多张显卡上时,负载最重的专家还会拖慢整层的计算。Switch Transformer 等模型在损失函数中加一项辅助损失,各专家处理的 Token 数越不均匀,这一项越大;DeepSeek-V3 改为给每个专家的路由分数加一个只用于选择专家的偏置,某个专家负载过高时调低它的偏置,辅助损失只保留一项权重极小的,防止单个序列内极度不均衡。
+
+辅助损失通常在微批次(micro-batch)内计算,一个微批次只有很少几个序列,近似于要求每个序列的 Token 都均匀分给所有专家,代码这类领域集中的序列也不例外。Qwen3 仍用辅助损失,但在整个全局批次上统计各专家的负载。DeepSeek-V3 和 Qwen3 都主要在整个批次上约束均衡,减轻了对单个序列的要求,有利于专家按领域分工。
### 注意力机制
@@ -190,6 +197,8 @@ MoE 与参数量相同的 Dense 模型相比:
除以 √d 是因为:当向量的各个分量相互独立、均值为 0、方差为 1 时,点积的方差等于 d。维度越大分数之间的差距越大,softmax 的结果几乎全部集中到一个 Token 上,梯度接近 0,训练变得困难。除以 √d 之后,点积的方差恢复为 1。
+例如按 128 维的随机向量、128 个 Token 计算,不缩放时分数的标准差约为 11,约三成的情况下最大的权重超过 0.99;除以 √d 后一次也没有出现。如果改为除以 d,分数的方差变为 1/d,本例中标准差只剩约 0.09,权重接近均匀(numpy 模拟,见[附录 A31](#a31-注意力分数的缩放))。这是按随机初始化时的假设推出的,训练之后的 Q、K 并不满足这个假设。
+
softmax 把任意分数变成非负、和为 1 的权重,处处可导,指数又拉大了高分与低分的差距。分数不直接用余弦相似度,是因为它的范围只有 −1 到 1,拉不开差距:上下文有 1000 个 Token 时,即使一个 Token 的余弦为 1、其余都是 −1,它的权重也只有 0.7%,要用余弦就得再乘一个较大的系数;点积则保留了向量长度,模型可以靠长度表达匹配的强弱。
生成文本时,一个 Token 只能看到它自己和前面的 Token,这称为因果掩码(Causal Mask)。实现方法是把“后面的 Token”对应的分数设为负无穷,softmax 之后权重为 0,即图中分数矩阵的右上角。训练时整段文本一起输入,不遮住后文的话,预测下一个 Token 时就能直接看到答案。
@@ -204,7 +213,7 @@ softmax 把任意分数变成非负、和为 1 的权重,处处可导,指数
#### 1. 多头注意力
-单头注意力中,每个 Token 只得到一组权重,只能按一种标准挑选信息。多头注意力(Multi-Head Attention, MHA)把 Q、K、V 都切成 h 份,每份独立计算注意力,再把各头的结果拼接起来,乘以输出矩阵 WO 混合在一起。不同的头可以关注不同的关系,例如一个头寻找指代的对象,另一个头关注相邻的词。
+单头注意力中,每个 Token 只得到一组权重,只能按一种标准挑选信息。多头注意力(Multi-Head Attention, MHA)把 Q、K、V 都切成 h 份,每份独立计算注意力,再把各头的结果拼接起来,乘以输出矩阵 WO 混合在一起。不同的头可以关注不同的关系,例如一个头寻找指代的对象,另一个头关注相邻的词。头数也不是越多越好:Transformer 论文在计算量不变时比较了不同的头数,单头的 BLEU 比 8 头低 0.9,32 头也比 8 头低。
以 Qwen2.5-7B 的查询为例,3584 维的向量分成 28 个头,每个头 128 维,形状的变化如下:
@@ -256,7 +265,7 @@ KV Cache 是用显存换计算。每个 Token 要在每一层缓存 K 和 V 两
- PagedAttention:推理框架 vLLM 把 KV Cache 切成固定大小的块按需分配,不再按最大长度预留连续的显存,相同前缀的请求还可以共享块,见[推理服务部署](#4-pagedattention);
- 量化:用 8 位甚至更少的位数存储 K、V。
-
+
GQA 减少的是缓存和读取的数据量,注意力的计算量基本不变:查询头仍是 28 个,每个头照样与全部的 K 计算分数。它能加快解码,是因为解码每一步都要从显存读出全部 KV Cache,速度受显存带宽限制(见[批处理](#1-批处理用延迟换吞吐))。一个 32K Token 的请求,GQA 每步读 1.75 GB,改用 MHA 要读 12.25 GB,接近权重本身。
@@ -277,6 +286,10 @@ GQA 减少的是缓存和读取的数据量,注意力的计算量基本不变
例如原始分布为 0.50、0.22、0.16、0.07、0.05,温度为 0.5 时最高的概率升到 0.75,温度为 1.5 时降到 0.40。Top-p 为 0.8 时,累加到第 3 个 Token 达到 0.88,后两个 Token 被排除,剩下的重新归一化。
+几个参数同时设置时,顺序也有影响:vLLM 和 Transformers 先除以温度,再做 Top-k、Top-p;llama.cpp 默认最后才应用温度。温度不改变排序,但会改变 Top-p 留下几个 Token,例如上面的分布在温度 0.5、Top-p 0.8 时,先做温度只留下 2 个(0.75 + 0.15),后做温度留下 3 个。
+
+请求中不传采样参数时,生效的往往是模型自带的 generation_config.json,例如 vLLM 默认读取它,Qwen2.5-7B-Instruct 中设的是温度 0.7、Top-p 0.8、Top-k 20。温度设为 0 时,vLLM 按贪心解码处理;Transformers 则要设置 do_sample=False。
+
抽取字段、生成代码、调用工具等需要稳定结果的场景使用较低的温度,写作类场景可以适当调高。
### 训练过程
@@ -303,13 +316,13 @@ SFT 之后还要做偏好对齐,是因为 SFT 只告诉模型“这个回答
奖励模型输入提示词和回答,输出一个分数,训练目标是让较好回答的分数高于较差回答,损失为 -log σ(rw - rl)。例如两个分数分别为 1.2 和 0.3,损失为 -log σ(0.9) ≈ 0.34,分差越大损失越小。
-PPO 更新模型时不直接使用奖励的高低,而是使用优势(Advantage):一个回答的实际得分比预期高多少。预期由价值模型(Critic)给出,它估计“从当前位置继续生成,平均能得多少分”。只看奖励时,简单的题目每个回答都得高分,模型分不清哪个回答真正更好;减去预期之后,比预期好的回答被强化,比预期差的被抑制。
+PPO 更新模型时不直接使用奖励的高低,而是使用优势(Advantage):一个回答的实际得分比预期高多少。预期由价值模型(Critic)给出,它估计“从当前位置继续生成,平均能得多少分”,InstructGPT 中价值模型从奖励模型初始化。只看奖励时,简单的题目每个回答都得高分,模型分不清哪个回答真正更好;减去预期之后,比预期好的回答被强化,比预期差的被抑制。
把生成过程看作强化学习时,奖励模型的分数在回答结束时才给出,每个 Token 的优势用广义优势估计(Generalized Advantage Estimation, GAE)由价值模型的逐步估计算出,计算方法和例子见[进阶:广义优势估计](大模型应用%20-%20进阶.md#1-广义优势估计)。
奖励模型只是人类偏好的近似,模型可能找到它的漏洞,生成得分高但没有意义的回答。所以 PPO 在奖励中减去新模型与参考模型(冻结的 SFT 模型)之间的 KL 散度,限制模型偏离太远。
-生成回答的代价很高,所以 PPO 用同一批回答更新多轮,而这批回答是更新前的旧模型生成的。用新旧模型生成同一个 Token 的概率之比给优势加权,就可以用旧模型的样本估计新模型的效果,这种方法称为重要性采样。比值离 1 越远,估计越不可靠,所以 PPO 把它截断在 [1 - ε, 1 + ε] 之内,ε 通常取 0.2:优势为正时比值超过 1 + ε、优势为负时比值低于 1 - ε,这个 Token 就不再提供梯度,避免一次更新的步子太大;朝相反方向偏离的部分不截断,仍然会被纠正。以旧概率为 0.20 的一个 Token 为例:
+生成回答的代价很高,所以 PPO 用同一批回答更新多次(拆成多个小批量,或者训练多轮),而这批回答是更新前的旧模型生成的。用新旧模型生成同一个 Token 的概率之比给优势加权,就可以用旧模型的样本估计新模型的效果,这种方法称为重要性采样。比值离 1 越远,估计越不可靠,所以 PPO 把它截断在 [1 - ε, 1 + ε] 之内,ε 通常取 0.2:优势为正时比值超过 1 + ε、优势为负时比值低于 1 - ε,这个 Token 就不再提供梯度,避免一次更新的步子太大;朝相反方向偏离的部分不截断,仍然会被纠正。以旧概率为 0.20 的一个 Token 为例:
@@ -331,7 +344,7 @@ loss = -log σ( β × [ (log π(y_w|x) - log π_ref(y_w|x)) - (log π(y_l|x) - l
训练开始时模型与参考模型相同,括号内为 0,损失是 log 2 ≈ 0.693。取 β = 0.1,如果训练后较好回答的对数概率比参考模型高了 2,较差回答的低了 1,括号内为 3,损失降为 -log σ(0.3) ≈ 0.554。DPO 在拉高较好回答概率的同时压低较差回答的概率,是一个二分类问题。
-参考模型和 β 起到了 PPO 中 KL 散度的作用,所以 DPO 不需要再单独加 KL 项。
+参考模型和 β 起到了 PPO 中 KL 散度的作用,所以 DPO 不需要再单独加 KL 项。需要注意的是,这个结论来自推导中的最优解。IPO 论文指出,偏好越接近确定,这个隐式的约束越弱;在论文只有两个回答的例子中,一方总是胜出时,最优解会把较差回答相对较好回答的概率比压向 0,β 取多大都拦不住;样本少时,经验上的偏好更容易呈现为一方总是胜出。IPO 等方法针对这一点修改了目标函数。
DPO 的损失只取决于两个回答的概率之差,需要注意的是:
@@ -346,11 +359,15 @@ DPO 的损失只取决于两个回答的概率之差,需要注意的是:
组相对策略优化(Group Relative Policy Optimization, GRPO)由 DeepSeekMath 提出,DeepSeek-R1 也使用了它。它去掉了价值模型:对同一个问题生成一组回答,用组内的平均分作为预期,比平均分高的回答得到正的优势,低的得到负的优势。
+去掉价值模型有两个原因:价值模型通常和策略模型一样大,显存和计算都要多一份;奖励只在回答结束时给出,很难训练出在每个 Token 上都准确的价值估计,推理模型的长思维链中前面的内容还可能被后面推翻,只看一部分回答更难预测最终得分。DeepSeek-R1 论文的对比实验中,PPO 把 GAE 的 λ 从常用的 0.95 调到 1.0 后,在 MATH 上的效果接近 GRPO,所以 GRPO 的好处主要是省资源、少调参。
+
例如对一道数学题生成 4 个回答,答对得 1 分、答错得 0 分,得分为 [1, 0, 0, 1],平均分 0.5、标准差 0.5,每个回答的优势 (得分 - 平均分) / 标准差 为 [1, -1, -1, 1],答对的回答被强化,答错的被抑制。
-如果 4 个回答全对或全错,优势都是 0,这道题对训练没有贡献,所以题目的难度要适中。数学、代码这类能自动判断对错的任务可以直接用规则打分,不需要奖励模型。
+如果 4 个回答全对或全错,优势都是 0,这道题对训练没有贡献,所以题目的难度要适中。数学、代码这类能自动判断对错的任务可以直接用规则打分,不需要奖励模型。DeepSeekMath 中的 GRPO 仍使用训练出的奖励模型,DeepSeek-R1 对推理任务改用规则打分,原因是神经网络的奖励模型在大规模强化学习中容易被模型钻空子(Reward Hacking),对通用的对话数据仍使用奖励模型。
-GRPO 把 KL 散度直接加在损失中,概率之比的限制与 PPO 相同。代价是每道题要生成一组回答,DeepSeekMath 中每题采样 64 个,生成的开销很大。
+GRPO 把 KL 散度直接加在损失中,概率之比的限制与 PPO 相同。PPO 把每个 Token 的 KL 惩罚加在奖励中,强化学习最大化的是累计奖励,回答越长累计的惩罚越多,DeepSeek-R1 的作者认为这可能抑制回答变长;GRPO 的 KL 项不进入优势的计算。R1 的训练中还每 400 步把参考模型换成最新的策略模型,让模型在长时间训练中能继续探索。
+
+代价是每道题要生成一组回答,DeepSeekMath 中每题采样 64 个,DeepSeek-R1 中每题 16 个,生成的开销很大。
大规模训练推理模型时,GRPO 还暴露出全对或全错的题没有梯度、熵坍塌、长度偏差和 MoE 模型训练不稳定等问题,DAPO、GSPO 等算法分别做了改进,见[进阶:GRPO 的改进](大模型应用%20-%20进阶.md#2-grpo-的改进)。
@@ -364,6 +381,8 @@ GRPO 把 KL 散度直接加在损失中,概率之比的限制与 PPO 相同。
三者的选择:DPO 实现简单、训练稳定,适合已有偏好数据的对齐;PPO 和 GRPO 在训练中生成新的回答,能探索出训练数据之外的更好回答,推理能力的训练通常使用 GRPO 这类强化学习方法。DPO 本身没有探索,只能在已有的偏好对之间学习,常见的补救是迭代 DPO:用训练后的模型重新生成回答、构造新的偏好对,再训练一轮。
+DPO 与 PPO 谁效果更好取决于实验设置:DPO 论文在 6B 以下的模型上报告与 PPO 相当或更好;Xu 等人把 PPO 调好之后,在他们的对话和代码实验中 PPO 都超过了 DPO,并指出 DPO 对偏好数据之外的回答缺少约束,可能学到偏向这类回答的策略。迭代 DPO 在他们的实验中缩小了一部分差距,但没有消除。
+
### 微调 🔥
微调指的是用特定任务的数据继续训练模型,最常见的形式是 SFT。它改变的主要是模型的行为:按什么格式回答、用什么语气、怎样完成某种专门的任务,而不是让模型记住新的知识。
@@ -386,7 +405,9 @@ RAG、微调和提示词解决的是不同的问题:
通常先尝试提示词,再考虑 RAG,最后才考虑微调。微调需要准备训练数据,基座模型升级后要重新训练,而且写入参数的知识难以更新,也无法给出来源。
-用微调灌输新知识的效果也不好。有实验表明,SFT 数据中包含模型预训练时不知道的事实时,这些样本学得明显更慢,而一旦被学会,模型编造答案的倾向随之增加。LIMA 只用 1000 条精选的样本微调 65B 的模型就得到了很好的对话效果,说明知识主要来自预训练,SFT 只需要少量数据教会模型回答的方式。
+用微调灌输新知识的效果也不好。有实验表明,SFT 数据中包含模型预训练时不知道的事实时,这些样本学得明显更慢,而一旦被学会,模型编造答案的倾向随之增加。在这项闭卷问答的实验中,负面影响主要出现在训练后期,表现为过拟合,按验证集提前停止训练、去掉模型不知道的样本,或者把它们的答案改为“不知道”,都能减轻。
+
+LIMA 只用 1000 条精选的样本微调 65B 的模型就得到了很好的对话效果,作者据此提出假说:知识和能力几乎都在预训练中学到,对齐主要教会模型回答时用什么格式。
#### 2. SFT 数据
@@ -483,6 +504,7 @@ LoRA 微调的效果不好时,按以下顺序检查:
- 数据:先抽查样本的质量,确认训练时的对话模板与推理时一致、只对回答计算损失,见上文的 SFT 数据;
- 学习率:LoRA 的最优学习率约为全参数微调的 10 倍,QLoRA 对 7B、13B 模型使用 2 × 10-4,33B、65B 减半,照搬全参数微调的学习率(例如 LIMA 用的 1 × 10-5)会学得过慢;
- 作用范围和秩:只作用在注意力上时改为全部线性层;数据量大、与预训练差异大时增大 r,仍然不够就改用全参数微调;
+- 批量:在一些设置下,批量增大后 LoRA 的损失比全参数微调上升得更多,增大 r 也不能缩小差距,批量较大时可以先试着减小批量;
- 过拟合:训练损失持续下降而验证集上变差,减少轮数,增加数据的多样性。
#### 5. 灾难性遗忘
@@ -499,6 +521,12 @@ LoRA 微调的效果不好时,按以下顺序检查:
量化(Quantization)指的是用更少的位数存储模型的参数,例如把 BF16(16 位)的权重转为 INT8 或 INT4。Qwen2.5-7B 的权重在 BF16 下约 15.2 GB,INT8 约 7.6 GB,INT4 约 3.8 GB(本节的 GB 按 109 字节计算,按 230 字节计算时 BF16 权重约 14.2 GB)。这是全部参数都按同样的位数折算的值。嵌入层和输出层通常不量化,Qwen2.5-7B 的这两层不共享权重,约 10.9 亿个参数,所以官方发布的 INT4(AWQ)模型权重实际约 5.6 GB。
+同样是 16 位,FP16 和 BF16 的分配不同:FP16 用 5 位指数、10 位尾数,最大只能表示 65504;BF16 用 8 位指数、7 位尾数,表示范围与 FP32 接近,精度更低。例如 70000 存成 FP16 会溢出为无穷大,存成 BF16 是 70144;0.1 存成 FP16 是 0.09998,存成 BF16 是 0.10010。用 FP16 训练要靠损失缩放(Loss Scaling)避免梯度过小变成 0,BF16 一般不需要,所以大模型多用 BF16。
+
+FP8 有两种编码:E4M3(4 位指数、3 位尾数)最大 448,E5M2(5 位指数、2 位尾数)最大 57344,0.1 分别存为 0.1016 和 0.0938。每个张量只用一个缩放系数时,通常权重和激活用 E4M3,梯度用 E5M2;缩放的粒度更细时,梯度也可以用 E4M3,例如 DeepSeek-V3 前向计算时激活按 1×128、权重按 128×128 分组缩放,以 FP8 计算的张量统一用 E4M3(arXiv 2412.19437)。
+
+在 NVIDIA 显卡上,FP8 的矩阵乘法需要 Ada、Hopper 及以后的架构;在 Turing、Ampere 上,vLLM 只把 FP8 当作权重的存储格式,计算时转回 16 位(W8A16)。
+
量化不仅省显存,还能加快生成。这是因为解码阶段每生成一个 Token,都要把全部权重从显存读一遍,而计算量很小,速度受显存带宽限制,权重越小读得越快。推理服务的整体显存怎样估算,见[显存:权重与 KV Cache](#3-显存权重与-kv-cache)。
#### 1. 怎样量化
@@ -544,6 +572,10 @@ KV Cache 也可以量化。Qwen2.5-7B 每个 Token 的 KV Cache 在 BF16 下占
量化都会损失一些精度,不同任务受到的影响不同,量化之后要在自己的任务上重新评测。
+评测时除了准确率,还要看答案有没有改变:准确率前后相近时,仍可能有一部分题由对变错、另一部分由错变对,Dutta 等人的实验中这个比例可达 13.6%,所以还要比较量化前后逐题的答案或输出分布的 KL 散度(arXiv 2407.09141)。
+
+长输入和非英语输入也要单独测。Mekala 等人在 5 个模型上的实验中,8 位量化(FP8、GPTQ-int8)的平均分只下降 0.2~0.8 个百分点;4 位方法下降更多,在长输入检索任务上通常随输入长度增加而加重,最多的是 Llama-3.1 70B 用 BNB-nf4 时,在 64K、128K 输入的检索任务上下降约 59 个百分点;部分 4 位方法在非英语输入上的下降也比英语大(arXiv 2505.20276)。
+
### 幻觉
#### 1. 什么是幻觉
@@ -567,11 +599,15 @@ KV Cache 也可以量化。Qwen2.5-7B 每个 Token 的 KV Cache 在 BF16 下占
有参考资料时(例如 RAG),检测的是忠实性:把回答拆成一条条结论,逐条检查能否在资料中找到依据,见[评测与优化](#评测与优化)中的忠实度。引用的原文是否真的出现在资料中、数字和实体是否与数据库一致,这些可以直接用代码检查,比让模型判断更可靠。
+但代码只能确认引用和数字确实存在,确认不了引用是否支持结论。例如回答“工作两年有 10 天年假”时引用了“已满 10 年不满 20 年的,年休假 10 天”,原文和数字都在资料中,结论却是错的,这一步仍要逐条判断。
+
RAG 的回答出现幻觉时,先看正确的资料是否在提示词中:不在时先查知识库有没有这份资料、检索有没有找到,同时查模型为什么没有回答不知道;在却答错是生成的问题。逐步排查见[排查顺序](#4-排查顺序)。测试用例要专门放入资料中没有答案、新旧版本冲突的问题,见[评测集](#3-评测集)。
没有参考资料时,可以对同一个问题多次采样再比较。模型真正知道的事实,多次回答的内容基本一致;编造的内容往往每次都不一样,例如问一个冷门开源库中某个函数的参数,三次可能给出三套不同的参数。
-用另一个模型判断回答是否有幻觉时,这个模型同样可能出错。让它只回答“这条结论是否被这段资料支持”这样范围很窄的问题,并用一批人工标注的样本测出它判断的准确率,才知道它的结果能否信任。
+这种方法需要额外多次采样(SelfCheckGPT 的实验中每个回答额外采样 20 次),而且它依赖多次回答之间的不一致,查不出始终一致的错误,例如模型因为训练数据中的常见误解而一贯答错的内容(arXiv 2303.08896;Farquhar 等,Nature 2024)。
+
+用另一个模型判断回答是否有幻觉时,这个模型同样可能出错。让它只回答“这条结论是否被这段资料支持”这样范围很窄的问题,并用一批人工标注的样本分别测出它漏掉幻觉和误报幻觉的比例,才知道它的结果能否信任,只看总的准确率会掩盖其中一种错误。
缓解方法:
@@ -609,8 +645,14 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
推理模型(Reasoning Model)在给出回答之前,会先生成一段思考过程,把复杂问题拆成多步解决。例如 DeepSeek-R1 通过大规模强化学习获得这种能力:对于数学、代码这类答案可以自动验证的题目,按最终答案是否正确给予奖励,模型在训练中自发地学会了更长的思考、检查和修正。
+小模型也可以通过蒸馏获得推理能力。经典的知识蒸馏让小模型(学生)拟合大模型(教师)在每个位置输出的概率分布,并提高温度使分布更平缓,让学生也学到次优选项之间的相对关系(arXiv 1503.02531)。
+
+也可以只用教师生成的文本训练学生,不需要教师的输出分布,这同样属于蒸馏。例如 DeepSeek-R1 的 Distill 系列只用 R1 参与整理的约 80 万条样本对 Qwen2.5、Llama 做 SFT,没有再做强化学习(arXiv 2501.12948)。
+
思考过程同样消耗 Token,更高的准确率是用更长的延迟和更高的成本换来的,简单问题没有必要使用推理模型。
+思考的 Token 按输出计费,界面上只显示思考的摘要或者不显示时,仍按完整的思考过程计费。接口一般用 effort(思考强度)调节思考的多少,以 Claude 为例,它只是引导,不是严格的预算;输出长度的上限(Claude 的 max_tokens、OpenAI Responses 的 max_output_tokens)才是硬上限,并且思考和正文共用这个上限,设得太小时正文可能还没写完就被截断。
+
## 二、提示词与上下文
### 提示词
@@ -637,7 +679,7 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
- 要求越具体越好,“简洁一些”不如“不超过 100 字”;
- 说明信息不足时怎么处理,否则模型倾向于编造;
-- 给出几组输入和期望的输出,称为少样本(Few-shot)提示,示例比文字描述更能说明格式;
+- 给出几组输入和期望的输出,称为少样本(Few-shot)提示,示例比文字描述更能说明格式。示例的选择和顺序会影响结果,例如分类任务中模型会偏向示例里占多数的类别和排在最后的类别,所以分类示例先保持类别均衡,再在评测集上比较不同的示例组合、顺序和数量,并不是越多越好;
- 让模型先写出推理步骤再给出答案,称为思维链(Chain of Thought, CoT),可以提高多步推理问题的准确率。
提示词改得好不好,要在评测集上比较:准备一批带期望结果的输入,每条运行多次,能用代码判断的用代码检查,其余交给人或模型裁判,见[评测](#2-评测)。例如[工具调用](#工具调用)一节的实验中,系统提示词写明先查订单、再查物流之后,7B 模型 10 次运行中答出包裹位置的次数从 0 变为 10。修改时从失败的样例入手,补上对应的规则或示例;APE 等自动优化方法由模型生成多个候选提示词,在评测集上打分后保留得分最高的。
@@ -646,7 +688,7 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
### 结构化输出
-程序需要解析模型的输出时,要求模型按照 JSON Schema 返回结果。只在提示词中要求输出 JSON 是不够的,模型仍可能多说一句解释、漏掉字段或者少写一个括号。
+程序需要解析模型的输出时,要求模型按照 JSON Schema 返回结果。JSON Schema 是用 JSON 描述数据格式的规范,写明有哪些字段、各自的类型、哪些必填以及取值范围,例如 {"type": "object", "properties": {"temp": {"type": "integer"}}, "required": ["temp"]} 要求输出一个带整数字段 temp 的对象。只在提示词中要求输出 JSON 是不够的,模型仍可能多说一句解释、漏掉字段或者少写一个括号。
#### 1. 约束解码
@@ -654,11 +696,13 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
-例如 Schema 规定 temp 是整数,已经生成了 `{"city": "杭州", "temp": ` 时,只有数字和负号是合法的,“晴”这类 Token 都会被屏蔽。vLLM 等推理框架和很多模型接口都支持这种方式,代价是 Schema 只能使用推理服务支持的子集。
+例如 Schema 规定 temp 是整数,已经生成了 `{"city": "杭州", "temp": ` 时,只有数字和负号是合法的,“晴”这类 Token 都会被屏蔽。vLLM 等推理框架和很多模型接口都支持这种方式,代价是 Schema 只能使用推理服务支持的子集。各家的子集不同,遇到不支持的关键字时,有的报错,有的直接忽略,例如 Gemini 不强制 minLength、maxLength,所以长度、数量这类约束仍要在本地校验。
需要注意的是,只保证输出是 JSON 的“JSON 模式”与约束解码不同。例如 DeepSeek 的 JSON Output(response_format 为 json_object)只保证输出合法的 JSON,有哪些字段仍要靠提示词中的样例约束;输出达到 max_tokens 时同样会被截断。
-字段的顺序也有影响:约束解码按 Schema 中的顺序逐个生成字段,需要推理的任务应把 reason 这类解释字段放在结论字段前面,否则模型要先给出结论,再为它找理由。
+字段的顺序也有影响:约束解码按 Schema 中的顺序逐个生成字段,需要推理的任务应把 reason 这类解释字段放在结论字段前面,否则模型要先给出结论,再为它找理由。Claude 的结构化输出会把必填字段排在可选字段之前,要固定顺序时把这几个字段都设为必填。
+
+不按 Schema 约束顺序的 JSON 模式也可能出现这个问题,例如 Tam 等(2024)用 gpt-3.5-turbo-0125 的实验中,JSON 模式的输出全都把 answer 写在 reason 前面,没有先写推理步骤,拼接单词末尾字母(Last Letter)任务的平均准确率只有 1.78%;这项研究的提示词设计存在争议,不能据此认定 JSON 模式必然降低准确率(arXiv 2408.02442)。
#### 2. 输出无法解析时
@@ -675,9 +719,13 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
#### 3. 格式正确不等于内容正确
-约束解码和 Schema 校验只能保证格式。订单是否属于当前用户、退款金额是否超过实付金额这类业务规则取决于当前用户和订单的数据,无法写进固定的 Schema,要在代码中按执行时的数据校验。JSON 合法但业务仍然出错时,先看 finish_reason 是否为 length,再对比字段类型是否与代码中的类一致,例如订单号是字符串 "1024" 还是数字 1024。
+约束解码和 Schema 校验只能保证格式。订单是否属于当前用户、退款金额是否超过实付金额这类业务规则取决于当前用户和订单的数据,无法写进固定的 Schema,要在代码中按执行时的数据校验。
-流式输出时,JSON 要等生成结束才能完整解析。需要边生成边显示时,可以只流式显示其中的文本字段。
+约束解码本身也有例外:模型拒答或输出达到 max_tokens 上限时,结果可能不完整、不符合 Schema。输出不符合预期时,先看结束原因,例如 finish_reason 是否为 length,再对比字段类型是否与代码中的类一致,例如订单号是字符串 "1024" 还是数字 1024。
+
+输入中缺少某个字段的信息、Schema 又没有办法表示“缺失”时,模型可能编造一个值填进去,所以 Schema 中应允许返回 null 或“其它”这类选项。
+
+流式输出时,完整的对象仍要等生成结束后再严格解析和校验。需要边生成边显示时,可以用部分 JSON 解析器把已收到的前缀补齐成合法的 JSON,得到中间对象,OpenAI 等官方 SDK 的流式助手提供了这种解析。中间对象只能用于显示,数字、布尔值和数组的最后一项可能还没写完,例如收到 "amount": 5 时中间对象中的 amount 是 5,完整的值却是 59.5(jiter 0.11 实测)。
### 上下文工程
@@ -692,7 +740,7 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
上下文并不是越多越好:
- 窗口有上限,输入越长,延迟和成本越高;
-- 输入越长,模型对其中信息的利用越差,关键信息位于中间时更容易被忽略,这个现象称为 Lost in the Middle。
+- 输入越长,模型对其中信息的利用越差,关键信息位于中间时更容易被忽略,这个现象称为 Lost in the Middle。论文的多文档问答实验中,给 GPT-3.5-Turbo 20 篇文档、答案在中间时,正确率比不给任何文档、只靠模型自己的知识(56.1%)还低。
因此只放入与当前任务相关的内容,重要的规则放在开头或者结尾。
@@ -731,8 +779,12 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
只有闲聊这类前后轮关系不大的对话,用滑动窗口就够了。客服对话中,用户在第 6 轮说的“取件前发短信,别打电话”到第 30 轮仍然有效;Agent 执行长任务时,早期做出的决定和用户提出的约束一直有用,这些场景适合摘要加外置。实际系统常把几种方法组合起来:上下文由“系统提示词 + 摘要 + 最近几轮的原文 + 当前问题”组成,更早的消息不断并入摘要,并且先清理工具结果,不够时再做摘要;需要读大量资料、反复试错的子任务交给[子 Agent](#多-agent),在它自己的上下文中完成,只把结论带回来。
+Codex 在本地压缩时把历史换成一段写给“接手的模型”的交接摘要(进展、关键决定、约束和下一步),同时从最新往前保留用户的原话,最多约 2 万个 Token,模型的回复和工具结果只保留在摘要里(Codex 源码,以最新版本为准)。
+
压缩的时机按 Token 数判断,而不是按轮数。上面的对话中,每轮新增的 Token 从 33 到 611 不等,查了物流的一轮是简单问答的十几倍。上一次请求的准确 Token 数可以从接口返回的 usage 中得到,再用分词器或者按字数估算新增的消息,超过阈值就压缩。常见的做法是设两道线:历史达到预算的约 70% 时在后台生成摘要,不影响当前的回答;达到上限时必须先压缩才能继续调用。MemGPT 就采用这种方式,在 70% 时提醒模型把重要的信息保存到外部,在 100% 时移出一部分消息,并把它们和原有的摘要合并成新的摘要;Claude Code 的做法见 [AI 编程助手](#ai-编程助手)。
+70% 只是一种取法,各产品的默认值相差很大,例如 Codex 没有配置阈值时在窗口的 90% 压缩,Claude Code 接近窗口上限时才压缩,Claude API 的服务端压缩默认在 15 万输入 Token 时触发(以官方最新文档为准)。
+
保留的“最近几轮”也要按 Token 计,而不是按条数。最近几条本身就超出预算时,通常是某条工具结果或用户粘贴的文档太大:先把这条外置,上下文中只留与当前任务相关的片段和读取位置,需要时再分段读取或检索;仍然放不下就减少保留的轮数,只保证系统提示词、任务状态和当前问题完整。
#### 4. 压缩后不丢关键事实
@@ -748,7 +800,8 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
- 由代码维护任务状态:订单号、退货单号、金额、地址这些字段,直接从工具调用的参数和结果中提取,单独保存,不经过模型的改写;
- 让承诺通过工具留下记录:例如“取件前短信联系”应该调用备注工具写进订单,而不是只在回答中说一句;
- 摘要分项输出,要求编号、金额、时间照原文抄写;“已办理”只写有工具结果确认的操作,只是说过要做、还在等用户确认的放进“待办”,否则“准备退款”可能被压缩成“已退款”;
-- 生成新摘要时在旧摘要的基础上合并,而不是只总结新移出的消息,否则早期的内容会在多次压缩后消失。
+- 生成新摘要时在旧摘要的基础上合并,而不是只总结新移出的消息,否则早期的内容会在多次压缩后消失;
+- 用框架的默认配置时,检查摘要模型实际看到了哪些消息。例如 LangChain 的摘要中间件默认把待摘要的部分裁剪到约 4000 个 Token(近似计数),保留末尾的消息和开头的系统消息,被裁掉的早期消息不参与摘要,其中往往有用户最初的要求。
压缩有没有丢掉关键信息,可以用两种方法检查,上面的实验就是这样做的:
@@ -885,15 +938,17 @@ Word、HTML、Markdown 文件本身记录了标题样式、表格和列表,按
| 按小节切 | 52 | 0.85 | 0.98 | 278 |
| 按小节切,单句检索、返回小节(父子切片) | 133 | 0.83 | 0.95 | 361 |
-按小节切的效果最好,放进提示词的字数也最少,因为固定长度的切片会把一句话切成两半,或者把两节的内容拼在一起。重叠对固定长度的切片帮助明显,200 字的 Recall@3 从 0.83 提高到 0.93,代价是切片数多了约四分之一。递归切分优先在段落和句子边界切开,不加重叠也有 0.95。语义切分的阈值不好定,改按 95 分位数切时,最长的切片有 841 字,超过了 512 Token。
+按小节切的效果最好,放进提示词的字数也最少,因为固定长度的切片会把一句话切成两半,或者把两节的内容拼在一起。重叠对固定长度的切片帮助明显,200 字的 Recall@3 从 0.83 提高到 0.93,代价是切片数多了约四分之一。递归切分优先在段落和句子边界切开,不加重叠也有 0.95。语义切分的阈值不好定,改按 95 分位数切时,最长的切片有 841 字,超过了 512 Token,而 LangChain 的 SemanticChunker 默认用的就是 95 分位数。
800 字的切片召回率也高,但主要是因为放进去的字多:前 3 个切片共约 2000 字,相当于知识库的四成,而且每个切片超过 512 Token 的部分没有参与向量计算。父子切片在这里没有优势,因为每个小节本身只有一两百字,小节很长、问题只对应其中一两句时它才有用。
-需要注意的是,40 个问题中一个问题就是 0.025,这组数字只说明趋势。另外,两个版本的同一节几乎一样,38 个问题的前 3 个结果里同时出现了新旧两个版本,加标题路径也区分不开,需要按版本过滤,见下文的向量数据库。
+需要注意的是,40 个问题中一个问题就是 0.025,这组数字只说明趋势。另外,两个版本的同一节几乎一样,38 个问题的前 3 个结果里同时出现了新旧两个版本,加标题路径也区分不开(Recall@1、Recall@3 与不加时相同,见[附录 A12](#a12-切片方式top-k-与相似度阈值)),需要按版本过滤,见下文的向量数据库。
#### 4. 怎么确定切片大小
-切片大小没有通用的最优值:先按文档结构切,用 Embedding 模型的最大长度给出上限;事实类的问题(几天、多少钱)适合小切片,需要综合理解的问题适合大一些的切片;最后在自己的评测集上对比不同大小和重叠的召回率。
+切片大小没有通用的最优值。公开的对照研究中,语义切分相对固定长度的收益在不同任务上并不稳定(Qu 等,2024);最优的切片大小随数据集和 Embedding 模型而变,答案简短的事实类数据集适合 64~128 Token,需要更多上下文的适合 512~1024 Token(Bhat 等,2025)。
+
+实践中先按文档结构切,用 Embedding 模型的最大长度给出上限;事实类的问题(几天、多少钱)适合小切片,需要综合理解的问题适合大一些的切片;最后在自己的评测集上对比不同大小和重叠的召回率。
### Embedding 与相似度
@@ -930,6 +985,8 @@ a 和 b 几乎没有相同的字,相似度却很高,这是向量检索相比
负样本越多、越难区分,学到的向量越精细。所以 bge 训练时一个批次多达 19,200 对,后期还为每对数据挖掘一个难负样本(Hard Negative),即与问题字面相近但并不相关的文档,例如为 E1024 的问题配上 E1025 的说明。
+直接把检索结果的前几名当作难负样本,容易混入实际相关的文档,称为假负样本(False Negative),例如 RocketQA 在 MS MARCO 上抽查 100 个问题,排在前面、没有被标为相关的文档约 70% 实际相关或高度相关(arXiv 2010.08191)。常见的做法是跳过最靠前的若干名,例如 FlagEmbedding 的挖掘脚本默认跳过前 10 名,从第 11~210 名中抽取;NV-Retriever 则把负样本分数的上限设为对应正样本分数的 95%(arXiv 2407.15831)。
+
#### 2. Top-K 检索
检索时把问题也转为向量,找出与它最相似的 K 个切片,称为 Top-K 检索。问题和切片必须使用同一个 Embedding 模型,更换模型后所有切片都要重新计算向量。K 怎么取见下文的 Top-K 与阈值。
@@ -938,10 +995,12 @@ a 和 b 几乎没有相同的字,相似度却很高,这是向量检索相比
#### 3. 选择模型
-选择模型时先看语言和领域是否匹配、最大长度是否够用,维度则决定了存储成本:1 亿个向量从 512 维换成 1024 维,原始向量的内存从 205 GB 变为 410 GB。维度一般随模型固定,Qwen3-Embedding 这类支持自定义维度的模型可以截取前若干维再归一化,0.6B 的模型可在 32~1024 维之间选,维度越低越省,效果通常也越差。公开榜单只能作初筛,业务资料的措辞与公开数据集差别很大,最终以候选模型在自己的评测集上的召回率为准。
+选择模型时先看语言和领域是否匹配、最大长度是否够用,维度则决定了存储成本:1 亿个向量从 512 维换成 1024 维,原始向量的内存从 205 GB 变为 410 GB。维度一般随模型固定,Qwen3-Embedding 这类支持自定义维度的模型可以截取前若干维再归一化,0.6B 的模型可在 32~1024 维之间选,维度越低越省,效果通常也越差。能这样截取,是因为这类模型训练时用了套娃表示学习(Matryoshka Representation Learning, MRL),对不同长度的前缀维度都计算损失;没有这样训练的模型直接截取前几维,效果可能明显下降,要在自己的评测集上验证,不能照搬。公开榜单只能作初筛,业务资料的措辞与公开数据集差别很大,最终以候选模型在自己的评测集上的召回率为准。
还要按模型的说明使用。问题短、切片长,两者的写法不同,所以很多模型要求只给问题加一段检索指令。例如 bge 中文模型建议在问题前加上“为这个句子生成表示以用于检索相关文章:”,切片不加;在切片实验的数据上,加前缀后 Recall@3 从 0.975 提高到 1.0,混入上述 12.5 万条短问题时从 0.575 提高到 0.700(见[附录 A12](#a12-切片方式top-k-与相似度阈值)、[A29](#a29-hnsw-的距离计算次数与知识库规模))。
+其它模型的约定不同,例如 e5 要求问题以“query: ”开头、切片以“passage: ”开头,Qwen3-Embedding 把查询写成 `Instruct: {任务描述}\nQuery:{问题}` 的格式(指令建议用英文),切片不加指令。用 sentence-transformers 时,encode_query() 默认使用模型配置中的查询前缀,bge-small-zh-v1.5 的配置中这个前缀为空,encode_query() 与 encode() 的结果相同,要显式传入 prompt 参数或自己拼接(sentence-transformers 6.2 实测,见[附录 A12](#a12-切片方式top-k-与相似度阈值))。
+
### 向量索引
#### 1. 为什么需要近似检索
@@ -1005,25 +1064,29 @@ FAISS 这类向量检索库只负责在内存中建立索引和检索。实际
embedding 由 text 经 Embedding 模型计算得到,bge-small-zh-v1.5 输出 512 维;扫描件 OCR 之后存的也是识别出的文本和它的向量。
+相似度的度量在建索引时指定,按 Embedding 模型推荐的度量和归一化方式选择,用索引查询时也要用同一种。各产品的默认值和限制不同,例如 Milvus 的稠密浮点向量默认用余弦;pgvector 每种距离对应一个运算符类,查询的距离与索引的运算符类一致才能用上索引;Elasticsearch 的 float 向量用 dot_product 时,文档向量和查询向量都必须是单位长度(以各产品的最新文档为准)。
+
#### 1. 过滤
-检索时往往要同时满足标量条件。如果先检索 Top-K 再过滤,结果可能远少于 K 个。例如 pgvector 的 HNSW 索引默认每次查询只保留 40 个候选,过滤条件只有 10% 的行满足时,平均只剩 4 条结果。
+检索时往往要同时满足标量条件。如果先检索 Top-K 再过滤,结果可能远少于 K 个。例如 pgvector 的 HNSW 索引默认每次查询只保留 40 个候选,过滤条件只有 10% 的行满足时,平均只剩 4 条结果;满足条件的行与查询向量的位置相关时(例如某个租户的文档都集中在一起,而查询离它们较远),可能一条都不剩。
-因此向量数据库一般在检索的过程中过滤:Milvus 先根据过滤条件生成一个位图,检索时跳过不满足条件的向量;Elasticsearch 的 knn 查询中的 filter 也在近似检索的过程中生效;pgvector 0.8.0 起支持迭代扫描,结果不够时自动继续扫描。
+因此向量数据库一般在检索的过程中过滤:Milvus 先根据过滤条件生成一个位图,检索时跳过不满足条件的向量;Elasticsearch 的 knn 查询中的 filter 也在近似检索的过程中生效,但写在外层 bool 查询中的过滤是检索完成后才执行的,同样可能少于 k 条;pgvector 0.8.0 起支持迭代扫描,结果不够时自动继续扫描,需要设置 hnsw.iterative_scan 开启,默认关闭,继续扫描的近似上限由 hnsw.max_scan_tuples 控制,默认 20000,它不限制最初的一次扫描。
-过滤还能直接提高效果。在切片一节的实验中,新旧两个版本的员工手册同时在库中,模型可能读到“次年 6 月底”这样的旧规定;检索时加上 version = 2026 的条件后,Recall@1 从 0.85 提高到 0.93(见[附录 A12](#a12-切片方式top-k-与相似度阈值))。多租户的系统同理,可以把租户 ID 设为分区键,查询时只搜索对应的分区。
+过滤还能直接提高效果。在切片一节的实验中,新旧两个版本的员工手册同时在库中,模型可能读到“次年 6 月底”这样的旧规定;检索时加上 version = 2026 的条件后,Recall@1 从 0.85 提高到 0.93(见[附录 A12](#a12-切片方式top-k-与相似度阈值))。
+
+多租户的系统可以把租户 ID 设为分区键,查询条件中带上租户 ID,就只搜索对应的分区。Milvus 默认只建 16 个分区,一个分区中通常有多个租户,所以仍要按租户 ID 过滤;租户少、隔离要求高时可以每个租户一个集合。
#### 2. 更新与删除
向量索引的修改比普通数据库贵。HNSW 插入一个向量相当于做一次检索,再为它和邻居建立连接;删除更麻烦,从图中删掉一个节点会破坏其他节点的连接,所以一般只做标记,检索时跳过,之后再统一清理;更新等于删除加插入。
-以 Milvus 为例,新写入的数据先进入可写的段(Growing Segment),持久化之后变为不可修改的段(Sealed Segment),再在这个段上建立索引;删除只是在位图中标记,后台的压缩(Compaction)合并小段时才真正去掉。所以频繁的小批量更新会产生大量的小段和被标记删除的数据,它们仍然占用内存、拖慢检索,更新应该尽量批量进行。
+以 Milvus 为例,新写入的数据先进入可写的段(Growing Segment),持久化之后变为不可修改的段(Sealed Segment),再在这个段上建立索引;删除只是记下被删除的主键,查询时过滤掉,由后台的压缩(Compaction)在合并或重写段时真正去掉;一个段中被删除的比例达到阈值(默认 20%)是触发压缩的条件之一。所以大量删除和更新会累积被标记删除的数据,压缩之前它们仍然占用内存、拖慢检索。
-文档修改后,按文档 ID 和内容的哈希值找出变化的文档,删除它的全部旧切片再写入新切片,不需要重建整个索引;只追加新切片会让旧内容继续被召回。只是权限或版本状态变化时,更新切片上的元数据即可,不必重新计算向量。文档的原文和元数据一般以关系数据库为准,向量库按切片 ID 幂等写入,失败时重试,并定期对账两边的数据。
+文档修改后,按文档 ID 和内容的哈希值找出变化的文档,删除它的全部旧切片再写入新切片,不需要重建整个索引;只追加新切片会让旧内容继续被召回。只是权限或版本状态变化时,更新切片上的元数据即可,不必重新计算向量。文档的原文和元数据一般以关系数据库为准,向量库按切片 ID 幂等写入,失败时重试,并定期对账两边的数据。要写入向量库的切片可以在修改文档的同一个事务中记一条同步记录,由后台读取后写入向量库,做法同[本地消息表](分布式.md#本地消息表),文档写成功、向量写失败时有记录可以重试。
哈希只能发现被修改的文档,整篇源文件被删除时,要靠文档系统的删除事件,或者定期拿源系统的全部文档 ID 与库中比对,删掉多出来的切片。
-删旧切片和写新切片不是一次完成的,期间查询可能缺少这篇文档或新旧内容并存;在意这一点时,先写入带新版本号的切片,再把元数据中的当前版本切过去,最后删除旧切片。另外,Milvus 默认的一致性级别是 Bounded,刚写入的数据可能要稍等一会才能查到。
+删旧切片和写新切片不是一次完成的,期间查询可能缺少这篇文档或新旧内容并存;在意这一点时,先写入带新版本号的切片,再把元数据中的当前版本切过去,最后删除旧切片。另外,Milvus 默认的一致性级别是 Bounded,默认容忍最近约 5 秒内写入的数据不可见;查询必须看到刚才的写入时指定 Strong,只需要读到同一个客户端自己的写入时可以用 Session,代价是查询要等待数据可见。
更换 Embedding 模型或切片方式时,所有向量都要重新计算。这时在新的集合中建好索引并评测,再把查询使用的别名(Alias)从旧集合切到新集合,切换过程中查询不中断,出问题时切回去。Milvus 和 Elasticsearch 都支持别名。换了模型时,查询向量也要用新模型计算,所以查询用的模型和集合作为一组配置同时切换、同时回滚。
@@ -1068,6 +1131,8 @@ IDF(t) = ln(1 + (N − n + 0.5) / (n + 0.5))
两路结果中都排名靠前的 A 和 C 排在最前面。也可以把两路的分数分别归一化到 0 到 1 之后按权重相加。
+关键词一路也可以不用 BM25。BGE-M3 这类模型在输出稠密向量的同时,为文本中的每个 Token 算一个权重(ReLU(W·h),h 是这个 Token 最后一层的输出),得到学出来的稀疏向量;同一个 Token 出现多次时只保留最大的权重,查询和文档的相关性是两边都出现的 Token 的权重乘积之和。它和 BM25 一样按词匹配,但权重由模型学出,一次编码同时得到两种向量。
+
混合检索不一定比单路好,权重要在评测集上确定。BM25 的优势在错误码、型号、函数名这类精确匹配上;在切片实验的 40 个口语化问题上,等权 RRF 的 Recall@3 为 0.950,低于单独的向量检索(0.975),BM25 的权重降到 0.5 后为 1.0(见[附录 A30](#a30-混合检索与重排的召回率))。也可以只在查询含有编号时提高 BM25 的权重。
### 重排
@@ -1097,7 +1162,7 @@ RAG 中有两个 K:召回的 K 是每路检索取多少个候选交给重排
#### 3. 放进提示词的 K
-放进提示词的 K 不是越大越好。Token 越多,成本和延迟越高;排名靠后的切片大多是无关或者半相关的内容,也可能是旧版本、互相矛盾的规定,模型可能用错资料;上下文越长,模型越容易忽略中间的信息,见[上下文工程](#上下文工程)。
+放进提示词的 K 不是越大越好。Token 越多,成本和延迟越高;排名靠后的切片大多是无关或者半相关的内容,也可能是旧版本、互相矛盾的规定,模型可能用错资料;上下文越长,模型越容易忽略中间的信息,见[上下文工程](#上下文工程)。例如 Lost in the Middle 的开放域问答实验中,检索到的文档从 20 篇增加到 50 篇,检索的召回率还在上升,GPT-3.5-Turbo(16K)的回答正确率只提高约 1.5 个百分点,输入长度却显著增加(2023 年的模型,arXiv 2307.03172)。
K 太小,需要多个切片才能回答的问题又会答不全,例如“几点以后算迟到”需要同时用到工作时间和迟到的定义两节。所以这个 K 要用端到端的回答正确率来确定,并受上下文预算的约束。
@@ -1111,7 +1176,9 @@ K 太小,需要多个切片才能回答的问题又会答不全,例如“几
| 0.55 | 2.5% | 20% |
| 0.60 | 10% | 10% |
-向量相似度的阈值只能在误拒和放行之间折中,适合用较低的值过滤明显无关的结果。判断有没有答案,更多依靠重排模型的分数(它直接判断问题与切片是否相关,阈值同样要在评测集上确定),以及在提示词中要求资料不足时回答不知道。更换模型后阈值要重新确定,不同模型的分数分布不同。
+向量相似度的阈值只能在误拒和放行之间折中,适合用较低的值过滤明显无关的结果。判断有没有答案,更多依靠重排模型的分数(它直接判断问题与切片是否相关,阈值同样要在评测集上确定),以及在提示词中要求资料不足时回答不知道。
+
+更换模型后阈值要重新确定,不同模型的分数分布不同。例如 bge 的模型卡说旧版模型的分数集中在 [0.6, 1],按自己数据的分布选阈值时举例 0.8、0.85 甚至 0.9;而在[附录 A12](#a12-切片方式top-k-与相似度阈值) 的数据上,bge-small-zh-v1.5 对有答案问题的最高相似度只有 0.774,照搬 0.8 会拒掉全部问题。
### 查询改写
@@ -1123,7 +1190,11 @@ K 太小,需要多个切片才能回答的问题又会答不全,例如“几
改写也可以扩展成多路召回:RAG-Fusion 让模型把问题改写成几个不同角度的查询,每个查询分别检索,再用 RRF 合并全部结果,覆盖的措辞更多,但改写偏离原意时会引入无关的结果;HyDE 生成的假设回答也可以作为其中一路。
-改写需要多调用一次模型,会增加延迟。
+改写需要多调用一次模型,会增加延迟。改写也可能变差:模型可能漏掉上下文或者偏离原意,改得差时检索效果反而不如原问题。
+
+HyDE 这类生成扩展内容的方法,通常对原本检索效果较差的模型(例如 BM25)提升明显,对效果已经较强的 Embedding 模型和交叉编码器重排收益有限,甚至下降。作者对下降样例的人工分析中,多是扩展加入了无关或错误的词,干扰了相关性判断(Weller 等,arXiv 2309.08541,英文基准)。
+
+已经用了混合检索和重排时,扩展要在评测集上确认有提升再加,也可以只用于召回差的查询;首轮和本身已经完整的问题可以不改写;评估改写时,用同一批标注了相关切片的问题,比较原问题和改写后的问题的 Recall@K、MRR,指标见[评测与优化](#评测与优化)。
### 评测与优化
@@ -1165,11 +1236,19 @@ RAG 需要分别评测检索和生成,这样才能定位问题出在哪一步
第二个回答把 3 月底写成了 6 月底,忠实度能发现,答案相关性发现不了,所以两个指标要一起看。评判模型本身也会出错:“工作两年有 5 天年假”需要从“满 1 年不满 10 年”推出来,7B 模型判为不支持,使正确的回答只得了 2 / 3。因此评判模型要选能力较强的,并抽样人工核对它的打分。
+忠实度低说明回答中有资料推不出的内容,可能是编造,也可能是用了模型自己的知识,它不说明检索的好坏。ragas 库中的 Context Precision、Context Recall 要对照标准答案判断检索结果,属于检索指标,与论文中不需要标准答案的指标不同。
+
+忠实度检查回答有没有资料支持,答案相关性检查有没有回应问题,两者都不能代替与标准答案比对对错。评测集有标准答案时,还要比较回答与标准答案:天数、日期这类短答案规范化后直接比较;长回答拆成几条陈述,与标准答案逐条比对,分别统计说错的和漏掉的,用模型比对时把标准答案交给它并要求以此为准,见[模型裁判的偏差](#3-模型裁判的偏差)。标准答案要写全要点,否则回答中正确但标准答案没写到的内容会被算成错误。
+
#### 3. 评测集
指标要在一批固定的问题上计算。问题优先从真实的用户问题中抽样,不够时让模型根据文档生成,再人工改写措辞,因为模型生成的问题往往照抄原文的词语,会高估向量检索的效果。除了常见问题,还要包含资料中没有答案的问题、需要多个切片的问题和新旧版本冲突的问题。没有答案的问题,标准答案标为“资料中没有”,回答不知道算对,编出答案算错;它和有答案的问题被误拒要一起统计,两者的取舍与[相似度阈值](#4-相似度阈值)相同。
-每个问题标注标准答案和回答所需的原文片段,而不是切片 ID,这样改变切片方式之后标注仍然可用,切片一节的实验就是这样判断是否找到的。几十到几百个问题就可以比较不同的方案,每次调整解析、切片、模型或参数,以及知识库中新增大量文档之后,都重新评测;线上发现的错误回答持续补充进来,避免评测集与真实的问题分布脱节。
+每个问题标注标准答案和回答所需的原文片段,而不是切片 ID,这样改变切片方式之后标注仍然可用,切片一节的实验就是这样判断是否找到的。几十到几百个问题就可以比较不同的方案(差距只有几道题时要结合逐题的结果判断,比较方法见 Agent 的[评测集与线上指标](#4-评测集与线上指标)),每次调整解析、切片、模型或参数,以及知识库中新增大量文档之后,都重新评测;线上发现的错误回答持续补充进来,避免评测集与真实的问题分布脱节。
+
+只标了一部分相关片段时,没标到的结果会被当作不相关,换一种检索方式后分数可能偏低。BEIR 在 TREC-COVID 上统计,稠密检索模型 TAS-B 的前 10 个结果中有 31.8% 没被标注过,补标之后 ANCE 的 nDCG@10 从 0.654 升到 0.735,BM25 只从 0.656 升到 0.668。比较新的检索方案之前,先检查前 K 个结果中有多少没被标注,补标后再比较。
+
+补标可以先交给大模型,但它容易把含有问题关键词、却没有给出答案的片段判为相关,例如 Alaofi 等人把查询词插入无关的文本后,多个模型会因此误判为相关(arXiv 2501.17969)。所以要先用人工标注核对模型的判断,重点检查判为相关的片段;也不要让系统中做重排的同一个模型来补标,否则评测会偏向它自己的排序。
#### 4. 排查顺序
@@ -1190,7 +1269,7 @@ RAG 需要分别评测检索和生成,这样才能定位问题出在哪一步
模型的上下文窗口越来越大,资料量不大时可以把全部资料直接放进上下文,不需要 RAG。但知识库很大时,RAG 仍然是必要的:
- 每次请求都处理大量 Token,成本高、延迟大;
-- 上下文越长,模型对其中信息的利用越差。
+- 上下文越长,模型对其中信息的利用越差,标称的窗口也不等于能用好的长度。例如 RULER 测试中,Qwen2-72B 标称支持 128K,但得分超过 85.6(Llama2-7B 在 4K 上的得分,作为基准)的最大长度只有 32K,128K 时得分为 53.7(满分 100)。
普通的 RAG 每个问题只检索一次。问题需要多步查找时,例如“负责支付系统的团队的负责人是谁”,要先查到团队,再查负责人。这时可以把检索作为一个工具交给 Agent,由模型决定检索什么、检索几次,称为 Agentic RAG。它更灵活,但调用次数和延迟都会增加,问题简单时固定流程的 RAG 更可控。
@@ -1209,9 +1288,17 @@ GraphRAG 在建索引时让模型从每个切片中抽取实体和关系,组
(交易平台组, 负责人, 王磊) 来自《组织架构》
```
-查询时先找到问题中的实体“支付系统”,沿着边走两步就得到答案,再把相关的原文交给模型生成回答。对于全局问题,微软的 GraphRAG 用社区检测算法把联系紧密的实体分组,为每组生成一段摘要;回答时先让每个摘要分别给出部分回答,再汇总成最终的回答。
+查询时先找到问题中的实体“支付系统”,沿着边走两步就得到答案,再把相关的原文交给模型生成回答,这是图检索的一般做法。微软的 GraphRAG 的局部搜索(Local Search)则用问题的向量匹配实体描述的向量,取出这些实体相邻的关系、所在社区的摘要和原文,一起放进一次上下文。
-代价是建索引要对每个切片调用大模型,比计算 Embedding 贵得多,抽取的错误会进入图中,文档更新后还要重新抽取。所以它适合实体和关系密集的资料,例如组织架构、系统依赖、条款之间的引用;普通的问答用向量检索加关键词检索就够了。
+对于全局问题,微软的 GraphRAG 用社区检测算法把联系紧密的实体分组,为每组生成一段摘要;回答时先让每个摘要分别给出部分回答,再汇总成最终的回答,称为全局搜索(Global Search)。
+
+代价是建索引要对每个切片调用大模型,比计算 Embedding 贵得多,抽取的错误会进入图中。
+
+全局搜索每次查询也要读一层全部的社区摘要,例如论文中约 100 万 Token 的播客语料,中间一层的社区摘要共约 57 万 Token,最上层只有约 2.7 万 Token,但答案的细节也少。
+
+文档更新时,微软的实现(v3.3.0)可以只抽取新增的文档再合并到原来的图中,但原有的社区和摘要不会重算,删除或修改文档一般要重建。
+
+所以 GraphRAG 适合实体和关系密集的资料,例如组织架构、系统依赖、条款之间的引用;普通的问答用向量检索加关键词检索就够了。
### 多模态 RAG
@@ -1251,7 +1338,7 @@ Agent(智能体)指的是让大模型通过调用工具来完成任务的系
-1. 应用在请求中附上工具定义,包括名称、用途和参数的 JSON Schema;
+1. 应用在请求中附上工具定义,包括名称、用途和参数的 JSON Schema(见[结构化输出](#结构化输出));
2. 推理服务按照模型的对话模板,把工具定义和调用格式的说明拼进提示词;
3. 模型在训练时学习过这种格式,判断需要调用工具时,会生成一段特定格式的文本,而不是直接回答;
4. 推理服务识别出这段文本,把它解析为结构化的 tool_calls 返回给应用。
@@ -1298,7 +1385,9 @@ Agent(智能体)指的是让大模型通过调用工具来完成任务的系
工具定义被拼进了系统消息,模型输出的是包在 <tool_call> 标签里的一段 JSON,应用用正则把它解析出来;工具结果则被包在 <tool_response> 中,作为下一条消息发回。不同模型的标签不同,但原理相同,OpenAI 兼容接口中的 tool_calls 字段就是推理服务从这段文本中解析出来的。
-工具结果使用单独的角色,模型能区分这是工具返回的数据而不是用户的指令,但这并不能防止结果中藏有恶意指令,见[安全](#安全)。
+需要注意的是,tool_call_id 只在接口中用来对应调用和结果,模型不一定看得到它。以 Qwen2.5 的对话模板为例,一次回复中的两个调用和两条结果渲染后没有 id,两个 <tool_response> 依次放在同一条 user 消息中,模型只能按顺序对应,所以多条结果要按调用的顺序追加(见[附录 A25](#a25-并发执行工具调用))。
+
+工具结果有单独的标记(OpenAI 兼容接口是 tool 角色,Claude 的接口是 user 消息中的 tool_result 块,Qwen2.5 的对话模板把它放在 user 轮的 <tool_response> 标签中),模型能区分这是工具返回的数据而不是用户的指令,但这并不能防止结果中藏有恶意指令,见[安全](#安全)。
模型只能根据工具的名称、描述和参数说明来决定是否调用、怎么填参数,所以工具的设计直接影响调用的准确率:
@@ -1350,6 +1439,28 @@ String run(String question) {
什么时候结束由模型判断,但模型可能一直不停,或者反复调用同一个工具,所以代码中必须限制最大步数,见下文的可靠性与评测。
+execute 不抛出异常,而是把错误写成文字作为工具结果返回,模型看到后可以修正参数、换一个工具或者向用户说明。下面是简化的写法,省略了参数校验和临时性错误的重试,实际应用中超时、限流这类错误先由代码重试,仍然失败或者需要模型处理的错误才作为工具结果返回,见[容错](#1-容错):
+
+```java
+String execute(ToolCall call) {
+ Tool tool = tools.get(call.name());
+ if (tool == null) {
+ return "错误:没有名为 " + call.name() + " 的工具,可用的工具有 " + tools.keySet();
+ }
+ Map args;
+ try {
+ args = parseJson(call.arguments());
+ } catch (Exception e) {
+ return "错误:参数不是合法的 JSON:" + e.getMessage();
+ }
+ try {
+ return tool.call(args);
+ } catch (Exception e) {
+ return "错误:" + e.getMessage(); // 例如“订单 9999 不存在”
+ }
+}
+```
+
模型在一次回复中给出多个工具调用,说明它认为这些调用互不依赖,例如同时查询两个订单,代码可以并发执行,全部完成后再调用模型:
```java
@@ -1368,14 +1479,14 @@ for (CompletableFuture f : results) {
### Agent 与工作流
-与 Agent 相对的是工作流(Workflow):执行哪些步骤、按什么顺序执行都由代码事先写好,大模型只负责其中的某些步骤,例如对用户的消息分类。常见的工作流有链式(Prompt Chain,上一步的输出是下一步的输入)、路由、并行,以及生成后由另一次调用评审、不合格就退回重写。
+与 Agent 相对的是工作流(Workflow):执行哪些步骤、按什么顺序执行都由代码事先写好,大模型只负责其中的某些步骤,例如对用户的消息分类。常见的工作流有链式(Prompt Chain,上一步的输出是下一步的输入)、路由、并行,以及生成后由另一次调用评审、不合格就退回重写。还有一种介于两者之间:子任务由模型根据输入临时拆分,但拆分、分派和汇总的骨架由代码写好,见多 Agent 一节的[主管分派](#2-协作方式)。
- 工作流可预测、成本可控,适合步骤固定的业务;
- Agent 更灵活,适合无法事先确定步骤的开放任务,代价是成本更高,错误会在多个步骤之间累积。
-例如每一步的成功率是 95%,连续 10 步都成功的概率只有 0.9510 ≈ 60%。因此应该先尝试单次调用或者工作流,确实需要时再引入 Agent,也可以只在工作流的某一步中使用 Agent。
+例如每一步的成功率是 95%,连续 10 步都成功的概率只有 0.9510 ≈ 60%。工作流同样会累积错误,但它的步骤和转移规则由代码事先定义,便于在步骤之间加入校验,不合格就停下或重试;Agent 也可以在工具调用前后校验,但执行路径由模型临时决定,更难事先覆盖所有情况。因此应该先尝试单次调用或者工作流,确实需要时再引入 Agent,也可以只在工作流的某一步中使用 Agent。
判断用哪一种,看步骤和步骤之间的转移规则能否事先写清。例如排查“订单 1024 为什么没发货”,如果原因只有库存不足、地址异常这几种,可以写成工作流的分支,分别查补货单和风控记录;原因五花八门、要根据查到的线索决定下一步查什么时,路径事先写不全,才交给 Agent。
@@ -1392,15 +1503,15 @@ for (CompletableFuture f : results) {
#### 1. 三种做法
- 规则:关键词、正则表达式,例如消息中有“退款”就进入退款流程。速度快、没有成本、结果可解释,但用户换个说法就匹配不到,不同类别的关键词还会撞车;
-- 分类模型:用标注数据训练一个小模型,例如 BERT 类的文本分类模型,或者 Embedding 加一个简单的分类器。延迟在毫秒级,输出的概率可以作为置信度,但需要标注数据,增加类别要重新训练;
+- 分类模型:用标注数据训练一个小模型,例如 BERT 类的文本分类模型,或者 Embedding 加一个简单的分类器。延迟在毫秒级,输出的概率可以作为置信度,但需要标注数据,增加类别要重新训练。也可以不训练,计算消息的 Embedding 与每类几条示例的类中心的相似度,选最近的一类,增加类别只要补示例,但同样要设阈值才能把不属于任何类别的消息分出来;
- 让大模型输出类别:在提示词中写清楚每个类别的定义,让模型只输出类别名。不需要训练数据,能理解没见过的说法,但每次请求多一次模型调用,输出还可能不在类别列表中。
-用 60 条客服消息比较这三种做法(5 个类别各 12 条,其中 10 条含有其它类别的关键词,例如“钱退到哪了”含有“到哪”):
+用 60 条客服消息比较规则、Embedding 相似度和大模型(5 个类别各 12 条,其中 10 条含有其它类别的关键词,例如“钱退到哪了”含有“到哪”):
- 关键词规则的准确率是 73.3%。它判为退款、改地址的消息全部正确,但 12 条退款消息只找到 7 条,“收到的杯子碎了,怎么处理”没有命中任何关键词,“快递还没到,但我不想要了”因为“快递”被分到了物流;
-- 分类模型(bge-small-zh-v1.5 加每类 6 条示例)的准确率是 78.3%,示例太少时容易被字面相似误导,例如把“保修多久”分到退款;
+- Embedding 相似度(bge-small-zh-v1.5 加每类 6 条示例)的准确率是 78.3%,示例太少时容易被字面相似误导,例如把“保修多久”分到退款;
- Qwen2.5-7B 的准确率是 95%,10 条含有其它类别关键词的消息全部分对。
需要注意的是,7B 模型把“快递员态度太差了,我要投诉”的类别输出成了“投诉”,这个类别并不存在(Qwen2.5 实测,见[附录 A6](#a6-意图路由的三种做法))。
@@ -1415,13 +1526,13 @@ for (CompletableFuture f : results) {
- 低置信度兜底:置信度低于阈值时不硬分,而是反问澄清(“您是想查物流还是申请退款?”),或者交给通用的 Agent、转人工。已经进入流程后发现分错(例如用户说“我不是要退款”),流程把请求退回路由重新分类,而不是继续执行;
- 看上下文:多轮对话中的“那退了吧”单独看无法分类,要把最近几轮对话一起交给分类器,或者先把它改写成完整的问题;一句话包含多个意图时,拆成多个请求,或者交给能分派任务的主 Agent。
-阈值要在验证集上扫一遍再定:阈值越高,自动路由的比例越低,准确率一般越高。例如上面的分类模型阈值从 0.5 提到 0.7,自动路由从 70% 降到 48%,准确率从 90.5% 升到 96.6%,其余转入兜底流程。按兜底流程能承接的量和可以接受的错误率选一个点,分错代价高的类别(例如会进入退款流程的)可以单独设更高的阈值。
+阈值要在验证集上扫一遍再定:阈值越高,自动路由的比例越低,准确率一般越高。例如上面 Embedding 相似度方法的置信度阈值从 0.5 提到 0.7,自动路由从 70% 降到 48%,准确率从 90.5% 升到 96.6%,其余转入兜底流程。按兜底流程能承接的量和可以接受的错误率选一个点,分错代价高的类别(例如会进入退款流程的)可以单独设更高的阈值。
阈值的效果还取决于置信度是否可靠:7B 模型分错的 3 条中有 2 条的置信度高于 0.93,阈值几乎拦不住。大模型的输出概率往往过于自信,可以改用多次采样看结论是否一致,或者在类别中加入“不确定”,让模型自己选择。
路由的准确率要靠评测集来衡量和守住:
-- 按类别收集样本,每类数量接近,特别要包含容易混淆的消息和不属于任何类别的消息;
+- 按类别收集样本,每类数量接近,特别要包含容易混淆的消息和不属于任何类别的消息,后者要单独统计召回率,例如在 CLINC150 数据集上,BERT 对 150 个意图的准确率约 96%~97%,范围外的消息却只召回 40.3%(把范围外当作一类训练)或 52.3%(按概率阈值判断)(Larson 等,arXiv 1909.02027);
- 按类别看精确率(判为这一类的消息中有多少是对的)和召回率(这一类的消息找回了多少),两种错误的代价不同:误进退款流程的代价通常比漏判后转人工高。例如本例规则对退款的精确率是 100%,召回率只有 58%(7/12);
- 看混淆矩阵,即每个类别被错分到了哪里,例如本例中规则的 16 个错误有 9 个是没有命中任何关键词、落入“其它”,5 个是被“快递”“到哪”这类词误导、分到了物流;
- 线上转人工、用户改口重说的会话说明路由可能错了,定期抽查后加入评测集;
@@ -1435,7 +1546,7 @@ for (CompletableFuture f : results) {
思维链(Chain-of-Thought, CoT)指的是让模型先写出中间的推理步骤,再给出答案。例如“3 件 59.9 元的 T 恤,满 150 减 20,实付多少”,模型先写出 3 × 59.9 = 179.7,超过 150,再减 20 得 159.7。写出步骤之后,后面的每个 Token 都能参考前面算出的中间结果,多步计算和推理的准确率明显提高。
-实现上,可以在少样本示例中写出推理过程,或者在提示词中要求“先逐步分析,再给出答案”;推理模型在训练中已经学会了这种方式,见[推理模型](#推理模型)。
+实现上,可以在少样本示例中写出推理过程,或者在提示词中要求“先逐步分析,再给出答案”;推理模型在训练中已经学会了这种方式,见[推理模型](#推理模型)。对推理模型通常不必再要求逐步思考:OpenAI 的文档认为这没有必要,有时还会变差;DeepSeek-R1 的论文报告少样本提示会降低它的效果,建议零样本直接描述问题和输出格式。
思维链只发生在一次调用之内,所用的信息全部来自上下文和模型自身的知识。如果某个事实记错了,后面的推理会顺着错误继续推下去。ReAct 论文在 HotpotQA 问答上做过人工分析:思维链的失败案例中 56% 是编造的事实,ReAct 为 0%。思维链适合信息已经在上下文中的推理和计算。
@@ -1465,7 +1576,7 @@ ReAct 适合下一步取决于上一步结果、步数不多的任务,例如
两种方式的比较:
-- 先规划后执行:互不依赖的步骤可以并行,进度容易展示和恢复;但计划可能与实际情况不符,需要重规划;
+- 先规划后执行:互不依赖的步骤可以并行,进度容易展示和恢复;但计划可能与实际情况不符,需要重规划。它不一定比 ReAct 省:步骤直接调用不依赖模型的工具、也不重规划时,整个任务可以只调用两次模型,分别用于规划和汇总,ReWOO 论文报告它在 HotpotQA 上的 Token 消耗约为 ReAct 的 1/5;每个步骤再用模型执行,或者每步都重规划时,调用次数可能比 ReAct 还多;
- ReAct:可以根据中间结果随时调整方向;但每一步都要调用一次大模型,长任务中容易偏离最初的目标。
实际系统常把两者结合:先生成一份计划,执行每个步骤时使用 ReAct,并在执行过程中更新计划。
@@ -1480,7 +1591,7 @@ ReAct 适合下一步取决于上一步结果、步数不多的任务,例如
反思(Reflexion)指的是任务失败后,让模型根据反馈写一段反思,说明错在哪、下次怎么做,保存到记忆中,下一次尝试时放进上下文。论文在 HumanEval 代码生成上用自己生成的单元测试作为反馈,pass@1 达到 91%,同期 GPT-4 为 80%。
-反思是否有效,关键在于反馈是否来自外部,例如测试结果、工具报错、校验规则。没有外部反馈时,让模型检查自己的答案往往发现不了错误,有研究发现在推理任务上这样做甚至会把对的答案改错。因此工程上的反思通常由失败信号触发,例如测试不通过或者参数校验失败,并限制重试次数;反复出现的经验可以写进提示词或者 Skill,变成长期的规则,见 [Harness](#harness)。
+反思是否有效,关键在于反馈是否来自外部,例如测试结果、工具报错、校验规则。没有外部反馈时,让模型检查自己的答案往往发现不了错误,有研究发现在推理任务上这样做甚至会把对的答案改错,例如 gpt-3.5-turbo-0613 在 GSM8K 数学题上自查两轮后,8.8% 的题由对改错、7.6% 由错改对,准确率从 75.9% 降到 74.7%(Huang 等,arXiv 2310.01798)。因此工程上的反思通常由失败信号触发,例如测试不通过或者参数校验失败,并限制重试次数;反复出现的经验可以写进提示词或者 Skill,变成长期的规则,见 [Harness](#harness)。
#### 5. 怎样选择
@@ -1495,7 +1606,7 @@ ReAct 适合下一步取决于上一步结果、步数不多的任务,例如
### 记忆 🔥
-模型本身不保存任何状态。模型服务的对话接口一般是无状态的,服务端不记录之前的请求,每次调用都要把需要的历史重新发过去。聊天应用看起来“记得”之前说过的话,是因为应用把这些信息保存了下来,在下一次调用时放回了上下文。
+模型本身不保存任何状态。模型服务的对话接口一般是无状态的,不会自动接续之前的请求,每次调用都要把需要的历史重新发过去。聊天应用看起来“记得”之前说过的话,是因为应用把这些信息保存了下来,在下一次调用时放回了上下文。也有接口可以由服务端维护对话状态,例如 OpenAI Responses API 可以用 previous_response_id 或 Conversations 接续之前的对话,由服务端把历史拼回输入,但之前的 Token 仍然按输入计费(以官方最新文档为准)。
所以记忆和上下文是两回事:上下文是一次调用的输入,调用结束就没有了;记忆是应用保存的数据,每次调用前从中挑出一部分放进上下文。记忆要解决的是保存什么、存在哪里,以及什么时候取出来。
@@ -1526,7 +1637,9 @@ EXPIRE session:7f3a:state 86400
- 消息用 List,RPUSH 在末尾追加,LRANGE 按顺序取出最近的若干条;
- 任务状态用 Hash,每个字段可以单独更新;
-- 设置过期时间,长时间不活跃的会话自动清理,需要长期保留的对话再异步写入数据库。
+- 设置过期时间,长时间不活跃的会话自动清理。
+
+Redis 的持久化不保证不丢数据,开启 AOF 的 everysec 时故障仍可能丢失约 1 秒的写入(见 [Redis 持久化](Redis.md#八持久化))。所以需要保留的原始消息和要恢复的任务状态要先写入数据库,Redis 只缓存最近几轮、摘要和任务状态,键过期或丢失后从数据库恢复。用 MongoDB 的 TTL 索引控制保留期限时,删除由后台任务约每 60 秒执行一次,到期的文档可能还在,读取时仍要按时间过滤。
#### 2. 长期记忆的写入
@@ -1556,8 +1669,16 @@ EXPIRE session:7f3a:state 86400
提取的结果要经过评测,模型同样会出错。用 Qwen2.5-7B 从前面的客服对话中提取:只要求“提取关于用户的信息”时,结果中既有已经过时的旧地址,还有一个用户从没说过的电话号码 1388823;写明“只提取用户明确说过的、以后仍然有用的信息,订单和物流进度不要提取”之后,提取出了新地址和手机尾号,却漏掉了“取件前发短信”这条偏好(Qwen2.5 实测,见[附录 A5](#a5-长期记忆的提取与更新))。
+评测提取时先人工标出对话中应该记住的事实,统计正确提取的条数,同时统计过时和编造的条目。例如这段对话应记住新地址、手机尾号和“取件前发短信”3 条,限定的提示词正确提取了 2 条,宽松的只有 1 条,另有过时和编造的各 1 条。
+
+端到端再准备一批测试问题,比较用记忆、不用记忆和把全部历史放进上下文的回答,题型可以参考 LongMemEval:信息抽取、跨会话推理、时间推理、知识更新和拒答(arXiv 2410.10813)。
+
每条记忆连同以下信息一起保存:用户 ID、内容、向量、类型、来源(哪一次会话,用户亲口说的还是模型推断的)、写入时间、最近被用到的时间,以及可选的过期时间。
+上面是按集合保存:一条信息一条记录,新增容易,但用的时候要靠检索,分散的条目也难以完整表达信息之间的关系。
+
+另一种存法是画像:每个用户一份 JSON,例如 {"address": "...", "phone_tail": "8823", "notify": "取件前发短信"},按字段更新,适合地址、称呼、联系偏好这类范围明确的信息。画像较小时可以按用户 ID 直接读出整份放进上下文,不需要按问题检索。画像的字段多了以后,让模型重写整份容易改错或者丢字段,可以只让它输出要修改的字段(LangGraph 的记忆文档中称为 profile 和 collection)。
+
#### 3. 长期记忆的检索
检索分三步:
@@ -1568,7 +1689,7 @@ EXPIRE session:7f3a:state 86400
不是每条记忆都要检索:称呼、回答风格这类每次都用得到的少量信息,可以固定放在系统提示词中;其它的按当前问题检索最相关的几条。也可以把检索做成一个 search_memory 工具,由模型判断什么时候需要,适合记忆很多、而大部分问题用不到记忆的场景。
-即使模型的窗口能放下全部记忆,也只放相关的几条,原因与上下文工程中的相同:上下文越长越贵,无关的内容还会干扰回答。
+按集合保存的记忆,即使模型的窗口能放下全部条目,也只放相关的几条,原因与上下文工程中的相同:上下文越长越贵,无关的内容还会干扰回答。
每一轮都检索会增加生成之前的等待时间,开销来自查询的向量化和一次带过滤的向量检索,取决于 Embedding 服务、网络和记忆的规模。例如 Mem0 论文中检索耗时的中位数为 0.15 秒,P95 为 0.2 秒。降低开销的办法是与 RAG 检索共用查询向量、两者并行执行,写入放到后台提取,或者改用上面的 search_memory 工具。
@@ -1634,7 +1755,7 @@ LangGraph 等框架用检查点(Checkpoint)在每一步之后自动保存整
- 压缩之后:被指代的内容可能已经被压缩掉了,所以任务状态中要记录当前在处理的订单,以及会话中提到过的所有订单;
- 有多个候选:用户先后提到了订单 1024 和 1025,再说“刚才那个退了吧”,两个都有可能,这时应该追问,而不是猜一个。
-改写时把最近几轮对话和任务状态一起交给模型,要求输出一个不依赖上下文也能看懂的完整问题。这个任务比较简单,可以交给较小的模型:
+改写时把最近几轮对话(包括助手的回答,后一个问题常常指向上一轮回答中的内容)和任务状态一起交给模型,要求输出一个不依赖上下文也能看懂的完整问题。这个任务可以交给较小的模型,但要在自己的对话上比较改写前后的检索效果:
```text
任务状态:当前订单 1026;本次会话提到的订单 1024、1025、1026
@@ -1663,6 +1784,8 @@ LangGraph 等框架用检查点(Checkpoint)在每一步之后自动保存整
- 把当前的目标、已确认的信息和待办作为任务状态单独维护,每次放在上下文的末尾,模型对开头和结尾的内容利用得最好;
- 一个话题结束后,把这部分对话压缩成一句结果,例如“订单 1025 已办理退货,单号 R2051”;
+- 一个任务没完成就被新话题打断时,把原任务和已经收集到的信息挂起,新任务完成后询问用户是否继续,例如“订单 1025 的退货还要继续吗?”,而不是自动恢复原任务,Rasa 默认的中断恢复流程就是这样处理的;
+- 任务结束后,清理任务状态中只属于这个任务的临时信息,收货地址这类之后还要用的信息才保留;
- 长任务中让 Agent 维护一份待办清单,每完成一步就更新,见[规划](#规划)。
以下情况适合开启新会话:
@@ -1744,7 +1867,11 @@ MCP 与相关概念的区别:
一个 Agent 接入多个服务端时,主机为每个服务端建立一个客户端,把各服务端的工具合并后交给模型。规范只建议工具名在一个服务端内唯一,不同服务端之间可能重名,所以主机通常加上服务端名作前缀(服务端自报的名称也不保证唯一,Claude Code 用的是用户配置中的名称),例如 Claude Code 中的工具名形如 mcp__服务端名__工具名。
-MCP 也有代价。每个服务端的工具定义都会进入上下文,Anthropic 的文档中举的例子是同时接入 GitHub、Slack、Sentry、Grafana、Splunk 五个服务端,工具定义就占约 5.5 万个 Token;可用的工具超过 30~50 个后,模型选对工具的能力也会明显下降。解决方法是按需加载:上下文中只放一个搜索工具和最常用的几个工具,模型先搜索,再把找到的工具的完整定义加入上下文;也可以由应用先根据问题检索出相关的工具,只把这几个交给模型。Anthropic 还介绍过一种做法:把 MCP 工具生成为代码文件,Agent 按需读取工具的定义、编写代码调用,中间结果留在执行环境中,不经过模型,例子中的 Token 消耗从 15 万降到 2000。
+MCP 也有代价。全量加载时,每个服务端的工具定义都会进入上下文,Anthropic 的工程博客中举的例子是同时接入 GitHub、Slack、Sentry、Grafana、Splunk 五个服务端,工具定义就占约 5.5 万个 Token。Anthropic 的 Agent SDK 文档还提到,同时加载的工具超过 30~50 个后,选择工具的准确率会下降。
+
+解决方法是按需加载:上下文中只放一个搜索工具和最常用的几个工具,模型先搜索,再把找到的工具的完整定义加入上下文;也可以由应用先根据问题检索出相关的工具,只把这几个交给模型。上述博客报告的内部评测中,启用工具搜索、按需加载后,Opus 4 在 MCP 评测中的准确率从 49% 提高到 74%。
+
+Anthropic 还介绍过一种做法:把 MCP 工具生成为代码文件,Agent 按需读取工具的定义、编写代码调用,中间结果留在执行环境中,不经过模型,例子中的 Token 消耗从 15 万降到 2000。
另外,第三方服务端的工具描述和返回结果可能包含恶意指令,应该像安装软件一样只使用可信的服务端。MCP 规范仍在快速演进,本节对照 2026-07-28 版,细节以官方最新版本为准。
@@ -1793,7 +1920,11 @@ description: 按团队模板生成周报。用户要求写周报、总结本周
3. 每条不超过一句话,链接放在末尾。
```
-name 只能包含小写字母、数字和连字符,最长 64 个字符;description 最长 1024 个字符,要同时写清楚做什么和什么时候用,Agent 就是根据它判断是否加载这个 Skill。需要接入外部系统、要求权限控制的能力(例如查询公司的工单系统)适合做成 MCP 服务端;告诉 Agent 怎样组合已有工具完成任务的流程和规范,适合写成 Skill。Skill 中可以包含脚本,等同于让 Agent 执行代码,只应该使用可信来源的 Skill。
+name 只能包含小写字母、数字和连字符,最长 64 个字符;description 最长 1024 个字符,要同时写清楚做什么和什么时候用,Agent 就是根据它判断是否加载这个 Skill。
+
+需要接入外部系统、要求权限控制的能力(例如查询公司的工单系统)适合做成 MCP 服务端;告诉 Agent 怎样组合已有工具完成任务的流程和规范,适合写成 Skill。知识量大、经常更新、每次只用到其中几段的资料(例如产品手册)则适合用 RAG 检索片段,因为 Skill 的正文读入后会整段留在上下文中。
+
+Skill 中可以包含脚本,等同于让 Agent 执行代码,只应该使用可信来源的 Skill。
Skill 与系统提示词、Agent 的区别:
@@ -1827,7 +1958,7 @@ Skill 越来越多时,目录中每个 Skill 的描述只占约 100 个 Token
主 Agent 选哪个子 Agent,依据的是这些工具的名称和描述,所以要像[意图识别与路由](#意图识别与路由)一样写清各自的边界,并用评测集检验。子 Agent 的返回最好约定格式,例如结论、依据和是否完成,同时限制它的最大轮数;超时或失败时,主 Agent 重试、换一种拆法,或者如实报告这部分没有完成。
-多 Agent 的主要好处是隔离上下文:每个子 Agent 在自己的上下文中工作,可以读大量资料、反复试错,最后只把精简的结论返回,主 Agent 的上下文不会被中间过程撑满。这也是它与“一个 Agent 调用多个 Skill”的区别:Skill 的内容和执行过程都进入同一个上下文,子 Agent 的过程则不进入。
+多 Agent 的主要好处是隔离上下文:每个子 Agent 在自己的上下文中工作,可以读大量资料、反复试错,最后只把精简的结论返回,主 Agent 的上下文不会被中间过程撑满。分派时可以只传任务说明,也可以附带必要的对话历史,传入的历史越多,子 Agent 的输入越长。这也是它与“一个 Agent 调用多个 Skill”的区别:Skill 的内容和执行过程都进入同一个上下文,子 Agent 的过程则不进入。
代价也很明显:
@@ -1852,6 +1983,8 @@ Agent 之间传递信息的方式有三种:主管分派时通过工具调用
- 加锁或乐观并发:修改数据库时使用行锁或版本号,修改文件前检查文件自读取后是否被改过,被改过就重新读取再修改;
- 共享状态显式传递:Agent 之间不共享上下文,需要的信息由主 Agent 在分派任务时写进子任务的说明,子 Agent 的结论写入文件或数据库,而不是依赖对方“记得”。
+子 Agent 超时或出错时,把错误信息作为工具结果返回给主 Agent,由它调整后续的步骤。Anthropic 的多 Agent 研究系统把这种由模型做的调整与程序控制的重试、定期的检查点结合起来,长任务出错后从断点继续,而不是从头重跑,见[中断与恢复](#5-中断与恢复)。
+
#### 3. 适用场景
多 Agent 不是越多越好,以下情况用单个 Agent 更合适:
@@ -1875,11 +2008,11 @@ Agent 之间传递信息的方式有三种:主管分派时通过工具调用
- 发现:物流 Agent 在 /.well-known/agent-card.json 发布 Agent Card,写明名称、描述、技能(例如查询轨迹、改派)、服务地址和认证方式,客服 Agent 读取后决定是否委托、怎么调用;
- 任务:客服 Agent 发送一条消息,物流 Agent 创建一个带 ID 的任务(Task)。任务有状态:已提交、处理中、需要输入(例如要收件人手机号后四位)、已完成、失败、已取消等,需要输入时客服 Agent 补充一条消息后任务继续;
- 内容:消息由若干部分(Part)组成,可以是文本、文件或结构化数据,任务的产出称为 Artifact;
-- 传输:基于 HTTP,可以用 JSON-RPC 2.0、gRPC 或者 HTTP+JSON;长任务用 SSE 推送进度,或者完成后回调客户端提供的地址。
+- 传输:基于 HTTP,可以用 JSON-RPC 2.0、gRPC 或者 HTTP+JSON;长任务用 SSE 推送进度,或者由对方在任务状态变化时回调客户端登记的地址,回调可能重复送达,规范建议接收方做幂等处理。
A2A 与 MCP 的区别:
-- MCP 连接的是 Agent 与工具、数据:工具的输入输出是确定的,一次调用就结束,调用方知道工具做了什么;
+- MCP 连接的是 Agent 与工具、数据:工具通常有定义好的输入输出,执行具体的功能,往往不保存调用之间的状态;
- A2A 连接的是 Agent 与 Agent:对方是一个黑盒,自己推理、使用自己的工具,任务有状态、可能持续很久、中途可能需要多轮交互,双方都不暴露内部的提示词、记忆和工具。
两者配合使用:每个 Agent 通过 MCP 使用自己的工具,Agent 之间通过 A2A 协作。同一个应用内部的子 Agent 直接包装成工具更简单,只有跨团队、跨组织的调用才需要 A2A 这样的协议。A2A 规范仍在演进,方法名和字段以官方最新版本为准。
@@ -1894,14 +2027,16 @@ Claude Code、Cursor、Codex 这类 AI 编程助手是目前最常用的 Agent
- 项目约定与记忆:项目根目录的 CLAUDE.md(Codex、Cursor 等使用 AGENTS.md)在每次会话开始时放入上下文,写入构建和测试命令、代码规范等每次都需要的信息,官方建议每个文件不超过 200 行,内容越长越占上下文,也越难被遵守。Claude Code 还会把开发者的纠正和偏好自己记下来,会话开始时只加载这些记忆的索引 MEMORY.md;
- 上下文管理:MCP 工具平时只占名称,Skill 只占描述,用到时才加载完整的定义和正文;子 Agent 在自己的上下文中工作,只把结论返回;接近上限时自动把对话压缩成摘要,再从磁盘重新读取 CLAUDE.md 和最近读写过的几个文件,只在对话中说过的要求可能丢失;
-- 安全:用文件工具修改文件前保存快照,可以撤销,但只被命令改动过的文件不在其中;执行命令、修改文件前按权限模式决定是否请求确认,命令还可以放进操作系统的沙箱,限制能写的目录和能访问的域名。对数据库、线上接口这类外部系统的操作无法撤销,需要用权限加以限制。
+- 安全:用文件工具修改文件前保存快照,可以撤销,但只被命令改动过的文件不在其中;执行命令、修改文件前按权限模式决定是否请求确认,命令还可以放进操作系统的沙箱,限制能写的目录和能访问的域名。对数据库、线上接口这类外部系统的操作无法撤销,需要用权限加以限制;
+- 隔离范围:内置的命令沙箱只约束命令及其子进程,文件工具由权限规则控制,钩子和本地 MCP 服务端不在沙箱中,无人值守地运行、尤其是跳过权限确认时,可以用容器、虚拟机等把整个 Agent 及它启动的进程都隔离起来(具体范围以官方最新文档为准)。
用好这类工具的方法与带一个新同事类似:
-- 给出可以验证的目标,例如“让 OrderServiceTest 通过”,而不是“优化一下代码”,Agent 才能自己运行测试判断是否完成;修 bug 时先让它写一个能复现问题的测试;
+- 给出可以验证的目标,例如“让 OrderServiceTest 通过”,而不是“优化一下代码”,Agent 才能自己运行测试判断是否完成;修 bug 时先让它写一个能复现问题的测试。完成后让它给出运行的命令和输出,只说“测试都通过了”不能作为依据;
- 先让它阅读代码、给出方案,确认后再修改,避免方向一开始就错了;要与已有代码保持一致时,指明参照哪个文件;
-- 任务拆小,一次只改一个功能,便于审查;生成的代码同样要做代码审查和测试;
+- 任务拆小,一次只改一个功能,便于审查;生成的代码同样要做代码审查和测试,审查最好交给没看过编写过程的新会话或另一个人,对照需求检查改动,只报影响正确性的问题,否则会被风格上的意见淹没;
- 测试用例按需求来写,由人检查边界,例如金额为 0;照着实现补的测试,断言记录的往往是代码当前的行为,错误也会被当成预期;
+- 测试失败时,Agent 可能修改或删除测试,或者针对测试的输入写特例来让它通过,审查时要看测试文件的改动;能做到时把已有测试设为只读,并在提示词中允许它发现测试与需求矛盾时停下来说明;
- 把反复纠正的要求写进 CLAUDE.md 这类项目约定文件,而不是每次在对话中重复。
Vibe Coding 这个说法由 Andrej Karpathy 在 2025 年 2 月提出,指的是完全凭感觉让 AI 写代码:接受所有修改,不看 diff,报错了就把错误信息贴回去,代码超出了自己的理解范围也不在意。他本人认为这种方式适合周末随手做的一次性项目。Simon Willison 进一步区分:用 AI 生成代码之后,自己审查、测试过,并且能向别人解释它是怎样工作的,就不属于 Vibe Coding,而是正常的软件开发。
@@ -1910,6 +2045,8 @@ Vibe Coding 这个说法由 Andrej Karpathy 在 2025 年 2 月提出,指的是
与 Vibe Coding 相对的是规格驱动开发(Spec-Driven Development, SDD),也称 Spec Coding:先把需求和验收标准写成规格文档,人审阅后再让 AI 拆成任务逐项实现、对照规格检查;规格文档留在仓库中,是新会话和代码审查的依据。
+需要注意的是,规格写得再细,Agent 仍可能不按规格实现,实现后仍要对照规格审查改动并运行测试。完整的规格流程也可能带来大量要审查的文档,例如 Böckeler 2025 年试用 Kiro 时,一个小 bug 被展开成 4 个用户故事、16 条验收标准,所以要按改动的大小和类型简化流程。
+
### Harness
Agent 由模型和 Harness 两部分组成。Harness 指的是模型之外的所有代码、配置和执行逻辑:Agent 循环、系统提示词、工具及其描述、上下文压缩、记忆、沙箱与权限、子 Agent 的调度,以及在固定时机执行的钩子(例如每次修改文件后自动格式化)。模型本身只接收文本、输出文本,不能保存状态、执行代码、获取实时信息,这些都要由 Harness 补上。前面不到二十行的 Agent 循环就是一个最简单的 Harness;Claude Code、Codex 是功能完整的 Harness,可以换用不同的模型。MCP 和 Skill 也是 Harness 的组成部分,前者负责接入工具,后者负责按需提供做事的方法。
@@ -1980,7 +2117,9 @@ Harness 的每个组件都隐含一个假设:模型自己做不到这件事。
- 自动执行工具调用的循环,直到模型返回不含工具调用的回答;
- 提供对话记忆、RAG、结构化输出等常用组件。
-框架减少了重复代码,代价见[怎样选择](#4-怎样选择)。
+框架减少了重复代码,但也隐藏了细节:排查问题时仍然要查看实际发送给模型的消息。Anthropic 的工程博客建议先直接调用接口,用框架时要弄清它底层做了什么。
+
+核心的循环只有几十行,只用一个模型、少量工具时可以直接调用接口;需要持久化、人工介入或在多个模型之间切换时,框架的价值更大。框架与低代码平台的取舍见[怎样选择](#4-怎样选择)。
#### 1. 为什么用图编排
@@ -2041,14 +2180,22 @@ app.invoke(Command(resume=True), cfg) # 人工同
- 多轮对话:下一次请求带上同一个 thread_id,就能接着上次的状态继续;
- 人工介入:暂停时状态已经保存,等多久都不影响;
-- 容错:某个节点失败后,从最近的 checkpoint 重新执行,而不是从头开始;
+- 容错:某个节点失败后,从最近的 checkpoint 重新执行,而不是从头开始。同一步中并行的节点有的已经成功时,它们的结果单独保存,恢复时只重跑失败的节点;
- 回放:查看每一步之后的状态,定位是哪个节点写错了数据。
人工介入的过程是:节点中调用 interrupt(),图暂停并保存状态,传给 interrupt() 的内容(例如退款金额)返回给调用方,用于展示审批页面;审批之后,用同一个 thread_id 和 Command(resume=True) 再次调用,interrupt() 的返回值就是 True。
需要注意的是,恢复时被暂停的节点会从头重新执行,而不是从 interrupt() 那一行继续。在上面 confirm 节点的开头加一行打印,暂停前和恢复后各输出一次(LangGraph 1.2.18 实测,见[附录 A8](#a8-langgraph-的-checkpoint-与人工确认))。因此 interrupt() 之前的代码不能有副作用,或者副作用必须是幂等的;真正的退款放在审批之后的单独节点中,并用订单号作为幂等键。
-生产环境中 checkpoint 要存进数据库,例如 PostgreSQL 或 Redis,内存实现只适合测试。checkpoint 会随步数不断增加,需要按会话设置保留期限(Redis 的实现可以直接配置 TTL)、定期清理已经结束的会话;状态中不要放大文件,只放文件的地址。
+interrupt() 是靠抛出一个特殊的异常来暂停的,所以不要用 except Exception 这类宽泛的异常处理把它吞掉,否则无法正常暂停,后面的代码可能不经审批就执行;一个节点中有多个 interrupt() 时,恢复值按调用的顺序匹配,不要让它们的顺序随条件变化(LangGraph 文档)。
+
+生产环境中 checkpoint 要存进数据库,例如 PostgreSQL 或 Redis,内存实现只适合测试。状态中不要放大文件,只放文件的地址。
+
+checkpoint 会随步数不断增加,而且默认每一步都写入各个状态字段的完整值,包括完整的消息列表,长对话增长很快,需要按会话设置保留期限、定期清理已经结束的会话。
+
+Redis 的实现可以配置 TTL(单位为分钟,默认不过期,需要 Redis 8 或者装了 RedisJSON、RediSearch 模块的 Redis)。PostgreSQL 的实现没有 TTL,可以定期调用 prune,按命名空间只保留最新的 checkpoint 和恢复所需的数据,或者用 delete_thread 删除已经结束的会话(langgraph-checkpoint-postgres 3.2)。
+
+checkpoint 什么时候写入由 durability 参数决定:默认的 async 在执行下一步的同时写入,进程恰好崩溃时可能没写下最近一步;sync 写完再执行下一步,更可靠但更慢,对重复执行敏感的流程可以使用;exit 只在这次执行结束、报错或人工中断时写入,进程中途崩溃时中间的进度无法恢复。
#### 3. Java 项目中的 Spring AI
@@ -2070,19 +2217,21 @@ String answer = ChatClient.create(chatModel)
.content();
```
-框架根据方法签名生成参数的 JSON Schema,模型返回工具调用后,由框架执行方法、把结果发回模型,直到得到最终回答。工具抛出运行时异常时,默认把异常信息作为工具结果发回模型,让它修正或者向用户说明。需要注意的是,编译时要保留参数名(javac 的 -parameters 选项,继承 spring-boot-starter-parent 时默认开启),否则 Schema 中的参数名是 arg0(Spring AI 2.0.2 实测,见[附录 A9](#a9-spring-ai-的工具调用循环))。
+框架根据方法签名生成参数的 JSON Schema,模型返回工具调用后,由框架执行方法、把结果发回模型,直到得到最终回答。工具抛出运行时异常时,默认把异常信息作为工具结果发回模型,让它修正或者向用户说明。但并不是所有错误都会回传:模型写了不存在的工具名时,框架直接抛出 IllegalStateException,整个请求失败;框架也不会在执行工具前按生成的 Schema 校验模型传入的参数,多余的字段被忽略,包装类型的参数缺失时传入 null(Spring AI 2.0.2 源码与测试)。已经进入工具方法的参数可以在方法内部校验并抛出异常,由框架回传给模型;不存在的工具名、基本类型的参数缺失这类框架不会自动回传的错误,要自己捕获后组织成工具的错误结果,必要时接管工具调用的循环。需要注意的是,编译时要保留参数名(javac 的 -parameters 选项,继承 spring-boot-starter-parent 时默认开启),否则 Schema 中的参数名是 arg0(Spring AI 2.0.2 实测,见[附录 A9](#a9-spring-ai-的工具调用循环))。
对话记忆由 ChatMemory 接口管理:默认的 MessageWindowChatMemory 保留最近的 20 条消息,系统消息始终保留;消息存放在 ChatMemoryRepository 中,默认在内存里,也可以换成 JDBC、Cassandra 等实现。使用时给 ChatClient 加上 MessageChatMemoryAdvisor,每次调用传入会话 ID,它在调用前按会话 ID 取出历史放进请求,调用后保存新的消息。需要注意的是,默认情况下工具调用的请求和结果不会写入记忆,下一轮对话时模型看不到上一轮查过什么(Spring AI 2.0.2 实测,见[附录 A28](#a28-spring-ai-的对话记忆与工具调用))。这是因为 MessageChatMemoryAdvisor 默认排在工具调用循环之外,只保存最终的用户消息和回答。
需要保存工具记录时,可以把 MessageChatMemoryAdvisor 的 order 设为大于 ToolCallingAdvisor 的值;Spring AI 2.0 的内置仓库中只有内存、Redis、Neo4j 能保存工具消息,JDBC 不能(以官方最新文档为准),用 JDBC 时由应用自己保存,见[短期记忆](#1-短期记忆)。
+可观测方面,Spring AI 基于 Micrometer Observation 为 ChatClient 调用、Advisor、每次模型调用和工具执行生成 span,模型调用的 span 上带模型名和 Token 数;提示词、回答和工具参数默认不记录,需要时用配置项开启(以官方最新文档为准)。它的 span 名称和层级与[可观测](#5-可观测)一节按 OpenTelemetry 约定画的示例不同,例如没有 invoke_agent 这一层,由 ChatClient 和 Advisor 的 span 充当。
+
Spring AI 主要解决模型接入和单个 Agent 的工具调用。Java 中另一个常用的框架是 LangChain4j,它不依赖 Spring,可以集成到 Spring Boot 或 Quarkus 中,特点是 AI Services:声明一个接口,框架生成实现这个接口的代理对象,与 Spring Data JPA 的 Repository 类似。两者的功能相近,Spring 项目用 Spring AI 与已有的配置、依赖注入结合得更自然。需要状态图编排时,可以用仿照 LangGraph 的 LangGraph4j,它可以与 LangChain4j 或 Spring AI 一起使用。
#### 4. 怎样选择
Dify、Coze 这类低代码平台用拖拽的方式编排工作流,内置模型调用、知识库检索、问题分类、条件分支、工具调用和人工介入等节点,搭好后可以直接发布成接口或网页。
-- 低代码平台:搭建快,产品和运营人员也能参与,调试界面直观;但复杂的循环、细粒度的错误处理、与现有系统的深度集成比较困难,流程的版本管理和代码评审也不方便;
+- 低代码平台:搭建快,产品和运营人员也能参与,调试界面直观。平台也提供循环、失败分支、重试和版本发布,但能力受节点的限制,例如 Dify 的循环中任何一个子节点失败,整个循环就终止,代码节点只能使用沙箱中预装的包;流程可以导出为 YAML 文件放进 Git,评审的对象是平台生成的配置和其中代码节点的代码;
- 代码框架:灵活,可以像普通代码一样测试、评审和发布;但开发成本更高,需要自己搭建调试和监控工具。
通常的取舍是:验证想法、流程标准的内部工具用低代码平台;核心业务、对可靠性和集成要求高的系统用代码框架。两者也可以组合,例如平台中的流程通过接口调用自研的服务。
@@ -2119,7 +2268,7 @@ Agent 的每一步都可能出错,容错的逻辑要写在控制循环(Harne
工具出错时,按错误的类型分别处理:
-- 临时性错误,例如超时、限流、服务端 5xx:由代码按指数退避重试 2~3 次,模型不需要知道,做法与调用模型接口时相同,见[稳定性](#稳定性);
+- 临时性错误,例如超时、限流、服务端 5xx:由代码按指数退避重试 2~3 次,模型不需要知道,做法与调用模型接口时相同,见[稳定性](#稳定性)。修改数据的工具只有在确认操作幂等时才自动重试,例如下游支持幂等键;否则超时后先查询这次操作的状态,确认原请求没有生效、之后也不会再生效时才重试;
- 模型可以修正的错误,例如参数格式不对、取值不在范围内:把错误信息作为工具结果回传给模型,信息中写明错在哪、正确的格式是什么;
- 重试后仍然失败:把失败如实回传给模型,由它换一种办法,或者告诉用户哪一步失败了、返回已经拿到的部分结果、转人工,不能让模型编造结果。
@@ -2136,23 +2285,27 @@ assistant 调用 refund({"order_id": "1024", "amount": 59.9})
tool 退款申请已提交,单号 R88
```
-一次回复中并行调用了多个工具、其中一部分失败时,成功的结果照常返回,失败的单独标明错误,由模型决定是否只重试失败的那一个。
+一次回复中并行调用了多个工具、其中一部分失败时,成功的结果照常返回,失败的单独标明错误,由模型决定是否只重试失败的那一个。每个调用都要回传一条结果,没有执行的也要回一条错误,Anthropic 的接口要求每个 tool_use 都有对应的 tool_result。
工具持续失败时,例如物流接口宕机,可以像模型接口一样[熔断](#3-限流降级与熔断),直接返回“暂不可用”,不必每个请求都先等超时再重试。
死循环的原因是模型每一步只看当前的上下文做决定:工具反复返回同样的结果时,上下文几乎不变,它就可能一次次做出同样的决定;没有明确的完成标准时,它可能一直查下去。因此控制循环中要有这些限制:
-- 限制步数、总耗时和 Token 预算,超出时停止,返回已有的结果或者转人工。步数上限参考评测中成功任务用的步数,例如取 P99 再留些余量;
-- 检测有没有进展:合理的循环每一轮都有进展,例如翻页;同样的参数连续几次得到同样的结果,很可能是在原地打转。所以要同时比较工具名、参数和结果中的关键字段,而不只是数工具调用了几次,连续几次没有进展时中止,或者提示模型换一种方法。等待异步任务完成时不要让模型反复查询,做法见 [Agent 循环](#agent-循环);
+- 限制步数、总耗时和 Token 预算,超出时停止,返回已有的结果或者转人工。步数上限参考评测中成功任务用的步数,例如取 P99 再留些余量,并显式设置,不依赖框架的默认值,例如 OpenAI Agents SDK v0.24 默认 10 次模型调用,LangGraph 1.2 默认 10007 个图步数,Claude Agent SDK 默认不限;
+- 检测有没有进展:合理的循环每一轮都有进展,例如翻页;同样的参数连续几次得到同样的结果,很可能是在原地打转。所以要同时比较工具名、参数和结果中的关键字段,而不只是数工具调用了几次,连续几次没有进展时先提示模型换一种方法,再次出现才中止(例如 Gemini CLI v0.66),避免误伤正常的重试。等待异步任务完成时不要让模型反复查询,做法见 [Agent 循环](#agent-循环);
- 重规划、反思、审核退回、Agent 之间的转交都要设次数上限。
有副作用的操作要额外处理:
-- 幂等:修改数据的工具超时,操作可能已经执行,只是响应没有返回;模型重复调用、从 checkpoint 恢复时节点重新执行,也会让同一个操作执行两次。所以这类工具要用幂等键去重,重试时沿用同一个键,例如同一个订单只能有一条进行中的退款,见[幂等](分布式.md#九幂等);
-- 人工确认:转账、删除数据、对外发送信息等操作由代码校验权限,执行前暂停,把操作内容展示给人确认,实现见 [checkpoint 与人工介入](#2-checkpoint-与人工介入);
-- 补偿:多步操作中后面的步骤失败、前面的需要撤销时,执行对应的补偿操作,例如取消已提交的退款申请,思路与 [Saga](分布式.md#saga) 相同;
+- 幂等:修改数据的工具超时,操作可能已经执行,只是响应没有返回;模型重复调用、从 checkpoint 恢复时节点重新执行,也会让同一个操作执行两次。所以这类工具要用幂等键去重,重试时沿用同一个键,例如同一个订单只能有一条进行中的退款,见[幂等](分布式.md#九幂等)。幂等键由程序在调用前生成并随任务保存,不让模型生成;参数变了就是新的操作,换新的键并重新确认。框架的自动重试(例如 LangGraph 节点的 RetryPolicy)也是重跑整个节点,所以自动重试的节点应当没有副作用,或者副作用是幂等的;
+- 人工确认:转账、删除数据、对外发送信息等操作由代码校验权限,执行前暂停,把操作内容展示给人确认,实现见 [checkpoint 与人工介入](#2-checkpoint-与人工介入),待确认的操作和参数保存在服务端,确认请求只带操作 ID 和决定,同一个操作只能被确认执行一次(例如原子地把状态从待确认改为执行中),避免重复提交或确认的内容被替换;
+- 补偿:多步操作中后面的步骤失败、前面的需要撤销时,执行对应的补偿操作,例如取消已提交的退款申请,思路与 [Saga](分布式.md#saga) 相同。只补偿已确认成功的步骤,下游既不支持幂等、也查询不到结果的步骤标为结果未知,转人工或对账,不自动重试,也不自动补偿;
- 隔离:执行代码的工具放在沙箱中运行,并记录审计日志。
+沙箱一般要限制可写的路径(工作目录和必要的临时目录等)、网络(默认拒绝或按域名白名单放行)、CPU 和内存,并由调用方计时、超时后终止。密钥不放进沙箱,可以由沙箱外的代理在转发请求时加上,沙箱里的代码即使被诱导也读不到密钥。
+
+常见的隔离机制有三种:共享宿主机内核的容器,配合 seccomp 限制可用的系统调用;gVisor 在用户态实现 Linux 的内核接口,容器中的程序不直接调用宿主机内核;Firecracker 这类轻量虚拟机有独立的内核,借助硬件虚拟化隔离。隔离效果取决于配置,开销取决于实现和工作负载,按要防范的风险选择。
+
确认得太频繁,人会不看内容就点同意,确认也就失去了作用。所以只确认涉及资金、无法撤销或者对外可见的操作,只读的查询、影响小且可以撤销的操作(例如创建售后工单)、在接触不到真实数据的沙箱中运行的代码自动执行并记录日志;需要确认的操作也可以按规则放行一部分,例如金额低于阈值的退款。
#### 2. 评测
@@ -2194,7 +2347,7 @@ checks:
- 位置偏差:比较两个回答时,倾向于选排在某个位置的那个,交换顺序后结论就变了。MT-Bench 的论文中,两个回答质量相近时,GPT-4 交换顺序后结论一致的比例只有 65%;
- 长度偏差:倾向于更长的回答,即使多出来的内容没有新信息。同一论文中,把回答里的列表换个说法重复一遍,Claude-v1 和 GPT-3.5 有 91.3% 的情况认为加长后的回答更好,GPT-4 为 8.7%;
-- 自我偏好:给自己生成的回答打更高的分。有研究发现 GPT-4、Llama 2 能在一定程度上认出自己的输出,认出的能力越强,这种偏差越大。
+- 自我偏好:给自己生成的回答打更高的分。有研究在摘要任务上发现 GPT-4、Llama 2 能在一定程度上认出自己的输出;对 GPT-3.5 和 Llama 2 的微调实验中,认出自己的能力越强,这种偏差也越大(arXiv 2404.13076)。
用 Qwen2.5-7B 作裁判做了一个小实验:12 对客服回答分为三组,每对按两种顺序各判一次:
@@ -2212,24 +2365,34 @@ checks:
#### 4. 评测集与线上指标
-评测集从 20~50 条真实任务开始就有价值,不必等到几百条:
+评测集从 20~50 条真实任务开始就有价值,不必等到几百条,只是这时只能发现明显的变化,例如 50 条用例上准确率为 70% 时,95% 的置信区间约为 ±13 个百分点(比较方法见下文):
- 来源:线上日志、转人工和差评的会话、测试人员发现的问题,每个修过的错误都变成一条用例;
- 覆盖:按意图和难度分层,正例和反例都要有,例如既有应该退款的,也有超过退款期限、应该拒绝的;
- 标准:每条任务写清楚怎样算成功,两个人独立判断能得出相同的结论,否则先改任务描述;
-- 环境:每次从干净的数据开始运行,避免上一次运行留下的状态影响结果;多轮对话的任务可以让另一个模型按设定的用户信息和诉求扮演用户,τ-bench 就是这样做的。
+- 环境:每次从干净的数据开始运行,避免上一次运行留下的状态影响结果,资源上限、超时这类配置固定并记录,基础设施导致的失败单独统计;多轮对话的任务可以让另一个模型按设定的用户信息和诉求扮演用户,τ-bench 就是这样做的。
每次修改提示词、工具或者模型,都在同一批任务上回归,并记录每次运行的完整轨迹,出错时才能复现和定位。评测分数上不去时,先读几条失败的轨迹,确认是 Agent 真的做错了,而不是评分规则把正确的做法判错了;再按出错的环节归类,例如检索没有召回、选错工具、参数错误、拿到了正确的工具结果却总结错,占比最高的一类就是优先要改的瓶颈。
反复对着同一批用例改提示词,分数会越来越高,但可能只是记住了这些用例。可以按同样的分层留出一部分用例,平时不看、只在大的改动后运行:平时用的用例分数持续上升,留出的用例却没有改善时,就要怀疑过拟合,用例少时还要多跑几次排除随机波动。
+比较两个版本时还要考虑用例的数量。假设 200 条用例是从真实问题中独立随机抽取的,测得准确率为 80% 时,总体准确率的 95% 置信区间约为 74.5%~85.5%(按正态近似),只看总分,80% 对 84% 判断不了差异是否可靠。要在同一批用例上逐条对比两个版本:新版本改对了 8 条、没有改错任何一条,与改对 20 条、改错 12 条,总分都是 84%,前者的提升可靠得多。
+
+选模型时可以先参考公开的基准(例如测多学科选择题的 MMLU、测修复真实 GitHub issue 的 SWE-bench),但还要用自己的业务评测集验证:题目可能已经进了训练数据,按 GSM8K 新出一套同类的题时,部分模型的准确率下降了多达 8 个百分点(GSM1k);题目本身也可能有错,估计 MMLU 约有 6.49% 的题有错(MMLU-Redux)。
+
+选型时先按硬条件筛选:数据能否交给外部服务、许可是否允许商用,上下文和输出长度是否够用,是否支持需要的工具调用和结构化输出;再在业务评测集上比较剩下几个模型的效果、延迟和每次任务的费用。
+
+服务商的模型名还可能改指新的模型,旧模型也会下线,例如 DeepSeek 的 deepseek-chat 先后对应过 11 个底层模型,阿里云百炼的正式版模型在启动下线后 3 个月停止服务。服务商提供固定版本的模型名(例如带日期的快照)时,生产环境尽量用它,换模型前在同一批用例上逐条对比(以各服务商的最新文档为准)。
+
上线后关注的指标:
- 效果:任务完成率、转人工率、用户重复提问或者改口的比例、满意度和差评;
- 过程:工具调用的错误率、平均步数、触发最大步数和重试上限的次数;
- 性能与成本:首个 Token 的延迟、总耗时、每次会话的 Token 消耗。
-失败的线上会话经过脱敏后加入评测集,形成“发现问题、加用例、修改、回归”的循环。
+差评和转人工只能发现一部分失败的会话:大多数用户不会反馈,反馈了也很少说明原因。所以还要按意图定期抽样阅读会话,并用模型裁判检查线上会话,例如回复与工具返回的结果是否一致,筛出可疑的会话再由人工确认。
+
+失败的线上会话经过脱敏和人工标注后,按出错的原因处理:资料缺失的补进知识库,提示词或工具有问题的修改对应部分,需要微调的整理成训练样本。按会话划分训练样本和评测用例,同一个会话不同时进入两者,否则评测分数会偏高;修改后用评测集回归,形成“发现问题、加用例、修改、回归”的循环。
## 五、工程实践
@@ -2254,12 +2417,12 @@ data: [DONE]
- 每条事件由若干行“字段: 值”组成,以空行结束,data 是事件的内容(一条事件有多行 data 时按换行拼接),id 是事件的编号,event 是事件的类型(省略时为 message);
- 以冒号开头的行是注释,客户端会忽略,常用作心跳,防止中间的代理因为连接长时间空闲而断开;
-- 浏览器的 EventSource 断线后会自动重连,并在请求首部 Last-Event-ID 中带上最后收到的事件编号,服务端可以据此从断点继续发送;
+- 浏览器的 EventSource 断线后会自动重连,并在请求首部 Last-Event-ID 中带上最后收到的事件编号,服务端可以据此从断点继续发送。EventSource 只能发 GET 请求、不能带请求体,对话接口用 POST 发送问题时,前端通常用 fetch 读取流,这时重连和 Last-Event-ID 都要自己实现;
- OpenAI 兼容的接口用 data: [DONE] 表示生成结束。
SSE 与 WebSocket 的区别:
-- SSE 基于普通的 HTTP 响应,只能由服务端向客户端推送,自带断线重连,正好符合“请求一次、持续返回”的场景;
+- SSE 基于普通的 HTTP 响应,只能由服务端向客户端推送,用浏览器的 EventSource 接收时自带断线重连,正好符合“请求一次、持续返回”的场景;
- WebSocket 是全双工的独立协议,适合客户端和服务端都需要随时发送消息的场景,例如实时语音对话。
后端调用模型的流式接口时,逐行读取响应,读到一行就处理一行:
@@ -2320,10 +2483,10 @@ LangGraph 等框架也提供类似的机制,按节点输出状态的更新或
- Nginx 等反向代理默认会缓冲上游的响应,流式输出变成了一次性输出,需要关闭缓冲,例如在响应首部中加上 X-Accel-Buffering: no;
- 网关和负载均衡的读超时要大于最长的生成时间;
-- 用户关闭页面后要及时取消对模型的请求,否则仍在消耗 Token;
-- 网络抖动导致断线时,模型的生成无法从中途重新开始。需要续传时,让生成任务与连接解耦:生成的内容按会话和事件编号写入 Redis 等缓存,客户端带着 Last-Event-ID 重连后,先从缓存中补发之后的事件,再继续推送新的内容,超时没有重连再取消生成;
+- 不支持续传时,用户关闭页面、连接断开后要及时取消对模型的请求,否则仍在消耗 Token,但断开能否停止生成和计费取决于服务商,例如 OpenRouter 的文档写明,非流式请求或不支持取消的服务商会继续处理并按完整的回答计费,所以还要用 max_tokens 限制输出的上限;
+- 网络抖动导致断线时,模型的生成无法从中途重新开始。需要续传时,让生成任务与连接解耦:生成的内容按会话和事件编号写入 Redis 等缓存,客户端带着 Last-Event-ID 重连后,先从缓存中补发之后的事件,再继续推送新的内容,超时没有重连再取消生成。这时断开连接不等于停止生成,“停止”按钮要调用单独的接口;补发缓存和开始订阅新事件之间产生的事件要一并补上,否则会漏;生成中的增量先写缓存,避免每个 Token 都写数据库,正常结束时保存完整的回答,停止时保存已经生成的部分并标记为未完成;
- Agent 任务断线后通常继续执行到当前步骤结束,已经发起的退款这类有副作用的工具调用不能中途放弃,结果写入缓存,等客户端重连后补发;
-- 工具调用的参数也是分片段流式返回的,需要按调用的序号拼接,生成结束后才能解析出完整的 JSON。
+- 工具调用的参数也是分片段流式返回的,需要按调用的序号拼接,这个调用结束后才能解析出完整的 JSON;结束原因是长度截断(finish_reason 为 length、stop_reason 为 max_tokens)时参数可能不完整,不能执行工具,要重试或把错误回传给模型。
### 延迟与成本
@@ -2352,6 +2515,8 @@ LangGraph 等框架也提供类似的机制,按节点输出状态的更新或
不考虑缓存时一次对话约 0.27 分,前缀全部命中缓存时约 0.082 分;每天 10 万次对话,分别约 270 元和 82 元(见[附录 A20](#a20-agent-任务的-token-数与前缀缓存))。
+这里按关闭思考模式计算,输出只有回答本身。deepseek-flash 默认开启思考模式,思维链也按输出计费,例如每次调用思考 1000 个 Token,3 次调用仅思维链就约 2.4 分,是上面前缀全部命中时的近 30 倍;带工具调用时,之前轮次的思维链还要回传,计入后面请求的输入。
+
这个例子的工具和提示词都很短。实际的 Agent 往往有几千 Token 的系统提示词和工具定义,每一步还会加入大段的工具结果。假设系统提示词和工具定义共 5000 个 Token,每一步新增 2000 个 Token,第 10 步的输入是 2.3 万个 Token,但 10 步累计的输入达到 14 万个 Token:
@@ -2369,15 +2534,21 @@ LangGraph 等框架也提供类似的机制,按节点输出状态的更新或
缓存是否命中可以从接口返回的用量中查看,例如 DeepSeek 返回的 prompt_cache_hit_tokens 和 prompt_cache_miss_tokens。需要注意的是,服务端的缓存是尽力而为的,构建需要时间,一段时间不用就会被清除,不能保证 100% 命中。
+前缀相同也不一定命中,因为服务端只在部分位置写入缓存。例如 DeepSeek 在每次请求的输入结束和输出结束处、多次请求的公共前缀处,以及长内容中每隔一定数量的 Token 写入,后一次请求要完整包含某个已写入的前缀才能命中;Anthropic 在 cache_control 指定的断点处写入。缓存的前缀还有最小长度,例如 OpenAI 较新的模型为 1024 个 Token,可以复用的前缀不到这个长度时不会命中。
+
+部分服务商对写入缓存加价,例如 Anthropic 5 分钟缓存的写入按输入单价的 1.25 倍计费,1 小时缓存按 2 倍,只写不读反而更贵。这些规则变化较快,以各家的官方文档为准。
+
#### 4. 减少 Token 消耗
前缀缓存降低的是单价,减少 Token 的数量要从上下文和输出两头入手:
- 控制工具返回的内容:工具默认返回摘要或者片段和文档编号,模型判断需要细节时再调用工具读取全文。例如检索返回 5 个片段约 1500 个 Token,把 5 篇文档的全文都放进去约 3 万个 Token,后者每一次调用的输入费用是前者的 20 倍,而且后面的每一步都要带着它;
-- 限制输出长度:设置 max_tokens,要求回答简洁。输出的单价是未命中缓存的输入的 4 倍;
+- 限制输出长度:设置 max_tokens,要求回答简洁。以上面的 deepseek-flash 为例,输出的单价是未命中缓存的输入的 4 倍;
- 按难度选择模型:先用小模型判断问题的类型,查物流、改地址这类简单问题由小模型完成,复杂的投诉交给大模型。路由会分错,需要在评测集上确认整体效果没有下降。
-完全相同的问题还可以直接返回缓存的结果。用 Embedding 判断问题是否相近的语义缓存命中率更高,但容易把不同的问题当成同一个,例如“订单 1024 到哪了”和“订单 1025 到哪了”的 Embedding 非常接近。
+完全相同的问题还可以直接返回缓存的结果。用 Embedding 判断问题是否相近的语义缓存命中率更高,但容易把不同的问题当成同一个。例如用 bge-small-zh-v1.5 计算余弦相似度,“订单 1024 到哪了”和“订单 1025 到哪了”为 0.82,“会员可以免运费吗”和“非会员可以免运费吗”为 0.95,都高于意思相同的“订单 1024 到哪了”和“我的快递 1024 现在在哪”(0.73),只靠阈值分不开(见[附录 A32](#a32-语义缓存的相似度))。
+
+所以涉及用户私有数据或个性化的回答时,语义缓存要按用户分区;订单号、日期这类会改变答案的参数要抽出来,要求完全相同;依赖实时数据(例如物流状态)的问题不缓存最终的回答。
Token 消耗增长过快时,先按步骤和工具汇总每一次调用的输入、命中缓存和输出的 Token 数。常见的原因正是上面几点没有做到:某个工具返回了大段的原始数据,模型反复调用同一个工具,历史一直没有压缩,或者修改提示词后在开头加入了会变化的内容,命中率下降。
@@ -2394,9 +2565,15 @@ Token 消耗增长过快时,先按步骤和工具汇总每一次调用的输
- 重复计算相同的开头:[前缀缓存](#5-前缀缓存);
- 只能逐个 Token 解码:投机解码(Speculative Decoding),小模型先连续猜几个 Token,大模型读一遍权重就能同时验证:设大、小模型给某个猜测的概率分别为 p、q,它以 min(1, p / q) 的概率被接受;第一个被拒绝的位置改从 max(0, p − q) 归一化后的分布重新采样,丢弃后面的猜测。这样输出的分布与大模型单独生成相同,适合并发不高的场景。
+投机解码的草稿也可以不来自独立的小模型:Medusa 在大模型上加几个解码头,同时预测后面的多个 Token,它常用的宽松接受规则不保证输出分布不变;EAGLE 用一个很小的网络,根据大模型输出层之前的隐藏状态和已经采样出的 Token 逐个预测后面的特征,再用原来的输出层得到草稿;DeepSeek-V3 预训练时带有多 Token 预测(Multi-Token Prediction, MTP)模块,推理时可以用它猜第 2 个 Token,技术报告的评测中接受率为 85%~90%,每秒生成的 Token 数达到原来的 1.8 倍(arXiv 2401.10774、2401.15077、2412.19437)。
+
#### 1. 批处理:用延迟换吞吐
-解码阶段每生成一个 Token,都要把全部权重从显存中读一遍,而计算量很小,速度主要受显存带宽限制。例如 A100 80GB 的显存带宽为 2039 GB/s,Qwen2.5-7B 的 BF16 权重约 152 亿字节,每一步至少需要 7.5 毫秒,单个请求每秒最多生成约 133 个 Token。
+解码阶段每生成一个 Token,都要把全部权重从显存中读一遍,而计算量很小,速度主要受显存带宽限制。例如 A100 80GB 的显存带宽为 2039 GB/s,Qwen2.5-7B 的 BF16 权重约 152 亿字节,每一步至少需要 7.5 毫秒,单个请求每秒最多生成约 134 个 Token。
+
+预填充则不同,读一遍权重就能处理全部输入,每个 Token 要做约 13 GFLOP 的矩阵乘法(各层线性权重约 65 亿个参数,每个参数一次乘加;输出层只为最后一个 Token 计算,忽略不计)。输入越长,计算越多,读权重的时间却不变,所以长输入的预填充受算力限制。
+
+按 A100 的 BF16 峰值 312 TFLOPS 估算,Qwen2.5-7B 处理 2048 个 Token 的输入至少约 0.09 秒,输入加倍到 4096 个时约 0.18 秒,略多于两倍,因为注意力的计算量与长度的平方成正比。实际达不到峰值算力,这些只是下限。
把多个请求放在同一批中,读一次权重就能为每个请求各生成一个 Token。批次越大,吞吐量越高;代价是每一步还要读取每个请求的 KV Cache、计算量也随之增加,每一步变慢,TPOT 变大。按带宽估算,32 个各有 2K Token 上下文的请求组成一批时,每一步至少 9.3 毫秒,吞吐上限约 3400 Token/s,是单个请求的 26 倍。
@@ -2410,6 +2587,10 @@ batch 从 1 增大到 64,总吞吐从 22 Token/s 提高到 214 Token/s,约 9
需要注意的是,同一个请求在不同大小的批次中,浮点运算的顺序可能不同,结果可能出现细微的差别(附录 A21 的实验中,各个 batch 大小下的贪心输出一致)。对可复现性要求高时,vLLM 提供批次不变(Batch Invariance)的模式,代价是速度可能变慢。
+前两名 Token 的分数非常接近时,这种细微的差别可能改变贪心选出的 Token,之后的上下文随之改变,生成的内容就可能分叉。例如 Thinking Machines 的实验用 Qwen3-235B-A22B-Instruct-2507 以温度 0 对同一个提示词生成 1000 次,前 102 个 Token 都相同,最终得到 80 种不同的结果;换成批次不变的算子后,1000 次完全相同。
+
+接口的 seed 参数只固定采样用的随机数,不能消除这类差别。OpenAI 的接口文档把 seed 写作尽力而为、不保证确定,Anthropic 的文档也写明温度为 0 时结果并不完全确定。
+
#### 2. 连续批处理
传统的静态批处理把一批请求一起送进模型,等这一批全部生成结束才接收新的请求。各个请求的输出长度相差很大,先结束的请求只能空等,新来的请求也要排队:
@@ -2447,7 +2628,7 @@ PagedAttention 借鉴操作系统的[分页](计算机操作系统%20-%20内存
同样 7 GB 的显存,按 8K 预留只能服务 16 个请求;按块分配时,如果请求平均只用到 2K Token,可以服务 64 个。批次越大吞吐越高,vLLM 论文中报告,在延迟相近的情况下吞吐量是 FasterTransformer、Orca 等系统的 2~4 倍。Ollama 则按“并行数 × 上下文长度”预留内存,适合本机试用,多用户的在线服务一般用 vLLM、SGLang。
-PagedAttention 管 KV Cache 放在哪里,FlashAttention 管注意力怎么算,两者可以同时使用,例如 vLLM 用支持块表的 FlashAttention 内核读取不连续的块。
+PagedAttention 管 KV Cache 放在哪里,并为此实现了按块表读取不连续块的注意力内核;FlashAttention 管注意力怎么算,减少中间矩阵的显存读写。两者可以同时使用,flash-attn 从 2.5 版起支持分页的 KV Cache,vLLM 的 FlashAttention 后端把块表传给它的内核。
#### 5. 前缀缓存
@@ -2457,9 +2638,15 @@ vLLM 的实现基于 PagedAttention 的块:每个写满的块用“前面所
模型服务商对命中缓存的输入按更低的价格计费,就是因为这部分不需要重新计算。需要注意的是,缓存保存在每个实例自己的显存中,部署多个实例时,同一个会话的请求应该尽量路由到同一个实例,否则缓存在别的实例上,无法命中。
+不同会话之间也能共享前缀,例如所有用户共用的系统提示词。这时路由器可以记录每个实例处理过哪些前缀,把开头相同的请求发到同一个实例。但命中缓存的实例可能正忙,排队的时间比重新预填充还长,所以路由器要在命中和负载之间折中。以 SGLang 的缓存感知路由为例:各实例待处理的请求数相差悬殊(差值和比值都超过阈值)时,直接选最空闲的实例;否则,前缀匹配的比例超过阈值才选匹配最长的实例,没有超过时仍选最空闲的实例(以框架的最新版本为准)。
+
+整个提示词都命中缓存时,最后一个 Token 仍要计算才能得到下一个 Token 的概率,vLLM 因为要按块对齐,会重算最后一整块,例如 256 个 Token 的提示词全部命中时复用 240 个。
+
+多个租户共用一个服务时,攻击者可能通过首 Token 的延迟推断别人的请求中有没有某段内容,vLLM 支持在请求中加 cache_salt,只有 salt 相同的请求才能复用缓存(vLLM 0.32 源码与文档)。
+
### 稳定性
-模型接口比普通的后端接口更慢,也更容易失败:生成一段回答要几秒到几十秒,请求可能在服务端排队;超过请求频率、Token 速率或者并发数的限制时返回 429,服务端故障或者负载过高时返回 500、503;流式输出还可能在中途断开。另一类错误是请求本身有问题,例如输入超过上下文窗口时返回 400,重试也不会成功。
+模型接口比普通的后端接口更慢,也更容易失败:生成一段回答要几秒到几十秒,请求可能在服务端排队;超过请求频率、Token 速率或者并发数的限制时返回 429,服务端故障或者负载过高时返回 500、503(Anthropic 的过载为 529);流式输出还可能在中途断开。另一类错误是请求本身有问题,例如输入超过上下文窗口时返回 400,重试也不会成功。
应对这些问题的手段从内到外是:为每次调用设置合适的超时,可恢复的错误按退避重试,重试仍然失败或者主模型持续出错时降级到备用模型,最后返回兜底的结果:
@@ -2479,7 +2666,7 @@ vLLM 的实现基于 PagedAttention 的块:每个写满的块用“前面所
#### 2. 重试与指数退避
-429、5xx 和超时可以重试,400、401 这类错误重试也不会成功,直接失败。重试的间隔按指数增长,例如 0.5 秒、1 秒、2 秒,并加入随机抖动,避免大量请求在同一时刻一起重试;响应中带有 Retry-After 首部时,按它指定的时间等待。
+临时限流导致的 429、5xx 和超时可以重试,400、401 这类错误重试也不会成功,直接失败。重试的间隔按指数增长,例如 0.5 秒、1 秒、2 秒,并加入随机抖动,避免大量请求在同一时刻一起重试;响应中带有 Retry-After 首部时,按它指定的时间等待。
```java
static Reply chatWithRetry(Request req) throws LlmException {
@@ -2499,7 +2686,9 @@ static Reply chatWithRetry(Request req) throws LlmException {
}
```
-OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx 重试 2 次,初始间隔 0.5 秒、最大 8 秒,并遵守 Retry-After。
+OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx 重试 2 次,初始间隔 0.5 秒、最大 8 秒,并遵守 Retry-After。但 Retry-After 过长时各语言的 SDK 处理不同,例如 Python 版超过 120 秒就不再重试,Node 版超过 60 秒则改用默认的退避间隔(openai-python 3.42、openai-node 7.47,以所用版本为准)。
+
+花费达到上限、配额用完时也可能返回 429。例如 Anthropic 达到月度花费上限时,返回的 429 与普通限流一样是 rate_limit_error,但没有 Retry-After,error.details.error_code 为 enforced_spend_limit_reached。包括 SDK 的自动重试在内,重试都不会成功,所以要在代码中识别它、停止重试,等额度重置或提高额度后再恢复。
重试次数要有上限。服务已经过载时,每一层都重试 3 次,请求量会被成倍放大,反而更难恢复。Agent 中模型调用和工具调用嵌套时,只在一层重试。工具调用的重试和容错见[可靠性与评测](#可靠性与评测)。
@@ -2507,6 +2696,14 @@ OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx
收到 429 再退避是被动的。大量 Agent 任务同时调用模型时,在调用方主动限流更好:按服务商的限额设置并发数上限和令牌桶,超出的请求排队等待,而不是发出去再被拒绝。
+服务商的限额一般按账号或组织计算,多个业务共用一份,例如 DeepSeek 的并发限制按账号计、与 API Key 无关。网关可以给各业务预留保底额度、空闲时允许借用;共享的是 Token 配额时,按 Token 用量分配份额,避免请求大的业务占去大部分额度。
+
+严格按优先级排队时,低优先级的请求可能一直排不上,要限制它的排队数量和等待时间,排队超时就拒绝;业务允许时,也可以转为异步任务。
+
+按 Token 数限流时,输出有多少 Token 要等生成完才知道。如果只在请求结束后累加,并发的请求会一起放行,例如每分钟限额 10000、20 个并发请求各用 1500,事后累加时 20 个都已放行,实际用了 30000。要做硬上限,就在请求前按“输入 + max_tokens”预扣,完成后按实际用量结算、退回多扣的部分,所以 max_tokens 应设得接近预期的长度。
+
+流式请求的用量也要留意,例如 OpenAI 的 Chat Completions 要设置 stream_options.include_usage,才会在结束前多收到一个带总用量的分块,流中途断开时可能收不到它。这时计费和统计先按已收到的内容估算,再与服务商的账单对账。
+
主模型不可用时降级:先切换到能力相近的备用模型,可以是另一家服务商;仍然失败时返回兜底的结果,例如“系统繁忙,已为您转人工客服”。备用模型的提示词、工具调用格式可能不同,上线前要在同一批评测任务上验证效果。
熔断用于避免每个请求都先等主模型超时再切换。主模型在一段时间内的失败率超过阈值时,熔断器打开,之后的请求直接走备用模型,过一段时间再放行少量请求试探主模型是否恢复。限流、降级和熔断的原理见[系统设计基础](系统设计基础.md#限流降级与熔断)。
@@ -2523,6 +2720,19 @@ OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx
记录每一次模型调用和工具调用的输入、输出、耗时、Token 数和错误码,以及使用的模型和提示词版本,出错时才能复现和定位问题。还要监控失败率、TTFT、TPOT 的分位数和缓存命中率,在用户感知之前发现模型服务变慢或者出错。
+Agent 的一次请求记为一条链路(trace),以这次 Agent 调用为父 span,每次模型调用和工具执行是它下面的子 span,多轮对话再用会话 ID 把多条链路关联起来,原理与[链路追踪](微服务.md#八链路追踪)相同。例如“订单 1024 到哪了”这次请求(省略了外层的 span):
+
+```text
+invoke_agent 客服 Agent 会话 ID
+├─ chat deepseek-flash 输入、输出的 Token 数,结束原因为 tool_calls
+├─ execute_tool get_logistics 耗时、是否出错,开启后还记录参数 order_id = 1024
+└─ chat deepseek-flash 输入、输出的 Token 数,结束原因为 stop
+```
+
+OpenTelemetry 的 GenAI 语义约定为这类 span 定义了 invoke_agent、chat、execute_tool 等操作名和 gen_ai.* 属性,目前仍处于开发阶段,以官方最新版本为准。
+
+提示词、消息内容和工具的参数、结果可能包含个人信息,约定把它们列为需要显式开启才记录的属性;需要保存时可以存到单独控制权限的地方,链路上只记引用。
+
### 安全
#### 1. 提示词注入
@@ -2541,6 +2751,7 @@ OpenAI 的官方 SDK 默认也是这样处理的:对 408、409、429 和 5xx
- 工具按最小权限设计,当前任务用不到的工具不提供,权限在代码中按当前用户校验,而不是依靠模型遵守提示词;
- 删除数据、转账、对外发送信息这类高风险操作需要人工确认;
- 模型的输出同样不可信,显示到页面之前要转义,防止 [跨站脚本攻击](攻击技术.md#一跨站脚本攻击),用于查询数据库时要使用参数化查询;
+- 模型直接生成整条 SQL 时(例如用自然语言查询数据),不能只靠参数化查询,要在数据库一侧限制:用只能 SELECT 少数视图的账号执行,并设置执行超时和返回行数的上限,不要依赖按关键字检查 SQL 或者会话自己就能改回的只读设置(MySQL 8.0 实测,见[附录 A33](#a33-模型生成-sql-的权限限制));
- 不要在提示词中放入密钥等敏感信息,提示词可能被诱导泄露。
以客服 Agent 为例,用户身份取自登录会话,不作为工具参数让模型填写,get_order 在代码中检查订单是否属于当前用户,模型即使被诱导传入别人的订单号,也查不到数据。退款工具只生成一条待确认的申请,前端展示金额和订单,用户点击确认后由普通的业务接口执行,模型自己无法完成退款。
@@ -2737,7 +2948,7 @@ EXTRACT = {
### A6 意图路由的三种做法
-评测集是自己编写的 60 条电商客服消息,物流、退款、改地址、商品咨询、其它 5 个类别各 12 条,其中 10 条含有其它类别的关键词。分类模型用 bge-small-zh-v1.5 的 [CLS] 向量,每类 6 条示例(不在评测集中)取平均作为类中心,按余弦相似度最近的类中心分类,置信度为相似度除以 0.05 后的 softmax。大模型用 float16 加载到 MPS,贪心解码,置信度取第一个 Token 在 5 个类别首 Token 上归一化后的概率(5 个类别名的首 Token 互不相同):
+评测集是自己编写的 60 条电商客服消息,物流、退款、改地址、商品咨询、其它 5 个类别各 12 条,其中 10 条含有其它类别的关键词。Embedding 相似度用 bge-small-zh-v1.5 的 [CLS] 向量,每类 6 条示例(不在评测集中)取平均作为类中心,按余弦相似度最近的类中心分类,置信度为相似度除以 0.05 后的 softmax。大模型用 float16 加载到 MPS,贪心解码,置信度取第一个 Token 在 5 个类别首 Token 上归一化后的概率(5 个类别名的首 Token 互不相同):
```python
RULES = [("退款", r"退款|退货|退钱|售后|取消订单|退差价|七天无理由"),
@@ -2762,16 +2973,16 @@ p = torch.softmax(torch.log_softmax(g.scores[0][0].float(), -1)[first_tokens], -
改地址 2 0 8 0 2
商品咨询 1 0 0 8 3
其它 1 0 0 0 11
-分类模型:准确率 47/60 = 78.3%
+Embedding 相似度:准确率 47/60 = 78.3%
Qwen2.5-3B:准确率 54/60 = 90.0%
Qwen2.5-7B:准确率 57/60 = 95.0%
显示已签收但我没收到 物流 -> 退款 0.931
少发了一个配件 退款 -> 商品咨询 0.503
有没有优惠券 商品咨询 -> 其它 0.995
快递员态度太差了,我要投诉 其它 -> 其它 0.999 原始输出:投诉
-在 10 条含其它类别关键词的样本上:规则 4 条正确,分类模型 6 条,3B 8 条,7B 10 条
+在 10 条含其它类别关键词的样本上:规则 4 条正确,Embedding 相似度 6 条,3B 8 条,7B 10 条
-分类模型 置信度阈值:
+Embedding 相似度 置信度阈值:
阈值 0.50:自动路由 42/60(70%),其中正确 38,准确率 90.5%
阈值 0.70:自动路由 29/60(48%),其中正确 28,准确率 96.6%
阈值 0.90:自动路由 7/60(12%),其中正确 6,准确率 85.7%
@@ -3033,6 +3244,19 @@ v = emb(sents); dist = 1 - (v[:-1] * v[1:]).sum(1); th = np.percentile(dist, p)
“结尾不在句末”统计正文切片中不以“。”“;”结尾的比例。200 字时段落本身都不超过 200 字,两种分隔符切出的结果相同。
+sentence-transformers 6.2.0 加载同一个 bge-small-zh-v1.5,比较 encode()、encode_query() 和手动拼接前缀的结果:
+
+```python
+a, b = m.encode(q), m.encode_query(q)
+c = m.encode('为这个句子生成表示以用于检索相关文章:' + q)
+```
+
+```text
+prompts {'query': '', 'document': ''}
+encode() 与 encode_query() 相同: True
+encode_query() 与手动加前缀相同: False
+```
+
### A13 IVF 与 PQ 的召回率
数据为本地收集的 83514 条中文短句(本地收集的问题文本,去重),用 bge-small-zh-v1.5 编码为 512 维向量;随机取 1000 条作查询,其余 82514 条作库,以暴力检索的 Top-10 为准计算 Recall@10。IVF 用 numpy 实现的 k-means(在 3 万条样本上训练 20 轮,nlist = 256);PQ 分 64 段,每段 256 个中心(训练 15 轮),用查表的方式计算内积。numpy 的下标取值开销大,耗时不代表 FAISS 等库的速度,只比较扫描比例:
@@ -3420,6 +3644,25 @@ Message[role=tool, toolCallId=call_2, content={"order_id": "1025", "status": "
openjdk version "21.0.13" 2026-10-20 LTS
```
+用 Transformers 按 Qwen2.5-3B-Instruct 的对话模板渲染两个调用和两条结果(tool 消息带 tool_call_id call_1、call_2),渲染结果中没有 id:
+
+```text
+<|im_start|>assistant
+
+{"name": "get_order", "arguments": {"order_id": "1024"}}
+
+
+{"name": "get_order", "arguments": {"order_id": "1025"}}
+<|im_end|>
+<|im_start|>user
+
+{"status": "已发货"}
+
+
+{"status": "待付款"}
+<|im_end|>
+```
+
### A26 停止前校验
环境:Qwen2.5-7B-Instruct、Qwen2.5-3B-Instruct,transformers 在 Apple M1 Pro 的 MPS 上以 float16 运行,temperature 0.7、top_p 0.9,每种配置用随机种子 0~9 运行 10 次。工具 get_order 返回运单号 SF1234567,get_logistics 返回位置“杭州转运中心”;回答中出现“杭州”算成功。两种系统提示词:
@@ -3602,45 +3845,154 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
干扰文本是短问句,与口语化的问题共用很多二元词(“几天”“怎么办”),BM25 又偏向短文本,所以干扰越多 BM25 越差,等权融合把向量检索的结果也拉低了;问题中的编号、专有名词较多时结果会不同。
+### A31 注意力分数的缩放
+
+Python 3.13、numpy 2.3(Apple M1 Pro)。q 和 128 个 k 的各分量独立、服从标准正态分布,d = 128,每种缩放方式重复 2000 次;“分数标准差”是每次 128 个分数的标准差的平均值,“最大权重”是每次 softmax 后最大的权重:
+
+```python
+import numpy as np, sys
+rng = np.random.default_rng(int(sys.argv[1])) # 随机种子
+d, n, T = 128, 128, 2000
+def softmax(x): e = np.exp(x - x.max()); return e / e.sum()
+for name, scale in [("不缩放", 1), ("除以 √d", np.sqrt(d)), ("除以 d", d)]:
+ sd, mx = [], []
+ for _ in range(T):
+ q = rng.standard_normal(d); K = rng.standard_normal((n, d))
+ s = K @ q / scale
+ sd.append(s.std()); mx.append(softmax(s).max())
+ mx = np.array(mx)
+ print(f"{name}: 分数标准差 {np.mean(sd):.3f},最大权重均值 {mx.mean():.3f},"
+ f"最大权重>0.99 的比例 {(mx > 0.99).mean() * 100:.1f}%")
+```
+
+种子为 1 时的输出:
+
+```text
+不缩放: 分数标准差 11.231,最大权重均值 0.838,最大权重>0.99 的比例 30.1%
+除以 √d: 分数标准差 0.989,最大权重均值 0.068,最大权重>0.99 的比例 0.0%
+除以 d: 分数标准差 0.088,最大权重均值 0.010,最大权重>0.99 的比例 0.0%
+```
+
+种子为 2、3 时结果基本相同,不缩放时最大权重超过 0.99 的比例为 30.8%、29.6%。均匀分布时每个权重为 1/128 ≈ 0.008。
+
+### A32 语义缓存的相似度
+
+Python 3.13、sentence-transformers 6.2,模型 BAAI/bge-small-zh-v1.5(Apple M1 Pro,CPU)。向量归一化后点积即余弦相似度:
+
+```python
+from sentence_transformers import SentenceTransformer
+m = SentenceTransformer("BAAI/bge-small-zh-v1.5")
+pairs = [("订单 1024 到哪了", "订单 1025 到哪了"), ("会员可以免运费吗", "非会员可以免运费吗"),
+ ("订单 1024 到哪了", "我的快递 1024 现在在哪"), ("退货的运费谁出", "退货要自己付运费吗")]
+for a, b in pairs:
+ e = m.encode([a, b], normalize_embeddings=True)
+ print(a, b, f"{float(e[0] @ e[1]):.3f}")
+```
+
+```text
+订单 1024 到哪了 订单 1025 到哪了 0.822 答案不同
+会员可以免运费吗 非会员可以免运费吗 0.948 答案不同
+订单 1024 到哪了 我的快递 1024 现在在哪 0.734 意思相同
+退货的运费谁出 退货要自己付运费吗 0.783 意思相同
+```
+
+### A33 模型生成 SQL 的权限限制
+
+MySQL 8.0.44,Apple M1 Pro。root 建表、建视图并创建账号 bi,bi 只被授予视图 v_orders 的 SELECT 权限,v_orders 不含手机号列:
+
+```sql
+CREATE DATABASE IF NOT EXISTS shop;
+USE shop;
+CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, amount DECIMAL(10,2), phone VARCHAR(20));
+INSERT INTO orders VALUES (1, 1001, 59.9, '13800000000'), (2, 1002, 120, '13900000000');
+CREATE VIEW v_orders AS SELECT id, user_id, amount FROM orders;
+CREATE USER 'bi'@'localhost' IDENTIFIED BY '...';
+GRANT SELECT ON shop.v_orders TO 'bi'@'localhost';
+```
+
+bi 连接时同样选择 shop 库,依次用 bi 和 root 执行下面的语句(输出中的 ERR 为捕获的异常):
+
+```text
+OK SELECT * FROM v_orders -> ((1, 1001, Decimal('59.90')), (2, 1002, Decimal('120.00')))
+ERR SELECT phone FROM orders -> (1142, "SELECT command denied to user 'bi'@'localhost' for table 'orders'")
+ERR DELETE FROM v_orders WHERE id = 1 -> (1142, "DELETE command denied to user 'bi'@'localhost' for table 'v_orders'")
+OK WITH t AS (SELECT 1 AS id) DELETE FROM orders WHERE id IN (SELECT id FROM t) -> []
+OK SELECT COUNT(*) FROM orders -> ((1,),)
+OK SET SESSION TRANSACTION READ ONLY -> []
+OK SET SESSION TRANSACTION READ WRITE -> []
+OK SELECT /*+ MAX_EXECUTION_TIME(100) */ SLEEP(1) FROM v_orders LIMIT 1 -> ((1,),)
+```
+
+前三条和最后三条由 bi 执行:bi 读不到原表的手机号,也不能删除;它可以把自己的会话设为只读,再改回读写。中间两条由 root 执行,说明以 WITH 开头的语句也可以删除数据,只检查语句开头的关键字拦不住。最后一条的 SLEEP 返回 1,表示执行到 100 毫秒时被中断。
+
## 参考资料
- [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
+- [Xiong R, et al. On Layer Normalization in the Transformer Architecture. arXiv:2002.04745](https://arxiv.org/abs/2002.04745)
- [Neural Machine Translation of Rare Words with Subword Units](https://arxiv.org/abs/1508.07909)
- [RoFormer: Enhanced Transformer with Rotary Position Embedding](https://arxiv.org/abs/2104.09864)
- [FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness](https://arxiv.org/abs/2205.14135)
- [GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints](https://arxiv.org/abs/2305.13245)
- [Efficient Memory Management for Large Language Model Serving with PagedAttention](https://arxiv.org/abs/2309.06180)
+- [Cai T, et al. Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads. arXiv:2401.10774](https://arxiv.org/abs/2401.10774)
+- [Li Y, et al. EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty. arXiv:2401.15077](https://arxiv.org/abs/2401.15077)
- [Qwen2.5-7B-Instruct 模型配置](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/main/config.json)
- [The Curious Case of Neural Text Degeneration](https://arxiv.org/abs/1904.09751)
- [Training Language Models to Follow Instructions with Human Feedback](https://arxiv.org/abs/2203.02155)
- [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](https://arxiv.org/abs/2305.18290)
+- [Azar M G, et al. A General Theoretical Paradigm to Understand Learning from Human Preferences. arXiv:2310.12036](https://arxiv.org/abs/2310.12036)
+- [Xu S, et al. Is DPO Superior to PPO for LLM Alignment? A Comprehensive Study. arXiv:2404.10719](https://arxiv.org/abs/2404.10719)
- [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://arxiv.org/abs/2402.03300)
- [AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration](https://arxiv.org/abs/2306.00978)
- [GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers](https://arxiv.org/abs/2210.17323)
+- [FP8 Formats for Deep Learning](https://arxiv.org/abs/2209.05433)
+- [Dutta A, et al. Accuracy is Not All You Need. arXiv:2407.09141](https://arxiv.org/abs/2407.09141)
+- [Mekala A, et al. Does quantization affect models' performance on long-context tasks? arXiv:2505.20276](https://arxiv.org/abs/2505.20276)
+- [Hinton G, et al. Distilling the Knowledge in a Neural Network. arXiv:1503.02531](https://arxiv.org/abs/1503.02531)
- [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685)
- [QLoRA: Efficient Finetuning of Quantized LLMs](https://arxiv.org/abs/2305.14314)
- [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://arxiv.org/abs/2501.12948)
- [Chain-of-Thought Prompting Elicits Reasoning in Large Language Models](https://arxiv.org/abs/2201.11903)
+- [Zhao T Z, et al. Calibrate Before Use: Improving Few-Shot Performance of Language Models. arXiv:2102.09690](https://arxiv.org/abs/2102.09690)
+- [OpenAI: Reasoning best practices](https://developers.openai.com/api/docs/guides/reasoning-best-practices)
- [Efficient Guided Generation for Large Language Models](https://arxiv.org/abs/2307.09702)
+- [Tam Z R, et al. Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models. arXiv:2408.02442](https://arxiv.org/abs/2408.02442)
+- [Farquhar S, et al. Detecting hallucinations in large language models using semantic entropy. Nature, 2024](https://www.nature.com/articles/s41586-024-07421-0)
- [Lost in the Middle: How Language Models Use Long Contexts](https://arxiv.org/abs/2307.03172)
+- [Hsieh C P, et al. RULER: What's the Real Context Size of Your Long-Context Language Models? arXiv:2404.06654](https://arxiv.org/abs/2404.06654)
+- [Qu R, Tu R, Bao F. Is Semantic Chunking Worth the Computational Cost? arXiv:2410.13070](https://arxiv.org/abs/2410.13070)
+- [Bhat S R, et al. Rethinking Chunk Size For Long-Document Retrieval: A Multi-Dataset Analysis. arXiv:2505.21700](https://arxiv.org/abs/2505.21700)
- [Effective Context Engineering for AI Agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)
- [DeepSeek API 文档:Token 用量计算](https://api-docs.deepseek.com/zh-cn/quick_start/token_usage)
- [DeepSeek API 文档:上下文硬盘缓存](https://api-docs.deepseek.com/zh-cn/guides/kv_cache)
+- [Anthropic: Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
+- [OpenAI: Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching)
+- [OpenAI: Conversation state](https://developers.openai.com/api/docs/guides/conversation-state)
- [Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks](https://arxiv.org/abs/2005.11401)
- [Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs](https://arxiv.org/abs/1603.09320)
- [Elasticsearch: Similarity settings](https://www.elastic.co/docs/reference/elasticsearch/index-settings/similarity)
- [Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf)
- [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084)
+- [Qu Y, et al. RocketQA: An Optimized Training Approach to Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2010.08191](https://arxiv.org/abs/2010.08191)
+- [Moreira G, et al. NV-Retriever: Improving text embedding models with effective hard-negative mining. arXiv:2407.15831](https://arxiv.org/abs/2407.15831)
- [Precise Zero-Shot Dense Retrieval without Relevance Labels](https://arxiv.org/abs/2212.10496)
+- [Weller O, et al. When do Generative Query and Document Expansions Fail? arXiv:2309.08541](https://arxiv.org/abs/2309.08541)
- [RAGAS: Automated Evaluation of Retrieval Augmented Generation](https://arxiv.org/abs/2309.15217)
+- [Alaofi M, et al. LLMs can be Fooled into Labelling a Document as Relevant. arXiv:2501.17969](https://arxiv.org/abs/2501.17969)
+- [Thakur N, et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. arXiv:2104.08663](https://arxiv.org/abs/2104.08663)
+- [Larson S, et al. An Evaluation Dataset for Intent Classification and Out-of-Scope Prediction. arXiv:1909.02027](https://arxiv.org/abs/1909.02027)
- [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629)
+- [Xu B, et al. ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models. arXiv:2305.18323](https://arxiv.org/abs/2305.18323)
- [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/)
- [Building Effective Agents](https://www.anthropic.com/engineering/building-effective-agents)
- [Effective Harnesses for Long-Running Agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents)
- [Harness Engineering for Coding Agent Users](https://martinfowler.com/articles/harness-engineering.html)
+- [Böckeler B. Understanding Spec-Driven-Development: Kiro, spec-kit, and Tessl](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
- [The Anatomy of an Agent Harness](https://www.langchain.com/blog/the-anatomy-of-an-agent-harness)
- [ArchUnit User Guide](https://www.archunit.org/userguide/html/000_Index.html)
- [Claude Docs: Tool Search Tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool)
+- [Claude Agent SDK: Scale to many tools with tool search](https://code.claude.com/docs/en/agent-sdk/tool-search)
+- [OpenTelemetry: Semantic conventions for generative AI systems](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
- [How We Built Our Multi-Agent Research System](https://www.anthropic.com/engineering/multi-agent-research-system)
- [DeepSeek API 文档:Tool Calls](https://api-docs.deepseek.com/zh-cn/guides/tool_calls)
- [Qwen2.5-7B-Instruct 对话模板](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/main/tokenizer_config.json)
@@ -3650,14 +4002,19 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [MCP: Connect to Local MCP Servers](https://modelcontextprotocol.io/docs/develop/connect-local-servers)
- [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
- [HTML Standard: Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
+- [OpenRouter: API Streaming](https://openrouter.ai/docs/api_reference/streaming)
- [Nginx: proxy_buffering](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering)
- [Orca: A Distributed Serving System for Transformer-Based Generative Models](https://www.usenix.org/conference/osdi22/presentation/yu)
- [OWASP Top 10 for LLM Applications: Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/)
- [τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains](https://arxiv.org/abs/2406.12045)
+- [Miller E. Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations. arXiv:2411.00640](https://arxiv.org/abs/2411.00640)
+- [Zhang H, et al. A Careful Examination of Large Language Model Performance on Grade School Arithmetic. arXiv:2405.00332](https://arxiv.org/abs/2405.00332)
+- [Gema A P, et al. Are We Done with MMLU? arXiv:2406.04127](https://arxiv.org/abs/2406.04127)
- [How Claude Code works](https://code.claude.com/docs/en/how-claude-code-works)
- [How Claude remembers your project](https://code.claude.com/docs/en/memory)
- [LLMs Get Lost In Multi-Turn Conversation](https://arxiv.org/abs/2505.06120)
- [MemGPT: Towards LLMs as Operating Systems](https://arxiv.org/abs/2310.08560)
+- [Wu D, et al. LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory. arXiv:2410.10813](https://arxiv.org/abs/2410.10813)
- [Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory](https://arxiv.org/abs/2504.19413)
- [Generative Agents: Interactive Simulacra of Human Behavior](https://arxiv.org/abs/2304.03442)
- [Cognitive Architectures for Language Agents](https://arxiv.org/abs/2309.02427)
@@ -3674,6 +4031,9 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [A2A and MCP](https://a2a-protocol.org/latest/topics/a2a-and-mcp/)
- [LangGraph: Graph API Overview](https://docs.langchain.com/oss/python/langgraph/graph-api)
- [LangGraph: Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)
+- [LangGraph: Checkpointers](https://docs.langchain.com/oss/python/langgraph/checkpointers)
+- [LangGraph: Memory overview](https://docs.langchain.com/oss/python/langgraph/memory)
+- [LangGraph: Fault tolerance](https://docs.langchain.com/oss/python/langgraph/fault-tolerance)
- [LangGraph: Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts)
- [LangGraph: Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs)
- [LangGraph4j](https://github.com/langgraph4j/langgraph4j)
@@ -3709,6 +4069,7 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [Qwen2 Technical Report](https://arxiv.org/abs/2407.10671)
- [Root Mean Square Layer Normalization](https://arxiv.org/abs/1910.07467)
- [GLU Variants Improve Transformer](https://arxiv.org/abs/2002.05202)
+- [Touvron H, et al. LLaMA: Open and Efficient Foundation Language Models. arXiv:2302.13971](https://arxiv.org/abs/2302.13971)
- [Mixtral of Experts](https://arxiv.org/abs/2401.04088)
- [Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity](https://arxiv.org/abs/2101.03961)
- [DeepSeek-V3 Technical Report](https://arxiv.org/abs/2412.19437)
@@ -3725,6 +4086,7 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [TRL: SFT Trainer](https://huggingface.co/docs/trl/sft_trainer)
- [LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale](https://arxiv.org/abs/2208.07339)
- [SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models](https://arxiv.org/abs/2211.10438)
+- [vLLM: FP8 W8A8](https://docs.vllm.ai/en/latest/features/quantization/fp8.html)
- [vLLM: Quantized KV Cache](https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache.html)
- [A Survey on Hallucination in Large Language Models: Principles, Taxonomy, Challenges, and Open Questions](https://arxiv.org/abs/2311.05232)
- [Why Language Models Hallucinate](https://arxiv.org/abs/2509.04664)
@@ -3750,6 +4112,7 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [DeepSeek-V3 config.json](https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/config.json)
- [Dai D, et al. DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models. arXiv:2401.06066](https://arxiv.org/abs/2401.06066)
- [Qwen Team. Qwen3 Technical Report. arXiv:2505.09388](https://arxiv.org/abs/2505.09388)
+- [Qiu Z, et al. Demons in the Detail: On Implementing Load Balancing Loss for Training Specialized Mixture-of-Expert Models. arXiv:2501.11873](https://arxiv.org/abs/2501.11873)
- [Schulman J, et al. High-Dimensional Continuous Control Using Generalized Advantage Estimation. arXiv:1506.02438](https://arxiv.org/abs/1506.02438)
- [Yu Q, et al. DAPO: An Open-Source LLM Reinforcement Learning System at Scale. arXiv:2503.14476](https://arxiv.org/abs/2503.14476)
- [Zheng C, et al. Group Sequence Policy Optimization. arXiv:2507.18071](https://arxiv.org/abs/2507.18071)
@@ -3763,12 +4126,16 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
- [MCP Specification: Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization)
- [Anthropic: Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp)
+- [Anthropic: Introducing advanced tool use for Claude](https://www.anthropic.com/engineering/advanced-tool-use)
- [Agent Skills: Optimizing skill descriptions](https://agentskills.io/skill-creation/optimizing-descriptions)
- [Agent Skills: Evaluating skill output quality](https://agentskills.io/skill-creation/evaluating-skills)
- [Agent Skills: How to add skills support to your agent](https://agentskills.io/client-implementation/adding-skills-support)
- [Simon Willison: Not all AI-assisted programming is vibe coding](https://simonwillison.net/2025/Mar/19/vibe-coding/)
- [Best practices for Claude Code](https://code.claude.com/docs/en/best-practices)
- [Claude Code: Explore the context window](https://code.claude.com/docs/en/context-window)
+- [Claude Code: Model configuration](https://code.claude.com/docs/en/model-config)
+- [Anthropic: Compaction](https://platform.claude.com/docs/en/build-with-claude/compaction)
+- [LangChain: Built-in middleware](https://docs.langchain.com/oss/python/langchain/middleware/built-in)
- [Claude Code: Choose a permission mode](https://code.claude.com/docs/en/permission-modes)
- [Claude Code: Checkpointing](https://code.claude.com/docs/en/checkpointing)
- [Claude Code: Configure the sandboxed Bash tool](https://code.claude.com/docs/en/sandboxing)
@@ -3782,6 +4149,8 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
- [Pi](https://pi.dev/)
- [C-Pack: Packed Resources For General Chinese Embeddings](https://arxiv.org/abs/2309.07597)
+- [Kusupati A, et al. Matryoshka Representation Learning. arXiv:2205.13147](https://arxiv.org/abs/2205.13147)
+- [Chen J, et al. M3-Embedding: Multi-Linguality, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation. arXiv:2402.03216](https://arxiv.org/abs/2402.03216)
- [Qwen3-Embedding-0.6B 模型说明](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)
- [Parameter-Efficient Transfer Learning for NLP](https://arxiv.org/abs/1902.00751)
- [Prefix-Tuning: Optimizing Continuous Prompts for Generation](https://arxiv.org/abs/2101.00190)
@@ -3790,6 +4159,7 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [DoRA: Weight-Decomposed Low-Rank Adaptation](https://arxiv.org/abs/2402.09353)
- [PEFT: Soft prompts](https://huggingface.co/docs/peft/main/en/conceptual_guides/prompting)
- [LoRA Without Regret](https://thinkingmachines.ai/blog/lora/)
+- [He H, Thinking Machines Lab. Defeating Nondeterminism in LLM Inference](https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/)
- [Agent Skills: Best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
- [LangChain v1 Release Notes](https://docs.langchain.com/oss/python/releases/langchain-v1)
- [LangChain Overview](https://docs.langchain.com/oss/python/langchain/overview)