设置

语言

为什么你的 AI Agent 总是丢失记忆(以及我们是如何解决的)

L
LemonData
·2026年2月28日·27 次浏览
#AI 智能体#记忆#回退机制#架构#lemonclaw
为什么你的 AI Agent 总是丢失记忆(以及我们是如何解决的)

为什么你的 AI Agent 总是丢失记忆(以及我们如何修复它)

你的 AI Agent 刚刚与用户进行了 30 分钟的对话。他们讨论了项目需求,分享了偏好,做出了决策。然后用户输入 /new 来开启一个新会话。

Agent 尝试将这段对话整合进长期记忆中。LLM 调用失败了。频率限制(Rate limit)、超时,或者模型返回了文本而不是调用所需的 Tool。

记忆消失了。三十分钟的上下文,烟消云散。

这种情况发生的频率比你想象的要高。我们在 LemonClaw 实例中追踪了这一情况:在任何单一模型上,记忆整合(memory consolidation)的失败率约为 15%。对于一个理应不可见的底层基础设施功能来说,这是不可接受的。

其他框架如何处理?(它们并不处理)

大多数 AI Agent 框架将记忆整合视为一次简单的 LLM 调用。如果成功了,很好;如果失败了,记忆就丢了。

OpenClaw 是最受欢迎的开源 Agent 框架,它在记忆整合和对话中使用相同的模型。一次 Claude Sonnet 调用耗费 0.003 美元且需要 8 秒以上,仅仅是为了总结一段用户永远不会看到的聊天记录。当调用失败(频率限制、超时、模型错误)时,框架只会记录一条警告并继续运行。用户的上下文丢失了。

nanobot 是另一个流行的框架,具有相同的架构。单一模型,尝试一次,没有备选方案(fallback)。整合函数甚至没有超时设置。缓慢的上游响应(Cloudflare 524 错误很常见)会阻塞整个会话,直到连接断开。

这两个框架都没有将整合逻辑与主模型分离。两者都没有针对记忆操作的备选逻辑。两者都无法区分“API 调用失败”和“API 调用成功但模型没有按要求执行”。

这些并非极端情况。由于任何单一模型的失败率都有 15%,一个每天运行 100 次记忆整合的框架会在其中 15 次丢失记忆。一周下来,就有 105 场对话被 Agent 彻底遗忘。

问题的深度超出了重试逻辑

显而易见的修复方法是带有指数退避(exponential backoff)的重试机制。我们曾有过这种机制,它能很好地处理瞬时 HTTP 错误:

# 重试循环:1s → 2s → 4s 退避
for attempt in range(3):
    try:
        response = await acompletion(**kwargs)
        return await self._collect_stream(response)
    except (RateLimitError, APIConnectionError) as e:
        await asyncio.sleep(RETRY_DELAYS[attempt])

这能捕捉到 429 错误和网络波动。但有两种失败模式会漏掉:

失败模式 1:模型无法进行 Tool calling。 某些模型,特别是在快速推理引擎上运行的小型模型,偶尔会在复杂的 Prompt 下无法生成有效的函数调用。API 返回 200 状态码,但内部包裹着 ServiceUnavailableErrorMidStreamFallbackError。你的重试逻辑看到异常,重试同一个模型,得到相同的错误。

失败模式 2:模型“成功”但没有调用 Tool。 LLM 返回了一个完美的响应。HTTP 200。没有错误。但它没有使用结构化数据调用 save_memory,而是写了一段纯文本总结。你的重试引擎认为这是成功的。整合函数检查 Tool 调用,一无所获,然后放弃。

第二种失败模式非常隐蔽。传输层认为一切正常,业务层知道它失败了。再多的 HTTP 级重试也无法修复一个无法理解你的 Tool schema 的模型。

双层备选架构(Dual-Layer Fallback Architecture)

我们通过在不同层面运行的两个独立备选循环解决了这个问题:

用户发送 /new
    │
    ▼
consolidate() ─── 业务层备选 (Business Layer Fallback)
    │               "模型调用 save_memory 了吗?"
    │               否 → 尝试链条中的下一个模型
    │
    ▼
_chat_with_retry() ─── 传输层备选 (Transport Layer Fallback)
    │                    HTTP 错误 → 指数退避
    │                    重试耗尽 → 遍历备选链
    │
    ▼
MODEL_MAP 备选链:
    llama-3.3-70b  ─$0.59/M─→  qwen3-32b  ─$0.29/M─→  llama-4-scout  ─$0.11/M─→  gpt-4.1-mini  ─→  claude-haiku
    (394 TPS)                   (662 TPS)                (594 TPS)                  (可靠)        (最后手段)

第 1 层处理传输失败。第 2 层处理业务逻辑失败。备选链在两层之间共享,并在中央目录中统一定义。

这与“重试同一模型”的方法有本质区别。当一个模型无法调用 Tool 时,使用相同的 Prompt 重试它收效甚微。切换到具有不同权重和不同 Tool calling 行为的另一个模型则非常有效。

模型目录:唯一事实来源

我们目录中的每个模型都有一个可选的 fallback 字段,指向下一个要尝试的模型:

@dataclass(frozen=True)
class ModelEntry:
    id: str
    label: str
    tier: str
    description: str
    fallback: str | None = None
    hidden: bool = False  # 对用户侧 /model 列表隐藏

MODEL_CATALOG = [
    # 用户可见模型 (16 个用户可以切换的模型)
    ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
               "Recommended", fallback="claude-sonnet-4-5"),
    ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
               "Stable tool calling", fallback="claude-haiku-4-5"),

    # 隐藏的整合模型 (仅限内部使用)
    ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
               "394 TPS", fallback="qwen3-32b", hidden=True),
    ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
               "662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
    # ...
]

hidden=True 标志使内部模型不出现在面向用户的 /model 命令中,同时仍参与备选链。用户看到的是他们可以切换的 16 个模型。系统实际使用了 19 个。这三个隐藏模型专门用于记忆整合等后台任务,在这些任务中,速度和成本比对话质量更重要。

该目录是所有模型路由的唯一事实来源。在备选链中添加新模型只需添加一行代码。无需同步配置文件,无需更新环境变量,也无需修改部署脚本。

传输层:带循环检测的链式备选

重试引擎使用已访问集合(visited set)来遍历备选链,以防止无限循环:

async def _chat_with_retry(self, kwargs, original_model):
    # 阶段 1:在主模型上进行指数退避
    for attempt in range(3):
        try:
            response = await acompletion(**kwargs)
            return await self._collect_stream(response)
        except (RateLimitError, APIConnectionError, APIError) as e:
            await asyncio.sleep(RETRY_DELAYS[attempt])
        except AuthenticationError:
            return LLMResponse(content="API key invalid.", finish_reason="error")

    # 阶段 2:遍历备选链
    visited = {original_model}
    current = original_model
    while True:
        entry = MODEL_MAP.get(current)
        if not entry or not entry.fallback or entry.fallback in visited:
            break
        current = entry.fallback
        visited.add(current)

        # 为此模型解析正确的 Gateway
        gw = self._resolve_gateway_for_model(current)
        resolved = self._resolve_model(current, gateway=gw)
        fb_kwargs = {**kwargs, "model": resolved}

        # 为目标模型的协议修正 api_base
        if gw and gw.default_api_base:
            fb_kwargs["api_base"] = gw.default_api_base

        try:
            response = await acompletion(**fb_kwargs)
            return await self._collect_stream(response)
        except Exception:
            continue  # 尝试链条中的下一个

    return LLMResponse(content="Service unavailable.", finish_reason="error")

visited 集合至关重要。如果没有它,像 A→B→A 这样的链条将陷入死循环。有了它,引擎会确保每个模型恰好被尝试一次。

Gateway 解析同样重要。不同的模型需要不同的 API 格式。Claude 模型通过 Anthropic 格式的 Gateway 路由(没有 /v1 后缀)。GPT 模型通过 OpenAI 兼容的 Gateway 路由(带有 /v1)。Groq 模型使用另一种端点。备选引擎会为链中的每个模型解析正确的 Gateway,防止出现将 Anthropic 请求发送到 OpenAI 端点等协议不匹配的情况。

这是大多数框架完全忽略的细节。它们假设所有模型都遵循相同的协议。在生产环境中,当 19 个模型跨越 4 种不同的 API 格式时,这种假设会立即崩溃。

业务层:Tool Call 验证

整合函数在顶层添加了自己的备选循环:

async def consolidate(self, session, provider, model, **kwargs):
    visited = set()
    current_model = model

    while current_model and current_model not in visited and len(visited) <= 3:
        visited.add(current_model)

        response = await asyncio.wait_for(
            provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
            timeout=30,
        )

        if response.has_tool_calls:
            # 成功:提取并保存记忆
            args = response.tool_calls[0].arguments
            self.write_long_term(args["memory_update"])
            self.append_history(args["history_entry"])
            return True

        # 模型没有调用 Tool —— 尝试链中下一个
        entry = MODEL_MAP.get(current_model)
        next_model = entry.fallback if entry else None
        if next_model and next_model not in visited:
            current_model = next_model
            continue

        return False  # 没有更多备选

    return False

这捕捉到了 _chat_with_retry 返回成功响应(HTTP 200,内容有效)但模型未使用 Tool 的情况。整合函数检查 has_tool_calls,如果缺失,则移动到链中的下一个模型。

超时包装器(asyncio.wait_for)也会触发备选。如果一个模型响应时间超过 30 秒(在慢速上游的 Cloudflare 524 错误中很常见),该函数会捕获 TimeoutError 并尝试下一个模型,而不是无限期地阻塞用户的会话。

为什么在整合中使用 Groq

记忆整合是一个后台任务。用户看不到输出,他们只需要它起作用。这使其成为快速、廉价模型的绝佳候选者。

大多数框架对所有任务都使用昂贵的模型。如果你正在运行 Claude Sonnet 进行对话,你也在运行 Claude Sonnet 进行记忆整合。对于一项产生的输出根本没有人阅读的任务,每千万输入 token 需花费 3 美元且每次整合耗时 8 秒以上。

我们将整合任务完全从对话模型中解耦。对话使用用户选择的任何模型,整合则使用专门的 Groq 托管模型链:

模型 速度 输入成本 输出成本
llama-3.3-70b-versatile 394 TPS $0.59/M $0.79/M
qwen3-32b 662 TPS $0.29/M $0.59/M
llama-4-scout-17b-16e 594 TPS $0.11/M $0.34/M
gpt-4.1-mini (此前) ~150 TPS $0.40/M $1.60/M

主模型(llama-3.3-70b)在约 5 秒内整合一个包含 60 条消息的会话。之前的默认模型(gpt-4.1-mini)需要 8 秒以上。每次整合的成本从约 0.003 美元降至约 0.001 美元。

权衡之处在于:Groq 模型在复杂 Prompt 下的 Tool calling 可靠性较低。这正是双层备选存在的原因。当 llama-3.3-70b 无法调用 Tool 时,qwen3-32b 会接手。如果它也失败了,llama-4-scout 会尝试。如果三个 Groq 模型都失败了,gpt-4.1-mini 以近乎 100% 的 Tool calling 可靠性来处理。

在生产环境中,我们看到主模型的成功率约为 85%。只有不到 2% 的整合任务会触及 gpt-4.1-mini。总失败率:实际为零。

生产环境结果

我们将此部署到两个 LemonClaw 实例,并使用真实的 Telegram 对话进行了测试。

第一次部署(仅有单层备选):

Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."

传输层捕获了第一次失败并进行了切换。但 qwen3-32b 返回了文本而没有调用 Tool。单层备选无法处理这种情况。这正是其他所有框架会默默丢失记忆的典型场景。

第二次部署(双层备选):

Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining

相同的模型,相同的消息量。这一次,它在第一次尝试时就成功了。Tool calling 失败的间歇性正是为什么你需要一个备选链,而不是单一备份模型的原因。

当主模型确实失败时,链条会捕捉到它:

llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done

尝试了四个模型,记忆保存成功。用户看到“新会话已开始”,完全不知道发生了这些波折。

架构差异

LemonClaw 的记忆系统与其他方案的功能对比:

能力 典型 AI Agent 框架 LemonClaw
整合模型 与对话相同(昂贵、缓慢) 独立模型链,Groq 加速
失败处理 记录警告,丢失记忆 双层备选,5 层深度
传输备选 重试同一模型 3 次 跨不同模型的链式备选
业务逻辑备选 Tool call 验证 + 模型切换
超时保护 无 (Cloudflare 524 阻塞会话) asyncio.wait_for(timeout=30) + 备选
会话截断 无 (上下文无限增长) 整合后截断旧消息
历史搜索 HISTORY.md 滚动窗口,支持 grep 搜索
内部模型 不支持 hidden=True 用于仅限系统使用的模型
循环预防 不需要(无链条) visited 集合防止 A→B→A 循环
Gateway 解析 假设单一 API 格式 带有协议检测的单模型 Gateway

表中的每一行都代表了我们亲身经历或在其他框架的 Issue 追踪器中观察到的生产环境失败。双层备选、隐藏模型目录、单模型 Gateway 解析、超时触发的备选:这些在 OpenClaw、nanobot 或我们研究过的任何其他开源 Agent 框架中都不存在。

我们的心得

“请求成功”不等于“任务成功”。 通用的重试引擎在 HTTP 层面运行。它们无法得知一个带有有效 JSON 的 200 响应实际上是失败的,仅仅因为模型没有使用你要求的 Tool。业务关键操作需要其自身的成功标准和备选逻辑。

小型模型的失败方式与大型模型不同。 大型模型(GPT-4.1, Claude Sonnet)在被要求时几乎总会调用 Tool。在快速推理引擎上的小型模型有时会生成看似有效但完全忽略 Tool schema 的响应。这不是通过 Prompt 工程可以修复的 Bug,而是一个需要架构层面缓解的能力差距。

使用生产数据而非合成数据进行测试。 我们最初使用 6 条合成消息进行的测试在每个模型上都通过了。但在包含 Tool 调用历史、时间戳和混合语言的真实 60 条消息会话中,三个 Groq 模型中有两个失败了。真实数据的复杂性会暴露干净的测试数据永远无法触及的失败模式。


LemonClaw 是一个开源 AI Agent 框架,内置多模型路由、持久化记忆和 10 余种聊天平台集成。此处描述的整个双层备选系统都在开源版本中提供。在你的服务器上运行它:github.com/hedging8563/lemonclaw

需要通过一个 API key 访问 300 多个 AI 模型?lemondata.cc 提供对 OpenAI, Anthropic, Google, DeepSeek, Groq 等的统一访问。

分享: