设置

语言

为什么你的 AI Agent 总是丢失记忆

L
LemonData
·2026年3月5日·743 次浏览
为什么你的 AI Agent 总是丢失记忆

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

Agent 尝试将该对话整合到长期记忆中。LLM 调用失败了。可能是速率限制、超时,或者是模型返回了文本而不是调用所需的工具。

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

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

如果你正在构建产品表面而不仅仅是记忆子系统,请将此页面与 单 Key 聊天机器人指南自托管 LemonClaw 指南 结合阅读。只有当 Agent 真正运行在可用的应用程序中时,记忆的持久性才有意义。

其他框架是如何处理的(其实它们并不处理)

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

LemonClaw 的前身框架在整合和对话时使用相同的模型。一次 Claude Sonnet 调用耗时 8 秒以上,成本为 $0.003,仅仅是为了总结一段用户永远不会看到的对话。当调用失败(速率限制、超时、模型错误)时,框架只会记录一条警告并继续运行。用户的上下文就这样丢失了。

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

这两个框架都没有将整合与主模型分离,也没有为记忆操作提供回退逻辑,更没有区分“API 调用失败”和“API 调用成功但模型未按要求执行”这两种情况。

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

问题比重试逻辑更深层

显而易见的修复方案是使用指数退避进行重试。我们曾经也这样做过。它能很好地处理瞬时 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:模型“成功”了但没有调用工具。LLM 返回了一个完全有效的响应。HTTP 200。没有错误。但它没有使用结构化数据调用 save_memory,而是写了一段纯文本总结。你的重试引擎认为这是成功的。整合函数检查工具调用,发现没有,于是放弃了。

第二种失败模式更具隐蔽性。传输层认为一切正常,但业务层知道它失败了。对于一个不理解你的工具 Schema 的模型,无论进行多少次 HTTP 级别的重试都无济于事。

双层回退架构

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

用户发送 /new
    │
    ▼
consolidate() ─── 业务层回退
    │               "模型调用 save_memory 了吗?"
    │               否 → 尝试链中的下一个模型
    │
    ▼
_chat_with_retry() ─── 传输层回退
    │                    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)                  (可靠)            (最后手段)

第一层处理传输失败。第二层处理业务逻辑失败。回退链在两层之间共享,并在中央目录中统一配置。

这与“重试同一模型”的方法有着本质的区别。当一个模型无法调用工具时,使用相同的 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):
    # 第一阶段:在主模型上进行指数退避
    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")

    # 第二阶段:遍历回退链
    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 格式的网关路由(没有 /v1 后缀);GPT 模型通过 OpenAI 兼容的网关路由(带有 /v1);Groq 模型则使用另一个端点。回退引擎会为链中的每个模型解析正确的网关,防止出现将 Anthropic 请求发送到 OpenAI 端点等协议不匹配的情况。

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

业务层:工具调用验证

整合函数在此基础上增加了自己的回退循环:

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

        # 模型没有调用工具 —— 尝试链中的下一个
        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,内容有效)但模型未调用工具的情况。整合函数检查 has_tool_calls,如果缺失,则移动到链中的下一个模型。

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

为什么选择 Groq 进行整合

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

大多数框架对所有任务都使用相同的昂贵模型。如果你使用 Claude Sonnet 进行对话,你也会使用 Claude Sonnet 进行记忆整合。对于一项产出物人类从不阅读的任务,这需要花费 $3/M 输入 token 且每次整合耗时 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 无法调用工具时,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 返回了文本而没有调用工具。单层回退无法处理这种情况。这正是其他所有框架都会默默丢失记忆的典型场景。

第二次部署(双层回退):

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

相同的模型,相同的消息量。这一次在第一次尝试时就成功了。工具调用失败的间歇性特征正是你需要回退链而不是单一备选模型的原因。

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

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 次 跨不同模型的链式回退
业务逻辑回退 工具调用验证 + 模型切换
超时保护 无(Cloudflare 524 阻塞会话) asyncio.wait_for(timeout=30) + 回退
会话截断 无(上下文无限增长) 整合后截断旧消息
历史搜索 HISTORY.md 滚动窗口,支持 grep 搜索
内部模型 不支持 hidden=True 仅供系统使用
循环预防 不需要(无链条) visited 集合防止 A→B→A 循环
网关解析 假设单一 API 格式 针对每个模型的网关及协议检测

表中的每一行都代表了我们亲身经历过或在其他框架的 issue 列表中观察到的生产环境失败。双层回退、隐藏模型目录、针对每个模型的网关解析、超时触发的回退:这些在我们研究过的 nanobot 或任何其他开源 Agent 框架中都不存在。

我们的经验教训

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

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

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

这也是为什么 AI API 速率限制指南 在这里很重要的原因。记忆系统不仅仅需要一个“更好的模型”。它需要传输策略、业务逻辑成功检查,以及一个在常规供应商故障下不会崩溃的回退阶梯。


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

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

分享: