Datawhale Easy Data × AI:构建知识与记忆驱动的 Agent笔记8

Datawhale Easy Data × AI:构建知识与记忆驱动的 Agent笔记8

Task 8 · D4(Agent 记忆系统开发)+ I7(PowerContext 设计与实现)· 打卡笔记


📚 D4 学习笔记:Agent 开发与记忆系统

Agent 的两种架构本质区别

维度 单次 API 调用(D1-D2) Agent 推理循环(ReAct,D4)
调用方式 llm.chat() 一次返回 for step in range(max_steps): 多步循环
工具调用 单次 Tool Use 可能多步、多次、并行
结果驱动 直接消费 工具结果反馈回 Agent 进入下一轮推理
安全阀 无特殊需要 max_steps 限制防止无限循环
适用场景 RAG 问答、单轮任务 复杂问题拆解、多工具协同

ReAct 循环伪代码(I6 第 5 节的动手练习代码 1 对应这里):

python 复制代码
def agent_loop(user_input, tools, max_steps=10):
    messages = [system_prompt, user_input]
    for step in range(max_steps):
        response = llm.chat(messages=messages, tools=tools)
        if response.has_tool_calls():
            for tool_call in response.tool_calls:
                result = execute_tool(tool_call)
                messages += [tool_call_msg, tool_result_msg(result)]
        else:
            return response.content
    return "抱歉,无法在有限步骤内完成。"

四种记忆在 ReAct 循环中的位置

复制代码
┌──────── 推理循环之前(Prompt 注入阶段)────────┐
│                                               │
│  ① 程序记忆 → System Prompt(行为规则)        │
│  ② 语义记忆 → search_memory(query=...)        │
│               检索用户事实和偏好注入 System    │
│  ③ 情景记忆 → search_episodes(query=...)      │
│               检索历史成功经验作为 few-shot    │
│                                               │
└──────────────────────┬────────────────────────┘
                       │
                       ▼
          ┌──── ReAct 推理循环 ────┐
          │   agent_loop(...)      │
          └───────────┬────────────┘
                      │
          ┌───────────▼────────────┐
          │ 推理后(提炼存储阶段)   │
          │ extract_facts(...)      │
          │ save_memory(facts)      │
          └────────────────────────┘

Note :所有长期记忆都是在 Agent 推理循环之前被注入到 System Prompt 中的。它们的作用是让 Agent 在开始推理时就已经"认识"这个用户、"记得"过去的经验。推理循环结束后再提炼新事实存入记忆库。

语义记忆的三个核心操作

操作 代码函数 实现机制 D4 对应函数
提炼(记) extract_facts_from_conversation LLM 从对话中 JSON 数组格式返回结构化事实 d4_3: 8~12 条事实提炼
检索(想起) search_memory 向量检索 + 时效性权重(艾宾浩斯简化版) query_texts=[query] + math.exp(-age_days/30)
降权(忘) age_days / 30 指数衰减 30 天半衰期 + access_count × 0.1 加分 recency_weight + access_bonus

艾宾浩斯简化公式(d4_3 第 228 行):

python 复制代码
age_days = (current_time - metadata["created_at"]) / 86400
recency_weight = math.exp(-age_days / 30)  # 30天半衰期
access_bonus = metadata.get("access_count", 0) * 0.1  # 每次被想起加分
weight = recency_weight + access_bonus

🔑 关键代码 + 运行输出(Windows 适配版)

环境适配说明 :课程原代码依赖 pyseekdb embedded client(只支持 Linux),Windows 上不可用。本笔记使用 ChromaDB + 自定义 seekdb_runtime 兼容层 实现完全等价的 API。

代码 1:ReAct 推理循环(I6 第 5 节第 3 小节动手练习)

python 复制代码
# d4_1_react_loop.py --- Agent 自主决定调用工具、调用几次
answer = agent_loop(
    "我想从北京去东京旅游,帮我查一下两个城市的天气,以及人民币兑日元的汇率。"
)

运行输出(2026-10-09 重跑):

复制代码
============================================================
ReAct 推理循环演示
============================================================

【测试 1】简单问题:单次工具调用
----------------------------------------
  [开始] 用户提问:北京今天天气怎么样?
  [步骤 1] 调用工具:get_weather({'city': '北京'})
           结果:晴天,气温 22°C,湿度 45%
  [步骤 2] Agent 给出最终回答(共 2 步)
  回答:北京今天天气如下:
- **天气状况**:晴天 ☀️
- **气温**:22°C
- **湿度**:45%
今天北京天气晴朗,气温舒适,湿度适中,非常适合外出活动。不过晴天紫外线可能较强,建议外出时做好防晒措施。

【测试 2】复合问题:需要多次工具调用
----------------------------------------
  [开始] 用户提问:我想从北京去东京旅游,帮我查一下两个城市的天气,以及人民币兑日元的汇率。
  [步骤 1] 调用工具:get_weather({'city': '北京'})    ← 并行 3 次工具调用
           结果:晴天,气温 22°C,湿度 45%
  [步骤 1] 调用工具:get_weather({'city': '东京'})
           结果:小雨,气温 18°C,湿度 85%
  [步骤 1] 调用工具:get_exchange_rate({'from_currency': 'CNY', 'to_currency': 'JPY'})
           结果:1 CNY = 20.83 JPY
  [步骤 2] Agent 给出最终回答(共 2 步)
  回答:以下是您从北京去东京旅游所需的查询结果:

## 🌤️ 天气情况
| 城市 | 天气 | 气温 | 湿度 |
|------|------|------|------|
| **北京** | ☀️ 晴天 | 22°C | 45% |
| **东京** | 🌧️ 小雨 | 18°C | 85% |
- **北京**:天气晴朗,气温舒适(22°C),湿度较低(45%),出行体感干爽。
- **东京**:正在下小雨,气温稍凉(18°C),湿度较高(85%),建议携带雨具和一件薄外套保暖。

## 💱 汇率信息
**1 人民币(CNY)= 20.83 日元(JPY)**
也就是说,如果您兑换 ...

【测试 3】闲聊问题:不需要工具
----------------------------------------
  [开始] 用户提问:你好,你能做什么?
  [步骤 1] Agent 给出最终回答(共 1 步)   ← 自主决定不调工具
  回答:你好!我是一个智能助手,可以帮你查询两类信息:
1. **天气信息** ------ 查询指定城市的当前天气,包括天气状况、气温和湿度。比如:北京、上海、东京、纽约等城市。
2. **汇率信息** ------ 查询两种货币之间的汇率,支持美元(USD)、人民币(CNY)和日元(JPY)。
如果你的问题需要同时用到这两类信息,我也可以分步查询后综合回答你。
有什么我可以做的?

============================================================

关键要点:

  1. Agent 的核心是一个推理循环,不是单次 API 调用
  2. Agent 自主决定调用哪个工具、调用几次
  3. max_steps 是安全阀,防止 Agent 无限循环
  4. 每次工具调用的结果都会反馈给 Agent,驱动下一轮推理

Note:

  • 测试 2:Agent 在同一步里并行调用了 3 个工具(北京天气、东京天气、CNY→JPY 汇率)
  • 测试 3:Agent 自主判断闲聊不需要工具,1 步直接返回回答
  • max_steps=5 安全阀防止无限循环

代码 2:无记忆 vs 有记忆 效果对比(I6 第 5 节第 5 小节动手练习)

同样的四轮对话,两个版本的回答对比:

轮次 用户输入 无记忆 Agent 有记忆 Agent
1 我是 Python 后端,喜欢简洁 明白 好的,记住了
2 推荐 Web 框架 罗列 Spring/Express/Django/Rails... 直接推荐 FastAPI,备选 Django/Flask
3 加缓存 泛泛而谈各种缓存方案 推荐 Redis + redis-py,Python 生态
4 继续昨天话题,选数据库 "我没有昨天的记录" (跨会话持久化后记得)

运行的记忆提炼(d4_3 对话第 1 轮):

复制代码
  [记忆] 存入 3 条新记忆:['用户是 Python 开发者', '用户主要做后端开发', '用户喜欢简洁的回答']

第 2 轮 System Prompt 自动注入:

python 复制代码
memory_context = "\n".join([
    "- 用户是 Python 开发者",
    "- 用户主要做后端开发",
    "- 用户喜欢简洁的回答"
])
system_prompt = f"""你是一个友好的技术助手。...你对这位用户有以下了解:
{memory_context}
请根据你了解的信息,提供个性化的、有针对性的回答。"""

代码 3:跨进程持久化验证(d4_4)

写入阶段(进程 1)--- 5 轮对话 → 9 条记忆:

复制代码
>>> 首次运行,记忆库已创建
  [记忆提炼] ['用户是一名 Python 开发者', '用户在一家做 SaaS 产品的创业公司工作']
  [记忆提炼] ['用户喜欢简洁的回答,不需要太多解释']
  [记忆提炼] ['用户所在的团队之前尝试过 RabbitMQ', '用户认为 RabbitMQ 配置太复杂']
  [记忆提炼] ['用户在做 SaaS', '用户之前嫌 Celery 配置烦']
--- 写入阶段完成,当前记忆库 ---
  [记忆库] 共 9 条记忆

读取阶段(全新进程 2 --- 重新启动程序):

复制代码
>>> 已加载历史记忆库,当前记忆数量:9    ← 不是"首次运行"!
  [记忆库] 共 9 条记忆

--- 验证查询 ---
用户:我之前说过我在哪种公司工作来着?
Agent:(从持久化记忆中检索出 SaaS 创业公司)

--- 持久化验证总结 ---
记忆库总数(含本轮新提炼):16
关键记忆验证:
  ✓ SaaS 创业公司:True
  ✓ Python 开发者:True
  ✓ 讨厌 RabbitMQ:True
  ✓ 喜欢简洁:True

为什么能持久化 :ChromaDB PersistentClient(path=...) 将记忆自动写入磁盘目录(memory_persistent.db/),进程结束时无需显式关闭,下次启动 has_collection() 返回 True → get_collection() 加载已有集合 → 记忆在。

代码 4:时效性降权(艾宾浩斯简化)

python 复制代码
# d4_3 第 226-230 行
age_days = (current_time - metadata["created_at"]) / 86400
recency_weight = math.exp(-age_days / 30)   # 30 天半衰期
access_bonus = metadata.get("access_count", 0) * 0.1
weight = recency_weight + access_bonus
记忆年龄 recency_weight access_count=0 access_count=5 access_count=10
1 天 0.967 0.967 1.467 1.967
7 天 0.794 0.794 1.294 1.794
30 天 0.500 0.500 1.000 1.500
60 天 0.250 0.250 0.750 1.250
90 天 0.125 0.125 0.625 1.125

含义:30 天前的记忆自然衰减一半,但如果在这 30 天里被检索了 10 次(每次 +0.1),总分 1.250 → 反而比新记忆更重。这就是 P3 讲的"经常被想起的记忆不会真正遗忘"的工程落地。


🏗️ I7 学习笔记:PowerContext 的设计与实现

一图总览:核心数据流

复制代码
Source → Artifact ← Trigger

Source(来源)         Artifact(产物)          Trigger(触发)
┌──────────────┐    ┌───────────────────┐    ┌────────────────┐
│ 材料快照     │    │ Memory(事实/决定)│    │ 按事件+状态    │
│ 外部引用     │───▶│ Topic Memory(主题)│───▶│ 决定是否处理   │
│ Task Outcome │    │ Handoff(交接)    │    │                │
└──────────────┘    │ Profile(背景)    │    └────────────────┘
                    │ Experience(经验)│
                    └───────────────────┘

写入路径取决于内容是否明确

I7 课程第 1 节的三条主要写入路径:

text 复制代码
Explicit entries ----------------------> Memory    ← 直接写入(已确认内容)

Source --+--> Extract ------------------> Memory    ← 提取路径(原始材料)
         |
         +--> Topic processing ---------> Topic Memory  ← 多材料共同主题

Task Outcome --> Candidate --approve--> Experience  ← 审核后才成为经验

关键设计决策:Task Outcome 也是 Source,它生成的是 Candidate(待审核提案),只有经过 ReviewService.approve() 批准后才变成正式 Experience。这保证了"未经证实的做法不会被自动写成成功经验"。

共用引用机制,各自决定如何使用

为什么要共用引用? 因为修订会生成新版本,但旧版本仍需精确读取。引用与内容一起流转,让后续操作有确定的核对对象。

ArtifactRef 三元组(I6 动手练习里见过):

python 复制代码
# family + artifact_id + revision 定位修订
ArtifactRef(family="memory", artifact_id=..., revision=...)

# MemoryCitation 在此基础上再加条目级粒度
MemoryCitation(
    artifact_ref=ArtifactRef(...),
    entry_id=...,           # 条目的稳定身份(修订前后不变)
    entry_version_id=...    # 每次修订变化
)

PreparedContext 装配流程(I7 第 3 节)

复制代码
Recall hits + Profile snapshot
              │
assembly ───▶ Select         # 按类型选择,用引用去重
              │
max_bytes ──▶ Fit            # 裁剪时保留引用和可读内容
              │
              ▼
            Render           # 渲染成可交付文本(含不可信历史标记)
              │
              ▼
       PreparedContext       # status=ready/empty,content_bytes 精确

去重为什么用引用而不是正文相似度?

  • 同一内容的两个版本即使文字相近,也有不同的 entry_version_id
  • 不同 revision 是否参与本次请求由准入规则决定,不是靠相似度猜测

预算裁剪的两条硬规则(I7 第 3 节)

  1. 先测后装:放入一条候选前先测完整输出(正文+引用+格式)是否还能放进预算
  2. 短条目不被长条目挡:靠前的长条目放不下 → 放弃它,继续试后面的短条目

I6 动手练习 6.1 对照实验(已在 Task 7 运行):

  • max_bytes=8000 → 正常装配返回 status=ready
  • max_bytes=512 → 如果连必要结构都装不下,返回 status=empty, content=null

交接与经验的不同完成条件(I7 第 4 节)

python 复制代码
# 交接可以走两条路:
# 路径 A:临时读取
handoff = handoff_service.prepare(target, evidence)
ready_handoff = handoff_service.finalize(handoff)
continue_handoff(prepared=ready_handoff)   # 只读,不保存修订

# 路径 B:持久提交
revision_ref = handoff_service.commit(ready_handoff)  # 保存修订
continue_handoff(revision=revision_ref, selection="exact")  # 新进程也能读到

为什么要分 prepare/finalize/commit 三步?

  • prepare:根据证据生成草稿,可能需要人工核对
  • finalize:校验证据完整性,返回 PreparedHandoff(含版本基线)
  • commit:持久化修订,系统检查基线是否仍有效(防止旧交接覆盖新进度)

实验:核对上下文接口的行为(I7 第 6 节)

6.1 对照预算,检查装配结果
bash 复制代码
# 8000 字节预算(默认)
curl -d '{"scope_id":"...","query":"Context lab entry",
  "max_bytes":8000,"assembly":{"sections":[{"family":"memory","limit":3}]}}'
  http://127.0.0.1:8000/v1/context/prepare
# → status=ready, content_bytes ≤ 8000

# 512 字节(最小允许值)
curl -d '{"scope_id":"...","query":"Context lab entry",
  "max_bytes":512,...}'
# → 如果装不下 → status=empty, content_bytes=0

要核对的 4 个字段:

字段 预期值 含义
schema powercontext.prepared-context.v1 数据版本标识
status ready 或 empty 有无可交付内容
content_bytes ≤ max_bytes 的精确 UTF-8 字节数 包含引用和格式
content 含历史材料标记 + 精确引用 缩短的是正文,不是引用
6.2 临时交接 vs 持久交接
bash 复制代码
# prepare → finalize:临时读取(不提交)
POST /v1/work/handoff_current_work
POST /v1/work/continue_handoff {"selection":"prepared","prepared":...}
# → 能读到,但没有新 revision

# commit:持久保存
POST /v1/work/commit_handoff
POST /v1/work/continue_handoff {"selection":"exact","revision":"..."}
# → 新会话也能精确读到同一份交接
6.3 停用条目,再读取原引用

三条验证语句:

读取方式 预期结果
search_memory(query=关键词) 不再返回被停用的条目
list_memory_entries(include_inactive=true) 该条目状态 = inactive
get_memory_entry(citation=旧引用) 仍能读到当时的正文和状态

为什么第三条能行? 停用控制的是当前召回 ,精确引用保留历史核对。这就是 Task 7 里代码 4-5 验证的"entry_id 不变、entry_version_id 变化、旧 citation 仍可读"的工程落地。

实验:矛盾条目的处理(可选 6.4)

PowerContext 不会自动裁决冲突。它的设计哲学是:

  1. 两条互不兼容的约束 → 都作为正式 Memory 保存
  2. 搜索时两条都返回,让 Agent 或用户决定用哪条
  3. 最终选择通过 revise + retire 执行(不是系统自动决策)

🌳 一张图收束:记忆系统的完整闭环

复制代码
写 ──▶ 提炼(LLM JSON 提取)
  │
  ├──▶ 压缩(事实化,20:1 压缩比)
  │
  ├──▶ 存储(向量 + 元数据,含 created_at/access_count)
  │
  ├──▶ 热层(近 7~30 天活跃)  ← 为延迟买单
  │
  ├──▶ 温层(偶发被问起)
  │
  ├──▶ 冷层(长期未访问)      ← 为容量买单
  │
  ├──▶ 定期清理
  │       ├── dry-run 先观测
  │       ├── 降级权保留  ← 硬淘汰只删低价值+零访问+非保护
  │       └── 级联清理(主库+向量+缓存)
  │
  └──▶ 命名空间隔离
          └── WHERE user_id='xxx' (查询时强过滤)

记 ←── 推理后提炼 + save_memory()
  │
  ▼
起 →── 推理前 search_memory() 注入 System Prompt
  │
  ▼
忘 →── 时效性降权 + 冷热分层 + 定期清理

🔧 Windows 环境适配

问题 根因 解决方案
seekdb_runtime 模块不存在 课程代码引用 seekdb 包装层,但 pyseekdb 1.4.0 没有这个模块 自定义 code/seekdb_runtime.py 用 ChromaDB 实现等价 API
pyseekdb embedded client Windows 不可用 pylibseekdb 只在 Linux 上发布 改用 ChromaDB PersistentClient
Config.require_api_key 不存在 config.py 只有 check_api_key,课程代码调用 require_api_key 给 config.py 加 require_api_key 方法
模型名硬编码 SiliconFlow 别名 deepseek-ai/DeepSeek-V3 是 SiliconFlow 别名 4 个 d4_*.py 全改 MODEL = "deepseek-chat"
API Provider 硬编码 SILICONFLOW Config.SILICONFLOW_API_KEY 和 Config.SILICONFLOW_BASE_URL 写死 PowerShell 脚本里设 $env:SILICONFLOW_API_KEY = $env:DEEPSEEK_API_KEY + $env:SILICONFLOW_BASE_URL = "https://api.deepseek.com/v1"
ChromaDB 下载默认 embedding 模型 首次 query 时自动下载 all-MiniLM-L6-v2 (80MB) 第一次跑一次后模型缓存在 ~/.cache/chroma/,后续秒开
wrapper 初版 close() 调 reset() reset 清掉所有数据,持久化验证全 False 改成 pass,ChromaDB PersistentClient 自动持久化

💡 Note

  1. 记忆系统 = 数据生命周期闭环(D4 第 6 节)。"记/想/忘" 全是数据操作:提炼是结构化、想起是检索、忘是生命周期管理。LOCOMO 基准上有选择地记忆(78.7%)远好于全量死记(52.9%)。

  2. 跨进程持久化不是"特性",是验证门槛 (d4_4 输出)。进程 1 写入 9 条记忆 → 进程 2 启动时 has_collection() 返回 True → get_collection() 加载 → 4 个关键记忆全 True。如果没有这一步验证,任何"持久化"都是纸上谈兵。

  3. PreparedContext 的预算裁剪比检索排序更难 (I7 第 3 节)。检索只要排序,装配还要按类型去重、按预算裁剪、按顺序让后面的短条目不被前面的长条目挡。status=empty 不等于"没检索到内容",可能只是预算不够。

  4. 停用 ≠ 删除的工程含义(I7 第 6.3 节)。Memory 条目有三种操作:

    • retire → 停用(退出当前召回,旧 citation 仍可读)
    • revise → 修订(同 entry_id,新 entry_version_id)
    • delete → 删除(物理清除,所有引用失效)
      实际产品中 revise + retire 是最常用的组合。
相关推荐
Fibocom广和通1 小时前
IMC 2026|从连接到规模部署,广和通深化印度IoT市场布局
人工智能
蜗牛互联网1 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端
华微软件1 小时前
华微软件 AI 领域新增一项国家发明专利
人工智能
陈年老古董1 小时前
Hive 函数学习笔记(下):窗口函数与经典案例
hive·笔记·学习
jimmyleeee2 小时前
大模型安全之四十五:从数据到输出----GenAI 版权、知识产权与伦理合规实战指南
人工智能·深度学习·安全
loulanyue_2 小时前
构建实时沉浸式世界模型体验:AI+IP与商业模式重构——读Reactor首席技术官Bryce 2026云栖专场演讲
人工智能·重构
面包狗AI4S2 小时前
【AI4S】生化环材高可信技术与产业周报(2026-10-03—2026-10-09)
人工智能·深度学习·机器学习·ai
前端大斗师2 小时前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
刘科领2 小时前
使用ollama & openweb-ui 本地搭建自己的AI
人工智能·python·ui·ai