package pkg import ( "math" "strings" "unicode" "github.com/cloudwego/eino/schema" ) const ( // Worker 模型最大输入上下文(用户提供) WorkerMaxInputTokens = 224000 // 给模型输出和协议开销预留的冗余 token ContextReserveTokens = 28000 // 缓存未命中时,从数据库拉取的历史消息上限 DefaultHistoryFetchLimit = 1200 // Redis 会话窗口上下限与缓冲 SessionWindowMin = 32 SessionWindowMax = 4096 SessionWindowBuffer = 2 // ---- Execute Context Compaction 预算 ---- // Execute 阶段 prompt 总 token 上限 ExecuteTokenBudget = 80000 // msg0 + msg3 固定开销 + 安全余量 ExecuteReserveTokens = 8000 StageTokenBudget = ExecuteTokenBudget StageReserveTokens = ExecuteReserveTokens ) // MaxContextTokensByModel 返回指定模型的最大上下文 token。 func MaxContextTokensByModel(modelName string) int { switch strings.ToLower(strings.TrimSpace(modelName)) { case "worker", "strategist": return WorkerMaxInputTokens default: return WorkerMaxInputTokens } } // HistoryFetchLimitByModel 返回缓存未命中时的历史拉取条数。 func HistoryFetchLimitByModel(_ string) int { return DefaultHistoryFetchLimit } // HistoryTokenBudgetByModel 计算“历史上下文”可使用的 token 预算。 func HistoryTokenBudgetByModel(modelName, systemPrompt, userInput string) int { maxTokens := MaxContextTokensByModel(modelName) baseTokens := EstimateTextTokens(systemPrompt) + EstimateTextTokens(userInput) + 64 budget := maxTokens - ContextReserveTokens - baseTokens if budget < 0 { return 0 } return budget } // EstimateTextTokens 粗略估算文本 token: // - CJK 字符约 1:1 // - ASCII 字符约 4:1 // - 其他字符约 2:1 func EstimateTextTokens(text string) int { if strings.TrimSpace(text) == "" { return 0 } var cjkCount, asciiCount, otherCount int for _, r := range text { switch { case unicode.IsSpace(r): continue case r <= unicode.MaxASCII: asciiCount++ case isCJK(r): cjkCount++ default: otherCount++ } } tokens := cjkCount + int(math.Ceil(float64(asciiCount)/4.0)) + int(math.Ceil(float64(otherCount)/2.0)) if tokens <= 0 { return 1 } return tokens } // EstimateMessageTokens 估算单条消息 token(包含固定协议开销)。 func EstimateMessageTokens(msg *schema.Message) int { if msg == nil { return 0 } const messageOverhead = 6 return messageOverhead + EstimateTextTokens(msg.Content) + EstimateTextTokens(msg.ReasoningContent) } // EstimateHistoryTokens 估算历史消息总 token。 func EstimateHistoryTokens(history []*schema.Message) int { total := 0 for _, msg := range history { total += EstimateMessageTokens(msg) } return total } // TrimHistoryByTokenBudget 从最旧消息开始裁剪,直到历史 token 不超过预算。 // 返回值:裁剪后历史、裁剪前 token、裁剪后 token、裁掉条数。 func TrimHistoryByTokenBudget(history []*schema.Message, historyBudget int) ([]*schema.Message, int, int, int) { if len(history) == 0 { return history, 0, 0, 0 } totalBefore := EstimateHistoryTokens(history) if historyBudget <= 0 { return []*schema.Message{}, totalBefore, 0, len(history) } if totalBefore <= historyBudget { return history, totalBefore, totalBefore, 0 } tokenPerMsg := make([]int, len(history)) total := 0 for i, msg := range history { t := EstimateMessageTokens(msg) tokenPerMsg[i] = t total += t } drop := 0 for total > historyBudget && drop < len(history) { total -= tokenPerMsg[drop] drop++ } return history[drop:], totalBefore, total, drop } // CalcSessionWindowSize 根据裁剪后消息条数计算 Redis 队列窗口大小。 func CalcSessionWindowSize(trimmedHistoryLen int) int { size := trimmedHistoryLen + SessionWindowBuffer if size < SessionWindowMin { size = SessionWindowMin } if size > SessionWindowMax { size = SessionWindowMax } return size } func isCJK(r rune) bool { return unicode.Is(unicode.Han, r) || unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) || unicode.Is(unicode.Hangul, r) } // StageTokenBreakdown 记录四条阶段消息的 token 分布。 type StageTokenBreakdown struct { Msg0 int `json:"msg0"` Msg1 int `json:"msg1"` Msg2 int `json:"msg2"` Msg3 int `json:"msg3"` Total int `json:"total"` Budget int `json:"budget"` } // ExecuteTokenBreakdown 保留为历史兼容别名,避免旧调用点改动。 type ExecuteTokenBreakdown = StageTokenBreakdown // EstimateStageMessagesTokens 估算四条阶段消息的 token 分布。 func EstimateStageMessagesTokens(msg0, msg1, msg2, msg3 string) StageTokenBreakdown { b := StageTokenBreakdown{ Msg0: EstimateTextTokens(msg0), Msg1: EstimateTextTokens(msg1), Msg2: EstimateTextTokens(msg2), Msg3: EstimateTextTokens(msg3), Budget: StageTokenBudget, } b.Total = b.Msg0 + b.Msg1 + b.Msg2 + b.Msg3 return b } // CheckStageTokenBudget 检查是否超出阶段预算,并给出需要压缩的消息标记。 // // 1. 先计算四条消息的 token 分布,便于后续日志和统计。 // 2. 如果总量没有超预算,直接返回。 // 3. 如果超预算,则按 msg1 / msg2 的相对占比判断是否需要分别压缩。 func CheckStageTokenBudget(msg0, msg1, msg2, msg3 string) (breakdown StageTokenBreakdown, overBudget bool, needCompactMsg1 bool, needCompactMsg2 bool) { breakdown = EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) overBudget = breakdown.Total > StageTokenBudget if !overBudget { return } // msg1 过大时,优先压缩历史对话。 available := StageTokenBudget - StageReserveTokens needCompactMsg1 = breakdown.Msg1 > available/2 // 若压缩 msg1 后仍然超限,再压缩执行记录区。 needCompactMsg2 = (breakdown.Total - breakdown.Msg1 + available/4) > StageTokenBudget return } // EstimateExecuteMessagesTokens 保留旧名称,内部复用阶段预算实现。 func EstimateExecuteMessagesTokens(msg0, msg1, msg2, msg3 string) StageTokenBreakdown { return EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) } // CheckExecuteTokenBudget 保留旧名称,内部复用阶段预算实现。 func CheckExecuteTokenBudget(msg0, msg1, msg2, msg3 string) (breakdown StageTokenBreakdown, overBudget bool, needCompactMsg1 bool, needCompactMsg2 bool) { return CheckStageTokenBudget(msg0, msg1, msg2, msg3) }