返回 AI 资讯
社区讨论LinuxDo 最新

什么应当进入上下文 - 我训练了一个按任务筛选裁剪上下文的模型

嗨,你好,这里是Linze。 标题有一点 clickbait,但是没关系 其实我最近一直在做 Agent 上下文相关的思考。然后越做我越发现,很多问题在工具返回结果的那一刻就已经开始了。 比如说,Agent 想要查清一个错误为什么会产生并试图修正,它就要去调用自己有的各种工具。工具把文件和资料返回后,里面可能会包含重试逻辑、认证、日志、序列化和各种辅助函数相关的逻辑。 但真正影响答案的,可能就只有几个判断条件对不对?它读了那么多内容,实际上只有一点点需要放到上下文里。你可能在网络上查了非常多的资料,但需要的只有一…

内容摘要

作者:#朱慧月 板块:#开发调优 嗨,你好,这里是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”

资讯来源

LinuxDo

原文链接

打开原文