Skip to main content

paper-reading, code&rl方向

· 27 min read
ayanami

Effi-code: Unleashing code efficiency in language modelsSWIFTCODER: Enhancing Code Generation in Large Language Models through Efficiency-Aware Fine-tuning

问题:以前的方法主要关注正确性忽略效率(effibench,gpt4 代码执行时间是标准解决方案的1.69与45.49倍(avg, worst))

衡量效率:本地测量执行时间和内存

具体来说是三个指标:

  • 执行时间ET
  • 最大内存使用量MU
  • 总内存使用量TMU

这篇论文并没有用RL的方法去激发LLM生成更高效率代码的能力,而是”优化“训练数据集的代码效率,并证明了训练集代码效率高也会让LLM生成更高效率的代码(ET 的相关性为 0.972,MU 的相关性为 0.950,TMU 的相关性为 0.986)

Refer to caption

效果:qwen2.5-coder-7b-instruct pass@1 44.8 -> 57.7,正确任务的执行时间减少48.4%

方法:构建代码生成数据集,进行微调

Refer to caption

具体来说,先拉下来开源数据集,过滤一遍后,直接让更强的LLM生成更好的解决方案,然后本地跑一遍得到效率

不同效率的代码示例 Refer to caption

文章认为的效果提升来源:

  1. 多语言数据集
  2. 数据准备阶段过滤充分
  3. 数据量(有超过 70,000 个训练示例,比先前的mercury 1.8k要大很多)

简评:大体上感觉是工作量密集型的工作,比如finetune多个llm和在多个数据集上进行相关评测,但方法上并没有创新之感,洗的数据是高效率代码的自然输出的结果效率也会变高,并不令人感觉新奇,只是讲好了我们需要同时看重代码质量(这里是效率)这一指标的故事


EffiBench: Benchmarking the Efficiency of Automatically Generated Code

问题:现在衡量代码正确性的文章已经有很多了,但是兼顾正确和效率的相对少

如果要考虑效率的话,一个问题是原先的代码数据集的任务太简单了,很难区分效率;同时很多任务也不是效率密集型的,并且效率相关的测试也不够

数据集构建:leetcode

“标准解决方案”: stackoverflow最多star/leetcode top answer

测试用例:LLM生成

简评:典型的benchmark工作


Diversity-Aware Policy Optimization for Large Language Model Reasoning

问题:diversity在reasoning能力中扮演重要角色,但缺乏定量的研究

动机:传统RL认为,多样性有助于策略探索(例如,SAC等算法),帮助跳出局部最优、加速训练收敛,但对于LLM呢?

方法:

直接增加熵,长度bias, 较长的响应 -> 引入token level diversity

tradeoff 质量和多样性 -> 仅对正样本用多样性增强,确保以性能标准为主导

贡献:

  • 多样性的Potential@k指标和LLM reasoning存在正相关关系
  • token-level diversity objective, selectively applied to positive samples

奖励:和R1一致,acc reward和format reward,前者和ground truth 比较,后者让答案以 \boxed{}格式呈现

多样性度量:response中不同方程的比例

DivEqu:=1NkUADivEqu := \frac{1}{N} \sum_k \frac{U}{A}

其中,U是k个采样中的独立方程数量,A是总方程数量

Potential@k: 衡量模型在第一次失败后,在k次(k=16 in paper)内纠正答案的能力

Potential@k:=Pass@k(1Pass@1)1Pass@1Potential@k := \frac{\sum Pass@k (1 - Pass@1)}{ \sum 1 - Pass@1}

(为啥定义这样一个指标?这个分子分母分别求和挺怪异的,也不是类似条件概率的算法)

结果:对于推理能力有限的LLM(Pass@1<0.4) 多样性和Potential@k没什么关系,但对于更好的LLM,就有明显的正相关关系

Refer to caption

定义的token-level熵

image-20250817161107663

在实测中,作者发现直接把这个熵带入训练会增强错误样本的多样性,相当于对错误样本做增强,因此打了只对正样本做的补丁

image-20250817161240377

而对这个式子求导能直观感受到多样性的部分

image-20250817161427155

对于大多数token, 采样概率π\pi的值都是小于e1e^{-1}的,则前一个乘项小于0 ,熵的梯度和采样概率的梯度成正相关,熵增有利于对稀有Token的采样

image-20250817161718666

实际取 λ=0.01\lambda=0.01

简评:

动机非常清晰,实验也比较充分,展示了虽然是 well-known 的需要基模能力达标多样性才有意义的结论。

对于LLM优化目标的改造的说明是合理的。理论说法是GRPO带来的更新依赖于组内样本的差异,(因为是用std/mean来计算优势函数A),所以增强多样性能避免优势消失带来的一些问题,本质上是在避免 rmeanstd\frac{r - mean}{std} 里面的stdstd 过小导致不稳定的问题

只对正样本应用多样性损失的trick也是有意义的。

但衡量多样性的时候比较草率,首先是局限在数学范围,其次感觉 方程多样性 != 解法多样性,所以多样性指标总感觉欠说服力。

他们自己也在文章中说:"许多现实世界的应用需要用户意图的多样性(例如,需要数学问题的代数和算术解,或者生成具有不同算法方法的代码), 这样的多样性不等于token level的多样性"


Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning

Refer to caption

问题: 现有RL for LLM算法对不同的token一视同仁,没有考虑token自身的异构性

观察: 低熵token主要决定语言结构,高熵token则作为关键的决策点。手动调整forking token的熵,适度增加这些token的熵可以显著提升推理性能,降低熵会导致性能下降。

仅保留20% token的策略梯度更新,剩下的mask掉,性能上能和全量媲美甚至超越,在RL过程中,只有一小部分高熵 token 对探索有实际贡献,而其他 token 则可能中性甚至有害

20%是实验得到的最优比例

另一个发现是32B的性能提升大于14B大于8B

熵定义:

image-20250817165117549

一个很直观的分布图,高熵Token基本是重要的转折词,而低熵token是一些前后缀等

Refer to caption

另一个稍微不直观的图

可以得到的结论是高熵token配合高温度能够得到好性能,而低熵token在不同温度下都不太影响最终效果

Refer to caption

作者还发现,RLVR的过程基本就是这些高熵token的熵改变的过程,低熵Token的熵相对稳定,初始熵较高的 token 在 RLVR 后往往会经历更大的熵增。

而只对高熵Token进行RLVR能得到更好的效果

Refer to caption

甚至作者在OOD数据(代码数据)上进行评测,发现高熵的token选择后,泛化性也更好

Refer to caption

讨论:

  1. RL倾向于保持高熵令牌的熵,而SFT倾向于将输出推向one-hot,降低熵,作者表示这可能是RL更能泛化而SFT容易记忆、难以泛化的原因
  2. 传统RL假设一整条轨迹上的熵是接近均匀分布的,这对LLM不成立
  3. LLM RL中,之前常用的熵损失鼓励探索可能未必适用,因为可以是低熵token的熵增也可能是高熵token的,而DAPO的clip higher能筛选出高熵token,即重要性比率ratio(π/πold\pi/\pi_{old})更高的token通常对应高熵token,这呼应了前文中,RLVF对高熵token的熵值有较大改变

简评:开始的手动把熵调高感觉等价于把奖励调高等价于数据增强,作为一种RL trick细想并不惊奇,是稀有样本情况下的常用技术

从这个角度继续往下想,如何理解这个多数token对训练甚至有害呢,感觉也是能合上现有对LLM RL的state定义不太合理,或者说探索空间太大,采样样本太少,不可能得到正确的Q函数,导致对于一些本来就很无所谓的token,更新的方向也是比较盲目

总之,这篇文章实验非常充足,分析也比较到位,效果也十分亮眼,确实是在当前的SOTA上往前推进的好文章


Pass@k Training for Adaptively Balancing Exploration and Exploitation of Large Reasoning Models

问题:RLVR 通常采用 Pass@1作为奖励,但容易收敛到局部最优;Pass@k则常用于验证,本文用Pass@k直接作为训练,并设计了对应的优势函数,发现效果比Pass@1更好(更高的Pass@k分数,保持的Pass@1分数)

Refer to caption

Pass@1 收敛到局部最优的问题在于正向奖励的探索可能路径太长,模型会倾向于利用而不是探索

Refer to caption

方法:

  • Full Sampling: 每组采样的k个rollout计算奖励,之后整组的奖励由每个rollout的最大值给出Refer to caption

  • Bootstrap Sampling: Full Sampling虽然提高了性能,但计算量太大。为了减少推理次数,同时保持组数不变,采用bootstrap采样,先生成一个候选池,再从这个池子里面抽取k个答案,就形成了一组(这样允许某个答案被分到多个组里面重用),论文中候选池大小就和正常top1大小相同,也就是平均而言每个样本被重用k次

Refer to caption

  • 既然Bootstrap Sampling只不过是对样本的采样重用,那其实可以直接计算对应的优势值的期望,所以可以省去采样这一步,直接计算候选池中正负样本的个数,通过解析解得到期望带入计算

其他实验:

Pass@k的熵在训练中是上升的,而Pass@1后期会收敛,支持了前面的探索-利用论

k的值的影响?k的值不是跳出局部解的重要因素,但k值越大,优势越小(因为只有抽样全负才会是负奖励,k值越大正奖励概率越高),步长越小,训练效率降低,这个结论和也可以在改变学习率中得到验证

无论是小规模还是大规模的 LLM,都可以从 Pass@k 训练中受益。此外,模型架构和模型系列不会影响持续 Pass@1 训练的提升,下游任务的领域和形式也不会影响 LLM Pass@k 性能向 Pass@1 性能的迁移

分析:

同样是奖励从0到1,Pass@k的梯度出现在比Pass@1更早的地方(一次做对和K次做对),会使得Pass@k更倾向于解决更难的问题而不是中等难度的问题(由于Pass@k有一个argmax, 所以提高已经会做的题的正确率的效果是不断减小的)

Pass@1

Refer to caption

Pass@k

Refer to caption

还做了一个对比试验是仅将简单问题的奖励设置为0,不能防止模型过度优化

Refer to caption

为了分析是否全是梯度曲线峰值带来的影响,手动调整奖励曲线,设计了一个这样的曲线

Refer to caption

发现太注重困难的样本也不好,模型后期乏力

既然Pass@k 更注重困难样本,Pass@1 更注重一般样本,能否动态结合?

一个样本池中,正样本越多,越需要注重困难样本;反之需要先学会一般样本,因此设计了这样的优势函数

image-20250818003914103

发现效果非常好

Refer to caption

文章还做了另一个实验是,用熵而不是正样本数量来判断一个问题是否是困难的,熵高的50%认为是困难问题,使用Pass@1, 低的使用Pass@k,也得到了不错的效果

简评:感觉没太多好说的了,他们做的相当好,从最开始的发现topk training可以提升效果,再到用bootstrap sample提高效率,再到公式的推出,自然发现topk就是本质上对应的难度-奖励曲线的不同,再到设计相关的实验验证,最后提出简单的自适应机制来结合Pass@1和Pass@k,也取得了相当好的实验效果,感觉挺一气呵成的,感觉是一个会成为范式的trick


Structure-Aware Fill-in-the-Middle Pretraining for Code

问题:现有的FIM将代码视为字符序列,而忽视句法结构

Refer to caption

方法: 结合AST和FIM, 在训练时,被mask的部分始终是AST的一个或多个完整子树

代码解析:Tree-sitter

mask算法:涵盖不同的AST节点,提高泛化能力,且与具体语言无关

  • 单节点mask: 按照对应文本的数量成比例抽样
  • 多节点mask: 先进行一次字符区间的采样,再找到包含这个字符区间的最小3节点的AST子树,子树中再取和原始字符区间有最大交并比的部分

评估:字符级别困惑度,文章给出不用实际benchmark的原因是大规模单测太难。

简评:非常直接的想法,就类似word-level BERT对原始BERT的改进,不过它怎么构建AST的倒是可以参考。文章最后的评估用困惑度说服力不高,但考虑到它的数据量确实大(256*H100训练),也可以理解


The Entropy Mechanism of Reinforcement Learning for Reasoning Language Models

问题:现有RL后训练存在策略熵减小导致模型快速收敛,后期探索较少难以提升的问题

Refer to caption

作者发现,在前1/3的epoch中,基本就已经达到了大部分的性能,而熵也进入低值,作者称之为熵崩溃"entropy collapse"

且对于不同的模型大小,对于不同的RL方法,都能拟合近似的定律 R=aeH+bR=-a e^{H} + b

有了这样的拟合公式,可以在训练早期估计后期的性能,且ab与算法几乎无关,极限就是 a+b-a+b

Refer to caption

另一个有趣的发现是,ab和模型大小呈对数线性关系

Refer to caption

也就是说,不仅可以在训练前期拟合后期,还可以用小模型预测大模型的RL效果

image-20250818222517778

熵变公式如上,直观地讲,如果动作 a 同时获得高/低概率和高/低优势,则熵会降低,反之亦然

Refer to caption

作者还提出,直接使用熵损失的方法,如L=L0αHL = L_0 - \alpha H, 不仅对超参数敏感,实验效果也并不优于基线

所以作者提出的方法是,针对高协方差的一小部分token做Clip或者KL,就能防止熵崩溃,下面的实验结果也表现很好,熵后期不下降,回答长度增长,正确率大幅度提高

Refer to caption

Refer to caption

作者发现策略熵对超参数设置非常敏感。具体来说,我们的方法仅干预一小部分 token( 10−4 到 10−3 ),却完全改变了熵曲线。这意味着几个“关键” token 对 LLM 的熵至关重要

简评:搬运作者对clip-higher的讨论,作者认为,clip-higher也有类似的功能,提高重要性采样的上限会带来更多低概率的token,上限阈值仅影响具有正优势的 token,这意味着 clip-higher 实际上在梯度计算中添加了更多低协方差(低概率、高优势)的 token,所以结论殊途同归。而作者直接提出协方差是更胜一筹。

不过作者也说了现在还不清楚熵和模型性能的完整关系,也不清楚最优的熵值

另一个就是这些熵的论文主要还是在math任务上做的,code任务能否有相同的结论还是一个问题 (我认为这两个任务关键在于中间过程是不是重要的,math只有结果可能会得到一些错误的结论)


Improving LLM-Generated Code Quality with GRPO

问题:take code quality into consideration,不多赘述

方法:维护了一个库,把现有的一些评估代码质量的方案整合了起来(code complexity, dead code, code structure(linter等), style&doc, safety, performance...), 然后质量奖励和正确性奖励一起放到奖励里面丢给GRPO

简评:只是占坑的,很草率的方法(对于奖励参数的设定),定量结果也不足。


Enhancing High-Quality Code Generation in Large Language Models with Comparative Prefix-Tuning

问题:take code quality into consideration

方法:比较有新意,将Dynamic Prefix和代码质量结合起来了,并且是使用对比学习的方法做这个前缀

基于Pylint打分,构建了一套数据处理流水线标注大量高、低质量的代码对(相似度高,质量差距大,且都至少通过一项基本测试)

然后在微调Dynamic Prefix的的时候,加上一个排名Loss,希望模型倾向于生成高质量的样本。

里面的掩码是用difflib做的,目标是聚焦于差异的导致质量出现区别的token,而不要考虑重复的token

image-20250818231615372

然后进行PEFT微调,再加上KL散度来保证不要丢失原始模型的代码生成能力

简评:这篇文章写得特别冗长,实验做的重点不突出,但思想是有意思的,并且明显可以继续挖,例如他们的代码相似度是简单的词频向量,自然挖掘出来的是细微处的代码风格问题,如是用index还是for each的形式遍历循环(只有这样的才会词频上高度相似)。但实际上是否可以用例如bge-code这样的代码语义嵌入呢?值得探究。

还有就是,这个数据收集的方法依赖于大语料库,也只能挖掘常见的代码模式,如果用自生成的方法,例如假设我们已经有一些高质量的代码库作为ground truth,

用llm得到的补全片段当负项,也能构造正负样本对啊,既然都是训练得到一个通用的“code style prefix”,这样数据丰富程度能高很多。他们明显的数据少训练小(2*A6000*3h)。


Augmenting Large Language Models with Static Code Analysis for Automated Code Quality Improvements

问题:LLM refactor code没结合静态分析

方法:RAG + 静态分析软件 + Prompt 工程

简评:垃圾文章,真要做也是做一个能排序Code Quality的专用BERT,或者对现有的code embedder/reranker做adapter研究怎么把code quality调进去


A Hierarchical and Evolvable Benchmark for Fine-Grained Code Instruction Following with Multi-Turn Feedback

只需要看一张图就行了

image-20250818234727444

现有模型在约束生成时,quality这种抽象的约束是满足最差的

而对于多种约束的组合,现有LLM都很差

而对于有反馈的情况(例如linter之类),在3轮迭代左右就能有很大的提升,但后续再增加轮数也难以获得更高收益


Training Language Models on Synthetic Edit Sequences Improves Code Synthesis

问题:LLM这样“一口气生成所有代码”和先前的软件工程实践(增量式开发)是相悖的,而现在的code agent又需要增量开发的能力,有绕远路之感,于是研究能不能从预训练的数据侧上解决这个问题,即大规模合成 增量编辑数据

方法: 文章提出了一个LintSeq的方法,对于一段已有的代码,从里面修建某些部分回退,让回退后的代码不会触发linter错误,则构建了一个edit stage

Refer to caption

然后这样构建了数据集后自己SFT codellm,发现确有提升

简评:简单有效,或许可以想想这个怎么和RL结合?

Focused-DPO: Enhancing Code Generation Through Focused Preference Optimization on Error-Prone Points

问题:代码错误很多是中间的“易错点”出错,对于所有token一视同仁的奖励函数在代码任务上可能未必高效

image-20250819001722237

方法:

  1. 该方法从真实代码库中提取概念,生成问题、代码和测试
  2. 由于有了测试,所以可以比较不同的生成代码的相对性能
  3. 通过共同前缀和共同后缀,得到中间不一样的中缀,就认为是“易错点”

然后DPO专注这一块的优化

image-20250819002037550

简评:感觉上是更软件工程的熵方法的简化,感觉这个易错点是能从LLM自身状态或者其他软件工程分析技巧中得到的,从前面几篇也可以看出,现在这种广义上的RL ”attention“ mask类工作越来越多了

这个方法要求生成测试,实际生产中感觉并不可用;抛开这个不谈,感觉就单纯对一个大型代码数据集,去分析里面的编码模式,找到相对少的n-gram,或者AST level迅速变化的地方作为易错点重点训都或许可行

投机解码简述

· 14 min read
ayanami

动笔的时候会有一种感觉,自己对这个方向了解的还是太少了... 所以大概不会讲得很学术,主打一个轻松愉快,让不了解的人也简单知道一下投机解码speculative decoding

投机的提出

当前,大型语言模型(LLM)在推理阶段普遍采用自回归解码策略,其核心特性是逐步串行生成 token,每一步都依赖前一步的输出。这一计算模式导致推理过程在系统层面面临严重的内存带宽瓶颈:每一步前向计算都需要将完整的模型参数从高带宽内存(HBM)加载到加速器缓存,但仅生成一个 token。由于每次只生成一个 token,导致大量的计算资源被闲置,无法充分发挥加速器的算力潜力,最终造成整体推理效率低下。 为解决这一问题,一种加速大型语言模型推理的思路是提高解码过程的算术强度(即总浮点运算次数 FLOPs 与数据传输量之间的比值),同时减少解码步骤。基于这一理念,研究者们提出了推测解码/投机解码(Speculative Decoding) 技术。Speculative Decoding 的核心思路如下图所示,首先以低成本的方式(一般来说是用小模型)快速生成多个候选 token,然后通过一次并行验证阶段快速验证多个 token,进而减少大模型的 decode 次数,从而达到加速的目的。

上面讲得比较学术,我尝试给一个自己的通俗些的解释:

llm的推理分成两个阶段,prefill 和 decode,prefill处理两个事情,计算输入(prompt)部分的attention和kvcache,输出第一个Token;而decode处理自回归的生成token的后续部分,即输出

为什么这样分呢?实际上是因为他们的计算负载不同,而更本质的原因是现有LLM的主流架构是CasualLM,即三角因果掩码,计算当前token时是无法看到未来token的。这带来了一个结果是,对于输入部分,我们可以并行的计算所有的输入token,但对于输出阶段,由于下一个token依赖于前一个token,所以我们只能串行的计算。

在LLM推理加速方面针对这两种计算的统一和调度有很多很多的研究,例如chunked prefill到新的pd分离、af/am分离等,但直接对这一传统范式发起挑战的大致就是几种:一种尝试换其他架构的模型,比如stable diffusion的dLLM,一种尝试采用多个输出头在训练时就学会“一次预测几个词”(deepseek MTP),剩下的就是投机解码

投机解码的核心思想就是,既然我们的decode阶段是内存密集型的(后面的token依赖于前面的token导致计算不能打满),那我可以把多余的算力利用起来,我用某种机制一次性猜测多个token,然后LLM从生成变为验证,就完成了并行化

Q1: 为什么说生成变为验证是并行化? A1: 因为验证这里有一个关键的地方是,在验证后一个token的时候,直接假设前面猜测的token都是对的,以猜测“千早爱音唐得没边”为例子,模型并不是串行的验证“千”对不对,“早”对不对,而是并行地验证这8个字,在验证“唐”的时候直接假设前面的输出“千早爱音”是对的。带来的效果是,如果“唐”被验证是错的,后续的所有token“唐的没边”都会被舍弃。

Q2:如何验证呢? A2:LLM生成token的最后一步是概率采样,如果猜测的概率是p1, LLM正常推理输出是p2, 如果p1 < p2(这里已经进行了猜测的采样),则选择猜测是对的;如果p1 > p2,则对的概率是 P(p2|p1)=p2/p1, 这样从直觉上就可以理解如何“验证”了,具体输出期望的一致性证明可以参考相关论文

Q3:投机在精度上是不是无损的? A: 看你如何定义。投机的核心是验证中的拒绝采样,学过rl的同学应该对这个概念很熟悉,拒绝采样带来的后果是,输出的期望是一样的,方差会变大。所以llm的期望是一样的,输出方差会变大,可能类似于调大温度。当然投机概率乘的多了还有一些数值精度上的问题。

Q4: 那并行的其他head空算不是更浪费算力和空间吗?一次all-layer的forward时间应该还挺长的 A:传统投机是不接受,但其他head的结果可以加入候选池,就是候选池改进的方法, 实际上不一定会这样验吧。medusa的tree attention就是,我不是序列地验head1,head2,而是尝试在树上直接找到综合接受期望最长的序列,也就是head1并非贪婪采样,不过现在推理引擎不是完全支持这个,据我所知sglang默认是有的,但vllm确实是这种序列的验法。浪费计算你说得对,所以投机work的前提是mem bound,但接受率越高浪费的不就越少吗,本质上还是接受率不够

如何生成猜测

主流是这几种方法:

  • 启发式,如n-gram,在很多任务中,输出会抄写prompt种已经给出的上文,比如总结任务,所以直接在给出的prompt中统计n-gram词频,取以现在输出末尾token开头的最佳选项作为猜测,优势是引入非常简单,劣势是吃任务类型(工作负载),对于很多任务没太大效果
  • 小模型,例如用qwen3-0.6b的输出作为qwen3-8b的输出的猜测
  • 自猜测,如medusa和eagle这种,给模型训练一个额外的附加结构,让其具备类似MTP的推理时猜多个token的能力。这个附加结构早期是放在模型的最后一层,即多个输出头,后来eagle提出最后一层(logits)不如倒数第二层(特征层),并且改造输出头的输入,再加上先前的token(即输入为,之前所有token的倒数第二层+当前token的倒数第二层+之前所有token的实际采样结果),效果非常好,能够达到7~8倍加速的疯狂数字

没有免费的午餐

那么古尔丹,代价是什么呢?

注意在开始我们就讲了,投机是一个利用空闲计算的方法,但实际上,利用空闲计算的方法不止投机一个,例如你有100张卡,你完全可以把不同的计算任务调度到不同的卡上,尽可能打满所有卡的计算

实际上这也是投机的痛点,或者说到底什么时候,投机才是有用的。

magicdec一文中已经指出,投机的适用场景常见于两种工作负载模式:

  1. 端侧推理,你只有一张卡,只能加载一个模型,这个模型还把你的显存占满了,那显然你无法通过加大batch来缓解memory bound,这时候投机是真有收益,eagle论文里面的7x加速也是batch=1的时候跑出来的
  2. 长上下文,你有大集群可以做不同任务的调度,但你的上下文实在太长,kvcache大小是随上下文线性增长的,上下文过长之后,你的大集群也硬生生被整成memory bound了(热知识:显存不是80G都是平等的,显然显卡也有SRAM/DRAM这样的高速低速区,更不提上下文太长之后有些kvcache直接就被offload到内存了)

端侧推理很好理解,那长上下文具体是多长呢?

magicdec给了一个指标是:对于接受率为0.8的投机,在实际的大batch size下(256),大概在3.2k token上下文开始投机能够取得收益(对于GQA这种模型而言,由于其在mem上较优,sd能加速的临界prefill长度会更高,对于非GQA模型是大概1.3k)

(关于端侧推理,我在我自己的一个项目上也试验过投机解码,平均输入长度大概是2k tokens,n-grams投机大概能加速30%,eagle由于我的训练数据等问题,也差不多)

当然以上只是一个最最简单的认识,实际上投机的很多算法相当复杂:

  • 能否快速剪枝某些置信度低的序列,不然预测k个token,可能的组合数指数增长吃不消?——medusa等

  • 剪枝之后如何高效计算?—— tree attention

  • 投机算法中,当出现拒绝验证时,后续的猜测token全部被丢弃,这些猜测token有没有可能被重用?——一系列维护候选池的方法

  • 小模型猜大模型很美好,但不是所有大模型都有对应的小模型,能否支持异构(大小模型词表不同)?—— huggingface uag tli等方法

  • 投机的超参数(例如一次猜几个token等)难以确认,能否用RL等方法优化超参选择? —— banditspec等

  • 能否通过LLM的置信度或者外部的一些规则等来动态开关投机,避免额外浪费的计算量?

  • 能否把投机也用到prefill过程中(选取kv)?—— specprefill

  • 在多模态场景中,如何使用投机,如果能的话,又该怎么做?——vllm roadmap(雾)

  • eagle还是太吃训练了,training方法如何做数据集选择?

  • 除了从prompt中选取候选,能否从参考资料等其他文本中选取猜测?—— snowflakes suffix decoding

  • 投机如何和现有大规模并行融合?(在vllm的投机集成中,投机的模型的并行都是简单的1,即投机模型不做tp来降低实现复杂度)—— 最新的 字节swiftspec

  • ...

展望?

最近投机是真的很火,aaai26中好像就有30篇投机的文章

如果从一个应用者的视角来说的话,现有推理框架(比如vllm&sglang)基本都有投机的集成了,只是集成多少的问题

而训练投机的话,sglang的specForge项目把它变得相当傻瓜化了,现在正在快速发展中

Paper reading - Context Pruning and beyond hard pruning

· 17 min read
ayanami

引子

我们知道,在现在Agent需要处理的一大问题是长上下文下性能的开销问题,对此infra团队有非常多的优化,从attention架构的优化如各种windowed attention到kv的压缩重用如cacheblend和megicdec等都提出了一系列的解决方案,但有一个最本质的方法是:有没有可能直接减少上下文的长度(去掉不必要的上下文?) ,这就是Context Pruning的出发点。

而截止2025年8月,相关的方法已经发展了两三年了,大体上可以分成几个类别,本文会对此做一些简单的介绍和总结。

借用naver lab最新的相关论文里面的说法,现在的方法可以被一个四方格归纳:

其中,Hard和Soft代表裁剪方法是直接作用于token上(hard,相当于裁剪结束后,输入的是一个新的prompt),还是作用于token的embedding上(soft,相当于裁剪结束后,输入的是一个新的qkv和其他东西,无法还原出“原始”的token输入)

在线和离线一般代表着这个裁剪方案是否依赖于用户查询q,依赖q的方案是在线的,不依赖q的方案是离线的,可以提前做好。但传统上,如果你的裁剪方法也需要用到和原始模型一样大的LLM,也一般称之为离线,或许“是否会对在线推理造成明显时延影响”做划分更好一些

离线硬裁剪

在最早期的时候,就有相关的一些朴素方法,例如直接对查询文本段做一次总结摘要,再用总结后的文本段去做后续的任务,这种方法是离线硬裁剪的典型代表。如果用的是llm就是离线的,如果用轻量级模型做摘要或者总结就是在线的

而在后面的时候,出现了例如微软的llmlingua这样的工作,直接用一个小模型(gpt2 small,llama-7b, etc)去预测哪些token是重要的,哪些token是不重要的,然后把不重要的token直接裁剪掉,这种方法也是离线硬裁剪的典型代表。(llmlingua2 换成了微调的 BERT 来做这个事情,所以可以说在线的), 其出发点和常规的硬裁剪可能有部分地方不同,例如llmlingua认为,裁剪本身是可以得到一些人类不可读但是大模型可以理解的token序列的,所以可解释性上可能并没有想象的那么强。

离线软裁剪

和硬裁剪同时推进的是软裁剪相关的工作,其想法很简单: 如果我牺牲解释性,直接调整prompt的embedding这类,即使产生的是不对应任何token的"fake embedding",其在高维空间中也应该融合了多个token的语义,理应得到更高的压缩率(可以理解为,在训练过程中为llm 扩充为无限词表,然后定义了一些高效的"额外语言")

比较早期的工作是 xRAG, 其裁剪策略非常极端,将整个段落都压缩成1个embedding X,怎么训练呢?一个在此类论文中经常出现的是重建loss,即

压缩前Doc+queryxDoc + query \to x

压缩后Doc+queryxDoc' + query \to x', L=L(x,x)=DKL(x,x)L=L(x',x)=D_{KL}(x',x), 即自蒸馏,希望压缩后依然能重建原始的输出,论文实际中可能会用变体版本来实现指令遵循等

xRAG的做法是,使用一个通用编码器E,把这个编码器E视作一个新的模态,仿照CLIP的方法直接用MLP projector做通用编码器和实际使用的LLM token embedding的模态对齐

但[大家实测下来](笔记:RAG 的相关优化方法之六(xRAG/PISCO) - 刀刀宁的文章 - 知乎 https://zhuanlan.zhihu.com/p/29292925032),xRAG的效果并不好,而相对较好的是更新的Pisco方法

Refer to caption

Pisco将检索到的文档D和memory tokens一起送到LLM中,产生embeddings

再将embeddings +query送到相同的LLM中,产生输出,这个 q+E 和原始的 q+D 比较, 计算交叉熵损失

这里有一些复杂的地方:

  • 虽然叫解码和编码,但是Student LLM都是同一个LLM, 只是训练不同LoRA模块

  • 交叉熵是怎么得出的? teacher模型和student模型都是采用的最大长度128的贪婪解码,就可以直接令 L=1logp+0log(1p)=ilogP(aiq,e,a<i,θc,θd)L=-\sum 1logp + 0log(1-p) = - \sum_i log P(a_i|q,e,a_{<i},\theta_c,\theta_d) , 优化目标是 θc\theta_cθd\theta_d 还有 memory_tokens

  • 如何理解memory token? 我觉得文章是借用了之前的一些研究比如ICAE, 在这些文章之中,训练的压缩机制是,将上下文压缩成一个定长的memory slot, 这里的memory token实际上只是多个embedding向量而已,而更关键的是LoRA微调的θc\theta_c,我的理解是,memory tokens只是一个后置的、可以看到Documents的所有信息(假设它没有魔改注意力)的语义位置,叫tokens也可以理解为直接扩了词表加入了l个特殊token,类似BERT里面的[BOS] ,只是decoder llm需要后置。

    • 文章并没有细说这里的注意力是怎么设置的,但从后文中发现的memory tokens具有明显的位置特性(例如1位mem token主要注意最开头一段),感觉应该是没改过
  • 文章的另一个重要的实验结论是,微调student llm(θd\theta_d)是必要的,之前的研究中没有相关模块,会导致性能的大幅度下降。这细想其实是一个很有趣的事情,可以注意到,压缩的时候是没有接触到query信息的(这也是为什么称为离线的原因),可以理解为某种意义上的LLM as an embedder,而加入了query和embedding再训练的时候,θd\theta_d一边学会了如何理解自己产生的embedding,另一方面学会了如何根据query去选择embedding,整体上类似于ColBERT架构的Reranker(前面是multi-vec embed, 后面是maxsim)

在线硬裁剪

Provence

之前的裁剪方案只注重于“自然语言是有冗余的”,所以主要做的都是token-level的pruning,而provence则更注重实际一些,它发掘了一个问题是,其实现在RAG里面的 “Chunk” 是一个特别微妙的概念

如果chunk切得大了,那上下文自然就长了,甚至效果也会明显下降(详见ground truth在chunk中的不同位置的position bias相关的研究,现有embedder对这个bias耐受性不佳,会狠狠掉点);但如果chunk切得小了,语义信息的丢失、检索的困难又是很恼人的事情(先不论检索,检索到了多个小块之后信息不够怎么办?一种是合并,但策略怎么定?另一种是Anthropic的Contextual Retrieval,把上下文放进来,本质上还是变成大块(我说这个a一串真是炒作勾啊.jpg))。

而Provence给了一个折中的方案,既然我们有句子级别的语义,为什么不用呢?分几步走

  1. 训练一个接受q,d的BERT,给每一个token打0~1分,并根据用户指定的阈值进行二值化变为0/1, 表示删除/留下
  2. 进行句子级别的聚类,裁剪掉0的token数量大于1的token数量的句子

如何训练呢?选取有5~10个句子的段(可以多次选取来拓展到更长的上下文),标上句子序号,让LLM选择相关句子来产生label,从而训练模型

这里其实做了很有意思的工程设计,

  • 如果让LLM来打token-level的标,肯定是收集不到足够的样本的,并且真的无所谓多出来的几个token,更在意句意的完整性

  • BERT带来了相当多的好处:

    • 这样进行的句子裁剪,每个句子都可以和整个chunk里面的所有上下文交互,使得一个句子的保留与否不仅取决于这个句子和查询的相关性,还取决于其于其他(和查询相关性高的)句子的相关性,这就使得这个方法必然会优于按句子切分的朴素方法

    • 我们的 reranker 也就是个BERT啊,完全可以训裁剪和训rerank一起进行,推的时候也一样,相当于和rerank overlap了

      image/png

在线软裁剪

Oscar

Pisco为代表的离线软裁剪有一个问题是,它的压缩需要微调,并且受限于难以对齐encoder-only架构的预训练编码器模态和实际推理使用的decoder LLM的模态,难以把压缩这一步在线做

Oscar就提出了一种方法是,我的对齐既然难做,我直接不对齐了,使用LLM的前L层 + memory token(他们也做了用Llama硬对齐的版本),足以得到够好的embedding,文章最大的贡献其实是实验证明了这样表达能力已经足够,能训出来(太神奇了LLM)。当然,L越大效果越好

而还是复用Provence的工程技巧,把裁剪和rerank overlap起来,OSCAR的compressor留了一个RR头,在这个头和Teacher Reranker对齐,整体的Loss就是rerank loss + generation loss

而令LLM理解embedding这件事情还是通过LoRA adapter来做,这篇文章其实像是序列工作的延申,综合了PISCO的训练方法,把PISCO的压缩部分从LLM + LoRA换成目标模型的前N层transformer,然后压缩器全参微调、生成器LoRA微调,再使用和Provence相同的技巧进行rerank的overlap

image-20250907213839337

异曲同工

从HyDE到“投机解码”

另一个有趣的工作是广义上的“裁剪”,或者就是更好的搜索吧。我们知道HyDE的思想是原始query一般都比较短,而生成的假设文档可能会更好地与索引文档对齐,所以使用 q‘ = q + generated d 来进行搜索。

而智谱的memorag 则提出了这样一种场景,我们是否能以低成本训练一个小模型,来根据源文本生成这个假设答案呢?(例如,使用Llama3-8B在哈利波特上训练比用Deepseek-R1在哈利波特上训练成本要低廉的多,将HyDE的生成方从R1自己换成小模型)这就非常像是投机解码的思想了

外接模块: memory decoder, catridges

其实这种将memory训为embedding的方法确实不少,如果说前面的压缩器是在训一个meta network,能够从doc生成embedding的话,外接模块的工作就是在训练embedding本身 -> 我能否直接从一个大的文档库中训练出一个参数化的memory?

最近的memory decoder选择的是直接扭曲生成过程,将一个小模型在目标适配数据集上训练,在大模型生成token时,将小模型的概率和大模型的概率相加(再重归一化),认为这样会带来领域知识的纠正(比较暴力www)

而另一篇catridges 则是在使用类似P-tuning的方式训一个Prefix KVCache,在推理时实时加载,而希望这个KV中有相关的memory

包括一系列的kvcache evict的工作也是在做类似的东西,为了决定evict哪些甚至都把搜索又搬上来了,比如clusterKV的knn(笑)

总结

总体而言,我感觉相关工作已经进入了深水区了,硬裁剪可能在某些程度上到头了,现在主流在探索一些牺牲解释性的,更能scale out的方法来进行参数化memory来解决长上下文、领域适配等一系列问题

而大家方法逐渐趋向于无标签学习的统一也再次证明了scale out能力在广义embedding能力的训练上的重要性

另一个很有意思的是,可以看到搜索中的多向量和多memory token有一些很有趣的相似性,或许在后续的一些工作中,我们能看到一些多向量的方法被用到memory之中,希望会让memory这个很多时候靠prompt编故事的领域更多可验证性吧

而从另一个方面,正如这里列出的部分文章说的,自从eagle在投机解码中得到了确实很好的效果之后,大家都开始用 token + embedding的混合来捕捉更强的信息了,还有HyDE和投机解码这种很有趣的对应

结构化输出与AI工具与Agent

· 17 min read
ayanami

假如大伙接到一个需求,需要把claude code接入jupyter前端(例如,在jupyter前端直接输入魔法指令和claude code交互,而后台claude code展示claude code的一些关键节点,工具调用,费用开销,输出结果等),会怎么做?

一种想法是,将claude code的输出塞到一个文件里面去,起一个后台线程读取这个文件,尝试解析之中的某些部分,再以插件的形式加载到jupyter前端

但带来了一个问题是,效果(尤其是工具数量upup,上下文长度upup后的效果)不稳定,纯prompt的形式约束claude code及时向这个文件中写入以向前端通信,在经过长的交互过程后,claude经常会把这个文件忘掉

那claude code直接全塞前端呢?

在claude code里面问一个问题,可能就是几千上万token的交互,全塞前端,那用户体验就烂掉了。

另一个很容易想到的方案是,那我们不要让他输出文件了,直接当场处理把,定义一些特殊块叫 display 之类的东西,在prompt里面指定这个块里面是什么格式,让他如果想要和前端输出的话,放到这个块里面

这样看起来比文件好一些,但带来了新的问题没解决,长上下文下,display块的结构偶尔会有不稳定,会有不少特殊的渲染格式如html等由于几个字符的差异退化成了纯文本

如何修复这个呢?一个简单的方法,也是你能在任意一个现在的agent中看到的,是及时判错,再把把错误的部分发给模型让他修复一下,但又带来了额外的开销,并且前端的呈现也收到影响

有没有更优雅的办法呢?

如果你做AI应用比较多的话,肯定注意到了这实际上是一个结构化输出(约束解码)的场景,但现在的问题是,输出不止是一个json,而是正常文本块和display块的交错

(对于不了解约束解码的简单介绍一下,就是把上层的json等约束编译成状态机之后,用于动态建立llm output logits的mask,从而杜绝输出非法输出的技术)

看起来似乎不能约束解码?但display块本身是可以约束解码的,好恶心。

让我们打开vllm文档,翻到 Structured Outputs,你会发现,除了常见的regex约束解码之外,还有两种更强语义的解决方案,救赎之道就在其中,ebnf解码和structure tags解码

实际上,json解码只不过是ebnf解码的特殊情况罢了,毕竟实际都是状态机 (不知道ebnf是什么的同学,可以搜索一下编译前端,BNF范式,就能看懂下面的示例啦)

官方给的一个ebnf解码的例子如下, 用于执行一个简化sql的约束解码以提升sql正确率

simplified_sql_grammar = """
root ::= select_statement

select_statement ::= "SELECT " column " from " table " where " condition

column ::= "col_1 " | "col_2 "

table ::= "table_1 " | "table_2 "

condition ::= column "= " number

number ::= "1 " | "2 "
"""

completion = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "Generate an SQL query to show the 'username' and 'email' from the 'users' table.",
}
],
extra_body={"guided_grammar": simplified_sql_grammar},
)
print(completion.choices[0].message.content)

如果放到这个问题,我们可以快乐地写出类似这样的定义

output := (display | normal text) *
display := (```display json ```)
json = ...
normal text = others

其中,display, json都是容易得到的,但恶心的地方在于什么是“others” 未拓展的ebnf是没有“非”定义的,从实操上虽然感觉可行(mask token取反),但这下已经没有支持了

(但ebnf解码肯定是有大用的,还是以Text2SQL举例,任何一个数据库都会给你他们的解析引擎的ebnf定义,都不需要你写)

怎么办呢,就带来了最后一个冷门工具,structured tags, 我先上代码,

def get_structural_tag_params(
tags: list[StructuralTag], triggers: list[str]
) -> dict:
return {
"type": "structural_tag",
"structures": [model.model_dump() for model in tags],
"triggers": triggers,
}

model_v2 = ChatOpenAI(
base_url=base_url,
model=model_name,
api_key=api_key,
temperature=0.15,
top_p=0.9,
extra_body={
"response_format": get_structural_tag_params(
tags=[
StructuralTag(
begin="<block=text>",
end="</block>",
schema=TextMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=image>",
end="</block>",
schema=ImageMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=tool_use>",
end="</block>",
schema=ToolUseMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=todo_list>",
end="</block>",
schema=TodoListMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=html>",
end="</block>",
schema=HTMLMsgSchema.model_json_schema(),
),
],
triggers=["<block="],
)
},
)

这个tags + triggers, 就是structured output的关键之处,它允许我们在trigger触发的时候才开始约束解码,在end结束的时候停止约束解码

至此,这个工作已经做完了


那约束解码和不约束带来的效果差距有多大呢,我在24B的Mistral-Small上做了个实验 最后的结果直接尝试解析后渲染到前端

Prompt如下,

sys_prompt = f"""

你是一个agent模型,你负责处理用户的问题,发起工具调用, 绘制图片、html、获取文本等。

由于你的token交互量很大,不是所有信息都需要展示给前端。

你可以正常思考和输出,但你需要将你认为需要展示给用户的有效信息包裹在 `<block={{tag}}> {{schema}} </block>` 中。

前端会将这部分内容进行渲染,交给用户。

你现在可用的tag有:

tags: "text", "image", "tool_use", "todo_list", "html"

对应的schema(pydantic格式)如下:

- {schemas_str}

例如,你可以先产生一个todo list,然后不断执行子任务,并更新todo list,直到所有任务完成。

由于你现在没有接入工具调用,所以对于所有工具调用交互,你只需要“假装”执行了工具调用并得到一个合理的响应就行,这是一个debug环境,

你需要根据用户的问题尽可能多的展示不同的block,并给出一个合理的响应。

"""

这个prompt下,<block=text>111</block> 这种就取代了上文所述的display块的效果

只定义了五种特殊的前端展示格式,文本,图片,TODO list,工具调用和HTML块

效果对比如下:

用户:帮我完成编写一个论坛帖子,打开浏览器的水源社区论坛,登录之后在discourse发帖的流程。

alt text alt text alt text alt text

可以看到,左侧没有约束解码的模型,在这样的任务负载下,json 参数就已经频频出现失误了,而右边的即使是24B模型的fp8量化非思考版本,却跑出了几百B agent的气势,并且token开销是来回倒腾的几分之一


一点感想: 我们常说一个子领域的知识对于另一个子领域是用处寥寥的,然而,这不是拒绝新领域知识的理由啊,vllm和xgrammer、outlines这种框架都把几种更强大的结构化解码方法摆到人们的脸上了,还是能在知乎看到“ebnf好像是编译原理的内容,(作为后端程序员)跳过”,或者是在各种开源仓库中还在广泛使用的拿prompt指导llm输出,完全不考虑(甚至不知道)结构化输出这样的东西

现在的后端、infra、算法,又有多少更深的优化方案是独立的呢?今天在看snowflakes优化方案,真是把上层算法和底层infra相辅相成,只是缺乏探索性的人们,会拿"这不是我的工作,这是专攻模型/infra/算法的人的工作"搪塞,最后又堆起来一个prompt史山罢了

在现在的agent框架中,充斥的也是prompt的兜底方案,带来的是qwen3-coder几个问题爆掉用户百万token,带来的是claude code问个“你是谁”都要花一角钱,但有没有一种可能,我们本可以用更确定的东西呢?LLM是一种万能的模糊推理,但好钢也要用在刀刃上啊。

参考完整代码如下

# ruff: noqa: E501
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import argparse
import asyncio
import enum
import json
import os
import re
from pathlib import Path
from typing import Any

import colorlog
import openai
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from dotenv import load_dotenv

load_dotenv()


class StructuralTag(BaseModel):
begin: str
end: str
schema: dict[
str, Any
] # JSON schema for validation, model_dump by pydantic model


class TextMsgSchema(BaseModel):
text: str = Field(..., description="Text message")

def to_html(self) -> str:
"""Render text message as HTML"""
return f'<div class="text-message">{self.text}</div>'


class HTMLMsgSchema(BaseModel):
raw_html: str = Field(..., description="raw html str, like <div></div>")

def to_html(self) -> str:
"""Render HTML message as HTML"""
return f'<div class="html-message">{self.raw_html}</div>'


class ImageMsgSchema(BaseModel):
image_url: str = Field(..., description="Image URL")
image_name: str = Field(..., description="Image name")

def to_html(self) -> str:
"""Render image message as HTML"""
return f"""<div class="image-message">
<img src="{self.image_url}" alt="{self.image_name}" style="max-width: 100%; height: auto;">
<p class="image-caption">{self.image_name}</p>
</div>"""


class ToolUseMsgSchema(BaseModel):
tool_name: str = Field(..., description="Tool name")
args: dict[str, Any] = Field(..., description="Tool args")
tool_output: dict[str, Any] = Field(..., description="Tool output")

def to_html(self) -> str:
"""Render tool use message as HTML"""
args_html = json.dumps(self.args, indent=2, ensure_ascii=False)
output_html = json.dumps(self.tool_output, indent=2, ensure_ascii=False)
return f"""<div class="tool-use-message">
<h4>Tool: {self.tool_name}</h4>
<div class="tool-args">
<strong>Arguments:</strong>
<pre>{args_html}</pre>
</div>
<div class="tool-output">
<strong>Output:</strong>
<pre>{output_html}</pre>
</div>
</div>"""


class TodoListMsgSchema(BaseModel):
todo_list: list[tuple[bool, str]] = Field(..., description="Todo list")

def to_html(self) -> str:
"""Render todo list message as HTML"""
items = []
for done, item in self.todo_list:
checked = "checked" if done else ""
item_class = "completed" if done else "pending"
items.append(
f'<li class="{item_class}"><input type="checkbox" {checked} disabled> {item}</li>'
)
items_html = "\n".join(items)
return f"""<div class="todo-list-message">
<h4>Todo List</h4>
<ul class="todo-list">
{items_html}
</ul>
</div>"""


def get_structural_tag_params(
tags: list[StructuralTag], triggers: list[str]
) -> dict:
return {
"type": "structural_tag",
"structures": [model.model_dump() for model in tags],
"triggers": triggers,
}


def parse_structured_response(response: str) -> str:
"""Parse structured response and convert blocks to HTML"""
# Schema mapping
schema_classes = {
"text": TextMsgSchema,
"image": ImageMsgSchema,
"tool_use": ToolUseMsgSchema,
"todo_list": TodoListMsgSchema,
"html": HTMLMsgSchema,
}

def replace_block(match):
tag_type = match.group(1)
content = match.group(2).strip()

if tag_type not in schema_classes:
return match.group(0) # Return original if unknown tag

try:
# Parse JSON content
data = json.loads(content)
# Create schema instance
schema_instance = schema_classes[tag_type](**data)
# Return HTML
return schema_instance.to_html()
except (json.JSONDecodeError, ValueError) as e:
return (
f'<div class="error">Error parsing {tag_type} block: {e}</div>'
)

# Replace all <block=type>content</block> with HTML
pattern = r"<block=(\w+)>\s*(.*?)\s*</block>"
return re.sub(pattern, replace_block, response, flags=re.DOTALL)


def create_comparison_html(response1: str, response2: str) -> str:
"""Create a comparison HTML page with both responses"""
parsed_response2 = parse_structured_response(response2)

css = """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: #2563eb;
color: white;
padding: 20px;
text-align: center;
}
.comparison {
display: flex;
min-height: 600px;
}
.column {
flex: 1;
padding: 20px;
border-right: 1px solid #e5e5e5;
}
.column:last-child {
border-right: none;
}
.column h3 {
margin-top: 0;
color: #1f2937;
border-bottom: 2px solid #e5e5e5;
padding-bottom: 10px;
}
.content {
line-height: 1.6;
color: #374151;
}

/* Schema-specific styles */
.text-message {
background: #f8fafc;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #3b82f6;
}
.image-message {
background: #f0fdf4;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #10b981;
text-align: center;
}
.image-caption {
margin: 10px 0 0 0;
font-style: italic;
color: #6b7280;
}
.tool-use-message {
background: #fefce8;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #eab308;
}
.tool-use-message h4 {
margin: 0 0 10px 0;
color: #92400e;
}
.tool-args, .tool-output {
margin: 10px 0;
}
.tool-args pre, .tool-output pre {
background: #1f2937;
color: #f9fafb;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
.todo-list-message {
background: #fdf2f8;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #ec4899;
}
.todo-list-message h4 {
margin: 0 0 10px 0;
color: #be185d;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
margin: 5px 0;
padding: 5px 0;
}
.todo-list li.completed {
text-decoration: line-through;
opacity: 0.7;
}
.html-message {
background: #f5f3ff;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #8b5cf6;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #dc2626;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
"""

return f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模型响应对比</title>
{css}
</head>
<body>
<div class="container">
<div class="header">
<h1>模型响应对比</h1>
<p>左侧:无结构化标签 | 右侧:带结构化标签(已渲染)</p>
</div>
<div class="comparison">
<div class="column">
<h3>无 Structure Tag</h3>
<div class="content">
<pre>{response1}</pre>
</div>
</div>
<div class="column">
<h3>Structure Tag(已渲染)</h3>
<div class="content">
{parsed_response2}
</div>
</div>
</div>
</div>
</body>
</html>
"""


if __name__ == "__main__":
base_url = "localhost:8000/v1"
model = openai.OpenAI(base_url=base_url, api_key="sk-")
schemas = [
TextMsgSchema.model_json_schema(),
ImageMsgSchema.model_json_schema(),
ToolUseMsgSchema.model_json_schema(),
TodoListMsgSchema.model_json_schema(),
HTMLMsgSchema.model_json_schema(),
]
schemas_str = "\n- ".join([json.dumps(s, indent=4) for s in schemas])
sys_prompt = f"""
你是一个agent模型,你负责处理用户的问题,发起工具调用, 绘制图片、html、获取文本等。
由于你的token交互量很大,不是所有信息都需要展示给前端。
你可以正常思考和输出,但你需要将你认为需要展示给用户的有效信息包裹在 `<block={{tag}}> {{schema}} </block>` 中。
前端会将这部分内容进行渲染,交给用户。

你现在可用的tag有:
tags: "text", "image", "tool_use", "todo_list", "html"
对应的schema(pydantic格式)如下:
- {schemas_str}

例如,你可以先产生一个todo list,然后不断执行子任务,并更新todo list,直到所有任务完成。
由于你现在没有接入工具调用,所以对于所有工具调用交互,你只需要“假装”执行了工具调用并得到一个合理的响应就行,这是一个debug环境,
你需要根据用户的问题尽可能多的展示不同的block,并给出一个合理的响应。

"""
base_url = "http://localhost:8000/v1"
model_name = "stelterlab/Mistral-Small-3.2-24B-Instruct-2506-FP8"
api_key = "sk-"
model = ChatOpenAI(
base_url=base_url,
model=model_name,
api_key=api_key,
temperature=0.15,
top_p=0.9,
)
print("-" * 50)
logger = colorlog.getLogger("Agent")
msgs = [
SystemMessage(content=sys_prompt),
HumanMessage(
content="帮我完成编写一个论坛帖子,打开浏览器的水源社区论坛,登录之后在discourse发帖的流程。"
),
]

model_v2 = ChatOpenAI(
base_url=base_url,
model=model_name,
api_key=api_key,
temperature=0.15,
top_p=0.9,
extra_body={
"response_format": get_structural_tag_params(
tags=[
StructuralTag(
begin="<block=text>",
end="</block>",
schema=TextMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=image>",
end="</block>",
schema=ImageMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=tool_use>",
end="</block>",
schema=ToolUseMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=todo_list>",
end="</block>",
schema=TodoListMsgSchema.model_json_schema(),
),
StructuralTag(
begin="<block=html>",
end="</block>",
schema=HTMLMsgSchema.model_json_schema(),
),
],
triggers=["<block="],
)
},
)

logger.info("=== 测试开始 ===")
response1 = model.invoke(msgs).content
logger.info(f"=== 测试结束 ===\n{response1}")

logger.info("=== 测试开始 ===")
response2 = model_v2.invoke(msgs).content
logger.info(f"=== 测试结束 ===\n{response2}")

# 生成对比HTML文件
comparison_html = create_comparison_html(response1, response2)
Path("tmp/test_comparison.html").write_text(
comparison_html, encoding="utf-8"
)
logger.info("已生成对比HTML文件: tmp/test_comparison.html")

# 保留原有的Markdown文件
with Path("tmp/test_diff.md").open("w", encoding="utf-8") as f:
f.write("无structure tag: \n")
f.write(response1)
f.write("\n\nstructure tag: \n")
f.write(response2)

context-engineering

· 10 min read
ayanami

rag那边最近看到的新的概念,现在prompt工程不叫prompt工程了,叫上下文工程 (context engineering),笑

上下文工程概念的兴起主要是两个方面,一是更关注多轮和工具,prompt无法很好地概括这些部分,二是模型的能力并不能做到和声称的上下文一样(支持1M长度的模型,可能长度超过32K指标就会严重下滑)

上下文失败的几种情况,参考 How Long Contexts Fail | Drew Breunig

  • 上下文中毒: 幻觉和错误进入上下文,并反复引用,这个主要来源于google在用智能体玩游戏时出现的一些现象(超长程规划)

An especially egregious form of this issue can take place with “context poisoning” – where many parts of the context (goals, summary) are “poisoned” with misinformation about the game state, which can often take a very long time to undo. As a result, the model can become fixated on achieving impossible or irrelevant goals.

  • 上下文干扰 & 混淆 上下文干扰是指上下文变得太长,以致模型过度关注上下文,而忽略了在训练期间学到的内容。

The Berkeley Function-Calling Leaderboard is a tool-use benchmark that evaluates the ability of models to effectively use tools to respond to prompts. Now on its 3rd version, the leaderboard shows that every model performs worse when provided with more than one tool4. Further, the Berkeley team, “designed scenarios where none of the provided functions are relevant…we expect the model’s output to be no function call.” Yet, all models will occasionally call tools that aren’t relevant.

随着模型变小,问题变得越来越严重 alt text

问题是:如果你把某些东西放入上下文中, 模型就必须注意它。 它可能是无关的信息或不必要的工具定义,但模型会将其考虑在内。大型模型,尤其是推理模型,在忽略或丢弃多余上下文方面做得越来越好,但我们仍然看到无用的信息绊倒了智能体

关于信息之间和信息与问题的交互,google和其他机构都有不少的research paper,现在广泛认为,信息自己有几个“原子事实”不太重要,但信息之间的的一致性和独立性(相互cover不同部分以从根本解决信息冲突的问题)以及信息和query的相关性很重要

  • 上下文冲突 A Microsoft and Salesforce team documented this brilliantly in a recent paper.

分阶段提供信息,模型的表现严重下降

We find that LLMs often make assumptions in early turns and prematurely attempt to generate final solutions, on which they overly rely. In simpler terms, we discover that when LLMs take a wrong turn in a conversation, they get lost and do not recover. 我们发现,LLM 们经常在早期阶段做出假设,并过早地尝试得出最终解决方案,而他们过度依赖这些解决方案。简而言之,我们发现,当 LLM 们在对话中走错方向时,他们会迷失方向,无法恢复。

Andrew Karpathy依然擅长炒作,他的观点是LLM as a new OS. Context is RAM

上下文工程:在上下文窗口中为下一步填充恰到好处的信息的科学

有哪些呢?

  • Instructions
  • Knowledge
  • Tools

上下文工程策略:写入上下文,选择上下文,压缩上下文,隔离上下文

写入上下文:

  • 临时笔记板,可以是会话的状态对象,也可以是简单的工具调用写文件 Anthropic 的研究表明,将“笔记板”工具与特定领域的提示配对使用可以带来显著的收益,与专业代理的基准相比,最高可提高 54%。这也称作上下文卸载(Context Offload), 参考 The "think" tool: Enabling Claude to stop and think \ Anthropic

Anthropic identified three scenarios where the context offloading pattern is useful: Anthropic 确定了上下文卸载模式有用的三种场景:

Tool output analysis. When Claude needs to carefully process the output of previous tool calls before acting and might need to backtrack in its approach; Policy-heavy environments. When Claude needs to follow detailed guidelines and verify compliance; and Sequential decision making. When each action builds on previous ones and mistakes are costly (often found in multi-step domains).

  • 记忆:跨模型、跨会话,独立存储。Reflexion + 定期整理记忆

选择上下文:

  • 记忆选择:Langchain将记忆归为几种类别:Semantic, Episodic, Procedural 对应 Facts,Experiences和Instructions,一个挑战是选择相关记忆。Claude Code使用CLAUDE.md,Cursor和Windsurf使用规则文件
  • 工具管理:例如对工具list用RAG,这个在现在的MCP中很多都在尝试,比如OSPP就有这样的项目

压缩上下文:Claude Code当交互占用超过上下文的95%之后,会自动压缩,总结用户-Agent的完整轨迹。可以是递归或者分层摘要。也可以在一些特定点添加摘要(如某些工具调用),Cognition为此使用微调模型 上下文修剪:启发式删除旧信息,provence作为上下文修剪器

隔离上下文:

  • 拆分到子Agent之间,OpenAI Swarm动机是关注点分离,一组Agent完成各自的子任务
  • 工具代码沙箱

LangGraph & LangSmith

LangGraph 在记忆(状态)上面做了努力,选择 上下文也通过这个State获取,压缩上下文通过状态对象进行自定义逻辑,隔离通过子图和节点

LangGraph基于状态机的实现倒是暗合了OS=状态机的观点,从一个比较底层的视角上为各种上层应用提供了可能,也可以复用业界关于状态机的一系列优化已有实践


而关于上下文管理的评估侧,一个比较热门的评估和观测系统Galieo设置了四种指标来评估一个RAG应用: Adherence,Completness, Utilization,Attribution 对上文的忠实度,上文本身对解答这个问题的完整性,答案对于上文的利用度,答案对于不同chunk的归因

在它的博客之中,给了一个非常真知灼见的观点是,过多的指标本身没什么意义,它选择这四个指标的原因是能定位出是链路的哪一块出现了问题,这个指标也只有low, medium, high三级,不做复杂的打分,倒是有点像是推荐系统里面的分桶离散特征

例如,如果整体利用度低,但完整度高,那么冗余信息太多了,减少chunk size和输入LLM的chunk数量N;如果整体忠实度高,但完整度低,则需要考虑是搜索的问题(多样性考虑不够)还是数据的问题(根本就没有足够多的文档);而归因性可以用来调整chunk size,裁剪等参数;忠实度不够则主要从prompt和chunk数量入手......

与其他一切最终被广泛利用的策略相同,Galieo也蒸馏了一个小BERT来代替昂贵的LLM进行打分,以此提供一个本地托管的方案


另一个上下文管理的有趣的工作是直接进行暴力的token-level prompt压缩,来自微软的LLMLingua论文,其基于两个观察

  • 自然语言有冗余
  • 传统的信息熵指标只有单向上下文, 且与提示压缩指标不一致 alt text 因此,开蒸!总之也是训了XLM-RoBERTa-large & mBERT的模型替代LLM(可以发现现在的RAG基本就是 寻找问题-大模型蒸馏训练-用专业小模型代替-形成nlp管道 的范式,在几乎每一个组件都是如此,效果也好)

从微调reranker到搜推工程实践

· 21 min read
ayanami

如何进行reranker微调?

之前我曾经花了一定时间找这个问题的经验,结果发现大部分reranker模型对于这个问题是一个回避状态,不愿意开源自己的训练集,更不提像OpenAI/Cohere的rerank/embed服务本身就在卖钱,而兜售rag解决方案的公司,更不肯将如何做领域适配这一赚钱核心逻辑公之于众

也就BAAI以一个非常开放的态度,公开了自己的微调方法和相关脚本和训练数据,但他们也更侧重与如何训练一个通用的模型,对于怎么微调,只知道构造正负样本,query,pos,neg,然后InfoNCE,至于为什么能work,pos/neg怎么选,可能觉得大家都知道,也没有多说

而兜兜转转的楼主最后在传统搜推里面找到了一整套硬负例挖掘方面的方案,rag整套方案其实都是抄搜推的一个劣化版本罢了 🤣

为什么采用的是正负对而不是交叉熵或者其他有label的损失?核心在于,搜推本身就是一个弱label的场景

乍一想,在有正负对的情况下的时候,交叉熵似乎也很自然,以01为例,两种损失项就是 <user,item+,1><user,item,0><user, item_+, 1>, <user, item_-, 0> ? 但一个随之而来的问题是哪来的01 label?

也就是说,这样做的前提是label的准确性,而在搜推场景中,负样本 <user,item,0><user, item_-, 0> 的一个设置是曝光过但没被user选择的真负样本

但召回层的大部分样本根本没被曝光过,label噪声很大(召回层是一个几亿->几千->几十条的过程,只有最后的几十被曝光了),如果只依赖这样的负样本的话,根本无法支撑模型训练。所以正负样本的设计某种意义上是无奈之举,我无法知道这个样本和用户的真实关系,但我可以从用户的行为中得到一些偏好信号,召回算法往往采用Pairwise LearningToRank (LTR),建模排序的相对准确性,模型的优化目标变成正样本匹配度高于负样本匹配度


现在我们知道了为什么采用正负样本,但真正上手就会发现,正负样本这一件事并没有想象中的简单。

如果你采用随机的语料作为负样本,带来的一个问题是这个负样本对模型太easy了,模型只能区分猫和狗,但无法区分哈士奇和狼狗,即忽视了细节信息,也即是我们所说的rag的领域细节的缺失

而解决的方法,也在搜推里面早就提出了,硬负样本挖掘,即设置一部分的硬负样本,这部分是有难度的,来迫使模型学会根据细节进行区分

而在rag里面大家常常是拍脑门的硬负样本设计,让reranker带上一些业务目标,在搜推里面也早是被玩烂的东西了。

先说业务目标: 比起rag中,大部分的应用还局限在文本相似度,搜推早就进入到多个因素的融合和全链路目标指向的优化,例如,很多搜推业务需要考虑地域性(如外卖,酒店等),于是其正负样本会这样设计: 有基于业务逻辑的,核心是增强某个指标的相似性,让模型考虑其他指标做出区分,以房屋销售为例

  • 增加与正样本同城的房间作为负样本,增强了正负样本在地域上的相似性,加大了模型的学习难度
  • 增加“被房主拒绝”作为负样本,增强了正负样本在“匹配用户兴趣爱好”上的相似性,加大了模型的学习难度

针对模型只学地域特征信息就可以进行打分的easy neg,设计了同城的hard neg强迫考虑其他特征

绝大部分负样本还是随机采样生成的。但是,Airbnb发现,用户点击序列中的listing多是同城的,导致正样本多是同城listing组成,而随机采样的负样本多是异地的,这其中存在的bias容易让模型只关注“地域”这个粗粒度特征。

为此,Airbnb在全局随机采样生成的负样本之外,还在与中心listing同城的listing中随机采样一部分listing作为hard negative,以促使模型能够关注除“地域”外的更多其他细节。

在电商场景下,负样本的业务构造也有很多:

  • 正样本:充足曝光高点击ctr样本(如:ctr大于同query下商品点击率平均值)
  • 负样本:
    • 同父类目的邻居子类目负采样。
    • 高曝光低点击类目样本:同一个query搜索下,根据全局点击商品的类目分布,取相对超低频类目样本作为负样本。
    • 充足曝光情况下,低于相应query平均曝光点击率一定百分比的样本做负样本。
    • 基于query核心term替换构造负样本:如,对于“品牌A+品类”结构的Query,使用“品牌B+品类”结构的query做其负样本。(这个lz当时在propilot构造领域词替换负样本的时候还觉得自己想到了个好方法,后来发现是早有之事)
    • 随机构造负样本:为增加随机性,该部分实现可在训练时使用同batch中其他样本做负样本,同时也可以引入经典的Hard Sample机制。(这部分涉及到很有趣的一个问题,后面讲)

不局限于业务,搜推还对RAG很少涉及的“如何选择hard neg”上面有非常久远的研究,如

  • 高置信样本挖掘,避免搜索点击行为日志“点击但不相关”的问题。

  • **定制化的负样本构造,避免模型收敛过快,**只能判断简单语义相关性,对难样本无法很好的区分。

  • 关于短文本的定制化需求, 如美团提到的他们实践的一些难Case,“大提琴”→“小提琴”以及“葡萄酒”→“葡萄”这类字面编辑距离小的case,会根据搜索结果做分析,以搜索无结果作为bad case进行负样本生成 alt text

  • 知识图谱也是被玩烂的东西 alt text

  • 图结构也是被玩烂的东西,如在Pinterest中,基于GCN的PinSAGE

和Airbnb一样,我们可以认为被同一个user消费过的两个item是相似的,但是这样的排列组合太多了

为此,PinSAGE采用随机游走的方式进行采样:在原始的user-item二部图上,以某个item作为起点,进行一次二步游走(item→user→item),首尾两端的item构成一条边。将以上二步游走反复进行多次,就构成了item-item同构图。

在这个新构建出来的item-item同构图上,每条边连接的两个item,因为被同一个user消费过,所以是相似的,构成了训练中的正样本。

  • 在训练开始前,
    • 从item-item图上的某个节点u,随机游走若干次。
    • 游走过程中遍历到的每个节点v,都被赋予一个分数L1-normalized visit count=该节点被访问到的次数 / 随机游走的总步数。
    • 这个分数,被视为节点v针对节点u的重要性,即所谓的Personal PageRank(PPR)。
  • 训练过程中
    • 针对item-item同构图上的某一条边u→v,u和v就构成了一条正样本,它们的embedding应该相近
    • 在图上所有节点中随机采样一部分ne,u和每个ne就构成了一条负样本,它们的embedding应该比较远。因为是随机采样得到的,所以ne是easy negative。
    • 除此之外,还将u所有的邻居,按照它们对u的重要性(PPR)从大到小排序,筛选出排名居中(e.g.论文中是2000~5000名)的那些item。这些item与u有几分相似,但是相似性又没那么强,从中再抽样一批item,作为"u"的hard negative。

....

  • 利用传统nlp思路的

在airbnb中,用户的点击序列,如果用类似word2vec+窗口的想法看成是一个“共现”问题的话,用户点击序列中的项的不像语言那样有一个很明显的长程衰减,embedding都应该是相近的。 但这样的组合太多,所以回退到窗口的方式,拿中心项和邻居项组成正样本对。但因为最后一次下单的点击有最强的业务信号,所以拿它和整个序列的每一项组成正样本对,“增加final booked listing作为global context加入每个滑窗”


解决了如何构造硬负样本的问题,那应该选择多少硬负样本呢?如果自己跑过reranker的微调就会知道,过高的硬负样本比例甚至会让模型崩掉。而更是有拿调reranker的数据集拿来调embedder的神人没错,就是我自己),BAAI官方的脚本中,这俩也没啥区别 🤣

然而,早在N年前Facebook的文章中,就给出了他们的经验教训

  1. 将比例维持在easy:hard=100:1
  2. 将rerank的数据拿来训embed(在搜推场景中是拿曝光未点击数据(rerank前列但未收到信号)来当召回(embed)的负样本)是完全错误的实践,离线数据可能不错但一上线就是一坨

这是为什么呢?因为召回不同于排序,在rag层要处理的文档没有那么多可能无感知,很多rag甚至没有排序层拿召回当排序,先下结论

如果说排序是特征的艺术,那么召回就是样本的艺术,特别是负样本的艺术。样本选择错了,那么上述的模型设计、特征工程,只能是南辕北辙,做得越卖力,错得越离谱。

alt text alt text

明白了这个数据分布的区别之后,就会对前面硬负样本和简单样本的比例在不同阶段是不同的这一个特点有更深的理解,对于召回而言

hard negative并非要替代easy negative,而是easy negative的补充。在数量上,负样本还是以easy negative为主,文章中经验是将比例维持在easy:hard=100:1。毕竟线上召回时,库里绝大多数的物料是与用户八杆子打不着的easy negative,保证easy negative的数量优势,才能hold住模型的及格线。

所以,全样本随机采样的负例才会很重要

而推荐甚至走的更远好几步,例如,随机采样不等于等概率采样,推荐系统中会出现放大的效应,即热门的样本会更容易被点击,进而各种指标特征表现更高,变得更热门,为了不然模型退化到只推荐一类样本,在实践之中会对热门正样本降采样,对热门负样本升采样

还有对硬负样本带来的左脚踩右脚

当业务逻辑没有那么明显的信号的时候,就需要依赖模型自己挖掘, 都是用上一版本的召回模型筛选出没那么相似的对,作为额外负样本,训练下一版本召回模型。怎么定义“没那么相似”?文章中是拿召回位置在101~500上的物料

Q: 这样选择出来的hard negative已经被当前模型判断为“没那么相似”了,那拿它们作为负样本训练模型,还能提供额外信息吗 A: 上一版本中,这批样本只是相似度靠后,现在直接划为负样本,能更迫使模型进行区分

而rag在玩的全链路RL优化,是推荐系统几年前玩了一波后来又扔到垃圾桶的东西 🤣性能不稳定,模拟和实测差距大,等等问题

包括现在在rag系统的reranker中还未广泛见到的刷点技巧,对不同难度级别的负例单独训小模型,然后做embedding融合


在工程性上,RAG的路也更像是把所有搜推的路再走一遍,

  • 如何解决冷启动问题?搜推已经证明了LR,FM这种一二阶特征就能得到一个不错的基线,并且可以将实数特征离散化,排0存储,排0计算进行O(N^2)到O(N)再二值化化乘为加得到在线级别的性能(用户每一次交互都是一次特征计算)

  • 如何解决系统效率问题?网络上参数服务器+只传递特征id,实数特征的分桶离散化,特征的Field级别合并减少NN的维度,log的一套大数据系统+redis冷热缓存+bloomfilter+......

  • 如何解决模型性能问题?在召回层禁止特征交叉,在排序层卷一系列现代架构,根据短文本特点进行深度语义层的裁剪,量化和蒸馏

  • 如何解决可解释性问题?用加权的ML模型做基线,bad case定位和迭代,先把神经网络丢一边......

  • 意图识别?训练NER任务,对查询做成分识别,丢掉不重要的词,在少无结果的时候做多级检索,甚至能把时延卷到10ms量级。BERT结合KG做领域词级别的mask而不是字符级别的mask,来达到对整个实体级别语义的理解效果 alt text

  • 多样性?召回通道的消重系统 alt text

  • 商业化?精排的广告插入......

  • 规模化? 一键训推平台,业务算法提交数据后集群分卡自动运行和效果验证

  • 稀疏样本?酒店这种看重订单率而不是相关性的就是最好的参考实践 alt text

现在传统RAG发现一个问题就是半结构化数据很难被embedding模型处理,但如果从这个角度反向想回去的话,搜推一直就是在处理结构化数据啊,还是走同一套特征离散化的逻辑,后面做Pooling和特征融合又可以复用各种实践,

  • 普通的Mean/Max Pooling,代表算法YoutubeNet,先embedding再pooling
  • Neural FM中,让属于同一field的feature embedding两两交叉,完成所谓的Bi-Interaction Pooling
  • 加权平均 - Attention, 阿里Deep Intereset Network (DIN),计算candidate item和用户各历史item的attention score,再根据这个score加权历史item的embedding,表示用户的历史偏好,使得用户的向量表达随着不同的candidate变化
  • 加权+时序,DIEN

所以,我们真的需要一个劣化的RAG系统吗?很多时候只是我们维护不起一套完整的搜推系统罢了,没有人力和体系力量去维护一个结构化的数据组,AB test和实时的线上反馈,又不在意系统的时延,吹嘘着LLM神话,消耗着大量的token,最后效果也就那样,还得根据线上信号进行优化,做来做去发现前人早就做过了(笑)

但是anyway,如果你需要做点rag的话,搜推这边的方法可能需要大规模人力物力不一定能用得上,但这边踩过的坑,再踩一次就是猪头了,也算是理解了为啥网上有做搜推的转RAG讲说从LLM转过来的完全不理解上线难点在哪里,会踩很多坑,或者永远停留在离线的状态

部分llm技术报告的阅读

· 19 min read
ayanami

Qwen

base model: 3 trillion tokens 三万亿

数据处理

预训练:公共文档,书籍,代码

HTML中提取文本,语言识别工具确定语言

增加多样性: MinHash/LSH 模糊去重

过滤低质量数据:rule-base/ML方法(语言模型,文本质量评分模型,内容审查模型)

进一步提高性能:高质量instruction数据集

tokenization: BPE byte pair encoding

cl100k base作为起点

最终152k词汇量

  • embedding: 解绑定的embedding方法,而不是绑定embedding和输出projection的权重 -> 内存换性能(why)
  • position embedding: RoPE fp32精度
  • Bias: 只有attention的QKV有
  • PreNorm & RMSNorm 提高训练稳定性,替代传统layer norm, prenorm > post-norm
  • activation function: SwiGLU

训练:上下文2048, flash attn, AdamW, 余弦学习率,最小为10%峰值,bf16混合精度训练

上下文长度拓展:在推理时做,训练时长上下文O(N^2)开销太大,NTK-aware interpolation

另外两个注意力机制: LogN-Scaling and window attention

sft对齐: ChatML-style format

该模型的训练过程采用AdamW优化器,具有以下超参数:β1设置为0.9,β2设置为0.95,(ϵ\epsilon)设置为(10810^{-8}). 序列长度限制为2048,batch size为128. 该模型总共经过4000步,学习率在前1430步逐渐增加,达到峰值(2×106)(2 \times 10^{-6}). 为了防止过拟合,应用权重衰减,值为0.1,dropout设置为0.1,梯度裁剪强制限制为1.0.

sft泛化和创造性的限制,容易过拟合 -> RLHF

PPO

tool-use造数据:左脚踩右脚,让Qwen生成更多示例相关的查询、特定格式的输出,应用规则+人工过滤产生训练样本,之后SFT

代码模型:继续预训练,900亿tokens + 8192长上下文

数学模型:1024上下文,数学问题较短加快训练速度

Qwen2.5

下载

预训练 18万亿tokens

大于100w样本的sft,多阶段强化学习

有效KVcache利用:GQA Group Query Attention

MoE架构:将标准FFN层替换为专门MoE层,每一层包括多个FFN专家和一个路由,路由将tokens分派给topk专家

tokenization从BPE升级到BBPE, 控制token增加

做数据:

  1. 更好的数据过滤

  2. 更好的数学和代码数据,在预训练就加

  3. 更好的合成数据,数学/代码/知识模型生成 + 奖励模型过滤

  4. 平衡样本分布

电子商务、社交媒体和娱乐等领域在网络规模数据中明显过度表示,通常包含重复的、基于模板的或机器生成的内容。 相反,技术、科学和学术研究等领域虽然包含更高质量的信息,但传统上代表性不足。 通过对过度表示的领域进行战略性降采样,以及对高价值领域进行升采样

长上下文拓展:

最终预训练 4k -> 32k, RoPE 10k->1000k

渐进式拓展

32k->64k->128k->256k

每一个截断包含40%的长序列和60%的短序列,避免遗忘

YARN + Dual Chunk Attention 降低困惑度来改善长序列推理性能

两阶段RL:

• Offline RL:此阶段侧重于开发奖励模型难以评估的能力,例如推理、事实性和 instruction-following。 通过对训练数据进行细致的构建和验证,我们确保 Offline RL 信号既可学习又可靠 (Xiang et al., 2024),使模型能够有效地获得这些复杂技能。

• Online RL:Online RL 阶段利用奖励模型检测输出质量细微差别的能力,包括真实性、helpfulness、简洁性、相关性、harmlessness 和 debiasing。 它使模型能够生成精确、连贯且结构良好的响应,同时保持安全性和可读性。 因此,模型的输出始终符合人类的质量标准和期望。

长上下文:准备long-response dataset:从pre-training data中生成long-text数据对应的长查询,并使用Qwen2过滤低质量data

数学:CoT + 拒绝采样 + reward model/annotated answer

编码:code-related QA web 合成示例,github收集snippet, sandbox code checking, 自动单元测试保证正确性

指令准随:利用可严格验证的code来做,拒绝采样

结构化数据:table QA/fact verification/error correct... 造数据集然后训

逻辑推理:70000个新query迭代改进

回复过滤:多智能体协作评分系统

离线rl, 客观查询领域,数学/编码/逻辑推理/指令追随, 确保回复的质量,只有通过质量检查的才是正示例,其他回答全是负示例

在线rl, 真实性,帮助性,简洁性,相关性,无害性和去偏见

GRPO, 先训response分数方差较高的query

当前的reward model评估benchmark无法准确预测在其指导下训练的RL模型的性能


Qwen3 Embedding

多阶段训练流程: 大规模无监督训练, 高质量数据集有监督微调

qwen3 产生高质量、多语言、多任务的文本相关性数据集,在无监督阶段利用,筛选出部分高质量用于有监督训练

Embedding, Instruction + query,之后直接**[EOS]**, 最后的嵌入是这个EOS对应的最后一层隐藏状态

Reranking, Instruction + query + doc,之后是Assistant:

image-20250629132905912

为了嵌入的统一,instruction和query会拼接成同一个输入,{Instruction} {Query}<|endoftext|>

重排: **pointwise,二分类问题,**遵循以下模板,实际相关性分数是yes|no的logits

image-20250629133305609

image-20250629133428708

使用InfoNCE作为损失函数

image-20250629133536398

qwen3采用批内负采样

即,在实际对比学习(尤其是检索任务)中,为了训练出区分能力强的模型,负样本非常关键。但生成或采样足够丰富且高质量的负样本往往代价很高或不现实。

因此,常用的做法是:

  • 批内负采样(In-batch Negative Sampling):利用当前训练批次中其他样本的 query qjq_j、正样本 dj+d_{j+}以及负样本 djd_{j-}作为当前样本qiq_i的负样本补充。

因为batch是随机取的,所以为了避免批内负采样形成错误信号,需要在qj,dj{q_j,d_j}中把实际是正样本的部分筛选掉,就是下面的掩码因子

image-20250629133751623

减轻false negative的影响

认为是错负例的:

  1. dj/qj=di+d_j/q_j = d_{i+}
  2. sim(dj/qj,qi)>sim(di+,qi)+0.1sim(d_j/q_j, q_i) > sim(d_{i+}, q_i) + 0.1的,即这个批内负样本比正样本还相似度高0.1以上,这个0.1可能是考虑到初期时数值的稳定性加的

此时掩码为0,LembeddingL_{embedding}的这一项是0,即不考虑这一项作为负样本

比起用if-else等前筛选方法,这个后筛选方法方便并行化,由于预计假负样本应该是比较少的,所以能提高效率

对于rerank, 定义SFT loss Lreranking=logp(lP(q,d))L_{reranking} = -log p(l| P(q,d))

ll对正面文档是"yes", 负面是"no"

创新点:

  1. 大规模合成数据,直接使用qwen3的合成数据而不是收集qa pair
  2. 高质量合成数据在SFT中的利用
  3. 模型合并:基于球面线性插值(slerp)对微调的多个ckpt进行合并,提升不同数据分布下的鲁棒性和泛化

image-20250629140122099

数据集合成:

Qwen3-32B, 利用检索模型从角色库中,识别出(文档可能对应的)前五个角色候选

然后,将文档+候选角色作为prompt,令模型输出最适合的角色配置

再将角色配置给到模型,生成查询

  • 多样性:使用查询类型(关键词、事实、摘要、判断、...)×查询长度×查询语言×查询难度×...查询类型(关键词、事实、摘要、判断、...) \times 查询长度 \times 查询语言 \times 查询难度 \times ... 作为模型合成数据的状态空间

最后产生了1.5亿对数据

高质量对:简单的余弦相似度计算,保留相似度大于0.7的,得到1200w对

结果 SOTA

两个分析:

  1. 不做合成数据训练,明显掉点
  2. 不做模型合并,也掉点

Qwen3

GQA, SwiGLU, RoPE, RSMNorm,Pre-Norm

移除QKV的bias, 在attn中引入QK-Norm

MoE, 128专家,激活8个,去除共享专家,采用全局批次负载平衡损失

预训练:36T tokens,100+语言,代码、STEM、推理、书籍、合成数据

部分数据是Qwen-2.5-VL对大量pdf做OCR再Qwen2.5进行文本优化得到的高质量文本数据

三阶段预训练:

  1. 通用阶段,30T tokens,4k max length,获取语言能力和世界知识
  2. 推理阶段,增加STEM, 代码,推理和合成数据的比例,5T tokens,4k max length,加速学习率衰减
  3. 长上下文阶段,32K,4k-16k 25% + 16k-32k 75%,RoPE 基础频率10000->1000000, 引入YARN和双向块注意力

后训练

image-20250629142730991

  1. 思考控制
  2. 强到弱蒸馏

CoT冷启动:query response两层过滤

query过滤用2.5-72B删除不容易验证的query, 如多个子问题和通用问题删除2.5-72B能直接正确回答不需要CoT的问题对每个Query进行领域标注,保持数据平衡

Response过滤用QwQ-32B, 每个问题生成N个response,

  1. 无法生成正确答案->人工标注
  2. 移除最后答案不正确的,存在大量重复的,猜测而缺乏推理的,推理和总结不一致的,混用语言的,可能和验证集过于相似的

冷启动数据直接SFT,学习基础推理模式,为后续RL打基础

推理RL:GRPO, 大Batch Size, 每个Query多Rollout

仅仅用了4k个数据,满足

  1. 冷启动没用过
  2. 冷启动模型可学习(不太难)
  3. 有挑战性
  4. 广泛子领域

235B-A22B进行了170个RL训练步骤

思考模式融合(控制不思考)

SFT数据集融合思考非思考的数据,同时设计聊天模板,非思考保留空思考块

image-20250629143421275

当模型学会在非思考和思考模式之间切换,就可以**处理基于不完整的思考生成答案,就可以让模型在思考过程中根据预算来强行停止思考过程。**即 当模型的思考长度达到定义的阈值时,插入停止思考指令:“考虑到用户的时间限制,我必须根据目前的思考直接给出解决方案。 \n</think>.\n\n”。并让模型继续根据其积累的推理生成最终响应。

通用RL

增强能力和稳定性

20多任务:指令遵循,格式遵循,偏好对齐,代理能力,特定场景能力(如RAG)

三种奖励:基于规则的奖励,基于模型的奖励(带参考答案),基于模型的奖励(无参考答案)

蒸馏:离线和在线

离线,教师模型产生输出给学生模型SFT

在线,教师和学生对相同prompt在输出logits对齐(最小KL散度)

Ablation

  • Math & code 做完 reasoning RL 达到顶峰
  • Agent tool use 能力,general RL 也很关键
  • 通用语言能力,很需要 General RL

有一个有趣的是thinking其实损害了长上下文的性能

img

RULER大海捞针,non-thinking mode更高,long CoT甚至掉点


GRPO & RLHF

RLHF大致两种,on policy(ppo)和off policy(dpo)

on policy的更耗卡,更耗时,但理论上限更高

ppo的四个模型:actor, critic, reference model, reward model

on policy:top_p = 1.0, top_k=-1,加大探索

critic 得出V(s)V(s),起一个降低方差的作用

ReMax: 用rrgreedyr-r_{greedy}作为baseline,(rgreedyr_{greedy}不是随机采样, 是状态的函数),解决critic model的开销

能够降低方差的原因是默认认为通常 SFT 模型已经经过一部分对齐,对于同一个 prompt 模型不太会输出差异性过大的答案

GRPO:直接退回PG是否有点原始

虽然Critic不仅占资源,并且在LLM这种全部回答的trajectory才重要的情况下,中间的“价值”也很难界定,但PPO还有别的先进feature可以保留,比如重要性采样和clip

img

只是将 AiA_i 的计算从criticcritic的输出换成了group内的相对值 rimean(rg)var(rg)\frac{r_i - mean(r_g)}{var(r_g)}

非常暴力,但非常有效

KL penalty用近似值保证KL始终是正数, ratio1logratioratio - 1 - log ratio

LLM中,通常将GAE的γ\gamma设置为1,因此也直接将这个得分复制到每一个token训练

尽管这种方法确实可以省掉一个 Critic,但成功需要具备 2 个关键:

  1. SFT 对给定的 prompt 不能有着太 diverse 的输出,否则方差会比较大。
  2. 对同一个 prmopt 采样的数量要可能大,这样才能降低方差

offline方法,DPO

降低不好答案被采样的概率,提升好回答的概率

DPO 有一个非常致命的问题

由于 DPO 的训练 loss 目标是「尽可能最大化好答案和坏答案之间的采样概率差」,

一种常见的情况是:好答案 & 坏答案被采样的概率同时在变低,只不过坏答案降低的比好答案更多

这种情况在 chosen 和 rejected 答案有大部分内容相同,仅有少部分内容不同时较为常见

DPOP添加了一个如果choosen答案在SFT模型(ref)中采样概率大于当前policy模型的采样概率,则减去的正则项(policy还没拟合好,少更新点)

TDPO 加上了KL,但是是forward KL(KL非对称,SFT计算采样概率是forward, policy model计算是backward KL)

由于 backward KL 的目标是拟合整个分布中的「一部分」,而 forward KL 的目标是尽可能 cover 整个分布中的大部分。因此,TDPO 训练后的模型会比 PPO 训练后的模型,在输出多样性上更加自由

token clipping 操作是导致性能下降的主要原因!尤其是像 "However"、"Recheck"、"Wait"、"Aha" 这类带有反思性质的 token 在初始模型中本就属于低概率 token,在更新过程中容易出现高奖励值继而被裁剪,从而无法继续为后续梯度更新提供贡献。

minimax提出CISPO,不丢弃token梯度,裁剪重要性采样权重

传统方法: 先计算 ratioAratio * A, 再裁剪乘积

CISPO: 先裁剪 ratioratio, 再乘以 AA

效率显著提高(2x speed)

早停: 目标不是对已经生成的重复文本进行惩罚,而是在模型进入重复循环前就终止生成。由于简单的字符串匹配难以应对复杂的重复模式,我们设计了一个基于 token 概率的启发式方法

一旦模型进入重复循环,所生成 token 的概率会大幅上升。因此我们制定了如下早停规则:

如果连续 3,000 个 token 的生成概率都大于 0.99,就立即终止生成。

Paper reading-Ask in Any Modality A Comprehensive Survey on Multimodal Retrieval-Augmented Generation

· 16 min read
ayanami

RAG 抽象来说就是,embed - opitional[rerank] - generate管道

有许多的增强方案,例如 Plan X RAG(将问题分解为子问题的DAG,然后设计一些critic LLM判断流的状态正常与否,一个执行LLM按照拓扑序执行DAG),Agentic RAG, feedback-driven iterative refinement

局限是:传统RAG主要针对文本,多模态集成还是挑战

流程概述如下图


Refer to caption


Multimodel RAG

LLM拓展为MLLM带来了多模态RAG的挑战

  • 检索哪些模态
  • 数据类型的有效融合
  • 跨模态相关性

特定模态的编码器将不同的模态映射到共享语义空间,实现跨模态对齐


现有数据集和基准

数据集

  • 图文任务(字幕、检索):MS-COCO, Flickr30K, LAION-400M

  • 利用外部知识的视觉问答: OK-VQA

  • 多模态推理:MultimodalQA

  • 视频文本任务:ActivityNet,YouCook2

  • 医学:MIMIC-CXR

许多数据集都是单模态的,随后与其他模态的互补数据集集成。


Benchmark

M2RAGM^2⁢R⁢A⁢G: 我们执行以下步骤来处理图像,以确保它们具有高质量并且与用户查询相关: (1)缓存和转换:使用 URL 下载所有图像,并将其转换为广泛接受的格式,例如 JPG、PNG、GIF 或 WEBP。无法成功下载或转换的图像将被丢弃; (2)过滤:小于某个阈值或与查询文本的基于 CLIP 相似度得分较低的图像将被删除。此类图像通常包含非代表性的视觉内容,例如图标、横幅等。 (3)重复数据删除:使用 PHash Zauner 算法删除重复或高度相似的图像。

指标设计:主要靠prompt gpt-4o做评估

  • 文本模态指标:流畅性,相关性,忠实度,上下文准确率
  • 多模态指标:图像连贯性(图像和周围文本逻辑的连贯性,图像有用性, 图像引用(验证图像和文本引用的适当性),图像召回率(高度相关图像的召回比例)
  • 取所有指标的平均值用于计算总分

两种联合建模策略

  • single-stage:直接生成多模态输出
  • multi-stage: 文本生成 - 图像插入 - 文本重润色 三个阶段


Refer to caption


视觉为中心的评估

MRAG-Bench, VQAv2, VisDoMBench, Dyn-VQA, ScienceQA

img


知识密集型评估

TriviaQA, RAG Check, Natural Questions


image-20250528162131659


image-20250528162212109


创新和方法

检索策略

高效和精度

现代MRAG将不同输入模态编码到统一的embedding空间实现直接跨模态检索

方法上,主要为Maximum inner product search (MIPS) 变体:近似MIPS,分布式MIPS,KNN变体,近似KNN,ScaNN


创新主要在效率提升和精度降低:

  • 混合搜索
  • 自适应量化
  • learned index: 神经网络驱动的索引建立,主要是数据库那边的工作

以模态为中心的检索

文本中心

  • BM25
  • bge-m3
  • ColBERT
  • RAFT(混合干扰和ground truth文档微调模型增强抗干扰能力)
  • ...

视觉中心

  • 直接用图像表示进行知识提取
  • 基于参考图像的检索,如EchoSight和ImgRet
    • EchoSight 引入了多模态重排
    • Teaser

具体来说,对于一个图文问题query, 先用image视觉相似度找到对应的wiki条目,再将wiki的section与图+文的完整query(经过Q-Former之后)进行文本rerank,最后综合视觉分数和文本rerank分数,选取topk后输入LLM。专注于问题和知识库都是图+文的情况,也只是finding, 感觉确实创新度不够 Overall Structure h:500


  • 组合多张图像特征形成综合查询表示
  • 图文映射:Pic2word 如下图,将视觉映射到文本描述

img


视频中心

  • iRAG,增量检索
  • MV-Adapter
  • Video RAG
  • RTime: 时间因果关系
  • OmAgent:分治处理复杂视频理解
  • DRVideo:基于文档检索处理长视频理解
  • ...

文档检索和布局理解

ColPali, ColQwen2: 端到端文档图像检索,动态分辨率处理,整体多页推理,绕过OCR技术,1.9k star

它的想法是这样的

  • OCR的多个组件和分块带来误差传播,且预处理流程耗时也长,能不能直接端到端一次使用文档截图解决
  • 但是如果将整页的文档编码成一个向量,肯定精度不够
  • 多向量方案最经典的ColBERT, 并且在这样一个视觉的情况下,视觉patch做多向量比文本token还合理

  • 贡献
    • benchmark ViDoRe
    • 将ColBERT和视觉语言模型结合,利用多向量不仅启发了文搜文,文搜图,还启发了“给一个文档,查找相似的文档”这样的任务
    • 提供了一个良好的视觉文本融合的范式(例如,解决了CLIP这样的模型缺乏文本细粒度的问题),允许最先进的VLM如Qwen-VL-2B,以相同的训练策略微调后作为嵌入器,+5.3 nDCG@5

Refer to caption


可不可以将这个范式沿用到引用溯源?

已经有一些了,ColPali自己就做了每个词条最显著的图像块

Refer to caption h:500

一些布局理解的新框架:ViTLP, DocLLM, CREAM, mPLUG-DocOwl


To our knowledge, no benchmark evaluates document retrieval systems in practical settings; in an end-to-end manner, across several document types and topics, and by evaluating the use of both textual and visual document features.

https://huggingface.co/blog/fsommers/document-similarity-colpali 基于 OCR 的文本提取,以及随后的布局和边界框分析,仍然是重要文档 AI 模型(例如 LayoutLM)的核心。例如, LayoutLMv3 对文档文本进行编码,包括文本标记序列的顺序、标记或线段的 OCR 边界框坐标以及文档本身。这在关键的文档 AI 任务中取得了最佳成果,但前提是第一步——OCR 文本提取——能够顺利完成。

但通常情况并非如此。

根据我最近的经验,OCR 瓶颈导致现实世界生产文档档案中的命名实体识别 (NER) 任务的性能下降近 50%。


Architecture h:600


为下游任务提供了一系列微调版本

  • Image Caption 加字幕
  • VQA
  • Detection (Detect [entity])
  • 图像实体分割
  • 文档理解

重排序和选择

多用多步骤检索,整合监督和非监督策略

  • probabilistic control keywords to improve credibility
    • 对示例的关键信息进行关键词提取,为关键词赋予概率权重,使用概率进行控制信号,让模型倾向于选择高概率关键词的示例
  • RULE 利用统计方法(Bonferroni校正)校准相关上下文
    • 利用统计方法,将“5%概率存在错误上下文”这样的朴素要求通过统计运算转换成单个上下文相关度的硬阈值
  • 视频检索中基于聚类的关键帧选择来提高多样性

相关性评估

  • SSIM (Structural Similarity Index Measure) 最早用于图像领域,衡量两幅图像间的结构、亮度、对比度相似度。现在常用于多模态信息检索,例如图片和文本联合时的相似性计算。
    • 比起传统的均方差等简单像素差,更符合人类对视觉感知的一致性判断,综合考虑亮度对比度等
  • NCC (Normalized Cross-Correlation) 标准化互相关,常见于信号处理,也可以衡量不同模态数据间的相关强度。
    • 衡量两个向量或数组的线性相关性
  • BERTScore 利用BERT这样的深度语义模型计算文本间的语义相似度,比传统关键词对齐更关注上下文语义一致性
  • 分层后处理:重排、相似度筛选、上下文窗口、合并、...

  • LDRE

    结合多种特征(如caption描述、上下文语义、实体识别等),通过权重自适应集成,提高不同表示方式下的检索相关性适应能力

  • BM25等传统排名的集成


过滤机制

  • 硬负样本挖掘:比起文本的硬负样本挖掘需要多处理跨模态的问题,如不同模态的bias等

    • GME
    • MM Embed
  • 共识过滤、多向量过滤

    • MuRAR
    • ColPali
  • 动态模态过滤

    • 训练retriever判断哪部分是噪声
    • RAFT, Img2Loc, MAIN-RAG

融合机制

分数融合和对齐

  • 训练交叉编码器将多模态转换为文本格式

  • 引入交错文本对,合并垂直多张few shot images(?)

  • CLIP分数融合,BLIP特征融合,嵌入到相同的空间

  • VISA 使用文档截图嵌入(DSE)模型,对齐文本查询和视觉文档表示

  • MA-LMM视频文本嵌入

  • LLM-RA 将文本和视觉嵌入连接成联合查询

  • ...

注意力机制:

注意力方法动态加权跨模态交互,支持特定任务推理

EMERGE, MORE, Alzheimer RAG,RAMM,RAGTrans, MV-Adapter, M2-RAAP


统一的框架和预测

M3DocRAG : 多页文档展平为单个嵌入张量

PDF-MVQA 融合了基于感兴趣区域 (RoI) 和基于块 (CLIP) 的视觉语言模型

DQU-CIR 图像转换为复杂查询的文本标题以及将文本叠加到图像上来统一原始数据,然后通过 MLP 学习的权重融合嵌入

SAM-RAG生成图像的标题来对齐图像-文本模态

UFineBench 利用共享粒度解码器进行超精细文本人物检索

Dense2Sparse 投影,将来自 BLIP/ALBEF Li 等人 ( 2022a ) 等模型的密集嵌入转换为稀疏词汇向量,使用层归一化和概率扩展控制来优化存储和可解释性


增强技术

Context Enrichment

查询 重构为结构化检索请求, Video-RAG,EMERGE 整合实体关系和语义描述

Img2Loc 提示中包含数据库中最相似的和最不相似的点来让模型排除预测中不可信的位置

虽然说只是prompt工作,但想法似乎挺有趣,只是这样的作法能否比简单的几层MLP强呢?

Refer to caption h:400


动态检索

  • SKURG 查询复杂度决定跳数

  • MR2AG 动态评估和过滤

  • OmniSearch 分解问题


生成技术

  • In context learning

    • 记忆数据 RAG-Driver(可解释的自动驾驶)

      • 检索引擎 接收到当前驾驶场景(如视频帧和对应的车辆控制信号)后,先在专家示范的记忆库中检索出与当前最相似的历史驾驶样本。
      • 多模态大语言模型处理 将检索到的样本与当前场景一同输入多模态大语言模型(MLLM),利用指令微调(Instruction Tuning),实现三项任务:
        • 动作解释(Driving Action Explanation):输出当前行为的自然语言解释;
        • 行为理由(Action Justification):对决策作出合理性说明;
        • 控制信号预测(Control Signal Prediction):给出下一个动作的具体数值(如速度和转角)

MY ALT TEXT h:600


  • 融合上下文Fusion-in-Context Learning (没太看懂RAVEN这篇论文和融合上下文这一个比较早期的encoder-decoder模型的机制有什么关系)

  • Reasoning

    • CoT RAGAR RAG链和RAG树,迭代方式优化事实核查
    • VisDoM CoT和证据整理
    • SAM-RAG 推理链和多阶段验证

指令调优:如mR2AG 用 mR2AG-IT的数据调优MLLM


来源归属

VISA 视觉来源归属

  • 看了看他的论文,VLM直接输出边界框(也就是,输入为文档图片,输出为答案 + Box)的,再LoRA微调......

image-20250528205321419 h:400


对齐

主要是对比学习:文档/图片/字幕...

噪声管理

RagVL 噪声注入训练,数据级别加负样本,token级别加Gauss噪声

RA-CM3 随机删除查询token做query dropout


MRAG解决的任务

  • 图像字幕
  • QA
  • 事实验证
  • 视觉叙事连贯性
  • 图文检索
  • .....

未来方向

泛化

  • 领域自适应

  • 模态偏差,过度依赖文本

  • 可解释性

  • 引用来源归属,在视觉/语音等模块更严重,难以识别出对应的小区域

  • 多模态的对抗性扰动,误导性信息


推理

多模态融入KG

如何进行实体感知检索

位置敏感性

冗余检索

具身智能

长上下文,效率,可拓展

  • 带图像的多页文档
  • 视频这种超长上下文

Paper reading - Fit and Prune Fast and Training-free Visual Token Pruning for Multi-modal Large Language Models

· 10 min read
ayanami

任务

当前MLLM依赖于大量的视觉token做出高精度的视觉推理,例如LLaVa使用576 image patches as visual tokens,这相较于纯文本带来了6.2倍的计算时长开销。此外,一些其他工作正在使用提高图像分辨率的方法来缓解MLLM的视觉缺陷,但进一步加剧了计算量

作者想要得到一种方法来在MLLM的图像token输入中,进行压缩,从而进行推理时的加速,且不能太影响下游任务精度。

同时,作者认为先前的方法依赖于大量的实验来确定超参数,他提出的方法需要具有一定的泛化能力,并且超参数确认简单 can be obtained in about 5 minutes for all VL tasks


motivation

  1. 大规模视觉token在MLLM中的存在明显的冗余,MLLMs 的多头注意力机制是单向的,而非真正“全局”的。简而言之,MLLMs 仅将信息从前一个标记传递到后一个标记,其视觉标记通常置于文本问题之前。在这种情况下,它们主要作用是为文本标记提供视觉语义,但实际上其中大部分并未被激活。

img


如图,大部分蓝色部分(不相关语义)实际上几乎不参加推理,图像到文本注意力非常集中。

image-20250527131819120


  1. 作者将确定压缩比例这一超参数的问题转换成一个统计问题。将压缩问题转换为这样的问题:给定一个采样样本集合DD, 再给定一个计算开销δ\delta ,设压缩策略为PP, 目标是找到一个压缩比够大(满足计算开销到δ\delta以下)的PP使得在DD上整体的注意力分布变化最小

方法

作者只对多头注意力层进行修剪

image-20250527155650905


得到修剪策略

对于采样样本集DD, 计算每一层的视觉token自注意力和视觉-文本交叉注意力。假设视觉token数N,文本token数M,第i层的第j个视觉token的平均注意力为 as,ci,j=m=1NAm,jia_{s,c}^{i,j}=\sum_{m=1}^{N}A_{m,j}^{i}, s和c分别代表自注意和交叉注意,A代表是在DD上取的平均

移除策略P可以建模成[t1,t2,...tk][t_1^*, t_2^*,...t_k^*] (假设模型有k层)

tit_i^*表示在第i层新移除的token数量,注意前面层移除的token也不会传递给后面层,也就是说移除的总数是单调增的

采用一个注意力相差阈值α\alpha和计算开销δ\delta两者一起控制裁剪,具体来说,δ\delta是提前给定的,α\alpha是二分查找计算出来的值


height:600 width:500


用通俗的话翻译就是:

  1. 将注意力分布的差别简化为平均每个token的自注意力/交叉注意力之和的差别,即是否删除某个token,注意力和的相对变化需要小于阈值α\alpha
  2. 由于只计算和,所以可以对自注意力、交叉注意力两个集合分别按照大小排序 —— 注意力分布变化最小的保证转化为,总是优先考虑删除注意力最小的token
  3. 给定一个阈值α\alpha, 对于每一层遍历,对于自注意力、交叉注意力分别不断尝试删除token,直到注意力变化达到阈值α\alpha, 而这一层最后的策略P,即token删除数量为自注意力删除集合TsT_s和交叉注意力集合TcT_c交集的大小
  4. 现在有了一个删除策略PP, 计算它是否满足计算开销约束(文中并没有具体说是怎么计算的,应该是根据模型的删除后token和参数量估算FLOPS,或者是某种直接测量计算量的工具,用的显卡是单张A100)

  1. 如果满足,说明删除策略PP是可行的,但说不定α\alpha太大删除太多了,需要调小α\alpha;如果不满足,说明删除策略PP不可行,说明α\alpha太小了,需要调大α\alpha。因此,二分查找α\alpha直到找到一个满足计算开销约束的α\alpha,且这个α\alpha的左右区间长度小于阈值ϵ\epsilon(后文实验是0.01),则这个α\alpha对应的删除策略PP就是最终的删除策略。

  2. 最后效果是在满足计算开销约束δ\delta的情况下,尽可能保留更多的视觉token


关于这样的算法最后带来的δα\delta - \alpha关系,作者附了这么一个曲线

image-20250527162141135


根据策略在推理时修剪

在实际推理时,作者将得到的删除策略PP应用到模型中。具体来说,对于每一层的视觉token,按照PP中给定的删除数量进行修剪。

具体删除哪些token呢?作者的方法是,

对于第i层

计算第i层剩余视觉token j的自注意力和asi,ja_s^{i,j}和交叉注意力和aci,ja_c^{i,j},然后将这两个和的乘积作为用于排序的参考,排序之后去除最小的kk个token(kk是删除数量)


实验结果

作者使用 LLaVA-655k 数据集(Liu et al. 2023b)中的 655 个样本(0.1%)来生成剪枝策略

在LLaVA, LLaVA-HR,LLaVA-NEXT三个具有不同大小的视觉token(7B模型,576,1024,2880 tokens)的模型上进行测试,十余个下游任务数据集上进行测试


image-20250527160437182


可以看到,剪枝之后,在保持准确率几乎不下降的情况下, 能够带来计算量的大幅下降

作者还做了其他几组实验

  1. 视觉冗余在不同层级的变化

    采用在不同层级上,随机删除裁剪视觉Token的方法。作者发现,深层次token的冗余度更高,裁剪深层次token几乎不影响准确度,可视化图也表明深层次的注意力几乎集中在最关键的元素中。但具体到每一层的最佳剪枝比例,层间也有比较大的不同


image-20250527161223832


image-20250527161358014


  1. 与baseline的对比

    对比了FastV和ToMe两种裁剪方法,表明了自身的SOTA性质。同时指出,在裁剪程度低的时候大家都差不多,裁剪程度高的时候才显露方法的性能差距

    image-20250527161538762


  1. 样本数量的消融实验

    作者将"LLaVA-655k 数据集(Liu et al. 2023b)中的 655 个样本(0.1%)来生成剪枝策略" 换成1%的数据,发现性能相当。作者进一步推测MLLM层间信息交换的模式可能更多地依赖于模型本身的特性,而在不同的输入样本上有较高的泛化性,FitPrune 方法可以有效地捕捉这种模式。同时下面的表还表明,这个方法有着很强的少样本泛化性,确实是模型的特性而不是样本数据集的特性,在仅有10个样本的时候也能得到非常优秀的策略

image-20250527162201012


结论

作者介绍了一种FitPrune的无训练方法,用于对 MLLMs 进行视觉标记剪枝。通过将标记剪枝问题表述为一个统计问题,FitPrune 旨在最小化注意力分布的偏差,从而实现冗余视觉token的高效剪枝,进而提高计算效率。FitPrune 能够基于少量数据生成最优的剪枝策略,避免了昂贵的手动试验。

Paper reading-Eagle Exploring The Design Space for Multi- modal LLMs with Mixture of Encoders

· 8 min read
ayanami

nvidia的论文, 主要还是实践训练MLLM上的一堆经验


任务

探究通过使用不同的视觉编码器和分辨率来提高MLLM系统性能的不同设计带来的效果


motivation

  1. 解读高分辨率的精细视觉信息是MLLM重要的课题,常用的CLIP-ViT 预训练时候的分辨率只有如224*224或者336*336,对OCR等细粒度信息不够好
  2. 近期研究发现enhanced visual perception显著减少幻觉和提高性能,许多近期MLLM用了混合视觉编码器
    • 有扩大视觉编码器的预训练量和参数的
    • 有将高分辨率编码器和CLIP融合的
    • 也有更复杂的融合和路由,根据任务选用不同编码器,"视觉MoE"的
  3. 但缺乏对此类方法设计的通用考量, 以及综合性的大benchmark

方法

  1. 不同的视觉编码器进行基准测试,寻找更高分辨率自适应的方案
  2. 不同的视觉编码器混合策略做同类比较(论文将近期的混合策略归为了CC,SA,LH等几类)
  3. 寻找多个视觉编码器的最优组合
  4. 改进pre-alignment和数据混合

增加输入分辨率的做法

  • Tiling 将输入分割为子图,CLIP-ViT单独编码
  • 直接放大输入分辨率,并对位置编码进行进行插值

Eagle做的实验:

预训练,LLaVA-1.5 + CLIP 基础模型,和LLaVA相同的 595k 图文对,冻结整个模型,只训练projection layer

SFT: 1809k 多模态对话数据

评估:11个任务,包含VQA任务, OCR/文档/图表理解,视觉中心任务,基于知识的任务


结果 - Strong CLIP

  1. 如果插值,需要unfrozen视觉编码器,否则损害性能。这个结论和以前实验不同。

  2. 输入分辨率和预训练分辨率差越大,插值越掉点

  3. 672分辨率下,插值和子图方法性能差不多,但是考虑效率的话还是插值更好

  4. 进行分辨率adaption,300M的CLIP-ViT性能接近6B的InternVL

按照下表,nvidia着重提了448*448+解锁视觉编码器的方案,300M就达到非常接近SOTA的性能了。


image-20250601233933871


Vision Encoder

选取了以下的encoder

  • 视觉语言对比学习的视觉Encoder,比如CLIP的ViT和OpenCLIP的ConxNeXt;

  • 以目标检测为中心的任务预训练的视觉Encoder,EVA-02

  • OCR上训练的Pix2Struct

  • 分割上预训练的SAM

  • 自监督训练的DINO-V2

对不同预训练的视觉encoder输出的特征图进行resize和插值,使得视觉token数量相同.


结果:

image-20250601234936395


分析:

  • 在freeze的情况下他们通常能在和自己预训练任务相近的MLLM benchmark上实现最佳性能。例如来自CLIP的ConvNeXt进行了图文对齐,因此在TextVQA、SQA任务上时所有编码器里表现的最好的。而Text Recognition任务上训练所得的Pix2Struct视觉编码器,在OCR任务上是表现的最好的。
  • 当跟随CLIP-ViT高分辨率拓展策略,unfreeze视觉编码器时,基本都能有性能提升,也有反超对应domain上训练的视觉编码器的可能性,例如CLIP-ConvNeXt微调后在OCR上性能超过了Pix2Struct。

融合策略:

Refer to caption


  • 序列维度拼接:SA sequence append
  • 通道维度拼接:CC concat channel
  • LLAVA-HR式:LH 将高分辨率特征使用adapter注入低分辨率特征中,维持序列长度、通道维度不变
  • Mini-Gemini式:MG 将高分辨率特征使用local windows cross attention注入到低分辨率的queries中。
  • Deformable Attention式:DA 将MG的local windows变成了Deformable Attention

结果:

image-20250601235208565

  • 融合策略越复杂,性能的提升似乎越差,简单的SA/CC稳定涨点

  • 由于SA需要处理边长的序列长度,所以后面用CC


Pre-Alignment

Refer to caption

考虑对其他的视觉专家进行预先的文本模态对齐,再学会去融合不同视觉专家的特征。因此在目前的两阶段MLLM训练框架之前,添加了一个vision-language pre-alignment training阶段,首先使用next-token prediction监督每个视觉专家的特征+各自单独的projector(与LLaVA原始预训练策略不同)训练,让其与一个冻结的较小语言模型对齐。


  • 进行一个额外的预先对齐,可以比较好提升MLLM性能。
  • 预对齐后,再合并所有的视觉专家,训练projector和encoder
  • 虽然在 SFT 期间解冻视觉专家有助于通过更新视觉专家以适应语言模型来提高性能,但预对齐策略更有效地减轻了每位视觉专家的固有偏差,并稳定了训练过程,从而提高了整体性能 (unfreeze + pre-align效果加性

Fusion choice

w h:600


采用上述的3阶段训练和最好最简单的Channel concat策略,就可以进一步研究哪种视觉编码器组合最好。组合的策略是依次增加模型视觉编码器的数量,每次的选择基于上一个数量下最好的组合进行进一步添加。四到五个编码器(X4, X5)目前看来就已经比较合适了。

最佳组合是 CLIPConvNeXtSAMPix2StructEVA-02


最终和benchmark的比较

Refer to caption


高分辨率的文档任务的展示: 红色baseline失败,蓝色eagle成功

h:600


结论

  1. MLLM训练期间解锁视觉编码器matters
  2. 设计先进的融合策略并不能较简单的通道级联显露优势
  3. 更多的视觉专家MoE能带来持续增益,是增强MLLM能力的有效途径
  4. 视觉专家如果开始时候设计的任务和文本无关(没有对齐),用冻结的LLM进行预对齐(+解锁)后再整体训练能显著提升性能