v1.7 Compactor 上下文压缩器 — 双重降级防线
远期历史区无情掩码 + Working Memory 区掐头去尾截断, System Prompt 和 ToolCalls 绝对保留。
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
// Compactor 负责监控和压缩上下文内存,防止大模型发生 OOM
|
||||
type Compactor struct {
|
||||
MaxChars int // 触发压缩的最大字符数阈值 (水位线,可参考使用的大模型的token窗口大小)
|
||||
RetainLastMsgs int // Working Memory 保护区:最近的 N 条消息
|
||||
}
|
||||
|
||||
func NewCompactor(maxChars int, retainLastMsgs int) *Compactor {
|
||||
return &Compactor{
|
||||
MaxChars: maxChars,
|
||||
RetainLastMsgs: retainLastMsgs,
|
||||
}
|
||||
}
|
||||
|
||||
// Compact 接收准备发送给大模型的消息数组。
|
||||
// 如果总长度超标,对远期历史区进行全量掩码 (Masking),对短期保护区进行超长局部截断 (Truncation)。
|
||||
func (c *Compactor) Compact(msgs []schema.Message) []schema.Message {
|
||||
currentLength := c.estimateLength(msgs)
|
||||
|
||||
// 如果没有超过水位线,直接返回原数组 (大多数情况下的正常路径)
|
||||
if currentLength < c.MaxChars {
|
||||
return msgs
|
||||
}
|
||||
|
||||
log.Printf("[Compactor] ⚠️ 内存告警:当前上下文长度 (%d 字符) 超过阈值 (%d),触发压缩清理...\n", currentLength, c.MaxChars)
|
||||
|
||||
var compacted []schema.Message
|
||||
msgCount := len(msgs)
|
||||
|
||||
// 计算受保护的 Working Memory 起始索引
|
||||
protectStartIndex := msgCount - c.RetainLastMsgs
|
||||
if protectStartIndex < 0 {
|
||||
protectStartIndex = 0
|
||||
}
|
||||
|
||||
for i, msg := range msgs {
|
||||
// 1. 系统提示词 (System Prompt) 绝对不能动,直接保留
|
||||
if msg.Role == schema.RoleSystem {
|
||||
compacted = append(compacted, msg)
|
||||
continue
|
||||
}
|
||||
|
||||
// 我们必须拷贝一份新消息,因为在并发环境中直接修改原引用可能导致底层数据结构被污染
|
||||
newMsg := msg
|
||||
|
||||
isInWorkingMemory := i >= protectStartIndex
|
||||
|
||||
// 【核心驾驭逻辑】: 双重降级防线
|
||||
if msg.Role == schema.RoleUser && msg.ToolCallID != "" {
|
||||
// 对于工具的返回结果 (Observation/ToolResult)
|
||||
if !isInWorkingMemory {
|
||||
// 【第一道防线:远期历史】如果是早期对话,执行无情替换 (Full Masking)
|
||||
if len(msg.Content) > 200 {
|
||||
newMsg.Content = fmt.Sprintf("...[为了节省内存,早期的工具输出已被系统强制清理。原始长度: %d 字节]...", len(msg.Content))
|
||||
}
|
||||
} else {
|
||||
// 【第二道防线:短期记忆】即使处于近期保护区,只要单条内容过大,也必须截断防 OOM (Head-Tail Truncation)
|
||||
// 我们保留前 500 字符和后 500 字符(掐头去尾法,大模型通常只需要看开头报错和结尾总结)
|
||||
const maxKeep = 1000
|
||||
if len(msg.Content) > maxKeep {
|
||||
head := msg.Content[:500]
|
||||
tail := msg.Content[len(msg.Content)-500:]
|
||||
newMsg.Content = fmt.Sprintf("%s\n\n...[内容过长,中间 %d 字节已被系统截断]...\n\n%s", head, len(msg.Content)-maxKeep, tail)
|
||||
}
|
||||
}
|
||||
} else if msg.Role == schema.RoleAssistant && msg.Content != "" {
|
||||
// 对于大模型的冗长推理废话 (Thinking Trace)
|
||||
if !isInWorkingMemory && len(msg.Content) > 200 {
|
||||
newMsg.Content = "...[早期的推理思考过程已折叠]..."
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:我们绝不会去动 msg.ToolCalls,因为这是模型行动的证据,是维系逻辑链的关键!
|
||||
compacted = append(compacted, newMsg)
|
||||
}
|
||||
|
||||
newLength := c.estimateLength(compacted)
|
||||
log.Printf("[Compactor] ✅ 压缩完成。上下文长度从 %d 降至 %d 字符。\n", currentLength, newLength)
|
||||
|
||||
return compacted
|
||||
}
|
||||
|
||||
// estimateLength 粗略计算当前上下文的总字符长度
|
||||
func (c *Compactor) estimateLength(msgs []schema.Message) int {
|
||||
length := 0
|
||||
for _, msg := range msgs {
|
||||
length += len(msg.Content)
|
||||
for _, tc := range msg.ToolCalls {
|
||||
length += len(tc.Name) + len(tc.Arguments)
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
Reference in New Issue
Block a user