这是我的专栏《春哥的Agent通关秘籍》系列文章的第 20 篇。
希望系统性跟着我一起学AI-Agent编码的同学可以关注一下我的这个专栏。
一、【事实记忆】的局限性
上一节课我们主要讲的是基于“事实(fact)”的记忆提取、分类、存储、压缩的思路。
但你是否知道【事实记忆】在Agent工程实践上,也有它的短板和不擅长处理的场景?
这个场景就是:复杂长任务。
在复杂的工程落地中,只用【事实记忆】会导致极其糟糕的体验,主要体现在以下两个高频痛点:
-
陷入“排错死循环”
- 假设你在进行一个复杂问题的解决,Agent先给你提出了方案A,你尝试后不行。Agent又给你了方案B,还是不行。
- 但在此过程中,因为Token到达一定长度,自动触发了上下文压缩,尝试A的过程被简化成了一个【事实A】。
- 当Agent继续尝试解决问题时,【事实A】能提供的信息过于有限,导致Agent还是给出了方案A的解决思路。
- 于时,整个对话陷入死循环。
-
任务的“断点续传”能力极差
- 我们在开发一套复杂的逻辑(比如写一个逻辑非常复杂的前端组件)时,通常不是一天能写完的。
- 到了周五下班,任务中断。
- 如果用户不小心在同一个Agent会话(Session)里处理了其他任务。
- 下周一回来,Agent 只记得“我们在写分类系统”(事实)
- 它不知道“周五下班前我们卡在了哪个具体的判断分支上”,你就得花大量口舌重新对齐上下文。
二、事件流
复杂的场景衍生出复杂的工具,而“事件流与情景摘要”则是专为复杂长任务而生的一种记忆手法。
接下来,先让我们认识一下【事件流】与【情景摘要】。
2.1 什么是事件流?
简单来说:事件流(Event Stream)是系统底层记录的、带有绝对时间戳的、极其细粒度的原子动作序列。
它具备以下特点:
-
特征:高频、客观、未经加工。
-
内容:不仅包含用户说了什么,还包含 Agent 调用了什么 Tool、系统返回了什么报错信息。
例如,以下就是一个典型的事件流格式,谁在什么时间做了什么事。
[
{
"action_type": "USER_INPUT",
"timestamp": "2026-03-14T09:40:00",
"payload": "帮我装一下 PyTorch"
},
{
"action_type": "TOOL_CALL",
"timestamp": "2026-03-14T09:40:05",
"payload": "pip install torch"
},
{
"action_type": "SYSTEM_ERROR",
"timestamp": "2026-03-14T09:45:12",
"payload": "ReadTimeoutError: HTTPSConnectionPool(host='pypi.org', port=443): Read timed out."
},
{
"action_type": "SYSTEM_ERROR",
"timestamp": "2026-03-14T09:56:40",
"payload": "RuntimeError: Numpy is not available"
}
]
在Agent开发过程中,事件流的产生,通常横跨了 Agent 运行的三个层面:
-
交互层:用户点了发送按钮、Agent 返回了流式文本。
-
工具层:Agent 决定调用
Function Calling时的入参。 -
环境层:工具运行完毕后返回的成功结果,或者抛出的
Timeout、SyntaxError等系统异常。
你完全可以理解,事件流就是人/Agent/LLM 交互的详细记录,它详实而无额外加工,属于最诚实最相机的日志信息。
2.2 如何生成事件流?
以LangGraph为例,我们尝试构建一个最简单的 Function Calling的demo:
并让用户提出要求:"帮我用 pip 安装一下 requests,然后再装一下 numpy。"
那么,如何监听到各种节点的消息,并拼装成 "事件流"呢?
答案是 BaseCallbackHandler,这是 langchain 内置的监听模块,你可以用它监听各种动向。
尝试撰写如下核心代码:
from langchain_core.callbacks import BaseCallbackHandler
class EventStreamRecorder(BaseCallbackHandler):
def __init__(self):
self.event_queue: List[Event] = []
# 拦截:大模型开始处理消息
def on_chat_model_start( self, serialized: Dict[str, Any],
messages: List[List[BaseMessage]], **kwargs: Any ) -> None:
# 略
# 拦截:大模型决定调用工具
def on_tool_start( self, serialized: Dict[str, Any], input_str: str, **kwargs: Any ) -> None:
# 略
# 拦截:工具执行成功
def on_tool_end(self, output: str, **kwargs: Any) -> None:
# 略
# 拦截:工具执行失败/抛出异常
def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
# 略
recorder = EventStreamRecorder()
response = agent.invoke(
{"messages": [("user", user_input)]},
config={"callbacks": [recorder]}
)
上面主要展示了 BaseCallbackHandler 的核心钩子,以及如何注册到 LLM的回调里。 完整代码看这里:github.com/zhangshichu…
尝试执行以上代码,查看控制台输出:
可以看到,事件流的信息比单纯的对话记录要更为丰富。
在涉及到本地操作和复杂工程问题的Agent开发过程中,这些记录都是千金难求的财富,因为它们才是知道AI下一步操作的【经验】。
不过,事件流也有一个致命的缺点:
事无巨细。
三、情景摘要
3.1 什么是情景摘要?
事件流虽然详细,但很难作为长期任务的参考文献,因为:
- 它过于详细以至于冗长、多余
- 它没有重要程度的区分标识,乃至很难分清主次
- 缺少每一步生成的原因
所以,我们需要一种结构,它具备以下特点:
- 高度概括,但包含基本的事情经过和因果关系。
- 时间确定
- 元数据,方便混合检索
- 重要程度评分
- 下一步规划
- 等等其他你需要的概括性内容
嗯,没错,这种内容,就是【情景摘要】!
3.2 情景摘要的结构
首先,我得声明一下,情景摘要的结构并不固定,不要被本文带偏。
但核心思想是一致的:对【事件流】进行概括。
我们以知名存储库 LangMem 为例,它的官方给出了自己的【情景摘要】的数据结构:
你可以在这里查看langchain-ai.github.io/langmem/gui…
很明显,它包含了以下基本信息:
- 分类
- 摘要内容
- 思考过程
- 行动
- 结果
- 时间
- 重要度评分
当然,如果你认为JSON的结构你不喜欢,你完全可以把以上内容都塞到一个大的文本里(除了元数据)。
{
"content": "2026-03-17 21:58:00,用户完成了Agent记忆专题事件流与情景摘要部分的撰写。下一步他计划看点小说然后睡觉",
"metadata": {"type": "preference"},
}
都可以,本质上【摘要】也是一种记忆压缩,但它需要遵循一些基本规则,以保持自身信息的完整。
使用LLM + 系统提示词,可以强制把一系列的【事件流】转化为如上更为精炼的记忆单元。
3.3 摘要时机
那么,什么时候进行摘要提炼呢?
主流的做法有这样几种思路:
- 固定条事件节点之后(比如20个)
- 当短期上下文(事件流)消耗到一定长度之后
- 某个任务目标完成或达成之后。(比如Claude完成了某个功能开发)
- 固定时间边界(比如每天至少提取一次)
总之,这一点比较灵活,可以根据自己的Agent的实际情况而定。
比较简单的做法就是直接按照固定条目的事件进行摘要。
由于这部分实践比较简单,我就不列举代码块了,没啥新东西,demo可以在这里获取:github.com/zhangshichu…
四、使用场景
结合上一章关于【事实记忆】的文章。
可以看出,【情景摘要】和【事实记忆】在核心思路上是类似的,都是按照以下步骤来实践的:
- 收集信息
- 提取精炼
- 存入向量库 + 混合检索
两者的差异主要点在于:
- 【事实记忆】比较简单高效稳定省Token
- 【情景摘要】更详实,还有重要性评分可以在后续精炼时忘掉评分低的记忆,有前因后果,有下一步计划
因此,对于使用选择上,一般来说:
- 情感伴侣,聊天对话等轻度场景,建议使用【事实记忆】
- 编码助手,工作流帮手等长任务场景,建议使用【情景摘要】
五、小结
这一节我们学习了【事件流】和【情景摘要】。
下一节我们将学习一种更为复杂的结构:知识图谱记忆(Graph Memory)。
它擅长记录更为多元和复杂的记忆。
敬请期待!