Skip to content

s09_memory:snip_compact 触发后 引发 memory_turn 索引漂移,该轮已检索到的记忆没注入进 prompt #496

Description

@ClearVIper3

背景

s09 的 agent_loop(s09_memory/code.py)用整数下标 memory_turn 标记“把记忆内容注入到哪条 user turn”:

  memory_turn = len(messages) - 1 if messages and isinstance(messages[-1].get("content"), str) else None

但这个下标是在 s08的compact运行之前一次性算好的。如果进行snip_compact会原地改短/重建消息数组,使之后面的所有下标都偏移:

  messages[:] = tool_result_budget(messages)
  messages[:] = snip_compact(messages)          # len>50 时砍掉中间,数组变短
  messages[:] = micro_compact(messages)
  if estimate_size(messages) > CONTEXT_LIMIT:
  messages[:] = compact_history(messages)   # 整个数组换成 1 条摘要

注入处只有一道“越界保护”:

  if memories_content and memory_turn is not None and memory_turn < len(messages):
      request_messages[memory_turn] = { ... 注入 memory ... }

压缩后 memory_turn 已不再指向那条 user query,于是 memory_turn < len(messages) 为False,记忆注入被整段跳过——这一轮模型看不到已检索到的记忆。这恰好发生在“对话变长、压缩触发”时,与 s09 “压缩会丢细节,要有一层不丢的 / 跨压缩、跨会话”的核心命题直接矛盾。

修复建议

根因是“先算下标、后压缩”。修复只改 agent_loop(不碰 compact_history / reactive_compact):把“记下标”改成“记消息对象”,压缩后每次用身份比较重新定位;若 query 被
compact_history 摘要掉,则回退到“最后一条 str 内容的 user turn”(即摘要消息),保证记忆仍注入、不会静默丢失。

  1. 在 agent_loop 开头改为捕获消息对象:
      memories_content = load_memories(messages)
      # 捕获当前 user turn 的*对象*,而非下标。压缩管线会改短数组、偏移下标,
      # 预计算的下标会漂移(记忆被静默丢弃)。改为压缩后按身份重新定位。
      memory_msg = messages[-1] if (messages
          and isinstance(messages[-1].get("content"), str)) else None
      system = build_system()
  1. 新增一个按身份重新定位的辅助函数(放在 agent_loop 之前):
  def _find_memory_turn(messages, memory_msg):
      """按对象身份定位要注入记忆的 user turn。

      s09 bug fix:压缩管线会改短/重建 messages,预计算的下标会漂移并静默丢弃
      记忆注入。每次压缩后按身份重定位。只返回 str 内容的 user turn,调用方可以
      安全地用字符串拼接把记忆接到 content 上。
      """
      if memory_msg is not None:
          for i, m in enumerate(messages):
              if m is memory_msg:
                  if isinstance(m.get("content"), str):
                      return i
                  break  # 对象还在,但已不是纯文本 turn
      for i in range(len(messages) - 1, -1, -1):
          if messages[i].get("role") == "user" and isinstance(messages[i].get("content"), str):
              return i
      return None
  1. 循环内、压缩之后重新定位,再注入:
          # 压缩后按*身份*重定位 user turn —— 压缩可能已偏移其下标
          memory_turn = _find_memory_turn(messages, memory_msg)

          try:
              request_messages = messages
              if memories_content and memory_turn is not None:
                  request_messages = messages.copy()
                  request_messages[memory_turn] = {
                      **messages[memory_turn],
                      "content": memories_content + "\n\n" + messages[memory_turn]["content"],
                  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions