第 39 章 · Agno Storage、Memory 与 State
关掉页面,它就把你忘得一干二净
还是那个跨境卖家。上一章他给 Research Agent 接上了知识库,问“含锂电池玩具能不能发德国”,Agent 能翻手册、能引条款,答得有理有据。他很满意,第二天打开电脑继续昨天的选品讨论——结果 Agent 一脸茫然:不记得他是谁、不记得昨天聊到哪、更不记得他反复强调过“报告要中文、只看欧盟市场”。每次对话都像第一次见面。
问题不在模型笨,而在于它没有记忆。前两章的工具和知识库,能力都只在“当前这一次调用”里有效:函数返回了、检索命中了,一旦这次 run 结束,用户是谁、聊过什么、任务进行到哪一步,全部随进程消散。一个记不住事的 Agent,永远只能当一次性问答机器,做不成能托付业务的长时系统。
这正是第 8、9、11 章反复处理的问题:上下文的持久化与记忆的分层。第 12 章讲多轮会话如何保存和恢复,第 8、9 章讲上下文里什么该留、什么该丢。这一章要做的,是看 Agno 用 Storage、Memory、State、Context 四件工具,把这套模式落成可运行的代码——并且讲透一个最容易搞混的问题:这四个东西到底各管什么。
四个概念,一句话辨析
初学者最大的困惑是 Storage、Memory、State、Context 听起来都像“记东西”,于是随手乱塞,最后 Memory 里堆满过期资料、State 污染进用户画像、Context 爆了窗口。其实它们回答的是四个完全不同的问题,记住这组对应关系,后面就不会错:
- Storage = 聊过什么:这次会话的完整消息记录,用来让系统能恢复、回放、审计。对应第 12 章的会话持久化。
- Memory = 用户是谁:跨会话仍有价值的长期偏好和稳定事实,比如“这个卖家只做欧盟、报告要中文”。对应第 8、9 章讲的“长期记忆”。
- State = 进行到哪:当前这个任务跑到第几步、查过哪些来源、审批过没有。是运行中的机器状态,任务结束就该丢。
- Context = 本次额外参考:这一轮临时注入给模型看的资料,比如当前店铺、当前品类、刚检索命中的片段。不一定持久,也不一定值得记住。
一个生活化的类比:Storage 是聊天记录,Memory 是通讯录里对这个人的备注,State 是这次通话的便签,Context 是通话时手边翻开的那份文件。四者独立又配合,混用就是灾难。下面逐个落到 Agno 代码上。
Storage:让会话能被恢复
Storage 负责“这次会话发生了什么”。给 Agent 挂上 SqliteStorage,配合 add_history_to_messages,它就能在下次以同一个 session_id 启动时,把最近几轮对话自动拼回上下文:
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.storage.sqlite import SqliteStorage
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
user_id="seller_001", # 谁在用(贯穿会话与记忆的主键)
session_id="research_2026_07_04", # 哪一次会话(同 id 才能续上)
storage=SqliteStorage(
table_name="agent_sessions",
db_file="tmp/agents.db", # 会话落地到本地 SQLite
),
add_history_to_messages=True, # 把历史消息自动拼进本轮提示
num_history_responses=3, # 只回填最近 3 轮,控制窗口和成本
instructions="你是跨境电商研究助手,回答时参考最近对话。",
markdown=True,
)
# 第二天用同一个 session_id 再跑,Agent 就能接上昨天的对话
agent.print_response("接着昨天的选品,德国那边还有别的品类值得看吗?", stream=True)
这和第 12 章的会话持久化是同一件事。要抓住一个关键点:Storage 的价值不是“让模型自动记住一切”,而是让系统能恢复、回放、审计。存进 SQLite 的是原始消息流,模型并不会自动“理解”它——真正让模型看见历史的是 add_history_to_messages 这个开关,而 num_history_responses 决定回填多少。存下来和喂进去是两件事,别混。
Memory:让它记住用户是谁
Storage 存的是“说过的话”,Memory 存的是“从话里提炼出的、关于这个用户的长期事实”。开启 enable_user_memories 后,Agno 会在对话中自动抽取值得长期记住的偏好,写进独立的 Memory 库;下次即便换了新会话,这些偏好依然生效:
from agno.agent import Agent
from agno.memory.v2.db.sqlite import SqliteMemoryDb
from agno.memory.v2.memory import Memory
from agno.models.openai import OpenAIChat
from agno.storage.sqlite import SqliteStorage
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
user_id="seller_001",
session_id="research_2026_07_05", # 换了新会话,但 user_id 不变
memory=Memory(
db=SqliteMemoryDb( # Memory 用独立的库,和会话物理隔离
table_name="user_memory",
db_file="tmp/user_memory.db",
),
),
storage=SqliteStorage(
table_name="agent_sessions",
db_file="tmp/agents.db",
),
enable_user_memories=True, # 自动抽取并保存用户长期偏好
enable_session_summaries=True, # 为每次会话生成摘要,便于跨会话回顾
add_history_to_messages=True,
instructions=[
"你是跨境电商研究助手。",
"可以使用记住的用户偏好,但不要重复暴露其隐私信息。",
],
markdown=True,
)
Memory 和 Storage 用两个独立的库,不是偶然——它们的生命周期和删除策略完全不同。会话可以定期归档清理,用户偏好却要长期保留、还要支持用户自己查看和删除。哪些该进 Memory,是一条工程红线:
适合进 Memory 的:用户偏好的语言/格式/详细程度、常做的业务类型、明确要求长期记住的事实、稳定且非敏感的工作背景。
绝对不能进 Memory 的:密码/API Key/银行卡等凭据、一次性任务参数、某次 RAG 检索命中的资料片段(上一章的坑四)、高敏感个人信息。核心判断标准就一条:它是“关于这个人的稳定事实”,还是“这次任务的临时数据”? 前者进 Memory,后者不进。
State:记住任务进行到哪一步
State 是运行中的机器状态:研究任务跑到第几步、已经查过哪些来源、审批是否通过。它服务当前任务,可恢复、可检查、也可丢弃。在 Agno 里,State 就是 session_state 这个字典,工具函数可以直接读写它:
from agno.agent import Agent
from agno.models.openai import OpenAIChat
def mark_source_checked(agent: Agent, source: str) -> str:
"""记录一个已经核查过的资料来源,避免重复核查。"""
# 工具直接读写 agent.session_state,这就是 State 的落点
checked = agent.session_state.setdefault("checked_sources", [])
if source in checked:
return f"来源已核查过,跳过:{source}"
checked.append(source)
return f"已记录来源:{source}(累计 {len(checked)} 个)"
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
session_state={"checked_sources": []}, # 任务开始时的初始状态
tools=[mark_source_checked],
add_state_in_messages=True, # 让模型在提示里看得到当前 state
instructions=[
"调研时记录已经检查过的来源。",
"不要重复检查同一个来源。",
],
)
agent.print_response("帮我核查德国玩具合规的三个官方来源,别重复。", stream=True)
State 的特点是贴着任务、随任务生灭。任务结束后,最多把有长期价值的结论提炼进 Memory(比如“该品类德国主管机构是 X”),绝不要把完整的运行过程全写进去——那只会把用户画像变成一堆流水账。
Context:这一轮的额外参考资料
Context 是本次运行注入给模型的临时资料,比如当前市场、当前品类、当前文件 ID。它用 context= 传入,配合 add_state_in_messages(或在 instructions 里用占位符引用),就能把这些值渲染进本轮提示:
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
context={"market": "Germany", "category": "toys"}, # 本轮参考,不持久
instructions=[
"当前市场:{market}",
"当前品类:{category}",
"回答时紧扣这个市场和品类,不要泛泛而谈。",
],
add_state_in_messages=True,
)
agent.print_response("这个市场这个品类,最近有什么值得关注的动向?", stream=True)
Context 和 State 形态上都像“运行时的一个字典”,区别在语义:State 是任务自己在推进中改写的进度,Context 是你从外部注入的本轮参考。检索命中的知识片段就是典型的 Context——它只应该活在这一轮,不该顺手写进 Memory。
深入一层:为什么 Agno 把它们做成四个东西
为什么不合并成一个“记忆”系统? 因为它们的数据生命周期、删除策略、隐私等级完全不同。Storage 要支持审计和归档,Memory 要支持用户查看和被遗忘权,State 要支持任务中断续跑,Context 用完即弃。合并成一个,等于把四种截然不同的治理需求塞进一个抽象里——这恰恰是第 8、9 章反复警告的“上下文一锅烩”。Agno 把它们拆成四个配置位,本质是把这条治理边界显式化了。
这和 pi 的做法有什么不同? 第 12 章里,pi 作为 harness,把这些统统当作“你自己在其上管理的上下文”:历史消息要你自己决定截断多少,长期记忆要你自己写工具去存取,任务状态要你自己维护一个对象。pi 给你的是机制和完全的掌控。Agno 则把这四件事做成了开箱即用的组件——add_history_to_messages、enable_user_memories、session_state、context 各是一个开关或参数。换来的是速度,代价是:边界仍然要你自己划。框架能帮你存,但“这条信息该进哪个桶”永远是你的判断,没有任何开关能替你决定“这段检索结果算不算用户偏好”。
enable_user_memories 自动抽取,靠谱吗? 它是便捷,不是保险。模型抽取偏好时可能把一次性需求误判成长期偏好(比如“这次报告我要英文的”被记成“用户偏好英文”)。所以生产系统里,Memory 需要一个用户可见、可编辑、可删除的入口,而不是全权交给自动抽取。这也对应第 10 章“记忆要可控、可纠错”的原则。
动手看看
拿本章的 Storage 示例做起点,做三个小实验,直观感受四者的差别:
- 把
num_history_responses从 3 改成 0,用同一个session_id再问“接着昨天的”——你会发现 Agent 又失忆了。这说明存下来 ≠ 喂进去,回填多少完全由这个参数决定。 - 开启
enable_user_memories,先告诉它“我只做欧盟市场,报告要中文”,然后换一个新的session_id再问它一个宽泛问题,看它是否还记得你的偏好。这是 Memory 跨会话生效的直接证据。 - 给 State 示例连续核查同一个来源两次,观察第二次是否命中
mark_source_checked里的“已核查过,跳过”——你就亲眼看到 State 如何服务当前任务的去重。
实战中的几个坑
坑一:以为存进 Storage,模型就自动知道了。
- 现象:明明配了
SqliteStorage,Agent 却对上一轮说过的话毫无印象。 - 原因:Storage 只负责把消息落地,让模型看见历史的是
add_history_to_messages,回填几轮由num_history_responses决定,二者没开等于白存。 - 对策:持久化和上下文回填是两件事,两个开关都要按需配置,并根据窗口和成本调
num_history_responses。
坑二:把临时检索结果写进了长期 Memory。
- 现象:某条已经过期的政策条款,被 Agent 反复当作“事实”引用,怎么都纠不过来。
- 原因:分不清 Knowledge/Context(本轮证据)和 Memory(长期偏好),把检索命中的片段顺手记成了长期记忆。
- 对策:检索结果只进本轮 Context,任务结束即弃;Memory 只存“关于用户的稳定事实”,政策资料靠知识库重新检索。
坑三:把 State 的运行过程污染进了用户画像。
- 现象:Memory 里堆满了“第 3 步查了某来源”“审批通过了”这类流水账,真正的用户偏好被淹没。
- 原因:任务结束时把整个
session_state无差别地提炼进了 Memory。 - 对策:State 随任务生灭,只把有长期价值的结论(而非过程)择要写进 Memory。
坑四:Memory 里记了敏感信息,又没有删除入口。
- 现象:用户要求删除自己的数据,却发现偏好、身份信息散落各处无从下手。
- 原因:
enable_user_memories自动抽取时可能带进敏感字段,且没有为 Memory 设计用户可控的查看/删除接口。 - 对策:入库前做敏感信息过滤,为 Memory 提供用户可见、可编辑、可删除的入口,把生命周期写进隐私策略。
Agno vs 主书 pi 做法
| 能力 | Agno 怎么做 | pi(harness)怎么做 | 各自适合谁 |
|---|---|---|---|
| 会话持久化 | SqliteStorage + add_history_to_messages + num_history_responses | 自己维护消息存储,自己决定截断与回填策略 | Agno 开箱即用;pi 完全掌控历史裁剪 |
| 长期记忆 | Memory + enable_user_memories 自动抽取偏好 | 自己写工具存取长期记忆,自己定抽取规则 | Agno 省心;pi 精确控制记什么、怎么记 |
| 任务状态 | session_state 字典,工具内直接读写 | 自己维护一个任务状态对象并在循环里传递 | 两者理念一致,Agno 提供现成的字典入口 |
| 本轮上下文 | context= + add_state_in_messages 渲染进提示 | 手动把参考资料拼进本轮 prompt | Agno 用占位符更方便;pi 更贴近原语 |
| 数据边界 | 拆成四个配置位,边界显式化,但划分仍靠你 | 全部是“你管理的上下文”,边界从头到尾由你划 | 都需人工判断,Agno 把边界摆在了明面上 |
分野和前两章一致:Agno 是应用框架,把持久化、记忆、状态、上下文做成四个开箱即用的组件,让你几个参数就能拥有一个“记得住事”的 Agent;pi 是 harness,把这些都当作“你在其上管理的上下文”,更灵活、也更要求你亲手划清每一条边界。底层的模式是同一套——会话恢复、记忆分层、状态管理、上下文注入——只是抽象层次不同。而无论用哪个,那条“Storage=聊过什么、Memory=用户是谁、State=进行到哪、Context=本次额外参考”的边界,永远是工程师的责任,不是框架的开关。
工程检查项
- 是否为每次会话明确
user_id与session_id(跨会话续接与记忆归属的主键)? - Storage 是否支持备份、审计和按需删除?
- Memory 是否有敏感信息过滤,以及用户可见、可编辑、可删除的入口?
- State 是否能在后台任务中断后恢复进度?
- Context 是否控制了长度,避免把无关资料塞爆窗口?
- 是否把 Storage/Memory/State/Context 各自的数据生命周期写进了隐私策略?
小结
- 四个概念一句话辨析:Storage=聊过什么(会话恢复/回放/审计)、Memory=用户是谁(跨会话的长期偏好)、State=进行到哪(当前任务进度)、Context=本次额外参考(本轮临时资料)。对应主书第 8、9、11 章。
- 存下来 ≠ 喂进去:
SqliteStorage负责落地,add_history_to_messages与num_history_responses才决定模型看见多少历史。 - Memory 只存“关于用户的稳定事实”:临时任务参数、检索片段、敏感凭据都不该进,且必须提供用户可控的删除入口。
- State 随任务生灭:只把有长期价值的结论提炼进 Memory,不要把运行过程污染进用户画像。
- Agno 把四者做成开箱即用的配置位,但数据边界的划分永远是工程师的责任——能恢复、能回放、能删除,才是真正可上线的长期 Agent。
现在 Research Agent 既能动手、能查资料,又能记住用户和进度,成了一个像样的长时系统。但它仍是一个 Agent 在单打独斗。下一章,我们把它拆成分工协作的团队,再用工作流把多个步骤稳稳地串起来。