突破LLM的token限制:多块上下文保留的实用系统(含code)

229 阅读9分钟

大语言模型(LLMs)取得了令人瞩目的进展,已广泛应用于文本生成、翻译、问答等诸多场景。然而,LLMs存在的一些局限性,如有限的上下文窗口(令牌限制)和缺乏长期记忆,限制了其在处理复杂任务时的表现。本文将深入探讨一种实用的解决方案,旨在克服这些限制,提升LLMs的性能。

一、大语言模型概述

LLMs是基于Transformer架构构建的深度学习模型,通过在海量文本数据集上进行训练来学习语言模式和知识。Transformer模型的核心机制是自注意力机制,它能够让模型在无需人工监督的情况下,自动学习输入文本中各部分之间的相关性。在处理文本时,LLMs会将文本拆分为词元(token),这些词元可以是子词或字符,随后将其输入到Transformer模型中进行处理,最终输出每个词元的嵌入表示,用于各种自然语言处理任务。

在实际应用中,GPT-4以其广泛的通用性、出色的指令遵循能力和强大的代码生成能力而闻名;Claude注重安全性和对话应用场景;Gemini旨在实现推理和多模态应用;LLaMA作为开源权重的大语言模型,在研究和微调方面应用广泛。这些模型虽然功能强大,但仍面临一些挑战。

图片

二、大语言模型的局限性

(一)长期记忆和个性化问题

大多数LLMs在默认情况下是无状态的,它们不会自动记住过去的对话内容。除非专门为其设计记忆机制,否则每个输入提示(prompt)都会被独立处理。这意味着在连续对话场景中,模型无法利用之前的交互信息,导致对话缺乏连贯性和上下文感知能力。例如,在多轮问答中,用户询问了一系列相关问题,但模型无法结合之前的回答进行更准确、更连贯的回应。

(二)知识局限性

基于静态数据集训练的LLMs,无法获取当前事件或实时数据。如果没有与网络搜索工具或API集成,模型的知识将局限于训练数据的时间范围,对于新出现的信息无法知晓。比如,当询问关于最新科技成果或时事新闻时,模型可能会给出过时的答案。

(三)上下文窗口限制(提示大小约束)

每个LLM都有一个固定的上下文窗口,即一次能够处理的最大词元数量。虽然一些先进的模型能够支持多达100万个词元,但在实际应用中,这个限制仍然会对处理大型代码库或长篇文档造成阻碍。此外,HTTP请求的大小限制也会进一步约束发送给模型的有效负载,导致无法一次性将全部输入内容发送给模型进行处理。

三、解决令牌限制和长期记忆问题的方法

(一)核心思路

为了解决缺乏长期记忆和有限提示窗口这两个关键问题,一种有效的方法是将提示词元进行结构化处理,分为用户输入(用户给出的任务)和上下文窗口(相关的支持数据片段)。具体而言,就是将分配给用户输入的总词元划分为分块提示词元和上下文词元。由于模型的词元限制,需要将大型输入(如完整的代码库或长篇文档)拆分成较小的块,然后按顺序处理这些块,并将每个块的响应摘要作为下一个块的上下文。通过这种方式,既可以模拟长期记忆,又能够突破提示窗口的限制。

(二)具体实现步骤

  1. 拆分用户提示:首先,将用户的输入文本按单词进行拆分。然后,依次遍历每个单词,将其添加到当前块中。当当前块的大小达到为单个提示设定的词元限制(由开发者自行定义)时,将该块确定为可执行的块,并将其添加到块列表中。接着,清空当前块,继续添加下一个单词,重复上述过程,直到整个提示被拆分成合适大小的块。例如,使用Python代码实现如下:

    def split_text_into_chunks(self, text, max_chunk_tokens=2000):
     words = text.split()
     chunks = []
     current_chunk = []
    
     for word in words:
         current_chunk.append(word)
         current_text = " ".join(current_chunk)
         if self.num_tokens(current_text) > max_chunk_tokens:
             current_chunk.pop()
             chunks.append(" ".join(current_chunk))
             current_chunk = [word]
    
     if current_chunk:
         chunks.append(" ".join(current_chunk))
    
     return chunks
    
  2. 处理大型提示:在拆分用户提示后,初始化一个空的overall_summarized_context,用于保存所有先前响应的单一摘要。对于每个分块,将当前的overall_summarized_context附加到该分块上,并发送给LLM以生成响应。然后,使用LLM对响应进行摘要(如果需要),并更新overall_summarized_context,使其包含最新响应的上下文。重复这个过程,直到处理完所有分块。最后,将所有的响应合并成一个最终的统一响应,并返回给用户。以下是Python代码示例:

    def process_large_prompt(self, user_prompt):
     chunks = self.split_text_into_chunks(user_prompt)
     running_context = []
     overall_summarized_context = ""
     results = []
     for i, chunk in enumerate(chunks):
         prompt = f"""
         You are reading a large document split into chunks.
         Here is chunk {i+1}/{len(chunks)}:
         {chunk}
         Previous context:
         {overall_summarized_context}
         Please analyze this chunk in light of the previous context.
         """
         messages = [
             {"role": "system", "content": "You are a smart assistant that reads the provided software code, and provide the detailed explanation of what business purpose the code is providing and how different components are connected, and interacting with each other."},
             {"role": "user", "content": prompt}
         ]
         response = self.chat(messages, temperature=0.3, max_tokens=1000)
         results.append(response.strip())
         context_messages = [
             {"role": "system", "content": "You are a smart assistant that summarizes the given content below within 1000 words so that it can be used as a context for remaining analysis."},
             {"role": "user", "content": response.strip()}
         ]
         overall_summarized_context = self.chat(context_messages, temperature=0.3, max_tokens=1000)
         running_context.append(overall_summarized_context)
     return {
         "final_summary": overall_summarized_context,
         "individual_context_summaries": running_context,
         "individual_results": results,
     }
    

    完整代码可在GitHub上获取:github.com/akshit04/Pr… API密钥,将任何GitHub仓库作为OpenRouter LLM的提示源。

四、关键考虑因素

(一)示例提示的GitHub项目

在实际测试中,使用了github.com/akshit04/St… 这个GitHub项目作为示例提示。该项目是一个基本的StackOverflow风格的项目,允许用户注册、提问、提交答案以及对现有答案进行点赞或点踩。为了确保测试的公正性,故意从README文件中排除了项目描述,这样最终的摘要就能真正反映模型从代码块和运行上下文中构建的理解,而不是简单地从项目描述中进行总结。

(二)单个用户提示块的最大词元限制

在本示例中,为单个用户提示块设置的最大词元限制为2000。需要注意的是,这个限制仅适用于用户提示,不包括上下文词元,发送给LLM的总提示词元数为用户提示词元数加上上下文词元数。这个2000的词元限制可以在openrouter_wrapper.py文件中进行配置。由于本示例只是一个概念验证(POC),故意设置了较低的词元限制,相较于大多数LLMs通常支持的数量要低很多。这样做的目的是在小规模项目上更便于手动分析结果的质量。

五、结果分析

通过对代码的试运行,得到了相应的结果,并将其中一个示例响应直接包含在仓库中(response1.md)。该文件主要分为三个部分:

(一)单个结果

这部分包含了发送给LLM的每个请求的原始输出,包括对单个提示块的部分响应,以及在每个步骤中使用的整体总结上下文。通过这些原始输出,可以清晰地看到模型对每个分块的具体处理结果。

(二)单个上下文摘要

在每次LLM响应后,都会生成一个摘要以捕获关键点。这些摘要随着时间的推移不断积累上下文信息,因为每个响应都是在访问运行上下文的情况下生成的,它们有效地将从第一个提示中获得的知识传递到当前提示。这种上下文的积累使得模型能够更好地理解整个输入的连贯性和相关性。

(三)最终摘要

这是通过组合单个上下文摘要而创建的综合摘要,等同于上一部分中提到的最后一个上下文摘要,代表了模型对所有分块处理后的最终理解。以测试的项目代码为例,最终摘要准确地总结出代码是一个使用Ruby on Rails构建的Web应用程序结构,涵盖了用户管理、问答发布和用户关系等功能模块,以及各个模块的具体实现细节和它们之间的关联。尽管是通过分块处理的方式逐步构建的,但最终摘要有效地捕捉了整个项目的上下文信息。

六、局限性与未来展望

(一)当前局限性

虽然这种多块上下文保留的方法在一定程度上克服了LLMs的令牌限制和长期记忆问题,但也存在一些局限性。由于将初始用户输入拆分成多个块,并对每个块和运行上下文多次调用LLM,多个请求会导致总处理时间增加。此外,当前代码假设输入提示是来自GitHub的代码库,如果要支持其他输入类型,如语音、图像或纯文本,需要对代码进行修改。不过,这只是实现层面的局限性,而非方法本身的问题。

(二)未来展望

在未来的研究和开发中,可以进一步优化算法,减少请求次数或提高每次请求的效率,以降低处理时间。例如,可以探索更智能的分块策略,根据文本的语义结构进行分块,而不仅仅是基于词元数量;或者开发更高效的上下文摘要算法,减少不必要的信息传递。同时,针对不同输入类型的支持扩展也将是一个重要的研究方向,通过开发通用的预处理模块,将各种类型的输入转换为适合LLMs处理的格式,从而扩大该方法的应用范围。此外,结合其他技术,如知识图谱、外部知识库等,与多块上下文保留方法相结合,有望进一步提升LLMs的性能和智能水平,使其能够更好地应对复杂多样的自然语言处理任务。