一、为什么 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 包含 stateeventssteps 等多个字段,完整还原整个工作流状态

在这里插入图片描述

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 在做危险操作前先问人。

更多推荐