Cài đặt

Ngôn ngữ

Tại sao AI Agent của bạn hay bị "mất trí nhớ" (Và cách chúng tôi đã khắc phục)

L
LemonData
·28 tháng 2, 2026·17 lượt xem
#tác nhân AI#bộ nhớ#phương án dự phòng#kiến trúc#LemonClaw
Tại sao AI Agent của bạn hay bị "mất trí nhớ" (Và cách chúng tôi đã khắc phục)

Tại sao AI Agent của bạn liên tục mất trí nhớ (Và cách chúng tôi đã khắc phục)

AI Agent của bạn vừa có một cuộc hội thoại 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, đưa ra 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 hội thoại đó vào bộ nhớ dài hạn. Lời gọi LLM thất bại. Lỗi giới hạn tốc độ (Rate limit). Hết thời gian chờ (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 việc này trên các instance LemonClaw của mình: việc hợp nhất bộ nhớ (memory consolidation) 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 được coi là cơ sở hạ tầng vô hình, điều đó là không thể chấp nhận được.

Các Framework khác xử lý việc này như thế nào (Thực ra là họ không xử lý)

Hầu hết các framework AI Agent coi việc hợp nhất bộ nhớ như một lời 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.

OpenClaw, framework agent mã nguồn mở phổ biến nhất, sử dụng cùng một model cho việc hợp nhất cũng như cho cuộc hội thoại. Một lời gọi Claude Sonnet tốn 0,003 USD và mất hơn 8 giây chỉ để tóm tắt một đoạn chat mà người dùng sẽ không bao giờ nhìn thấy. Khi lời gọi đó thất bại (giới hạn tốc độ, timeout, lỗi model), framework 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 kết nối 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ời gọi API thất bại" và "lời gọi API thành công nhưng model không làm theo yêu cầu."

Đây không phải là các trường hợp ngoại lệ. 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ớ ở 15 lần trong số đó. Trong một tuần, đó là 105 cuộc hội thoại mà agent quên sạch mọi thứ.

Vấn đề sâu sắc hơn cả Logic thử lại (Retry)

Cách khắc phục hiển nhiên là thử lại với khoảng thời gian chờ tăng dần (exponential backoff). Chúng tôi đã có điều đó. Nó xử lý tốt các lỗi HTTP tạm thời:

# Vòng lặp thử lại: 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 chớp nhoáng. 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 gọi tool (tool calling). Một số model, đặc biệt là các model nhỏ chạy trên các engine inference nhanh, thỉnh thoảng thất bại trong việc tạo ra các lời gọi hàm hợp lệ trên các prompt phức tạp. API trả về mã 200 với một ServiceUnavailableError nằm bên trong MidStreamFallbackError. Logic thử lại của bạn thấy một ngoại lệ, thử lại chính model đó và nhận 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 đoạn tóm tắt bằng văn bản thuần túy. Engine thử lại 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ời gọi tool, không tìm thấy gì và bỏ cuộc.

Chế độ thất bại thứ hai là loại thâm độc nhất. Lớp truyền tải nghĩ rằng mọi thứ đều ổn. Lớp nghiệp vụ biết là không phải vậy. Không có số lượng lần thử lại ở cấp độ HTTP nào có thể sửa được một model không hiểu schema tool của bạn.

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() ─── Fallback lớp nghiệp vụ (Business Layer Fallback)
    │               "Model có gọi save_memory không?"
    │               Không → thử model tiếp theo trong chuỗi
    │
    ▼
_chat_with_retry() ─── Fallback lớp truyền tải (Transport Layer Fallback)
    │                    Lỗi HTTP → exponential backoff
    │                    Hết lượt thử lại → duyệt 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)

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

Đây là một cách tiếp cận khác biệt cơ 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 giúp ích. Việc chuyển sang một model khác với trọng số (weights) khác và hành vi gọi tool khác thì có tác dụng.

Danh mục Model: 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 cho 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",
               "Khuyên dùng", fallback="claude-sonnet-4-5"),
    ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
               "Gọi tool ổn định", fallback="claude-haiku-4-5"),

    # Các model hợp nhất bị ẩn (chỉ 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 hướng tới 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. 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ả các định tuyến model. Thêm một model mới vào chuỗi fallback có nghĩa là thêm một dòng code. Không cần đồng bộ 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 truyền tải: Fallback theo chuỗi với khả năng phát hiện vòng lặp

Engine thử lại duyệt qua chuỗi fallback bằng cách sử dụng một tập hợp visited để 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 không hợp lệ.", finish_reason="error")

    # Giai đoạn 2: Duyệt 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)

        # Xác định 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 đúng 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="Dịch vụ không khả dụng.", 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 xác định 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 xác định 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ư 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 tất cả các model đều nói cùng một giao thức. Trong môi trường production, với 19 model trên 4 định dạng API khác nhau, giả định đó sẽ đổ vỡ ngay lập tức.

Lớp nghiệp vụ: Xác minh gọi Tool

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 nữa

    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, nó sẽ chuyển sang model tiếp theo trong chuỗi.

Trình bao bọc 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 cho việc hợp nhất

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 một ứ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 cho hội thoại, bạn cũng đang chạy Claude Sonnet cho 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 hội thoại. Hội thoại sử dụng bất kỳ model nào người dùng đã chọn. 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í đầu vào Chi phí đầu ra
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 đó) ~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ừ ~$0,003 xuống ~$0,001.

Sự đánh đổi: Các model Groq có khả năng gọi tool kém tin cậy hơn trên các prompt phức tạp. Đó chính 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 nó 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 gọi tool gần như 100%.

Trong thực tế, 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 đến gpt-4.1-mini trong ít hơn 2% các lần hợp nhất. Tỷ lệ thất bại tổng thể: gần như bằng không.

Kết quả thực tế

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 hội thoại Telegram thực tế.

Triển khai lần đầu (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 truyền tải đã 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ớ.

Triển khai lần 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 lượng tin nhắn. Lần này nó hoạt động ngay lần thử đầu tiên. Bản chất không ổn định của lỗi gọi tool chính là lý do tại sao bạn cần một chuỗi fallback thay vì một model dự phòng duy nhất.

Khi model chính thực sự thất bại, chuỗi sẽ bắt được:

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 AI Agent Framework thông thường LemonClaw
Model hợp nhất Giống như hội thoại (đắ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
Fallback truyền tải Thử lại cùng một model 3 lần Fallback theo chuỗi qua các model khác nhau
Fallback logic nghiệp vụ Không có Xác minh gọi tool + 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 (Truncation) Không có (ngữ cảnh tăng mãi mãi) Cắt bớt tin nhắn cũ sau khi hợp nhất
Tìm kiếm lịch sử Không có Cửa sổ trượt HISTORY.md, 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ành cho hệ thống
Ngăn chặn vòng lặp Không cần (không có chuỗi) Tập hợp visited ngăn chặn vòng lặp A→B→A
Xác định Gateway Giả định một định dạng API duy nhất Gateway cho mỗi model với nhận diện giao thức

Mỗi hàng trong bảng này đại diện cho một thất bại trong thực tế mà chúng tôi đã tự 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, xác định gateway theo từng model, fallback kích hoạt bằng timeout: không điều nào trong số này tồn tại trong OpenClaw, nanobot, hoặc bất kỳ framework agent mã nguồn mở nào khác mà chúng tôi đã xem xét.

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

"Yêu cầu thành công" không phải là "tác vụ thành công." Các engine thử lại 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 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 về nghiệp vụ cần các tiêu chí thành công và logic fallback của riêng 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 nhanh đôi khi 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à lỗi bạn có thể sửa bằng prompt engineering. Đó là một lỗ hổng về khả năng đòi hỏi sự giảm thiểu bằng kiến trúc.

Kiểm thử 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 60 tin nhắn thực tế 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ế bộc lộ các chế độ thất bại mà dữ liệu kiểm thử sạch sẽ không bao giờ có.


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 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 API key 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: