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

前端:无 仓库:无
This commit is contained in:
LoveLosita
2026-04-15 22:01:37 +08:00
parent e77d42fce5
commit 8bde981592
23 changed files with 1921 additions and 8532 deletions

View File

@@ -218,6 +218,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt
ToolRegistry: st.Deps.ToolRegistry,
ScheduleState: scheduleState,
SchedulePersistor: st.Deps.SchedulePersistor,
CompactionStore: st.Deps.CompactionStore,
WriteSchedulePreview: st.Deps.WriteSchedulePreview,
OriginalScheduleState: st.OriginalScheduleState,
AlwaysExecute: st.Request.AlwaysExecute,

View File

@@ -55,6 +55,7 @@ type ExecuteNodeInput struct {
ToolRegistry *newagenttools.ToolRegistry
ScheduleState *schedule.ScheduleState
SchedulePersistor newagentmodel.SchedulePersistor
CompactionStore newagentmodel.CompactionStore
WriteSchedulePreview newagentmodel.WriteSchedulePreviewFunc
OriginalScheduleState *schedule.ScheduleState
AlwaysExecute bool // true 时写工具跳过确认闸门直接执行
@@ -180,6 +181,12 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
// 5. 构造本轮执行输入,请求 LLM 输出 ExecuteDecision。
messages := newagentprompt.BuildExecuteMessages(flowState, conversationContext)
// 5.1 Token 预算检查 & 上下文压缩。
messages = compactExecuteMessagesIfNeeded(
ctx, messages, input, flowState, emitter,
)
log.Printf(
"[DEBUG] execute LLM context begin chat=%s round=%d message_count=%d\n%s\n[DEBUG] execute LLM context end chat=%s round=%d",
flowState.ConversationID,

View File

@@ -0,0 +1,197 @@
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)
}
}