资讯动态

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

发布时间:2026/10/22 12:16:30 来源:尧图企业网站定制
18上下文与记忆Agent 应该记住哪些信息《Eino 实战用 Go 构建 AI 应用与智能体》· 第 18 篇关键词对话历史、SessionValues、上下文窗口、摘要压缩、长期记忆前置条件已完成 17Runner 与事件流。第 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{historyappend(history,schema.UserMessage(question))iter:runner.Run(ctx,history)// ... 消费事件拿到回复historyappend(history,schema.AssistantMessage(reply,nil))}两个必须遵守的规则和第 03 篇一致助手回复要以assistant角色回到历史。每个会话的历史必须独立绝不能跨用户共享同一个切片。三、上下文窗口历史不能无限增长历史越长上下文越大。后果有三层影响表现成本每轮都重发全部历史Token 消耗随轮次上升延迟输入变长首字节时间增加效果关键信息被大量历史稀释模型反而更抓不住重点“历史全带上”不是稳妥策略而是资源浪费。通常需要主动管理。三种常用策略1. 滑动窗口只保留最近 N 轮constmaxTurns10iflen(history)maxTurns*21{// 1 是系统消息historyappend(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(contextfmtloggithub.com/cloudwego/eino/adkgithub.com/cloudwego/eino/components/modelgithub.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;i2;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;i2;i{historyappend(history,schema.UserMessage(fmt.Sprintf(第 %d 个问题,i)))iter:runner.Run(ctx,history)reply:printOutput(iter)// 把回复追加回历史下一轮才能看到。ifreply!{historyappend(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.Outputnil||event.Output.MessageOutputnil{continue}mv:event.Output.MessageOutputifmv.Message!nil{outmv.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 日Eino ADK 概述Agent 协作SessionValues 部分Eino 主仓库本文示例按Eino v0.9.21编写使用模拟模型不访问网络已实机运行正文输出为真实结果。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑