• 朱慧月

    嗨,你好,这里是Linze。

    标题有一点 clickbait,但是没关系

    其实我最近一直在做 Agent 上下文相关的思考。然后越做我越发现,很多问题在工具返回结果的那一刻就已经开始了。

    比如说,Agent 想要查清一个错误为什么会产生并试图修正,它就要去调用自己有的各种工具。工具把文件和资料返回后,里面可能会包含重试逻辑、认证、日志、序列化和各种辅助函数相关的逻辑。

    但真正影响答案的,可能就只有几个判断条件对不对?它读了那么多内容,实际上只有一点点需要放到上下文里。你可能在网络上查了非常多的资料,但需要的只有一点点。你没办法去约束 MCP 工具自己的规范,它们丢进上下文的内容确实会比较多,相应地也会比较复杂。

    其实在这种情况下,你只需要让工具去调出它需要的内容。这也是我最近在探索的方向,就是在它调用工具的过程中,去增加一次有明确目的的筛选

    给模型一个当前的问题,再给它一份原始的工具调用,然后它只需要在工具调用里面去高光它需要的内容,其他东西我们全部都可以省略掉。

    那这样的话,其实你就会发现我们可以省非常非常多的上下文(针对这些无关的内容),而且可以以一个比较好的方式去提高模型的注意力。

    先看一个简化的例子。```
    def can_retry(status, replayable):
    if not replayable:
    return False
    return status == 429 or status >= 500
    ```如果问题是"遇到 500 时会不会重试",你去让 Agent 用 grep 搜 500,很容易找到最后一行。但你会发现它前面其实还有一个条件:请求必须能够重发,程序才会符合最终的判断。

    在传统的检索过程中,通常会出现两种情况:

    - 先漏搜,之后再补搜,占用一个多余的回合。

    - 一次性把太多东西塞进上下文,导致模型没办法很好地注意到关键内容。

    如果把整个文件都交给模型,或者让模型用自己原有训练的方式进行丰富,确实可以避免这次遗漏,但依然会让上下文包含大量和当前问题无关的实现。

    每次都采用这种方法的话,无论是 Human 还是 Agent,就不得不去越来越多的材料里重新筛选有用和无用的部分。这样对模型来说目标并不明确,就相当于以前 prompt 没写明确导致 LLMs性能下降一样

    其实我们的 Agent 读文件,只是为了了解重试条件。那我们就可以设计一个环节,让它只围绕重试条件去复制/过滤返回的内容。

    比如:

    - 它下一步想了解退避时间,那就只需要单独去看计算延迟的部分

    - 它要开始 explore 重复执行的风险,那就只需要把“请求是否可以重放”、“调用方能否设置幂等”等状态信息找回来,对吧?

    其实上下文在大部分情况下,因为你每次工具调用都是有明确目的的,那么最终的结果就是:尽可能让每一个回合都只获取自己需要的内容。

    所以你只需要保证进入上下文的内容根据它的条件进行变化,整体表现其实就会相对来说更好

    OK,那我们现在已经有了我们的目的,对吧?

    接下来我们的问题就只是:怎么样让模型去学会做这个筛选?也就是说,我们选什么模型去做这个筛选?

    其实你会发现,我刚讲的这个东西非常简单,因为它只是把相关的内容留下来。这听起来可能比较容易,但实际开始训练的时候,你还是会有一些模糊,对吧?

    我们还是回到刚刚的重试逻辑。

    比如我们刚刚的那段例子,最后一行直接出现了 500,它当然是相关的对吧?但是前面的 replayable 没有出现这个词,它只是决定了后面的代码有没有机会执行。

    我们其实希望模型能够理解:每一行虽然没有直接回答问题,但依然可能是答案成立的前提。如果把它删除,剩下的内容就可能会让人产生误解。

    在这种情况下,单纯去做关键词匹配肯定是不太够的,对吧?我们需要让模型结合原文和问题,去判断哪些内容共同构成了答案,这也是highlight 想达成的一个目的

    那么我们现在可以开始了

    首先我们要先有 dataset。因为要训练这样的模型,肯定得先有数据,再去考虑怎么训练。

    这个数据都需要包含什么:

    - 一个具体问题

    - 一份原始内容

    - 这段内容里需要保留的部分

    虽然看着很简单,但数据从哪里来?有没有纯粹的 dataset 可以直接利用?我现在去找的话其实没有,公开的倒是有一个,但质量不太好,所以不推荐给大家。

    这里面最容易出问题的反而就是那个"具体的问题"。举个例子:比如有个工具调用是读取某文件的 20 到 80 行,它只会说明你去拿了哪一类材料,却没说你想从中知道什么。如果直接拿工具调用的方法去当训练目标,标注模型其实什么也学不到,反而会觉得这几十行都是用户要求读取的,于是全部保留;这样训练出来的模型就什么都不删,毫无意义。

    所以我们最终把标注拆成了两步:

    - 让 LLM 通过任务意图和资料去生成一个具体的信息需求。比如把"读取这个文件"转化成"这个请求在什么条件下会被重试"。

    - 让标注模型去看当前的问题和材料,筛选出相关、能保留的行。

    我们选的 teacher 模型是 Kimi(主要是 NVIDIA 提供了免费额度,可以直接拿来练;当然我找的是中转站,因为可以多并发调用,这个大家千万不要学)。

    我们大概标注了有 10 万对数据。这不算多,但相对来说,对于实验意图足够了。

    在此期间,我们最需要注意的是:不要为了删而删。

    我肯定不希望给标注模型一个固定的硬性要求,比如一定要删 80% 或 90%,这是不合时宜的。在大部分任务中,有些问题确实只需要几行,但有些问题需要把一整个逻辑全部看完。如果你只是为了凑压缩率(比如压到 90%、98%、99%),反而把条件、错误处理和变量定义等相关内容全部删掉,最后得到的训练标签就会有问题。

    我们的标注要求主要是留下足够理解当前问题的内容。如果不确定删掉某一行会不会丢失相关含义,就倾向于保留。在重设的例子里,这样的要求会让前面的判断条件有理由留下来。

    还有一种情况:比如原文注释说“以后需要在这里增加校验”,但代码里实际没有实现,那么生成的问题就不能写成“这段代码怎样完成校验”。如果 teacher 已经在问题里假设了一个不存在的行为,哪怕后面的标注再认真,也只是围绕错误前提在做无用功。因此,生成的问题要尽量落到材料已经说明的事实、行为和条件中。

    举个例子:我们语料里有一段 48 行的 JavaScript 测试代码,问题是“用户使用通配符时应该产生什么样的 URL,以及回调会检查返回账号上的哪些属性”。标注模型其实只保留了 4 行,下面把比较长的断言换行展示一下```
    9 case '/accounts?username=chariz*':
    26 username: 'chariz*'
    35 test.equals(account.get('username'),
    'charizard');
    36 test.equals(account.get('type'),
    Enum_AccountTypes.MEMBER);
    ```你会发现这几行就能够说明输入参数、预期的 URL,以及返回账号的用户名和类型,对吧?

    相对来说,原文里相关的错误分支和清理逻辑,就完全没有被这次标注选中。如果把问题换成"测试结果在哪里销毁",留下的内容就会发生变化,对不对?

    这就是我们希望模型学会的:面对同一段原文,在不同的问题下给出不一样的选择。对,就是这个样子

    除此之外,标注模型返回的是行范围,但我们训练时需要把它对应到 token 上。

    我的做法是:

    - 保留原文

    - 先把选中的行转换成字符范围

    - 利用 tokenize 返回的 offset,判断每个 token 是否覆盖了应该保留的内容

    问题本身和输入模板不参与这部分计算。这一段听起来可能像普通的数据处理,但实际上原文的换行、Unicode 字符以及 token 的边界都需要做对齐。不然 teacher 明明选对了一行,数据处理时却把标签贴在旁边,模型最后学到的东西就会蛮奇怪的。

    除此之外,我们还对训练集和验证集之间的重复做了处理,包括已经解析的仓库身份、分组、原文和问题相关的信息,也都留在了审查中。

    最终筛选结束后,我们得到的数据是 10 万份训练样本和 5000 份验证样本。大部分都是代码语料(主要也是 dataset 现在代码语料比较多),剩下的内容则是 agent 的工具输出。这些在公开数据集里其实蛮少的,所以我标注起来也很困难:

    • 训练集里应该只有 5000 多条 agent 工具样本

    • 验证集里只有 127 条

    所以现在最直接、最明显的,其实还只是代码相关的这些内容范畴

    说来惭愧,其实我们也只是在巨人的肩膀上更进了一步。

    我们是在已有任务感知筛选模型的基础上继续微调的。底座用的是千问 3 Reranker 0.6B,融合不同层的表示后,再通过压缩头产生 token 的保留和删除分数;训练目标里使用了 CRF 来学习保留和删除的标签序列。微调时,我们放开了最后两层 backbone、融合模块和压缩头,既希望保留已有模型读代码的能力,同时尽可能向我们的标注要求对齐。它其实要学会的,只是面对具体问题时原文中有哪些内容可以留下。

    模型内部虽然可以给 token 打分,但最终给调用方的内容绝不能按 token 碎片直接拼接。举个例子:原文是 “128 days”,如果开头 “1” 的得分较低被删掉,剩下的内容就变成了 “28 days”;输出虽然看着依然通顺,但含义已经完全变了。

    所以我们目前采用的是完整行输出:将 token 的分数聚合成行分数,选中某一行就保留整行文字,被删除的部分则用省略标记表示,以此尽量避免行内被误剪裁的问题。至于多行之间的条件依赖关系,目前还是希望模型在训练中能尽量识别出来,不过现在处理得还不算好,后续我们可能需要探索一些新的方案来解决。

    训练过程中我们其实还有一个精度问题,但是这个我们略过不谈。

    最终讲一下效果吧:我们大概训练了两个 epochs, F1达到了 79.2%;原模型的 F1 其实是 62.45%,我们大概提升了 16.39 分。

    在这个结果里,变化其实很明显:60% 左右在生产环境里根本没法用,可能会漏掉很多东西;但到了 80%,甚至如果把阈值调得更高一点,可以达到 93% 以上,这就已经完全可用了。

    事实上,我们这个数字衡量的就是一致程度,它也能说明我们确实在这个筛选任务里做得比起始模型好了不少

    相应来说,现在我也把这个东西应用到了我们现在的 Agent 上。

    在什么都不干、保证相同结果的情况下,我们大概能达到 50% 的 token 节省。它可以帮我们节省大量无关上下文的读取,并且让模型保持更高的性能。

    但严格来说,现在其实还是不能到实际应用的场景,我们可能还要再针对这个条件去做进一步的探索。

    这就是目前的情况,希望能跟大家分享一下,就这样

    以上

    只是在巨人的肩膀上做进一步的探索

    Credit

    图片

    arXiv.org

    图片

    SWE-Pruner: Self-Adaptive Context Pruning for Coding Agents

    LLM agents have demonstrated remarkable capabilities in software development, but their performance is hampered by long interaction contexts, which incur high API costs and latency. While various context compression approaches such as LongLLMLingua...

    图片

    arXiv.org

    图片

    XProvence: Zero-Cost Multilingual Context Pruning for Retrieval-Augmented...

    This paper introduces XProvence, a multilingual zero-cost context pruning model for retrieval-augmented generation (RAG), trained on 16 languages and supporting 100+ languages through effective cross-lingual transfer. Motivated by the growing use of...

    blog.ame.rip

    What Deserves to Enter the Context? · IZANAGI

    An agent reads a file to answer a question. We trained a selector to keep the evidence that question needs, then worked through what selective reading requires from the rest of the agent.

    主楼
  • Yohji1

    感谢佬的分享。文章写得很好,我虽然0代码基础,但我大概听懂了佬做的东西。

    我vibecoding的时候就有这个问题,每次启动一个新会话继续之前的项目时,哪怕已经写了HANDOFF文档和项目的结构地图,但是还是会多读一些代码,哪怕这次只是加一个小东西。

    文章写的很好,能一步步看下去 ^-^

    #1
  • 朱慧月

    啊 能听懂就好了

    主要太晚了 我待会要睡个觉

    训练大概用了三四天 一直在做这方面的工作 有点累就直接typeless了,可能还有错误 等我后续醒了更正好了

    #2

发表回复

登录后回复