From 6464ca4880142edff8b4b4f333c46c10c6a0ea98 Mon Sep 17 00:00:00 2001 From: Candlest Date: Mon, 16 Feb 2026 22:05:35 +0800 Subject: [PATCH] feat: add configurable summary and compression trigger thresholds - Add CompressionTriggerRatio, SummaryTriggerMessages, SummaryTriggerRatio to config - Pass maxTokens and temperature to subagent for consistent LLM settings - Update ToolLoop to support configurable MaxTokens and Temperature - Update default config values and documentation - Fix hardcoded values (8192, 0.7) to use configurable parameters --- README.md | 29 +++++++++++- README.zh.md | 29 +++++++++++- config/config.example.json | 5 +- pkg/agent/loop.go | 81 +++++++++++++++++++-------------- pkg/config/config.go | 34 ++++++++------ pkg/tools/subagent.go | 14 ++++-- pkg/tools/subagent_tool_test.go | 18 ++++---- pkg/tools/toolloop.go | 17 +++++-- 8 files changed, 159 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 0a9dacce6..ab3172a4c 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,10 @@ picoclaw onboard "model": "glm-4.7", "max_tokens": 8192, "temperature": 0.7, - "max_tool_iterations": 20 + "max_tool_iterations": 20, + "compression_trigger_ratio": 0.75, + "summary_trigger_messages": 20, + "summary_trigger_ratio": 0.75 } }, "providers": { @@ -650,6 +653,30 @@ The subagent has access to tools (message, web_search, etc.) and can communicate * `PICOCLAW_HEARTBEAT_ENABLED=false` to disable * `PICOCLAW_HEARTBEAT_INTERVAL=60` to change interval +### Threshold Configuration + +PicoClaw supports customizable conversation summary and compression trigger thresholds to prevent premature context truncation. + +| Configuration | Default | Description | +|--------------|---------|-------------| +| `max_tokens` | `8192` | Context window size and LLM output token limit | +| `compression_trigger_ratio` | `0.75` | Trigger force compression when context exceeds `max_tokens × compression_trigger_ratio` | +| `summary_trigger_messages` | `20` | Trigger automatic summary when history message count exceeds this value (set to 0 to disable message count-based triggering) | +| `summary_trigger_ratio` | `0.75` | Trigger automatic summary when history token estimate exceeds `max_tokens × summary_trigger_ratio` | + +**Environment variables:** + +* `PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS=16384` Increase context window +* `PICOCLAW_AGENTS_DEFAULTS_COMPRESSION_TRIGGER_RATIO=0.9` Raise compression trigger threshold (e.g., from 75% to 90%) +* `PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_MESSAGES=50` Delay message count-based triggering (e.g., from 20 to 50 messages) +* `PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_RATIO=0.85` Raise summary trigger threshold (e.g., from 75% to 85%) + +**Usage recommendations:** + +- **Longer conversations**: Increase all thresholds for longer conversations +- **Faster response**: Decrease thresholds for quicker summarization and compression +- **Token-only control**: Set `summary_trigger_messages=0` to only use `summary_trigger_ratio` for summary triggering + ### Providers > [!NOTE] diff --git a/README.zh.md b/README.zh.md index 2ca2987bb..db355e30e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -204,7 +204,10 @@ picoclaw onboard "model": "glm-4.7", "max_tokens": 8192, "temperature": 0.7, - "max_tool_iterations": 20 + "max_tool_iterations": 20, + "compression_trigger_ratio": 0.75, + "summary_trigger_messages": 20, + "summary_trigger_ratio": 0.75 } }, "providers": { @@ -521,6 +524,30 @@ Agent 读取 HEARTBEAT.md * `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用 * `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔 +### 对话阈值配置 (Threshold Configuration) + +PicoClaw 支持自定义对话总结和压缩触发阈值,避免过早截断上下文。 + +| 配置项 | 默认值 | 描述 | +| --- | --- | --- | +| `max_tokens` | `8192` | 上下文窗口大小和 LLM 输出 token 上限 | +| `compression_trigger_ratio` | `0.75` | 当上下文超过 `max_tokens × compression_trigger_ratio` 时触发强制压缩(forceCompression) | +| `summary_trigger_messages` | `20` | 当历史消息数超过此值时触发自动总结(设为 0 可禁用按条数触发) | +| `summary_trigger_ratio` | `0.75` | 当历史消息 token 估算超过 `max_tokens × summary_trigger_ratio` 时触发自动总结 | + +**环境变量:** + +* `PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS=16384` 提升上下文窗口 +* `PICOCLAW_AGENTS_DEFAULTS_COMPRESSION_TRIGGER_RATIO=0.9` 提高压缩触发阈值(例如从 75% 提升到 90%) +* `PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_MESSAGES=50` 延迟按条数触发(例如从 20 条提升到 50 条) +* `PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_RATIO=0.85` 提高总结触发阈值(例如从 75% 提升到 85%) + +**使用建议:** + +- **延长对话**: 提高所有阈值,让对话保持更长时间 +- **快速响应**: 降低阈值,更快触发总结和压缩 +- **仅按 token 控制**: 设置 `summary_trigger_messages=0`,仅使用 `summary_trigger_ratio` 控制总结触发 + ### 提供商 (Providers) > [!NOTE] diff --git a/config/config.example.json b/config/config.example.json index 3c9158e9c..870ef3a22 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -6,7 +6,10 @@ "model": "glm-4.7", "max_tokens": 8192, "temperature": 0.7, - "max_tool_iterations": 20 + "max_tool_iterations": 20, + "compression_trigger_ratio": 0.75, + "summary_trigger_messages": 20, + "summary_trigger_ratio": 0.75 } }, "channels": { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cd4276155..c27ad4aa9 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -31,19 +31,23 @@ import ( ) type AgentLoop struct { - bus *bus.MessageBus - provider providers.LLMProvider - workspace string - model string - contextWindow int // Maximum context window size in tokens - maxIterations int - sessions *session.SessionManager - state *state.Manager - contextBuilder *ContextBuilder - tools *tools.ToolRegistry - running atomic.Bool - summarizing sync.Map // Tracks which sessions are currently being summarized - channelManager *channels.Manager + bus *bus.MessageBus + provider providers.LLMProvider + workspace string + model string + contextWindow int // Maximum context window size in tokens + maxIterations int + sessions *session.SessionManager + state *state.Manager + contextBuilder *ContextBuilder + tools *tools.ToolRegistry + running atomic.Bool + summarizing sync.Map // Tracks which sessions are currently being summarized + channelManager *channels.Manager + compressionTriggerRatio float64 + summaryTriggerMessages int + summaryTriggerRatio float64 + temperature float64 } // processOptions configures how a message is processed @@ -114,7 +118,14 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry := createToolRegistry(workspace, restrict, cfg, msgBus) // Create subagent manager with its own tool registry - subagentManager := tools.NewSubagentManager(provider, cfg.Agents.Defaults.Model, workspace, msgBus) + subagentManager := tools.NewSubagentManager( + provider, + cfg.Agents.Defaults.Model, + cfg.Agents.Defaults.MaxTokens, + cfg.Agents.Defaults.Temperature, + workspace, + msgBus, + ) subagentTools := createToolRegistry(workspace, restrict, cfg, msgBus) // Subagent doesn't need spawn/subagent tools to avoid recursion subagentManager.SetTools(subagentTools) @@ -137,17 +148,21 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers contextBuilder.SetToolsRegistry(toolsRegistry) return &AgentLoop{ - bus: msgBus, - provider: provider, - workspace: workspace, - model: cfg.Agents.Defaults.Model, - contextWindow: cfg.Agents.Defaults.MaxTokens, // Restore context window for summarization - maxIterations: cfg.Agents.Defaults.MaxToolIterations, - sessions: sessionsManager, - state: stateManager, - contextBuilder: contextBuilder, - tools: toolsRegistry, - summarizing: sync.Map{}, + bus: msgBus, + provider: provider, + workspace: workspace, + model: cfg.Agents.Defaults.Model, + contextWindow: cfg.Agents.Defaults.MaxTokens, + maxIterations: cfg.Agents.Defaults.MaxToolIterations, + sessions: sessionsManager, + state: stateManager, + contextBuilder: contextBuilder, + tools: toolsRegistry, + summarizing: sync.Map{}, + compressionTriggerRatio: cfg.Agents.Defaults.CompressionTriggerRatio, + summaryTriggerMessages: cfg.Agents.Defaults.SummaryTriggerMessages, + summaryTriggerRatio: cfg.Agents.Defaults.SummaryTriggerRatio, + temperature: cfg.Agents.Defaults.Temperature, } } @@ -443,8 +458,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M "model": al.model, "messages_count": len(messages), "tools_count": len(providerToolDefs), - "max_tokens": 8192, - "temperature": 0.7, + "max_tokens": al.contextWindow, + "temperature": al.temperature, "system_prompt_len": len(messages[0].Content), }) @@ -463,8 +478,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M maxRetries := 2 for retry := 0; retry <= maxRetries; retry++ { response, err = al.provider.Chat(ctx, messages, providerToolDefs, al.model, map[string]interface{}{ - "max_tokens": 8192, - "temperature": 0.7, + "max_tokens": al.contextWindow, + "temperature": al.temperature, }) if err == nil { @@ -723,9 +738,9 @@ func (al *AgentLoop) updateToolContexts(channel, chatID string) { func (al *AgentLoop) maybeSummarize(sessionKey, channel, chatID string) { newHistory := al.sessions.GetHistory(sessionKey) tokenEstimate := al.estimateTokens(newHistory) - threshold := al.contextWindow * 75 / 100 + threshold := int(float64(al.contextWindow) * al.summaryTriggerRatio) - if len(newHistory) > 20 || tokenEstimate > threshold { + if len(newHistory) > al.summaryTriggerMessages || tokenEstimate > threshold { if _, loading := al.summarizing.LoadOrStore(sessionKey, true); !loading { go func() { defer al.summarizing.Delete(sessionKey) @@ -887,8 +902,8 @@ func (al *AgentLoop) summarizeSession(sessionKey string) { toSummarize := history[:len(history)-4] // Oversized Message Guard - // Skip messages larger than 50% of context window to prevent summarizer overflow - maxMessageTokens := al.contextWindow / 2 + // Skip messages larger than compression_trigger_ratio of context window to prevent summarizer overflow + maxMessageTokens := int(float64(al.contextWindow) * al.compressionTriggerRatio) validMessages := make([]providers.Message, 0) omitted := false diff --git a/pkg/config/config.go b/pkg/config/config.go index d189ff00b..bbcc58d9b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -59,13 +59,16 @@ type AgentsConfig struct { } type AgentDefaults struct { - Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` - RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` - Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` - MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` - Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` - MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` + RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` + Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` + Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` + MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` + MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + CompressionTriggerRatio float64 `json:"compression_trigger_ratio" env:"PICOCLAW_AGENTS_DEFAULTS_COMPRESSION_TRIGGER_RATIO"` + SummaryTriggerMessages int `json:"summary_trigger_messages" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_MESSAGES"` + SummaryTriggerRatio float64 `json:"summary_trigger_ratio" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARY_TRIGGER_RATIO"` } type ChannelsConfig struct { @@ -219,13 +222,16 @@ func DefaultConfig() *Config { return &Config{ Agents: AgentsConfig{ Defaults: AgentDefaults{ - Workspace: "~/.picoclaw/workspace", - RestrictToWorkspace: true, - Provider: "", - Model: "glm-4.7", - MaxTokens: 8192, - Temperature: 0.7, - MaxToolIterations: 20, + Workspace: "~/.picoclaw/workspace", + RestrictToWorkspace: true, + Provider: "", + Model: "glm-4.7", + MaxTokens: 8192, + Temperature: 0.7, + MaxToolIterations: 20, + CompressionTriggerRatio: 0.75, + SummaryTriggerMessages: 20, + SummaryTriggerRatio: 0.75, }, }, Channels: ChannelsConfig{ diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index efa1d33aa..06138118f 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -26,6 +26,8 @@ type SubagentManager struct { mu sync.RWMutex provider providers.LLMProvider defaultModel string + maxTokens int + temperature float64 bus *bus.MessageBus workspace string tools *ToolRegistry @@ -33,11 +35,13 @@ type SubagentManager struct { nextID int } -func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager { +func NewSubagentManager(provider providers.LLMProvider, defaultModel string, maxTokens int, temperature float64, workspace string, bus *bus.MessageBus) *SubagentManager { return &SubagentManager{ tasks: make(map[string]*SubagentTask), provider: provider, defaultModel: defaultModel, + maxTokens: maxTokens, + temperature: temperature, bus: bus, workspace: workspace, tools: NewToolRegistry(), @@ -123,17 +127,17 @@ After completing the task, provide a clear summary of what was done.` sm.mu.RLock() tools := sm.tools maxIter := sm.maxIterations + maxTokens := sm.maxTokens + temperature := sm.temperature sm.mu.RUnlock() loopResult, err := RunToolLoop(ctx, ToolLoopConfig{ Provider: sm.provider, Model: sm.defaultModel, + MaxTokens: maxTokens, + Temperature: temperature, Tools: tools, MaxIterations: maxIter, - LLMOptions: map[string]any{ - "max_tokens": 4096, - "temperature": 0.7, - }, }, messages, task.OriginChannel, task.OriginChatID) sm.mu.Lock() diff --git a/pkg/tools/subagent_tool_test.go b/pkg/tools/subagent_tool_test.go index 8a7d22f24..27e83f142 100644 --- a/pkg/tools/subagent_tool_test.go +++ b/pkg/tools/subagent_tool_test.go @@ -39,7 +39,7 @@ func (m *MockLLMProvider) GetContextWindow() int { // TestSubagentTool_Name verifies tool name func TestSubagentTool_Name(t *testing.T) { provider := &MockLLMProvider{} - manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", nil) tool := NewSubagentTool(manager) if tool.Name() != "subagent" { @@ -50,7 +50,7 @@ func TestSubagentTool_Name(t *testing.T) { // TestSubagentTool_Description verifies tool description func TestSubagentTool_Description(t *testing.T) { provider := &MockLLMProvider{} - manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", nil) tool := NewSubagentTool(manager) desc := tool.Description() @@ -65,7 +65,7 @@ func TestSubagentTool_Description(t *testing.T) { // TestSubagentTool_Parameters verifies tool parameters schema func TestSubagentTool_Parameters(t *testing.T) { provider := &MockLLMProvider{} - manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", nil) tool := NewSubagentTool(manager) params := tool.Parameters() @@ -115,7 +115,7 @@ func TestSubagentTool_Parameters(t *testing.T) { // TestSubagentTool_SetContext verifies context setting func TestSubagentTool_SetContext(t *testing.T) { provider := &MockLLMProvider{} - manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", nil) tool := NewSubagentTool(manager) tool.SetContext("test-channel", "test-chat") @@ -129,7 +129,7 @@ func TestSubagentTool_SetContext(t *testing.T) { func TestSubagentTool_Execute_Success(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() - manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", msgBus) tool := NewSubagentTool(manager) tool.SetContext("telegram", "chat-123") @@ -185,7 +185,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) { func TestSubagentTool_Execute_NoLabel(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() - manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", msgBus) tool := NewSubagentTool(manager) ctx := context.Background() @@ -208,7 +208,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) { // TestSubagentTool_Execute_MissingTask tests error handling for missing task func TestSubagentTool_Execute_MissingTask(t *testing.T) { provider := &MockLLMProvider{} - manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", nil) tool := NewSubagentTool(manager) ctx := context.Background() @@ -259,7 +259,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) { func TestSubagentTool_Execute_ContextPassing(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() - manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", msgBus) tool := NewSubagentTool(manager) // Set context @@ -288,7 +288,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) { // Create a mock provider that returns very long content provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() - manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) + manager := NewSubagentManager(provider, "test-model", 4096, 0.7, "/tmp/test", msgBus) tool := NewSubagentTool(manager) ctx := context.Background() diff --git a/pkg/tools/toolloop.go b/pkg/tools/toolloop.go index 1302079b4..9da12f8b7 100644 --- a/pkg/tools/toolloop.go +++ b/pkg/tools/toolloop.go @@ -20,6 +20,8 @@ import ( type ToolLoopConfig struct { Provider providers.LLMProvider Model string + MaxTokens int + Temperature float64 Tools *ToolRegistry MaxIterations int LLMOptions map[string]any @@ -55,10 +57,17 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider // 2. Set default LLM options llmOpts := config.LLMOptions if llmOpts == nil { - llmOpts = map[string]any{ - "max_tokens": 4096, - "temperature": 0.7, - } + llmOpts = map[string]any{} + } + + // Set max_tokens if not specified in LLMOptions + if _, ok := llmOpts["max_tokens"]; !ok && config.MaxTokens > 0 { + llmOpts["max_tokens"] = config.MaxTokens + } + + // Set temperature if not specified in LLMOptions + if _, ok := llmOpts["temperature"]; !ok && config.Temperature > 0 { + llmOpts["temperature"] = config.Temperature } // 3. Call LLM