# Memory & Tool Architecture Plan v3 *Cập nhật sau review: Approach C vẫn là hướng đúng, nhưng bản production nên là **Approach C+**. Trọng tâm không chỉ là dynamic tool loading, mà là policy catalog rõ ràng, router multi-label, DTO nhỏ, memory budget theo model, và state có TTL/idempotency.* --- ## 1. Kết luận ngắn **Không nên load toàn bộ tools cho mọi request.** Khi domain Loyalty mở rộng từ prototype 14 tools lên 80+ tools, tool schema sẽ ăn phần lớn context window, nhất là các create tool dùng DTO sâu như `CampaignRule`. **Hướng nên làm:** Dynamic Tool Loading + Domain Knowledge Registry, nhưng không filter bằng substring `contains`. Cần chuyển thành capability policy rõ ràng: - Router phân loại `Action + Set + confidence + workflow phase` - Tool policy catalog khai báo domain, operation, risk, phase, aliases - Per-request capability bundle chỉ expose tools cần thiết - Create/update tools dùng DTO nhỏ theo từng bước, không expose full domain model - Memory budget theo model context window, không dùng một con số 8K chung - Workflow/draft state có TTL, cleanup, idempotency, optimistic locking nếu chạy production --- ## 2. Evidence hiện tại trong repo | Evidence | Ý nghĩa | |----------|---------| | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/classifier/IntentClassifier.java` đang là rule-based, single-label | Dễ drop tool cần thiết khi user hỏi vừa campaign vừa rule, hoặc query phụ trong workflow | | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/executor/AgentToolScope.java` đang suy capability từ tool name | Mở rộng bằng `contains("campaign")`/`contains("rule")` sẽ dễ false positive/false negative | | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/executor/SimpleAgentExecutor.java` hiện gọi tools bằng `READ_ONLY` | Chưa có routing scope theo domain/action ở runtime | | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/memory/InMemoryChatMemory.java` dùng estimate token và remove từng message | Có thể cắt lệch một turn user/assistant/tool, không phải whole-turn eviction | | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/config/AgentProperties.java` mặc định `maxMemoryTokens = 8000` | Không an toàn cho mọi agent vì chat model có context 8K, query/creator model 32K | | `loyalty-agent/src/main/java/dev/sonpx/loyalty/agent/core/workflow/WorkflowExecutor.java` dùng `ConcurrentHashMap` active workflows | Có cleanup khi success/cancel, nhưng chưa có TTL nền và chưa bền khi restart | | Không thấy `loyalty-mcp-server/.../draft/DraftSessionManager.java` | Ghi chú cũ trong `context.md`/plan về DraftSessionManager 1h TTL là stale, không được coi là fact | | `loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/model/CampaignRule.java` có nhiều field và nested DTO | Full `createRule(CampaignRule dto)` làm tool schema rất lớn | --- ## 3. Problem statement đúng ### Prototype hiện tại 14 MCP tools vẫn fit context khá thoải mái. Vấn đề memory chưa phải bottleneck lớn nhất. ### Khi scale full domain Nếu expose 80+ tools, đặc biệt có nhiều create/update tool với nested DTO: ```text 32K context - System prompt: ~400 tokens - 80 tool definitions: ~20K-30K tokens - Conversation memory: ~4K-8K tokens - Tool results/response: ~3K-5K tokens => Dễ overflow hoặc làm model 4B chọn sai tool ``` Vì vậy bài toán chính là **tool selection + schema size**, không chỉ conversation memory. --- ## 4. Target Architecture: Approach C+ ```mermaid flowchart TB U["User message"] --> R["Intent router"] R --> D["Action + domains + confidence + phase"] D --> P["ToolPolicyCatalog"] P --> B["Capability bundle"] B --> L["LLM call with selected tools"] D --> K["DomainKnowledgeRegistry"] K --> L L --> T["MCP tools"] L --> M["Chat memory"] W["Workflow state"] --> D W --> B ``` ### Core rule Mỗi request chỉ được thấy tools đúng domain/action/phase. Unknown intent không được load `ALL`; chỉ load discovery/read-only tools có rủi ro thấp. --- ## 5. Implementation Plan ### Phase 1: Policy catalog + safer routing 1. Tạo `ToolPolicyCatalog` - Mỗi tool khai báo: `name`, `domains`, `operation`, `risk`, `workflowPhases`, `aliases`, `requiresConfirmation` - Default deny nếu tool chưa có policy - Không suy domain bằng substring tool name 2. Nâng `IntentClassifier` - Output không chỉ là một enum intent - Dùng structure kiểu: ```java record RoutingDecision( Action action, Set domains, double confidence, WorkflowPhase phase, boolean sideQuery ) {} ``` 3. Update `SimpleAgentExecutor` - Thay `AgentToolScope.READ_ONLY` cố định bằng capability bundle sinh từ `RoutingDecision` - Nếu confidence thấp: load discovery/read-only tools, không load create/update/delete tools 4. Test cần có - Contract test: mọi `@McpTool` phải có policy hoặc bị reject có chủ đích - Routing test: campaign query, rule query, campaign+rule query, unknown query, create workflow side-query - Safety test: unknown/create-like prompt không được expose create tool khi confidence thấp ### Phase 2: Shrink tool schemas 1. Không expose full model như `CampaignRule` cho create/update - Tạo command DTO nhỏ: `CreateRuleDraftCommand`, `SetRuleFormulaCommand`, `SetRuleCriteriaCommand`, `SubmitRuleCommand` - Chia theo workflow phase để mỗi step chỉ cần schema nhỏ 2. Formula nên chọn theo type - Không load cả 8 formula schemas cùng lúc nếu user đang chọn một formula type - Dùng discriminated union hoặc staged tool: chọn formula type trước, load schema cụ thể sau 3. Test cần có - Đo token/schema size trước-sau cho selected tools - Golden tests cho create rule flow - Test validation: thiếu required field phải hỏi lại, không tự bịa value ### Phase 3: Memory budget theo model 1. Tách budget theo agent/model - Chat 8K context không nên giữ `maxMemoryTokens = 8000` - Query/creator 32K có thể giữ lớn hơn, nhưng phải trừ tool schema + result budget 2. Sửa eviction theo whole turn - Không remove lẻ từng message nếu nó làm mất cặp user/assistant/tool result - Giữ system message, prune theo turn hoặc summary 3. Test cần có - Memory không vượt budget sau nhiều turn - Tool-call/result message không bị cắt lệch khiến transcript invalid ### Phase 4: Workflow/draft persistence 1. Không dựa vào `ConcurrentHashMap` cho production - Thêm TTL nền và cleanup định kỳ nếu vẫn single-node - Nếu multi-node: dùng DB/Redis/shared store 2. State cần có - `conversationId`, `workflowId`, `version`, `expiresAt`, `owner`, `idempotencyKey` - Optimistic locking/CAS để tránh double submit hoặc update đè 3. Test cần có - Expired workflow bị cleanup - Duplicate submit dùng cùng idempotency key không tạo hai record - Hai request song song không overwrite state sai ### Phase 5: DomainKnowledgeRegistry 1. Bắt đầu bằng static registry có version/source - Mỗi snippet ghi rõ domain, source file/spec, owner, last reviewed date - Inject theo domain/action, không inject toàn bộ 2. Khi nào mới cần RAG - Tổng domain knowledge thực sự vượt khả năng maintain bằng static snippets - Knowledge thay đổi thường xuyên - Có nhu cầu search tài liệu dài/không cấu trúc RAG không giải quyết tool bloat. RAG chỉ giải quyết domain knowledge retrieval. --- ## 6. Edge Cases cần cover | Edge case | Risk | Guardrail | |-----------|------|-----------| | User hỏi nhiều domain trong một câu: "campaign X có rule nào?" | Single-label router chỉ load campaign hoặc rule | Router trả `Set` | | User đang create flow nhưng hỏi phụ: "có campaign nào active?" | Scope create thiếu read-only tools cần thiết | `sideQuery=true` cho phép bounded read-only fallback | | Tool name trùng hoặc ambiguous: `campaignRule` chứa cả campaign và rule | Substring filter load sai tools | ToolPolicyCatalog explicit | | Unknown intent nhưng prompt có từ "tạo" | Lỡ expose create tool | Unknown chỉ discovery/read-only; create cần confidence + workflow phase | | Create tool schema quá lớn | Context overflow, model chọn sai field | Staged command DTOs, schema measurement gate | | Memory 8K trên model context 8K | Không còn chỗ cho prompt/tools/response | Per-model budget formula | | Prune lẻ message | Mất tool result hoặc assistant turn, transcript invalid | Whole-turn eviction | | Restart service giữa workflow | Mất active workflow | Persist state trước production | | User submit hai lần | Double create | Idempotency key + CAS/version | | Tool result bị truncate giữa JSON | Model nhận JSON invalid | Truncate structured/result-aware, không substring raw JSON | | DomainKnowledgeRegistry stale | Bot trả sai nghiệp vụ | Version/source/owner/review date + tests | --- ## 7. Notes for implementation - Ưu tiên Spring AI built-in tool filtering/resolver nếu đang dùng được trong version hiện tại; chỉ tự build filter layer khi cần thêm policy/risk/phase. - Không dùng `ALL` trong production path trừ admin/debug mode có guard riêng. - Tool policy phải fail closed: tool mới thêm mà chưa khai báo policy thì test fail. - Với destructive hoặc state-changing tools, cần confirmation/risk flag riêng, không chỉ dựa vào intent. - Nên log `routingDecision`, selected tool names, schema token estimate, latency, tool-call success/failure để benchmark. - Quyết định RAG/multi-agent phải dựa trên số liệu: selected tool count, schema token size, tool accuracy, latency, prompt overflow rate. - Tất cả con số token hiện tại là estimate. Trước khi implement rộng, cần đo schema thực tế bằng tokenizer/model target. --- ## 8. Roadmap cập nhật ```mermaid gantt title Memory & Tool Architecture Roadmap v3 dateFormat YYYY-MM-DD section Phase 1: Routing Safety ToolPolicyCatalog + fail-closed tests :p1a, 2026-07-23, 2d RoutingDecision multi-domain/action :p1b, after p1a, 2d Runtime capability bundle :p1c, after p1b, 1d section Phase 2: Schema Reduction Create/update command DTOs :p2a, after p1c, 3d Formula staged loading :p2b, after p2a, 2d Schema token measurement gate :p2c, after p2b, 1d section Phase 3: Memory Per-model memory budgets :p3a, after p1c, 1d Whole-turn eviction :p3b, after p3a, 2d section Phase 4: Workflow Production TTL cleanup :p4a, after p3b, 1d Persistent workflow store :p4b, after p4a, 3d Idempotency + optimistic locking :p4c, after p4b, 2d section Phase 5: Knowledge Versioned DomainKnowledgeRegistry :p5a, after p2c, 2d Benchmark RAG/multi-agent necessity :p5b, after p5a, 2d ``` --- ## 9. Final verdict Approach C là đúng cho prototype và giai đoạn scale gần, nhưng chưa đủ để gọi là production best practice nếu chỉ implement bằng `contains()` và single-intent routing. Best practice nên chốt là **Approach C+**: 1. Dynamic tool loading theo policy explicit 2. Router multi-label theo action/domain/phase 3. Small command DTOs thay vì full domain DTO 4. Memory budget theo model + whole-turn eviction 5. Workflow state có TTL/persistence/idempotency 6. Domain knowledge có version/source, chưa cần RAG cho tới khi có số liệu chứng minh