Cài đặt

Ngôn ngữ

Tại sao AI Agent của bạn liên tục bị mất bộ nhớ

T
TokenLab
·5 tháng 3, 2026·1330 lượt xem
Tại sao AI Agent của bạn liên tục 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, đư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 (consolidate) cuộc trò chuyện đó vào bộ nhớ dài hạn. Lệnh gọi LLM thất bại. Rate limit. Timeout. Hoặc model trả về văn bản thay vì gọi tool được 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ĩ. Trong runtime của Hermes Agent, 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 trước khi chúng tôi thêm fallback chuyên dụng. Đối với một tính năng được coi là cơ sở 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 rate limiting AI API. Độ bền của bộ nhớ chỉ quan trọng khi Agent thực sự sống 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)

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

Cấu hình cơ bản của Hermes Agent từng 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 log 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ó 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 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 hy hữu. 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 đề nằm sâu hơn cả logic Retry

Cách khắc phục hiển nhiên là thử lại (retry) với 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 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 suy luận nhanh, thỉnh thoảng không tạo được các lệnh 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 được bọc bên trong MidStreamFallbackError. Logic retry của bạn thấy một ngoại lệ, thử lại cùng một 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 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 thấy cái nào và bỏ cuộc.

Chế độ thất bại thứ hai là thứ cực kỳ nguy hiểm. Lớp truyền tải nghĩ rằng mọi thứ đã hoạt động. Lớp nghiệp vụ biết rằng nó không hoạt động. Không có số lượng retry 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)
    │               "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)
    │                    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 lỗi truyền tải. Lớp 2 xử lý các lỗ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 trên cùng một model. Khi một model không gọi được 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ố khác và hành vi tool calling khác thì có hiệu quả.

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 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úp các model nội bộ không xuất hiện trong lệnh /model phía người dùng trong khi vẫn tham gia vào 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 (single source of truth) cho tất cả việ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 dạng chuỗi với khả năng phát hiện vòng lặp

Engine retry 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 invalid.", 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 giao thức của model đích
        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. Nếu 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ư 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 thực tế sản xuất, 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 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

Cách 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 gặp 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 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 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 để trò chuyện, bạn cũng đang chạy Claude Sonnet để hợp nhất bộ nhớ. Đó là 3 USD/1 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í đầ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 đây) ~150 TPS $0.40/M $1.60/M

Model chính (llama-3.3-70b) hợp nhất một phiên 60 tin nhắn trong khoảng 5 giây. Model 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 gọi tool 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 không gọi được 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 gọi tool gần như 100%.

Trong thực tế sản xuất, chúng tôi thấy model chính thành công khoảng 85% thời gian. Chuỗi chỉ chạm đến 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ế 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 runtime của Hermes Agent và thử nghiệm với các cuộc trò chuyện thực tế trên Telegram.

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 truyền tải đã bắt được lỗ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 gọi tool 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 sẽ bắt được nó:

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 "Phiên mới đã bắt đầu." 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

So sánh hệ thống bộ nhớ với các lựa chọn thay thế, theo từng tính năng:

Khả năng Framework AI Agent điển hình Thiết kế Fallback hai lớp
Model hợp nhất Giống như 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
Fallback truyền tải Thử lại cùng một model 3 lần Fallback dạng 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 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ó Cửa sổ trượt HISTORY.md, có thể tì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 khả 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 thực tế sản xuất mà chúng tôi đã tự mình trải nghiệm hoặc quan sát thấy trong các 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 có điều nào trong số này tồn tại trong nanobot hoặc 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 về nghiệp vụ 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 suy luận nhanh 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 mà 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ờ làm được.

Đây cũng là lý do tại sao hướng dẫn rate limiting AI API 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 truyền tải, một kiểm tra thành công logic nghiệp vụ 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.


Bài viết này mô tả một mô hình độ tin cậy từ Hermes Agent. Hãy sử dụng mô hình kiến trúc ở đây làm tài liệu tham khảo cho độ bền của bộ nhớ, thiết kế fallback và định tuyến model nhận biết gateway.

Cần hơn 300 model AI thông qua một API key duy nhất? tokenlab.sh 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.

Chia sẻ: