OpenAI Agents SDK #5:Memory——让 Agent 真正「记住」你

从「Agent 为什么总是失忆」的开发者痛点切入,系统讲解 SDK Memory 模块的核心机制:两种上下文(本地 Context vs LLM Context)的本质区别、四种对话状态管理策略对比、SQLiteSession 的两种存储模式与完整代码示例、session_id 颗粒度设计、WAL 并发安全、SessionSettings 的 Token 成本控制,以及自定义 Session Backend 的扩展路径。结尾以三层记忆体系(Working Memory / Session Memory / Long-term Memory)收尾,给出 3 条可立即落地的实践建议,并预告 #6 Sandbox。

リサーチノート

你有没有遇到过这个场景:用 Agent 聊了半小时,换个话题绕回来,它对你前面说的一切毫无印象——就像第一次见面。
不是模型变笨了。是因为 LLM 本身是无状态的。每一次 run() 调用都是一张白纸,你不显式喂给它历史,它就不知道历史存在。
Memory 模块存在的意义就在这里。

先搞清楚:「记忆」在 SDK 里是什么

OpenAI Agents SDK 有两种完全不同的「上下文」,经常被混淆1
本地 Context(Local Context):工具函数、生命周期钩子需要的依赖和状态,通过 Runner.run(..., context=your_object) 传入。这个对象不会发送给 LLM,纯粹是 Python 运行时层面的依赖注入。
LLM Context(对话历史):LLM 在对话中实际「看到」的内容——系统提示、用户消息、工具调用记录、历史回复。这才是「记忆」真正所在的地方。
Memory 模块做的事情只有一件:在多次 run() 调用之间,把 LLM 应该知道的历史消息持久化,下次调用时自动喂回去。
Memory 架构示意:工作记忆、情景记忆、语义记忆的分层结构
Memory 架构示意:工作记忆、情景记忆、语义记忆的分层结构
图片来自本文 AI 生成插图

四种对话状态管理策略

在讲 Session 之前,先把完整图景放出来。SDK 支持四种方式延续对话状态2
策略方式适用场景
result.to_input_list()手动把上次结果拼回输入完全自控,灵活但繁琐
session(客户端管理)SDK 自动维护历史,存本地本地持久化,本文重点
conversation_id(OpenAI 服务端)OpenAI 服务器存历史云端对话,无本地存储
previous_response_id响应链接,指向上条回复轻量级链接,无需完整历史
to_input_list() 最直接,代码大概长这样:
result = await Runner.run(agent, "第一个问题")
new_input = result.to_input_list() + [{"role": "user", "content": "第二个问题"}]
result2 = await Runner.run(agent, new_input)
能用。但十轮对话之后,这段拼接逻辑会让你头疼。

Session:推荐的自动化方案

Session 是 SDK 推荐的持久化方案,核心思路是把历史管理从业务代码里剥离出去3
Session 是一个 Protocol(协议接口),定义了四个方法:
# Session Protocol 定义
async def get_items(limit: int | None) -> list[TResponseInputItem]: ...
async def add_items(items: list[TResponseInputItem]): ...
async def pop_item() -> TResponseInputItem | None: ...
async def clear_session(): ...
你把 session 传给 Runner.run(),SDK 自动在每次 run 前读取历史、run 后写入新消息。业务代码完全感知不到这个过程。

SQLiteSession:开箱即用的实现

SDK 内置了 SQLiteSession,基于 SQLite 实现3,支持两种模式:
内存数据库(默认):进程退出后数据消失,适合单次运行内的多轮对话:
from agents.memory import SQLiteSession

session = SQLiteSession(session_id="user-123")
# 默认 db_path=':memory:',数据只在进程内存在
文件持久化:数据写入磁盘,跨进程、跨重启都能恢复:
session = SQLiteSession(
    session_id="user-123",
    db_path="./conversations.db"  # 指定文件路径
)
把 session 传给 Runner:
result = await Runner.run(
    agent,
    "你好,我叫 Alice",
    session=session
)

# 第二次 run,SDK 自动注入前面的对话历史
result2 = await Runner.run(
    agent,
    "你还记得我叫什么吗?",
    session=session  # 同一个 session 实例
)
# Agent 会正确回答「Alice」
不需要手动调用 to_input_list(),不需要管理消息列表。Session 在后台静默完成了历史的读写。

session_id 是关键设计

session_id 是区分不同对话线程的关键3。同一个 db_path 下的不同 session_id,互相完全隔离:
# 用户 Alice 的对话
alice_session = SQLiteSession("alice-2026", db_path="./app.db")

# 用户 Bob 的对话
bob_session = SQLiteSession("bob-2026", db_path="./app.db")

# 两人的历史分开存,互不干扰
一个 .db 文件服务多个用户,完全没问题。生产代码里,session_id 通常是用户 ID 或对话 ID,按自己的业务模型定就好。

并发安全:WAL 模式

多线程场景下共用同一个 SQLite 数据库,SQLiteSession 通过 WAL(Write-Ahead Logging)模式处理并发3
  • 内存数据库:用共享连接避免线程隔离问题(SQLite 内存库默认每个连接独立,共享连接才能看到同一份数据)
  • 文件数据库:用线程本地连接(threading.local()),每个线程拥有独立连接,通过 WAL 模式实现并发读写
普通 Web 应用的并发量,SQLiteSession 完全撑得住。真的需要更高并发,v0.14.2 起 SDK 已支持 MongoDB session 后端4,社区贡献,PR 作者 alexbevi。

SessionSettings:控制历史长度

对话历史不能无限增长。SessionSettingslimit 参数控制每次 get_items() 返回的消息数量上限3
from agents.memory import SQLiteSession, SessionSettings

session = SQLiteSession(
    session_id="user-123",
    db_path="./app.db",
    session_settings=SessionSettings(limit=50)  # 只保留最近 50 条消息
)
历史越长,每次调用注入的 context 越多,LLM 推理成本越高。limit=50 是个不错的起点,之后盯着 token 用量按需调。

自定义 Session Backend

Session 只是一个 Protocol,不和任何具体存储绑定3。只要实现四个方法,任何存储都能接入:
from agents.memory import Session

class RedisSession:
    def __init__(self, session_id: str, redis_client):
        self.session_id = session_id
        self.redis = redis_client

async def get_items(self, limit=None):
        # 从 Redis 读取历史消息
        ...

async def add_items(self, items):
        # 写入 Redis
        ...

async def pop_item(self):
        # 弹出最新一条
        ...

async def clear_session(self):
        # 清空该 session
        ...
接入之后,Runner.run() 那边一行代码都不用改。SDK 不在乎底层是 Redis、PostgreSQL 还是向量数据库,它只认 Protocol。

Session 和 Context 的边界

这里有一个常见误区值得单独说明。
context 参数(本地 Context)是运行时的依赖注入容器,用于传递数据库连接、配置对象、用户身份等运行时依赖。它不会被发送给 LLM,也不参与对话历史管理1
session 参数(Memory)则是专门管理 LLM 能「看到」的对话历史。
两者职责不重叠:
result = await Runner.run(
    agent,
    "用户输入",
    context=AppContext(db=db_conn, user_id=user_id),  # 本地依赖,LLM 看不到
    session=SQLiteSession(session_id=user_id)          # 对话历史,LLM 看得到
)
混淆这两个参数,是 Memory 相关 bug 的高频来源5。Agent 「记不住」,先别急着怀疑模型——八成是状态放错地方了。

三类「记忆」的存储位置对比

Session 解决的是跨 run 的对话历史。但「记忆」这件事比这更大。完整的 Agent 记忆体系通常分三层6
正在加载统计卡片...
OpenAI Agents SDK 的 Memory 模块覆盖前两层。第三层——长期知识沉淀——通常通过 FileSearchTool 或自定义工具对接外部向量库,那是 Tools 模块的事情,不在这里展开。

最新动态:MongoDB 后端和 GPT-5.5 默认值

SDK 近期版本在 Memory 相关方向有两个值得关注的更新:
v0.14.2(2026-04-18)正式加入 MongoDB session 后端4,文档在 v0.14.6 得到补充7。对于已在生产环境使用 MongoDB 的团队,这意味着无需额外引入 SQLite 依赖,直接复用现有存储。
v0.14.6(2026-04-25)把默认模型从 GPT-4 系列更新为 GPT-5.57。更长的 context window 直接影响每次 run 能注入多少对话历史——Session 的 limit 参数可以适当放宽。

三条实践建议

① 区分存储策略,按 session_id 颗粒度设计
生产环境中,session_id 通常是 f"{user_id}:{thread_id}",而不是纯用户 ID。这样同一个用户可以开多条独立对话线,互不干扰。
② 设置合理的 limit,控制 Token 成本
limit=30 开始试,观察 Token 消耗和对话质量的平衡点。对于长任务 Agent,考虑在业务层实现历史压缩(summarization),先把早期历史浓缩成摘要再存入 session。
③ 用 session.clear_session() 管理对话生命周期
新任务开始时,主动调用 clear_session() 清除旧历史,避免无关上下文干扰新任务的推理。这比「不断累积历史直到 token 溢出」要稳定得多。

Memory 让 Agent 有了「记忆」。但一个只会说话的 Agent,终究还是只能说话。
下篇讲 #6 Sandbox。当 Agent 需要真正「动手」——执行代码、读写文件、跑 Shell 命令——该怎么把它关进一个安全的盒子里?我们会拆解 SandboxAgentManifest(工作区合约),以及 Docker/Modal/E2B 等托管环境的选择逻辑。

このコンテンツについて、さらに観点や背景を補足しましょう。

  • ログインするとコメントできます。