ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于认知科学的AI Agent记忆系统设计与TypeScript实现

2026/8/14 8:13:32 拓冰建站 浏览量
基于认知科学的AI Agent记忆系统设计与TypeScript实现

1. 项目概述:为什么AI Agent需要一个记忆系统?

如果你正在开发一个AI Agent,无论是聊天机器人、自动化助手还是更复杂的决策系统,你肯定遇到过这样的场景:用户问“我昨天提到的那个项目进展如何了?”,或者Agent在处理一个多步骤任务时,完全忘记了第一步自己做了什么决定。这种“健忘症”让Agent显得非常愚蠢,用户体验大打折扣。这背后的核心问题,就是大多数基于大语言模型的Agent缺乏一个持久化、结构化的记忆系统。

这个项目,就是从认知科学的视角出发,探讨如何为AI Agent设计和实现一个真正有用的记忆系统,并用TypeScript代码将其落地。这不仅仅是简单地存储聊天记录,而是借鉴人类记忆的工作机制——比如工作记忆、情景记忆、语义记忆——来构建一个能让Agent“记住”关键信息、“回忆”相关上下文、“遗忘”无用噪音的智能架构。一个强大的记忆系统是Agent实现长期对话、个性化服务、复杂任务规划和持续学习的基础。无论你是想构建一个能进行深度对话的陪伴型Agent,还是一个能处理复杂工作流的自动化工具,理解并实现记忆系统都是绕不开的关键一步。

2. 记忆系统的认知科学基础与设计思路

2.1 人类记忆模型对AI的启示

在动手写代码之前,我们必须先想清楚要构建什么。盲目地存储所有交互历史,只会得到一个臃肿且低效的“垃圾堆”。认知科学为我们提供了清晰的蓝图。人类的记忆并非单一仓库,而是一个多层次的系统:

  • 感觉记忆:瞬时存储大量感官信息。对应到Agent,可以理解为对单次输入(用户消息、API返回、传感器数据)的原始缓存,留存时间极短,主要用于初步过滤。
  • 工作记忆:容量有限,用于处理当前任务中的信息。这是Agent的“思考白板”,存放正在被推理、组合和操作的上下文。LLM本身的上下文窗口(Context Window)在某种程度上扮演了这个角色,但其容量和持久性远远不够。
  • 长期记忆:容量近乎无限,用于存储需要长期保留的知识和经验。这正是我们要为Agent构建的核心。它又可以细分为:
    • 情景记忆:对特定事件、经历的记忆,包括时间、地点、人物、情感。例如,“用户张三在2023年10月26日下午3点抱怨过登录缓慢”。这对实现个性化对话和历史回溯至关重要。
    • 语义记忆:对概念、事实和知识的记忆,与具体经历无关。例如,“巴黎是法国的首都”,“登录流程需要验证用户名和密码”。这构成了Agent的“常识库”或“知识图谱”。
    • 程序性记忆:关于“如何做”的记忆,比如骑自行车、打字。对应到Agent,就是它学会的技能(Skills)和执行特定任务的流程(Workflows)。

注意:我们并非要完全复刻人脑,而是借鉴其分层、分类、有选择性地存储与提取的核心思想。这直接决定了我们代码中的数据结构设计。

2.2 AI Agent记忆系统的核心设计目标

基于以上认知模型,一个实用的AI Agent记忆系统应满足以下几个设计目标:

  1. 持久化与可检索性:记忆必须能跨越会话(Session)存在,并能通过高效的查询被快速召回。这要求我们将记忆存储在外部数据库(如向量数据库、关系型数据库)中,而非仅仅依赖LLM的临时上下文。
  2. 结构化与语义化:原始文本存储不利于精确检索。我们需要将记忆结构化,例如提取实体(用户、项目、产品)、动作(询问、确认、执行)、情感(正面、负面)和关键事实,并转换为语义向量(Embeddings)。这样,Agent不仅能通过关键词,更能通过“意思”来查找相关记忆。
  3. 动态更新与整合:记忆不是一次写入就永不改变。新的交互可能会修正旧记忆(“用户更正了手机号”),或与旧记忆整合形成新的认知(“用户每次周一早上都会询问周报模板”)。
  4. 选择性遗忘与记忆强化:并非所有信息都值得长期记住。系统需要机制来衰减不重要的记忆(如琐碎的寒暄),或定期清理过时数据,同时强化高频访问或标记为重要的记忆。
  5. 与推理循环集成:记忆系统不能是孤立的。它需要无缝嵌入Agent的“感知-思考-行动”循环中。在“思考”阶段,Agent应能自动查询相关记忆作为上下文;在“行动”后,重要结果应能自动存储为新的记忆。

3. 核心模块拆解与TypeScript接口设计

接下来,我们将设计目标转化为具体的TypeScript模块和接口。我们将系统分为几个核心层,每一层职责明确。

3.1 记忆表示层:定义记忆的数据结构

这是记忆系统的基石。我们首先定义Memory这个核心接口。

// 记忆的元数据,用于管理和检索 interface MemoryMetadata { id: string; // 唯一标识符,通常使用UUID timestamp: Date; // 记忆创建或发生的时间 source: string; // 记忆来源,如 “user_message”, “agent_action”, “system” agentId?: string; // 产生此记忆的Agent ID(多Agent场景) tags: string[]; // 标签,用于分类,如 [“project-alpha”, “complaint”, “preference”] importance: number; // 重要性评分,0-1,可由LLM或规则生成 lastAccessed?: Date; // 最后访问时间,用于实现基于访问频率的强化 } // 记忆的内容本体,采用联合类型支持多种记忆形式 type MemoryContent = | { type: ‘observational’; text: string } // 观察性记忆(用户说了什么,系统看到了什么) | { type: ‘factual’; subject: string; predicate: string; object: string } // 事实性记忆(三元组),便于结构化查询 | { type: ‘procedural’; skillName: string; steps: string[] } // 程序性记忆 | { type: ‘summary’; ofMemoryIds: string[]; text: string }; // 摘要性记忆,用于压缩长对话 // 核心记忆接口 interface Memory { metadata: MemoryMetadata; content: MemoryContent; embedding?: number[]; // 语义向量,由文本内容生成,用于向量检索 }

设计理由:将元数据与内容分离,便于独立索引和管理。MemoryContent的联合类型设计,允许系统灵活支持从简单文本到复杂知识图谱的不同粒度记忆。embedding字段是可选的,因为并非所有存储后端都支持向量检索。

3.2 记忆存储层:持久化与检索抽象

存储层负责记忆的增删改查。我们定义一个抽象接口MemoryStore,以便未来可以轻松切换不同的存储后端(如内存、SQLite、PostgreSQL、向量数据库Pinecone/Weaviate)。

interface MemoryStore { // 增 create(memory: Memory): Promise<string>; // 返回创建的记忆ID createBatch(memories: Memory[]): Promise<string[]>; // 删 delete(memoryId: string): Promise<boolean>; deleteByFilter(filter: MemoryFilter): Promise<number>; // 根据条件批量删除 // 改 update(memoryId: string, updates: Partial<Memory>): Promise<boolean>; // 特别重要的:更新访问时间或重要性 touch(memoryId: string): Promise<boolean>; // 更新lastAccessed时间 reinforce(memoryId: string, delta: number): Promise<boolean>; // 调整重要性分数 // 查 - 这是核心中的核心 get(memoryId: string): Promise<Memory | null>; // 基于元数据的过滤查询(精确匹配) findByMetadata(filter: MemoryFilter): Promise<Memory[]>; // 基于语义的相似性搜索(向量检索) searchByEmbedding( queryEmbedding: number[], options: { limit: number; similarityThreshold?: number } ): Promise<Array<{ memory: Memory; similarity: number }>>; // 混合搜索:结合元数据过滤和语义搜索 hybridSearch( filter: MemoryFilter, queryText?: string, options: { limit: number } ): Promise<Memory[]>; } // 用于过滤记忆的条件 interface MemoryFilter { startTime?: Date; endTime?: Date; source?: string; tags?: string[]; minImportance?: number; // ... 其他可索引的字段 }

实操心得hybridSearch方法非常关键。在实际应用中,用户可能既想查找“上周”(时间过滤)关于“项目A”(标签过滤)的“所有讨论”(语义搜索)。纯向量搜索可能把“项目B”的相似讨论也找出来,而混合搜索能更精准地定位目标。

3.3 记忆处理层:记忆的生成、提取与压缩

这一层是系统的“大脑”,负责将原始信息加工成记忆,以及从记忆中提取有用的上下文。

interface MemoryProcessor { // 1. 记忆生成:从原始交互中提取结构化记忆 extractMemoriesFromInteraction(interaction: { role: ‘user’ | ‘agent’ | ‘system’; content: string; timestamp: Date; // 可能包含更丰富的原始数据 }): Promise<Memory[]>; // 2. 记忆摘要:将一系列相关记忆压缩成一条摘要记忆,防止记忆爆炸 summarizeMemories(memoryIds: string[]): Promise<Memory>; // 3. 查询理解:将用户的自然语言查询或Agent的当前目标,转换为搜索记忆的指令 formulateMemoryQuery(context: string): Promise<{ semanticQuery?: string; // 用于生成向量的查询文本 filter?: MemoryFilter; // 用于精确过滤的条件 }>; // 4. 上下文组装:获取相关记忆后,将其格式化成适合送入LLM上下文的文本 formatMemoriesForContext(memories: Memory[]): string; }

核心实现细节extractMemoriesFromInteraction函数通常会调用LLM(如GPT-4)或更轻量的NLP模型。你可以设计一个Prompt,让LLM从一段对话中识别出可能成为长期记忆的要点,并以指定的JSON格式输出。例如:

你是一个记忆提取助手。请从以下对话中,识别出需要存入长期记忆的关键信息。 输出格式为JSON数组,每个元素包含`type`(observational/factual), `content`等字段。 对话: 用户:“我更喜欢用邮件接收报告,别发Slack了。” Agent:“好的,已更新您的偏好。”

LLM应输出一个包含typefactual,content{“subject”: “user”, “predicate”: “prefers”, “object”: “email for reports”}的记忆。

4. 集成到AI Agent工作流:让记忆流动起来

设计好模块后,最关键的一步是将其融入Agent的推理循环。一个典型的基于LLM的Agent循环(ReAct模式)包括:观察、思考、行动。记忆系统应深度嵌入“思考”阶段。

4.1 在每次推理前自动检索相关记忆

我们创建一个AgentWithMemory类,它包装了基础的LLM调用。

class AgentWithMemory { constructor( private llmClient: LLMClient, private memoryStore: MemoryStore, private memoryProcessor: MemoryProcessor ) {} async runCycle(userInput: string, currentContext: any): Promise<AgentAction> { // 步骤1:观察 - 将当前输入和上下文转化为记忆查询 const query = await this.memoryProcessor.formulateMemoryQuery( `用户说:“${userInput}”。当前任务上下文:${JSON.stringify(currentContext)}` ); // 步骤2:检索 - 从记忆库中获取相关记忆 let relevantMemories: Memory[] = []; if (query.semanticQuery) { // 生成查询向量(需要嵌入模型) const queryEmbedding = await generateEmbedding(query.semanticQuery); const vectorResults = await this.memoryStore.searchByEmbedding(queryEmbedding, { limit: 5 }); relevantMemories = vectorResults.map(r => r.memory); } if (query.filter) { const filteredMemories = await this.memoryStore.findByMetadata(query.filter); // 合并结果,去重 relevantMemories = this.mergeAndDedupeMemories(relevantMemories, filteredMemories); } // 步骤3:思考 - 将相关记忆格式化为上下文,与当前输入一起送给LLM const memoryContext = this.memoryProcessor.formatMemoriesForContext(relevantMemories); const fullPrompt = ` 以下是来自过去的相关记忆: ${memoryContext} 当前对话: 用户:${userInput} 请基于以上信息(包括历史记忆)进行思考并决定下一步行动。 `; const llmResponse = await this.llmClient.complete(fullPrompt); const action = this.parseLlmResponse(llmResponse); // 解析出工具调用或回复 // 步骤4:行动 - 执行动作 const result = await this.executeAction(action); // 步骤5:记忆 - 将本轮交互的重要结果存储下来 const newMemories = await this.memoryProcessor.extractMemoriesFromInteraction({ role: ‘user’, content: userInput, timestamp: new Date(), }); // 也可能存储Agent行动的结果作为记忆 const resultMemories = await this.memoryProcessor.extractMemoriesFromInteraction({ role: ‘agent’, content: `执行了动作 ${action.type},结果:${JSON.stringify(result)}`, timestamp: new Date(), }); await this.memoryStore.createBatch([...newMemories, ...resultMemories]); // 更新相关记忆的“最后访问时间”,强化它们 for (const mem of relevantMemories) { await this.memoryStore.touch(mem.metadata.id); } return { action, result }; } }

注意事项记忆爆炸是常见问题。如果每一轮交互都存储多条记忆,数据库会飞速膨胀。解决方案是:

  1. 设置重要性阈值:只有importance分数高于某个值(如0.7)的记忆才被长期存储。
  2. 定期摘要:启动一个后台进程,定期(如每天)将同一主题的大量细粒度记忆(如关于“项目A”的20条讨论),通过summarizeMemories合并成一条摘要记忆,然后归档或删除原始记忆。
  3. 遗忘机制:实现一个“记忆清理”函数,定期降低长时间未被访问的记忆的importance分数,当分数低于阈值时自动删除。

4.2 记忆的主动触发与提醒

高级的记忆系统不应只是被动查询,还应能主动提醒。例如,当用户再次提到“项目A”时,系统可以主动提示:“根据上次记录,您提到项目A的截止日期是本周五,需要我为您查看当前进度吗?”这需要在检索到高相关度记忆,并且该记忆关联着未完成的待办事项(Todo)或重要提醒时,由MemoryProcessor在格式化上下文时额外添加一个“主动提醒”部分。

5. 实战:基于本地向量数据库的实现方案

理论说再多,不如跑通一个最小可行产品。我们用一个完全可在本地运行的技术栈来实现核心流程。

5.1 技术栈选择

  • 运行时/框架:Node.js + TypeScript。这是AI Agent生态的主流选择。
  • 向量数据库Chroma。它轻量、开源、可以嵌入运行,非常适合原型开发和中小项目。也可以选择LanceDBSQLite-VSS(如果更喜欢SQL接口)。
  • 嵌入模型OpenAI的text-embedding-3-small。对于原型来说,它的效果、速度和成本平衡得最好。如果想完全本地化,可以使用**Hugging Face上的all-MiniLM-L6-v2**模型,通过@xenova/transformers库在本地运行,虽然速度慢些,但无需网络和API密钥。
  • LLM:用于记忆提取和Agent推理。可以选择OpenAI GPT、Anthropic Claude的API,或者本地运行的Ollama(运行Llama 3、Qwen等开源模型)。

5.2 核心代码实现片段

首先,安装依赖:npm install chromadb openai

import { ChromaClient } from ‘chromadb’; import OpenAI from ‘openai’; // 1. 初始化客户端 const chromaClient = new ChromaClient({ path: “http://localhost:8000” }); // 假设Chroma在本地运行 const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 2. 实现一个基于Chroma的MemoryStore class ChromaMemoryStore implements MemoryStore { private collection: any; constructor(collectionName: string) { this.initializeCollection(collectionName); } private async initializeCollection(name: string) { // 创建或获取一个集合,指定嵌入向量的维度(text-embedding-3-small是1536维) this.collection = await chromaClient.getOrCreateCollection({ name, metadata: { “hnsw:space”: “cosine” } // 使用余弦相似度 }); } async create(memory: Memory): Promise<string> { const id = memory.metadata.id; const embedding = memory.embedding || await this.generateEmbedding(memory); const metadata = { timestamp: memory.metadata.timestamp.toISOString(), source: memory.metadata.source, tags: JSON.stringify(memory.metadata.tags), importance: memory.metadata.importance.toString(), }; await this.collection.add({ ids: [id], embeddings: [embedding], metadatas: [metadata], documents: [JSON.stringify(memory.content)], // 将内容作为文档存储 }); return id; } async searchByEmbedding( queryEmbedding: number[], options: { limit: number; similarityThreshold?: number } ): Promise<Array<{ memory: Memory; similarity: number }>> { const results = await this.collection.query({ queryEmbeddings: [queryEmbedding], nResults: options.limit, }); const memories: Array<{ memory: Memory; similarity: number }> = []; for (let i = 0; i < results.ids[0].length; i++) { const distance = results.distances[0][i]; // Chroma返回的是距离,余弦相似度=1-距离 const similarity = 1 - distance; if (options.similarityThreshold && similarity < options.similarityThreshold) { continue; } const memory: Memory = { metadata: { id: results.ids[0][i], timestamp: new Date(results.metadatas[0][i].timestamp), source: results.metadatas[0][i].source, tags: JSON.parse(results.metadatas[0][i].tags), importance: parseFloat(results.metadatas[0][i].importance), }, content: JSON.parse(results.documents[0][i]), }; memories.push({ memory, similarity }); } return memories; } // ... 实现其他接口方法(findByMetadata, update, delete等) // findByMetadata需要遍历或依赖Chroma的元数据过滤功能(如果版本支持) private async generateEmbedding(memory: Memory): Promise<number[]> { // 根据记忆内容生成文本描述 const textToEmbed = this.getTextRepresentation(memory); const response = await openai.embeddings.create({ model: “text-embedding-3-small”, input: textToEmbed, }); return response.data[0].embedding; } private getTextRepresentation(memory: Memory): string { // 将记忆内容转换为用于生成嵌入的文本 switch (memory.content.type) { case ‘observational’: return memory.content.text; case ‘factual’: return `${memory.content.subject} ${memory.content.predicate} ${memory.content.object}`; case ‘procedural’: return `Skill: ${memory.content.skillName}. Steps: ${memory.content.steps.join(‘, ‘)}`; case ‘summary’: return `Summary: ${memory.content.text}`; } } }

踩坑记录

  1. 嵌入维度一致性:确保你使用的嵌入模型维度与创建向量数据库集合时指定的维度一致。text-embedding-3-small是1536维,text-embedding-3-large是3072维,混用会导致错误。
  2. 元数据序列化:Chroma的元数据字段值通常要求是字符串。存储数组或对象时,需要使用JSON.stringify(),读取时使用JSON.parse()
  3. 相似度阈值:余弦相似度范围是[-1, 1],但通常处理后是[0, 1]。对于文本,0.7~0.8以上通常表示强相关。需要根据你的嵌入模型和数据进行调整,设置similarityThreshold可以过滤掉低质量结果。

5.3 一个简单的记忆提取处理器实现

class OpenAIMemoryProcessor implements MemoryProcessor { async extractMemoriesFromInteraction(interaction: any): Promise<Memory[]> { const prompt = ` 你是一个记忆提取专家。请分析以下对话,提取出值得放入AI长期记忆的信息。 这些信息包括:用户明确陈述的偏好或事实、重要的承诺或决定、关键的个人或项目信息。 对于每个提取出的记忆,请按以下JSON格式输出: { “type”: “factual” | “observational”, “content”: { ... } // 根据type不同,结构见下文 } 如果是事实(factual),content应为 {“subject”: “”, “predicate”: “”, “object”: “”} 格式。 如果是观察(observational),content应为 {“text”: “”} 格式。 对话内容: ${interaction.content} 请只输出一个JSON数组。 `; const response = await openai.chat.completions.create({ model: “gpt-4-turbo-preview”, messages: [{ role: ‘user’, content: prompt }], response_format: { type: “json_object” }, }); const extracted = JSON.parse(response.choices[0].message.content || ‘[]’); // 将提取的数据包装成Memory对象 return extracted.map((item: any) => ({ metadata: { id: uuidv4(), timestamp: interaction.timestamp, source: interaction.role, tags: [], // 可以尝试让LLM也生成标签 importance: 0.5, // 初始重要性 }, content: item, })); } // ... 实现其他接口方法 }

6. 性能优化与高级特性探讨

当系统跑起来后,你会面临性能和扩展性挑战。

6.1 检索优化策略

  • 分层记忆检索:不要每次都用向量搜索全库。可以先通过时间、标签等元数据过滤出一个较小的候选集,再在这个集合上做向量相似度计算。这能大幅减少计算量。
  • 缓存热点记忆:对于当前会话或任务中频繁访问的记忆,可以缓存在内存中(如使用LRU Cache),避免重复查询数据库。
  • 预计算记忆关联:在记忆存储时,通过图数据库或简单的关联表,记录记忆与记忆之间的关系(如“属于同一话题”、“是前因后果”)。检索时可以先找“种子记忆”,再通过关联关系找到相关记忆,这比纯向量搜索有时更精准。

6.2 记忆的评估与遗忘机制

实现一个后台的“记忆管理”服务。

class MemoryManager { constructor(private memoryStore: MemoryStore) {} async decayAndForget() { // 1. 重要性衰减:长时间未访问的记忆,重要性降低 const oldMemories = await this.memoryStore.findByMetadata({ endTime: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30天前 }); for (const mem of oldMemories) { // 如果超过90天未访问,重要性降为原来的0.9倍 const daysSinceAccess = (Date.now() - (mem.metadata.lastAccessed?.getTime() || mem.metadata.timestamp.getTime())) / (1000*60*60*24); if (daysSinceAccess > 90) { await this.memoryStore.reinforce(mem.metadata.id, -0.1); // 减少0.1 } } // 2. 清理低重要性记忆 const lowImportanceMemories = await this.memoryStore.findByMetadata({ maxImportance: 0.2, // 重要性低于0.2 }); for (const mem of lowImportanceMemories) { await this.memoryStore.delete(mem.metadata.id); } // 3. 定期摘要:将同一标签下的大量记忆合并 const tags = await this.getAllTags(); for (const tag of tags) { const memories = await this.memoryStore.findByMetadata({ tags: [tag] }); if (memories.length > 10) { // 超过10条就摘要 const summary = await this.memoryProcessor.summarizeMemories(memories.map(m => m.metadata.id)); await this.memoryStore.create(summary); // 可选:删除或归档原始记忆 // await this.memoryStore.deleteByFilter({ tags: [tag], endTime: someTime }); } } } }

6.3 多模态记忆

未来的Agent不仅处理文本,还会处理图像、音频。记忆系统也需要扩展。例如,一张用户上传的图表可以提取其文本描述生成向量存储,同时将原文件存储在对象存储(如S3)中,并在记忆元数据里保存文件链接。检索时,先通过文本描述找到相关记忆,再根据需要获取原文件。

7. 测试与评估:如何判断记忆系统是否有效

搭建完成后,如何评估其好坏?不能只靠感觉。

  • 定量评估
    • 检索准确率:构建一个测试集,包含一系列查询和“标准答案”记忆。计算系统召回的相关记忆中,有多少是真正相关的。
    • 上下文相关性增益:设计两组对话,一组使用记忆系统提供历史上下文,另一组不使用。让人类评估员或另一个LLM判断哪组对话的回复更连贯、更个性化、更准确。
    • 任务完成率:对于需要历史信息的任务(如“继续编辑我上周未完成的文档”),对比有记忆和无记忆的Agent任务完成成功率。
  • 定性评估
    • 用户体验:进行用户测试,观察用户是否注意到Agent“记得”他们之前说过的话,并询问他们的感受。
    • 错误分析:定期检查被错误检索的记忆(噪音)和被漏检的关键记忆(遗漏),分析原因,是嵌入模型问题、查询表述问题还是元数据设计问题。

构建AI Agent的记忆系统是一个从认知理论到工程实践的深度旅程。它没有一劳永逸的解决方案,需要你根据Agent的具体应用场景(是客服、编程助手还是游戏NPC)不断调整记忆的粒度、检索策略和更新机制。从本文介绍的基础架构开始,实现一个最小可用的版本,然后在真实交互中观察、迭代和优化,你的Agent才会真正变得越来越“聪明”和“贴心”。