让长时间的代理回合保持在上下文窗口之内
模型一次能读取的内容有限,这就是它的上下文窗口。Mythex 的代理会进行长时间的工作:读取大量文件、运行命令,并把有界的任务交给子代理。我们发现长时间工作会以三种方式出问题:子代理不断增长,直到模型拒绝每一个请求;一个子代理因为以为自己的空间不够而提前停止;以及我们对俄语、中文和日语文本的 token 计数严重失准。以下是我们做出的改变。
先了解几个术语
- Token 是模型读取的文本块。限制和成本都以 token 计算,而不是以字符或单词计算。
- 上下文窗口是模型在一次请求中最多能读取的内容。我们还为每次请求设置了自己更低的预算,以使成本和速度保持合理。
- 工具结果是代理从工具那里取回的内容:它读取的文件内容,或它运行的命令输出。在长时间的工作中,它们占了所发送内容的大部分。
- 子代理是主代理为一个独立的任务启动的帮手,例如「对照笔记检查这些章节」。它只获得一条指令,然后自己通过许多次工具调用来完成工作。
问题一:子代理不断增长,直到模型拒绝它们
每一步,代理都会把到目前为止的对话发送给模型。当对话变得太大时,我们的代码会裁剪它,但只在用户消息的开头进行裁剪,从而确保工具调用和它的结果从不被拆开。它保留第一条消息,添加一条说明已裁剪更早对话的备注,并保留能放下的最新完整回合。
子代理只有一条用户消息:它的指令。没有地方可以裁剪。因此,它每一步都会发送整个历史记录,并且每次都会变得更大,直到请求超过模型所能接受的大小,大约一百万个 token。在一项包含 8 个这样的子代理的测试中,8 个全部被拒绝。
修复:先清除旧的工具结果
现在,请求会通过三个步骤来适配预算,只有在前一步不够时才尝试下一步:
- 清除最旧的工具结果。 它们的内容会被替换为一段简短的说明:该结果为了让对话保持在模型的上下文范围内已被清除,如果代理需要,可以重新运行该工具。一小时前读取的文件通常是请求中最大、也最没用的东西,而且文件仍然可以再次读取。最新的结果保持完整。
- 保留完整的用户回合。 与之前相同:第一条消息、一条说明,以及能放下的最新完整回合。
- 保留完整的工具轮次。 在一个长回合内部(子代理只有这一种回合),保留第一条消息、一条说明,以及最新的完整轮次,即「调用一个工具,获得其结果」。
这只影响发送的内容。代理保存的历史记录保留了所有内容。在同一项测试中,8 个中有 0 个被拒绝,全部完成。
分批清除,让缓存继续生效
模型提供商可以复用请求开头的工作,前提是它与上一次请求完全相同,这被称为前缀缓存。如果我们每次调用都多清除一个结果,请求的开头每次都会改变,缓存就永远不会起作用。
因此,最旧的结果是每次清除四个,而不是一个。这样,被清除的部分在多次调用之间保持不变。与备注相比,如果某个结果并没有大多少,就保持不动:清除它几乎节省不了什么,却仍会改变请求的开头。保留多少完整内容由大小而不是数量决定:能放进预算四分之一的最新结果会被保留,并且至少保留一个。之前的版本固定保留六个完整结果,这对于较短的命令输出是合适的,但在读取长文件时,却让大部分预算被旧结果占据。
拒绝时现在会说明原因
被模型判定为无效的请求不会再被重试;再次发送同一请求只会得到同样的答案。如果对话对模型来说太长,用户会看到:「这段对话太长,模型无法一次读完。请为下一步开始一个新的聊天,目前为止构建的所有内容都会保留。」这是用户需要采取行动的事情,而不是服务故障,因此不会通知我们值班。
问题二:代理为了「节省空间」而停止
一个子代理被要求读取 40 个长章节,结果只读了 2 个就停了,并说它想节省空间。它大约用了 400,000 token 预算中的 100,000 个 token。
它无从知道旧的结果现在会为它被清除。因此,我们告诉了它。代理的指令现在会说明:当对话增长到超过模型所能容纳的范围时,最旧的工具结果会被清除,最新的结果保持完整。因此,不要为了节省空间而中止长时间的任务,继续进行即可,如果你需要被清除的内容,可以重新读取文件。
模型看不见的修复,从模型的角度来看并不是修复。这种机制只有在代理信任它时才有帮助。
问题三:把所有语言都当作英语来计算 token
我们在不运行分词器的情况下估算 token,即字符数除以四。这对英语来说是合理的经验法则,但对其他文字并不适用:俄语大约每个 token 2.2 个字符,中文或日语大约每个 token 1 个字符。
因此,读取俄语章节的子代理发送了一个约 640,000 token 的请求,而我们的估算却认为它低于 400,000 的预算。这种估算同样可能让请求超过模型的硬性上限。
现在估算会按文字类型来计算:普通 ASCII 为每 token 四个字符,中文、日文和韩文为每 token 一个字符,其他所有文字(西里尔字母、希腊字母、阿拉伯字母、带重音的字母、表情符号)为每 token 两个字符。它的设计目标是偏高估计。以一章俄语为例,现在估算为 43,423 个 token,实际测得约 44,600 个;之前的估算只有 25,023 个。由于一条消息一旦写入就不会再改变,它的估算只计算一次并记住,而不是每一步都重新计算。
我们从中得到的经验
- 在真正占空间的地方裁剪。 在代理工作中,那是工具结果,而不是用户消息。
- 告诉代理它的安全网。 否则它会围绕已经不再适用的限制进行规划。
- 「字符数除以四」是基于英语的假设。 任何为多语言用户计算 token 的系统,都需要按文字类型计算,或者使用真正的分词器。
- 注意缓存。 每次调用开头都会改变的请求,成本更高,运行也更慢。
如果你要在自己的应用中加入 AI 功能,我们的指南什么是大语言模型和如何使用大语言模型 API从一开始就介绍了上下文窗口和 token。关于子代理在 Mythex 中如何工作的更多信息,请参阅子代理文档。