玩转LlamaIndex:Context 记忆机制
一、为什么 Agent 需要记忆?
上一篇我们写的 Agent 有一个明显的"毛病":金鱼记忆。
你问完一个问题,下一个问题它就忘了上一个。
比如:

为什么?
因为每次 agent.run() 都被框架当成全新的对话处理。
要解决这个问题,就得用 Context(上下文)。
LlamaIndex 的 Context 概念和 LangChain 的 MemoryBuffer 类似,但更灵活——它基于 Workflow 引擎,可以做短期记忆、长期记忆、跨会话持久化。
二、Context 的两种典型用法
2.1 短期记忆(同一次会话内)
用 Context(agent) 创建一个绑定到当前 agent 的上下文对象,把它传给 agent.run(user_msg=..., ctx=ctx),Agent 就会自动维护这次会话的历史消息。
代码示例:
......
# 核心:创建一个 Context
ctx = Context(agent)
async def main():
# 第一轮对话
r1 = await agent.run(user_msg="你好,我叫张三,今年 30 岁", ctx=ctx)
print("第一轮:", r1)
# 第二轮:问 Agent 记住的信息
print("-" * 50)
r2 = await agent.run(user_msg="我叫什么?多大了?", ctx=ctx)
print("第二轮:", r2)
# 第三轮:在历史基础上做新任务
print("-" * 50)
r3 = await agent.run(user_msg="请帮我算 30 + 7", ctx=ctx)
print("第三轮:", r3)
if __name__ == "__main__":
asyncio.run(main())
注意第二、三轮,Agent 完美地"记住"了第一轮里你告诉它的信息。
这就是 Context 的作用:把历史消息累积起来,每次调用 LLM 都带上。

2.2 代码逐行解析
# 关键代码
ctx = Context(agent)
r = await agent.run(user_msg="...", ctx=ctx)
Context(agent):创建一个上下文对象,绑定到当前 agent 实例。agent.run(..., ctx=ctx):把 ctx 传给 run。Agent 内部会自动做两件事:- 从 ctx 里读取历史消息
- 把本轮新消息(包括工具调用结果)写回 ctx
绝对不能每轮都新建 Context:
# ❌ 错误写法:每轮新建 Context,历史全丢
r1 = await agent.run(user_msg="...", ctx=Context(agent))
r2 = await agent.run(user_msg="...", ctx=Context(agent)) # 失忆!
# ✅ 正确写法:复用同一个 ctx
ctx = Context(agent)
r1 = await agent.run(user_msg="...", ctx=ctx)
r2 = await agent.run(user_msg="...", ctx=ctx) # 记忆延续
三、Context 内部工作流
Context 本身是基于 LlamaIndex 的 Workflow 引擎 实现的。理解它内部怎么走,有助于排查"为啥没记忆"之类的诡异问题。

核心数据结构:ctx.state 是一个 dict,里面有个特殊的 key "memory" 用来存消息列表。你可以打印它看看:
print("当前 ctx.state 内容:", ctx.state)
四、长期记忆:跨会话持久化
短期记忆只在同一个 Python 进程内有效。一旦脚本退出,Context 就消失了。
要实现"昨天跟 Agent 说过的事,今天它还记得",就得把 Context 序列化到外部存储(Redis、SQLite、文件都行)。
4.1 Context 序列化示例
import json
from pathlib import Path
async def save_ctx(ctx: Context, filepath: str):
"""把 ctx 存到磁盘"""
state = ctx.to_dict()
Path(filepath).write_text(json.dumps(state, ensure_ascii=False, indent=2))
print(f"✅ 上下文已保存到 {filepath}")
async def load_ctx(agent, filepath: str) -> Context:
"""从磁盘恢复 ctx"""
state = json.loads(Path(filepath).read_text())
ctx=Context.from_dict(
agent,
state
)
print(f"✅ 上下文已从 {filepath} 恢复")
return ctx
# 使用
async def persistent_demo():
# 第一次会话
ctx = Context(agent)
await agent.run(user_msg="我叫李四,住在北京", ctx=ctx)
await save_ctx(ctx, "./ctx_state.json")
# 模拟"第二天重新打开程序"
print("\n=== 重启程序,从磁盘恢复 ctx ===\n")
new_ctx = await load_ctx(agent, "./ctx_state.json")
r = await agent.run(user_msg="我住哪儿?", ctx=new_ctx)
print("恢复后回答:", r) # "你住在北京"
关键 API:
ctx.to_dict():Context → dict(可序列化)ctx.from_dict(state):dict → Context(恢复)- 序列化的 dict 包含
state、events、steps等多个字段,完整还原整个工作流状态

4.2 序列化内容?

五、给 Context 加"自动摘要"避免爆 token
Context 累积的消息越多,每次调 LLM 的 token 消耗就越大。多轮对话后,token 可能爆掉。
解决思路:当历史超过阈值时,自动用 LLM 把老消息压缩成摘要。
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.llms import ChatMessage, MessageRole
# 自定义一个带摘要的 memory
memory = ChatMemoryBuffer.from_defaults(
token_limit=4000, # 超过 4000 token 自动摘要
)
# 手动管理历史
async def demo_with_summary():
# 把历史消息塞到 memory 里
memory.put(ChatMessage(role=MessageRole.USER, content="今天天气真好"))
memory.put(ChatMessage(role=MessageRole.ASSISTANT, content="是啊,适合出门"))
# 获取所有消息(超 token 限制时会自动摘要)
messages = memory.get()
print(f"当前消息数: {len(messages)}")
for m in messages:
print(f" [{m.role}] {m.content[:50]}")
asyncio.run(demo_with_summary())
⚠️ LlamaIndex 0.14 版本的 Context 内部已经自动用了 memory buffer,但如果你的对话特别长,建议手动控制或定期清理 ctx。
六、常见问题
Q1:为什么我每次都新建 Context,对话还是没记忆?
检查你有没有复用 ctx 对象。ctx = Context(agent) 每次新建就会丢掉历史。
Q2:Context 和 LangChain 的 ConversationBufferMemory 啥区别?
- LangChain 的 Memory 是"线性追加",基于字符串
- LlamaIndex 的 Context 是基于 Workflow 状态机,可以存任意 Python 对象(包括工具调用记录、临时变量等),更灵活但稍重
Q3:Context 能存多少消息?
理论上无限,但受 LLM 上下文窗口限制。
DeepSeek-chat 是 32K,超过会自动截断或报错(取决于 memory buffer 配置)。
七、小结
这一篇我们学到了:
- 短期记忆:
Context(agent)+ctx=ctx在单次会话内复用 - 长期记忆:
ctx.to_dict()序列化,ctx.from_dict()恢复,配合 Redis/SQLite 实现跨会话 - 自动摘要:用
ChatMemoryBuffer防止 token 爆炸 - 核心原则:同一个 ctx 对象,贯穿整个对话
下一篇我们讲 HITL(人类参与循环)——让 Agent 在做危险操作前先问人。
更多推荐
所有评论(0)