- 人工智能
- 大模型
- 预训练
- 微调
- LoRA
- RLHF
- 强化学习
- 分布式训练
【免费下载链接】PaddleNLP
Easy-to-use and powerful LLM and SLM library with awesome model zoo.
导读
PaddleNLP(本仓库中位于 slm/pipelines 目录)为 LLM Agent 提供了一个轻量级的记忆(Memory)机制,用于在多轮推理与工具调用之间保存和回放对话上下文。本文围绕该模块的 API 文档主题,深入剖析抽象基类Memory、对话记忆实现ConversationMemory与空实现NoMemory的源码结构、行为细节,以及它们如何被Agent主循环调用,帮助读者掌握在 Agent 应用中接入、配置乃至自定义记忆组件的完整方法。
一、模块定位:Agent 记忆抽象接口
记忆模块位于仓库的 slm/pipelines/pipelines/agents/memory 目录下,由四个文件构成:
base.py:定义抽象基类Memory;conversation_memory.py:定义ConversationMemory,负责存储对话历史;no_memory.py:定义NoMemory,一个不保存任何数据的空实现;__init__.py:统一导出上述类,供外部以from pipelines.agents.memory import Memory, ConversationMemory, NoMemory方式导入。
在 Agent 中,记忆的核心作用是跨推理步骤保存"输入—输出—观察(observation)"的上下文,使得多跳(Multi-hop)问答等需要分步推理的场景能够持续引用此前各轮的中间结果。这一点在 Agent 基类的类注释 中有明确体现:Agent 会迭代地"生成思考 → 选择工具 → 生成输入 → 观察工具输出",而记忆正是连接这些迭代的信息载体。
抽象基类 Memory 的三个契约方法
从 base.py 源码可以看到,Memory是一个继承abc.ABC的抽象类,定义了三个必须由子类实现的抽象方法:
| 方法 | 签名 | 职责 |
|---|---|---|
load | load(keys: Optional[List[str]] = None, **kwargs) -> Any | 从记忆中加载本次模型运行所需的上下文 |
save | save(data: Dict[str, Any]) -> None | 将本次模型运行的上下文写入记忆 |
clear | clear() -> None | 清空记忆内容 |
load的可选参数keys用于指定需要加载的数据项;save接收一个字典。任何自定义记忆类只需实现这三个方法,即可被 Agent 无缝使用——这正是该模块的扩展点所在。
二、ConversationMemory:基于 OrderedDict 的对话历史存储
ConversationMemory 是默认的对话记忆实现,其核心数据结构是一个 Python 列表,每个元素为一个保留插入顺序的collections.OrderedDict,形如{"Human": ..., "AI": ...}。
构造参数:input_key 与 output_key
def __init__(self, input_key: str = "input", output_key: str = "output"):构造时接收两个字符串参数:
input_key:用于从save(data)传入的字典中取出用户输入的键,默认值为"input";output_key:用于取出模型输出的键,默认值为"output"。
这两个键名需要与 Agent 调用save时传入的数据结构保持一致(详见下文 Agent 集成部分)。
save:把一次对话快照写入列表
def save(self, data: Dict[str, Any]) -> None: chat_snippet = collections.OrderedDict() chat_snippet["Human"] = data[self.input_key] chat_snippet["AI"] = data[self.output_key] self.list.append(chat_snippet)save从data字典中按下述方式提取内容:data[self.input_key]作为"Human"(用户消息),data[self.output_key]作为"AI"(模型回复),组装成一个OrderedDict追加到self.list尾部。因此记忆按时间顺序累积,每次调用save就新增一轮对话。
load:格式化为 Human/AI 转写文本
def load(self, keys: Optional[List[str]] = None, **kwargs) -> str: chat_transcript = "" window_size = kwargs.get("window_size", None) if window_size is not None: chat_list = self.list[-window_size:] else: chat_list = self.list for chat_snippet in chat_list: chat_transcript += f"Human: {chat_snippet['Human']}\n" chat_transcript += f"AI: {chat_snippet['AI']}\n" return chat_transcriptload将存储的对话历史渲染为一段可直接拼入 Prompt 的文本,其关键特性如下:
- 输出格式:每轮对话按
Human: ...与AI: ...两行输出,并以换行符分隔; - window_size 窗口控制:通过
kwargs传入可选参数window_size(整数),指定只加载最近 N 轮对话。实现使用self.list[-window_size:]截取列表尾部,从而实现"滑动窗口"效果,有效控制送入 LLM 的上下文长度; - 返回空串语义:记忆为空时返回
"",与NoMemory.load()的返回值一致,方便上层统一处理。
clear:清空全部历史
def clear(self) -> None: self.list = []直接重置内部列表,达到清空记忆的目的。
三、NoMemory:无记忆模式的空实现
NoMemory 是Memory的一个"空操作"实现,用于关闭记忆功能,避免每次迭代都产生额外状态:
load()直接返回空字符串"";save(data)为空操作(pass),不存储任何数据;clear()为空操作。
从源码注释看,其设计意图非常明确——"不存储任何数据的记忆类"。值得注意的是,Agent 的构造函数中self.memory = memory or NoMemory()表明:当用户未显式传入记忆实例时,Agent 默认启用 NoMemory,即默认行为是不保留上下文。
四、与 Agent 主循环的集成方式
记忆并非孤立组件,其实际消费方是 slm/pipelines/pipelines/agents/base.py 中的Agent类。理解集成点有助于读者掌握"何时写入、写入什么"。
构造注入与默认值
在Agent.__init__中(约第 243~272 行),memory是一个可选参数,类型标注为Optional[Memory]:
memory: Optional[Memory] = None, ... self.memory = memory or NoMemory()即传入ConversationMemory()实例即可开启记忆;不传则自动退化为NoMemory。
迭代中的自动保存
在Agent._step方法中(约第 401~415 行),每次完成一轮"规划 + 工具调用"后都会触发记忆写入:
observation = self.tm.run_tool(next_step.prompt_node_response, params) if not next_step.is_last() else None memory_data = self.prepare_data_for_memory(input=query, output=prompt_node_response, observation=observation) self.memory.save(data=memory_data)这里保存的数据包含三个字段:input(当前查询)、output(LLM 的规划回复)与observation(工具观察结果),与ConversationMemory默认的input_key/output_key一一对应。
prepare_data_for_memory 的归一化逻辑
Agent.prepare_data_for_memory(约第 440~446 行)负责把原始参数转换为可存入记忆的字典,其实现值得注意:
return { k: v if isinstance(v, str) else next(iter(v)) for k, v in kwargs.items() if isinstance(v, (str, Iterable)) }它会过滤掉既非字符串也非可迭代对象的字段;对可迭代但非字符串的值取第一个元素(next(iter(v)))。同时,该方法是可覆写的扩展点——文档注释明确指出可重写它来定制保存到记忆的数据格式,例如追加额外的元信息或调整字段命名。
自定义记忆的两种接入方式
结合上述源码结构,读者可按需选择:
- 复用现成实现:构造
ConversationMemory(input_key=..., output_key=...)后传入 Agent; - 继承
Memory:实现load/save/clear三个方法(例如将对话写入外部存储、数据库或缓存),再注入 Agent;如需改变写入内容,还可同时覆写prepare_data_for_memory。
五、行为验证:测试用例逐条对照
仓库中 slm/pipelines/tests/agents/test_memory.py 用三个单元测试完整锁定了上述行为,可作为理解实现语义的权威参照:
- test_no_memory:验证
NoMemory().load() == "",且save/clear可安全调用而不报错; - test_conversation_memory:验证依次
save两轮对话({"input": "Hello", "output": "Hi there"}与{"input": "How are you?", "output": "I'm doing well, thanks."})后,load()输出逐行拼接的Human: .../AI: ...转写;load(window_size=1)只返回最近一轮;clear()后load()重新变为空串; - test_conversation_memory_window_size:单独验证窗口截断逻辑,包括清空后再以
window_size=1加载同样返回空串的边界情况。
这些断言与前述源码实现完全吻合,读者亦可在本地运行python -m unittest tests/agents/test_memory.py(在 slm/pipelines 目录下)复现验证。
六、小结与扩展指引
Memory模块以极简的三方法抽象(load/save/clear)提供了足够的表达力:ConversationMemory内置有序列表存储、Human/AI 格式化输出与window_size窗口控制,覆盖了 Agent 多轮对话的基本需求;NoMemory则提供了零成本的无状态默认路径。二者在Agent的_step循环中自动联动,并通过prepare_data_for_memory保留了数据定制入口。
进一步深入可查阅:
- 抽象基类定义:理解方法契约与可扩展性;
- 对话记忆实现:窗口控制与格式化细节;
- 空记忆实现:默认无状态语义;
- Agent 集成源码:构造注入与迭代保存调用链;
- 单元测试:全部行为的可执行验证。
- 人工智能
- 大模型
- 预训练
- 微调
- LoRA
- RLHF
- 强化学习
- 分布式训练
【免费下载链接】PaddleNLP
Easy-to-use and powerful LLM and SLM library with awesome model zoo.
相关推荐
PaddleNLP Pipelines Agent 模块详解:Agent、ToolsManager、Tool 与 AgentStep 源码级解析
PaddleNLP Pipelines Agent 模块详解:Agent、ToolsManager、Tool 与 AgentStep 源码级解析 本文以仓库中
人工智能大模型预训练微调LoRARLHF强化学习分布式训练模型推理服务推理引擎模型量化模型压缩本地部署NLPPaddleNLP Pipelines Retriever 模块全解析:Dense、Sparse、多模态与 Web 检索器
PaddleNLP Pipelines Retriever 模块全解析:Dense、Sparse、多模态与 Web 检索器 本文以 PaddleNLP 仓库中
人工智能大模型预训练微调LoRARLHF强化学习分布式训练模型推理服务推理引擎模型量化模型压缩本地部署NLPPaddleNLP paddlenlp.losses 模块深度解析:RDropLoss 与 R-Drop 正则化的实现与应用
PaddleNLP paddlenlp.losses 模块深度解析:RDropLoss 与 R Drop 正则化的实现与应用 导读 本文聚焦 PaddleNLP
人工智能大模型预训练微调LoRARLHF强化学习分布式训练模型推理服务推理引擎模型量化模型压缩本地部署NLP
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考