Tôi đang vận hành SRE K8s Agent (một chatbot AI được xây dựng cho đội ngũ SRE tại GreenNode). Nhưng có một vấn đề khá rõ ràng: mỗi khi mở một tab mới, agent gần như bắt đầu lại từ đầu. Người dùng phải liên tục cung cấp lại context, đang theo dõi cluster nào, vấn đề gì đang xảy ra và những gì đã trao đổi trước đó.

Bài toán cần giải quyết: agent "quên" sau mỗi lần mở tab mới

SRE K8s Agent tại GreenNode có khả năng query metrics, đọc log, phân tích node health và chạy runbook tự động trên các cụm VKS, chạy trên nền LLM qwen/qwen3-5-27b qua MaS API. Về mặt năng lực, agent đủ mạnh để trả lời các câu hỏi vận hành phức tạp.

Tuy nhiên, khả năng reasoning của LLM không giải quyết được một vấn đề cơ bản: agent không có long-term memory giữa các session.

Ví dụ, người dùng đã trao đổi với agent về: cluster prod-01 đang gặp memory leak tại namespace payments.

Nhưng khi mở một tab mới, toàn bộ context này biến mất. Người dùng phải cung cấp lại cluster, namespace và vấn đề đang xảy ra.

Đây là friction point rất thực trong công việc hàng ngày. Người dùng gõ lại "cluster prod-01 đang có vấn đề với memory leak ở namespace payments": thông tin mà agent đã biết từ hôm qua, chỉ vì họ mở một tab mới.

Câu hỏi đặt ra không mới, nhưng lời giải thì không hiển nhiên: có cách nào để agent nhớ được lịch sử hội thoại xuyên suốt các session mà không cần nhồi toàn bộ chat history vào prompt, vừa tốn token, vừa chậm?

TencentDB-Agent-Memory là gì?

TencentDB-Agent-Memory (với 17.6k star trên GitHub) là một team-level memory hub cho AI agent. Thay vì lưu raw chat log rồi nhét ngược vào prompt như cách RAG thông thường vẫn làm, nó chiết lọc hội thoại, tài liệu và code thành bốn loại memory asset có thể tái sử dụng: Chat Memory (lịch sử hội thoại, preferences, facts, decisions xuyên session), Skill (quy trình tái sử dụng như troubleshooting, code review, release checklist), LLM-Wiki (tài liệu, design spec, runbook tổ chức thành structured page có link graph), và CodeGraph (index code symbol, file, call relationship và impact path).

Điểm khác biệt cốt lõi so với RAG phẳng nằm ở kiến trúc bốn lớp, với độ trừu tượng tăng dần từ raw text đến profile dài hạn:

4 layers architecture

LayerLưu trữ gìCơ chế chínhDùng để làm gì
L0: Raw ConversationToàn bộ cuộc hội thoại, timestamp, sourceBM25 full-text searchKiểm tra chính xác từ ngữ, truy nguồn gốc
L1: AtomFacts, preferences, constraints trích từ hội thoạiLLM distillation (30s idle timeout)Recall thông tin hành động cụ thể
L2: ScenarioKnowledge block theo project/chủ đềScene clustering sau L1 30sKhôi phục nhanh working context
L3: PersonaLong-term profile, stable patternPersona build sau L2Agent nhanh chóng hiểu người dùng, team

Về mặt thiết kế, pipeline L1→L2→L3 chạy bất đồng bộ và không block response stream. L1 được kích hoạt sau một khoảng thời gian idle, sau đó L2 và L3 tiếp tục xây dựng context ở mức trừu tượng cao hơn. Tuy nhiên, trong quá trình triển khai thực tế với hội thoại tiếng Việt, pipeline này không hoạt động như kỳ vọng. Phần này sẽ được phân tích chi tiết ở các vấn đề triển khai bên dưới.

Về mặt hiệu quả, benchmark PersonaMem — đo khả năng agent hiểu và áp dụng thông tin người dùng sau nhiều lượt tương tác — cho kết quả 48% khi không có memory, so với 76% khi có TencentDB-Agent-Memory, tương đương cải thiện +59%.

Kiến trúc triển khai trên VM GreenNode

Toàn bộ SRE Agent chạy trên một VM, các service được containerize và kết nối qua Docker bridge network sre-net. Container giao tiếp với nhau qua container name như hostname — không cần IP tĩnh. sre-backend gọi tdai-memory qua http://tdai-memory:3701, Docker DNS tự resolve container name trên cùng bridge network; port 3701 chỉ expose ra 127.0.0.1 trên host, không public ra ngoài.

Docker network

Bốn endpoint được dùng thực tế trong production:

EndpointMô tảSử dụng thực tế
POST /captureLưu một lượt hội thoại vào L0Gọi sau khi agent trả lời (background thread)
POST /search/conversationsBM25 full-text search trực tiếp L0Thay thế /recall — nhanh hơn, không cần LLM
POST /recallTruy xuất memory từ L1+ (LLM-based)Không dùng — L1 thất bại với tiếng Việt
GET /healthHealth check gatewayMonitoring, readiness probe

Đáng chú ý nhất trong bảng trên là dòng thứ ba: endpoint được thiết kế làm tính năng chính của package lại là endpoint bị loại khỏi production flow — lý do nằm ở phần gotcha bên dưới.

6 bước để AI Agent truy xuất và lưu memory trong mỗi request

Mỗi lượt hội thoại đi qua bốn giai đoạn chính, trong đó giai đoạn capture chạy nền để không chặn response stream:

  1. POST /api/v1/agent-ops/chat - frontend gửi {message, history, session_id}
  2. POST /search/conversations - sre-backend gọi BM25 search lấy memory liên quan (timeout 3s)
  3. Inject vào system prompt - nếu có kết quả, thêm vào section "Lịch sử hội thoại liên quan"
  4. Chat completion streaming - gọi MaaS LLM với system prompt đã được augment
  5. Render response - SSE stream token về client, render trong UI
  6. POST /capture (background thread) - lưu user_content và assistant_content vào L0, không block response

Luồng xử lý có thể tóm tắt như sau: User → Search Memory → Inject Context → LLM → Stream Response → Capture Conversation. Việc capture được chạy ở background để không ảnh hưởng đến response stream của agent.

Cách triển khai: code thực tế

Bước 1: deploy container tdai-memory:

# Tạo network nếu chưa có
docker network create sre-net

docker run -d \
  --name tdai-memory \
  --network sre-net \
  -p 127.0.0.1:3701:3701 \
  -v /opt/tdai-data:/data/tdai-memory \
  -e TDAI_LLM_BASE_URL=https://maas-llm-aiplatform-hcm.api.vngcloud.vn/v1 \
  -e TDAI_LLM_API_KEY=<maas-api-key> \
  -e TDAI_LLM_MODEL=qwen/qwen3-5-27b \
  tencentdb-agent-memory:latest

⚠️ Gateway tìm config tại ./tdai-gateway.yaml (CWD = /opt/tdai-gateway/), không phải TDAI_DATA_DIR. Đây là gotcha quan trọng nhất — đặt file nhầm chỗ làm config bị ignore hoàn toàn, không có error message nào cảnh báo.

Bước 2: tdai-gateway.yaml, phần cấu hình quyết định:

memory:
  recall:
    strategy: keyword   # keyword=BM25; hybrid=cần EmbeddingService (không dùng)
    maxResults: 5
    scoreThreshold: 0.1
  extraction:
    enabled: true
    maxMemoriesPerSession: 20
    pipeline:
      everyNConversations: 3
      l1IdleTimeoutSeconds: 30   # giảm từ 600s mặc định
      l2DelayAfterL1Seconds: 30
      l2MinIntervalSeconds: 60
  bm25:
    enabled: true
    language: en   # en vì BM25 tiếng Việt chưa optimize tốt

Bước 3: helper function phía Python, chỉ dùng urllib stdlib, không thêm dependency:

_TDAI_URL = "http://tdai-memory:3701"

def _tdai_search(query: str, session_key: str, limit: int = 3) -> str:
    try:
        data = json.dumps({
            "query": query, "limit": limit, "session_key": session_key
        }).encode()
        req = urllib.request.Request(
            f"{_TDAI_URL}/search/conversations",
            data=data, headers={"Content-Type": "application/json"}, method="POST"
        )
        with urllib.request.urlopen(req, timeout=3) as resp:
            return json.loads(resp.read().decode()).get("results", "")
    except Exception as e:
        log.debug(f"[tdai] search: {e}")
        return ""

def _tdai_capture_bg(user_content: str, assistant_content: str, session_key: str):
    try:
        data = json.dumps({
            "user_content": user_content,
            "assistant_content": assistant_content,
            "session_key": session_key,
        }).encode()
        req = urllib.request.Request(
            f"{_TDAI_URL}/capture",
            data=data, headers={"Content-Type": "application/json"}, method="POST"
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            resp.read()
    except Exception as e:
        log.debug(f"[tdai] capture: {e}")

Bước 4: tích hợp vào agent_chat endpoint:

# Search memory trước khi gọi LLM
_mem_context = await loop.run_in_executor(None, _tdai_search, message, session_id)
if _mem_context:
    system_prompt += f"\n\n## Lịch sử hội thoại liên quan\n{_mem_context}"
    log.info(f"[tdai] injected len={len(_mem_context)} session={session_id!r}")

# Capture async sau khi agent done (background thread)
finally:
    if _response_parts:
        threading.Thread(
            target=_tdai_capture_bg,
            args=(message, "".join(_response_parts), session_id),
            daemon=True,
        ).start()

Kết quả thực tế

Sau khi deploy, log từ sre-backend xác nhận memory injection và capture hoạt động đúng như thiết kế:

[tdai] injected memory context len=342 session="prod-session-01"
[tdai] captured session="prod-session-01"

Các con số đo được: ~3ms cho memory search latency (BM25 chạy local, không qua mạng), HTTP 200 cho mọi response /capture, 0 dependency thêm ngoài urllib stdlib, và capture chạy qua background thread nên không ảnh hưởng đến response stream.

Kết quả: agent giờ có "ký ức" — một cải thiện UX rất rõ ràng khi vận hành production K8s hàng ngày. Backend log xác nhận [tdai] injected memory context và [tdai] captured ở mỗi request. Nhưng con đường đến kết quả này không thẳng — sáu gotcha dưới đây là phần tốn thời gian nhất.

6 vấn đề thực tế khi tích hợp TencentDB-Agent-Memory

#Vấn đềNguyên nhânCách xử lý
1Config lookup path ≠ TDAI_DATA_DIRGateway tìm config tại ./tdai-gateway.yaml (CWD), không phải thư mục dataĐặt file đúng thư mục /opt/tdai-gateway/
2Strategy hybrid yêu cầu EmbeddingServiceDefault config dùng hybrid. Gọi /recall trả về code 10001Đổi sang keyword (BM25) trong tdai-gateway.yaml
3L1 extraction thất bại với tiếng ViệtLLM chạy 48s, 977 input token, chỉ sinh 77 token — memory list rỗngBypass L1, dùng /search/conversations (L0 BM25) trực tiếp
4/capture API format: user_content/assistant_contentTài liệu và code example có inconsistencyDùng flat object: user_content, assistant_content, session_key
5Memory recall cần đặt SAU khi loop khởi tạoawait loop.run_in_executor cần biến loop tồn tại trướcKiểm tra thứ tự khởi tạo biến trong hàm async
6Container settings bị override khi rebuild/root/.sre-agent/settings.json trong image có URL cũMount settings.json qua Docker volume thay vì bake vào image

Gotcha số 3 là điểm đáng nói nhất, vì nó không phải lỗi cấu hình mà là giới hạn thực sự của pipeline: LLM distillation cho L1 chạy mất 48 giây, tiêu tốn 977 input token, nhưng chỉ sinh ra 77 token đầu ra — và danh sách memory kết quả rỗng. Nói cách khác, pipeline L1→L2→L3 không hoạt động với hội thoại tiếng Việt trong thực tế. Memory recall production hiện tại chỉ là BM25 keyword match trên L0 raw storage, không phải semantic understanding như thiết kế ban đầu của package. Đây là một compromise thực dụng: hệ thống vẫn có context, nhưng chưa có "hiểu" thật sự. Để fix triệt để, cần customize extraction prompt hoặc dùng model tốt hơn cho tiếng Việt.

Hướng cải thiện tiếp theo

  • Mount settings.json qua Docker volume để không bị mất khi rebuild container
  • Customize L1 extraction prompt cho tiếng Việt và domain SRE (tên cluster, tên pod, thuật ngữ K8s)
  • Thử strategy embedding với local embedding model để có semantic recall thay vì chỉ BM25 keyword
  • Implement memory TTL — tự động xoá conversation cũ hơn 30 ngày để tránh L0 phình to
  • Thêm memory namespace theo từng cluster để recall có thể filter theo VKS cluster cụ thể
  • Xem xét TencentDB-Agent-Memory v2.0+ với Memory Hub đầy đủ — hỗ trợ Skill, Wiki, CodeGraph và team sharing

Kết luận

Kết quả cuối cùng không phải là "memory hoàn hảo" như tên package hứa hẹn, mà là một compromise chấp nhận được: BM25 keyword search trên raw conversation, đủ để agent không còn là trang giấy trắng ở mỗi tab mới, với chi phí gần như bằng không - 3ms latency, không thêm dependency, không block response stream. Phần LLM distillation nhiều lớp phía sau vẫn còn đó như một hướng cải thiện, không phải một tính năng đang chạy. Với một hệ thống production tiếng Việt, đây có lẽ là bài học chung: đọc kỹ log trước khi tin vào tài liệu, và sẵn sàng dùng lớp đơn giản nhất hoạt động được thay vì lớp phức tạp nhất được quảng cáo.