第五章:会话与记忆 -- 让 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())

AppNameUserID 是必填的,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 中只有 AgentSessionService 是必填的。 MemoryServiceArtifactService 都是可选的,按需配置。

运行 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 存入 SessionPartial 事件除外)
    |
    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 执行的各个环节插入自定义逻辑, 实现日志记录、权限控制、结果过滤等高级功能。