想象一下,如果每次和智能体对话,它都不记得之前说了什么,会怎么样?
你可能会遇到这样的场景:
- "我刚才问过的问题,你怎么又问一遍?"
- "刚刚你答应的事情,怎么转眼就忘了?"
- "我们讨论到一半,你怎么从头开始了?"
没有记忆的智能体,就像一个只有短时记忆丧失的病人------每次对话都是第一次。
这显然无法满足大多数场景的需求。
会话管理就是为了解决这个问题而生。它负责保存对话历史、维护状态、管理上下文。
在 ADK 中,会话系统是一个设计精良的子系统。它不仅负责:
- 保存对话历史(Events)
- 维护工作状态(State)
- 支持多种存储后端(内存、数据库、Vertex AI)
- 实现三级状态作用域(应用级、用户级、会话级)
- 提供长期记忆服务(Memory Service)
本文将深入 ADK 的会话系统,从接口设计到具体实现,详细解析其工作原理。
1. 核心接口设计
ADK 的会话系统建立在三个核心接口之上。它们共同定义了会话的抽象模型。
1.1 Session 接口:会话的抽象
go
type Session interface {
ID() string // 返回会话的唯一标识符
AppName() string // 返回应用名称,用于多租户隔离
UserID() string // 返回用户标识,关联到具体用户
State() State // 返回会话状态,键值存储
Events() Events // 返回事件列表,即对话历史
LastUpdateTime() time.Time // 返回最后更新时间
}
Session 接口的每个方法都承担着特定的职责。ID() 方法返回会话的唯一标识符,这个标识符用于区分不同的对话线程,在检索和更新会话时作为关键参数。AppName() 方法返回应用名称,这个字段用于多租户场景下的隔离------不同的应用可以有独立的会话空间。UserID() 方法返回用户标识,将对话关联到特定的用户,支持基于用户的状态管理和权限控制。
State() 方法返回会话的状态对象,这是一个键值存储,用于保存对话过程中产生的临时数据和上下文信息。智能体可以通过状态来记住用户的偏好、跟踪任务进度、累积信息等。Events() 方法返回事件列表,即完整的对话历史,包括用户消息、智能体回复、工具调用和响应等所有交互记录。LastUpdateTime() 方法返回会话的最后更新时间戳,这对于排序、过期检查和活动监控非常有用。
一个会话由三部分组成:身份标识(ID、AppName、UserID)用于唯一定位和关联,状态数据(State)用于保存对话上下文,历史记录(Events)用于完整追溯对话过程。这三部分共同构成了智能体与用户对话的完整容器。
1.2 State:键值状态
State 接口的设计遵循极简原则------只有三个方法:Get 用于根据键获取值,Set 用于设置键值对,All 用于返回所有键值对的迭代器。这种极简的设计有几个好处。首先是易于实现,任何键值存储都可以很容易地实现这个接口,无论是内存 Map、Redis 还是数据库。其次是易于理解,开发者不需要学习复杂的 API,只需要理解基本的键值操作概念。最后是易于替换,可以轻松切换不同的存储后端,只需要改变底层的实现,上层的调用代码无需修改。
ReadonlyState 接口进一步简化了设计,它只提供 Get 和 All 方法,不提供 Set。这种只读接口的设计遵循最小权限原则------如果某个组件只需要读取状态而不需要修改,就应该使用这个更窄的接口。这不仅可以避免意外的状态修改,还可以在编译时提供更强的类型检查。
1.3 Events:事件序列
go
type Events interface {
All() iter.Seq[*Event] // 返回所有事件的迭代器
Len() int // 返回事件数量
At(i int) *Event // 返回指定索引的事件
}
使用迭代器而不是直接返回切片,是为了支持不同的底层实现。比如,数据库实现可以实现按需加载,而不是一次性把所有事件都加载到内存中。
2. Event:对话的基本单元
Event 是会话中发生的事情的基本单元。每一条用户消息、每一次智能体回复、每一次工具调用,都会被记录为一个 Event。
2.1 Event 结构
go
type Event struct {
model.LLMResponse // 嵌入 LLM 响应内容
ID string // 事件的唯一标识符
Timestamp time.Time // 事件发生的时间戳
InvocationID string // 调用 ID,同一次调用的所有事件共享此 ID
Branch string // 分支路径,用于多智能体隔离
Author string // 事件的作者("user" 或智能体名称)
Actions EventActions // 事件附带的动作集合
LongRunningToolIDs []string // 长运行工具的 ID 列表
}
Event 结构体字段设计体现了事件驱动的核心思想。ID 字段是事件的唯一标识,每个事件在创建时都会自动生成一个 UUID 作为标识符。Timestamp 字段记录事件发生的时间,这对于事件的排序和时间范围查询非常重要。InvocationID 字段标识一次完整的用户请求调用,同一次调用中的所有事件共享这个 ID,方便追溯整个交互过程。Branch 字段用于多智能体场景中的事件隔离,当多个子智能体并行运行时,每个子智能体的事件会标记不同的分支路径,避免历史记录混淆。Author 字段标识事件的作者,可以是 "user" 表示用户消息,也可以是智能体的名称表示智能体的响应。Actions 字段携带事件附带的各种动作,如状态修改、制品更新、智能体转移等。LongRunningToolIDs 字段记录长运行工具的 ID 列表,用于提示客户端这些工具调用需较长时间完成。
2.2 EventActions:智能体的动作
EventActions 是事件携带的动作集合,定义了智能体可以执行的操作:
go
type EventActions struct {
StateDelta map[string]any // 状态增量,要修改的状态键值对
ArtifactDelta map[string]int64 // 制品增量,要更新的制品版本号
RequestedToolConfirmations map[string]toolconfirmation.ToolConfirmation // 请求的工具确认
SkipSummarization bool // 是否跳过摘要步骤
TransferToAgent string // 转移到指定智能体的名称
Escalate bool // 是否升级(退出循环)
}
EventActions 结构体的每个字段都对应着智能体执行过程中的特定动作类型。StateDelta 字段是状态增量,它记录了要修改的状态键值对。当工具或回调需要修改会话状态时,不是直接修改状态对象,而是将变更写入 StateDelta,这样状态变更可以和事件一起原子地持久化。ArtifactDelta 字段是制品增量,记录要更新的制品文件及其版本号,用于追踪文件变更。
RequestedToolConfirmations 字段用于 HITL(Human-in-the-loop)确认机制,当工具需要用户确认时,会在这里记录确认请求信息。SkipSummarization 字段是一个布尔标记,当设置为 true 时,LLM 不会对这个事件进行总结处理,这通常用于确认请求或工具响应等不需要 LLM 处理的事件。TransferToAgent 字段指定了要转移到的智能体名称,当设置为非空字符串时,控制权会转移到指定的子智能体,这是多智能体协作的核心机制。Escalate 字段用于 LoopAgent 终止条件,当设置为 true 时,循环会提前退出。
2.3 IsFinalResponse():判断是否为最终响应
Event 提供了 IsFinalResponse() 方法来判断事件是否为最终响应:
go
func (e *Event) IsFinalResponse() bool {
// 条件1:如果设置了跳过摘要标记,直接返回 true
if e.Actions.SkipSummarization || len(e.LongRunningToolIDs) > 0 {
return true
}
// 条件2:没有函数调用、没有函数响应、不是部分响应、没有尾随代码执行结果,才返回 true
return !hasFunctionCalls(&e.LLMResponse) &&
!hasFunctionResponses(&e.LLMResponse) &&
!e.LLMResponse.Partial &&
!hasTrailingCodeExecutionResult(&e.LLMResponse)
}
事件被认为是最终响应的条件:
SkipSummarization = true:跳过摘要的事件- 包含长运行工具调用
- 没有函数调用
- 没有函数响应
- 不是部分响应(流式模式)
- 没有尾随的代码执行结果
判断逻辑分为两层:
第一层:直接返回 true 的情况
SkipSummarization为 true:工具明确要求跳过总结,这通常发生在 HITL 确认场景LongRunningToolIDs非空:事件包含长时间运行的工具调用,客户端需要知道这些调用
第二层:四个条件全部满足才返回 true
- 没有函数调用 (
!hasFunctionCalls):如果 LLM 请求调用工具,这不是最终响应 - 没有函数响应 (
!hasFunctionResponses):如果是工具响应事件,还需要 LLM 来总结 - 不是部分响应 (
!Partial):流式响应的中间块不是最终响应 - 没有尾随的代码执行结果 (
!hasTrailingCodeExecutionResult):如果最后一个 Part 是代码执行结果,还需要 LLM 处理
#mermaid-svg-UbyGaRLWH3bLjVI7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UbyGaRLWH3bLjVI7 .error-icon{fill:#552222;}#mermaid-svg-UbyGaRLWH3bLjVI7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UbyGaRLWH3bLjVI7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .marker.cross{stroke:#333333;}#mermaid-svg-UbyGaRLWH3bLjVI7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UbyGaRLWH3bLjVI7 p{margin:0;}#mermaid-svg-UbyGaRLWH3bLjVI7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster-label text{fill:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster-label span{color:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster-label span p{background-color:transparent;}#mermaid-svg-UbyGaRLWH3bLjVI7 .label text,#mermaid-svg-UbyGaRLWH3bLjVI7 span{fill:#333;color:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .node rect,#mermaid-svg-UbyGaRLWH3bLjVI7 .node circle,#mermaid-svg-UbyGaRLWH3bLjVI7 .node ellipse,#mermaid-svg-UbyGaRLWH3bLjVI7 .node polygon,#mermaid-svg-UbyGaRLWH3bLjVI7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .rough-node .label text,#mermaid-svg-UbyGaRLWH3bLjVI7 .node .label text,#mermaid-svg-UbyGaRLWH3bLjVI7 .image-shape .label,#mermaid-svg-UbyGaRLWH3bLjVI7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-UbyGaRLWH3bLjVI7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .rough-node .label,#mermaid-svg-UbyGaRLWH3bLjVI7 .node .label,#mermaid-svg-UbyGaRLWH3bLjVI7 .image-shape .label,#mermaid-svg-UbyGaRLWH3bLjVI7 .icon-shape .label{text-align:center;}#mermaid-svg-UbyGaRLWH3bLjVI7 .node.clickable{cursor:pointer;}#mermaid-svg-UbyGaRLWH3bLjVI7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .arrowheadPath{fill:#333333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UbyGaRLWH3bLjVI7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UbyGaRLWH3bLjVI7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UbyGaRLWH3bLjVI7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster text{fill:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 .cluster span{color:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UbyGaRLWH3bLjVI7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UbyGaRLWH3bLjVI7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-UbyGaRLWH3bLjVI7 .icon-shape,#mermaid-svg-UbyGaRLWH3bLjVI7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UbyGaRLWH3bLjVI7 .icon-shape p,#mermaid-svg-UbyGaRLWH3bLjVI7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UbyGaRLWH3bLjVI7 .icon-shape .label rect,#mermaid-svg-UbyGaRLWH3bLjVI7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UbyGaRLWH3bLjVI7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UbyGaRLWH3bLjVI7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UbyGaRLWH3bLjVI7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
是
否
是
否
是
否
事件
SkipSummarization
= true?
返回 true
最终响应
LongRunningToolIDs
非空?
hasFunctionCalls
= true?
返回 false
非最终响应
hasFunctionResponses
= true?
Partial
= true?
hasTrailingCode
ExecutionResult?
2.4 NewEvent():创建事件
NewEvent() 函数创建一个新的事件:
go
func NewEvent(invocationID string) *Event {
return &Event{
ID: uuid.NewString(), // 生成唯一的 UUID 作为事件 ID
InvocationID: invocationID, // 绑定调用 ID
Timestamp: time.Now(), // 记录当前时间戳
Actions: EventActions{StateDelta: make(map[string]any), ArtifactDelta: make(map[string]int64)}, // 初始化状态增量和制品增量为空 map
}
}
默认初始化了 StateDelta 和 ArtifactDelta,避免空指针。
2.5 状态增量的处理
状态增量是 ADK 状态管理的核心机制。当智能体修改状态时,不是直接修改会话状态,而是通过 StateDelta 传递变更:
go
// 在工具中修改状态
func myTool(ctx tool.Context, args MyArgs) (MyResult, error) {
ctx.Actions().StateDelta["my_key"] = "my_value" // 将状态变更写入 StateDelta,而非直接修改 State
return MyResult{}, nil
}
这种设计的好处:
- 可追溯:所有状态变更都记录在事件中,可以审计
- 可回放:可以通过重放事件来重建状态
- 一致性:事件和状态变更原子地持久化
- 隔离性:多个并发操作不会互相干扰
3. 三级状态作用域
ADK 的状态管理一个很有特色的设计是三级状态作用域。
状态键通过前缀来区分不同的作用域:
go
const (
KeyPrefixApp string = "app:" // 应用级:所有用户和会话共享
KeyPrefixTemp string = "temp:" // 临时级:仅当前调用有效,调用结束后丢弃
KeyPrefixUser string = "user:" // 用户级:同一用户的所有会话共享
)
#mermaid-svg-rXuznd5ukiANv9HL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rXuznd5ukiANv9HL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rXuznd5ukiANv9HL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rXuznd5ukiANv9HL .error-icon{fill:#552222;}#mermaid-svg-rXuznd5ukiANv9HL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rXuznd5ukiANv9HL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rXuznd5ukiANv9HL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rXuznd5ukiANv9HL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rXuznd5ukiANv9HL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rXuznd5ukiANv9HL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rXuznd5ukiANv9HL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rXuznd5ukiANv9HL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rXuznd5ukiANv9HL .marker.cross{stroke:#333333;}#mermaid-svg-rXuznd5ukiANv9HL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rXuznd5ukiANv9HL p{margin:0;}#mermaid-svg-rXuznd5ukiANv9HL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rXuznd5ukiANv9HL .cluster-label text{fill:#333;}#mermaid-svg-rXuznd5ukiANv9HL .cluster-label span{color:#333;}#mermaid-svg-rXuznd5ukiANv9HL .cluster-label span p{background-color:transparent;}#mermaid-svg-rXuznd5ukiANv9HL .label text,#mermaid-svg-rXuznd5ukiANv9HL span{fill:#333;color:#333;}#mermaid-svg-rXuznd5ukiANv9HL .node rect,#mermaid-svg-rXuznd5ukiANv9HL .node circle,#mermaid-svg-rXuznd5ukiANv9HL .node ellipse,#mermaid-svg-rXuznd5ukiANv9HL .node polygon,#mermaid-svg-rXuznd5ukiANv9HL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rXuznd5ukiANv9HL .rough-node .label text,#mermaid-svg-rXuznd5ukiANv9HL .node .label text,#mermaid-svg-rXuznd5ukiANv9HL .image-shape .label,#mermaid-svg-rXuznd5ukiANv9HL .icon-shape .label{text-anchor:middle;}#mermaid-svg-rXuznd5ukiANv9HL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rXuznd5ukiANv9HL .rough-node .label,#mermaid-svg-rXuznd5ukiANv9HL .node .label,#mermaid-svg-rXuznd5ukiANv9HL .image-shape .label,#mermaid-svg-rXuznd5ukiANv9HL .icon-shape .label{text-align:center;}#mermaid-svg-rXuznd5ukiANv9HL .node.clickable{cursor:pointer;}#mermaid-svg-rXuznd5ukiANv9HL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rXuznd5ukiANv9HL .arrowheadPath{fill:#333333;}#mermaid-svg-rXuznd5ukiANv9HL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rXuznd5ukiANv9HL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rXuznd5ukiANv9HL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXuznd5ukiANv9HL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rXuznd5ukiANv9HL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXuznd5ukiANv9HL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rXuznd5ukiANv9HL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rXuznd5ukiANv9HL .cluster text{fill:#333;}#mermaid-svg-rXuznd5ukiANv9HL .cluster span{color:#333;}#mermaid-svg-rXuznd5ukiANv9HL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rXuznd5ukiANv9HL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rXuznd5ukiANv9HL rect.text{fill:none;stroke-width:0;}#mermaid-svg-rXuznd5ukiANv9HL .icon-shape,#mermaid-svg-rXuznd5ukiANv9HL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXuznd5ukiANv9HL .icon-shape p,#mermaid-svg-rXuznd5ukiANv9HL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rXuznd5ukiANv9HL .icon-shape .label rect,#mermaid-svg-rXuznd5ukiANv9HL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXuznd5ukiANv9HL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rXuznd5ukiANv9HL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rXuznd5ukiANv9HL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 所有用户共享
同一用户共享
仅当前调用
⚡ 临时状态(temp:前缀)
temp:calculation
计算结果
💬 会话级状态(无前缀)
current_topic
当前话题
workflow_step
工作流步骤
👤 用户级状态(user:前缀)
user:preferences
用户偏好
user:profile
用户档案
🌐 应用级状态(app:前缀)
app:config
全局配置
app:stats
共享统计
ADK 的三级状态作用域设计体现了对不同类型数据的精细化管理思想。app: 前缀的状态属于应用级,这些状态在整个应用范围内共享,所有用户和所有会话都可以访问。应用级状态的生命周期是永久的,适合存储全局配置、共享知识库等需要跨用户共享的信息。
user: 前缀的状态属于用户级,这些状态在同一用户的所有会话之间共享,但不同用户之间是隔离的。用户级状态的典型用途包括存储用户偏好设置、用户档案信息等与特定用户相关的数据。
temp: 前缀的状态属于临时级,这些状态只在当前调用中有效,调用结束后会被自动清理。临时状态非常适合存储中间计算结果、临时变量等不需要持久化的数据。没有前缀的状态默认属于会话级,这是最常用的作用域,适合存储与当前对话相关的状态,如对话上下文、任务进度等。会话级状态的生命周期跟随会话,会话结束后状态也会随之消失。
3.1 状态合并与分离
三级状态作用域的核心是状态合并 和状态分离两个操作。
分离:将一个状态 Map 分离成三级
go
func extractStateDeltas(delta map[string]any) (
appStateDelta, userStateDelta, sessionStateDelta map[string]any,
) {
appStateDelta = make(map[string]any) // 初始化应用级状态增量
userStateDelta = make(map[string]any) // 初始化用户级状态增量
sessionStateDelta = make(map[string]any) // 初始化会话级状态增量
for key, value := range delta {
if cleanKey, found := strings.CutPrefix(key, session.KeyPrefixApp); found {
// 如果键以 "app:" 开头,去掉前缀后放入应用级状态
appStateDelta[cleanKey] = value
} else if cleanKey, found := strings.CutPrefix(key, session.KeyPrefixUser); found {
// 如果键以 "user:" 开头,去掉前缀后放入用户级状态
userStateDelta[cleanKey] = value
} else if !strings.HasPrefix(key, session.KeyPrefixTemp) {
// 如果键不以 "temp:" 开头(即无前缀的会话级键),放入会话级状态
// "temp:" 开头的键被丢弃,不进入任何持久化状态
sessionStateDelta[key] = value
}
}
return
}
合并:将三级状态合并成一个
go
func mergeStates(appState, userState, sessionState map[string]any) map[string]any {
totalSize := len(appState) + len(userState) + len(sessionState) // 计算总容量
mergedState := make(map[string]any, totalSize) // 预分配足够容量
maps.Copy(mergedState, sessionState) // 先复制会话级状态(无前缀)
for key, value := range appState {
mergedState[session.KeyPrefixApp+key] = value // 为应用级状态加上 "app:" 前缀
}
for key, value := range userState {
mergedState[session.KeyPrefixUser+key] = value // 为用户级状态加上 "user:" 前缀
}
return mergedState
}
4. Service 接口:会话存储服务
Service 接口负责会话的创建、获取、持久化等管理操作:
go
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 // 向会话追加事件
}
4.1 GetRequest 的事件过滤
GetRequest 提供了两个可选的事件过滤参数,用于性能优化。根据 service.go 第 58-70 行的定义:
go
type GetRequest struct {
AppName string // 应用名称
UserID string // 用户标识
SessionID string // 会话标识
// NumRecentEvents 返回最多 N 个最近的事件
// 可选:如果为零,不应用此过滤
NumRecentEvents int
// After 返回时间戳 >= 给定时间的事件
// 可选:如果为零,不应用此过滤
After time.Time
}
这两个参数解决了一个实际问题:随着对话变长,会话中的事件会越来越多。如果每次都加载全部事件,会消耗大量内存和时间。通过 NumRecentEvents 可以只加载最近 N 条事件,通过 After 可以只加载某个时间点之后的事件。
4.2 AppendEvent 的临时状态清理
AppendEvent 方法的一个重要职责是追加事件到会话,同时清理临时状态键。根据接口注释:
AppendEvent is used to append an event to a session, and remove temporary state keys from the event.
这意味着 temp: 前缀的状态键在事件被持久化时会被移除。这符合临时状态的设计------它们只在单次调用中有效,调用结束后就应该被丢弃。
4.3 Memory Service:长期记忆
除了会话级别的短期记忆,ADK 还提供了长期记忆服务。长期记忆允许智能体在多个会话之间共享知识。
4.3.1 Service 接口
go
type Service interface {
AddSessionToMemory(ctx context.Context, s session.Session) error // 将会话添加到记忆中
SearchMemory(ctx context.Context, req *SearchRequest) (*SearchResponse, error) // 根据查询搜索相关记忆
}
AddSessionToMemory 方法负责将完整的会话内容添加到长期记忆库中。一个会话在其生命周期中可以被多次添加------每次添加都会更新该会话在记忆库中的记录。这个方法会提取会话中的所有事件内容,包括用户消息、智能体回复和工具交互,然后存储到记忆服务中,为后续的语义搜索做准备。SearchMemory 方法根据查询请求搜索相关的记忆条目。它接收一个搜索请求,包含查询字符串、用户标识和应用名称,然后返回匹配的记忆条目列表。如果没有任何匹配的记忆,会返回空列表而不是错误。这两个方法共同实现了跨会话的知识检索能力。
4.3.2 SearchRequest 和 SearchResponse
go
type SearchRequest struct {
Query string // 搜索查询字符串
UserID string // 用户标识
AppName string // 应用名称
}
type SearchResponse struct {
Memories []Entry // 匹配的记忆条目列表
}
type Entry struct {
ID string // 记忆条目的唯一标识
Content *genai.Content // 记忆内容
Author string // 记忆的作者
Timestamp time.Time // 记忆的时间戳
CustomMetadata map[string]any // 自定义元数据
}
4.3.3 内存实现
go
func InMemoryService() Service {
return &inMemoryService{
store: make(map[key]map[sessionID][]value), // 初始化三层嵌套的存储结构
}
}
type inMemoryService struct {
mu sync.RWMutex // 读写锁,保证线程安全
store map[key]map[sessionID][]value // 存储结构:key(应用+用户) -> 会话ID -> 记忆条目列表
}
数据结构:
key:由appName和userID组成sessionID:会话标识value:记忆条目,包含内容和预计算的关键词
AddSessionToMemory:
go
func (s *inMemoryService) AddSessionToMemory(ctx context.Context, curSession session.Session) error {
var values []value // 用于收集当前会话的所有记忆条目
for event := range curSession.Events().All() {
if event.LLMResponse.Content == nil {
continue // 跳过没有内容的事件
}
// 提取关键词用于后续搜索
words := make(map[string]struct{})
for _, part := range event.LLMResponse.Content.Parts {
if part.Text == "" {
continue // 跳过空文本
}
maps.Copy(words, extractWords(part.Text)) // 提取文本中的关键词
}
if len(words) == 0 {
continue // 跳过没有关键词的事件
}
values = append(values, value{
id: event.ID, // 事件 ID
content: event.LLMResponse.Content, // 事件内容
author: event.Author, // 事件作者
timestamp: event.Timestamp, // 事件时间戳
customMetadata: event.CustomMetadata, // 自定义元数据
words: words, // 预计算的关键词集合
})
}
// 构建存储键
k := key{appName: curSession.AppName(), userID: curSession.UserID()}
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.store[k]
if !ok {
v = map[sessionID][]value{} // 如果该键不存在,创建新的会话映射
s.store[k] = v
}
v[sessionID(curSession.ID())] = values // 将记忆条目存入对应会话
return nil
}
SearchMemory:
go
func (s *inMemoryService) SearchMemory(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// 提取查询中的关键词
queryWords := extractWords(req.Query)
k := key{appName: req.AppName, userID: req.UserID}
s.mu.RLock()
values, ok := s.store[k] // 查找该应用和用户的记忆
s.mu.RUnlock()
if !ok {
return &SearchResponse{}, nil // 没有记忆,返回空结果
}
res := &SearchResponse{}
// 简单的关键词匹配:遍历所有记忆条目,检查关键词交集
for _, events := range values {
for _, e := range events {
if checkMapsIntersect(e.words, queryWords) { // 检查记忆关键词与查询关键词是否有交集
res.Memories = append(res.Memories, Entry{...}) // 匹配则加入结果
}
}
}
return res, nil
}
4.3.4 Vertex AI 实现
ADK 还提供了基于 Vertex AI 的 Memory Service 实现。这个实现利用 Vertex AI 的向量搜索能力,提供更精准的语义搜索。
4.4 存储后端对比
三种存储后端各有其适用场景和特点。内存实现是最简单快速的选择,所有数据都保存在内存中,不需要任何外部依赖。这种实现非常适合开发和测试阶段,可以快速迭代验证功能。但内存实现的缺点也很明显------数据不持久化,应用重启后所有会话和状态都会丢失,因此不适合生产环境。
数据库实现基于 GORM,支持 SQLite、MySQL、PostgreSQL 等主流数据库。这种实现提供了数据的持久化存储,应用重启后数据不会丢失。同时,数据库还提供了强大的查询能力,可以按时间范围、应用名称、用户标识等条件检索会话。数据库实现适合中小规模的生产环境,但需要部署和维护数据库服务。
Vertex AI 实现是 Google Cloud 提供的托管服务,它不仅提供会话存储,还集成了向量搜索等高级能力。这种实现适合大规模生产环境,具有高可用性和自动扩展能力,但需要依赖 Google Cloud 服务并承担相应的成本。
由于这三种实现都遵循相同的 Service 接口,切换实现非常容易。开发阶段可以使用内存实现快速迭代,上线时只需更改一行代码切换到数据库或 Vertex AI 实现。这种接口统一的设计大大降低了开发和部署的复杂度。
5. 内存实现:inMemoryService
5.1 数据结构
go
type inMemoryService struct {
mu sync.RWMutex // 读写锁,保证并发安全
sessions omap.Map[string, *session] // 有序映射,存储所有会话
userState map[string]map[string]stateMap // 用户级状态:appName -> userID -> stateMap
appState map[string]stateMap // 应用级状态:appName -> stateMap
}
sessions:有序映射,存储所有会话userState:用户级状态,按appName -> userID -> stateMap三级嵌套appState:应用级状态,按appName -> stateMap两级嵌套mu:读写锁,保证线程安全
5.2 有序编码:高效的会话查找
sessions 使用 omap.Map(有序映射)而非普通的 map。这不是偶然的选择。根据 inmemory.go 的实现,会话的键通过 ordered.Encode 编码:
go
func (id id) Encode() string {
return string(ordered.Encode(id.appName, id.userID, id.sessionID)) // 将三个字段编码为可排序的字节串
}
ordered.Encode 把三个字符串字段编码成一个可排序的字节串。这带来一个关键优势:List 操作可以使用范围扫描(Scan(lo, hi))高效查找特定应用或用户下的所有会话:
go
func (s *inMemoryService) List(ctx context.Context, req *ListRequest) (*ListResponse, error) {
lo := id{appName: appName, userID: userID}.Encode() // 编码范围下界
hi := id{appName: appName, userID: userID + "\x00"}.Encode() // 编码范围上界(userID 后加 \x00)
for k, storedSession := range s.sessions.Scan(lo, hi) {
// 只扫描编码值在 [lo, hi) 范围内的会话
}
}
注意 hi 的构造方式:在 userID 后面加了 \x00。这是有序编码的常见技巧------\x00 是字符串的最小字符,userID + "\x00" 恰好是所有以 userID 开头的键的上界。这种范围扫描比遍历所有会话再过滤要高效得多。
5.3 trimTempDeltaState:临时状态的自动清理
当事件被持久化到会话时,AppendEvent 会调用 trimTempDeltaState 清理临时状态键。根据 inmemory.go 第 429-446 行的实现:
go
func trimTempDeltaState(event *Event) *Event {
if len(event.Actions.StateDelta) == 0 {
return event // 如果 StateDelta 为空,直接返回
}
filteredStateDelta := make(map[string]any) // 创建过滤后的状态增量
for key, value := range event.Actions.StateDelta {
if !strings.HasPrefix(key, KeyPrefixTemp) {
// 只保留不以 "temp:" 开头的键
filteredStateDelta[key] = value
}
}
event.Actions.StateDelta = filteredStateDelta // 替换为过滤后的状态增量
return event
}
这个函数遍历 StateDelta 中的所有键,移除以 temp: 开头的键。这确保了临时状态只在当前调用中有效,调用结束后,它们不会残留在会话历史中。
注意这个清理只发生在事件持久化时。在调用过程中,临时状态仍然可以通过 State.Get("temp:xxx") 正常访问。这种设计既满足了临时状态的使用需求,又避免了它们污染长期会话历史。
5.4 State 的并发安全迭代
State.All() 方法在迭代前会先复制整个状态映射,然后释放读锁再迭代:
go
func (s *state) All() iter.Seq2[string, any] {
s.mu.RLock() // 获取读锁
stateCopy := maps.Clone(s.state) // 复制状态快照
s.mu.RUnlock() // 释放读锁,避免长时间持有
return func(yield func(key string, val any) bool) {
for k, v := range stateCopy { // 在快照上迭代,而非实时状态
if !yield(k, v) {
return
}
}
}
}
这个设计避免了在迭代过程中长时间持有读锁。如果迭代回调函数执行耗时操作(比如网络请求),其他 goroutine 仍然可以读写状态。代价是迭代看到的是调用 All() 时的快照,而不是实时状态。
5.5 Create:创建会话
go
func (s *inMemoryService) Create(ctx context.Context, req *CreateRequest) (*CreateResponse, error) {
// 验证参数
if req.AppName == "" || req.UserID == "" {
return nil, fmt.Errorf("app_name and user_id are required") // 应用名和用户 ID 为必填项
}
// 生成会话 ID
sessionID := req.SessionID
if sessionID == "" {
sessionID = uuid.NewString() // 如果没有提供会话 ID,自动生成 UUID
}
s.mu.Lock()
defer s.mu.Unlock()
// 分离三级状态
s.sessions.Set(encodedKey, val) // 将编码键和会话值存入有序映射
appDelta, userDelta, _ := sessionutils.ExtractStateDeltas(req.State) // 分离应用级、用户级、会话级状态增量
appState := s.updateAppState(appDelta, req.AppName) // 更新应用级状态
userState := s.updateUserState(userDelta, req.AppName, req.UserID) // 更新用户级状态
val.state = sessionutils.MergeStates(appState, userState, state) // 合并三级状态到会话
return &CreateResponse{Session: copiedSession}, nil
}
#mermaid-svg-ukO9UvIPSDjiAMrP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ukO9UvIPSDjiAMrP .error-icon{fill:#552222;}#mermaid-svg-ukO9UvIPSDjiAMrP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ukO9UvIPSDjiAMrP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ukO9UvIPSDjiAMrP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ukO9UvIPSDjiAMrP .marker.cross{stroke:#333333;}#mermaid-svg-ukO9UvIPSDjiAMrP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ukO9UvIPSDjiAMrP p{margin:0;}#mermaid-svg-ukO9UvIPSDjiAMrP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster-label text{fill:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster-label span{color:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster-label span p{background-color:transparent;}#mermaid-svg-ukO9UvIPSDjiAMrP .label text,#mermaid-svg-ukO9UvIPSDjiAMrP span{fill:#333;color:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP .node rect,#mermaid-svg-ukO9UvIPSDjiAMrP .node circle,#mermaid-svg-ukO9UvIPSDjiAMrP .node ellipse,#mermaid-svg-ukO9UvIPSDjiAMrP .node polygon,#mermaid-svg-ukO9UvIPSDjiAMrP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ukO9UvIPSDjiAMrP .rough-node .label text,#mermaid-svg-ukO9UvIPSDjiAMrP .node .label text,#mermaid-svg-ukO9UvIPSDjiAMrP .image-shape .label,#mermaid-svg-ukO9UvIPSDjiAMrP .icon-shape .label{text-anchor:middle;}#mermaid-svg-ukO9UvIPSDjiAMrP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ukO9UvIPSDjiAMrP .rough-node .label,#mermaid-svg-ukO9UvIPSDjiAMrP .node .label,#mermaid-svg-ukO9UvIPSDjiAMrP .image-shape .label,#mermaid-svg-ukO9UvIPSDjiAMrP .icon-shape .label{text-align:center;}#mermaid-svg-ukO9UvIPSDjiAMrP .node.clickable{cursor:pointer;}#mermaid-svg-ukO9UvIPSDjiAMrP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ukO9UvIPSDjiAMrP .arrowheadPath{fill:#333333;}#mermaid-svg-ukO9UvIPSDjiAMrP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ukO9UvIPSDjiAMrP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ukO9UvIPSDjiAMrP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ukO9UvIPSDjiAMrP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ukO9UvIPSDjiAMrP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ukO9UvIPSDjiAMrP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster text{fill:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP .cluster span{color:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ukO9UvIPSDjiAMrP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ukO9UvIPSDjiAMrP rect.text{fill:none;stroke-width:0;}#mermaid-svg-ukO9UvIPSDjiAMrP .icon-shape,#mermaid-svg-ukO9UvIPSDjiAMrP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ukO9UvIPSDjiAMrP .icon-shape p,#mermaid-svg-ukO9UvIPSDjiAMrP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ukO9UvIPSDjiAMrP .icon-shape .label rect,#mermaid-svg-ukO9UvIPSDjiAMrP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ukO9UvIPSDjiAMrP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ukO9UvIPSDjiAMrP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ukO9UvIPSDjiAMrP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
CreateRequest
验证参数
(AppName, UserID)
SessionID
为空?
生成 UUID
使用提供的 ID
编码键
ordered.Encode()
会话
已存在?
返回错误
创建会话对象
分离三级状态
ExtractStateDeltas()
更新 appState
应用级状态
更新 userState
用户级状态
合并三级状态
MergeStates()
返回会话副本
5.6 AppendEvent:追加事件
go
func (s *inMemoryService) AppendEvent(ctx context.Context, curSession Session, event *Event) error {
// 部分事件不持久化
if event.Partial {
return nil // 流式响应的中间块不持久化
}
s.mu.Lock()
defer s.mu.Unlock()
// 更新内存中的会话
if err := sess.appendEvent(event); err != nil {
return err
}
// 应用状态增量
if len(event.Actions.StateDelta) > 0 {
appDelta, userDelta, sessionDelta := sessionutils.ExtractStateDeltas(event.Actions.StateDelta) // 分离三级状态增量
s.updateAppState(appDelta, curSession.AppName()) // 更新应用级状态
s.updateUserState(userDelta, curSession.AppName(), curSession.UserID()) // 更新用户级状态
maps.Copy(stored_session.state, sessionDelta) // 将会话级增量合并到存储的会话状态中
}
return nil
}
#mermaid-svg-qMH4ME4qX5Bfxwah{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qMH4ME4qX5Bfxwah .error-icon{fill:#552222;}#mermaid-svg-qMH4ME4qX5Bfxwah .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qMH4ME4qX5Bfxwah .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qMH4ME4qX5Bfxwah .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qMH4ME4qX5Bfxwah .marker.cross{stroke:#333333;}#mermaid-svg-qMH4ME4qX5Bfxwah svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qMH4ME4qX5Bfxwah p{margin:0;}#mermaid-svg-qMH4ME4qX5Bfxwah .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster-label text{fill:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster-label span{color:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster-label span p{background-color:transparent;}#mermaid-svg-qMH4ME4qX5Bfxwah .label text,#mermaid-svg-qMH4ME4qX5Bfxwah span{fill:#333;color:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah .node rect,#mermaid-svg-qMH4ME4qX5Bfxwah .node circle,#mermaid-svg-qMH4ME4qX5Bfxwah .node ellipse,#mermaid-svg-qMH4ME4qX5Bfxwah .node polygon,#mermaid-svg-qMH4ME4qX5Bfxwah .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qMH4ME4qX5Bfxwah .rough-node .label text,#mermaid-svg-qMH4ME4qX5Bfxwah .node .label text,#mermaid-svg-qMH4ME4qX5Bfxwah .image-shape .label,#mermaid-svg-qMH4ME4qX5Bfxwah .icon-shape .label{text-anchor:middle;}#mermaid-svg-qMH4ME4qX5Bfxwah .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qMH4ME4qX5Bfxwah .rough-node .label,#mermaid-svg-qMH4ME4qX5Bfxwah .node .label,#mermaid-svg-qMH4ME4qX5Bfxwah .image-shape .label,#mermaid-svg-qMH4ME4qX5Bfxwah .icon-shape .label{text-align:center;}#mermaid-svg-qMH4ME4qX5Bfxwah .node.clickable{cursor:pointer;}#mermaid-svg-qMH4ME4qX5Bfxwah .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qMH4ME4qX5Bfxwah .arrowheadPath{fill:#333333;}#mermaid-svg-qMH4ME4qX5Bfxwah .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qMH4ME4qX5Bfxwah .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qMH4ME4qX5Bfxwah .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qMH4ME4qX5Bfxwah .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qMH4ME4qX5Bfxwah .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qMH4ME4qX5Bfxwah .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster text{fill:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah .cluster span{color:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qMH4ME4qX5Bfxwah .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qMH4ME4qX5Bfxwah rect.text{fill:none;stroke-width:0;}#mermaid-svg-qMH4ME4qX5Bfxwah .icon-shape,#mermaid-svg-qMH4ME4qX5Bfxwah .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qMH4ME4qX5Bfxwah .icon-shape p,#mermaid-svg-qMH4ME4qX5Bfxwah .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qMH4ME4qX5Bfxwah .icon-shape .label rect,#mermaid-svg-qMH4ME4qX5Bfxwah .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qMH4ME4qX5Bfxwah .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qMH4ME4qX5Bfxwah .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qMH4ME4qX5Bfxwah :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
AppendEvent
Partial
= true?
直接返回
不持久化
获取写锁
s.mu.Lock()
查找存储的会话
更新内存中的会话
追加到存储会话
StateDelta
非空?
返回
分离三级状态
ExtractStateDeltas()
更新 appState
应用级状态
更新 userState
用户级状态
更新 sessionState
会话级状态
6. 数据库实现:databaseService
ADK 提供了基于 GORM 的数据库实现,支持 SQLite、MySQL、PostgreSQL 等数据库。
6.1 创建数据库服务
go
func NewSessionService(dialector gorm.Dialector, opts ...gorm.Option) (session.Service, error) {
db, err := gorm.Open(dialector, opts...) // 使用 GORM 打开数据库连接
if err != nil {
return nil, fmt.Errorf("error creating database session service: %w", err) // 包装错误信息
}
return &databaseService{db: db}, nil // 返回数据库会话服务实例
}
6.2 自动迁移
go
func AutoMigrate(service session.Service) error {
dbservice, ok := service.(*databaseService) // 类型断言,检查是否为数据库服务
if !ok {
return fmt.Errorf("invalid session service type") // 类型不匹配则返回错误
}
err := dbservice.db.AutoMigrate(&storageSession{}, &storageEvent{}, &storageAppState{}, &storageUserState{}) // 自动创建或更新四张表
if err != nil {
return fmt.Errorf("auto migrate failed: %w", err) // 迁移失败则包装错误
}
return nil
}
6.3 数据库表结构
数据库实现使用四个表:
storageSession:会话表storageEvent:事件表storageAppState:应用级状态表storageUserState:用户级状态表
6.4 Create:创建会话(数据库版)
go
func (s *databaseService) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) {
// ... 参数验证和初始化 ...
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 在事务中执行所有操作
// 获取或创建应用状态
storageApp, err := fetchStorageAppState(tx, req.AppName)
// 获取或创建用户状态
storageUser, err := fetchStorageUserState(tx, req.AppName, req.UserID)
// 分离状态增量
appDelta, userDelta, sessionState := extractStateDeltas(req.State)
// 更新应用状态
if len(appDelta) > 0 {
maps.Copy(storageApp.State, appDelta) // 将应用级增量合并到存储的应用状态
tx.Save(&storageApp) // 保存更新后的应用状态
}
// 更新用户状态
if len(userDelta) > 0 {
maps.Copy(storageUser.State, userDelta) // 将用户级增量合并到存储的用户状态
tx.Save(&storageUser) // 保存更新后的用户状态
}
// 创建会话
tx.Create(createdSession) // 在数据库中创建新会话记录
return nil
})
return &session.CreateResponse{Session: val}, nil
}
6.5 AppendEvent:追加事件(数据库版)
数据库实现的 AppendEvent 使用事务来保证一致性:
go
func (s *databaseService) applyEvent(ctx context.Context, session *localSession, event *session.Event) error {
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 在事务中执行所有操作
// 获取存储的会话
var storageSess storageSession
tx.Where(&storageSession{...}).First(&storageSess) // 按条件查询存储的会话
// 版本检查(防止并发冲突,乐观锁)
storageUpdateTime := storageSess.UpdateTime.UnixMicro() // 存储层的更新时间(微秒)
sessionUpdateTime := session.updatedAt.UnixMicro() // 本地会话的更新时间(微秒)
if storageUpdateTime > sessionUpdateTime {
return fmt.Errorf("stale session error") // 存储层版本更新,说明有并发写入,拒绝当前操作
}
// 获取应用和用户状态
storageApp, _ := fetchStorageAppState(tx, session.AppName()) // 获取应用级状态
storageUser, _ := fetchStorageUserState(tx, session.AppName(), session.UserID()) // 获取用户级状态
// 分离并应用状态增量
appDelta, userDelta, sessionDelta := extractStateDeltas(event.Actions.StateDelta)
if len(appDelta) > 0 {
maps.Copy(storageApp.State, appDelta) // 合并应用级状态增量
tx.Save(&storageApp) // 保存应用级状态
}
if len(userDelta) > 0 {
maps.Copy(storageUser.State, userDelta) // 合并用户级状态增量
tx.Save(&storageUser) // 保存用户级状态
}
if len(sessionDelta) > 0 {
maps.Copy(storageSess.State, sessionDelta) // 合并会话级状态增量
}
// 创建事件记录
storageEv, _ := createStorageEvent(session, event) // 将事件转换为存储格式
tx.Create(storageEv) // 在数据库中创建事件记录
// 更新会话时间戳
storageSess.UpdateTime = event.Timestamp // 更新会话的最后修改时间
tx.Save(&storageSess) // 保存会话
return nil
})
return err
}
数据库实现的关键特性:
- 事务保证:所有操作在一个事务中完成
- 版本检查:防止并发冲突(乐观锁)
- 时间戳精度:微秒级精度,防止舍入误差
6.6 Vertex AI 实现
ADK 还提供了基于 Google Cloud Vertex AI 的会话服务实现。这个实现将会话存储在 Vertex AI 的会话管理服务中,适合大规模生产环境。
特点:
- 托管服务:无需管理数据库
- 高可用:Google Cloud 提供的高可用性
- 自动扩展:根据负载自动扩展
- 集成 Vertex AI 功能:与其他 Vertex AI 服务深度集成
7. Branch 分支机制:子智能体的对话隔离
在多智能体系统中,一个父智能体可能有多个子智能体。当用户与子智能体 A 交互后,切换到子智能体 B 时,B 不应该看到 A 的对话历史。这就是 Branch 机制要解决的问题。
根据 session.go 第 101-108 行的定义,每个事件都有一个 Branch 字段:
go
// The branch of the event.
//
// The format is like agent_1.agent_2.agent_3, where agent_1 is
// the parent of agent_2, and agent_2 is the parent of agent_3.
//
// Branch is used when multiple sub-agent shouldn't see their peer agents'
// conversation history.
Branch string // 分支路径,格式为 agent_1.agent_2.agent_3,用点号连接从根到当前的完整路径
分支路径的格式是 agent_1.agent_2.agent_3,用点号连接从根智能体到当前智能体的完整路径。比如:
- 根智能体
orchestrator的分支是orchestrator - 子智能体
researcher的分支是orchestrator.researcher - 子智能体
coder的分支是orchestrator.coder
当 researcher 和 coder 是同级子智能体时,它们的分支路径不同。这样在构建 LLM 请求的消息历史时,可以只选择与当前智能体分支匹配的事件,实现对话隔离。
#mermaid-svg-Wn0TmvdswYJ3okSp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wn0TmvdswYJ3okSp .error-icon{fill:#552222;}#mermaid-svg-Wn0TmvdswYJ3okSp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wn0TmvdswYJ3okSp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wn0TmvdswYJ3okSp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wn0TmvdswYJ3okSp .marker.cross{stroke:#333333;}#mermaid-svg-Wn0TmvdswYJ3okSp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wn0TmvdswYJ3okSp p{margin:0;}#mermaid-svg-Wn0TmvdswYJ3okSp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster-label text{fill:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster-label span{color:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster-label span p{background-color:transparent;}#mermaid-svg-Wn0TmvdswYJ3okSp .label text,#mermaid-svg-Wn0TmvdswYJ3okSp span{fill:#333;color:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp .node rect,#mermaid-svg-Wn0TmvdswYJ3okSp .node circle,#mermaid-svg-Wn0TmvdswYJ3okSp .node ellipse,#mermaid-svg-Wn0TmvdswYJ3okSp .node polygon,#mermaid-svg-Wn0TmvdswYJ3okSp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wn0TmvdswYJ3okSp .rough-node .label text,#mermaid-svg-Wn0TmvdswYJ3okSp .node .label text,#mermaid-svg-Wn0TmvdswYJ3okSp .image-shape .label,#mermaid-svg-Wn0TmvdswYJ3okSp .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wn0TmvdswYJ3okSp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wn0TmvdswYJ3okSp .rough-node .label,#mermaid-svg-Wn0TmvdswYJ3okSp .node .label,#mermaid-svg-Wn0TmvdswYJ3okSp .image-shape .label,#mermaid-svg-Wn0TmvdswYJ3okSp .icon-shape .label{text-align:center;}#mermaid-svg-Wn0TmvdswYJ3okSp .node.clickable{cursor:pointer;}#mermaid-svg-Wn0TmvdswYJ3okSp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wn0TmvdswYJ3okSp .arrowheadPath{fill:#333333;}#mermaid-svg-Wn0TmvdswYJ3okSp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wn0TmvdswYJ3okSp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wn0TmvdswYJ3okSp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wn0TmvdswYJ3okSp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0TmvdswYJ3okSp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0TmvdswYJ3okSp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster text{fill:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp .cluster span{color:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wn0TmvdswYJ3okSp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wn0TmvdswYJ3okSp rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wn0TmvdswYJ3okSp .icon-shape,#mermaid-svg-Wn0TmvdswYJ3okSp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wn0TmvdswYJ3okSp .icon-shape p,#mermaid-svg-Wn0TmvdswYJ3okSp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wn0TmvdswYJ3okSp .icon-shape .label rect,#mermaid-svg-Wn0TmvdswYJ3okSp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0TmvdswYJ3okSp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wn0TmvdswYJ3okSp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wn0TmvdswYJ3okSp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 看不到
看不到
看不到
orchestrator
分支: orchestrator
researcher
分支: orchestrator.researcher
coder
分支: orchestrator.coder
writer
分支: orchestrator.writer
8. ParentMap:智能体树的验证与查询
分支机制依赖于智能体树结构的正确性。ADK 通过 parentmap 包来验证和维护智能体之间的父子关系。这个包虽然小巧,但在多智能体系统中扮演着关键角色。
8.1 核心数据结构
根据 parentmap/map.go 的定义:
go
type Map map[string]agent.Agent // 从智能体名称到其父智能体的映射
Map 是一个从智能体名称到其父智能体的映射。例如,如果智能体 researcher 是 orchestrator 的子智能体,那么 Map["researcher"] = orchestrator。
8.2 New():构建并验证智能体树
New() 函数以根智能体为起点,递归遍历整个智能体树,同时进行三项验证:
go
func New(root agent.Agent) (Map, error) {
res := make(map[string]agent.Agent) // 初始化结果映射
rootName := root.Name() // 记录根智能体名称
pointerMap := map[agent.Agent]string{root: "is root agent"} // 记录每个智能体实例的引用来源
var f func(cur agent.Agent) error
f = func(cur agent.Agent) error {
for _, subAgent := range cur.SubAgents() { // 遍历当前智能体的所有子智能体
// 验证1:一个智能体只能有一个父智能体
if p, ok := pointerMap[subAgent]; ok {
return fmt.Errorf(
"%q agent cannot have >1 parents, found: %q, %q",
subAgent.Name(), p, cur.Name()) // 检测到同一个智能体实例被多次引用
}
// 验证2:智能体名称在树中必须唯一
if _, ok := res[subAgent.Name()]; ok || subAgent.Name() == rootName {
return fmt.Errorf(
"agent names must be unique in the agent tree, found duplicate: %q",
subAgent.Name()) // 检测到重复的智能体名称
}
res[subAgent.Name()] = cur // 记录子智能体的父智能体
pointerMap[subAgent] = cur.Name() // 记录子智能体的引用来源
if err := f(subAgent); err != nil { // 递归遍历子智能体的子树
return err
}
}
return nil
}
return res, f(root) // 从根智能体开始遍历
}
两项关键验证:
-
单父约束 :通过
pointerMap记录每个智能体实例是否已被引用。因为 Go 的 map 以指针为键,同一个智能体实例被多次添加到不同父智能体下时会被检测出来。这确保了树结构而非图结构。 -
名称唯一性:所有智能体在树中必须有唯一的名称。这不仅包括直接子智能体,还包括跨层级的智能体------根智能体的名称也不能被子智能体重复使用。
#mermaid-svg-ipgUVoDhTNBAIgGX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ipgUVoDhTNBAIgGX .error-icon{fill:#552222;}#mermaid-svg-ipgUVoDhTNBAIgGX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ipgUVoDhTNBAIgGX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ipgUVoDhTNBAIgGX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ipgUVoDhTNBAIgGX .marker.cross{stroke:#333333;}#mermaid-svg-ipgUVoDhTNBAIgGX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ipgUVoDhTNBAIgGX p{margin:0;}#mermaid-svg-ipgUVoDhTNBAIgGX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster-label text{fill:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster-label span{color:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster-label span p{background-color:transparent;}#mermaid-svg-ipgUVoDhTNBAIgGX .label text,#mermaid-svg-ipgUVoDhTNBAIgGX span{fill:#333;color:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX .node rect,#mermaid-svg-ipgUVoDhTNBAIgGX .node circle,#mermaid-svg-ipgUVoDhTNBAIgGX .node ellipse,#mermaid-svg-ipgUVoDhTNBAIgGX .node polygon,#mermaid-svg-ipgUVoDhTNBAIgGX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ipgUVoDhTNBAIgGX .rough-node .label text,#mermaid-svg-ipgUVoDhTNBAIgGX .node .label text,#mermaid-svg-ipgUVoDhTNBAIgGX .image-shape .label,#mermaid-svg-ipgUVoDhTNBAIgGX .icon-shape .label{text-anchor:middle;}#mermaid-svg-ipgUVoDhTNBAIgGX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ipgUVoDhTNBAIgGX .rough-node .label,#mermaid-svg-ipgUVoDhTNBAIgGX .node .label,#mermaid-svg-ipgUVoDhTNBAIgGX .image-shape .label,#mermaid-svg-ipgUVoDhTNBAIgGX .icon-shape .label{text-align:center;}#mermaid-svg-ipgUVoDhTNBAIgGX .node.clickable{cursor:pointer;}#mermaid-svg-ipgUVoDhTNBAIgGX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ipgUVoDhTNBAIgGX .arrowheadPath{fill:#333333;}#mermaid-svg-ipgUVoDhTNBAIgGX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ipgUVoDhTNBAIgGX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ipgUVoDhTNBAIgGX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ipgUVoDhTNBAIgGX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ipgUVoDhTNBAIgGX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ipgUVoDhTNBAIgGX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster text{fill:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX .cluster span{color:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ipgUVoDhTNBAIgGX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ipgUVoDhTNBAIgGX rect.text{fill:none;stroke-width:0;}#mermaid-svg-ipgUVoDhTNBAIgGX .icon-shape,#mermaid-svg-ipgUVoDhTNBAIgGX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ipgUVoDhTNBAIgGX .icon-shape p,#mermaid-svg-ipgUVoDhTNBAIgGX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ipgUVoDhTNBAIgGX .icon-shape .label rect,#mermaid-svg-ipgUVoDhTNBAIgGX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ipgUVoDhTNBAIgGX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ipgUVoDhTNBAIgGX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ipgUVoDhTNBAIgGX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
New 根节点构造器 root
初始化 res map
pointerMap 标记根代理
递归遍历 root.SubAgents
pointerMap 是否已存在子代理?
错误:单父约束违反
代理名称与根名称重复?
错误:代理名称不唯一
存入映射表:name→代理、代理→名称
递归遍历当前子代理的下级代理
8.3 RootAgent():查找根智能体
RootAgent() 方法从任意智能体出发,向上追溯到根智能体:
go
func (m Map) RootAgent(cur agent.Agent) agent.Agent {
if cur == nil {
return nil // 空智能体直接返回 nil
}
for {
parent := m[cur.Name()] // 查找当前智能体的父智能体
if parent == nil {
return cur // 没有父智能体,当前就是根智能体
}
cur = parent // 继续向上追溯
}
}
这个循环简单但高效:沿着父链一直向上走,直到遇到没有父智能体的节点(即根智能体)。在多智能体系统中,这个方法用于获取全局指令------因为全局指令只存在于根智能体中。
8.4 上下文注入与提取
parentmap 通过 Go 的 context.WithValue 机制在上下文中传递:
go
func ToContext(ctx context.Context, parents Map) context.Context {
return context.WithValue(ctx, mapCtxKey, parents) // 将 ParentMap 注入到上下文中
}
func FromContext(ctx context.Context) Map {
m, ok := ctx.Value(mapCtxKey).(Map) // 从上下文中提取 ParentMap
if !ok {
return nil // 上下文中没有 ParentMap,返回 nil
}
return m
}
type ctxKey int
const mapCtxKey ctxKey = 0 // 私有类型的上下文键,避免与其他包冲突
使用私有类型 ctxKey 作为键,避免了与其他包注入的上下文值冲突------这是 Go 中 context 使用的最佳实践。
8.5 在 LLM Flow 中的使用
ParentMap 在 LLM 的请求处理链中被广泛使用。以智能体转移为例(agent_transfer.go 第 68-97 行):
go
func AgentTransferRequestProcessor(ctx agent.InvocationContext, req *model.LLMRequest, f *Flow) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
agent := ctx.Agent() // 获取当前智能体
if !shouldUseAutoFlow(agent) {
return // 如果不使用自动流程,直接返回
}
// 从上下文中获取 parentmap
parents := parentmap.FromContext(ctx)
// 计算当前智能体可以转移到的目标
targets := transferTargets(agent, parents[agent.Name()]) // 使用 parentmap 查找父智能体
if len(targets) == 0 {
return // 没有可转移的目标
}
// 注入 transfer_to_agent 工具和指令
transferToAgentTool := &TransferToAgentTool{}
si, err := instructionsForTransferToAgent(
agent, parents[agent.Name()], targets, transferToAgentTool) // 构建转移指令
// ...
}
}
transferTargets 函数使用 parentmap 来确定转移方向:
go
func transferTargets(agent, parent agent.Agent) []agent.Agent {
targets := slices.Clone(agent.SubAgents()) // 子智能体始终是可转移目标
llmAgent := asLLMAgent(agent) // 尝试将当前智能体转为 LLMAgent
llmParent := asLLMAgent(parent) // 尝试将父智能体转为 LLMAgent
if llmParent == nil {
return targets // 父智能体不是 LLMAgent,只返回子智能体
}
// 允许向父智能体转移
if !llmAgent.internal().DisallowTransferToParent {
targets = append(targets, parent) // 如果未禁止,将父智能体加入目标列表
}
// 允许向同级智能体转移
if !llmAgent.internal().DisallowTransferToPeers {
if shouldUseAutoFlow(parent) {
for _, peer := range parent.SubAgents() {
if peer.Name() != agent.Name() {
targets = append(targets, peer) // 将同级智能体(排除自己)加入目标列表
}
}
}
}
return targets
}
8.6 指令处理中的 ParentMap
在 instructionsRequestProcessor 中,parentmap 用于获取根智能体的全局指令:
go
func instructionsRequestProcessor(ctx agent.InvocationContext, req *model.LLMRequest, f *Flow) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
llmAgent := asLLMAgent(ctx.Agent()) // 获取当前智能体为 LLMAgent
if llmAgent == nil {
return // 不是 LLMAgent,直接返回
}
parents := parentmap.FromContext(ctx) // 从上下文获取 ParentMap
// 找到根智能体以获取全局指令
rootAgent := asLLMAgent(parents.RootAgent(ctx.Agent())) // 通过 ParentMap 追溯到根智能体
if rootAgent == nil {
rootAgent = llmAgent // 如果找不到根智能体,使用当前智能体作为根
}
// 先注入全局指令
if err := appendGlobalInstructions(ctx, req, rootAgent.internal()); err != nil {
yield(nil, fmt.Errorf("failed to append global instructions: %w", err)) // 全局指令注入失败
return
}
// 再注入智能体自身指令
if err := appendInstructions(ctx, req, llmAgent.internal()); err != nil {
yield(nil, fmt.Errorf("failed to append instructions: %w", err)) // 自身指令注入失败
return
}
}
}
这个设计确保了全局指令(来自根智能体)作用于所有子智能体,而子智能体可以通过自身指令覆盖或补充全局指令。
9. 会话系统在 Runner 中的使用
MemoryService Session SessionService Runner MemoryService Session SessionService Runner #mermaid-svg-jZ76aAalLvF8zbyI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jZ76aAalLvF8zbyI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jZ76aAalLvF8zbyI .error-icon{fill:#552222;}#mermaid-svg-jZ76aAalLvF8zbyI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jZ76aAalLvF8zbyI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jZ76aAalLvF8zbyI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jZ76aAalLvF8zbyI .marker.cross{stroke:#333333;}#mermaid-svg-jZ76aAalLvF8zbyI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jZ76aAalLvF8zbyI p{margin:0;}#mermaid-svg-jZ76aAalLvF8zbyI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jZ76aAalLvF8zbyI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jZ76aAalLvF8zbyI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jZ76aAalLvF8zbyI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jZ76aAalLvF8zbyI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jZ76aAalLvF8zbyI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jZ76aAalLvF8zbyI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jZ76aAalLvF8zbyI .sequenceNumber{fill:white;}#mermaid-svg-jZ76aAalLvF8zbyI #sequencenumber{fill:#333;}#mermaid-svg-jZ76aAalLvF8zbyI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jZ76aAalLvF8zbyI .messageText{fill:#333;stroke:none;}#mermaid-svg-jZ76aAalLvF8zbyI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jZ76aAalLvF8zbyI .labelText,#mermaid-svg-jZ76aAalLvF8zbyI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jZ76aAalLvF8zbyI .loopText,#mermaid-svg-jZ76aAalLvF8zbyI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jZ76aAalLvF8zbyI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jZ76aAalLvF8zbyI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jZ76aAalLvF8zbyI .noteText,#mermaid-svg-jZ76aAalLvF8zbyI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jZ76aAalLvF8zbyI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jZ76aAalLvF8zbyI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jZ76aAalLvF8zbyI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jZ76aAalLvF8zbyI .actorPopupMenu{position:absolute;}#mermaid-svg-jZ76aAalLvF8zbyI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jZ76aAalLvF8zbyI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jZ76aAalLvF8zbyI .actor-man circle,#mermaid-svg-jZ76aAalLvF8zbyI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jZ76aAalLvF8zbyI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 会话存在 会话不存在 loop 每个智能体事件 Get(AppName, UserID, SessionID) 返回会话 Create(AppName, UserID, SessionID) 返回新会话 SearchMemory(query) 返回相关记忆 AppendEvent(session, userEvent) AppendEvent(session, agentEvent) AddSessionToMemory(session)
流程:
- 获取或创建会话
- 搜索长期记忆
- 追加用户消息事件
- 执行智能体,逐个持久化事件
- 将会话添加到长期记忆
10. 总结
ADK 的会话系统虽然看起来简单,但设计上有很多精巧之处:
-
接口抽象 :
Session和Service接口清晰定义了会话的行为和管理 -
三级状态作用域:通过键前缀约定实现应用级、用户级、会话级三级状态,满足不同的共享需求
-
事件驱动:所有状态变更都通过事件动作来传递,可追溯、可回放
-
临时状态 :通过
temp:前缀和持久化时过滤实现临时状态 -
多种存储实现:内存、数据库、Vertex AI,满足不同场景
-
长期记忆服务:支持跨会话的知识共享
-
线程安全:使用读写锁保护并发访问
-
事件过滤:支持按数量和时间过滤事件,优化长对话的性能
-
乐观锁:数据库实现使用版本检查防止并发冲突
会话系统是智能体记忆的基础。有了记忆,智能体才能进行有上下文的对话、维护工作流、记住用户偏好。