基于向量数据库的AI记忆系统:为Claude构建持久化记忆模块
1. 项目概述与核心价值最近在折腾一个挺有意思的项目叫zelinewang/claudemem。乍一看这个标题可能有点摸不着头脑它不像那些直接叫“XX管理系统”或者“XX工具包”的项目那么直白。但恰恰是这种命名往往藏着开发者最核心的创意和巧思。claudemem这个名字我拆解了一下感觉是 “Claude” 和 “Memory” 的组合体。Claude 是谁熟悉 AI 领域的朋友应该不陌生它是 Anthropic 公司开发的一款强大的大语言模型以其出色的安全性和逻辑推理能力著称。而 “Memory” 就是记忆。所以这个项目的核心目标很可能就是为 Claude 这类大语言模型LLM构建一个外部的、可持久化的记忆系统。为什么需要这个用过 ChatGPT 或 Claude 的朋友都知道标准的对话是“无状态”的。你问一句它答一句上下文仅限于当前对话窗口。一旦关闭窗口或者开始新对话模型就“忘记”了之前聊过的所有内容。这对于需要长期、连贯交互的场景来说是个巨大的痛点。比如你想让 AI 助手帮你长期管理项目进度、记录你的个人偏好、或者作为一个知识库不断积累你们讨论过的知识点没有记忆功能就完全无法实现。zelinewang/claudemem瞄准的正是解决这个“AI 健忘症”的问题。它试图在模型外部搭建一个专属的、结构化的记忆存储与检索机制让 AI 在每次交互时都能“回忆”起相关的历史信息从而实现真正个性化的、有连续性的智能体Agent。这个项目适合谁呢首先肯定是 AI 应用开发者。如果你正在基于 Claude API 构建需要长期记忆的聊天机器人、个人助理或者复杂的多轮工作流这个项目提供了一个现成的、可集成的记忆模块。其次对于 AI 爱好者和技术极客来说研究其实现原理能让你深入理解如何为 LLM 设计上下文管理、向量检索等关键技术。最后即便是普通用户理解这个概念也能帮你更好地构想未来 AI 助手应该是什么样子——它不应该每次聊天都像初次见面。2. 核心架构与设计思路拆解2.1 记忆系统的核心挑战与方案选型为 AI 构建记忆听起来简单做起来却有一系列工程和算法上的挑战。首要问题就是“记什么”和“怎么记”。AI 和人类的对话是海量的文本流我们不可能像录像一样把每一字一句都原封不动地存下来那样效率低下且在检索时毫无针对性。因此一个高效的记忆系统必须解决以下几个核心问题记忆的表示Representation如何将一段自由文本对话内容转化为计算机可以高效处理和比对的结构化形式记忆的存储Storage采用什么样的数据结构和数据库来存放这些记忆以支持快速的写入和查询记忆的检索Retrieval当新问题到来时如何从海量记忆中快速、准确地找到最相关的那部分记忆的更新与遗忘Update Forgetting记忆不是一成不变的。新的信息可能强化、修正或否定旧记忆。系统是否需要以及如何实现记忆的更新、合并甚至主动遗忘目前业界针对这些挑战最主流、最有效的解决方案是“向量数据库 嵌入模型”的技术栈。这也是claudemem这类项目几乎必然采用的核心架构。其工作流程可以概括为编码Encode使用一个嵌入模型Embedding Model将每一段需要记忆的文本例如用户的一句话或 AI 的一个回复转换成一个高维度的向量Vector。这个向量就像是这段文本的“数学指纹”语义相近的文本其向量在空间中的距离也会很近。存储Store将这个向量连同原始的文本片段或其它元数据如时间戳、对话 ID 等存入一个专门为向量检索优化的数据库例如 Chroma, Pinecone, Weaviate 或 Qdrant。检索Retrieve当用户提出一个新问题时同样用嵌入模型将其转换为查询向量。然后向向量数据库发起一个“相似性搜索”Similarity Search请求例如计算余弦相似度找出与查询向量最接近的 Top-K 个记忆向量。回忆Recall将检索到的 Top-K 个原始文本片段作为“相关的历史记忆”与当前的新问题一起组合成增强版的上下文Prompt发送给 Claude 模型。这样Claude 在生成回答时就能“看到”这些历史信息。claudemem的设计思路大概率就是围绕这个核心流程构建一个封装良好、易于使用的 Python 库或服务。它需要优雅地处理嵌入模型的调用、向量数据库的连接与操作、以及和 Claude API 的协同。2.2 项目模块分解与职责界定基于上述思路我们可以推断claudemem项目至少会包含以下几个核心模块记忆核心模块Memory Core这是项目的心脏。它定义“记忆”的数据结构。一个记忆单元可能包含唯一 ID、原始文本内容、对应的嵌入向量、创建时间戳、所属会话/用户 ID、以及可能的权重或重要性标签。这个模块还负责记忆的“生命周期”管理包括创建、更新、标记为过期等。向量化服务模块Embedding Service负责与嵌入模型交互。它需要支持配置不同的嵌入模型如 OpenAI 的text-embedding-3-small,text-embedding-ada-002或开源的BGE,Sentence-Transformers模型。该模块将文本列表批量转换为向量列表并处理可能出现的网络错误、速率限制等问题。存储后端模块Storage Backend抽象出向量数据库的操作接口。理想的设计是支持可插拔的后端比如默认集成一个轻量级的本地向量数据库如 Chroma同时允许用户配置连接到云端的向量数据库服务如 Pinecone。这个模块封装了创建集合Collection、插入向量、查询相似向量、删除数据等底层操作。检索与组装模块Retrieval Assembly这是智能所在。它不仅仅执行简单的向量相似度搜索。高级的记忆系统可能会实现混合检索Hybrid Search结合向量相似度语义搜索和关键词匹配全文搜索以提高检索的准确性和召回率。时间衰减加权Recency Weighting给更近期的记忆更高的权重因为最近的对话通常相关性更高。重要性过滤Importance Filtering尝试自动或手动标记某些记忆为“重要”确保它们更容易被检索到。上下文窗口管理Context Window ManagementClaude API 有上下文长度限制如 200K tokens。检索模块需要智能地选择最相关的记忆并确保它们组装进 Prompt 后不会超出限制必要时进行摘要或截断。Claude 客户端集成模块Claude Client Integration提供便捷的 API让开发者能够以“记忆感知”的方式调用 Claude。例如一个chat_with_memory方法内部自动完成“检索记忆 - 组装 Prompt - 调用 Claude API - 存储新记忆”的完整流程。注意在方案选型上使用向量数据库而非传统关系型数据库如 MySQL进行语义检索是性能和应用场景上的必然选择。传统数据库的 LIKE 查询完全无法理解语义而专门的向量数据库经过优化能在毫秒级时间内从百万级数据中找出最相似的条目这是实现流畅对话体验的基础。3. 关键技术细节与实操要点3.1 嵌入模型的选择与调优嵌入模型是将文本映射到向量空间的关键其质量直接决定了记忆检索的准确性。claudemem需要在此提供灵活性。云端 API 与本地模型OpenAI Embeddings如text-embedding-3-small和text-embedding-3-large。优点是质量高、稳定、无需管理基础设施。缺点是会产生持续 API 费用且有网络延迟和速率限制。对于生产级应用这是常见选择。开源本地模型如BAAI/bge-small-en-v1.5或sentence-transformers/all-MiniLM-L6-v2。优点是完全免费、数据隐私性好、延迟低。缺点是需要本地 GPU 或 CPU 资源进行推理且模型效果可能略逊于顶级商用模型。适合对成本敏感或数据隐私要求极高的场景。实操建议在项目初期或开发阶段可以优先使用 OpenAI 的 API快速验证想法。准备上线时根据成本、隐私和性能需求评估是否切换到本地模型。claudemem的配置项应该允许轻松切换这两种模式。文本分块策略我们不是存储整个对话记录而是需要将其切割成有意义的“块”再进行向量化。分块策略至关重要固定长度分块简单按字符或 Token 数切割如每 500 字符一块。可能切断句子或段落的完整性。语义分块基于句子或段落边界进行切割尽可能保证每个“块”语义独立。这是更优的选择。可以使用langchain的RecursiveCharacterTextSplitter并设置separators为[\n\n, \n, . , , ]并指定chunk_size和chunk_overlap重叠部分避免语义断裂。对话轮次分块对于 QA 格式的对话直接将每一轮“用户问 AI 答”作为一个记忆块能很好地保留对话逻辑。3.2 向量数据库的集成与数据管理后端选择claudemem很可能以Chroma作为默认或首选后端。原因如下轻量易用Chroma 是一个开源向量数据库可以嵌入到 Python 应用中无需单独部署服务器非常适合开源项目和快速原型开发。Python 原生其 API 设计非常 Pythonic与claudemem这样的 Python 库集成起来代码简洁。功能完备支持基本的 CRUD、相似性搜索、元数据过滤能满足记忆系统的核心需求。 当然一个设计良好的系统应该抽象存储层未来可以方便地扩展支持Pinecone云服务高性能、Qdrant开源功能丰富或Weaviate开源带图数据库特性。集合Collection设计在向量数据库中数据存储在“集合”中。一个合理的设计是为每个独立对话或每个用户创建一个单独的集合。这样做的好处是隔离性不同用户/对话的记忆完全隔离互不干扰。检索效率搜索范围限定在单个集合内速度更快。易于清理可以方便地删除整个会话来清理内存。 集合的命名可以用user_{user_id}_session_{session_id}或类似的模式。元数据Metadata的利用除了向量和文本存入数据库的每条记忆都应该附带丰富的元数据例如metadata { timestamp: 2023-10-27T10:30:00Z, speaker: user, # 或 assistant session_id: sess_abc123, importance_score: 0.8, # 可选重要性评分 tags: [project_planning, deadline] # 可选标签 }元数据可以在检索时用于过滤。比如你可以查询“在某个会话中用户提到的所有关于‘截止日期’的记忆”。这比纯向量检索更精确。3.3 检索策略的进阶实现简单的向量相似度搜索KNN是基础但一个成熟的记忆系统需要更智能的检索。查询重写Query Rewriting用户的当前问题可能很短或指代不清如“上次说的那个事”。直接用它去搜索效果不好。可以在检索前先用 LLM甚至是一个小模型对查询进行重写或扩展。例如将“那个事”结合最近的聊天历史重写为“关于周三下午讨论的XX项目启动会议的时间安排”。混合检索Hybrid Search同时进行向量搜索和关键词BM25搜索然后将两者的结果按分数融合。这能兼顾语义相似性和字面匹配对于包含特定名称、代号、数字的记忆检索尤其有效。Chroma 等数据库已开始支持混合检索。重排序Re-ranking第一步先用向量数据库召回 Top-N比如 20 条相关记忆。第二步使用一个更精细的通常是交叉编码器 Cross-Encoder重排序模型对这 20 条记忆与查询的相关性进行更精确的评分选出最终的 Top-K比如 3 条注入上下文。这一步能显著提升精度但会增加延迟和计算成本。记忆摘要与压缩当检索到的相关记忆总长度超过模型上下文窗口时需要处理。粗暴截断会丢失信息。更好的方法是使用 LLM 对这些记忆进行摘要生成一个浓缩版后再注入。这属于“记忆压缩”的高级课题。4. 实战构建一个简易的 Claude 记忆代理下面我们抛开claudemem的具体实现从零开始勾勒一个具备核心记忆功能的 Claude 代理的构建步骤。这能帮助你理解其内部机理。4.1 环境准备与依赖安装首先创建一个新的 Python 虚拟环境并安装核心库。# 创建并激活虚拟环境以 conda 为例 conda create -n claude-mem python3.10 conda activate claude-mem # 安装核心依赖 pip install anthropic # Claude 官方 SDK pip install chromadb # 向量数据库 pip install sentence-transformers # 本地嵌入模型可选也可用 openai pip install tiktoken # 用于计算 Token管理上下文长度如果你选择使用 OpenAI 的嵌入模型还需要安装openai库并配置 API 密钥。4.2 记忆系统的核心类实现我们创建一个ClaudeMemoryAgent类来封装所有功能。import uuid from datetime import datetime from typing import List, Dict, Any, Optional import chromadb from chromadb.config import Settings import anthropic import tiktoken # 根据选择导入 openai 或 sentence_transformers class ClaudeMemoryAgent: def __init__(self, anthropic_api_key: str, embedding_model_name: str all-MiniLM-L6-v2, # 本地模型 use_openai_embedding: bool False, openai_api_key: Optional[str] None, persist_directory: str ./chroma_db): 初始化记忆代理。 Args: anthropic_api_key: Claude API 密钥。 embedding_model_name: 嵌入模型名称。 use_openai_embedding: 是否使用 OpenAI 嵌入。 openai_api_key: OpenAI API 密钥如果使用。 persist_directory: Chroma 数据持久化目录。 self.anthropic_client anthropic.Anthropic(api_keyanthropic_api_key) self.embedding_model_name embedding_model_name self.use_openai_embedding use_openai_embedding if use_openai_embedding: import openai self.openai_client openai.OpenAI(api_keyopenai_api_key) self._get_embeddings self._get_openai_embeddings else: from sentence_transformers import SentenceTransformer self.local_embedder SentenceTransformer(embedding_model_name) self._get_embeddings self._get_local_embeddings # 初始化 Chroma 客户端持久化存储 self.chroma_client chromadb.PersistentClient(pathpersist_directory) # 我们为每个代理实例使用一个固定的集合名实际应用中可按用户/会话区分 self.collection_name claude_memory self.collection self.chroma_client.get_or_create_collection(nameself.collection_name) self.encoding tiktoken.get_encoding(cl100k_base) # Claude 使用的编码 def _get_local_embeddings(self, texts: List[str]) - List[List[float]]: 使用本地 Sentence Transformer 模型获取嵌入向量。 embeddings self.local_embedder.encode(texts, convert_to_numpyTrue, normalize_embeddingsTrue) return embeddings.tolist() def _get_openai_embeddings(self, texts: List[str]) - List[List[float]]: 使用 OpenAI API 获取嵌入向量。 response self.openai_client.embeddings.create( modeltext-embedding-3-small, inputtexts ) return [data.embedding for data in response.data] def _compute_tokens(self, text: str) - int: 计算文本的 Token 数用于上下文窗口管理。 return len(self.encoding.encode(text)) def add_memory(self, text: str, metadata: Optional[Dict] None): 添加一段记忆到向量数据库。 if metadata is None: metadata {} # 生成唯一 ID memory_id str(uuid.uuid4()) # 获取文本向量 embedding self._get_embeddings([text])[0] # 补充基础元数据 metadata.update({ timestamp: datetime.now().isoformat(), text: text, # 存储原始文本 token_count: self._compute_tokens(text) }) # 存入 Chroma self.collection.add( embeddings[embedding], documents[text], # Chroma 也可以存储 documents metadatas[metadata], ids[memory_id] ) print(fMemory added: {text[:50]}...) def retrieve_memories(self, query: str, n_results: int 5) - List[Dict]: 根据查询检索相关记忆。 # 获取查询向量 query_embedding self._get_embeddings([query])[0] # 在集合中搜索 results self.collection.query( query_embeddings[query_embedding], n_resultsn_results ) # 整理返回结果 memories [] if results[documents]: for i in range(len(results[documents][0])): memory { text: results[documents][0][i], metadata: results[metadatas][0][i], distance: results[distances][0][i] # 相似度距离 } memories.append(memory) return memories def chat_with_memory(self, user_input: str, session_context: str , max_tokens: int 1000) - str: 核心对话方法检索记忆 - 组装上下文 - 调用 Claude - 存储新记忆。 # 1. 检索相关记忆 relevant_memories self.retrieve_memories(user_input, n_results3) memory_context if relevant_memories: memory_texts [mem[text] for mem in relevant_memories] memory_context \n\nRelevant past memories:\n- \n- .join(memory_texts) print(fRetrieved {len(memory_texts)} memories.) # 2. 组装系统提示词注入记忆和会话上下文 system_prompt fYou are a helpful assistant with a memory system. Below are some relevant past interactions that might help you answer the current question. {session_context} {memory_context} Current conversation: # 注意实际应用中需要更精细地管理 Token 数这里做了简化 full_prompt f{system_prompt}\n\nHuman: {user_input}\n\nAssistant: # 3. 调用 Claude API try: message self.anthropic_client.messages.create( modelclaude-3-haiku-20240307, # 使用合适的模型 max_tokensmax_tokens, systemsystem_prompt, messages[ {role: user, content: user_input} ] ) assistant_response message.content[0].text except Exception as e: assistant_response fError calling Claude API: {e} # 4. 将本轮交互的重要部分存入记忆这里简单存储用户输入和AI回复 # 更复杂的策略可以只存储关键信息或由AI决定是否存储 self.add_memory(fHuman: {user_input}, metadata{speaker: user}) self.add_memory(fAssistant: {assistant_response}, metadata{speaker: assistant}) return assistant_response4.3 运行一个简单的对话循环# 使用示例 if __name__ __main__: import os ANTHROPIC_API_KEY os.getenv(ANTHROPIC_API_KEY) # 如果使用 OpenAI Embedding还需要 OPENAI_API_KEY agent ClaudeMemoryAgent( anthropic_api_keyANTHROPIC_API_KEY, use_openai_embeddingFalse, # 使用本地嵌入模型以节省成本 persist_directory./my_memory_db ) print(Claude Memory Agent Started. Type quit to exit.) session_ctx This is a conversation about project planning for Project Phoenix. while True: user_input input(\nYou: ) if user_input.lower() quit: break response agent.chat_with_memory(user_input, session_contextsession_ctx) print(f\nClaude: {response})这个简易版本实现了核心流程存储、检索、对话。你可以通过多次对话询问之前提过的事情比如“我之前说的项目截止日是哪天”来观察记忆系统是否生效。实操心得在组装 Prompt 时系统提示词system_prompt的设计非常关键。你需要清晰地告诉 Claude 这些“记忆”是什么、该如何使用它们。例如可以指示“以下是你之前对话的相关片段请参考它们来回答用户当前的问题如果记忆中的信息与当前问题无关请忽略。” 这能防止 AI 被不相关的历史信息带偏。5. 高级特性与优化方向一个基础的记忆系统搭建起来后我们可以朝着claudemem可能具备的更高级特性去思考和优化。5.1 记忆的主动管理与维护记忆不能只增不减。一个智能系统需要“忘记”或降权不重要的信息。基于时间的衰减为每条记忆附加一个“强度”或“新鲜度”分数随着时间推移自动衰减。在检索时将相似度分数与新鲜度分数结合进行排序。基于访问频率的强化被频繁检索和使用的记忆其重要性应该提高。这模拟了人类的“反复记忆加深”过程。记忆去重与合并当新增的记忆与已有记忆在语义上高度相似时系统可以自动合并它们而不是创建一条新的、冗余的记忆。这需要设定一个相似度阈值并在合并时整合文本和元数据。显式记忆删除提供 API 让用户或系统可以主动删除特定的、错误的或不再需要的记忆。5.2 上下文管理的艺术Claude 模型有巨大的上下文窗口如 200K但并非无限且更长的上下文意味着更高的 API 成本和可能的性能下降。动态上下文窗口不是固定地注入前 N 条聊天记录而是根据当前查询动态地从向量数据库中检索最相关的 N 条记忆长短不一并确保它们的总 Token 数不超过一个安全阈值比如模型上限的 70%。记忆摘要链对于长时间、高密度的对话如一场长达一小时的会议记录可以定期例如每 10 轮对话使用 Claude 的“总结”能力生成一个该段对话的摘要并将这个摘要作为一条新的、高权重的“宏观记忆”存入数据库。这样在后续检索时系统可以先尝试匹配摘要如果需要细节再定位到原始对话片段。分层记忆结构设计短期记忆Session Memory和长期记忆Long-term Memory。短期记忆存放当前对话窗口的原始记录检索优先级最高长期记忆存放经过摘要、提炼后的核心知识用于跨会话的知识积累。5.3 与智能体框架的集成claudemem不应只是一个孤立的库而应该能无缝集成到更复杂的 AI 智能体框架中如 LangChain、LlamaIndex 或 AutoGen。作为 LangChain Tool / Memory可以将claudemem包装成一个 LangChain 的BaseMemory类或一个自定义 Tool。这样在基于 LangChain 构建的智能体中可以很方便地调用它来记住工具执行的结果、用户偏好等。多模态记忆扩展目前的记忆主要是文本。未来的方向是支持多模态——将用户上传的图片、文档PDF, Word通过多模态模型如 Claude 3 Vision进行理解提取关键信息并生成文本描述存入记忆系统。当用户问“我上次给你看的那张图表……”时系统能回忆起图表的内容。记忆的元认知让 AI 自己决定什么该记什么不该记。可以在系统提示词中赋予 Claude 一个“元指令”在每次回复后判断当前对话中是否有值得长期存储的信息并输出一个结构化的“记忆存储建议”由外部系统执行存储。这实现了记忆过程的半自动化。6. 常见问题、排查与性能调优在实际使用或开发类似claudemem的系统时你会遇到一些典型问题。6.1 检索效果不佳症状AI 的回答似乎没有利用到记忆或者引用了不相关的历史信息。排查与解决检查嵌入模型尝试不同的嵌入模型。对于中文场景BAAI/bge系列通常比通用英文模型效果好。可以先用一些标准句子测试不同模型的相似度计算是否合理。优化分块大小记忆块太大如一整页文档会包含太多噪声信息降低检索精度太小如单个句子可能丢失上下文。尝试调整chunk_size如 200-500 词和chunk_overlap如 50 词。调整检索数量n_results参数很重要。太小可能漏掉相关记忆太大会引入噪声并占用宝贵上下文。通常从 3-5 开始调整。引入元数据过滤如果记忆带有speaker,topic等元数据在检索时可以利用它们进行过滤缩小搜索范围提高精度。实施查询扩展如前所述对简短模糊的查询进行重写或扩展能大幅提升召回率。6.2 响应速度慢症状对话有明显延迟感觉卡顿。排查与解决向量数据库性能如果记忆条数很多10万本地 Chroma 的检索速度可能成为瓶颈。考虑升级到性能更强的向量数据库如Qdrant或Weaviate或者使用云服务如Pinecone。嵌入模型延迟如果使用本地嵌入模型确保它运行在 GPU 上以获得最佳速度。如果使用 OpenAI API网络延迟和速率限制可能是问题考虑增加重试机制、使用批处理请求或者降级到更快的模型如text-embedding-3-small。异步操作将“存储记忆”这个步骤改为异步操作例如放入一个后台任务队列不要阻塞主对话线程。用户发出消息后系统应立即开始检索和生成回复存储动作可以稍后完成。缓存频繁查询对于一些常见或重复的查询结果可以在内存中做短期缓存避免重复的向量计算和数据库查询。6.3 上下文窗口溢出症状API 调用返回错误提示上下文长度超限。排查与解决严格计算 Token使用tiktoken库精确计算系统提示词、记忆文本、用户输入和预留的回复空间的总 Token 数。设定一个安全上限如模型限制的 80%。动态记忆选择实现一个算法在总 Token 数超限时优先保留相似度最高的记忆剔除相似度较低或较旧的记忆。记忆摘要对于必须保留但很长的记忆实时调用 Claude 的摘要功能进行压缩。虽然这增加了一次 API 调用但能从根本上解决问题。分页检索如果单次检索到的记忆总长度过长可以尝试先检索更多条如 10 条然后根据 Token 限制智能地选择其中最相关的子集。6.4 记忆的“幻觉”与冲突症状AI 基于模糊或冲突的记忆产生了不准确或矛盾的回复。排查与解决为记忆添加置信度在存储记忆时可以尝试让 AI 对自己陈述的信息做一个置信度评估如果可能或者根据信息来源如用户陈述 vs AI 推理赋予不同的可信度权重。在 Prompt 中处理冲突当检索到多条可能冲突的记忆时可以在系统提示词中明确指示 Claude“你检索到以下多条历史信息它们之间可能存在不一致。请仔细辨别以最可靠或最新的信息为准进行回答并可以指出其中的不确定性。”实现记忆溯源在返回记忆给用户或 AI 时附带该记忆的来源如时间戳、原始对话片段链接。这有助于进行人工核查和修正。提供记忆修正接口允许用户对错误的记忆进行标记或更正。系统收到更正后可以更新或覆盖原有的错误记忆。开发像zelinewang/claudemem这样的项目本质上是在为 AI 构建“第二大脑”。它连接了 LLM 强大的即时推理能力和外部系统的持久化存储与结构化检索能力。从简单的向量检索到复杂的记忆生命周期管理、摘要与压缩每一步都充满了工程和算法的挑战。通过深入理解其架构和亲手实践你不仅能打造一个更智能的对话助手更能深刻体会到下一代 AI 应用如何从“单次对话工具”演进为“持续学习的伙伴”。这个领域仍在快速演进新的检索算法、更高效的向量数据库、更智能的记忆压缩策略会不断涌现保持关注和实践是跟上浪潮的最好方式。