第五章:会话与记忆 -- 让 Agent 记住你
到目前为止,我们的 Agent 每次运行完就"失忆"了。你跟它说了半天,下次再聊它啥也不记得。 这显然不行对吧?想象一下,你每次打开微信跟朋友聊天,对方都不记得你是谁 -- 那还聊个啥。
这一章,我们就来解决这个问题。让 Agent 拥有"记忆"。
本章你将学到
- Session(会话) -- 管理一次对话的完整上下文
- State(状态) -- 在对话中存储和传递键值数据
- Event(事件) -- 对话中每一条消息的结构化表示
- Runner(运行器) -- 驱动 Agent 执行的引擎
- Memory(记忆) -- 跨会话的长期记忆能力
- 两种记忆工具的区别:自动预加载 vs 主动搜索
- 动手搭建一个能记住历史对话的聊天程序
Session:对话线程
打个比方
Session 就像微信的聊天窗口。你跟张三有一个聊天窗口,跟李四有另一个聊天窗口,它们互不干扰。
每个 Session 包含: - 一个唯一 ID -- 标识这个对话 - 对话历史(Events) -- 你说了什么、Agent 回了什么,全都记录在案 - 状态字典(State) -- 存一些对话过程中需要用到的键值数据
用代码来看,Session 是一个接口:
// Session 代表用户和 Agent 之间的一系列交互
type Session interface {
ID() string // 会话唯一 ID
AppName() string // 应用名称
UserID() string // 用户 ID
State() State // 状态字典
Events() Events // 对话事件列表
LastUpdateTime() time.Time // 最后更新时间
}
一个 Session 绑定到具体的 AppName + UserID + SessionID 三元组。也就是说,
同一个用户在同一个应用中,可以有多个会话(就像微信里你可以跟同一个人有多个群聊一样)。
Session Service:会话的增删改查
Session 不会凭空出现,你需要一个 session.Service 来管理它们。
ADK 内置了一个内存版本的实现,开发和测试时用它就够了。
创建 Service
import "google.golang.org/adk/session"
// 创建一个内存版的 Session Service
// 数据存在内存里,程序一关就没了,适合开发测试
sessionService := session.InMemoryService()
创建会话
ctx := context.Background()
// 创建一个新会话
resp, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: "my_app", // 应用名
UserID: "user_123", // 用户 ID
// SessionID: "xxx", // 可选,不填就自动生成一个 UUID
// State: map[string]any{"key": "value"}, // 可选,初始状态
})
if err != nil {
log.Fatalf("创建会话失败: %v", err)
}
sess := resp.Session
fmt.Printf("会话 ID: %s\n", sess.ID())
AppName 和 UserID 是必填的,SessionID 不填的话会自动生成一个 UUID。
你也可以在创建时传入初始的 State。
获取会话
// 根据三元组获取一个已有的会话
getResp, err := sessionService.Get(ctx, &session.GetRequest{
AppName: "my_app",
UserID: "user_123",
SessionID: sess.ID(),
// NumRecentEvents: 10, // 可选,只返回最近 N 条事件
// After: time.Now().Add(-1 * time.Hour), // 可选,只返回某个时间点之后的事件
})
if err != nil {
log.Fatalf("获取会话失败: %v", err)
}
fmt.Printf("会话中有 %d 条事件\n", getResp.Session.Events().Len())
Get 还支持两个可选的过滤参数:
- NumRecentEvents:只返回最近 N 条事件,做分页或者限制上下文长度时很有用
- After:只返回某个时间点之后的事件
列出所有会话
// 列出某个用户在某个应用下的所有会话
listResp, err := sessionService.List(ctx, &session.ListRequest{
AppName: "my_app",
UserID: "user_123",
})
if err != nil {
log.Fatalf("列出会话失败: %v", err)
}
for _, s := range listResp.Sessions {
fmt.Printf("会话: %s, 最后更新: %s\n", s.ID(), s.LastUpdateTime())
}
删除会话
// 删除一个会话
err = sessionService.Delete(ctx, &session.DeleteRequest{
AppName: "my_app",
UserID: "user_123",
SessionID: sess.ID(),
})
if err != nil {
log.Fatalf("删除会话失败: %v", err)
}
完整生命周期示例
把上面这些串起来,看一个完整的 Session 管理流程:
package main
import (
"context"
"fmt"
"log"
"google.golang.org/adk/session"
)
func main() {
ctx := context.Background()
// 1. 创建 Session Service
svc := session.InMemoryService()
// 2. 创建会话
createResp, err := svc.Create(ctx, &session.CreateRequest{
AppName: "chat_app",
UserID: "xiaoming",
State: map[string]any{"language": "zh-CN"}, // 初始状态
})
if err != nil {
log.Fatal(err)
}
sess := createResp.Session
fmt.Printf("创建会话: %s\n", sess.ID())
// 3. 读取初始状态
lang, _ := sess.State().Get("language")
fmt.Printf("用户语言: %s\n", lang)
// 4. 列出该用户所有会话
listResp, _ := svc.List(ctx, &session.ListRequest{
AppName: "chat_app",
UserID: "xiaoming",
})
fmt.Printf("小明有 %d 个会话\n", len(listResp.Sessions))
// 5. 用完了可以删掉
_ = svc.Delete(ctx, &session.DeleteRequest{
AppName: "chat_app",
UserID: "xiaoming",
SessionID: sess.ID(),
})
fmt.Println("会话已删除")
}
State:Agent 的状态字典
State 是挂在 Session 上的键值存储。你可以把它理解成一个 map[string]any,
用来在对话过程中保存各种数据。
基本用法
// 获取 Session 的状态
state := sess.State()
// 写入
state.Set("user_name", "小明")
state.Set("visit_count", 1)
// 读取
name, err := state.Get("user_name")
if err != nil {
// 如果 key 不存在,会返回 session.ErrStateKeyNotExist
fmt.Println("key 不存在")
}
fmt.Println(name) // "小明"
// 遍历所有状态
for key, value := range state.All() {
fmt.Printf("%s = %v\n", key, value)
}
前缀系统:控制状态的作用域
这是 State 最精妙的设计。通过给 key 加不同的前缀,你可以控制这个状态的"可见范围":
| 前缀 | 作用域 | 生命周期 | 使用场景 |
|---|---|---|---|
| 无前缀 | Session 级别 | 整个会话期间有效 | 当前对话的上下文数据 |
temp: |
临时级别 | 单次调用结束就清除 | 中间计算结果、临时标记 |
app: |
应用级别 | 所有用户、所有会话共享 | 全局配置、公共数据 |
user: |
用户级别 | 同一用户的所有会话共享 | 用户偏好、用户画像 |
举几个具体的例子:
// Session 级别 -- 只在当前会话中可见
state.Set("current_topic", "Go语言")
// 临时级别 -- 这轮对话结束就没了
state.Set("temp:thinking_step", "分析用户意图")
// 应用级别 -- 所有用户都能看到
state.Set("app:announcement", "系统将于今晚维护")
// 用户级别 -- 小明的所有会话都能看到
state.Set("user:nickname", "Go语言爱好者")
这个设计特别实用。比如你想让 Agent 记住用户的名字,用 user: 前缀就行了,
这样不管用户开几个会话,Agent 都能叫出他的名字。
在 Instruction 模板中使用 State
State 的另一个强大用法是在 Agent 的 Instruction 中使用模板变量。
你可以用 {key_name} 语法引用 State 中的值:
agent, _ := llmagent.New(llmagent.Config{
Name: "greeter",
// Instruction 中使用 {user_name} 占位符
// ADK 会在运行时自动从 State 中查找并替换
Instruction: "你是一个友好的助手。用户的名字是 {user_name},请用他的名字打招呼。",
Model: myModel,
})
当 Agent 运行时,ADK 会自动从 Session State 中查找 user_name 这个 key,
然后替换到 Instruction 里。
几个注意事项:
- key 名必须符合 ^[a-zA-Z_][a-zA-Z0-9_]*$ 这个正则,否则会被当成普通文本
- 如果 State 中没有对应的 key,会报错。想忽略缺失的 key,在后面加个 ?,写成 {user_name?}
- 如果你不想用模板替换功能(比如 Instruction 里本来就有大括号),用 InstructionProvider 代替
// 可选的 key,找不到也不报错
Instruction: "欢迎 {user_name?}!今天可以帮你什么?"
Event:对话事件
Session 中的每条消息、每次响应,都被包装成一个 Event。
你可以把 Event 理解成聊天记录中的一条一条消息。
Event 的结构
type Event struct {
model.LLMResponse // 内嵌了 LLM 响应,包含 Content 等字段
ID string // 事件唯一 ID(自动生成)
Timestamp time.Time // 时间戳
InvocationID string // 所属的调用 ID
Branch string // 分支标识(多 Agent 场景用)
Author string // 作者:可以是 "user" 或 Agent 名字
Actions EventActions // 这个事件触发的动作
}
EventActions:事件携带的动作
type EventActions struct {
// 状态变更 -- 这个事件导致了哪些状态变化
StateDelta map[string]any
// 制品变更 -- key 是文件名,value 是版本号
ArtifactDelta map[string]int64
// Agent 转移 -- 如果设了这个,表示要把对话转给另一个 Agent
TransferToAgent string
// 升级标记 -- Agent 表示自己搞不定,需要上级处理
Escalate bool
// 跳过摘要 -- 告诉系统不要对函数返回值做摘要
SkipSummarization bool
}
判断最终响应
Event 有一个很实用的方法 IsFinalResponse(),用来判断这个事件是不是 Agent 的最终回复:
for event := range sess.Events().All() {
if event.IsFinalResponse() {
// 这是最终响应,可以展示给用户了
fmt.Printf("[%s] %s\n", event.Author, event.Content.Parts[0].Text)
}
}
什么算"最终响应"呢?简单来说,就是那些不是函数调用、不是函数返回、不是流式片段的事件。
Agent 处理一个问题可能需要好几步(调工具、查资料),但最终给用户的回复就是 IsFinalResponse() == true 的那个。
遍历事件
events := sess.Events()
// 方式一:用迭代器
for event := range events.All() {
fmt.Printf("[%s] %s: %v\n",
event.Timestamp.Format("15:04:05"),
event.Author,
event.Content,
)
}
// 方式二:用索引
for i := 0; i < events.Len(); i++ {
event := events.At(i)
fmt.Printf("第 %d 条: %s\n", i+1, event.Author)
}
Runner:Agent 执行引擎
前面几章我们创建了 Agent,但还没详细讲它是怎么跑起来的。 答案就是 Runner。
Runner 是整个 ADK 的核心调度器,它负责: 1. 管理 Session 的生命周期 2. 把用户消息喂给 Agent 3. 收集 Agent 产生的 Event 并存入 Session 4. 协调 Memory 服务的预加载 5. 在多 Agent 场景中找到该由谁来处理
创建 Runner
import (
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/memory"
)
// 先准备好各种服务
sessionService := session.InMemoryService()
memoryService := memory.InMemoryService() // 可选
// 创建 Runner
r, err := runner.New(runner.Config{
AppName: "my_app", // 应用名
Agent: rootAgent, // 根 Agent
SessionService: sessionService, // 必填,Session 管理服务
MemoryService: memoryService, // 可选,长期记忆服务
})
if err != nil {
log.Fatalf("创建 Runner 失败: %v", err)
}
runner.Config 中只有 Agent 和 SessionService 是必填的。
MemoryService 和 ArtifactService 都是可选的,按需配置。
运行 Agent
Runner 的核心方法是 Run(),它返回一个事件迭代器:
import "google.golang.org/genai"
// 构造用户消息
userMsg := genai.NewContentFromText("你好,我叫小明", genai.RoleUser)
// 运行 Agent
// 参数:ctx, 用户ID, 会话ID, 用户消息, 运行配置
for event, err := range r.Run(ctx, "user_123", sess.ID(), userMsg, agent.RunConfig{}) {
if err != nil {
log.Printf("运行出错: %v", err)
continue
}
// event 可能是中间步骤(函数调用等),也可能是最终响应
if event.IsFinalResponse() && event.Content != nil {
for _, part := range event.Content.Parts {
if part.Text != "" {
fmt.Println(part.Text)
}
}
}
}
流式输出
如果你想实现像 ChatGPT 那样逐字输出的效果,用 SSE 流式模式:
for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{
StreamingMode: agent.StreamingModeSSE, // 开启 SSE 流式输出
}) {
if err != nil {
log.Printf("错误: %v", err)
continue
}
if event.Content == nil {
continue
}
// Partial 为 true 表示这是流式片段,还没说完
if event.LLMResponse.Partial {
for _, part := range event.Content.Parts {
fmt.Print(part.Text) // 用 Print 而不是 Println,不换行
}
}
}
fmt.Println() // 最后换一行
流式模式下,Runner 会不断产出 Partial = true 的事件,每个事件包含一小段文本。
只有最后一个 Partial = false 的事件会被存入 Session。
Runner 运行流程
Runner 内部做的事情其实不少,我们来捋一下:
1. 接收用户消息
|
2. 从 SessionService 获取会话(包含历史对话)
|
3. 确定由哪个 Agent 来处理
(如果上次对话发生了 Agent 转移,就继续用那个 Agent)
|
4. 把用户消息作为 Event 存入 Session
|
5. 调用 Agent.Run() 开始处理
|
6. Agent 可能会:
- 直接回复 -> 生成 Event
- 调用工具 -> 工具返回 -> 继续处理
- 转移给子 Agent -> 子 Agent 处理
|
7. 每个非 Partial 的 Event 都存入 Session
|
8. 返回所有事件给调用者
Memory:跨会话的长期记忆
好了,Session 解决了"一次对话内记住上下文"的问题。但如果用户关掉这个会话, 开了一个新的会话呢?之前聊过的内容全没了。
这时候就需要 Memory -- 长期记忆。
打个比方
- Session 是短期记忆 -- 就像你正在跟朋友聊的这场天,你记得刚才说了什么
- Memory 是长期记忆 -- 就像你之前跟这个朋友的所有对话,你还记得一些重要的事
Memory 的工作原理是:把过去的 Session 数据"消化"进记忆库,然后在新的 Session 中, Agent 可以搜索这些记忆来回忆之前的对话。
Memory Service
和 Session 类似,Memory 也有一个 Service 接口:
import "google.golang.org/adk/memory"
// 创建内存版的 Memory Service
memoryService := memory.InMemoryService()
Memory Service 的接口非常简洁,就两个方法:
type Service interface {
// 把一个 Session 的内容添加到记忆库
AddSession(ctx context.Context, s session.Session) error
// 搜索记忆
Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error)
}
添加记忆
当一个会话结束(或者你觉得合适的时候),把它加到记忆库:
// 把某个历史会话添加到记忆
err := memoryService.AddSession(ctx, previousSession)
if err != nil {
log.Fatalf("添加记忆失败: %v", err)
}
// 同一个 Session 可以多次添加(比如对话还在进行中,你想阶段性地更新记忆)
// 新的数据会覆盖旧的
搜索记忆
// 搜索与查询相关的记忆
searchResp, err := memoryService.Search(ctx, &memory.SearchRequest{
Query: "东京旅行", // 搜索关键词
UserID: "user_123", // 限定用户
AppName: "my_app", // 限定应用
})
if err != nil {
log.Fatalf("搜索记忆失败: %v", err)
}
// 遍历搜索结果
for _, entry := range searchResp.Memories {
fmt.Printf("[%s] %s: ", entry.Timestamp.Format("2006-01-02"), entry.Author)
for _, part := range entry.Content.Parts {
fmt.Print(part.Text)
}
fmt.Println()
}
内存版的实现用的是简单的关键词匹配(分词后取交集),够开发测试用了。 生产环境可以接入向量数据库来做语义搜索。
两种记忆工具:preloadmemorytool vs loadmemorytool
ADK 提供了两种记忆工具,它们的使用方式完全不同:
preloadmemorytool -- 自动预加载
这个工具在每次 LLM 请求前自动运行,不需要 Agent 主动调用。 它会拿用户的当前输入去搜索记忆,然后把相关的历史对话注入到系统指令中。
import "google.golang.org/adk/tool/preloadmemorytool"
agent, _ := llmagent.New(llmagent.Config{
Name: "assistant",
Model: myModel,
Instruction: "你是一个有记忆的助手。",
Tools: []tool.Tool{
preloadmemorytool.New(), // 自动预加载记忆
},
})
工作原理:
1. 用户发消息:"你还记得我上次的旅行吗?"
2. preloadmemorytool 自动用这句话去搜索记忆
3. 找到相关记忆后,注入到系统指令里(放在 <PAST_CONVERSATIONS> 标签中)
4. Agent 就能"看到"这些历史对话了
优点:简单省事,Agent 不需要学会调用任何工具 缺点:每次都会搜索一遍,有时候搜到的可能不太相关
loadmemorytool -- 主动搜索
这个工具是一个标准的函数工具,Agent 需要主动决定何时去搜索记忆。 它会给 Agent 注入一条指令:"你有记忆功能,需要的时候可以调用 load_memory 来搜索。"
import "google.golang.org/adk/tool/loadmemorytool"
agent, _ := llmagent.New(llmagent.Config{
Name: "assistant",
Model: myModel,
Instruction: "你是一个有记忆的助手。当用户提到过去的事情时,搜索记忆来回答。",
Tools: []tool.Tool{
loadmemorytool.New(), // Agent 可以主动搜索记忆
},
})
工作原理:
1. 用户发消息:"你还记得我上次的旅行吗?"
2. Agent 判断需要查记忆,主动调用 load_memory(query: "旅行") 工具
3. 工具返回搜索结果
4. Agent 根据结果来回答
优点:更精准,Agent 可以根据需要构造搜索词 缺点:依赖 Agent 的判断力,有时候它可能忘了去搜
两个一起用
实际项目中,两个一起用是最靠谱的做法:
Tools: []tool.Tool{
preloadmemorytool.New(), // 兜底:自动预加载
loadmemorytool.New(), // 补充:Agent 还可以主动搜索
},
这样 preloadmemorytool 先自动加载一波相关记忆,如果 Agent 觉得不够, 还可以用 loadmemorytool 主动搜索更精确的内容。
完整对话流程
到这里,让我们把所有概念串起来,看看一次完整的对话是怎么跑的:
用户输入消息
|
v
Runner.Run(ctx, userID, sessionID, msg, cfg)
|
v
从 SessionService 获取会话(带历史事件和状态)
|
v
确定由哪个 Agent 处理
|
v
用户消息作为 Event 存入 Session
|
v
[如果有 preloadmemorytool]
预加载 Memory --> 相关记忆注入到系统指令中
|
v
Agent 开始处理
|-- 可能直接回复
|-- 可能调用工具(包括 loadmemorytool 搜索记忆)
|-- 可能转移给子 Agent
|
v
生成 Event(包含回复内容、状态变更等)
|
v
Event 存入 Session(Partial 事件除外)
|
v
返回给调用者
完整示例:有记忆的聊天机器人
下面我们来写一个完整的例子:一个能记住之前对话的聊天机器人。
这个例子会: 1. 先创建一个"历史会话"并填入一些对话(模拟之前的聊天) 2. 把历史会话加入记忆库 3. 开一个新的会话,用户可以问关于之前聊天的内容
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/memory"
"google.golang.org/adk/model"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/loadmemorytool"
"google.golang.org/adk/tool/preloadmemorytool"
)
func main() {
ctx := context.Background()
// ============================================
// 第一步:创建模型
// ============================================
llm, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// ============================================
// 第二步:创建 Agent(带记忆工具)
// ============================================
myAgent, err := llmagent.New(llmagent.Config{
Name: "memory_bot",
Description: "一个能记住历史对话的聊天机器人",
Model: llm,
Instruction: `你是一个友好的中文助手,名叫"小记"。
你有记忆功能,能回忆起之前和用户的对话。
如果用户问到之前聊过的内容,请利用你的记忆来回答。
如果预加载的记忆不够,请主动使用 load_memory 工具搜索更多信息。`,
Tools: []tool.Tool{
preloadmemorytool.New(), // 自动预加载相关记忆
loadmemorytool.New(), // 手动搜索记忆
},
})
if err != nil {
log.Fatalf("创建 Agent 失败: %v", err)
}
// ============================================
// 第三步:创建 Service
// ============================================
appName := "memory_chat"
userID := "xiaoming"
sessionService := session.InMemoryService()
memoryService := memory.InMemoryService()
// ============================================
// 第四步:创建一些"历史对话"来填充记忆
// ============================================
previousSession, err := createHistorySession(ctx, sessionService, appName, userID)
if err != nil {
log.Fatalf("创建历史会话失败: %v", err)
}
// 把历史会话添加到记忆库
if err := memoryService.AddSession(ctx, previousSession); err != nil {
log.Fatalf("添加记忆失败: %v", err)
}
fmt.Println("=== 小记聊天机器人 ===")
fmt.Println("已加载历史对话(关于一次北京旅行的记忆)")
fmt.Println("试试问:你还记得我上次去哪旅行吗?")
fmt.Println("输入 quit 退出")
fmt.Println()
// ============================================
// 第五步:创建新会话
// ============================================
createResp, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: appName,
UserID: userID,
})
if err != nil {
log.Fatalf("创建会话失败: %v", err)
}
currentSession := createResp.Session
// ============================================
// 第六步:创建 Runner
// ============================================
r, err := runner.New(runner.Config{
AppName: appName,
Agent: myAgent,
SessionService: sessionService,
MemoryService: memoryService,
})
if err != nil {
log.Fatalf("创建 Runner 失败: %v", err)
}
// ============================================
// 第七步:开始对话循环
// ============================================
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("你: ")
input, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
// 去掉换行符
input = input[:len(input)-1]
if input == "quit" {
fmt.Println("再见!")
break
}
// 构造用户消息
userMsg := genai.NewContentFromText(input, genai.RoleUser)
// 运行 Agent
fmt.Print("小记: ")
for event, err := range r.Run(ctx, userID, currentSession.ID(), userMsg, agent.RunConfig{
StreamingMode: agent.StreamingModeSSE,
}) {
if err != nil {
fmt.Printf("\n[错误] %v\n", err)
continue
}
if event.Content == nil {
continue
}
// 流式输出 -- 只打印 Partial 片段
if event.LLMResponse.Partial {
for _, part := range event.Content.Parts {
fmt.Print(part.Text)
}
}
}
fmt.Println()
fmt.Println()
}
}
// createHistorySession 创建一个填充了历史对话的 Session
// 模拟用户之前和 Agent 聊过北京旅行的话题
func createHistorySession(
ctx context.Context,
svc session.Service,
appName, userID string,
) (session.Session, error) {
// 创建一个新会话
resp, err := svc.Create(ctx, &session.CreateRequest{
AppName: appName,
UserID: userID,
})
if err != nil {
return nil, fmt.Errorf("创建会话失败: %w", err)
}
sess := resp.Session
// 模拟一段关于北京旅行的对话
conversations := []struct {
author string
content string
}{
{"user", "我上周去了北京玩了三天!"},
{"model", "北京是个好地方!三天时间玩了哪些地方呀?"},
{"user", "第一天去了故宫和天安门,故宫真的好大,走了两万多步。"},
{"model", "故宫确实很壮观,占地72万平方米呢!两万步不算多,有的人逛一整天能走三万步。"},
{"user", "第二天爬了长城,去的是八达岭,累得够呛但是风景真好。"},
{"model", "八达岭长城是最经典的段落了!不到长城非好汉,你现在是好汉了。"},
{"user", "哈哈对!第三天去了颐和园和798艺术区,798的涂鸦特别有意思。"},
{"model", "颐和园的昆明湖在秋天特别美。798艺术区是文艺青年的天堂,那里经常有很棒的展览。听起来你的北京之行非常充实!"},
}
// 把对话作为事件添加到 Session
for _, conv := range conversations {
event := session.NewEvent("history-session")
event.Author = conv.author
event.LLMResponse = model.LLMResponse{
Content: genai.NewContentFromText(conv.content, genai.Role(conv.author)),
}
if err := svc.AppendEvent(ctx, sess, event); err != nil {
return nil, fmt.Errorf("添加事件失败: %w", err)
}
}
return sess, nil
}
运行效果大概是这样的:
=== 小记聊天机器人 ===
已加载历史对话(关于一次北京旅行的记忆)
试试问:你还记得我上次去哪旅行吗?
输入 quit 退出
你: 你还记得我上次去哪旅行吗?
小记: 当然记得!你上周去了北京,玩了三天。第一天去了故宫和天安门,
走了两万多步;第二天爬了八达岭长城;第三天去了颐和园和798艺术区。
听起来是一趟很充实的旅行!
你: 我在798看到了什么?
小记: 你在798艺术区看到了很多有意思的涂鸦,你当时说那些涂鸦特别有意思。
你: quit
再见!
State 在实战中的用法
前面讲了 State 的基本概念,这里再补充几个实战中常见的模式。
模式一:在回调中读写 State
Agent 的回调函数可以访问 State,这是做自定义逻辑最常用的方式:
agent, _ := llmagent.New(llmagent.Config{
Name: "counter_bot",
Model: myModel,
Instruction: "你是一个助手。用户已经和你对话了 {visit_count?} 次。",
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) (*genai.Content, error) {
state := ctx.State()
// 读取访问计数
count := 0
if val, err := state.Get("visit_count"); err == nil {
count = val.(int)
}
// 计数加一
count++
state.Set("visit_count", count)
return nil, nil // 返回 nil 表示继续执行 Agent
},
},
})
模式二:用 OutputKey 自动保存输出
OutputKey 可以把 Agent 的回复自动存到 State 中,方便后续使用:
// 分析 Agent -- 它的输出会自动存到 State 的 "analysis_result" key
analysisAgent, _ := llmagent.New(llmagent.Config{
Name: "analyzer",
Model: myModel,
Instruction: "分析用户的问题,给出结构化的分析。",
OutputKey: "analysis_result", // 输出自动存到这个 key
})
// 回答 Agent -- 可以读取分析结果
answerAgent, _ := llmagent.New(llmagent.Config{
Name: "answerer",
Model: myModel,
Instruction: "根据分析结果 {analysis_result} 来回答用户的问题。",
})
模式三:用 temp: 前缀做一次性计算
// 在工具函数中存临时数据
func myTool(ctx tool.Context, args any) (map[string]any, error) {
// 存临时数据,这轮对话结束就自动清除
ctx.State().Set("temp:calculation_step", "正在计算...")
// ... 做一些计算 ...
return map[string]any{"result": 42}, nil
}
temp: 前缀的状态会在这次调用(invocation)结束后自动清除,
不会污染 Session 的持久状态。
小结
这一章信息量不小,我们来回顾一下关键概念:
| 概念 | 比喻 | 作用 |
|---|---|---|
| Session | 微信聊天窗口 | 管理一次对话的上下文 |
| State | 对话中的小本本 | 存储键值数据,支持不同作用域 |
| Event | 聊天记录中的一条消息 | 结构化表示每次交互 |
| Runner | 调度中心 | 串联 Agent、Session、Memory |
| Memory | 大脑的长期记忆 | 跨会话记住历史对话 |
| preloadmemorytool | 自动贴心的管家 | 每次请求前自动加载相关记忆 |
| loadmemorytool | 随叫随到的搜索引擎 | Agent 主动搜索记忆 |
State 前缀速记表:
| 前缀 | 记忆口诀 |
|---|---|
| 无前缀 | 这场聊天记得住 |
temp: |
说完就忘 |
app: |
全世界都知道 |
user: |
只有我自己知道(跨会话) |
核心的数据流是:
Session Service ←管理→ Session ←包含→ Events + State
↑
Runner ←协调→ Agent ←产生→ Event ─存入─┘
↑
Memory Service ←提供→ 历史记忆
动手练习
练习 1:带状态的问候 Agent
创建一个 Agent,使用 State 记住用户的名字。
- 第一次对话时,让 Agent 询问用户名字,并存到 user:name 中
- 后续对话直接用名字打招呼
提示:
- 用 BeforeAgentCallbacks 来检查 State 中是否有 user:name
- 用 Instruction 模板 {user_name?} 来引用状态
练习 2:对话总结器
创建一个程序: 1. 让用户进行几轮对话 2. 对话结束后,遍历 Session 中的所有 Event 3. 打印出完整的对话记录(区分用户消息和 Agent 回复) 4. 统计总共有多少轮对话
提示:
- 用 sess.Events().All() 遍历事件
- 用 event.Author 判断是用户还是 Agent
练习 3:完整的记忆聊天应用(挑战)
基于本章的完整示例,做以下增强:
1. 支持多轮对话后自动将当前会话存入记忆(每 5 轮存一次)
2. 在退出时打印对话统计:总消息数、会话时长
3. 用 app: 前缀的 State 记录全局的总对话轮次
提示:
- 用一个计数器记录当前轮次,每 5 轮调用 memoryService.AddSession()
- 对话统计可以通过遍历 Events 来计算
- app:total_conversations 可以用来记录全局数据
下一章,我们将学习 Callback 机制,看看如何在 Agent 执行的各个环节插入自定义逻辑, 实现日志记录、权限控制、结果过滤等高级功能。