Skip to main content
xbot
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

xbot 上下文管理架构抽象与开关机制设计

中书省拟 | 2026-03-19 前置文档:Phase 1 设计Phase 2 设计


一、调研结果

1.1 现有压缩架构

文件职责关键结构
agent/compress.go压缩逻辑实现CompressResult(L18-21)、compressContext()(L240)、extractDialogueFromTail()thinTail()
agent/engine.go运行引擎,调用压缩CompressConfig(L177-181)、RunConfig.AutoCompress(L82)、maybeCompress()(L172)
agent/engine_wire.go构建运行配置buildMainRunConfig()buildSubAgentRunConfig()buildCronRunConfig()
agent/context_handler.go/context 命令handleContext()(L13)— 仅展示统计信息
agent/command.go命令接口Command 接口(L12)、CommandRegistry(L49)
agent/command_builtin.go内置命令注册contextCmd(L168-175)、registerBuiltinCommands()(L178-190)
agent/agent.goAgent 核心配置Agent 结构体含 enableAutoCompress/maxContextTokens/compressionThreshold(L136-138)
config/config.go全局配置加载Load() 一次性读取所有环境变量,EnableAutoCompress 默认 true(L107)

1.2 当前压缩调用链

engine.go Run()
  └── maybeCompress()(L172)
        ├── cfg.AutoCompress == nil → 跳过
        ├── CountMessagesTokens() → 判断是否超阈值
        └── cc.CompressFunc()(L191-192)→ 实际压缩(函数指针)
              └── agent.compressContext()  // Phase 1 实现

engine.go Run()(L264-299)
  └── 输入超限强制压缩路径
        └── cc.CompressFunc() → 同上

关键发现:

  • 压缩逻辑已通过函数指针注入CompressConfig.CompressFunc 签名为 func(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error)engine.go:180),已有策略模式雏形
  • 开关粒度粗enableAutoCompress 是全局 bool,只能开/关整个自动压缩,无法区分 Phase 1/Phase 2
  • /context 命令只读:现有 contextCmdcommand_builtin.go:168-175)只匹配精确的 /context,仅展示统计信息
  • 配置体系config.Config.Load()agent.ConfigAgent,环境变量统一在 Load() 中一次性读取

1.3 Phase 2 设计要点(尚未实现)

Phase 2 设计了三层渐进压缩、智能触发、话题分区、质量保障等高级功能。 核心区别:Phase 1 是"一刀切压缩",Phase 2 是"渐进式多级压缩"。 两者的接口签名一致(均返回 *CompressResult),为策略模式切换提供天然基础。


二、方案设计

2.1 架构总览

                    ┌─────────────────────────┐
                    │   ContextManager 接口    │  ← 统一抽象
                    └───────────┬─────────────┘
                                │
                    ┌───────────┴─────────────┐
                    │ ContextManagerConfig     │  ← 开关 + 策略选择 + 并发保护
                    │  (sync.RWMutex)          │
                    └───────────┬─────────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                  │
    ┌─────────▼──────┐ ┌───────▼──────────┐ ┌────▼──────────┐
    │ Phase1Manager  │ │  Phase2Manager   │ │  NoopManager  │
    │ (现有实现)      │ │  (未来实现)       │ │  (不压缩)     │
    └────────────────┘ └──────────────────┘ └───────────────┘

2.2 ContextManager 接口定义

新文件agent/context_manager.go

package agent

import (
    "context"
    "xbot/llm"
    "xbot/session"
)

// ContextMode 上下文管理模式
type ContextMode string

const (
    // ContextModePhase1 Phase 1 双视图架构(当前默认)
    ContextModePhase1 ContextMode = "phase1"
    // ContextModePhase2 Phase 2 三层渐进压缩(未来实现)
    ContextModePhase2 ContextMode = "phase2"
    // ContextModeNone 禁用自动上下文压缩
    ContextModeNone ContextMode = "none"
)

// ValidContextModes 所有可能的上下文模式
var ValidContextModes = []ContextMode{ContextModePhase1, ContextModePhase2, ContextModeNone}

// IsValidContextMode 检查是否为有效的上下文模式
func IsValidContextMode(mode ContextMode) bool {
    for _, m := range ValidContextModes {
        if m == mode {
            return true
        }
    }
    return false
}

// ContextManager 上下文管理器统一接口。
// 所有压缩策略实现此接口,通过策略模式实现新旧架构可切换。
type ContextManager interface {
    // Mode 返回当前管理模式标识。
    Mode() ContextMode

    // ShouldCompress 判断是否需要触发自动压缩。
    // 参数:
    //   - messages: 当前上下文消息
    //   - model: LLM 模型名(用于 token 计数)
    //   - toolTokens: 工具定义占用的 token 数
    ShouldCompress(messages []llm.ChatMessage, model string, toolTokens int) bool

    // Compress 执行上下文压缩。
    Compress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error)

    // ManualCompress 手动压缩(/compress 命令使用)。
    // 关键契约:无论当前模式如何,ManualCompress 都应尽力执行压缩。
    // 即使 auto=false 的 noopManager,ManualCompress 也降级到 Phase1 执行,
    // 保留 /compress 手动命令始终可用的现有语义。
    ManualCompress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error)

    // ContextInfo 返回上下文统计信息(/context info 命令使用)。
    ContextInfo(messages []llm.ChatMessage, model string, toolTokens int) *ContextStats

    // SessionHook 返回压缩后的 session 持久化钩子(可选,返回 nil 表示无特殊处理)。
    // Phase 2 可能需要在此钩子中做额外操作(如更新话题分区索引)。
    SessionHook() SessionCompressHook

    // SetMemoryTools 注册 memory 相关工具(recall_memory / store_memory)。
    // ContextManager 需要感知这些工具以在压缩时正确处理记忆消息。
    SetMemoryTools(tools []Tool)
}

// ContextStats 上下文统计信息
type ContextStats struct {
    SystemTokens    int
    UserTokens      int
    AssistantTokens int
    ToolMsgTokens   int
    ToolDefTokens   int
    TotalTokens     int
    MaxTokens       int
    Threshold       int
    Mode            ContextMode
    IsRuntimeOverride bool // 是否为运行时覆盖
    DefaultMode     ContextMode
}

// SessionCompressHook 压缩后的 session 处理钩子
type SessionCompressHook interface {
    // AfterPersist 在 session 持久化压缩结果后调用
    AfterPersist(ctx context.Context, tenantSession *session.TenantSession, result *CompressResult)
}

2.3 ContextManagerConfig 配置结构(含并发安全)

新文件agent/context_manager.go

import "sync"

// ContextManagerConfig 上下文管理器配置。
// 包含全局配置(环境变量/Agent.Config)和运行时开关(命令行切换)。
// 所有读写操作通过 sync.RWMutex 保护,确保并发安全。
type ContextManagerConfig struct {
    mu sync.RWMutex

    // MaxContextTokens 最大上下文 token 数(默认 100000)
    MaxContextTokens int
    // CompressionThreshold 触发压缩的 token 比例阈值(默认 0.7)
    CompressionThreshold float64

    // DefaultMode 默认压缩模式(启动时决定,来自环境变量或 Agent.Config)
    DefaultMode ContextMode

    // runtimeMode 运行时模式覆盖(通过 /context mode 命令切换)
    // 空值表示使用 DefaultMode,非空值覆盖 DefaultMode
    runtimeMode ContextMode
}

// EffectiveMode 返回当前生效的模式(RuntimeMode 优先)。
// 读锁保护。
func (c *ContextManagerConfig) EffectiveMode() ContextMode {
    c.mu.RLock()
    defer c.mu.RUnlock()
    if c.runtimeMode != "" {
        return c.runtimeMode
    }
    return c.DefaultMode
}

// RuntimeMode 返回当前运行时覆盖模式(无覆盖时返回空字符串)。
// 读锁保护。
func (c *ContextManagerConfig) RuntimeMode() ContextMode {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.runtimeMode
}

// SetRuntimeMode 设置运行时模式覆盖。
// 写锁保护。
func (c *ContextManagerConfig) SetRuntimeMode(mode ContextMode) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.runtimeMode = mode
}

// ResetRuntimeMode 清除运行时覆盖,恢复默认模式。
// 写锁保护。
func (c *ContextManagerConfig) ResetRuntimeMode() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.runtimeMode = ""
}

2.4 Agent 结构体变更

agent/agent.go 中修改 Agent 结构体:

type Agent struct {
    // ... 现有字段 ...

    // 上下文压缩配置(新增,替换旧的三个字段)
    contextManagerConfig *ContextManagerConfig
    contextManagerMu     sync.RWMutex            // 保护 contextManager 的并发读写
    contextManager       ContextManager           // 当前激活的管理器实例

    // 以下旧字段将在步骤 7(清理)中删除,步骤 1-6 期间保留用于向后兼容:
    // maxContextTokens     int
    // compressionThreshold float64
    // enableAutoCompress   bool
}

新增 Agent 方法(并发安全读写 ContextManager):

// GetContextManager 获取当前上下文管理器(读锁保护)。
// 用于 buildMainRunConfig / buildSubAgentRunConfig / handleCompress 等场景。
func (a *Agent) GetContextManager() ContextManager {
    a.contextManagerMu.RLock()
    defer a.contextManagerMu.RUnlock()
    return a.contextManager
}

// SetContextManager 替换当前上下文管理器(写锁保护)。
// 用于 /context mode 命令运行时切换。
func (a *Agent) SetContextManager(cm ContextManager) {
    a.contextManagerMu.Lock()
    defer a.contextManagerMu.Unlock()
    a.contextManager = cm
}

Agent.Config 中新增:

type Config struct {
    // ... 现有字段 ...

    // 上下文管理配置(新增)
    // 优先级:ContextMode > EnableAutoCompress 旧字段
    // 默认 "phase1"(保持现有行为)
    ContextMode ContextMode

    // 旧字段保留向后兼容(步骤 1-6 期间保留):
    // MaxContextTokens     int
    // CompressionThreshold float64
    // EnableAutoCompress   bool
}

2.5 三种实现

2.5.1 Phase1Manager(现有逻辑重构,消除 Agent 引用)

新文件agent/context_manager_phase1.go

核心改动:将 compressContext 从 Agent 方法提取为独立函数,消除循环引用。

package agent

import (
    "context"
    "fmt"
    "strings"

    "xbot/llm"
    log "xbot/logger"
)

// phase1Manager Phase 1 双视图压缩管理器。
// 封装现有 compress.go 中的逻辑,行为与现有完全一致。
// 不持有 *Agent 引用,仅依赖配置和独立函数。
type phase1Manager struct {
    config *ContextManagerConfig
}

func newPhase1Manager(cfg *ContextManagerConfig) *phase1Manager {
    return &phase1Manager{config: cfg}
}

func (m *phase1Manager) Mode() ContextMode { return ContextModePhase1 }

func (m *phase1Manager) ShouldCompress(messages []llm.ChatMessage, model string, toolTokens int) bool {
    if len(messages) <= 3 {
        return false
    }
    msgTokens, err := llm.CountMessagesTokens(messages, model)
    if err != nil {
        return false
    }
    tokenCount := msgTokens + toolTokens
    threshold := int(float64(m.config.MaxContextTokens) * m.config.CompressionThreshold)
    return tokenCount >= threshold
}

func (m *phase1Manager) Compress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    return compressMessages(ctx, messages, client, model)
}

func (m *phase1Manager) ManualCompress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    return compressMessages(ctx, messages, client, model)
}

func (m *phase1Manager) ContextInfo(messages []llm.ChatMessage, model string, toolTokens int) *ContextStats {
    cfg := m.config
    var systemTokens, userTokens, assistantTokens, toolMsgTokens int

    for _, msg := range messages {
        tokens, err := llm.CountMessagesTokens([]llm.ChatMessage{msg}, model)
        if err != nil {
            continue
        }
        switch msg.Role {
        case "system":
            systemTokens += tokens
        case "user":
            userTokens += tokens
        case "assistant":
            assistantTokens += tokens
        case "tool":
            toolMsgTokens += tokens
        }
    }

    total := systemTokens + userTokens + assistantTokens + toolMsgTokens + toolTokens
    threshold := int(float64(cfg.MaxContextTokens) * cfg.CompressionThreshold)

    return &ContextStats{
        SystemTokens:    systemTokens,
        UserTokens:      userTokens,
        AssistantTokens: assistantTokens,
        ToolMsgTokens:   toolMsgTokens,
        ToolDefTokens:   toolTokens,
        TotalTokens:     total,
        MaxTokens:       cfg.MaxContextTokens,
        Threshold:       threshold,
        Mode:            cfg.EffectiveMode(),
        IsRuntimeOverride: cfg.RuntimeMode() != "",
        DefaultMode:     cfg.DefaultMode,
    }
}

func (m *phase1Manager) SessionHook() SessionCompressHook { return nil }

关键重构:将 compressContext() 提取为独立函数 compressMessages()(放在 agent/compress.go 中):

// compressMessages 使用 LLM 压缩对话历史(独立函数,不依赖 Agent receiver)。
// 逻辑与现有 compressContext() 完全一致,仅为消除 phase1Manager 对 *Agent 的引用。
// 现有 compressContext() 改为调用此函数:
//
//   func (a *Agent) compressContext(...) { return compressMessages(...) }
func compressMessages(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    // 第一步:找到尾部安全切割点
    tailStart := len(messages)
    for i := len(messages) - 1; i >= 1; i-- {
        msg := messages[i]
        if msg.Role == "user" {
            tailStart = i
            break
        }
        if msg.Role == "assistant" && len(msg.ToolCalls) == 0 {
            tailStart = i
            break
        }
        if i == 1 {
            tailStart = 1
        }
    }
    // ... 后续逻辑完全复用现有 compressContext 的代码 ...
    // (extractDialogueFromTail、thinTail、LLM 压缩调用等,不动)
}

2.5.2 Phase2Manager(预留接口)

新文件agent/context_manager_phase2.go

// phase2Manager Phase 2 三层渐进压缩管理器。
// 目前为空壳实现,Phase 2 实现时填充。
// NewContextManager() 中会自动降级到 Phase 1。
type phase2Manager struct {
    config *ContextManagerConfig
    // Phase 2 专用字段(未来实现):
    // topicPartitioner  *TopicPartitioner
    // qualityChecker    *QualityChecker
    // progressiveLevels [3]CompressionLevel
}

func newPhase2Manager(cfg *ContextManagerConfig) *phase2Manager {
    return &phase2Manager{config: cfg}
}

func (m *phase2Manager) Mode() ContextMode { return ContextModePhase2 }

func (m *phase2Manager) ShouldCompress(messages []llm.ChatMessage, model string, toolTokens int) bool {
    // Phase 2 智能触发逻辑(未来实现)
    // 临时 fallback:与 Phase 1 相同的阈值判断
    if len(messages) <= 3 {
        return false
    }
    msgTokens, err := llm.CountMessagesTokens(messages, model)
    if err != nil {
        return false
    }
    tokenCount := msgTokens + toolTokens
    threshold := int(float64(m.config.MaxContextTokens) * m.config.CompressionThreshold)
    return tokenCount >= threshold
}

func (m *phase2Manager) Compress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    // TODO: Phase 2 三层渐进压缩实现
    return nil, fmt.Errorf("phase 2 compression not yet implemented")
}

func (m *phase2Manager) ManualCompress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    // ManualCompress 契约:无论模式如何,都尽力执行。
    // Phase 2 未实现时,降级到 compressMessages(Phase 1 逻辑)。
    return compressMessages(ctx, messages, client, model)
}

func (m *phase2Manager) ContextInfo(messages []llm.ChatMessage, model string, toolTokens int) *ContextStats {
    // 复用 Phase 1 的统计逻辑(统计方式相同)
    return newPhase1Manager(m.config).ContextInfo(messages, model, toolTokens)
}

func (m *phase2Manager) SessionHook() SessionCompressHook { return nil }

2.5.3 noopManager(禁用自动压缩,但 /compress 仍可用)

新文件agent/context_manager.go

关键设计决策:noopManager 只禁用自动压缩(ShouldCompress 返回 false),ManualCompress 降级到 Phase 1 执行,确保 /compress 命令始终可用(保持现有语义)。

// noopManager 禁用自动压缩的管理器。
// ShouldCompress 始终返回 false,但 ManualCompress 仍可执行(降级到 Phase 1)。
type noopManager struct {
    config  *ContextManagerConfig
    phase1  *phase1Manager // 内嵌 Phase1 用于 ManualCompress 和 ContextInfo
}

func newNoopManager(cfg *ContextManagerConfig) *noopManager {
    return &noopManager{
        config: cfg,
        phase1: newPhase1Manager(cfg),
    }
}

func (m *noopManager) Mode() ContextMode { return ContextModeNone }

func (m *noopManager) ShouldCompress([]llm.ChatMessage, string, int) bool {
    return false // 自动压缩始终禁用
}

func (m *noopManager) Compress(context.Context, []llm.ChatMessage, llm.LLM, string) (*CompressResult, error) {
    // 自动路径不应到达这里(ShouldCompress 返回 false)
    return nil, fmt.Errorf("auto compression is disabled (mode=none)")
}

func (m *noopManager) ManualCompress(ctx context.Context, messages []llm.ChatMessage, client llm.LLM, model string) (*CompressResult, error) {
    // /compress 手动命令:降级到 Phase 1 执行,保持命令始终可用
    return m.phase1.ManualCompress(ctx, messages, client, model)
}

func (m *noopManager) ContextInfo(messages []llm.ChatMessage, model string, toolTokens int) *ContextStats {
    // 仍返回完整统计信息(复用 Phase1 的实现)
    stats := m.phase1.ContextInfo(messages, model, toolTokens)
    stats.Mode = ContextModeNone
    return stats
}

func (m *noopManager) SessionHook() SessionCompressHook { return nil }

2.6 ContextManager 工厂

新文件agent/context_manager.go

// NewContextManager 根据配置创建对应的 ContextManager 实例。
func NewContextManager(cfg *ContextManagerConfig) ContextManager {
    mode := cfg.EffectiveMode()
    switch mode {
    case ContextModePhase2:
        // Phase 2: 即使未实现也创建 phase2Manager,
        // Compress 会返回错误并自动降级,ManualCompress 降级到 Phase 1
        log.WithField("mode", mode).Warn("Phase 2 not yet implemented, will fallback to Phase 1 on actual compression")
        return newPhase2Manager(cfg)
    case ContextModeNone:
        return newNoopManager(cfg)
    case ContextModePhase1, "":
        return newPhase1Manager(cfg)
    default:
        log.WithField("mode", mode).Warnf("Unknown context mode %q, falling back to Phase 1", mode)
        return newPhase1Manager(cfg)
    }
}

2.7 开关机制设计

2.7.1 配置层(启动时)

环境变量AGENT_CONTEXT_MODE(遵循现有 AGENT_ 前缀命名规范)

行为
phase1Phase 1 双视图压缩(默认)
phase2Phase 2 渐进压缩(未实现时 Compress 降级到 Phase 1)
none禁用自动压缩(/compress 仍可用)
(空)EnableAutoCompress 旧字段决定:true → phase1,false → none

config/config.goLoad() 中新增(与现有 AGENT_ENABLE_AUTO_COMPRESS 同级):

// config/config.go Load() 函数中 Agent 配置段新增:
func Load() Config {
    // ... 现有代码 ...
    agent := AgentConfig{
        // ... 现有字段 ...
        ContextMode:         getEnvString("AGENT_CONTEXT_MODE", ""),      // 新增
        EnableAutoCompress:  getEnvBoolOrDefault("AGENT_ENABLE_AUTO_COMPRESS", true),
        MaxContextTokens:    getEnvInt("AGENT_MAX_CONTEXT_TOKENS", 100000),
        CompressionThreshold: getEnvFloat("AGENT_COMPRESSION_THRESHOLD", 0.7),
    }
    // ...
}

向后兼容逻辑(在 Agent.New() 中):

// resolveContextMode 根据新旧配置确定上下文管理模式。
// 优先级:ContextMode > EnableAutoCompress > 默认 phase1
func resolveContextMode(cfg Config) ContextMode {
    // 1. 优先使用新配置
    if cfg.ContextMode != "" {
        if IsValidContextMode(cfg.ContextMode) {
            return cfg.ContextMode
        }
        log.WithField("mode", cfg.ContextMode).Warn("Invalid AGENT_CONTEXT_MODE, ignoring")
    }
    // 2. 向后兼容:旧字段
    if !cfg.EnableAutoCompress {
        return ContextModeNone
    }
    // 3. 默认 phase1
    return ContextModePhase1
}

2.7.2 命令层(运行时)

修改现有 contextCmdcommand_builtin.go:168-175),不新建命令。

现有代码:

// command_builtin.go:168-175
type contextCmd struct{}
func (c *contextCmd) Name() string      { return "/context" }
func (c *contextCmd) Aliases() []string { return nil }
func (c *contextCmd) Match(s string) bool {
    trimmed := strings.TrimSpace(s)
    return strings.ToLower(trimmed) == "/context"
}
func (c *contextCmd) Execute(ctx context.Context, a *Agent, msg bus.InboundMessage) (*bus.OutboundMessage, error) {
    return a.handleContext(ctx, msg, ...)
}

修改后:

// command_builtin.go — 修改现有 contextCmd
type contextCmd struct{}

func (c *contextCmd) Name() string      { return "/context" }
func (c *contextCmd) Aliases() []string { return nil }

// Match 支持子命令匹配:/context, /context info, /context mode ...
func (c *contextCmd) Match(s string) bool {
    trimmed := strings.TrimSpace(strings.ToLower(s))
    return trimmed == "/context" || strings.HasPrefix(trimmed, "/context ")
}

func (c *contextCmd) Concurrent() bool  { return true }

func (c *contextCmd) Execute(ctx context.Context, a *Agent, msg bus.InboundMessage) (*bus.OutboundMessage, error) {
    content := strings.TrimSpace(msg.Content)
    subCmd := strings.TrimSpace(strings.TrimPrefix(strings.ToLower(content), "/context"))

    switch {
    case subCmd == "" || subCmd == "info":
        return a.handleContextInfo(ctx, msg)
    case strings.HasPrefix(subCmd, "mode"):
        mode := strings.TrimSpace(strings.TrimPrefix(subCmd, "mode"))
        return a.handleContextMode(ctx, msg, mode)
    default:
        return &bus.OutboundMessage{
            Channel: msg.Channel,
            ChatID:  msg.ChatID,
            Content: "未知子命令。用法: /context [info|mode [phase1|phase2|none|default]]",
        }, nil
    }
}

模式切换处理:

// handleContextMode 处理 /context mode 子命令
func (a *Agent) handleContextMode(ctx context.Context, msg bus.InboundMessage, modeStr string) (*bus.OutboundMessage, error) {
    cfg := a.contextManagerConfig

    if modeStr == "" {
        // 仅查询当前模式
        stats := a.GetContextManager().ContextInfo(nil, "", 0)
        overrideInfo := ""
        if stats.IsRuntimeOverride {
            overrideInfo = fmt.Sprintf("(运行时覆盖,默认为 %s)", stats.DefaultMode)
        }
        return &bus.OutboundMessage{
            Channel: msg.Channel,
            ChatID:  msg.ChatID,
            Content: fmt.Sprintf("当前上下文模式: %s %s", cfg.EffectiveMode(), overrideInfo),
        }, nil
    }

    target := ContextMode(modeStr)
    if target == "default" {
        cfg.ResetRuntimeMode()
        a.SetContextManager(NewContextManager(cfg))
        return &bus.OutboundMessage{
            Channel: msg.Channel,
            ChatID:  msg.ChatID,
            Content: fmt.Sprintf("已恢复默认上下文模式: %s", cfg.DefaultMode),
        }, nil
    }

    if !IsValidContextMode(target) {
        return &bus.OutboundMessage{
            Channel: msg.Channel,
            ChatID:  msg.ChatID,
            Content: "无效模式。可选: phase1, phase2, none, default",
        }, nil
    }

    // Phase 2 未实现时的额外提示
    extraMsg := ""
    if target == ContextModePhase2 {
        extraMsg = "(Phase 2 尚未实现,压缩时将自动降级到 Phase 1)"
    }

    // 原子操作:先设置配置,再替换 manager
    cfg.SetRuntimeMode(target)
    a.SetContextManager(NewContextManager(cfg))

    return &bus.OutboundMessage{
        Channel: msg.Channel,
        ChatID:  msg.ChatID,
        Content: fmt.Sprintf("已切换上下文模式: %s %s", target, extraMsg),
    }, nil
}

2.7.3 engine.go 集成点修改

RunConfig 中新增字段:

type RunConfig struct {
    // ... 现有字段 ...

    // AutoCompress 自动压缩配置(旧字段,向后兼容,步骤 7 删除)
    AutoCompress *CompressConfig

    // ContextManager 上下文管理器(新字段,优先级高于 AutoCompress)
    // 如果设置了此字段,AutoCompress 被忽略
    ContextManager ContextManager
}

修改 maybeCompressengine.go Run() 内部):

// engine.go Run() 中修改 maybeCompress 闭包:
maybeCompress := func() {
    // 优先使用 ContextManager(新路径)
    if cm := cfg.ContextManager; cm != nil {
        toolDefs := cfg.Tools.AsDefinitionsForSession(sessionKey)
        toolTokens, _ := llm.CountToolsTokens(toolDefs, cfg.Model)

        if !cm.ShouldCompress(messages, cfg.Model, toolTokens) {
            return
        }

        // ... 通知逻辑不变 ...

        result, compressErr := cm.Compress(ctx, messages, cfg.LLMClient, cfg.Model)
        // ... 后续持久化逻辑不变 ...

        // 调用 SessionHook(如果有的话)
        if hook := cm.SessionHook(); hook != nil && cfg.Session != nil {
            hook.AfterPersist(ctx, cfg.Session, result)
        }
        return
    }

    // 回退路径:旧的 CompressFunc(向后兼容)
    cc := cfg.AutoCompress
    if cc == nil || len(messages) <= 3 {
        return
    }
    // ... 现有 maybeCompress 逻辑完全不变 ...
}

修改输入超限强制压缩路径engine.go L264-299):

// 在输入超限强制压缩处,同样优先使用 ContextManager:
if cc := cfg.AutoCompress; cc != nil {
    // 旧路径(保持不变)
    result, compressErr := cc.CompressFunc(ctx, messages, cfg.LLMClient, cfg.Model)
    // ...
} else if cm := cfg.ContextManager; cm != nil {
    // 新路径:手动压缩(不检查阈值)
    result, compressErr := cm.ManualCompress(ctx, messages, cfg.LLMClient, cfg.Model)
    // ... 后续重试逻辑不变 ...
}

2.7.4 handleCompress 适配

修改 agent/compress.go 中的 handleCompress(),保留外壳逻辑,只替换核心压缩调用:

// handleCompress 处理 /compress 命令:手动触发上下文压缩
func (a *Agent) handleCompress(ctx context.Context, msg bus.InboundMessage, tenantSession *session.TenantSession) (*bus.OutboundMessage, error) {
    // 手动 /compress 始终可用,不受模式开关限制

    llmClient, model, _, _ := a.llmFactory.GetLLM(msg.SenderID)

    // buildPrompt、token 计数等外壳逻辑完全保留 ...
    messages, err := a.buildPrompt(ctx, msg, tenantSession)
    // ... 阈值检查、进度消息等保留不变 ...

    // 核心改动:通过 ContextManager.ManualCompress 执行
    cm := a.GetContextManager()
    result, err := cm.ManualCompress(ctx, messages, llmClient, model)
    // ... 后续 session 持久化逻辑完全保留不变 ...
}

2.7.5 buildMainRunConfig / buildSubAgentRunConfig 适配

buildMainRunConfig()engine_wire.go):

func (a *Agent) buildMainRunConfig(...) RunConfig {
    // ... 现有代码 ...
    cfg := RunConfig{ /* ... */ }

    // 新路径:注入 ContextManager
    cfg.ContextManager = a.GetContextManager()

    // 旧路径兼容(步骤 7 删除):
    // if cfg.ContextManager == nil && a.enableAutoCompress { ... }

    return cfg
}

buildSubAgentRunConfig()engine_wire.go):

SubAgent 共用主 Agent 的 ContextManager 实例(通过 a.GetContextManager() 获取)。

  • 运行时 /context mode 切换对后续新建的 SubAgent 生效
  • 正在运行的 SubAgent 不受影响(它已持有 RunConfig.ContextManager 的引用)
  • 这与现有行为一致:SubAgent 的 AutoCompress 也是构建时注入的,运行时不可变
func (a *Agent) buildSubAgentRunConfig(...) RunConfig {
    // ... 现有代码 ...
    cfg := RunConfig{ /* ... */ }

    // SubAgent 共享主 Agent 的 ContextManager(caps.Memory=true 时)
    if caps.Memory && a.enableAutoCompress {
        cfg.ContextManager = a.GetContextManager()
    }
    // SubAgent 不需要独立的 ContextManager——它与主 Agent 共用同一套压缩配置
    // 未来如果需要 SubAgent 独立模式,可通过 caps.ContextMode 字段扩展

    return cfg
}

buildCronRunConfig()engine_wire.go):

无需修改。Cron 消息不使用自动压缩(现有行为),也不需要 ContextManager。

2.7.6 /context info 输出增强

修改现有 /context 统计输出,增加模式信息:

📊 上下文 Token 统计

| 角色 | Token | 占比 |
|------|-------|------|
| System | 5000 | 5.0% |
| User | 40000 | 40.0% |
| Assistant | 15000 | 15.0% |
| Tool (消息) | 30000 | 30.0% |
| Tool (定义) | 10000 | 10.0% |
| **总计** | **100000** | 100% |

⚙️ 配置:
- 最大上下文: 100000 tokens
- 压缩阈值: 70000 tokens (70%)
- 当前模式: phase1(运行时覆盖,默认为 phase1)

4.1 功能验证

#场景预期结果验证方式
1默认启动(无 AGENT_CONTEXT_MODE)使用 Phase 1 压缩,行为与现有完全一致现有测试全部通过
2AGENT_CONTEXT_MODE=none 启动禁用自动压缩,/compress 仍可用手动测试
3AGENT_CONTEXT_MODE=phase2 启动自动降级到 Phase 1,日志记录警告检查日志
4/context显示统计信息 + 当前模式手动测试
5/context mode phase2切换到 Phase 2,提示降级手动测试
6/context mode none切换后不再自动压缩,/compress 仍可用手动测试
7/context mode default恢复启动时配置的模式手动测试
8/compress 手动压缩(mode=none 时)正常执行(降级到 Phase 1)手动测试
9SubAgent 自动压缩(mode=none 时)SubAgent 不触发自动压缩日志验证
10运行时模式切换 + 新 SubAgent新 SubAgent 使用切换后的模式日志验证

4.2 兼容性验证

#场景预期结果
1AGENT_ENABLE_AUTO_COMPRESS=false,不设 AGENT_CONTEXT_MODE等价于 CONTEXT_MODE=none
2不设置任何新字段默认 Phase 1,行为不变
3/context 无参数输出格式与现有基本一致,增加模式行

五、风险与注意

风险影响缓解措施
运行时模式切换并发安全/context mode (Concurrent=true) 和 maybeCompress 并发读写ContextManagerConfigsync.RWMutexAgent.contextManager 用独立的 sync.RWMutex + Get/Set 方法;运行 go test -race 验证
Phase 2 降级时的用户体验用户切到 Phase 2 期望新功能,实际仍是 Phase 1/context mode phase2 时明确提示降级;phase2Manager.Compress 返回 error 时 maybeCompress 降级到 ManualCompress
/context 命令改造向后兼容现有 /context 输出可能变化/context 无参数输出格式保持不变,仅末尾增加一行模式信息
SubAgent 模式继承SubAgent 是否应继承父 Agent 模式SubAgent 共用主 Agent 的 ContextManager 实例(构建时注入),运行时切换影响后续新建的 SubAgent,不影响正在运行的 SubAgent