diff --git a/docs/reme_v2_design.md b/docs/reme_v2_design.md new file mode 100644 index 00000000..18d14657 --- /dev/null +++ b/docs/reme_v2_design.md @@ -0,0 +1,378 @@ +# ReMeV2 - agent memory kit design + +ReMeV2 是一个面向智能体(Agent)的分层记忆系统,用于从连续的对话与交互中,逐步沉淀出稳定、可检索、可更新的长期记忆。 +系统强调从**原始轨迹 → 事件索引 → 高维抽象记忆**的分层存储,并通过**检索器(Retriever)**与**总结器(Summarizer)**实现动态更新。 + +--- + +## 设计目标 + +本设计旨在构建一个可嵌入任意智能体框架(如对话 Agent、任务 Agent 等)的记忆子系统,使其具备以下能力: + +- **支持多天、多会话的长期记忆积累**,不丢失原始细节。 +- **在推理时按需检索**与当前问题高度相关的记忆片段。 +- **在后台持续做分层总结与压缩**,提炼出抽象的“世界观 / 自我认知 / 用户画像 / 任务知识 / 工具经验”。 +- **支持记忆的增删改**,能够对过时或冲突信息进行更新,保持长期记忆的时效性与一致性。 +- **支持渐进式检索**,从高层抽象记忆逐步回溯到中层事件索引与底层原始轨迹,按需展开细节、控制上下文长度与推理成本。 +- **支持渐进式总结**,从最新对话开始按块增量总结并更新索引与主题记忆,而非每次对全量历史做重复总结。 + +--- + +## 架构概览 + +系统整体可以分为两个维度来看: + +- **静态维度**:记忆如何被分层存储(底层消息块 → 中层索引 → 顶层主题记忆)。 +- **动态维度**:记忆如何被检索与更新(上行 Summarizer、下行 Retriever)。 + +核心组件包括: + +- **底层记忆层(Raw Dialogue Store)**:多天对话轨迹切分成 `messages block`。 +- **中层索引层(Index & Summary Store)**:为每个 `messages block` 生成 `block_id: time + summary`。 +- **顶层主题记忆层(High-level Memories)**:自我认知、任务记忆、工具记忆、用户画像等高维抽象。 +- **Meta Agent(记忆中枢)**:持有高层记忆视图与索引,负责路由检索与更新请求。 +- **Sub Agents(记忆子智能体)**:面向具体主题(如某个用户、某类任务、某组工具)的记忆管理单元。 +- **Retriever(检索器)**:从高层到低层,渐进式查找相关记忆。 +- **Summarizer(总结器)**:从低层到高层,渐进式抽象和更新长期记忆。 + +--- + +## 静态记忆三层架构 + +### 垂直层级划分 + +系统将记忆分为自下而上的三个层级,抽象程度逐级升高: + +- **底层:记忆碎片(历史对话 / 轨迹)** + - 存放原始的、未经压缩的对话与交互记录。 + - **规则**:不去重、不解冲突,只做追加与切片。 + +- **中层:记忆索引(事件总结)** + - 对原始消息块做首轮总结,形成“时间 + 简要摘要”的事件索引。 + - 连接底层细节与顶层抽象。 + - **规则**:允许存在部分冗余与矛盾,主要目标是加速检索。 + +- **顶层:主题记忆(高维抽象知识)** + - 聚合并抽象出关于“自我、任务、工具、用户”等长期知识。 + - **规则**:需要去重(Deduplication)与解冲突(Resolve Conflict),追求紧凑、一致、可解释。 + +### 底层:原始对话与轨迹(Raw Dialogue) + +- 按时间顺序堆叠的对话与行为轨迹: + - `Day1 Messages → Day2 Messages → ... → DayN Messages` +- 通过时间窗口、业务逻辑、固定长度(可重叠)等策略切分为多个 `messages block`: + - `Day1 Messages Block1` + - `Day1 Messages Block2` + - `DayN Messages BlockN` +- 特性: + - 不丢失任何历史细节。 + - 支持后续回溯、审计与再训练。 + +### 中层:记忆索引与事件总结(Index & Summary) + +- 对每个 `messages block` 生成一个结构化索引: + - 格式:`block_id: time + summary` +- 示例: + - `block_id1: 2025-11-11 + 用户提到他非常喜欢吃西瓜。` + - `block_id2: 2025-11-12 + 用户表达自己很孤独。` +- 作用: + - 作为底层原始消息与顶层抽象记忆之间的“桥梁”与“目录”。 + - 为时间感知检索、语义检索提供最小粒度的事件单元。 + +### 顶层:主题记忆(High-Dimensional Abstraction) + +顶层记忆由多个“记忆气泡”(Memory Bubble)构成,每个气泡代表一类主题知识,并通过关联的 `block_id` 与中层索引相连。主要包括: + +- **自我认知记忆(Self-Cognition Memory)** + - 示例: + - “我叫 Remy” + - “我每天需要上班” + - “我是一名面向长期陪伴的智能助手” + - 位置:处于记忆拓扑中心,与其他记忆类别广泛连接。 + +- **金融任务记忆(Financial Task Memory)** + - 示例: + - “做财报分析前需要调研企业的经营数据” + - “估值时可以优先考虑 forward PE” + - 作用:支撑在特定领域(如金融)的任务推理能力。 + +- **工具记忆(Tool Memory)** + - 示例: + - “`bing_search` 很不稳定,经常失败” + - “`tongyi_search` 的结果质量较好,常需要配合 `web_extract` 使用” + - 作用:让智能体在工具选择、调用顺序和错误预期上具有“经验”。 + +- **用户画像记忆(Per-User Memory,如用户 A / 用户 B)** + - 对用户 A 的记忆示例: + - “A 很喜欢吃西瓜” + - “A 非常有礼貌” + - “A 计划下周出行” + - 与中层事件关联: + - 顶层的“喜欢吃西瓜”记忆与 `block_id1` 建立逻辑连接。 + - 每个用户独立维护一套长期画像,避免混淆。 + +--- + +## 动态流程一:ReMeV2 Retriever(检索流程) + +检索流程是一个**自上而下**的渐进式过程:从高维抽象出发,逐步定位到具体事件甚至原始对话块,为当前问答或决策提供证据。 + +```mermaid +flowchart TD + op0[op: 当前查询 / 上下文输入] + op1[op: Meta Agent 读取自我认知与主题索引] + op2[op: Meta Agent 基于 topic/intent 选择记忆子空间] + op3[op: 调度对应 Sub Agent 实例] + op4[op: 在顶层/中层记忆空间检索相关 Memories / 索引条目] + op5[op: 根据需要回溯到底层 messages block(retrieve_block)] + op6[op: 汇总得到的 messages block 列表,构造检索上下文] + op7[op: 将上下文交给上层 Agent 做回答 / 决策] + + op0 --> op1 --> op2 --> op3 --> op4 --> op5 --> op6 --> op7 +``` + +### Meta Agent:记忆入口与路由 + +- 持有: + - **常驻的自我认知记忆**(总是加载在工作内存中)。 + - 各类主题记忆的**名称 + 描述索引**,例如: + - `1. 金融任务记忆` + - `2. 工具记忆` + - `3. 用户A记忆` + - `4. 用户B记忆` +- 职责: + - 根据当前查询的 **topic / intent**,选择合适的记忆子空间。 + - 按需创建或调度具体的 `Sub_Agent` 来完成深入检索。 + +### Sub Agent:按主题检索 + +- **预定义子智能体**(如:工具记忆 Agent、某个用户画像 Agent 等): + - 支持接口: + - `retrieve_by_query(query)`:基于语义相似度的检索。 + - `time_aware_retrieve(time)`:基于时间的窗口检索(适合用户时间线类记忆)。 +- **自定义子智能体**: + - 可以实现更复杂的检索逻辑(例如结合时间衰减、多源融合等)。 + +### 渐进式检索与回溯 + +- 子智能体首先在顶层 / 中层记忆空间中找到语义相关的 **Memories / 索引条目**: + - 每条记忆通常携带 `block_id` 与简要 summary。 +- 若需要更细节的证据,系统可以进一步调用: + - `retrieve_block(block_id)`:回溯到底层 `messages block`。 + - `time_match_memories(time_range)`:根据时间窗口获取原始对话块。 +- 最终输出: + - 一组与当前问题高度相关的 `messages block List`,作为上下文,供 Agent 生成回答或做决策。 + +--- + +## 动态流程二:ReMeV2 Summarizer(总结流程) + +总结流程是一个**自下而上**的学习与更新过程:将新发生的对话转化为结构化事件,再进一步纳入长期记忆。 + +```mermaid +flowchart TD + op0[op: 收集 DayN 的原始对话与行为轨迹] + op1[op: 按时间/业务逻辑/固定长度切片为 messages block] + op2[op: 对每个 block 做摘要,生成 block_id: time + summary] + op3[op: 将 block summary 流交给 Meta Agent] + op4[op: Meta Agent 判断主题并路由/创建对应 Sub Agent] + op5[op: Sub Agent 加载当前主题的长期记忆与新 block] + op6[op: 通过 recall_similar_memory 检索历史相似记忆] + op7[op: 对比新旧信息,决定 delete/new/update 记忆操作] + op8[op: 写回更新后的高层主题记忆,维护 block_id 链接] + op9[op: 将最新记忆视图回流给 Meta Agent,用于后续检索] + + op0 --> op1 --> op2 --> op3 --> op4 --> op5 --> op6 --> op7 --> op8 --> op9 +``` + +### 输入与切片 + +- 输入源: + - `DayN Messages / Trajectory`(第 N 天的完整对话与行为轨迹)。 +- 切片策略: + - 按时间窗口切片(例如每 5 分钟一个 block)。 + - 按业务逻辑边界切片(如一个任务完成即成一个 block)。 + - 按固定长度(可带重叠)切片,兼顾局部上下文。 +- 结果: + - `messages block1, block2, ... blockN` + +### 初步总结:生成索引条目 + +- 对每个 `messages block` 进行摘要,形成: + - `block_id: time + summary` +- 示例: + - `id: 20251111_01 - 用户询问“什么是 AI”并表达对技术的好奇。` +- 这一阶段的目标: + - 为中层索引层提供结构化事件单元,尚不过度抽象。 + +### Meta Agent 路由与子智能体更新 + +- Meta Agent 接收新的 `block summary` 流: + - 根据内容判断其归属主题(例如用户相关、任务相关、工具体验相关等)。 + - 调用 `create_sub_agent(topic)` 或路由到已有的 `Sub_Agent`。 +- 子智能体内的处理步骤: + - **Step 1:加载上下文记忆** + - 加载与当前主题相关的长期记忆(如“对用户 A 的记忆”)与当前 `messages block`。 + - **Step 2:回忆(Recall)** + - 通过 `recall_similar_memory(query)` 拉取历史相似记忆,用于对比新旧信息。 + - **Step 3:更新(Update)** + - 基于新旧信息的对比,决定对顶层记忆的操作: + - `delete_memory(mem_id)`:删除已过时或被否定的记忆。 + - `new_memory(mem_content)`:新增一条新的长期记忆。 + - `update_memory(mem_id, mem_content)`:对既有记忆做内容更新或细化。 + +### 输出与闭环 + +- Summarizer 输出一批新的或更新后的 **高层记忆项(New Memories)**,并附带 `block_id` 链接。 +- 这些记忆将: + - 被 Meta Agent 纳入其主题索引中。 + - 为后续 Retriever 流程提供可检索的抽象知识。 +- 至此,形成从“原始对话 → 索引总结 → 高维记忆 → 检索使用 → 再次总结更新”的完整闭环。 + +--- + +## 总结 + +- **分层存储**:底层保留原始细节,中层作为事件索引,顶层沉淀抽象认知;中层通过 `block_id` 将两端连通。 +- **按需调用**:检索时从高层主题记忆出发,由 Meta Agent 路由到 Sub Agent,再逐级回溯到底层对话。 +- **动态增删改**:新对话先被切片与总结,再与旧记忆对比,通过新增、更新、删除操作保持长期记忆“准确 + 精简 + 有时序感”。 +- **渐进式检索与总结**:在检索方向自上而下逐级展开,在学习方向自下而上逐级抽象,实现“用多少展开多少、学多少沉淀多少”的流式记忆管理。 + + +``` python code +base_memory中增加 +# block_id: str = Field(default=..., description="block_id") +block_ids: List[str] = Field(default=..., description="block_id") + + +from typing import List, Tuple + +from flowllm.core.schema import Message +from pydantic import BaseModel, Field + +class MessageModel(Message): + """ + 被存储到传统的db中的数据结构,一行一条message + def retrieve_message(key_word=None, time_start=None, limit: int=50): + 支持关键字检索和时间检索 + content like %{key_word}% + 暂时不支持向量检索,向量只做抽取的memory的 + """ + name: str = Field(default="", description="The name of the character who actually outputs this message") + message_id: str = Field(default=..., description="message_id, uuid,每一条message不重样") + block_id: str = Field(default=..., description="block_id,message分块的id,用户链接summary") + + +from reme_ai.schema.memory import BaseMemory + +""" +基本和原来一致,增加block compress的memory + +工具记忆放到传统db中 +BlockCompress放到向量库中 +Personal + Task + Self-Cognition 放到向量库中 +""" + + + + +class BlockCompressMemory(BaseMemory): + start_time: str = Field(default=..., description="block的开始时间") + end_time: str = Field(default=..., description="block的结束时间") + +class SelfCognitionMemory(BaseMemory): + target_name: str = Field(default="自我认知") + memory_type: str = Field(default="self") + +class ToolMemory(BaseMemory): + target_name: str = Field(default="", description="工具名称") + memory_type: str = Field(default="tool") + +class PersonalMemory(BaseMemory): + target_name: str = Field(default="", description="这是记录谁的记忆?比如TOM、Jerry?") + memory_type: str = Field(default="personal") + +class TaskMemory(BaseMemory): + target_name: str = Field(default="", description="这是什么任务的记忆、比如bfcl、appworld、金融?") + memory_type: str = Field(default="task") + + + +# Handoffs allow meta agent to delegate tasks to another agent. +""" +summarizer +""" + +def compress_message_block(message_block: List[Message]) -> BlockCompressMemory: + """ + 压缩message_block信息到BlockCompressMemory + """ + +def handoff_to_summary_agent(agent_name: str) -> str: + """ + 如果agent_name是存在的,那直接使用现成的,否则可以创建新的agent(默认是不开启的) + """ + +def retrieve_memory_by_query(query: str, limit_size: int = 50, threshold = 0.5) -> List[BaseMemory]: + """ + 如果之前使用过handoff_to_summary_agent + 在自我认知 + """ + +def datetime_tool(**kwargs) + """ + 时间工具 + Returns: + + """ + +def delete_memory(memory_id: str): + ... + +# 做实验 +def update_memory(memory_id: str, content: str, time_stamp: str): + ... + + +def new_memory(content: str, time_stamp: str) -> BaseMemory: + ... + + +""" +summary reviser / Reward Model +""" + +def revise_memory(messages: List[Message], + origin_memory: List[BaseMemory], + output_memory: List[BaseMemory]) -> Tuple["", bool]: + """ + reasoning and result + 是否所有信息都加入了? + 当前结果是不是有冲突和重复。 + 如果有问题,就重新react或者重试 + """ + +""" +retriever +""" + +def retrieve_memory_by_query(query: str, limit_size: int = 50, threshold = 0.5) -> List[BaseMemory]: + """ + 和上面不一致的在于,这里可以搜索到compress的记忆 + """ + +def retrieve_message(key_word=None, time_offset=None, time_limit=None): + """ + 支持关键字检索和时间检索 + content like %{key_word}% + 暂时不支持向量检索,向量只做抽取的memory的 + """ + +def retrieve_block_id(block_id): + """ + 支持关键字检索和时间检索 + content like %{key_word}% + 暂时不支持向量检索,向量只做抽取的memory的 + """ + +``` \ No newline at end of file diff --git a/test/record_audio.py b/test/record_audio.py new file mode 100644 index 00000000..d9446813 --- /dev/null +++ b/test/record_audio.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +macOS 麦克风录音脚本 +需要安装: pip install pyaudio wave +""" + +import pyaudio +import wave +import sys +import os +from datetime import datetime + + +class AudioRecorder: + """macOS 音频录制器""" + + def __init__(self, output_dir="recordings"): + """ + 初始化录音器 + + Args: + output_dir: 录音文件保存目录 + """ + self.output_dir = output_dir + self.chunk = 1024 # 每次读取的音频块大小 + self.format = pyaudio.paInt16 # 16位深度 + self.channels = 1 # 单声道 + self.rate = 44100 # 采样率 44.1kHz + + # 创建输出目录 + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + def record(self, duration=5, filename=None): + """ + 录制音频 + + Args: + duration: 录制时长(秒) + filename: 输出文件名,如果为None则自动生成 + + Returns: + str: 保存的文件路径 + """ + # 生成文件名 + if filename is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"recording_{timestamp}.wav" + + filepath = os.path.join(self.output_dir, filename) + + # 初始化PyAudio + audio = pyaudio.PyAudio() + + try: + # 打开音频流(这会触发macOS的麦克风权限请求) + print("正在请求麦克风权限...") + stream = audio.open( + format=self.format, + channels=self.channels, + rate=self.rate, + input=True, + frames_per_buffer=self.chunk + ) + + print(f"开始录音,时长: {duration} 秒") + print("录音中...") + + frames = [] + + # 录制音频 + for i in range(0, int(self.rate / self.chunk * duration)): + data = stream.read(self.chunk) + frames.append(data) + + # 显示进度 + progress = (i + 1) / (self.rate / self.chunk * duration) * 100 + sys.stdout.write(f"\r进度: {progress:.1f}%") + sys.stdout.flush() + + print("\n录音完成!") + + # 停止并关闭流 + stream.stop_stream() + stream.close() + + # 保存为WAV文件 + print(f"正在保存到: {filepath}") + wf = wave.open(filepath, 'wb') + wf.setnchannels(self.channels) + wf.setsampwidth(audio.get_sample_size(self.format)) + wf.setframerate(self.rate) + wf.writeframes(b''.join(frames)) + wf.close() + + print(f"✓ 文件已保存: {filepath}") + return filepath + + except Exception as e: + print(f"\n错误: {e}") + print("\n提示:") + print("1. 请确保已安装 pyaudio: pip install pyaudio") + print("2. 在macOS上,首次运行会弹出权限请求对话框") + print("3. 如果权限被拒绝,请前往 系统偏好设置 > 安全性与隐私 > 隐私 > 麦克风") + return None + + finally: + audio.terminate() + + def record_interactive(self): + """交互式录音""" + print("=" * 50) + print("macOS 麦克风录音工具") + print("=" * 50) + + try: + duration = input("\n请输入录音时长(秒,默认5秒): ").strip() + duration = int(duration) if duration else 5 + + filename = input("请输入文件名(留空自动生成): ").strip() + filename = filename if filename else None + if filename and not filename.endswith('.wav'): + filename += '.wav' + + print() + self.record(duration=duration, filename=filename) + + except KeyboardInterrupt: + print("\n\n录音已取消") + except ValueError: + print("输入无效,请输入数字") + + +def main(): + """主函数""" + recorder = AudioRecorder() + + if len(sys.argv) > 1: + # 命令行模式 + try: + duration = int(sys.argv[1]) + filename = sys.argv[2] if len(sys.argv) > 2 else None + recorder.record(duration=duration, filename=filename) + except ValueError: + print("用法: python record_audio.py [时长(秒)] [文件名(可选)]") + print("示例: python record_audio.py 10 my_recording.wav") + else: + # 交互式模式 + recorder.record_interactive() + + +if __name__ == "__main__": + main()