Cài đặt

Ngôn ngữ

Tại sao Agent AI của bạn thường xuyên bị mất bộ nhớ

L
LemonData
·5 tháng 3, 2026·748 lượt xem
Tại sao Agent AI của bạn thường xuyên bị mất bộ nhớ

AI Agent của bạn vừa có một cuộc trò chuyện kéo dài 30 phút với người dùng. Họ đã thảo luận về các yêu cầu dự án, chia sẻ sở thích và đưa ra các quyết định. Sau đó, người dùng nhập /new để bắt đầu một phiên làm việc mới.

Agent cố gắng hợp nhất cuộc trò chuyện đó vào bộ nhớ dài hạn. Lệnh gọi LLM thất bại. Lỗi Rate limit. Timeout. Hoặc model trả về văn bản thay vì gọi tool theo yêu cầu.

Bộ nhớ biến mất. Ba mươi phút ngữ cảnh đã bốc hơi hoàn toàn.

Điều này xảy ra thường xuyên hơn bạn nghĩ. Chúng tôi đã theo dõi vấn đề này trên các instance của LemonClaw: việc hợp nhất bộ nhớ có tỷ lệ thất bại khoảng 15% trên bất kỳ model đơn lẻ nào. Đối với một tính năng vốn được coi là hạ tầng ẩn, điều đó là không thể chấp nhận được.

Nếu bạn đang xây dựng bề mặt sản phẩm xung quanh thay vì chỉ là hệ thống con của bộ nhớ, hãy kết hợp trang này với hướng dẫn chatbot một khóahướng dẫn tự triển khai LemonClaw. Độ bền của bộ nhớ chỉ thực sự quan trọng khi agent thực sự tồn tại bên trong một ứng dụng có thể sử dụng được.

Cách các Framework khác xử lý vấn đề này (Thực tế là họ không làm gì cả)

Hầu hết các framework AI Agent coi việc hợp nhất bộ nhớ là một lệnh gọi LLM đơn giản. Nếu nó hoạt động, tuyệt vời. Nếu không, bộ nhớ sẽ bị mất.

Framework tiền thân của LemonClaw sử dụng cùng một model cho cả việc hợp nhất và trò chuyện. Một lệnh gọi Claude Sonnet tốn 0,003 USD và mất hơn 8 giây chỉ để tóm tắt một cuộc trò chuyện mà người dùng sẽ không bao giờ thấy. Khi lệnh gọi đó thất bại (Rate limit, timeout, lỗi model), framework sẽ ghi lại một cảnh báo và tiếp tục. Ngữ cảnh của người dùng đã mất.

nanobot, một framework phổ biến khác, cũng có kiến trúc tương tự. Một model, một lần thử, không có phương án dự phòng (fallback). Hàm hợp nhất thậm chí không có timeout. Một upstream chậm (lỗi Cloudflare 524 rất phổ biến) sẽ chặn toàn bộ phiên làm việc cho đến khi kết nối bị ngắt.

Cả hai framework đều không tách biệt việc hợp nhất khỏi model chính. Cả hai đều không có logic fallback cho các hoạt động bộ nhớ. Cả hai đều không phân biệt giữa "lệnh gọi API thất bại" và "lệnh gọi API thành công nhưng model không làm những gì chúng ta yêu cầu."

Đây không phải là những trường hợp hiếm gặp. Với tỷ lệ thất bại 15% trên bất kỳ model đơn lẻ nào, một framework thực hiện 100 lần hợp nhất mỗi ngày sẽ mất bộ nhớ trong 15 lần. Trong một tuần, đó là 105 cuộc trò chuyện mà agent quên sạch mọi thứ.

Vấn đề sâu sắc hơn cả logic Retry

Cách khắc phục hiển nhiên là retry với exponential backoff. Chúng tôi đã từng áp dụng cách đó. Nó xử lý tốt các lỗi HTTP tạm thời:

# Vòng lặp Retry: 1s → 2s → 4s backoff
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])

Cách này bắt được lỗi 429 và các sự cố mạng thoáng qua. Nhưng có hai chế độ thất bại vẫn lọt qua được:

Chế độ thất bại 1: model không thể thực hiện tool calling. Một số model, đặc biệt là các model nhỏ chạy trên các engine inference tốc độ cao, thỉnh thoảng không tạo ra được các lệnh gọi hàm hợp lệ với các prompt phức tạp. API trả về mã 200 với một ServiceUnavailableError nằm bên trong MidStreamFallbackError. Logic retry của bạn thấy một ngoại lệ, thử lại chính model đó và nhận lại cùng một lỗi.

Chế độ thất bại 2: model "thành công" nhưng không gọi tool. LLM trả về một phản hồi hoàn toàn hợp lệ. HTTP 200. Không có lỗi. Nhưng thay vì gọi save_memory với dữ liệu có cấu trúc, nó lại viết một bản tóm tắt bằng văn bản thuần túy. Engine retry của bạn coi đây là một thành công. Hàm hợp nhất kiểm tra các lệnh gọi tool, không tìm thấy lệnh nào và bỏ cuộc.

Chế độ thất bại thứ hai là cực kỳ nguy hiểm. Lớp transport nghĩ rằng mọi thứ đã hoạt động. Lớp business biết rằng nó đã thất bại. Không có lượng retry ở cấp độ HTTP nào có thể sửa được một model không hiểu schema của tool mà bạn cung cấp.

Kiến trúc Fallback hai lớp

Chúng tôi đã giải quyết vấn đề này bằng hai vòng lặp fallback độc lập hoạt động ở các cấp độ khác nhau:

Người dùng gửi /new
    │
    ▼
consolidate() ─── Business Layer Fallback
    │               "Model có gọi save_memory không?"
    │               Không → thử model tiếp theo trong chuỗi
    │
    ▼
_chat_with_retry() ─── Transport Layer Fallback
    │                    Lỗi HTTP → exponential backoff
    │                    Hết lượt retry → đi tiếp trong chuỗi fallback
    │
    ▼
Chuỗi fallback 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)                  (tin cậy)        (lựa chọn cuối cùng)

Lớp 1 xử lý các thất bại về transport. Lớp 2 xử lý các thất bại về logic business. Chuỗi fallback được chia sẻ giữa cả hai lớp, được định nghĩa một lần trong một danh mục trung tâm.

Đây là một cách tiếp cận khác biệt căn bản so với việc thử lại cùng một model. Khi một model thất bại trong việc gọi một tool, việc thử lại nó với cùng một prompt hiếm khi có tác dụng. Việc chuyển sang một model khác với trọng số khác và hành vi tool calling khác thì có.

Model Catalog: Nguồn sự thật duy nhất

Mỗi model trong danh mục của chúng tôi có một trường fallback tùy chọn trỏ đến model tiếp theo cần thử:

@dataclass(frozen=True)
class ModelEntry:
    id: str
    label: str
    tier: str
    description: str
    fallback: str | None = None
    hidden: bool = False  # Ẩn khỏi danh sách /model phía người dùng

MODEL_CATALOG = [
    # Các model hiển thị cho người dùng (16 model người dùng có thể chuyển đổi)
    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"),

    # Các model hợp nhất ẩn (chỉ sử dụng nội bộ)
    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),
    # ...
]

Cờ hidden=True giữ cho các model nội bộ không xuất hiện trong lệnh /model của người dùng trong khi vẫn tham gia vào các chuỗi fallback. Người dùng thấy 16 model họ có thể chuyển đổi. Hệ thống sử dụng 19 model. Ba model ẩn tồn tại duy nhất cho các tác vụ nền như hợp nhất bộ nhớ, nơi tốc độ và chi phí quan trọng hơn chất lượng hội thoại.

Danh mục này là nguồn sự thật duy nhất cho tất cả việc định tuyến model. Thêm một model mới vào chuỗi fallback chỉ cần thêm một dòng. Không cần đồng bộ hóa file cấu hình, không cần cập nhật biến môi trường, không cần sửa đổi script triển khai.

Lớp Transport: Fallback dạng chuỗi với tính năng phát hiện vòng lặp

Engine retry đi qua chuỗi fallback bằng cách sử dụng một tập hợp các model đã truy cập để ngăn chặn vòng lặp vô hạn:

async def _chat_with_retry(self, kwargs, original_model):
    # Giai đoạn 1: Exponential backoff trên model chính
    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")

    # Giai đoạn 2: Đi qua chuỗi fallback
    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)

        # Phân giải gateway chính xác cho model này
        gw = self._resolve_gateway_for_model(current)
        resolved = self._resolve_model(current, gateway=gw)
        fb_kwargs = {**kwargs, "model": resolved}

        # Sửa api_base cho giao thức của model mục tiêu
        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  # Thử model tiếp theo trong chuỗi

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

Tập hợp visited là cực kỳ quan trọng. Không có nó, một chuỗi như A→B→A sẽ lặp mãi mãi. Với nó, engine sẽ thử mỗi model đúng một lần.

Việc phân giải gateway cũng rất quan trọng. Các model khác nhau cần các định dạng API khác nhau. Các model Claude định tuyến qua gateway định dạng Anthropic (không có hậu tố /v1). Các model GPT định tuyến qua gateway tương thích OpenAI (có /v1). Các model Groq sử dụng một endpoint khác nữa. Engine fallback phân giải gateway chính xác cho từng model trong chuỗi, ngăn chặn sự không khớp giao thức như việc gửi yêu cầu Anthropic đến một endpoint OpenAI.

Đây là một chi tiết mà hầu hết các framework bỏ qua hoàn toàn. Họ giả định rằng tất cả các model đều nói cùng một giao thức. Trong thực tế production, với 19 model trên 4 định dạng API khác nhau, giả định đó sẽ sụp đổ ngay lập tức.

Lớp Business: Xác minh Tool Call

Hàm hợp nhất thêm vòng lặp fallback của riêng nó lên trên:

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:
            # Thành công: trích xuất và lưu bộ nhớ
            args = response.tool_calls[0].arguments
            self.write_long_term(args["memory_update"])
            self.append_history(args["history_entry"])
            return True

        # Model không gọi tool — thử model tiếp theo trong chuỗi
        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  # Không còn fallback nào khác

    return False

Điều này bắt được trường hợp _chat_with_retry trả về một phản hồi thành công (HTTP 200, nội dung hợp lệ) nhưng model không sử dụng tool. Hàm hợp nhất kiểm tra has_tool_calls, và nếu thiếu, sẽ chuyển sang model tiếp theo trong chuỗi.

Wrapper timeout (asyncio.wait_for) cũng kích hoạt fallback. Nếu một model mất hơn 30 giây (thường thấy với lỗi Cloudflare 524 trên các upstream chậm), hàm sẽ bắt TimeoutError và thử model tiếp theo thay vì chặn phiên làm việc của người dùng vô thời hạn.

Tại sao chọn Groq để hợp nhất bộ nhớ

Hợp nhất bộ nhớ là một tác vụ nền. Người dùng không thấy kết quả đầu ra. Họ chỉ cần nó hoạt động. Điều này làm cho nó trở thành ứng cử viên hoàn hảo cho các model nhanh và rẻ.

Hầu hết các framework sử dụng cùng một model đắt tiền cho mọi thứ. Nếu bạn đang chạy Claude Sonnet để trò chuyện, bạn cũng đang chạy Claude Sonnet để hợp nhất bộ nhớ. Đó là 3 USD/triệu token đầu vào và hơn 8 giây cho mỗi lần hợp nhất, cho một tác vụ tạo ra kết quả mà không con người nào đọc.

Chúng tôi đã tách biệt hoàn toàn việc hợp nhất khỏi model trò chuyện. Việc trò chuyện sử dụng bất kỳ model nào người dùng đã chọn. Việc hợp nhất sử dụng một chuỗi các model chuyên dụng được host trên Groq:

Model Tốc độ Chi phí Input Chi phí Output
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 (trước đây) ~150 TPS $0.40/M $1.60/M

Model chính (llama-3.3-70b) hợp nhất một phiên làm việc 60 tin nhắn trong khoảng 5 giây. Mặc định trước đây (gpt-4.1-mini) mất hơn 8 giây. Chi phí cho mỗi lần hợp nhất giảm từ khoảng 0,003 USD xuống còn khoảng 0,001 USD.

Sự đánh đổi: Các model Groq có khả năng tool calling kém tin cậy hơn trên các prompt phức tạp. Đó chính xác là lý do tại sao fallback hai lớp tồn tại. Khi llama-3.3-70b thất bại trong việc gọi tool, qwen3-32b sẽ tiếp quản. Nếu model đó cũng thất bại, llama-4-scout sẽ thử. Nếu cả ba model Groq đều thất bại, gpt-4.1-mini sẽ xử lý với độ tin cậy tool calling gần như 100%.

Trong môi trường production, chúng tôi thấy model chính thành công khoảng 85% thời gian. Chuỗi fallback chỉ chạm tới gpt-4.1-mini trong chưa đầy 2% số lần hợp nhất. Tỷ lệ thất bại tổng thể: thực tế là bằng không.

Kết quả thực tế trong môi trường Production

Chúng tôi đã triển khai hệ thống này cho hai instance LemonClaw và thử nghiệm với các cuộc trò chuyện Telegram thực tế.

Lần triển khai đầu tiên (chỉ có fallback một lớp):

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."

Lớp transport đã bắt được thất bại đầu tiên và thực hiện fallback. Nhưng qwen3-32b đã trả về văn bản mà không gọi tool. Fallback một lớp không thể xử lý việc này. Đây chính là kịch bản mà mọi framework khác sẽ âm thầm làm mất bộ nhớ.

Lần triển khai thứ hai (fallback hai lớp):

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

Cùng một model, cùng một khối lượng tin nhắn. Lần này nó đã hoạt động ngay từ lần thử đầu tiên. Bản chất không ổn định của lỗi tool calling chính là lý do tại sao bạn cần một chuỗi fallback thay vì chỉ một model dự phòng duy nhất.

Khi model chính thực sự thất bại, chuỗi fallback sẽ xử lý:

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

Bốn model đã được thử, bộ nhớ đã được lưu. Người dùng thấy thông báo "New session started." và không hề biết bất kỳ điều gì trong số này đã xảy ra.

Khoảng cách về kiến trúc

Hệ thống bộ nhớ của LemonClaw so với các lựa chọn thay thế, tính năng theo tính năng:

Khả năng Framework AI Agent thông thường LemonClaw
Model hợp nhất Giống như model trò chuyện (đắt, chậm) Chuỗi model độc lập, tăng tốc bởi Groq
Xử lý thất bại Ghi log cảnh báo, mất bộ nhớ Fallback hai lớp, sâu 5 model
Transport fallback Retry cùng một model 3 lần Fallback dạng chuỗi qua các model khác nhau
Business logic fallback Không có Xác minh Tool Call + chuyển đổi model
Bảo vệ Timeout Không có (Cloudflare 524 chặn phiên) asyncio.wait_for(timeout=30) + fallback
Cắt ngắn phiên làm việc Không có (ngữ cảnh tăng mãi mãi) Cắt ngắn tin nhắn cũ sau khi hợp nhất
Tìm kiếm lịch sử Không có HISTORY.md dạng rolling window, có thể tìm kiếm bằng grep
Model nội bộ Không hỗ trợ hidden=True cho các model chỉ dùng cho hệ thống
Ngăn chặn vòng lặp Không cần thiết (không có chuỗi) Tập hợp visited ngăn chặn vòng lặp A→B→A
Phân giải Gateway Giả định một định dạng API duy nhất Gateway theo từng model với tính năng phát hiện giao thức

Mỗi hàng trong bảng này đại diện cho một thất bại trong môi trường production mà chúng tôi đã tự mình trải nghiệm hoặc quan sát thấy trong trình theo dõi lỗi của các framework khác. Fallback hai lớp, danh mục model ẩn, phân giải gateway theo từng model, fallback kích hoạt bởi timeout: không điều nào trong số này tồn tại trong nanobot hay bất kỳ framework agent mã nguồn mở nào khác mà chúng tôi đã kiểm tra.

Những gì chúng tôi đã học được

"Yêu cầu thành công" không có nghĩa là "tác vụ thành công." Các engine retry thông thường hoạt động ở cấp độ HTTP. Chúng không thể biết rằng một phản hồi 200 với JSON hợp lệ thực chất là một thất bại vì model đã không sử dụng tool mà bạn yêu cầu. Các hoạt động quan trọng đối với business cần các tiêu chí thành công riêng và logic fallback riêng của chúng.

Các model nhỏ thất bại theo cách khác với các model lớn. Các model lớn (GPT-4.1, Claude Sonnet) hầu như luôn gọi tool khi được yêu cầu. Các model nhỏ trên các engine inference tốc độ cao thỉnh thoảng tạo ra các phản hồi trông có vẻ hợp lệ nhưng lại bỏ qua hoàn toàn schema của tool. Đây không phải là một lỗi bạn có thể sửa bằng prompt engineering. Đó là một khoảng cách về khả năng đòi hỏi sự giảm thiểu về mặt kiến trúc.

Thử nghiệm với dữ liệu thực tế, không phải dữ liệu giả lập. Thử nghiệm ban đầu của chúng tôi với 6 tin nhắn giả lập đã vượt qua trên mọi model. Phiên làm việc thực tế 60 tin nhắn với lịch sử gọi tool, dấu thời gian và ngôn ngữ hỗn hợp đã thất bại trên hai trong số ba model Groq. Sự phức tạp của dữ liệu thực tế làm lộ ra các chế độ thất bại mà dữ liệu thử nghiệm sạch sẽ không bao giờ có.

Đây cũng là lý do tại sao hướng dẫn giới hạn tốc độ API AI lại quan trọng ở đây. Hệ thống bộ nhớ không chỉ cần một "model tốt hơn". Nó cần một chính sách transport, một kiểm tra thành công logic business và một thang fallback không bị sụp đổ dưới những thất bại thông thường của nhà cung cấp.


LemonClaw là một framework AI Agent mã nguồn mở với tính năng định tuyến đa model tích hợp sẵn, bộ nhớ bền vững và tích hợp hơn 10 nền tảng chat. Toàn bộ hệ thống fallback hai lớp được mô tả ở đây đều có sẵn trong bản phát hành mã nguồn mở. Chạy nó trên máy chủ của riêng bạn: github.com/hedging8563/lemonclaw

Cần hơn 300 model AI thông qua một khóa API duy nhất? lemondata.cc cung cấp quyền truy cập thống nhất vào OpenAI, Anthropic, Google, DeepSeek, Groq và nhiều hơn nữa.

Share: