☰
18|上下文与记忆:Agent 应该记住哪些信息?-eino教程
2026/10/21 11:32:01 网站建设 项目流程

18|上下文与记忆:Agent 应该记住哪些信息?

《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 18 篇
关键词:对话历史、SessionValues、上下文窗口、摘要压缩、长期记忆
前置条件:已完成 17|Runner 与事件流。

第 17 篇末尾提到,Agent 不会自动记住上一次运行。这一篇正面回答:“记住”到底有几种含义,各自由谁负责。

把这件事讲清楚,能避免两类常见错误:以为框架会自动记忆,以及把所有东西都塞进对话历史。


一、先区分四种“记忆”

“记忆”这个词被用得太宽泛了。在 Agent 系统里,至少对应四种不同机制:

类型生存范围由谁管理典型内容
对话历史一次请求内传递应用层维护用户与助手的往返消息
运行内共享状态一次运行内框架(SessionValues)租户 ID、请求追踪号
单次执行内的状态一次执行内框架(State)循环计数、中间结果
长期记忆跨请求、跨天应用 + 存储用户偏好、历史结论

框架只负责中间两种。对话历史和长期记忆都需要应用层自己处理。


二、对话历史:必须手动维护

下面这段程序用两次独立运行和一次累积历史做对比(已实机运行):

=== 两次独立运行(无历史)=== 模型看到/回复: 收到 2 条消息,最后一条: "第 1 个问题" 模型看到/回复: 收到 2 条消息,最后一条: "第 2 个问题" === 传入累积历史 === 模型看到/回复: 收到 3 条消息,最后一条: "第 1 个问题" 模型看到/回复: 收到 5 条消息,最后一条: "第 2 个问题"

数字变化说明了机制:

  • 独立运行:每次都是 2 条消息(系统消息 + 用户消息),互不影响。
  • 累积历史:第二次运行是 5 条(系统 + 用户1 + 助手1 + 用户2 + 助手2),模型能看到完整上下文。

维护历史的代码很直白:

history:=[]*schema.Message{schema.SystemMessage("你是工单助手。")}for_,question:=rangequestions{history=append(history,schema.UserMessage(question))iter:=runner.Run(ctx,history)// ... 消费事件,拿到回复history=append(history,schema.AssistantMessage(reply,nil))}

两个必须遵守的规则(和第 03 篇一致):

  1. 助手回复要以assistant角色回到历史。
  2. 每个会话的历史必须独立,绝不能跨用户共享同一个切片。

三、上下文窗口:历史不能无限增长

历史越长,上下文越大。后果有三层:

影响表现
成本每轮都重发全部历史,Token 消耗随轮次上升
延迟输入变长,首字节时间增加
效果关键信息被大量历史稀释,模型反而更抓不住重点

“历史全带上”不是稳妥策略,而是资源浪费。通常需要主动管理。

三种常用策略

1. 滑动窗口:只保留最近 N 轮

constmaxTurns=10iflen(history)>maxTurns*2+1{// +1 是系统消息history=append(history[:1],history[len(history)-maxTurns*2:]...)}

简单可靠,但会丢失早期重要信息(比如用户最初说明的问题背景)。

2. 摘要压缩:把早期历史总结成一段话

把较早的消息送给模型总结,用一段摘要替代原文:

[早期对话摘要] 用户报告打印机离线,已确认网线与网段配置正常,尝试重启无效。 [最近对话] 用户:那接下来怎么办? 助手:……

压缩能保留要点、显著减小体积,代价是细节丢失,且摘要本身也要调用模型(额外成本)。

3. 按需检索:历史也当作可检索的资料

把历史存起来,需要时按相关性召回,而不是全量带上。适合长期会话,实现复杂度最高。

怎么选

场景建议
短会话(几轮内解决)全量带上,不必优化
中等长度、偶尔超长滑动窗口
长会话、需要保留背景摘要压缩
跨天或长期陪伴式检索 + 摘要组合

先测量再优化。记录实际历史的 Token 用量,看它是否真的成为瓶颈。


四、SessionValues:一次运行内的共享键值

有些数据不属于“对话”,而属于“这次运行的上下文”。例如当前用户、租户、追踪号。

这类数据用 SessionValues 传递:

iter:=runner.Run(ctx,messages,adk.WithSessionValues(map[string]any{"user_id":"u-1001","tenant_id":"t-01",}))

在工具或中间件内部读取:

ifv,ok:=adk.GetSessionValue(ctx,"user_id");ok{// 使用 v}

它的特点:

特点说明
基于context随调用链传递,天然线程安全地共享
一次运行内有效不跨请求
并发安全可被多个 Agent 并发读写

有一个容易踩的坑:Runner 运行时会重新初始化 Context。所以想在运行前注入数据,必须用WithSessionValues这个 Option,而不是自己往ctx里塞值:

// 正确:通过 Option 注入runner.Run(ctx,messages,adk.WithSessionValues(map[string]any{"user_id":"u-1001"}))// 错误:直接 context.WithValue 会被运行时的初始化覆盖

运行过程中若需要新增值,用adk.AddSessionValue(ctx, key, value)。

不要把大块内容放进 SessionValues。它是用来传递小型上下文标识的,不是数据仓库。会话变量还可能被序列化进检查点,过大的值会影响性能。


五、执行状态与检查点:容易误解的地方

这里有两个机制,用途不同,容易混:

机制作用生存期
执行状态(ProcessState)一次执行内跨节点共享数据单次执行
检查点(Checkpoint)中断后恢复运行现场直到恢复或过期

第 07 篇讲过执行状态:每次执行都是全新的,不跨请求保留。

检查点则用于人工审批等场景(第 20 篇):运行到一半被中断,把现场存下来,稍后接着跑。

两者都不是长期记忆:

  • 执行状态解决“同一次执行内部怎么共享数据”。
  • 检查点解决“中断了怎么接着跑”。
  • 长期记忆需要你自己设计存储结构并持久化。

六、长期记忆:属于应用层的问题

Agent 要“记住用户上次说过什么”,需要应用层设计。这不是框架能替你决定的,因为它涉及业务语义、隐私和合规。

需要想清楚的问题:

问题为什么重要
记什么?用户偏好?历史结论?全部对话?
存多久?过期策略与合规要求(如个人信息保存期限)
谁可见?跨用户隔离、跨租户隔离
怎么用?全量注入历史,还是检索相关片段
如何删除?用户要求删除时能不能真的删干净

一种常见做法是分两层:

会话历史(短期) └─ 存最近若干轮,随请求带上 用户画像(长期) └─ 存稳定的事实,如偏好、常用设备、所属部门 └─ 以结构化字段或可检索片段形式存在 └─ 按需注入,而不是每轮全带

关键原则:长期记忆要“按需注入”,而不是“全量携带”。把用户一年的历史都塞进上下文,既贵又无效。

隐私红线

  • 用户数据要有明确的保留期限和删除机制。
  • 敏感信息不应进入模型上下文,除非业务确有必要。
  • 记住的内容要在界面上可见、可管理——用户有权知道系统记住了什么。

七、完整示例

下面这段程序演示历史维护与 SessionValues 注入,不访问网络:

packagemainimport("context""fmt""log""github.com/cloudwego/eino/adk""github.com/cloudwego/eino/components/model""github.com/cloudwego/eino/schema")// passthroughModel 直接回显最后一条用户消息,便于观察上下文。typepassthroughModelstruct{}func(m*passthroughModel)Generate(_context.Context,in[]*schema.Message,_...model.Option)(*schema.Message,error){returnschema.AssistantMessage(fmt.Sprintf("收到 %d 条消息,最后一条: %q",len(in),in[len(in)-1].Content),nil),nil}func(m*passthroughModel)Stream(ctx context.Context,in[]*schema.Message,opts...model.Option)(*schema.StreamReader[*schema.Message],error){msg,err:=m.Generate(ctx,in,opts...)iferr!=nil{returnnil,err}sr,sw:=schema.Pipe[*schema.Message](1)gofunc(){defersw.Close();sw.Send(msg,nil)}()returnsr,nil}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{ctx:=context.Background()agent,err:=adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{Name:"assistant",Description:"助手",Instruction:"你是工单助手。",Model:&passthroughModel{},})iferr!=nil{returnerr}runner:=adk.NewRunner(ctx,adk.RunnerConfig{Agent:agent})// 1. 每次 Run 都是独立执行:历史不会自动累积。fmt.Println("=== 两次独立运行(无历史)===")fori:=1;i<=2;i++{iter:=runner.Run(ctx,[]*schema.Message{schema.UserMessage(fmt.Sprintf("第 %d 个问题",i))})printOutput(iter)}// 2. 手动维护历史并传入,模型才能看到完整上下文。fmt.Println("\n=== 传入累积历史 ===")history:=[]*schema.Message{schema.SystemMessage("你是工单助手。")}fori:=1;i<=2;i++{history=append(history,schema.UserMessage(fmt.Sprintf("第 %d 个问题",i)))iter:=runner.Run(ctx,history)reply:=printOutput(iter)// 把回复追加回历史,下一轮才能看到。ifreply!=""{history=append(history,schema.AssistantMessage(reply,nil))}}// 3. SessionValues:一次运行内跨节点共享的键值,可通过 Option 注入。fmt.Println("\n=== 通过 WithSessionValues 注入运行期数据 ===")iter2:=runner.Run(ctx,[]*schema.Message{schema.UserMessage("读取会话变量")},adk.WithSessionValues(map[string]any{"user_id":"u-1001"}))// 在工具或节点内部可用 adk.GetSessionValue(ctx, "user_id") 读取。_=iter2 fmt.Println("已注入 user_id 到本次运行的 SessionValues(供工具/中间件读取)")returnnil}funcprintOutput(iter*adk.AsyncIterator[*adk.AgentEvent])string{varoutstringfor{event,ok:=iter.Next()if!ok{break}ifevent.Err!=nil{fmt.Println(" 错误:",event.Err)continue}ifevent.Output==nil||event.Output.MessageOutput==nil{continue}mv:=event.Output.MessageOutputifmv.Message!=nil{out=mv.Message.Content fmt.Println(" 模型看到/回复:",mv.Message.Content)}}returnout}

八、常见问题

Agent 记不住上一轮
历史没有传进去。每次Run都是独立执行,需自行维护并传入历史。

多用户之间串了上下文
历史切片被共享了。每个会话必须有独立的历史结构。

请求越来越慢、越来越贵
历史未做截断,Token 随轮次增长。加入窗口或摘要策略。

注入的 SessionValue 读不到
用了context.WithValue而非WithSessionValues。Runner 会重新初始化 Context,直接塞进去的值会丢失。

检查点恢复后状态不对
检查点恢复的是运行现场,不等于业务数据的一致性。涉及写操作时仍要保证幂等(第 29 篇)。

用户要求“忘记我说过的话”
需要应用层实现删除。设计存储时就要考虑可删除性,不要只往一个不断增长的日志里追加。


九、小结

  • “记忆”分四种:对话历史、运行内共享值、执行状态、长期记忆,框架只负责中间两种。
  • 对话历史必须手动维护,回复要以assistant角色回填。
  • 历史不能无限增长,可用滑动窗口、摘要压缩或按需检索控制。
  • SessionValues 用于传递小型运行期标识;必须用WithSessionValues注入。
  • 检查点是“中断恢复现场”,不是长期记忆,也不保证业务幂等。
  • 长期记忆属于应用层设计,遵循按需注入、明确保留期限、可删除的原则。

下一篇《Middleware 实战:在决策循环中加入控制能力》,我们把审计、校验、上下文处理等横切能力挂到 Agent 的生命周期上。


参考资料与版本说明

资料核对日期为2026 年 10 月 9 日:

  1. Eino ADK 概述
  2. Agent 协作(SessionValues 部分)
  3. Eino 主仓库

本文示例按Eino v0.9.21编写,使用模拟模型(不访问网络),已实机运行,正文输出为真实结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询