Files
smartmate/backend/newAgent/node/execute_compact.go
LoveLosita 8bde981592 Version: 0.9.20.dev.260415
后端:
1. 修复 query_available_slots section_from/section_to 错误覆盖 duration 并使用精确匹配而非范围包含
- 更新backend/newAgent/tools/schedule/read_filter_tools.go:移除 span = exactTo - exactFrom + 1 对 duration 的覆盖;matchSectionRange
  从精确匹配改为范围包含语义(slotStart < exactFrom || slotEnd > exactTo)
2. Execute 上下文窗口从硬编码裁剪改造为 80k token 动态预算 + LLM滚动压缩
- 基础设施层:AgentChat 新增 compaction 三个持久化字段,dao 新增 CRUD,Redis 新增缓存;pkg 新增 ExecuteTokenBudget常量、ExecuteTokenBreakdown 结构体、CheckExecuteTokenBudget 预算检查函数
- prompt 层:新建 compact_msg1.go / compact_msg2.go 分别实现msg1(历史对话)和 msg2(ReAct Loop)的 LLM 压缩;execute_context.go 移除 msg1 的 1400 字符/30 轮/120 字符三重裁剪和 msg2 的 8 条窗口限制,改为全量加载
- node 层:新建 execute_compact.go(compactExecuteMessagesIfNeeded:预算检查 → msg1 优先压缩 → msg2 兜底 → SSE 通知 → token 分布持久化);execute.go ReAct 循环插入 compact 调用 - 服务/API 层:AgentGraphDeps / AgentService 新增 CompactionStore 注入链路;新增 GET /api/v1/agent/context-stats 查询接口
- 启动层:cmd/start.go 注入 agentRepo 为 CompactionStore
3. 新增 Execute Context Compaction 决策报告
- 新建docs/功能决策记录/Execute_Context_Compaction_决策记录.md

前端:无 仓库:无
2026-04-15 22:01:37 +08:00

198 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentnode
import (
"context"
"encoding/json"
"fmt"
"log"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
"github.com/LoveLosita/smartflow/backend/pkg"
"github.com/cloudwego/eino/schema"
)
// compactExecuteMessagesIfNeeded 检查 Execute prompt 的 token 预算,
// 超限时对 msg1历史对话和 msg2ReAct Loop执行 LLM 压缩。
//
// 消息布局约定(由 BuildExecuteMessages 返回):
//
// [0] system — msg0: 系统规则
// [1] assistant — msg1: 历史对话上下文
// [2] assistant — msg2: 当轮 ReAct Loop 记录
// [3] system — msg3: 当前状态 + 用户提示
func compactExecuteMessagesIfNeeded(
ctx context.Context,
messages []*schema.Message,
input ExecuteNodeInput,
flowState *newagentmodel.CommonState,
emitter *newagentstream.ChunkEmitter,
) []*schema.Message {
if len(messages) != 4 {
return messages
}
// 提取四条消息的文本内容
msg0 := messages[0].Content
msg1 := messages[1].Content
msg2 := messages[2].Content
msg3 := messages[3].Content
// Token 预算检查
breakdown, overBudget, needCompactMsg1, needCompactMsg2 := pkg.CheckExecuteTokenBudget(msg0, msg1, msg2, msg3)
log.Printf(
"[COMPACT] token budget check: total=%d budget=%d over=%v compactMsg1=%v compactMsg2=%v (msg0=%d msg1=%d msg2=%d msg3=%d)",
breakdown.Total, breakdown.Budget, overBudget, needCompactMsg1, needCompactMsg2,
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
)
if !overBudget {
// 未超限,记录 token 分布后直接返回
saveTokenStats(ctx, input, flowState, breakdown)
return messages
}
// ---- msg1 压缩 ----
if needCompactMsg1 {
msg1 = compactMsg1IfNeeded(ctx, input, flowState, emitter, msg1)
messages[1].Content = msg1
// 压缩 msg1 后重算预算
breakdown = pkg.EstimateExecuteMessagesTokens(msg0, msg1, msg2, msg3)
}
// ---- msg2 压缩 ----
if needCompactMsg2 || breakdown.Total > pkg.ExecuteTokenBudget {
msg2 = compactMsg2IfNeeded(ctx, input, flowState, emitter, msg2)
messages[2].Content = msg2
breakdown = pkg.EstimateExecuteMessagesTokens(msg0, msg1, msg2, msg3)
}
// 记录最终 token 分布
saveTokenStats(ctx, input, flowState, breakdown)
log.Printf(
"[COMPACT] after compaction: total=%d budget=%d (msg0=%d msg1=%d msg2=%d msg3=%d)",
breakdown.Total, breakdown.Budget,
breakdown.Msg0, breakdown.Msg1, breakdown.Msg2, breakdown.Msg3,
)
return messages
}
// compactMsg1IfNeeded 对 msg1历史对话执行 LLM 压缩。
func compactMsg1IfNeeded(
ctx context.Context,
input ExecuteNodeInput,
flowState *newagentmodel.CommonState,
emitter *newagentstream.ChunkEmitter,
msg1 string,
) string {
compactionStore := input.CompactionStore
if compactionStore == nil {
log.Printf("[COMPACT] CompactionStore is nil, skip msg1 compaction")
return msg1
}
// 加载已有压缩摘要
existingSummary, _, err := compactionStore.LoadCompaction(ctx, flowState.UserID, flowState.ConversationID)
if err != nil {
log.Printf("[COMPACT] load existing compaction failed: %v, proceed without cache", err)
}
// SSE: 压缩开始
tokenBefore := pkg.EstimateTextTokens(msg1)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg1", "context_compact_start",
fmt.Sprintf("正在压缩对话历史(%d tokens...", tokenBefore),
false,
)
// 调用 LLM 压缩
newSummary, err := newagentprompt.CompactMsg1(ctx, input.Client, msg1, existingSummary)
if err != nil {
log.Printf("[COMPACT] compact msg1 failed: %v", err)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg1", "context_compact_done",
"对话历史压缩失败,使用原始文本",
false,
)
return msg1
}
// SSE: 压缩完成
tokenAfter := pkg.EstimateTextTokens(newSummary)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg1", "context_compact_done",
fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter),
false,
)
// 持久化压缩结果
if err := compactionStore.SaveCompaction(ctx, flowState.UserID, flowState.ConversationID, newSummary, flowState.RoundUsed); err != nil {
log.Printf("[COMPACT] save compaction failed: %v", err)
}
return newSummary
}
// compactMsg2IfNeeded 对 msg2ReAct Loop 记录)执行 LLM 压缩。
func compactMsg2IfNeeded(
ctx context.Context,
input ExecuteNodeInput,
flowState *newagentmodel.CommonState,
emitter *newagentstream.ChunkEmitter,
msg2 string,
) string {
// SSE: 压缩开始
tokenBefore := pkg.EstimateTextTokens(msg2)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg2", "context_compact_start",
fmt.Sprintf("正在压缩执行记录(%d tokens...", tokenBefore),
false,
)
// 调用 LLM 压缩
compressed, err := newagentprompt.CompactMsg2(ctx, input.Client, msg2)
if err != nil {
log.Printf("[COMPACT] compact msg2 failed: %v", err)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg2", "context_compact_done",
"执行记录压缩失败,使用原始文本",
false,
)
return msg2
}
// SSE: 压缩完成
tokenAfter := pkg.EstimateTextTokens(compressed)
_ = emitter.EmitStatus(
executeStatusBlockID, "compact_msg2", "context_compact_done",
fmt.Sprintf("执行记录已压缩:%d → %d tokens", tokenBefore, tokenAfter),
false,
)
return compressed
}
// saveTokenStats 持久化当前 token 分布到 DB。
func saveTokenStats(
ctx context.Context,
input ExecuteNodeInput,
flowState *newagentmodel.CommonState,
breakdown pkg.ExecuteTokenBreakdown,
) {
compactionStore := input.CompactionStore
if compactionStore == nil {
return
}
statsJSON, err := json.Marshal(breakdown)
if err != nil {
log.Printf("[COMPACT] marshal token stats failed: %v", err)
return
}
if err := compactionStore.SaveContextTokenStats(ctx, flowState.UserID, flowState.ConversationID, string(statsJSON)); err != nil {
log.Printf("[COMPACT] save token stats failed: %v", err)
}
}