状态管理

ADK-Rust 中的会话状态允许 Agent 存储和检索在对话轮次中持久存在的数据。状态通过键前缀进行组织,这些前缀决定了数据的范围和生命周期。

概述

状态以键值对的形式存储,其中:

  • 键是带有可选前缀的字符串
  • 值是 JSON 值 (serde_json::Value)

前缀系统支持不同的作用域级别:

  • 会话作用域:默认,绑定到单个会话
  • 用户作用域:在用户的所有会话中共享
  • 应用作用域:在应用程序的所有用户中共享
  • 临时作用域:每次调用后清除

状态 Trait

State trait 定义了状态访问的接口:

use serde_json::Value;
use std::collections::HashMap;

pub trait State: Send + Sync {
    /// Get a value by key
    fn get(&self, key: &str) -> Option<Value>;
    
    /// Set a value
    fn set(&mut self, key: String, value: Value);
    
    /// Get all state as a map
    fn all(&self) -> HashMap<String, Value>;
}

还有一个 ReadonlyState trait 用于只读访问:

pub trait ReadonlyState: Send + Sync {
    fn get(&self, key: &str) -> Option<Value>;
    fn all(&self) -> HashMap<String, Value>;
}

状态键前缀

ADK-Rust 使用三个键前缀来控制状态作用域:

前缀常量范围
app:KEY_PREFIX_APP所有用户和会话共享
user:KEY_PREFIX_USER用户的所有会话共享
temp:KEY_PREFIX_TEMP每次调用后清除
(无)-会话范围(默认)

app: - 应用程序状态

在应用程序的所有用户和会话之间共享的状态。

use adk_session::KEY_PREFIX_APP;

// KEY_PREFIX_APP = "app:"
let key = format!("{}settings", KEY_PREFIX_APP);  // "app:settings"

用例:

  • 应用程序配置
  • 共享资源
  • 全局计数器或统计信息

user: - 用户状态

在特定用户的所有会话之间共享的状态。

use adk_session::KEY_PREFIX_USER;

// KEY_PREFIX_USER = "user:"
let key = format!("{}preferences", KEY_PREFIX_USER);  // "user:preferences"

用例:

  • 用户偏好设置
  • 用户配置文件数据
  • 跨会话用户上下文

temp: - 临时状态

每次调用后清除的状态。不持久化。

use adk_session::KEY_PREFIX_TEMP;

// KEY_PREFIX_TEMP = "temp:"
let key = format!("{}current_step", KEY_PREFIX_TEMP);  // "temp:current_step"

用例:

  • 中间计算结果
  • 当前操作上下文
  • 不应持久化的数据

无前缀 - 会话状态

没有前缀的键是会话范围的(默认行为)。

let key = "conversation_topic";  // Session-scoped

用例:

  • 对话上下文
  • 会话特定数据
  • 逐轮状态

设置初始状态

在创建会话时可以初始化状态:

use adk_session::{InMemorySessionService, SessionService, CreateRequest, KEY_PREFIX_APP, KEY_PREFIX_USER};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut initial_state = HashMap::new();

    // App-scoped state
    initial_state.insert(
        format!("{}version", KEY_PREFIX_APP),
        json!("1.0.0")
    );

    // User-scoped state
    initial_state.insert(
        format!("{}name", KEY_PREFIX_USER),
        json!("Alice")
    );

    // Session-scoped state
    initial_state.insert(
        "topic".to_string(),
        json!("Getting started")
    );

    let service = InMemorySessionService::new();
    let session = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "user_123".to_string(),
        session_id: None,
        state: initial_state,
    }).await?;
    
    Ok(())
}

读取状态

通过会话的 state() 方法访问状态:

let state = session.state();

// Get a specific key
if let Some(value) = state.get("topic") {
    println!("Topic: {}", value);
}

// Get app-scoped state
if let Some(version) = state.get("app:version") {
    println!("App version: {}", version);
}

// Get all state
let all_state = state.all();
for (key, value) in all_state {
    println!("{}: {}", key, value);
}

通过事件更新状态

状态通常通过事件操作更新。当事件附加到会话时,其 state_delta 将被应用:

use adk_session::{Event, EventActions};
use serde_json::json;
use std::collections::HashMap;

let mut state_delta = HashMap::new();
state_delta.insert("counter".to_string(), json!(42));
state_delta.insert("user:last_seen".to_string(), json!("2024-01-15"));

let mut event = Event::new("invocation_123");
event.actions = EventActions {
    state_delta,
    ..Default::default()
};

// When this event is appended, state is updated
service.append_event(session.id(), event).await?;

状态作用域行为

会话服务自动处理状态作用域:

在会话创建时

  1. 提取 app: 前缀的键 → 存储到应用程序状态
  2. 提取 user: 前缀的键 → 存储到用户状态
  3. 剩余的键(除了 temp:) → 存储到会话状态
  4. 合并所有作用域以返回会话

在会话检索时

  1. 为应用程序加载应用程序状态
  2. 为用户加载用户状态
  3. 加载会话状态
  4. 合并所有作用域(应用程序 → 用户 → 会话)

在事件附加时

  1. 从事件中提取状态增量
  2. 过滤掉 temp: 键(不持久化)
  3. app: 增量应用于应用程序状态
  4. user: 增量应用于用户状态
  5. 将剩余增量应用于会话状态

完整示例

use adk_session::{
    InMemorySessionService, SessionService, CreateRequest, GetRequest,
    KEY_PREFIX_APP, KEY_PREFIX_USER,
};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let service = InMemorySessionService::new();
    
    // Create first session with initial state
    let mut state1 = HashMap::new();
    state1.insert(format!("{}theme", KEY_PREFIX_APP), json!("dark"));
    state1.insert(format!("{}language", KEY_PREFIX_USER), json!("en"));
    state1.insert("context".to_string(), json!("session1"));
    
    let session1 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s1".to_string()),
        state: state1,
    }).await?;
    
    // Create second session for same user
    let mut state2 = HashMap::new();
    state2.insert("context".to_string(), json!("session2"));
    
    let session2 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s2".to_string()),
        state: state2,
    }).await?;
    
    // Session 2 inherits app and user state
    let s2_state = session2.state();
    
    // App state is shared
    assert_eq!(s2_state.get("app:theme"), Some(json!("dark")));
    
    // User state is shared
    assert_eq!(s2_state.get("user:language"), Some(json!("en")));
    
    // Session state is separate
    assert_eq!(s2_state.get("context"), Some(json!("session2")));
    
    println!("State scoping works correctly!");
    Ok(())
}

使用状态进行指令模板化

可以使用 {key} 语法将状态值注入到代理指令中:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("personalized_assistant")
    .instruction("You are helping {user:name} with {topic}. Their preferred language is {user:language}.")
    .model(Arc::new(model))
    .build()?;

当代理运行时,{user:name}{topic}{user:language} 将被会话状态中的值替换。

最佳实践

1. 使用适当的作用域

// ✅ Good: User preferences in user scope
"user:theme"
"user:timezone"

// ✅ Good: Session-specific context without prefix
"current_task"
"conversation_summary"

// ✅ Good: App-wide settings in app scope
"app:model_version"
"app:feature_flags"

// ❌ Bad: User data in session scope (lost between sessions)
"user_preferences"  // Should be "user:preferences"

2. 将临时状态用于中间数据

// ✅ Good: Intermediate results in temp scope
"temp:search_results"
"temp:current_step"

// ❌ Bad: Intermediate data persisted unnecessarily
"search_results"  // Will be saved to database

3. 保持状态键一致

// ✅ Good: Consistent naming convention
"user:preferences.theme"
"user:preferences.language"

// ❌ Bad: Inconsistent naming
"user:theme"
"userLanguage"
"user-timezone"

上一页← 会话 | 下一页回调 →