From 572665b3acc82469dd7b6a6529ee73e3c87159a4 Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Fri, 29 May 2026 21:41:22 +0800 Subject: [PATCH] =?UTF-8?q?v1.7=20Compactor=20=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=99=A8=20=E2=80=94=20=E5=8F=8C=E9=87=8D?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E9=98=B2=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 远期历史区无情掩码 + Working Memory 区掐头去尾截断, System Prompt 和 ToolCalls 绝对保留。 --- internal/prompt/compactor.go | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 internal/prompt/compactor.go diff --git a/internal/prompt/compactor.go b/internal/prompt/compactor.go new file mode 100644 index 0000000..0995f2d --- /dev/null +++ b/internal/prompt/compactor.go @@ -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 +}