ADK 会话与状态管理:三级状态、内存服务与多存储后端

想象一下,如果每次和智能体对话,它都不记得之前说了什么,会怎么样?

你可能会遇到这样的场景:

  • "我刚才问过的问题,你怎么又问一遍?"
  • "刚刚你答应的事情,怎么转眼就忘了?"
  • "我们讨论到一半,你怎么从头开始了?"

没有记忆的智能体,就像一个只有短时记忆丧失的病人------每次对话都是第一次。

这显然无法满足大多数场景的需求。

会话管理就是为了解决这个问题而生。它负责保存对话历史、维护状态、管理上下文。

在 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 接口进一步简化了设计,它只提供 GetAll 方法,不提供 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)
}

事件被认为是最终响应的条件:

  1. SkipSummarization = true:跳过摘要的事件
  2. 包含长运行工具调用
  3. 没有函数调用
  4. 没有函数响应
  5. 不是部分响应(流式模式)
  6. 没有尾随的代码执行结果

判断逻辑分为两层:

第一层:直接返回 true 的情况

  • SkipSummarization 为 true:工具明确要求跳过总结,这通常发生在 HITL 确认场景
  • LongRunningToolIDs 非空:事件包含长时间运行的工具调用,客户端需要知道这些调用

第二层:四个条件全部满足才返回 true

  1. 没有函数调用!hasFunctionCalls):如果 LLM 请求调用工具,这不是最终响应
  2. 没有函数响应!hasFunctionResponses):如果是工具响应事件,还需要 LLM 来总结
  3. 不是部分响应!Partial):流式响应的中间块不是最终响应
  4. 没有尾随的代码执行结果!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
    }
}

默认初始化了 StateDeltaArtifactDelta,避免空指针。

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:由 appNameuserID 组成
  • 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

researchercoder 是同级子智能体时,它们的分支路径不同。这样在构建 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 是一个从智能体名称到其父智能体的映射。例如,如果智能体 researcherorchestrator 的子智能体,那么 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) // 从根智能体开始遍历
}

两项关键验证:

  1. 单父约束 :通过 pointerMap 记录每个智能体实例是否已被引用。因为 Go 的 map 以指针为键,同一个智能体实例被多次添加到不同父智能体下时会被检测出来。这确保了树结构而非图结构。

  2. 名称唯一性:所有智能体在树中必须有唯一的名称。这不仅包括直接子智能体,还包括跨层级的智能体------根智能体的名称也不能被子智能体重复使用。

#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)

流程:

  1. 获取或创建会话
  2. 搜索长期记忆
  3. 追加用户消息事件
  4. 执行智能体,逐个持久化事件
  5. 将会话添加到长期记忆

10. 总结

ADK 的会话系统虽然看起来简单,但设计上有很多精巧之处:

  1. 接口抽象SessionService 接口清晰定义了会话的行为和管理

  2. 三级状态作用域:通过键前缀约定实现应用级、用户级、会话级三级状态,满足不同的共享需求

  3. 事件驱动:所有状态变更都通过事件动作来传递,可追溯、可回放

  4. 临时状态 :通过 temp: 前缀和持久化时过滤实现临时状态

  5. 多种存储实现:内存、数据库、Vertex AI,满足不同场景

  6. 长期记忆服务:支持跨会话的知识共享

  7. 线程安全:使用读写锁保护并发访问

  8. 事件过滤:支持按数量和时间过滤事件,优化长对话的性能

  9. 乐观锁:数据库实现使用版本检查防止并发冲突

会话系统是智能体记忆的基础。有了记忆,智能体才能进行有上下文的对话、维护工作流、记住用户偏好。

相关推荐
阿里云大数据AI技术1 小时前
DataWorks Data Agent的演进与工程化实践
人工智能·agent
weigangwin2 小时前
LlamaIndex 第一次试用:别先写 RAG Demo,先验上下文合同
python·ai·agent·rag·检索·llamaindex·观测性
闲研随记3 小时前
【文献阅读】BunnyFinder:探寻以太坊共识机制中的激励机制漏洞
区块链·agent·漏洞·以太坊·ppo·incentive flaws
阿拉斯攀登3 小时前
Embedding 模型选择与领域适配
人工智能·chatgpt·embedding·agent·ai编程·loop·rag
饼干哥哥3 小时前
用爱马仕多Agent组建跨境电商公司,出海赚美金
openai·agent·ai编程
SelectDB6 小时前
阶跃星辰 Agent 可观测实践:为什么 Trace 数据底座选择 SelectDB?
大数据·数据库·agent
i晟6 小时前
Hermes Agent 自进化架构深度解析(二):记忆系统——2200 字符如何成为自我整理的引擎
agent
‿hhh6 小时前
Dify核心模块详解:从文本生成到智能体
人工智能·学习·microsoft·agent·上下文·记忆
leeyi6 小时前
自定义 Workflow:Eino 之上的 Agent 编排
agent·workflow·工作流引擎