QwenPaw 记忆与对话管理架构
一、整体架构
QwenPaw 采用三层记忆设计:
| 层级 | 存储位置 | 内容 | 生命周期 |
|---|---|---|---|
| 活跃记忆 | 内存 + Session JSON | 完整对话消息(用户/Agent/工具调用/工具结果/思考过程) | 单次会话 |
| 压缩摘要 | 内存 + Session JSON | 旧消息被 LLM 浓缩后的 Markdown 摘要 | 单次会话,累积更新 |
| 长期记忆 | 工作区文件(MEMORY.md + memory/*.md) | 跨会话持久知识 | 永久 |
二、Session 管理
2.1 Session 概念
一个 Session 代表同一个用户在同一个渠道下与 Agent 的持续对话上下文:
erlang
Session(会话)
├── 第1轮:用户提问 → Agent 回答
├── 第2轮:用户提问 → Agent 思考 → 调工具 → 拿结果 → 回答
├── 第3轮:用户提问 → Agent 回答
└── ...
2.2 Session 隔离
每个 (user_id + session_id + channel) 组合对应一个独立的 JSON 文件:
{save_dir}/{channel}/{safe_uid}_{safe_sid}.json
- 不同用户、不同会话、不同渠道完全隔离
- 一个用户同一个 Agent 默认只有一个活跃 Session
- 没有历史 Session 列表,不能回溯或恢复旧 Session
2.3 Session 文件内容
csharp
{
"agent": {
"content": [
[msg_dict_1, ["marks"]],
[msg_dict_2, ["marks"]]
],
"_compressed_summary": "压缩后的摘要文本..."
},
"plan": { ... }
}
content 包含完整的对话消息:用户消息、Agent 回复、工具调用、工具结果、思考块等。
三、对话保存机制
3.1 存储位置
| 文件类型 | 路径 | 作用 |
|---|---|---|
| Session JSON | {save_dir}/{channel}/{uid}_{sid}.json |
当前活跃会话的完整快照 |
| JSONL 归档 | {dialog_path}/YYYY-MM-DD.jsonl |
被压缩掉的旧消息归档 |
| MEMORY.md | {workspace}/MEMORY.md |
长期记忆(精炼知识) |
| 每日笔记 | {workspace}/memory/YYYY-MM-DD.md |
每天的原始记录 |
3.2 保存时机
| 时机 | 触发方法 | 操作 |
|---|---|---|
| 每轮对话结束 | save_session_state() |
内存完整消息列表 + 压缩摘要 → 写入 Session JSON |
| 上下文压缩时 | mark_messages_compressed() |
旧消息写入 JSONL 归档,然后从内存删除 |
| /new 命令时 | clear_content() |
所有消息写入 JSONL 归档,然后清空内存 |
3.3 对话生命周期
scss
第 1 轮:
1. load_session_state() → 文件不存在,内存为空 []
2. 用户说 "你好"
3. Agent 回复 "你好!"
4. save_session_state() → JSON: [用户消息, Agent回复]
第 2 轮:
1. load_session_state() → 从 JSON 恢复: [用户消息1, Agent回复1]
2. 用户说 "我叫小明"
3. Agent 回复 "好的小明!"
4. save_session_state() → JSON: [消息1, 回复1, 消息2, 回复2]
... 持续积累 ...
第 N 轮(token 超限触发压缩):
1. load_session_state() → 恢复 30 条消息
2. 用户说 "帮我写个脚本"
3. pre_reasoning 钩子检测 token 超限
4. 压缩前 20 条 → 归档 JSONL + 生成摘要
5. 内存只剩 [摘要, 第21~30条, 新用户消息]
6. Agent 推理 → 调工具 → 回复
7. save_session_state() → JSON: 当前内存快照
四、对话压缩机制
4.1 触发条件
在 pre_reasoning 钩子中检查,当 token 总量超过阈值时触发:
ini
总 token = 系统提示词 token + 压缩摘要 token + 活跃消息 token
阈值 = max_input_length × compact_threshold_ratio(默认约 80%)
4.2 压缩流程
markdown
1. 拆分消息
活跃消息 → messages_to_compact(旧的)+ messages_to_keep(近期的)
分界线由 reserve_threshold_ratio(默认约 10%)决定
2. 生成摘要
创建独立的 ReActAgent,输入:
- 旧消息原文
- 之前的压缩摘要(如果有)
输出:带 ## 标题的 Markdown 摘要
3. 归档旧消息
messages_to_compact → 写入 dialog/YYYY-MM-DD.jsonl
→ 从内存中删除
4. 更新摘要
_compressed_summary = 新生成的摘要
5. 触发后台记忆总结(可选)
summarize_when_compact → 后台任务写入 MEMORY.md
4.3 摘要累积
压缩摘要是累积更新的,不是每次从零开始:
css
第1次压缩:摘要A = LLM(msg1~msg20)
第2次压缩:摘要B = LLM(摘要A + msg21~msg40) ← 摘要A 参与压缩
第3次压缩:摘要C = LLM(摘要B + msg41~msg60) ← 摘要B 参与压缩
信息逐层浓缩,但核心内容通过 LLM 总结保留。
4.4 /new 和 /clear 的区别
| 命令 | 内存消息 | JSONL 归档 | MEMORY.md |
|---|---|---|---|
/new |
清空 | 旧消息写入归档 | 后台总结追加 |
/clear |
清空 | 不写入 | 不变 |
/compact |
压缩旧的 | 旧消息写入归档 | 不变 |
五、记忆读取与保存时机
5.1 一轮对话的完整生命周期
scss
load_session_state() ← 【读取】恢复上次的对话上下文(Session JSON → 内存)
│
pre_reply 钩子 ← 【读取】搜索 MEMORY.md + memory/*.md,注入相关知识
│
ReAct 循环(推理 + 工具调用)
│
pre_reasoning 钩子 ← 如果 token 超限:
├── 压缩旧消息 → 归档 JSONL
└── summarize_when_compact → 【保存】后台总结写入 MEMORY.md
│
post_acting 钩子 ← 截断过大的工具输出
│
Agent 返回回复
│
post_reply 钩子 ← 【保存】每 N 条用户消息,自动提取记忆
│
save_session_state() ← 【保存】对话上下文写入 Session JSON
5.2 读取时机汇总
| 时机 | 方法 | 读取什么 | 目的 |
|---|---|---|---|
| 对话开始 | load_session_state() |
Session JSON | 恢复对话上下文 |
| 每轮回复前 | pre_reply → auto_memory_search() |
MEMORY.md + memory/*.md | 自动注入相关长期记忆 |
5.3 保存时机汇总
| 时机 | 方法 | 保存什么 | 写到哪里 |
|---|---|---|---|
| 对话结束 | save_session_state() |
完整消息列表 + 压缩摘要 | Session JSON |
| 上下文压缩 | mark_messages_compressed() |
被压缩的旧消息 | JSONL 归档 |
| 上下文压缩 | summarize_when_compact() |
对话总结 | MEMORY.md |
| 每 N 条用户消息 | auto_memory() |
近期对话总结 | MEMORY.md |
| /new 命令 | add_summarize_task() |
全部消息总结 | MEMORY.md |
六、长期记忆系统
6.1 文件结构
yaml
工作区/
├── MEMORY.md ← 精炼后的长期记忆(核心知识)
└── memory/
├── 2026-06-05.md ← 当天的原始记录(流水账)
├── 2026-06-06.md
└── 2026-06-07.md
6.2 MEMORY.md vs memory/YYYY-MM-DD.md
| 对比项 | MEMORY.md | memory/YYYY-MM-DD.md |
|---|---|---|
| 定位 | 长期记忆(精炼知识) | 每日笔记(原始记录) |
| 类比 | 大脑长期记忆 | 日记本 |
| 内容 | 用户偏好、重要决策、经验教训 | 当天讨论了什么、做了什么 |
| 谁写入 | Agent 主动 + Dream 整理 + 后台总结 | Agent 主动 + 后台总结 |
| 关系 | 由每日笔记提炼而来 | MEMORY.md 的上游数据源 |
6.3 写入方式
方式一:Agent 主动写入
系统提示词中明确指示 Agent:
"每次会话都是全新的。工作目录下的文件是你的记忆延续。" "写下来远比用脑子记住更好。" "主动记录 --- 别总是等人叫你记!"
Agent 在对话过程中通过 write_file / edit_file 工具自己写入文件。
方式二:后台总结任务
系统自动触发,创建一个带文件读写工具的独立 Agent,分析对话后写入记忆文件。
触发条件:
- 上下文压缩时 →
summarize_when_compact() - 每 N 条用户消息 →
auto_memory()(N 由auto_memory_interval配置) /new命令时 →add_summarize_task()
6.4 Dream 梦境优化
定期(cron 定时任务)运行一次"做梦",整理 MEMORY.md:
markdown
1. 备份 MEMORY.md → backup/
2. 创建专用 ReActAgent(DreamOptimizer)
3. 读取 MEMORY.md + 当天日志 memory/YYYY-MM-DD.md
4. 按原则整理:
- 极简去冗:只保留核心决策、用户偏好、高价值经验
- 状态覆写:新信息替换旧信息,不允许矛盾并存
- 归纳整合:相似条目合并
- 废弃剔除:删除过时内容
5. 覆写 MEMORY.md
七、记忆匹配规则
7.1 关键词提取
从用户最新消息中提取最多 100 个字符作为搜索查询,然后通过 tokenize_query() 分词:
- CJK 字符(中/日/韩文)→ 每个字作为一个 token
- 非 CJK 字符 → 按空格拆分,整个单词作为一个 token
- 混合词 → 中文字逐个拆开,英文部分保留
- 上限 50 个 token
arduino
输入:"你好 我叫小明 用python写个脚本"
分词:["你", "好", "我", "叫", "小", "明", "用", "python", "写", "个", "脚", "本"]
7.2 搜索方案 --- 混合搜索(Hybrid Search)
两路并行搜索,加权合并:
路 1:向量搜索(语义匹配,默认权重 70%)
css
查询文本 → Embedding 模型 → 向量 [0.12, -0.34, ...]
→ ChromaDB 余弦相似度搜索
→ 返回语义最相近的文档片段
- MEMORY.md 和 memory/*.md 在写入时被分块(Chunk)
- 每个块通过 Embedding 模型转成向量存入 ChromaDB
- 搜索时把查询也转成向量,用余弦距离计算相似度
- 距离转分数:
score = 1.0 - distance / 2.0
需要配置 Embedding API(base_url + model_name),否则不启用。
路 2:关键词搜索(全文匹配,默认权重 30%)
bash
查询分词 → ChromaDB $contains 子串匹配(大小写不敏感)
→ 计算匹配词占比得分
打分算法:
ini
匹配词数 = 文档中包含多少个查询词
base_score = 匹配词数 / 总查询词数
完整短语匹配加分 = +0.2
final_score = min(1.0, base_score + phrase_bonus)
合并规则
ini
vector_weight = 0.7
text_weight = 0.3
# 同一文档在两路都命中
final_score = vector_score × 0.7 + keyword_score × 0.3
# 只在一路命中
final_score = 单路 score × 对应权重
# 按 final_score 降序排列,取 top N(默认 5 条)
# 过滤掉 score < 0.1 的结果
7.3 存储后端
| 后端 | 条件 | 搜索能力 |
|---|---|---|
| ChromaDB | 非 Windows + chromadb 可导入 | 向量搜索 + 关键词搜索 |
| Local | ChromaDB 不可用 | 仅关键词搜索 |
7.4 注入方式
搜索结果不是直接拼接到系统提示词中,而是伪装成 Agent 自己的操作注入到对话历史:
arduino
assistant 消息: "Searching memory for relevant context..."
tool_result 消息: [匹配到的记忆片段,含路径、行号、内容]
这样 Agent 在对话中看到的是"我之前查过记忆",而不是系统注入的内容。
八、核心设计理念
8.1 "每次会话都是全新的"
Agent 不依赖内存中的对话历史做长期记忆,而是把所有重要的东西写到文件里。好处:
- 不丢失 --- 对话上下文会被压缩、清空,但文件永远在
- 可搜索 --- 通过语义搜索精准找到相关记忆,比翻长对话高效
- 可维护 --- Dream 机制定期整理,避免 MEMORY.md 膨胀
- 可移植 --- 记忆就是 Markdown 文件,可以复制、备份、版本管理
8.2 单 Session 滚动
- 一个用户 + 一个 Agent + 一个渠道 = 一个 Session
/new不是新建 Session,而是清空当前 Session 重新开始- 没有历史 Session 列表,不能回溯旧会话
- 通过 MEMORY.md 实现跨会话记忆
九、Skill 技能系统
9.1 什么是 Skill
Skill 是以 SKILL.md 文件形式定义的扩展能力,存放在工作区的子目录中。每个 Skill 目录包含一个 SKILL.md 文件,用 YAML frontmatter 描述元信息,正文描述使用方法。
9.2 SKILL.md 文件格式
yaml
---
name: web_search
description: 搜索互联网获取最新信息
---
## 使用方法
1. 使用 search_web(query) 工具进行搜索
2. 读取返回的搜索结果
3. 总结关键信息回答用户
9.3 注册流程
markdown
启动时:
1. 扫描工作区下的 Skill 目录
2. 读取每个 SKILL.md 的 YAML frontmatter(name, description)
3. toolkit.register_agent_skill() 注册到工具箱
4. 工具箱自动生成技能描述 prompt
9.4 注入方式 --- 系统提示词
Skill 不是 作为工具定义(function calling)提交给 LLM 的,而是作为系统提示词中的描述文本:
csharp
# Agent Skills
## web_search
搜索互联网获取最新信息
Check "/path/to/skills/web_search/SKILL.md" for how to use this skill.
Agent 看到的只是"你有这个技能,详细用法请读 SKILL.md"。需要使用时,Agent 通过 read_file 工具主动读取 SKILL.md 获取完整说明。
设计意图:节省系统提示词的 token 开销,只在需要时才加载完整技能说明。
十、MCP(Model Context Protocol)工具
10.1 什么是 MCP
MCP 是一种标准化的外部工具接入协议。通过 MCP,Agent 可以调用外部服务提供的工具(如网页搜索、数据库查询、API 调用等)。
10.2 注册流程
vbscript
启动时:
1. 读取配置中的 mcp_servers 列表
2. 为每个 MCP server 创建 Client 连接
3. 获取该 server 提供的所有工具列表(名称、参数 Schema、描述)
4. toolkit.register_mcp_client(client) 注册到工具箱
5. MCP 工具变成标准的 tool function
10.3 注入方式 --- 工具定义(Function Calling)
MCP 工具以标准的 tools JSON Schema 形式提交给 LLM:
json
{
"tools": [
{
"type": "function",
"function": {
"name": "mcp_web_search",
"description": "搜索互联网获取信息",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "搜索关键词" }
},
"required": ["query"]
}
}
}
]
}
MCP 工具和内置工具(write_file、read_file、execute_shell 等)享有同等地位,LLM 可以直接通过 function calling 调用,无需额外步骤。
10.4 Skill vs MCP 对比
| 对比项 | Skill | MCP |
|---|---|---|
| 定义方式 | SKILL.md 文件(YAML + Markdown) | 外部 MCP Server 协议 |
| 提交方式 | 系统提示词文本 | tools JSON Schema(Function Calling) |
| LLM 看到的 | 技能名称 + 描述 + SKILL.md 路径 | 完整的工具定义(名称、参数、描述) |
| 调用方式 | Agent 需先 read_file 读取 SKILL.md,再间接使用 | 直接 function calling |
| 扩展性 | 本地文件,适合文档类技能 | 外部服务,适合 API 类工具 |
| 典型场景 | "你会写代码"、"你会做数据分析" | 网页搜索、数据库查询、发送消息 |
十一、提交给大模型的完整内容结构
11.1 请求结构总览
每次 Agent 推理时,提交给 LLM 的内容包含两部分:
ini
提交给 LLM 的请求
├── messages: [消息列表] ← 对话上下文
│ ├── [0] system message ← 系统提示词
│ ├── [1] 压缩摘要(如有) ← assistant 角色注入
│ ├── [2] 记忆搜索结果(如有) ← assistant + tool_result 伪装
│ ├── [3..N] 活跃对话消息 ← 真实对话历史
│ └── [N+1] 最新用户消息 ← 当前输入
│
└── tools: [工具定义列表] ← Function Calling 工具
├── 内置工具 ← write_file, read_file, execute_shell 等
└── MCP 工具 ← 外部服务提供的工具
11.2 系统提示词(System Message)详细结构
系统提示词由多个部分拼接而成:
csharp
# {AGENTS.md 文件内容}
(包含工作流、规则、指南;已过滤 heartbeat 和 memory 标记段)
# {SOUL.md 文件内容}
(核心身份和行为原则)
# {PROFILE.md 文件内容}
(Agent 身份和用户画像)
{记忆引导 prompt}
(告诉 Agent 要主动写入 MEMORY.md,"每次会话都是全新的")
# Agent Skills
## web_search
搜索互联网获取最新信息
Check "/path/to/SKILL.md" for how to use this skill.
## code_executor
执行代码片段
Check "/path/to/SKILL.md" for how to use this skill.
{多模态提示}
(如果模型不支持多模态,提示 Agent 告知用户)
11.3 消息列表(Messages)完整结构
swift
[
// ===== 1. 系统提示词 =====
{
"role": "system",
"content": "# AGENTS.md\n...(完整系统提示词)..."
},
// ===== 2. 压缩摘要(如果有被压缩过的消息)=====
{
"role": "assistant",
"content": "Searching memory for relevant context..."
},
{
"role": "tool_result", // 伪装成工具结果
"content": "## 压缩摘要\n\n之前的对话要点总结...(Markdown 格式,带 ## 标题分段)"
},
// ===== 3. 记忆搜索结果(如果匹配到相关长期记忆)=====
{
"role": "assistant",
"content": "Searching memory for relevant context..."
},
{
"role": "tool_result",
"content": "找到的记忆片段:\n- {文件路径}:{行号}\n {内容}\n- ..."
},
// ===== 4. 活跃对话历史 =====
{
"role": "user",
"content": "你好"
},
{
"role": "assistant",
"content": "你好!有什么可以帮你的?"
},
{
"role": "user",
"content": "帮我写个 Python 脚本"
},
// ... Agent 的 ReAct 循环中间步骤(思考、工具调用、工具结果)...
{
"role": "assistant",
"content": "好的,我已经为你创建了脚本..."
},
// ===== 5. 最新用户消息 =====
{
"role": "user",
"content": "帮我创建一个文件"
}
]
11.4 工具定义(Tools)结构
css
[ { "type": "function", "function": { "name": "read_file", "description": "Read the content of a file at the given path", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "File path to read" } }, "required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write content to a file at the given path",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path to write" },
"content": { "type": "string", "description": "Content to write" }
},
"required": ["path", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "execute_shell",
"description": "Execute a shell command",
"parameters": {
"type": "object",
"properties": {
"command": { "type": "string", "description": "Shell command to execute" }
},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "memory_search",
"description": "Search long-term memory for relevant information",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "mcp_web_search",
"description": "Search the internet (from MCP server)",
"parameters": { "..." : "..." }
}
}
]
11.5 ReAct 循环中的消息流转
css
第1次 LLM 调用:
输入 → [system + 摘要 + 记忆 + 对话历史 + 用户消息]
输出 → thought: "需要先读取文件" + tool_call: read_file("config.json")
第2次 LLM 调用:
输入 → [system + 摘要 + 记忆 + 对话历史 + 用户消息
+ assistant(思考+工具调用) + tool_result(文件内容)]
输出 → thought: "文件内容是..." + tool_call: write_file("output.txt", ...)
第3次 LLM 调用:
输入 → [... + 之前所有步骤 ...]
输出 → 最终回复文本(不再调用工具)
最多循环 max_iters(默认 10)次。
11.6 内容结构汇总图
sql
┌─────────────────────────────────────────────┐
│ LLM API 请求 │
├─────────────────────────────────────────────┤
│ │
│ messages: │
│ ┌─ system ─────────────────────────────┐ │
│ │ AGENTS.md(工作流、规则) │ │
│ │ SOUL.md(身份、原则) │ │
│ │ PROFILE.md(画像) │ │
│ │ 记忆引导 prompt │ │
│ │ Skill 描述列表 │ │
│ │ 多模态提示 │ │
│ └──────────────────────────────────────┘ │
│ ┌─ assistant + tool_result ────────────┐ │
│ │ 压缩摘要(累积更新) │ │
│ └──────────────────────────────────────┘ │
│ ┌─ assistant + tool_result ────────────┐ │
│ │ 长期记忆搜索结果(MEMORY.md 匹配) │ │
│ └──────────────────────────────────────┘ │
│ ┌─ user / assistant / tool* ───────────┐ │
│ │ 活跃对话历史(近期消息) │ │
│ └──────────────────────────────────────┘ │
│ ┌─ user ───────────────────────────────┐ │
│ │ 当前用户消息 │ │
│ └──────────────────────────────────────┘ │
│ │
│ tools: │
│ ┌─ 内置工具 ──────────────────────────┐ │
│ │ read_file, write_file, edit_file │ │
│ │ execute_shell, list_directory │ │
│ │ memory_search, memory_get │ │
│ └──────────────────────────────────────┘ │
│ ┌─ MCP 工具 ──────────────────────────┐ │
│ │ mcp_web_search, mcp_* │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘