Mini Agent 源码解析——3 会话和记忆

Mini Agent 源码解析——3 会话和记忆

导言

在前面两篇里,我们花了大量篇幅讨论 Mini Agent 的工具系统:从基础工具的直接调用,到 base.py 中的抽象基类和 schema 导出,再到 BashTool 内部如何完成命令执行与进程管理。这些内容帮助我们理解了一个核心问题——Agent 是如何借助工具与外部世界交互的

但交互只是问题的一面。另一面同样关键的是:Agent 是如何在一次会话中保持上下文、记住之前说过什么、以及如何把历史信息融入当前推理的

我们来看一个常见的场景:用户先让 Agent 写了一个函数,后来又问“我刚才写的那个函数能不能改一下”。如果 Agent 完全不记得之前的内容,这个多轮对话就无法完成。事实上,真正可用的 Agent 系统几乎都必须处理三类信息:

  • 短期记忆:当前对话上下文中的多轮交互历史
  • 长期记忆:跨越会话积累下来的持久化知识或经验
  • 工作内存:当前任务执行过程中正在使用的临时状态

对于一个基于大模型的 Agent 来说,模型本身并不天然具备记忆能力——每一次新的请求对模型而言都是从零开始。因此,框架必须主动承担起“管理对话历史”和“维护会话状态”的职责,在模型需要的时候把相关信息注入上下文。

这篇我们就来聚焦这个话题:Mini Agent 是如何组织会话结构、如何管理对话历史的,以及它的记忆机制在整个 Agent 执行流程中是如何配合工作的。

代码案例

我们结合 examples/03_session_notes.py 来看看 Mini Agent 的记忆机制是如何通过工具层面的设计来落地的。这个示例主要展示了两件事:一个是 SessionNoteToolRecallNoteTool 的直接用法,另一个是将它们接入 Agent 后如何实现跨会话记忆。整个示例的核心思路并不复杂:把“记忆”本身也当成一种工具能力来暴露

1. 直接使用 SessionNoteTool 与 RecallNoteTool

先看第一个演示函数 demo_direct_note_usage(),它展示了两个工具如何被独立调用:

1
2
3
4
5
6
7
8
9
10
11
12
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
note_file = f.name

# 创建两个工具,共享同一个 memory_file
record_tool = SessionNoteTool(memory_file=note_file)
recall_tool = RecallNoteTool(memory_file=note_file)

# 记录一条笔记
result = await record_tool.execute(
content="User is a Python developer working on agent systems",
category="user_info",
)

这里有一个很关键的设计细节:记忆不是存在内存里,而是写入一个 JSON 文件。这意味着即使用户关闭了程序,下次重新启动时只要指向同一个文件,记忆就能被恢复。这种方式非常直接——不依赖任何外部存储服务,只要有个文件路径就能工作。

再看 record_tool.execute() 的参数:content 是笔记的正文内容,category 则允许调用方给笔记打上分类标签。这个分类在后续检索时会非常有用,因为 Agent 并不总是需要把所有记忆都翻出来,有时候只需要定位某一类信息,比如“用户偏好”或“项目信息”。

记录完之后,就可以用 RecallNoteTool 把内容取回来:

1
2
3
4
5
6
7
# 全部召回
result = await recall_tool.execute()
print(result.content)

# 按分类召回
result = await recall_tool.execute(category="user_preference")
print(result.content)

execute() 不传参数时默认返回所有笔记;传入 category 则做过滤。这种简单的设计其实非常贴近实际使用场景——Agent 可以在 system prompt 的引导下自行决定什么时候该全量召回,什么时候只需要特定类别的记忆。

2. 记忆文件长什么样

examples/03_session_notes.py 的第 67 到 71 行,有一个展示记忆文件内容的片段:

1
2
3
4
print("\n📄 Memory file content:")
print("=" * 60)
notes = json.loads(Path(note_file).read_text())
print(json.dumps(notes, indent=2, ensure_ascii=False))

如果运行过示例,实际的 JSON 大致是这样一个结构:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"content": "User is a Python developer working on agent systems",
"category": "user_info",
"timestamp": "2026-04-03T10:30:00"
},
{
"content": "User prefers concise, well-documented code",
"category": "user_preference",
"timestamp": "2026-04-03T10:30:05"
}
]

每条笔记都是一个包含内容、分类和时间戳的简单对象。这种格式非常适合直接塞进 prompt 里让模型理解,因为它是结构化的、人类可读的,模型也能很容易地推断出哪些记忆和当前任务相关。

3. 把记忆工具接入 Agent

单独使用工具是一回事,更有意思的是让 Agent 自己决定什么时候该记录、什么时候该召回。demo_agent_with_notes() 展示了完整的做法。

第一步是准备工具列表,把记忆相关的工具和其他工具一起注册进去:

1
2
3
4
5
6
7
8
9
memory_file = Path(workspace_dir) / ".agent_memory.json"

tools = [
ReadTool(workspace_dir=workspace_dir),
WriteTool(workspace_dir=workspace_dir),
BashTool(),
SessionNoteTool(memory_file=str(memory_file)),
RecallNoteTool(memory_file=str(memory_file)),
]

这里把 SessionNoteToolRecallNoteTool 和普通文件工具放在了同一层级,说明在 Mini Agent 的框架设计里,记忆工具和其他工具是完全平等的——它们都有统一的 namedescriptionparametersexecute() 接口。

第二步是在 system prompt 里写清楚使用指南:

1
2
3
4
5
6
7
8
9
10
11
12
note_instructions = """
IMPORTANT - Session Note Management:
You have access to record_note and recall_notes tools. Use them to:
- record_note: Save important facts, preferences, decisions that should persist
- recall_notes: Retrieve previously saved notes

Guidelines:
- Proactively record key information during conversations
- Recall notes at the start to restore context
- Categories: user_info, user_preference, project_info, decision, etc.
"""
system_prompt += note_instructions

这一步其实很关键。Agent 本身并不“知道”自己有记忆能力,它只是看到自己有两个工具可用。因此,system prompt 实际上承担了“告诉 Agent 应该如何使用记忆系统”的职责。这不是框架自动做的,而是开发者通过 prompt 注入的领域知识。从这个角度看,Mini Agent 的记忆系统其实是“工具 + prompt 引导”共同配合的结果,而不是某个全知全能的内置智能。

4. 跨会话记忆如何实现的

整个示例最有价值的地方,在于它演示了“第一次会话记录、第二次会话召回”这个跨进程的记忆能力。

第一次会话中,Agent 收到了这样一条任务:

1
2
3
4
5
6
7
8
9
10
11
task1 = """
Hello! Let me introduce myself:
- I'm Alex, a senior Python developer
- I'm building an AI agent framework called "mini-agent"
- I use Python 3.12 with asyncio
- I prefer type hints and comprehensive docstrings
- My coding style: clean, functional, well-tested

Please remember this information for future conversations.
Also, create a simple README.md file acknowledging you understood.
"""

在处理这个任务的过程中,如果 Agent 主动调用了 record_note,这些信息就会被写入 .agent_memory.json。到了第二次会话,虽然是重新实例化的 Agent 对象,但由于工具指向的是同一个文件,RecallNoteTool 仍然能读到之前写入的数据:

1
2
3
4
5
6
7
8
9
10
11
12
agent2 = Agent(
llm_client=llm_client,
system_prompt=system_prompt,
tools=tools,
max_steps=10,
workspace_dir=workspace_dir,
)

task2 = """
Hello! I'm back. Do you remember who I am and what project I'm working on?
What were my code style preferences?
"""

这个场景听起来简单,但背后的意义很深:Agent 并不需要在两次会话之间保持进程,它只需要一个共享的存储介质——这里是 JSON 文件——就能延续记忆。这和人类依赖笔记本或笔记应用的场景非常类似。

5. 为什么说这是“工具化”的记忆

到这里可以回头重新审视一下这个设计的思路。

在 Mini Agent 里,记忆并不是框架内置的一个“黑盒智能功能”,而是被明确地抽象成了两个工具:record_note 负责写入,recall_notes 负责读取。它们遵循和其他工具完全一样的接口约定,暴露同样的 namedescriptionparameters 属性,并且可以被 Agent 自动选择和调用。

这种做法有几个显而易见的好处:

第一,可组合性强。 记忆工具和其他文件工具、bash 工具处于同一套工具体系里,Agent 在做决策时不需要区分“你是系统内置的还是我自己加的”,调用方式完全统一。

第二,可控性高。 记忆内容存在哪个文件、格式是什么、什么时候写入,都由开发者决定。框架本身不做隐式的记忆管理,所有记忆行为都是显式的工具调用。

第三,容易扩展。 如果未来想把记忆从 JSON 文件换成数据库或者向量存储,只需要改 SessionNoteToolRecallNoteTool 的内部实现,对外接口保持不变,Agent 代码无需修改。

这也再次呼应了我们在第二篇里讨论过的设计哲学:Mini Agent 始终倾向于把复杂能力拆解成标准化的可调用单元,记忆系统也不例外。

工具类源码解析

有了代码示例的使用场景做铺垫,接下来我们直接从源码层面理解这两个工具的实现逻辑。mini_agent/tools/note_tool.py 总共只有两百多行,结构也非常清晰,两个类分别对应两个工具,每个工具都遵循 Tool 基类定义的统一接口。

1. SessionNoteTool:把笔记写入持久化存储

先看 SessionNoteTool 的几个核心属性:

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
@property
def name(self) -> str:
return "record_note"

@property
def description(self) -> str:
return (
"Record important information as session notes for future reference. "
"Use this to record key facts, user preferences, decisions, or context "
"that should be recalled later in the agent execution chain. Each note is timestamped."
)

@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The information to record as a note. Be concise but specific.",
},
"category": {
"type": "string",
"description": "Optional category/tag for this note (e.g., 'user_preference', 'project_info', 'decision')",
},
},
"required": ["content"],
}

这段实现和之前 BashTool 等工具的思路完全一致:工具名是 "record_note",描述清晰地说明了它的用途,参数定义只要求必填的 content 和可选的 category。注意到 category 虽然是可选参数,但它在 prompt 里的描述已经给出了一个参考分类体系(user_preferenceproject_infodecision 等),这本质上是在引导模型使用更结构化的方式组织记忆。

然后看 execute() 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def execute(self, content: str, category: str = "general") -> ToolResult:
try:
notes = self._load_from_file()
note = {
"timestamp": datetime.now().isoformat(),
"category": category,
"content": content,
}
notes.append(note)
self._save_to_file(notes)

return ToolResult(
success=True,
content=f"Recorded note: {content} (category: {category})",
)
except Exception as e:
return ToolResult(success=False, content="", error=f"Failed to record note: {str(e)}")

整个写入流程非常简洁:先从文件加载已有笔记列表,追加新笔记(含时间戳),再写回文件。值得注意的是,它是先把新笔记追加到列表尾部,而不是整块重写,这在笔记量不大的时候是完全够用的。

2. 惰性初始化:文件何时真正被创建

__init___save_to_file 里藏着一个很实用的设计细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def __init__(self, memory_file: str = "./workspace/.agent_memory.json"):
self.memory_file = Path(memory_file)
# Lazy loading: file and directory are only created when first note is recorded

def _load_from_file(self) -> list:
if not self.memory_file.exists():
return []
try:
return json.loads(self.memory_file.read_text())
except Exception:
return []

def _save_to_file(self, notes: list):
self.memory_file.parent.mkdir(parents=True, exist_ok=True)
self.memory_file.write_text(json.dumps(notes, indent=2, ensure_ascii=False))

__init__ 里并没有立即创建文件或目录,而是把路径存好,等真正有笔记要写入时才通过 _save_to_file() 创建。这叫惰性初始化(lazy initialization)。

这样做有两层好处:第一,工具实例化时完全不需要关心目标路径是否存在,不会因为目录不存在就报错;第二,只有在第一次记录笔记时才会真正创建文件,节省了无谓的 IO 操作。结合 _load_from_file() 里“文件不存在就返回空列表”的设计,整个工具在没有笔记的时候几乎是零成本的。

3. RecallNoteTool:按条件检索已记录的笔记

SessionNoteTool 配对使用的是 RecallNoteTool,它负责把笔记从文件里读回来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@property
def name(self) -> str:
return "recall_notes"

@property
def description(self) -> str:
return (
"Recall all previously recorded session notes. "
"Use this to retrieve important information, context, or decisions "
"from earlier in the session or previous agent execution chains."
)

@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Optional: filter notes by category",
},
},
}

注意它的参数只有一个可选的 category,不传参数时默认返回全部笔记。description 里特意提到了"from earlier in the session or previous agent execution chains",这是在向模型说明这个工具可以召回跨会话的历史信息。

再看 execute() 的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async def execute(self, category: str = None) -> ToolResult:
try:
if not self.memory_file.exists():
return ToolResult(success=True, content="No notes recorded yet.")

notes = json.loads(self.memory_file.read_text())

if not notes:
return ToolResult(success=True, content="No notes recorded yet.")

if category:
notes = [n for n in notes if n.get("category") == category]
if not notes:
return ToolResult(success=True, content=f"No notes found in category: {category}")

formatted = []
for idx, note in enumerate(notes, 1):
timestamp = note.get("timestamp", "unknown time")
cat = note.get("category", "general")
content = note.get("content", "")
formatted.append(f"{idx}. [{cat}] {content}\n (recorded at {timestamp})")

result = "Recorded Notes:\n" + "\n".join(formatted)
return ToolResult(success=True, content=result)

召回的流程分几层:首先检查文件是否存在,其次检查是否有笔记内容,然后按 category 过滤(如果传了的话),最后把结果格式化成人类可读的文本块返回。

这里有一个细节值得注意:formatted 的输出格式是 "序号. [分类] 内容\n (recorded at 时间戳)" 的形式。这种格式对人类可读很友好,对模型来说也容易解析,因为每条笔记的结构是固定的,模型如果要在后续推理中提取某一条笔记的关键信息,完全可以从这个格式里稳定地抽出来。

4. 与 Tool 基类的对应关系

把这两个工具的实现和前面第二篇里分析的 Tool 基类对照起来看,可以更清楚地理解 Mini Agent 工具系统的一致性。

SessionNoteToolRecallNoteTool 都完整实现了 namedescriptionparametersexecute() 四个必需成员。它们不需要关心自己是被哪个模型调用的,也不需要关心调用结果最终会交给谁处理。框架只需要保证:

  • 工具可以被序列化成一个 schema(通过 to_schema()to_openai_schema()
  • 工具执行后返回 ToolResult(成功/失败/内容/错误)
  • 工具的元信息(名称、描述、参数定义)被准确传递给模型

这四个条件它们都满足了,所以它们可以无缝融入 Mini Agent 的工具体系,和 WriteToolBashTool 等没有任何区别。

总结

这一篇我们围绕 Mini Agent 的会话与记忆机制,从使用场景到源码实现做了一个完整的梳理。

首先是代码案例部分。通过 examples/03_session_notes.py,我们看到了 SessionNoteToolRecallNoteTool 是如何被直接调用的,以及它们接入 Agent 之后如何实现跨会话的记忆持久化。最值得关注的设计思路是:Mini Agent 并不在框架层面内置一个“记忆模块”,而是把记忆本身也抽象成工具——record_note 负责写入,recall_notes 负责读取,二者都遵循和其他工具完全一致的接口约定。这种“工具化”的做法让记忆系统具备了高度的可组合性和可扩展性。

然后是源码解析部分。mini_agent/tools/note_tool.py 中的两个工具类实现都非常直接:写入时通过 JSON 文件追加记录,并附上时间戳和分类信息;召回时支持全部返回和按分类过滤,最终把结构化数据格式化成人类可读、模型也容易解析的文本。惰性初始化的设计让工具实例化几乎是零成本的,而 JSON 文件作为存储介质既简单又足够满足跨会话持久化的需求。

把这两部分串起来看,可以更清晰地理解 Mini Agent 对“记忆”这件事的整体思路:它没有选择把记忆做成框架内置的隐式功能,而是选择了更透明、更可控的工具模式。这样做的好处是,所有记忆行为都是显式的工具调用,开发者完全可以控制记忆存在哪里、什么时候写入、召回后如何使用。同时,由于工具都遵循统一的抽象接口,未来把记忆后端从 JSON 文件换成数据库或者向量检索服务,也只需要改动工具的内部实现,不需要动框架代码。


Mini Agent 源码解析——3 会话和记忆
https://onlyar.site/2026/04/03/MiniMax-Agent-Guide-3/
作者
Only(AR)
发布于
2026年4月3日
许可协议