Skip to main content

从现代Coding Agent视角回看代码搜索与嵌入

· 24 min read
ayanami

应该如何说起代码搜索呢,先说代码搜索的几个小的子流派吧,这方面可能略微和其他领域不同

代码的检索我认为是可以分解成以下几种的:

  • 搜索引擎式
  • grep传统搜
  • 向量embedding搜
  • 码仓index

每一种都是什么意思呢,举个例子

搜索引擎式是复用传统的搜索引擎,如elasticsearch, meilisearch等;也有一些变体,比如github的代码搜索,主要是弥补传统搜索引擎在代码搜索的不足

grep如其名字,基于linux grep或者riggrep,在agent内使用最广

向量embedding搜则有多个子任务,如NL2Code, Code2Code, Code2NL等,每个还有一些细微差别,这个很有意思,待会讲;

码仓index呢,则大致有两种:一种基于传统的AST分析,希望构建整个码仓的符号树或者CKG来辅助搜,另一种则寄希望于新型的生成式大模型,希望让LLM读了仓库之后能生成”索引“,典型的做法比如deepwiki。

搜索引擎式

传统的搜索引擎其实在代码搜上有很多问题,参见

https://github.blog/engineering/architecture-optimization/the-technology-behind-githubs-new-code-search/

最严重的问题甚至不是想象的NL2Code的问题,而是传统搜索引擎的部分优化不够、部分优化又不足,总之不与代码适配

  1. 索引的开销, 恐怖的内存消耗

当我们第一次部署 Elasticsearch 时,需要几个月的时间才能索引 GitHub 上的所有代码(当时大约有 800 万个存储库)。今天,这个数字已经超过 2 亿,而且代码不是静态的:它不断变化,这对搜索引擎来说是相当具有挑战性的。

  1. 可不可以不要索引?对大规模库高并发是不可能的:

首先,让我们探讨一下解决问题的蛮力方法。我们经常收到这样的问题:“你为什么不直接使用 grep?为了回答这个问题,让我们使用 ripgrep 对这 115 TB 的内容进行一些数学计算。在具有八核 Intel CPU 的机器上,ripgrep 可以在 2.769 秒内对缓存在内存中的 13 GB 文件运行详尽的正则表达式查询 ,即大约 0.6 GB/秒/内核。 我们很快就会发现,这对于我们拥有的大量数据来说确实不起作用。代码搜索在 64 个核心、32 个机器集群上运行。即使我们设法将 115 TB 的代码放入内存中并假设我们可以完美并行化工作,我们也会在 96 秒内使 2,048 个 CPU 内核饱和,以处理单个查询!只能运行一个查询。其他人都必须排队。结果是每秒 0.01 次查询

  1. 分词器不需要了:搜索引擎依赖分词来减少构建倒排的成本和作为基础搜索单元,但就和Tokenizer的引入一样,这个开销的减少从来不是免费的,接下来的问题就是:代码如何分词?另一个问题是:停用词全部没用了!无论是!@?/#$:{}()[]还是什么别的乱七八糟的符号,都是编程语言的最爱, 这使得传统的分词系统更加雪上加霜

    1. 如果提前跑一遍AST分析呢?我们的CPU要算爆炸了:), 并且你如何统一没有LSP小众语言,不同的语法版本(py2->3)... 工程量立刻爆炸了
    2. 能不能在char level倒排?太爆炸了索引,所以github是使用3-char的倒排的 argument -> arg、rgu、gum、...
    3. 代码里面还有注释,注释还有多语言,甚至单个仓库都很常见中英两种语言的注释...看起来朴素的分词器比如jieba要全面阵亡了...
  2. 基于git的增量更新:增量更新本身可以用merkel tree,这也是cursor在用的技术,但结合git版本?你发现事情变得复杂起来了,这是纯工程的复杂性。一次git commit涉及到十几个文件的几行变化,需要触发至少十几个chunk的embedding更新?如何处理多分支呢?我的每一个存储代码块是否还得加一个tag标识它的branch,然后在搜索引擎里面支持完备地按tag过滤,省的不同branch的代码在同一个搜索引擎中返回?(然后发现branch name作为tag简直太烂了,它是一个完全动态的无限的集合)

鉴于恐怖的存储代码数量,github采用的搜索引擎相当简陋,甚至是弱化版本的完全匹配:3-grams索引

对于代码搜索,我们需要一种特殊类型的倒排索引,称为 ngram 索引,它对于查找内容的子字符串很有用。ngram 是长度为 n 的字符序列。例如,如果我们选择 n=3,则构成内容“limits”的 ngram 是 limimimitits(二元组的选择性不够,四元组占用了太多空间)

这个索引显然是非常大无法放入内存的,所以github采用了一些传统数据库里面的懒加载和流式优化技术,使得可以仅读取一个小子集完成搜索

而关于构建索引本身,github还有很多特殊设计,但这其实属于system/后端任务了,不细讲:

  • 用Git blob object ID来分片,kafka分区

  • 用path, branch, repository + 元信息(owner, visibility, etc.) 来构建增量索引key

  • commit-level的一致性

  • Github相当多的blob是相同的,使用增量编码很有吸引力, 这里用到了概率上的近似数据结构和一些分布式图(近似)算法

    To determine the optimal ingest order, we need a way to tell how similar one repository is to another (similar in terms of their content), so we invented a new probabilistic data structure to do this in the same class of data structures as MinHash and HyperLogLog. This data structure, which we call a geometric filter, allows computing set similarity and the symmetric difference between sets with logarithmic space. In this case, the sets we’re comparing are the contents of each repository as represented by (path, blob_sha) tuples. Armed with that knowledge, we can construct a graph where the vertices are repositories and edges are weighted with this similarity metric. Calculating a minimum spanning tree of this graph (with similarity as cost) and then doing a level order traversal of the tree gives us an ingest order where we can make best use of delta encoding. Really though, this graph is enormous (millions of nodes, trillions of edges), so our MST algorithm computes an approximation that only takes a few minutes to calculate and provides 90% of the delta compression benefits we’re going for.


Grep

grep属实是在Coding Agent时代焕发了第二春,由于其系统级别自带+完美匹配Bash工具和Unix文本管道的特性,在现代的LLM之中都大量训练了如何写出各种米奇妙妙grep的数据

claude code这种经过更多优化的grep会更过分一点,它会有几个细节优化:

  • 使用更现代的rg(riggrep)代替原始的grep
  • 逆向cc源码可知,它的grep有七八个参数,分别对应grep里面的不同参数比如 -A -E -C , 除了一些呈现格式(比如带不带行号和文件名)之类的差别,主要的几个参数就是在匹配行前保留多少行、匹配行后保留多少行、和上下保留多少行
    • 如果读者熟悉coding agent的工作的话,其实早在swe-agent就已经探究过这个context window开多少的问题,原始论文的实验结论是50行
❯ tldr grep
grep
Find patterns in files using regular expressions.More information: https://www.gnu.org/software/grep/manual/grep.html.

- Search for a pattern within a file:
grep "{{search_pattern}}" {{path/to/file}}

- Search for an exact string (disables regular expressions):
grep {{[-F|--fixed-strings]}} "{{exact_string}}" {{path/to/file}}

- Search for a pattern in all files recursively in a directory, showing line numbers of matches, ignoring binary files:
grep {{[-r|--recursive]}} {{[-n|--line-number]}} --binary-files {{without-match}} "{{search_pattern}}" {{path/to/directory}}

- Use extended regular expressions (supports ?, +, {}, (), and |), in case-insensitive mode:
grep {{[-E|--extended-regexp]}} {{[-i|--ignore-case]}} "{{search_pattern}}" {{path/to/file}}

- Print 3 lines of [C]ontext around, [B]efore or [A]fter each match:
grep --{{context|before-context|after-context}} 3 "{{search_pattern}}" {{path/to/file}}

- Print file name and line number for each match with color output:
grep {{[-H|--with-filename]}} {{[-n|--line-number]}} --color=always "{{search_pattern}}" {{path/to/file}}

- Search for lines matching a pattern, printing only the matched text:
grep {{[-o|--only-matching]}} "{{search_pattern}}" {{path/to/file}}

- Search stdin for lines that do not match a pattern:
cat {{path/to/file}} | grep {{[-v|--invert-match]}} "{{search_pattern}}"

另一个有趣的事情是,现在的coding agent不约而同地使用了grep而不是rag作为其系统原生的工具,我觉得理由也是非常清晰的:

  1. grep的输出是标准可预测的,而rag的输出依赖于 {基础模型, 分块方法,召回topk,重排模型} 等多个配置参数,一个标准的输出带来的好处是 可强化学习, 如果对一个 code llm + rag 的系统做RL,最后的搜索策略一定会是拟合到和rag的embedding模型和具体策略相匹配,丧失了可迁移性
  2. 除此之外的好处也有很多,比如RL环境不需要embedding的额外开销(存储和计算上甚至编码成本上),整体轨迹可解释,精确匹配效果好,速度快...

rag的index开销其实相当大,学界不在乎这个,为了提升精度每个token一个embedding的方法也有,但一个embedding是一个1024维的向量,光存储开销就是4KB,对于百万行级别的代码仓库,其chunk可能在数万,达到了GB级别的存储开销

而工业项目有百万码仓,在TB级别的存储上进行高效地索引和查询着实压力很大,可以参考 美团和milvus/lancedb的相关文章 ,索引优化也有相当多的新实践,但这是做DB的人考虑的(雾

但Grep就是万灵药吗?并非如此

Grep提供了一个切面,能够让模型Agentic Search,根据搜索到的局部反馈调整搜索方法,从部分开始探索整个代码仓库——大部分需求的完成不需要对全仓的理解

——吗?

一个Grep的bad case是高阶语义的需求

  • 哪里导致了这个bug?
  • 某个模块的核心逻辑是什么?
  • 整体的代码结构?
  • ...

模型要么老实cat,要么就只能在log中见到它尝试“猜”你的变量名字,比如你问“...的实现”就会开始Grep impl,如果你把所有变量换成abcde,它立刻就GG了

比起失败的搜索浪费的上下文更糟地是浪费的交互轮数 —— 长达几百轮的agent轨迹是相当稀少的训练数据,如果再配合没那么好的历史压缩方法,或是没有精心设计的防止模型死循环的额外环境反馈,连续失败的grep会让agent的性能迅速地劣化

基于这方面的需求,在推理阶段Grep还是得配合别的工具,比如deepwiki,比如CKG(代码知识图谱,例如每个函数的caller和callee),

比如Code RAG

这方面也有一些新的探索,例如 https://cognition.ai/blog/swe-grep 的RL并行工具调用(关键不在速度,关键在减少交互轮数!),比如在工程侧融合Grep和RAG如https://github.com/daimh/mgrephttps://github.com/zilliztech/claude-context ,以及我们将要发的一篇文章(自吹自擂一下,关注主包后续的工作谢谢喵)


code embedding

主包主包,code embedding和文本的embedding有什么区别呢?为什么要强调code?

非常好问题,爱来自AI4SE。我认为code其实和图像比较像,某种意义上算是一种特殊的模态,不完全是文本——code某种意义上是“反语言常理”的,例如大部分语言的上下文有限,一句话很难和1000个字之前的某个东西形成强烈的联系,而这种长程交互在code之中非常常见——甚至有跨文件、跨模块、跨仓库的交互

而另一个很有趣的事情是,当我们在讲“某段代码的语义”的时候,这件事本身是模糊的,文本没有那么强的二义性,太阳就是太阳,月亮就是月亮,但一个递归斐波那契函数的语义到底是 “递归“ 还是 ”斐波那契“?这其实折射出了代码的某种特殊性,它同时具备字面义 ”斐波那契“ 和运行义 ”递归“(甚至”低效“、”算法“、“python”),而在一个代码仓库之中,代码还具备了上下游的属性:谁是我的caller,谁是我的callee?

这件事情为什么重要呢,因为传统的embedding向量相似度产出的是一个标量,它只能衡量一个维度的相似性!

  • 当你在说“查找与function A相关的代码”时,你想要的到底是什么?
    • function A的字面义相关的代码?
    • function A的运行效果相关的代码?
    • function A的caller/callee?
    • ...

然后你就发现从这个角度上来说,Code2Code的向量搜是很诡异的一件事情,至少传统的cos相似度无法干这件事——

  • 更悲伤(从学术研究的角度上来说或许是兴奋)的是,在现实需求中,我们真的不在意找到和一个function的字面意相关的function...假设你想要补全一段代码,你可能更需要关注谁会是它的caller,假设你需要优化一段代码,你可能搜索的方法是某种低效的pattern...而这些embedding相似度全部做不到
    • 据我所知,企业对这个接近摆烂了,只有MSRA有一个group还在研究,我之前溯源到的比较早的上下游建模技术是Order Embedding,感兴趣的或许可以试试做

直接结果是:我们只有NL2Code了

并且这个NL也只能关注一个方面...

什么样的NL才是真实会问会写的NL呢?Coding Agent的轨迹数据

没有轨迹数据怎么办?从大规模代码中挖掘注释作为NL

一个人写注释的方法和提问的方法不一样,这个语义空间的unmatch如何处理?各家自显神通

  • 例如2025年5月快手的OASIS: Order-Augmented Strategy for Improved Code Search,认为现有的code embedding往往关注的是代码的字面相似性,即只把代码认为是一种特殊的“语言”,而忽略了代码的非文字意义上的相似性

    • 对代码片段(结合其他静态分析信息),用LLM产生其作用的描述文本
    • 计算这个描述文本和其他代码片段的相似性,以此来挖掘难负样本
    • 因为这个文本描述的是相对High Level的函数作用,能够一定程度上避免变量名字等带来的影响,专注于实际作用
  • 24年12月的Nomic AI的cornstack

    • 强调<文档,代码>对的相关性重要性,并采用双重过滤:如果文档与代码间相似度低,或者并不在topK,只要有一个满足就筛掉
    • 动态硬负例挖掘策略:对于批内负样本挖掘,采用softmax概率采样,但是在训练过程中,逐步改变softmax的温度,前期温度高提高多样性,后期温度低,注重难负样本的区分
  • 25年5月的BAAI Towards A Generalist Code Embedding Model Based On Massive Data Synthesis

    • 强调退火训练,第一阶段纯文本,第二阶段全数据训练text-code能力,第三阶段纯代码

码仓Index - DeepWiki/CKG

这个说起来就比较简单了,deepwiki重要的始终是LLM的能力,而CKG则是静态分析的质量,开源的tree-sitter固然可用,企业也有一些统一各种语言的私有AST,静态分析已经日趋成熟,困难的是如何将这个信息给到LLM

很早在Google的博客中就有论述: 现在的vibe coding就像是把一个几千行的代码粘贴到记事本里面,然后让程序员来修改bug —— LLM看到的就是 “记事本”,而不是程序员的带有各种Lint和跳转的IDE界面

AST分析树如此庞大,除了摆烂式地提供一个获取caller/callee的mcp工具给agent之外,还可以做些什么?

先前在web领域有一个llms.txt的旨在LLM-friendly的格式,代码领域却暂时缺乏哪怕是新兴的统一处理格式

AI-friendly IDE可能对于Coding Agent的能力提升相当重要,这也是moonbit社区他们宣传的,不过我没有实际上手用过,也不是学PL的,就不瞎讲了,感兴趣的可以看张宏波的演讲

【AI时代下的基础软件 | 张宏波 刘子悦 蚂蚁&MoonBit Meetup杭州站回放】

https://www.bilibili.com/video/BV1wL8DzgEXZ/

除此之外,可能我们原先认为不能搜索或者没必要搜索的部分,现在也正在发挥着额外的作用

  • python的package,众所周知(可能并非),pip包只是一个特殊的压缩包,可以直接看到文本格式的原始代码,现代的claude-sonnet-4等模型在环境出错的时候会主动读/搜 pyproject.toml, .venv等特殊文件,遇到import的不了解api还会尝试进入package观看源码
  • 而某些binary风格或是一串神秘哈希引入的包可能对于LLM并不是很友好...

除此之外也有很多新兴的想法,例如注释本身可不可以作为一个天生的码仓Index? ...

CLI版本的Coding Agent好处是可以在各处方便的引用,尤其利于大规模并发采集数据

IDE版本的Coding Agent则会更加地“懂人”,原因是AI IDE在背后做了一大堆不仅仅是diff等格式渲染的工作,用户环境信息,用户系统信息,... 这些都被从后台塞入了Coding Agent的system prompt,使得你在Linux上运行Copilot的时候,模型不会让你执行 brew install命令

但文本本身依然有着局限,或许在某个未来,我们能看到真正的code native架构,不在绞尽脑汁地想把编译器的报错,AST的分析等等原本结构化的东西转成markdown再塞入永远不够的agent上下文...

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 能够基于少量数据生成最优的剪枝策略,避免了昂贵的手动试验。