adk-go Session详解
如果说 Agent 决定了“怎么思考”,那么 Session 决定的就是“这次对话记住了什么”。
在 adk-go 里,Session 不只是一个聊天 ID,它同时承担了会话历史、短期状态、运行上下文入口这三层职责。
很多人第一次看 ADK 时,会把 Session 和 Memory 混在一起。其实两者边界很清楚:
- Session 负责单次会话线程内的上下文、事件历史和状态。
- Memory 负责跨 Session 的长期知识检索。
所以如果你要解释“为什么这个 Agent 能接着上一轮继续聊”,第一责任人通常不是 Memory,而是 Session。
Session 的内部成员
先说一个 Go 里的关键点:adk-go 并没有暴露一个 Session struct 给你直接 new,而是暴露了一个 session.Session 接口。
也就是说,框架更在意的是“Session 要提供什么能力”,而不是“你必须用哪一个具体结构体”。
官方暴露出来的核心接口如下:
1 2 3 4 5 6 7 8 9
| type Session interface { ID() string AppName() string UserID() string
State() State Events() Events LastUpdateTime() time.Time }
|
从这个接口你就能看出,一个 Session 至少包含这些核心成员:
ID():当前会话的唯一 ID。
AppName():属于哪个应用。
UserID():属于哪个用户。
State():当前会话的状态存储区。
Events():当前会话的事件历史。
LastUpdateTime():最后更新时间。
这六个成员,基本就构成了 Session 的“数据骨架”。
其中最重要的是 State 和 Events。
State 是结构化的可变状态,适合存业务变量,比如当前订单号、用户偏好、流程节点。
Events 是完整的事件流,适合记录用户输入、模型回复、工具调用、状态变更等全过程。
State 的接口很简单:
1 2 3 4 5
| type State interface { Get(string) (any, error) Set(string, any) error All() iter.Seq2[string, any] }
|
而 Event 本身也不是只有一段文本,它里面还带了调用链信息和状态变更信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| type Event struct { model.LLMResponse
ID string Timestamp time.Time
InvocationID string Branch string Author string
Actions EventActions LongRunningToolIDs []string }
type EventActions struct { StateDelta map[string]any ArtifactDelta map[string]int64 RequestedToolConfirmations map[string]toolconfirmation.ToolConfirmation TransferToAgent string Escalate bool SkipSummarization bool }
|
所以从工程角度看,Session 其实就是:
- 一份会话级元数据
- 一块可读写状态区
- 一条完整事件流水
Session 的基本设置
Session 本身一般不手动构造,而是交给 session.Service 创建和管理。
它的核心接口如下:
1 2 3 4 5 6 7
| type Service interface { Create(context.Context, *CreateRequest) (*CreateResponse, error) Get(context.Context, *GetRequest) (*GetResponse, error) List(context.Context, *ListRequest) (*ListResponse, error) Delete(context.Context, *DeleteRequest) error AppendEvent(context.Context, Session, *Event) error }
|
最基础的创建方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package main
import ( "context" "fmt" "log"
"google.golang.org/adk/session" )
func main() { ctx := context.Background() sessionSvc := session.InMemoryService()
createResp, err := sessionSvc.Create(ctx, &session.CreateRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_001", State: map[string]any{ "user:name": "张三", "user:city": "上海", "app:tenant": "cn-prod", "temp:current_intent": "refund", }, }) if err != nil { log.Fatal(err) }
s := createResp.Session fmt.Println("session id:", s.ID()) fmt.Println("app name:", s.AppName()) fmt.Println("user id:", s.UserID()) fmt.Println("event count:", s.Events().Len())
city, err := s.State().Get("user:city") if err != nil { log.Fatal(err) } fmt.Println("user city:", city) }
|
这里有一个非常值得注意的点:State 的 key 是支持作用域语义的。
adk-go 官方内置了三个前缀:
| 前缀 |
含义 |
典型用途 |
app: |
应用级共享状态 |
多用户共享配置、租户级参数 |
user: |
用户级共享状态 |
用户画像、昵称、偏好、会员等级 |
temp: |
当前 invocation 临时状态 |
单轮推理中的中间变量、临时结果 |
也就是说:
app: 适合“整个应用共享”的值。
user: 适合“同一个用户跨多个 Session 复用”的值。
temp: 适合“只在这一次调用里临时存在”的值。
其中 temp: 最特殊,它不是拿来长期保留的,而是拿来存本轮运行中的临时变量。
这里的“作用域语义”,你可以简单理解成:
同样是在 State 里存一个值,但这个值到底应该在哪个范围内生效、应该被谁复用、应该保留多久,框架会通过 key 前缀把这层含义表达出来。
如果只停留在“定义”层面,这一节会很抽象。真正写代码时,你可以把它理解成一句话:
你在 State 里存值时,不只是“存一个变量”,而是在顺手告诉框架和后续代码,这个变量应该按什么范围来使用。
获取、列出、删除 Session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| getResp, err := sessionSvc.Get(ctx, &session.GetRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_001", }) if err != nil { log.Fatal(err) } fmt.Println("loaded session:", getResp.Session.ID())
listResp, err := sessionSvc.List(ctx, &session.ListRequest{ AppName: "customer-service", UserID: "u_1001", }) if err != nil { log.Fatal(err) } fmt.Println("session count:", len(listResp.Sessions))
err = sessionSvc.Delete(ctx, &session.DeleteRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_001", }) if err != nil { log.Fatal(err) }
|
Session 是如何生成的,怎么使用的
在实际运行里,Session 的生成链路一般是这样的:
- 应用先准备一个
session.Service。
- 新会话时调用
Create(...),老会话时用已有 sessionID 继续跑。
runner.Run(...) 根据 userID + sessionID 找到对应 Session。
- Agent 在运行过程中读取
Session.State() 和 Session.Events()。
- 新产生的回复、工具调用、状态改动会被包装成
Event 并追加回 Session。
也就是说,Session 不是“跑完模型才顺便存一下”,而是整个 Runner 执行链路的核心容器。
下面给一个完整一点的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package main
import ( "context" "fmt" "log" "os"
"google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/provider/openai" "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/genai" )
func main() { ctx := context.Background()
model, err := openai.NewModel(openai.Config{ ModelName: "deepseek-chat", BaseURL: "https://api.deepseek.com/v1", APIKey: os.Getenv("DEEPSEEK_API_KEY"), }) if err != nil { log.Fatal(err) }
rootAgent, err := llmagent.New(llmagent.Config{ Name: "assistant", Model: model, Instruction: "你是一个客服助手。回答用户问题时尽量简洁,并利用上下文保持连续对话。", }) if err != nil { log.Fatal(err) }
sessionSvc := session.InMemoryService()
r, err := runner.New(runner.Config{ AppName: "customer-service", Agent: rootAgent, SessionService: sessionSvc, }) if err != nil { log.Fatal(err) }
createResp, err := sessionSvc.Create(ctx, &session.CreateRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_001", State: map[string]any{ "user:name": "张三", "user:city": "上海", }, }) if err != nil { log.Fatal(err) }
msg := genai.NewContentFromText("你好,我刚才住在哪个城市?", genai.RoleUser)
for event, err := range r.Run(ctx, "u_1001", createResp.Session.ID(), msg, agent.RunConfig{}) { if err != nil { log.Fatal(err) }
fmt.Printf("author=%s final=%v partial=%v\n", event.Author, event.IsFinalResponse(), event.Partial)
} }
|
这段代码里,Session 的角色很清楚:
Create() 负责生成一个会话容器。
runner.Run() 负责把用户输入送进这个会话。
- Runner 内部会继续把新的
Event 追加回这个会话。
如果你第二轮继续传同一个 sessionID,上下文就自然延续了:
1 2 3 4 5 6 7 8
| nextMsg := genai.NewContentFromText("那我的名字呢?", genai.RoleUser)
for event, err := range r.Run(ctx, "u_1001", "sess_001", nextMsg, agent.RunConfig{}) { if err != nil { log.Fatal(err) } fmt.Println("next event author:", event.Author) }
|
所以 Session 的真正使用方式不是“手动拼历史”,而是“把同一个 sessionID 持续交给 Runner”。
Session 的上下文(Context)
在 ADK 里,Session 不会孤零零地存在,它总是通过 Context 被 Agent 读到。
最常见的是三种上下文接口:
agent.ReadonlyContext
agent.CallbackContext
agent.InvocationContext
1. ReadonlyContext:只读地读 Session 信息
它常用于 InstructionProvider 这种动态提示词场景。
1 2 3 4 5 6 7 8 9 10 11 12 13
| agent, err := llmagent.New(llmagent.Config{ Name: "profile-aware-agent", Model: model, InstructionProvider: func(ctx agent.ReadonlyContext) (string, error) { city, _ := ctx.ReadonlyState().Get("user:city") return fmt.Sprintf( "当前用户ID=%s,会话ID=%s,用户城市=%v。请结合这些上下文回答。", ctx.UserID(), ctx.SessionID(), city, ), nil }, })
|
这里你能拿到:
ctx.UserID()
ctx.AppName()
ctx.SessionID()
ctx.InvocationID()
ctx.ReadonlyState()
也就是说,动态提示词完全可以按用户、会话、状态来实时生成。
2. CallbackContext:在回调里读写 Session State
如果你想在 Agent 执行前后打点、写临时状态、记录本轮时间戳,CallbackContext 会更方便,因为它比只读上下文多了 State()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| agent, err := llmagent.New(llmagent.Config{ Name: "trace-agent", Model: model, BeforeAgentCallbacks: []agent.BeforeAgentCallback{ func(ctx agent.CallbackContext) (*genai.Content, error) { err := ctx.State().Set("temp:request_started_at", time.Now().Format(time.RFC3339)) return nil, err }, }, AfterAgentCallbacks: []agent.AfterAgentCallback{ func(ctx agent.CallbackContext) (*genai.Content, error) { err := ctx.State().Set("temp:last_agent", ctx.AgentName()) return nil, err }, }, })
|
这类写法非常适合做:
3. InvocationContext:Agent 运行时拿到完整 Session
如果你写的是自定义 Agent,那么 Run(...) 里拿到的就是 agent.InvocationContext。
1 2 3 4 5 6 7 8 9 10
| func myRun(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { return func(yield func(*session.Event, error) bool) { currentSession := ctx.Session()
userName, _ := currentSession.State().Get("user:name") fmt.Println("current user:", userName) fmt.Println("current session:", currentSession.ID()) fmt.Println("history size:", currentSession.Events().Len()) } }
|
InvocationContext 比较像“本次运行的总入口”,它除了能拿到 Session(),还可以拿到:
Agent()
Artifacts()
Memory()
UserContent()
InvocationID()
RunConfig()
所以从调用链上说:
- Session 是数据容器
- Context 是运行时访问 Session 的入口
Session 记录的内存存储与持久化存储
这里要先澄清一个容易混淆的点:
这里说的“存储”是 Session 本身的历史记录和状态存储,不是 ADK 里那个长期记忆 memory.Service。
1. 内存存储:InMemoryService
最简单的方案就是:
1
| sessionSvc := session.InMemoryService()
|
它的特点也非常直接:
- 优点:零配置、启动快、示例代码最短。
- 缺点:进程一重启,Session 历史和 State 全丢。
- 适用场景:本地开发、demo、单元测试。
如果你只是本地验证 Agent 流程,InMemoryService 完全够用。
2. 持久化存储:MySQL + GORM
adk-go 官方已经提供了数据库版 SessionService:google.golang.org/adk/session/database。
它底层就是通过 GORM 连接关系型数据库,所以如果你项目本身就在用 GORM,这条路是最顺的。
最常见写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package main
import ( "context" "fmt" "log"
"gorm.io/driver/mysql" "gorm.io/gorm"
"google.golang.org/adk/session" sessiondb "google.golang.org/adk/session/database" )
func main() { ctx := context.Background()
dsn := "root:123456@tcp(127.0.0.1:3306)/adk_demo?charset=utf8mb4&parseTime=True&loc=Local"
sessionSvc, err := sessiondb.NewSessionService( mysql.Open(dsn), &gorm.Config{}, ) if err != nil { log.Fatal(err) }
if err := sessiondb.AutoMigrate(sessionSvc); err != nil { log.Fatal(err) }
createResp, err := sessionSvc.Create(ctx, &session.CreateRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_mysql_001", State: map[string]any{ "user:name": "张三", "user:city": "上海", }, }) if err != nil { log.Fatal(err) }
fmt.Println("created session:", createResp.Session.ID())
getResp, err := sessionSvc.Get(ctx, &session.GetRequest{ AppName: "customer-service", UserID: "u_1001", SessionID: "sess_mysql_001", }) if err != nil { log.Fatal(err) }
fmt.Println("loaded session:", getResp.Session.ID()) fmt.Println("event count:", getResp.Session.Events().Len()) }
|
这段代码的意义很大,因为它说明了一个工程上很舒服的特性:
- 你的业务代码依然只依赖
session.Service
- 你只是在初始化时把实现从
InMemoryService() 换成了 database.NewSessionService(...)
也就是说,Runner 层根本不用改。
1 2 3 4 5
| r, err := runner.New(runner.Config{ AppName: "customer-service", Agent: rootAgent, SessionService: sessionSvc, })
|
这就是接口抽象的价值。
这一层底层大概存了什么
从官方 session/database 文档可以看出,这个实现会通过 GORM 自动迁移内部存储模型,例如 storageSession、storageEvent。
你可以把它理解成至少会持久化两类信息:
- Session 元数据:
session_id / app_name / user_id / last_update_time
- Session 内容:
state 和 events
也就是说,数据库版不是只存一个“会话壳子”,而是把整条会话线程和状态一起保存下来。
如果要换 PostgreSQL / SQLite
做法其实一样,只是换一个 GORM dialector:
所以 adk-go 这一层的数据库适配思路非常朴素:
- 框架对上层暴露统一的
session.Service
- 底层交给 GORM 和不同数据库驱动去适配
如果你想用自己的 GORM 表结构
官方 session/database 已经足够大多数场景,但有些团队会希望:
- 把
state 单独拆表
- 给 event 增加租户字段、审计字段
- 对 event 内容做分区、冷热分层
这时候最直接的办法,就是自己实现 session.Service 接口。
例如你可以先定义自己的表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| type ChatSession struct { ID string `gorm:"primaryKey;size:64"` AppName string `gorm:"index:idx_app_user,priority:1;size:128"` UserID string `gorm:"index:idx_app_user,priority:2;size:128"` StateJSON datatypes.JSON `gorm:"type:json"` LastUpdateTime time.Time `gorm:"index"` CreatedAt time.Time UpdatedAt time.Time }
type ChatEvent struct { ID string `gorm:"primaryKey;size:64"` SessionID string `gorm:"index;size:64"` InvocationID string `gorm:"index;size:64"` Author string `gorm:"size:64"` Branch string `gorm:"size:255"` ContentJSON datatypes.JSON `gorm:"type:json"` ActionsJSON datatypes.JSON `gorm:"type:json"` Timestamp time.Time `gorm:"index"` CreatedAt time.Time }
|
再由你自己的 repository 去实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| type MySessionService struct { db *gorm.DB }
func (s *MySessionService) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) { panic("implement me") }
func (s *MySessionService) Get(ctx context.Context, req *session.GetRequest) (*session.GetResponse, error) { panic("implement me") }
func (s *MySessionService) List(ctx context.Context, req *session.ListRequest) (*session.ListResponse, error) { panic("implement me") }
func (s *MySessionService) Delete(ctx context.Context, req *session.DeleteRequest) error { panic("implement me") }
func (s *MySessionService) AppendEvent(ctx context.Context, sess session.Session, event *session.Event) error { panic("implement me") }
|
这条路线更重,但自由度最高,适合:
- 你已经有成熟的会话表设计
- 你要做复杂审计
- 你要把会话数据对接公司现有数据平台
如果只是普通业务系统,我的建议仍然是优先用官方的 session/database,因为它已经把 GORM 接入这一层封装好了。
最后总结
理解 adk-go 的 Session,最重要的是记住三句话:
- Session 是单个会话线程的容器,不是长期 Memory。
- Session 的核心由
ID/AppName/UserID/State/Events/LastUpdateTime 组成。
- 真正驱动 Session 生命周期的是
session.Service + runner.Run(...)。
你可以把它理解成 Agent 的“短期记忆体”和“当前运行沙盒”。
只要你把 Session 这层吃透了,后面再去看 State、Memory、Artifact、Multi-Agent 路由,很多地方都会一下子顺起来。