跳转至

Session 会话管理

概述

tRPC-Agent-Go 框架提供了强大的会话(Session)管理功能,用于维护 Agent 与用户交互过程中的对话历史和上下文信息。通过自动持久化对话记录、智能摘要压缩和灵活的存储后端,会话管理为构建有状态的智能 Agent 提供了完整的基础设施。

定位

Session 用于管理当前会话的上下文,隔离维度为 <appName, userID, SessionID>,保存这一段对话里的用户消息、Agent 回复、工具调用结果以及基于这些内容生成的简要摘要,用于支撑多轮问答场景。

在同一条对话中,它让多轮问答之间能够自然承接,避免用户在每一轮都重新描述同一个问题或提供相同参数。

🎯 核心特性

  • 上下文管理:自动加载历史对话,实现真正的多轮对话
  • 会话摘要:使用 LLM 自动压缩长对话历史,在保留关键上下文的同时显著降低 token 消耗
  • 事件限制:控制每个会话存储的最大事件数量,防止内存溢出
  • TTL 管理:支持会话数据的自动过期清理
  • 多存储后端:支持内存、SQLite、Redis、PostgreSQL、MySQL、ClickHouse 存储
  • 并发安全:内置读写锁保证并发访问安全
  • 自动管理:集成 Runner 后自动处理会话创建、加载和更新
  • 软删除支持:PostgreSQL/MySQL/ClickHouse 支持软删除,数据可恢复

快速开始

集成到 Runner

tRPC-Agent-Go 的会话管理通过 runner.WithSessionService 集成到 Runner 中,Runner 会自动处理会话的创建、加载、更新和持久化。

支持的存储后端: 内存(Memory)、SQLite、Redis、PostgreSQL、MySQL、ClickHouse

默认行为: 如果不配置 runner.WithSessionService,Runner 会默认使用内存存储(Memory),数据在进程重启后会丢失。

基础示例

package main

import (
    "context"
    "fmt"
    "time"

    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/model"
    "trpc.group/trpc-go/trpc-agent-go/model/openai"
    "trpc.group/trpc-go/trpc-agent-go/runner"
    "trpc.group/trpc-go/trpc-agent-go/session/inmemory"
    "trpc.group/trpc-go/trpc-agent-go/session/summary" // 可选:启用摘要功能时需要
)

func main() {
    // 1. 创建 LLM 模型
    llm := openai.New("gpt-4", openai.WithAPIKey("your-api-key"))

    // 2. (可选)创建摘要器 - 自动压缩长对话历史
    summarizer := summary.NewSummarizer(
        llm, // 使用相同的 LLM 模型生成摘要
        summary.WithChecksAny( // 任一条件满足即触发摘要
            summary.CheckEventThreshold(20),           // 超过 20 个事件后触发
            summary.CheckTokenThreshold(4000),         // 超过 4000 个 token 后触发
            summary.CheckTimeThreshold(5*time.Minute), // 5 分钟无活动后触发
        ),
        summary.WithMaxSummaryWords(200), // 限制摘要在 200 字以内
    )

    // 3. 创建 Session Service(可选,不配置则使用默认内存存储)
    sessionService := inmemory.NewSessionService(
        inmemory.WithSummarizer(summarizer), // 可选:注入摘要器
        inmemory.WithAsyncSummaryNum(2),     // 可选:2 个异步 worker
        inmemory.WithSummaryQueueSize(100),  // 可选:队列大小 100
    )

    // 4. 创建 Agent
    agent := llmagent.New(
        "my-agent",
        llmagent.WithModel(llm),
        llmagent.WithInstruction("你是一个智能助手"),
        llmagent.WithAddSessionSummary(true), // 可选:启用摘要注入到上下文
        // 注意:WithAddSessionSummary(true) 时会忽略 WithMaxHistoryRuns 配置
        // 摘要会包含所有历史,增量事件会完整保留
    )

    // 5. 创建 Runner 并注入 Session Service
    r := runner.NewRunner(
        "my-agent",
        agent,
        runner.WithSessionService(sessionService),
    )

    // 6. 第一次对话
    ctx := context.Background()
    userMsg1 := model.NewUserMessage("我叫张三")
    eventChan, err := r.Run(ctx, "user123", "session-001", userMsg1)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Print("AI: ")
    for event := range eventChan {
        if event == nil || event.Response == nil {
            continue
        }
        if event.Response.Error != nil {
            fmt.Printf("\nError: %s (type: %s)\n", event.Response.Error.Message, event.Response.Error.Type)
            continue
        }
        if len(event.Response.Choices) > 0 {
            choice := event.Response.Choices[0]
            // 流式输出,优先使用 Delta.Content,否则使用 Message.Content
            if choice.Delta.Content != "" {
                fmt.Print(choice.Delta.Content)
            } else if choice.Message.Content != "" {
                fmt.Print(choice.Message.Content)
            }
        }
        if event.IsFinalResponse() {
            break
        }
    }
    fmt.Println()

    // 7. 第二次对话 - 自动加载历史,AI 能记住用户名字
    userMsg2 := model.NewUserMessage("我叫什么名字?")
    eventChan, err = r.Run(ctx, "user123", "session-001", userMsg2)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Print("AI: ")
    for event := range eventChan {
        if event == nil || event.Response == nil {
            continue
        }
        if event.Response.Error != nil {
            fmt.Printf("\nError: %s (type: %s)\n", event.Response.Error.Message, event.Response.Error.Type)
            continue
        }
        if len(event.Response.Choices) > 0 {
            choice := event.Response.Choices[0]
            // 流式输出,优先使用 Delta.Content,否则使用 Message.Content
            if choice.Delta.Content != "" {
                fmt.Print(choice.Delta.Content)
            } else if choice.Message.Content != "" {
                fmt.Print(choice.Message.Content)
            }
        }
        if event.IsFinalResponse() {
            break
        }
    }
    fmt.Println() // 输出:你叫张三
}

Runner 自动提供的能力

集成 Session Service 后,Runner 会自动提供以下能力,无需手动调用任何 Session API

  1. 自动会话创建:首次对话时自动创建会话(如果 SessionID 为空则生成 UUID)
  2. 自动会话加载:每次对话开始时自动加载历史上下文
  3. 自动会话更新:对话结束后自动保存新的事件
  4. 上下文连续性:自动将历史对话注入到 LLM 输入,实现多轮对话
  5. 自动摘要生成(可选):满足触发条件时后台异步生成摘要,无需手动干预

核心概念

Session 结构

Session 是会话管理的核心数据结构,包含以下字段:

字段 类型 说明
ID string 会话 ID
AppName string 应用名称
UserID string 用户 ID
State StateMap 会话状态(键值对)
Events []event.Event 会话事件列表
Tracks map[Track]*TrackEvents Track 事件映射
Summaries map[string]*Summary 会话摘要映射
UpdatedAt time.Time 最后更新时间
CreatedAt time.Time 创建时间

Key 结构

Session 通过 Key 结构唯一标识:

1
2
3
4
5
type Key struct {
    AppName   string // app name
    UserID    string // user id
    SessionID string // session id
}

Service 接口

所有存储后端都实现了 session.Service 接口:

type Service interface {
    // CreateSession creates a new session.
    CreateSession(ctx context.Context, key Key, state StateMap, options ...Option) (*Session, error)

    // GetSession gets a session.
    GetSession(ctx context.Context, key Key, options ...Option) (*Session, error)

    // ListSessions lists all sessions by user scope.
    ListSessions(ctx context.Context, userKey UserKey, options ...Option) ([]*Session, error)

    // DeleteSession deletes a session.
    DeleteSession(ctx context.Context, key Key, options ...Option) error

    // UpdateAppState updates the app-level state.
    UpdateAppState(ctx context.Context, appName string, state StateMap) error

    // DeleteAppState deletes the app-level state by key.
    DeleteAppState(ctx context.Context, appName string, key string) error

    // ListAppStates lists all app-level states.
    ListAppStates(ctx context.Context, appName string) (StateMap, error)

    // UpdateUserState updates the user-level state.
    UpdateUserState(ctx context.Context, userKey UserKey, state StateMap) error

    // ListUserStates lists all user-level states.
    ListUserStates(ctx context.Context, userKey UserKey) (StateMap, error)

    // DeleteUserState deletes the user-level state by key.
    DeleteUserState(ctx context.Context, userKey UserKey, key string) error

    // UpdateSessionState updates the session-level state directly.
    UpdateSessionState(ctx context.Context, key Key, state StateMap) error

    // AppendEvent appends an event to a session.
    AppendEvent(ctx context.Context, session *Session, event *event.Event, options ...Option) error

    // CreateSessionSummary triggers summarization for the session.
    CreateSessionSummary(ctx context.Context, sess *Session, filterKey string, force bool) error

    // EnqueueSummaryJob enqueues a summary job for asynchronous processing.
    EnqueueSummaryJob(ctx context.Context, sess *Session, filterKey string, force bool) error

    // GetSessionSummaryText returns the latest summary text for the session.
    GetSessionSummaryText(ctx context.Context, sess *Session, opts ...SummaryOption) (string, bool)

    // Close closes the service.
    Close() error
}

核心能力详解

1️⃣ 上下文管理

会话管理的核心功能是维护对话上下文,确保 Agent 能够记住历史交互并基于历史进行智能响应。

工作原理:

  • 自动保存每轮对话的用户输入和 AI 响应
  • 在新对话开始时自动加载历史事件
  • Runner 自动将历史上下文注入到 LLM 输入中

默认行为: 通过 Runner 集成后,上下文管理完全自动化,无需手动干预。

2️⃣ 事件限制(EventLimit)

控制每个会话存储的最大事件数量,防止长时间对话导致内存溢出。

工作机制:

  • 超过限制时自动淘汰最老的事件(FIFO)
  • 只影响存储,不影响业务逻辑
  • 适用于所有存储后端

配置示例:

1
2
3
4
// 限制每个会话最多保存 500 个事件
sessionService := inmemory.NewSessionService(
    inmemory.WithSessionEventLimit(500),
)

推荐配置:

场景 推荐值 说明
短期对话 100-200 客服咨询、单次任务
中期会话 500-1000 日常助手、多轮协作
长期会话 1000-2000 个人助理、持续项目(需配合摘要)
调试/测试 50-100 快速验证,减少干扰

3️⃣ TTL 管理(自动过期)

支持为会话数据设置生存时间(Time To Live),自动清理过期数据。

支持的 TTL 类型:

  • SessionTTL:会话状态和事件的过期时间
  • AppStateTTL:应用级状态的过期时间
  • UserStateTTL:用户级状态的过期时间

配置示例:

1
2
3
4
5
sessionService := inmemory.NewSessionService(
    inmemory.WithSessionTTL(30*time.Minute),
    inmemory.WithAppStateTTL(24*time.Hour),
    inmemory.WithUserStateTTL(7*24*time.Hour),
)

TTL 刷新行为:

TTL 仅在写操作时刷新(如 CreateSession、AppendEvent、UpdateSessionState 等)。读操作(GetSession)不会刷新 TTL。

过期行为:

存储类型 过期机制 自动清理
内存存储 定期扫描 + 访问时检查
SQLite 定期扫描(软删除或硬删除)
Redis 存储 Redis 原生 TTL
PostgreSQL 定期扫描(软删除或硬删除)
MySQL 定期扫描(软删除或硬删除)
ClickHouse 应用层清理 + Native TTL

存储后端对比

tRPC-Agent-Go 提供六种会话存储后端,满足不同场景需求:

存储类型 适用场景 持久化 分布式 复杂查询
内存存储 开发测试、小规模
SQLite 本地持久化、单机
Redis 存储 生产环境、分布式
PostgreSQL 生产环境、复杂查询
MySQL 生产环境、复杂查询
ClickHouse 生产环境、海量日志

Hook 能力

Session Service 支持 Hook 机制,允许在事件写入和会话读取时进行拦截和修改。

AppendEventHook

事件写入前的拦截/修改/终止。可用于内容安全、审计打标,或直接阻断存储。

1
2
3
4
5
6
7
8
type AppendEventContext struct {
    Context context.Context
    Session *Session
    Event   *event.Event
    Key     Key
}

type AppendEventHook func(ctx *AppendEventContext, next func() error) error

GetSessionHook

会话读取后的拦截/修改/过滤。可用来剔除带特定标签的事件,或动态补充返回的 Session 状态。

1
2
3
4
5
6
7
type GetSessionContext struct {
    Context context.Context
    Key     Key
    Options *Options
}

type GetSessionHook func(ctx *GetSessionContext, next func() (*Session, error)) (*Session, error)

使用示例

sessionService := inmemory.NewSessionService(
    inmemory.WithAppendEventHook(func(ctx *session.AppendEventContext, next func() error) error {
        // 存储前进行内容过滤
        if containsSensitiveContent(ctx.Event) {
            return fmt.Errorf("sensitive content detected")
        }
        return next()
    }),
    inmemory.WithGetSessionHook(func(ctx *session.GetSessionContext, next func() (*session.Session, error)) (*session.Session, error) {
        sess, err := next()
        if err != nil {
            return nil, err
        }
        // 读取后过滤事件
        sess.Events = filterEvents(sess.Events)
        return sess, nil
    }),
)

责任链执行:Hook 通过 next() 形成链式调用,可提前返回以短路后续逻辑,错误会向上传递。

跨后端一致:内存、Redis、PostgreSQL、MySQL、ClickHouse 所有存储后端均已统一接入 Hook 机制,构造服务时注入 Hook 切片即可,使用方式完全一致。

高级用法

直接使用 Session Service API

在大多数情况下,您应该通过 Runner 使用会话管理,Runner 会自动处理所有细节。但在某些特殊场景下(如会话管理后台、数据迁移、统计分析等),您可能需要直接操作 Session Service。

查询会话列表

1
2
3
4
5
6
7
8
sessions, err := sessionService.ListSessions(ctx, session.UserKey{
    AppName: "my-agent",
    UserID:  "user123",
})

for _, sess := range sessions {
    fmt.Printf("SessionID: %s, Events: %d\n", sess.ID, len(sess.Events))
}

手动删除会话

1
2
3
4
5
err := sessionService.DeleteSession(ctx, session.Key{
    AppName:   "my-agent",
    UserID:    "user123",
    SessionID: "session-id-123",
})

手动获取会话详情

// 获取完整会话
sess, err := sessionService.GetSession(ctx, session.Key{
    AppName:   "my-agent",
    UserID:    "user123",
    SessionID: "session-id-123",
})

// 获取最近 10 个事件的会话
sess, err := sessionService.GetSession(ctx, key,
    session.WithEventNum(10))

// 获取指定时间后的事件
sess, err := sessionService.GetSession(ctx, key,
    session.WithEventTime(time.Now().Add(-1*time.Hour)))

直接追加事件到会话

在某些场景下,您可能需要直接将事件追加到会话中,而不调用模型。这在以下场景中很有用:

  • 从外部源预加载对话历史
  • 在首次用户查询前插入系统消息或上下文
  • 将用户操作或元数据记录为事件
  • 以编程方式构建对话上下文

重要提示:Event 既可以表示用户请求,也可以表示模型响应。当您使用 Runner.Run() 时,框架会自动为用户消息和助手回复创建事件。

示例:追加用户消息

import (
    "context"
    "github.com/google/uuid"
    "trpc.group/trpc-go/trpc-agent-go/event"
    "trpc.group/trpc-go/trpc-agent-go/model"
    "trpc.group/trpc-go/trpc-agent-go/session"
)

// 获取或创建会话
sessionKey := session.Key{
    AppName:   "my-agent",
    UserID:    "user123",
    SessionID: "session-123",
}
sess, err := sessionService.GetSession(ctx, sessionKey)
if err != nil {
    return err
}
if sess == nil {
    sess, err = sessionService.CreateSession(ctx, sessionKey, session.StateMap{})
    if err != nil {
        return err
    }
}

// 创建用户消息
message := model.NewUserMessage("你好,我正在学习 Go 编程。")

// 创建事件,必填字段:
// - invocationID: 唯一标识符(必填)
// - author: 事件作者,用户消息使用 "user"(必填)
// - response: *model.Response,包含 Choices 和 Message(必填)
invocationID := uuid.New().String()
evt := event.NewResponseEvent(
    invocationID, // 必填:唯一调用标识符
    "user",       // 必填:事件作者
    &model.Response{
        Done: false, // 推荐:非最终事件设为 false
        Choices: []model.Choice{
            {
                Index:   0,       // 必填:选择索引
                Message: message, // 必填:包含 Content 或 ContentParts 的消息
            },
        },
    },
)
evt.RequestID = uuid.New().String() // 可选:用于追踪

// 追加事件到会话
if err := sessionService.AppendEvent(ctx, sess, evt); err != nil {
    return fmt.Errorf("append event failed: %w", err)
}

示例:追加系统消息

systemMessage := model.Message{
    Role:    model.RoleSystem,
    Content: "你是一个专门帮助 Go 编程的助手。",
}

evt := event.NewResponseEvent(
    uuid.New().String(),
    "system", // 系统消息的作者
    &model.Response{
        Done:    false,
        Choices: []model.Choice{{Index: 0, Message: systemMessage}},
    },
)

if err := sessionService.AppendEvent(ctx, sess, evt); err != nil {
    return err
}

示例:追加助手消息

assistantMessage := model.Message{
    Role:    model.RoleAssistant,
    Content: "Go 是一种静态类型、编译型的编程语言。",
}

evt := event.NewResponseEvent(
    uuid.New().String(),
    "assistant", // 助手消息的作者(或使用 agent 名称)
    &model.Response{
        Done:    false,
        Choices: []model.Choice{{Index: 0, Message: assistantMessage}},
    },
)

if err := sessionService.AppendEvent(ctx, sess, evt); err != nil {
    return err
}

Event 必填字段

使用 event.NewResponseEvent() 创建事件时,以下字段是必填的:

  1. 函数参数

    • invocationID (string): 唯一标识符,通常使用 uuid.New().String()
    • author (string): 事件作者("user""system" 或 agent 名称)
    • response (*model.Response): 包含 Choices 的响应对象
  2. Response 字段

    • Choices ([]model.Choice): 至少包含一个 Choice,包含 IndexMessage
    • Message: 必须包含 ContentContentParts
  3. 自动生成字段(由 event.NewResponseEvent() 自动设置):

    • ID: 自动生成的 UUID
    • Timestamp: 自动设置为当前时间
    • Version: 自动设置为 CurrentVersion
  4. 持久化要求

    • Response != nil
    • !IsPartial(或包含 StateDelta
    • IsValidContent() 返回 true

与 Runner 配合使用

当您后续使用 Runner.Run() 处理同一会话时:

  1. Runner 会自动加载会话(包括所有已追加的事件)
  2. 将会话事件转换为消息
  3. 将所有消息(已追加的 + 当前的)包含在对话上下文中
  4. 一起发送给模型

所有已追加的事件都会成为对话历史的一部分,并在后续交互中可供模型使用。

示例:见 examples/session/appendevent代码

Track 事件

Track 事件是 Session 中独立于主对话事件的轨迹存储机制,目前主要用于 AGUI 场景下的事件存储。它允许在会话中记录特定类型的事件,而不影响主对话流程。

接口说明

Track 事件的 API 定义在 session.TrackService 接口上,它独立于 session.Service

1
2
3
type TrackService interface {
    AppendTrackEvent(ctx context.Context, sess *Session, event *TrackEvent, opts ...Option) error
}

并非所有存储后端都实现了 TrackService。使用时需要通过类型断言获取:

存储后端 是否实现 TrackService
内存存储(inmemory)
Redis 存储
PostgreSQL 存储
MySQL 存储
ClickHouse 存储

基本用法

// 通过类型断言获取 TrackService
trackService, ok := sessionService.(session.TrackService)
if !ok {
    log.Fatal("当前存储后端不支持 TrackService")
}

// 追加 Track 事件
payload, _ := json.Marshal(map[string]any{"action": "button_click"})
err := trackService.AppendTrackEvent(ctx, sess, &session.TrackEvent{
    Track:     "ui-events",
    Payload:   payload,
    Timestamp: time.Now(),
})

// 从会话中获取 Track 事件
trackEvents, err := sess.GetTrackEvents("ui-events")

相关文档

参考资源