后端: 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 前端:无 仓库:无
198 lines
6.0 KiB
Go
198 lines
6.0 KiB
Go
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(历史对话)和 msg2(ReAct 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 对 msg2(ReAct 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)
|
||
}
|
||
}
|