AI Agent 是时下热门的一个方向,在 OpenAI 应用研究主管 LilianWeng 写的万字长文中[1],她提出 Agent = LLM+ 记忆 + 规划技能 + 工具使用。

 

AI agent里的长期记忆和短期记忆_Memory

图1 Overview of a LLM-powered autonomous agent system

组件二:记忆

  • 我们可以将上下文学习(context)看成是利用模型的短期记忆(也就是模型能接受输入的最大长度)来学习
  • 长期记忆为 Agent 提供了长期存储和召回信息的能力,通常利用外部向量储存和快速检索来实现。

记忆是指获取、储存、保留和后续检索信息的过程。人脑中有多种记忆类型:

  • 感觉记忆(Sensory Memory):这是记忆的最早阶段,提供在原始刺激结束后保留感官信息(视觉、听觉等)的印象的能力。感觉记忆通常只持续几秒钟。
  • 短期记忆(Short-Term Memory, STM)或工作记忆(Working Memory):它储存我们当前意识到的信息,用于进行复杂的认知任务,比如学习和推理。短期记忆容量通常为7个项目左右,持续时间为20-30秒。
  • 长期记忆(Long-Term Memory, LTM):长期记忆可以储存信息很长一段时间,从几天到几十年,其储存容量基本上是无限的。长期记忆有两个子类型:
  • 显性 / 陈述记忆(Explicit / declarative memory):指可被有意识回忆的事实和事件的记忆,包括情景记忆(经历和经验)和语义记忆(事实和概念)。
  • 隐性 / 程序记忆(Implicit / procedural memory):这种记忆是无意识的,涉及自动执行的技能和例行程序,比如骑自行车或打字。

AI agent里的长期记忆和短期记忆_数据_02

对于 Agent 来说:

  • 感觉记忆作为原始输入,可以是文本、图像或者其他模态的输入。
  • 短期记忆则用于上下文学习。它是短暂和有限的,因为它受到Transformer有限上下文窗口长度的限制。
  • 长期记忆则是 Agent 可以在查询和关注的外部向量存储,通过快速检索来访问。

 

 

在AI代理中,短期记忆(short-term memory)和长期记忆(long-term memory)是用于存储和处理信息的两种不同机制。它们的实现方式各有不同,通常结合使用以提高AI的性能和智能水平。

短期记忆(Short-term Memory)

短期记忆通常用于存储和处理当前任务或会话中的信息。它的特点是容量有限,信息保留时间较短。以下是一些实现短期记忆的技术:

  1. 缓存(Cache):在计算机科学中,缓存是一种用于存储临时数据的高效存储机制。AI代理可以使用缓存来快速访问最近使用的数据。
  2. 循环神经网络(RNN):RNNs,尤其是长短时记忆网络(LSTM)和门控循环单元(GRU),是处理序列数据的神经网络架构,能够在短期内保留信息。
  3. 上下文窗口(Context Window):在自然语言处理任务中,AI模型可以使用上下文窗口来处理和记住当前会话中的信息。

长期记忆(Long-term Memory)

长期记忆用于存储需要长时间保留的信息,通常涉及更复杂的数据结构和存储机制。以下是一些实现长期记忆的技术:

  1. 数据库(Database):使用关系型或非关系型数据库来存储和检索长期信息。这种方法适用于需要持久化存储的数据。
  2. 知识图谱(Knowledge Graphs):知识图谱是一种用于表示实体及其关系的结构化数据模型,适合存储和检索复杂的知识。
  3. 文件系统(File System):将信息存储在文件中,以便长期保存和访问。
  4. 强化学习中的经验回放(Experience Replay):在强化学习中,经验回放用于存储过去的经验,以便在训练过程中反复使用。
  5. 向量数据库(Vector Database):用于存储和检索高维向量数据,适合处理嵌入和相似性搜索。

 

记忆流与检索

记忆流(Memory Stream)记录了Agent的全部经历。它是一个内存对象列表,每个对象包含自然语言描述、创建时间戳和最近访问时间戳。记忆流的基本元素是观察(Observation),这是Agent直接感知的事件。观察可以是Agent自身执行的行为,也可以是Agent感知到其他Agent或非Agent对象执行的行为。每个Agent都有自己独立的记忆流。

检索功能根据Agent的当前情况,从记忆流中检索一部分记忆,供语言模型使用。排序打分包括三个方面:

  • 近期性(Recency):最近访问的记忆对象得到更高的分数,因此刚刚发生的事件或今天早上的事件可能会更受Agent关注。近期性使用指数衰减函数来衡量,衰减因子为0.99,衰减的基准是上次检索记忆以来的时间。
  • 重要性(Importance):根据Agent认为的重要程度,为记忆对象分配不同的分数,区分普通记忆和核心记忆。例如,平凡的事件(比如吃早餐)得到低重要性分数,而与重要的人开会这事件得到高分。重要性分数可以使用不同的实现方式,类似的解决方案就是使用了这个具体的评分模型来输出一个整数分数。
  • 相关性(Relevance)为与当前情况相关的记忆对象分配更高的分数。使用常见的向量检索引擎来实现相关性评估。

 

千问AI agent的memory代码实现==》属于长期记忆:

import json
from importlib import import_module
from typing import Dict, Iterator, List, Optional, Union

import json5

from qwen_agent import Agent
from qwen_agent.llm import BaseChatModel
from qwen_agent.llm.schema import ASSISTANT, DEFAULT_SYSTEM_MESSAGE, USER, Message
from qwen_agent.log import logger
from qwen_agent.settings import (DEFAULT_MAX_REF_TOKEN, DEFAULT_PARSER_PAGE_SIZE, DEFAULT_RAG_KEYGEN_STRATEGY,
                                 DEFAULT_RAG_SEARCHERS)
from qwen_agent.tools import BaseTool
from qwen_agent.tools.simple_doc_parser import PARSER_SUPPORTED_FILE_TYPES
from qwen_agent.utils.utils import extract_files_from_messages, extract_text_from_message, get_file_type


class Memory(Agent):
    """Memory is special agent for file management.

    By default, this memory can use retrieval tool for RAG.
    """

    def __init__(self,
                 function_list: Optional[List[Union[str, Dict, BaseTool]]] = None,
                 llm: Optional[Union[Dict, BaseChatModel]] = None,
                 system_message: Optional[str] = DEFAULT_SYSTEM_MESSAGE,
                 files: Optional[List[str]] = None,
                 rag_cfg: Optional[Dict] = None):
        """Initialization the memory.

        Args:
            rag_cfg: The config for RAG. One example is:
              {
                'max_ref_token': 4000,
                'parser_page_size': 500,
                'rag_keygen_strategy': 'SplitQueryThenGenKeyword',
                'rag_searchers': ['keyword_search', 'front_page_search']
              }
              And the above is the default settings.
        """
        self.cfg = rag_cfg or {}
        self.max_ref_token: int = self.cfg.get('max_ref_token', DEFAULT_MAX_REF_TOKEN)
        self.parser_page_size: int = self.cfg.get('parser_page_size', DEFAULT_PARSER_PAGE_SIZE)
        self.rag_searchers = self.cfg.get('rag_searchers', DEFAULT_RAG_SEARCHERS)
        self.rag_keygen_strategy = self.cfg.get('rag_keygen_strategy', DEFAULT_RAG_KEYGEN_STRATEGY)

        function_list = function_list or []
        super().__init__(function_list=[{
            'name': 'retrieval',
            'max_ref_token': self.max_ref_token,
            'parser_page_size': self.parser_page_size,
            'rag_searchers': self.rag_searchers,
        }, {
            'name': 'doc_parser',
            'max_ref_token': self.max_ref_token,
            'parser_page_size': self.parser_page_size,
        }] + function_list,
                         llm=llm,
                         system_message=system_message)

        self.system_files = files or []

    def _run(self, messages: List[Message], lang: str = 'en', **kwargs) -> Iterator[List[Message]]:
        """This agent is responsible for processing the input files in the message.

         This method stores the files in the knowledge base, and retrievals the relevant parts
         based on the query and returning them.
         The currently supported file types include: .pdf, .docx, .pptx, .txt, .csv, .tsv, .xlsx, .xls and html.

         Args:
             messages: A list of messages.
             lang: Language.

        Yields:
            The message of retrieved documents.
        """
        # process files in messages
        rag_files = self.get_rag_files(messages)

        if not rag_files:
            yield [Message(role=ASSISTANT, content='', name='memory')]
        else:
            query = ''
            # Only retrieval content according to the last user query if exists
            if messages and messages[-1].role == USER:
                query = extract_text_from_message(messages[-1], add_upload_info=False)

            # Keyword generation
            if query and self.rag_keygen_strategy.lower() != 'none':
                module_name = 'qwen_agent.agents.keygen_strategies'
                module = import_module(module_name)
                cls = getattr(module, self.rag_keygen_strategy)
                keygen = cls(llm=self.llm)
                response = keygen.run([Message(USER, query)], files=rag_files)
                last = None
                for last in response:
                    continue
                if last:
                    keyword = last[-1].content.strip()
                else:
                    keyword = ''

                if keyword.startswith('```json'):
                    keyword = keyword[len('```json'):]
                if keyword.endswith('```'):
                    keyword = keyword[:-3]
                try:
                    keyword_dict = json5.loads(keyword)
                    if 'text' not in keyword_dict:
                        keyword_dict['text'] = query
                    query = json.dumps(keyword_dict, ensure_ascii=False)
                    logger.info(query)
                except Exception:
                    query = query

            content = self.function_map['retrieval'].call(
                {
                    'query': query,
                    'files': rag_files
                },
                **kwargs,
            )
            if not isinstance(content, str):
                content = json.dumps(content, ensure_ascii=False, indent=4)

            yield [Message(role=ASSISTANT, content=content, name='memory')]

    def get_rag_files(self, messages: List[Message]):
        session_files = extract_files_from_messages(messages, include_images=False)
        files = self.system_files + session_files
        rag_files = []
        for file in files:
            f_type = get_file_type(file)
            if f_type in PARSER_SUPPORTED_FILE_TYPES and file not in rag_files:
                rag_files.append(file)
        return rag_files
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.

  

 

这段代码定义了一个继承自 Agent 类的 Memory 类,专门用于文件管理和检索增强生成(RAG)任务。以下是它如何实现“记忆”的详细说明:

初始化

  1. 参数和配置:
  • Memory 类在初始化时接受多个参数,包括工具列表(function_list)、语言模型(llm)、系统消息、文件列表(files)和RAG配置字典(rag_cfg)。
  • RAG配置包括参数如 max_ref_tokenparser_page_sizerag_searchers 和 rag_keygen_strategy。这些参数有默认值,如 DEFAULT_MAX_REF_TOKEN
  1. 功能设置:
  • Memory 类通过扩展的 function_list 初始化其父类 Agent,其中包括具有特定设置的 retrieval 和 doc_parser 工具。
  1. 文件管理:
  • 它维护一个系统文件列表(system_files),可以在多个会话中使用。

记忆实现

记忆的实现主要用于管理与文件相关的操作,并与检索工具集成以支持RAG功能。

  1. 文件处理:
  • get_rag_files:该方法从消息列表中提取文件,并将其与任何系统文件结合,创建一个文件列表(rag_files),这些文件由解析器支持并准备好进行检索。
  • 它根据 PARSER_SUPPORTED_FILE_TYPES 检查文件类型,以确保只处理合适的文件。
  1. 查询处理和检索:
  • _run 方法:该方法管理与检索工具的交互。它处理输入消息并提取任何查询。
  • 关键词生成:可以使用指定的 rag_keygen_strategy 生成关键词。这涉及动态加载策略类并运行它以根据查询生成关键词。
  • 调用检索:使用组装的查询和文件调用 retrieval 函数以获取相关信息。
  • 输出处理:检索到的内容作为来自 ASSISTANT 角色的消息返回。
  1. 迭代响应生成:
  • _run 方法是一个生成器(使用 yield),这使得它能够通过在结果准备好时逐步返回结果来平稳处理可能较长或异步的操作。

总结

这个 Memory 类充当一个专门的代理,用于管理文件输入、生成查询、执行基于关键词的信息检索,并以对话格式返回这些结果。这里的记忆更多是概念上的,通过其对数据的处理和持久化来模拟一种“记忆”,而不是传统的记忆系统,如那些在会话中存储用户数据的系统。它利用工具和策略,通过处理和持久化数据来实现一种记忆功能

 

上述代码本质上是在实现基于关键词的知识检索。以下是代码如何实现这一点的详细说明:

  1. 关键词生成:
  • 代码中有一个关键词生成策略(rag_keygen_strategy),用于从用户的查询中提取关键词。这是通过动态加载一个关键词生成策略类并运行它来实现的。
  • 关键词生��的结果用于构建更有效的查询,以便在文件中检索相关信息。
  1. 文件处理和检索:
  • get_rag_files 方法负责从消息中提取文件,并确保这些文件是支持的类型。
  • 在 _run 方法中,提取的关键词和文件一起被传递给 retrieval 工具。这个工具负责在文件中搜索与关键词匹配的内容。
  1. 检索工具的调用:
  • retrieval 工具被调用时,会使用生成的关键词和文件列表来查找相关信息。
  • 检索到的内容被格式化并返回给用户,模拟了一个基于关键词的知识检索过程。

通过这些步骤,代码实现了一个简单的基于关键词的知识检索系统,能够从用户提供的文件中提取相关信息并返回给用户。

 

而在阿里的superAGI里的做法==》属于短期记忆:

agent_summary.txt

要求AI生成系统、用户和助手之间先前互动的简洁总结。总结应涵盖对话的主要点,突出讨论的关键问题、做出的决定以及分配的任何行动。这应作为过去互动的回顾,提供对话内容和结果的清晰理解。请确保总结不超过设定的字符限制。
  • 1.

==》这种思路对于缩减对话长度很有用!  

 

原始:

AI, your task is to generate a concise summary of the previous interactions between the system, user, and assistant.
The interactions are as follows:

{past_messages}

This summary should encapsulate the main points of the conversation, highlighting the key issues discussed, decisions made, and any actions assigned.
It should serve as a recap of the past interaction, providing a clear understanding of the conversation's context and outcomes.
Please ensure that the summary does not exceed {char_limit} characters.

 

agent_recursive_summary.txt

AI,你需要根据系统、用户和助手之间的先前交互总结以及原始总结中未包括的额外对话来提供信息。
如果先前的总结为空,你的任务是仅基于新的互动创建一个总结。
 
先前总结:{previous_ltm_summary}
 
{past_messages}
 
如果先前的总结不为空,你的最终总结应将新的互动整合到现有总结中,以创建一个全面的所有互动的回顾。
如果先前的总结为空,你的总结应概括新对话的主要点。
在两种情况下,都要突出讨论的关键问题、做出的决定以及分配的任何行动。
请确保最终总结不超过{char_limit}个字符。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

  

原始:

AI, you are provided with a previous summary of interactions between the system, user, and assistant, as well as additional conversations that were not included in the original summary.
If the previous summary is empty, your task is to create a summary based solely on the new interactions.

Previous Summary: {previous_ltm_summary}

{past_messages}

If the previous summary is not empty, your final summary should integrate the new interactions into the existing summary to create a comprehensive recap of all interactions.
If the previous summary is empty, your summary should encapsulate the main points of the new conversations.
In both cases, highlight the key issues discussed, decisions made, and any actions assigned.
Please ensure that the final summary does not exceed {char_limit} characters.