diff --git a/memory_analysis.md b/memory_analysis.md new file mode 100644 index 0000000..8b61e54 --- /dev/null +++ b/memory_analysis.md @@ -0,0 +1,244 @@ +# 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 diff --git a/start-all.ps1 b/start-all.ps1 index 9a647bc..ea55250 100644 --- a/start-all.ps1 +++ b/start-all.ps1 @@ -31,71 +31,50 @@ foreach ($port in 9331, 9332, 9333) { } } -$ProcessList = @() -try { - Write-Host "==========================================" - Write-Host "Starting Loyalty Agent Services" - Write-Host "==========================================" +Write-Host "==========================================" +Write-Host "Starting Loyalty Agent Services" +Write-Host "==========================================" - # 1. Start MCP Server - Write-Host "Starting loyalty-mcp-server..." - $mcpLog = Join-Path $LogDir "loyalty-mcp-server.log" - $mcpProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c ..\mvnw.cmd spring-boot:run -Dmaven.test.skip=true > `"$mcpLog`" 2>&1" -WorkingDirectory (Join-Path $ProjectRoot "loyalty-mcp-server") -PassThru -WindowStyle Hidden - $ProcessList += $mcpProcess - Write-Host "loyalty-mcp-server started with PID: $($mcpProcess.Id)" +# 1. Start MCP Server +Write-Host "Starting loyalty-mcp-server..." +$mcpLog = Join-Path $LogDir "loyalty-mcp-server.log" +$mcpProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c ..\mvnw.cmd spring-boot:run -Dmaven.test.skip=true > `"$mcpLog`" 2>&1" -WorkingDirectory (Join-Path $ProjectRoot "loyalty-mcp-server") -PassThru -WindowStyle Hidden +Write-Host "loyalty-mcp-server started with PID: $($mcpProcess.Id)" - Write-Host "Waiting for loyalty-mcp-server to initialize..." - while ($true) { - $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9331" - if ($netstat) { break } - Start-Sleep -Seconds 2 - } - Write-Host "loyalty-mcp-server is up!" - - # 2. Start Agent Service - Write-Host "Starting loyalty-agent..." - $agentLog = Join-Path $LogDir "loyalty-agent.log" - $agentProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c ..\mvnw.cmd spring-boot:run -Dmaven.test.skip=true > `"$agentLog`" 2>&1" -WorkingDirectory (Join-Path $ProjectRoot "loyalty-agent") -PassThru -WindowStyle Hidden - $ProcessList += $agentProcess - Write-Host "loyalty-agent started with PID: $($agentProcess.Id)" - - Write-Host "Waiting for loyalty-agent to initialize..." - while ($true) { - $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9332" - if ($netstat) { break } - Start-Sleep -Seconds 2 - } - Write-Host "loyalty-agent is up!" - - # 3. Start Frontend UI - Write-Host "Starting Frontend UI..." - $uiLog = Join-Path $LogDir "frontend.log" - $uiProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c pnpm dev > `"$uiLog`" 2>&1" -WorkingDirectory $ProjectRoot -PassThru -WindowStyle Hidden - $ProcessList += $uiProcess - Write-Host "Frontend UI started with PID: $($uiProcess.Id)" - - Write-Host "==========================================" - Write-Host "All services started successfully!" - Write-Host "Log files are stored in: $LogDir" - Write-Host "- $mcpLog" - Write-Host "- $agentLog" - Write-Host "- $uiLog" - Write-Host "==========================================" - Write-Host "Press Ctrl+C to stop all services..." - - # Wait indefinitely - while ($true) { - Start-Sleep -Seconds 1 - } +Write-Host "Waiting for loyalty-mcp-server to initialize..." +while ($true) { + $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9331" + if ($netstat) { break } + Start-Sleep -Seconds 2 } -finally { - Write-Host "`nStopping services..." - foreach ($p in $ProcessList) { - if (-not $p.HasExited) { - taskkill /PID $p.Id /T /F 2>&1 | Out-Null - Write-Host "Stopped process tree for PID $($p.Id)" - } - } - Write-Host "All services stopped." +Write-Host "loyalty-mcp-server is up!" + +# 2. Start Agent Service +Write-Host "Starting loyalty-agent..." +$agentLog = Join-Path $LogDir "loyalty-agent.log" +$agentProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c ..\mvnw.cmd spring-boot:run -Dmaven.test.skip=true > `"$agentLog`" 2>&1" -WorkingDirectory (Join-Path $ProjectRoot "loyalty-agent") -PassThru -WindowStyle Hidden +Write-Host "loyalty-agent started with PID: $($agentProcess.Id)" + +Write-Host "Waiting for loyalty-agent to initialize..." +while ($true) { + $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9332" + if ($netstat) { break } + Start-Sleep -Seconds 2 } +Write-Host "loyalty-agent is up!" + +# 3. Start Frontend UI +Write-Host "Starting Frontend UI..." +$uiLog = Join-Path $LogDir "frontend.log" +$uiProcess = Start-Process -FilePath "cmd.exe" -ArgumentList "/c pnpm dev > `"$uiLog`" 2>&1" -WorkingDirectory $ProjectRoot -PassThru -WindowStyle Hidden +Write-Host "Frontend UI started with PID: $($uiProcess.Id)" + +Write-Host "==========================================" +Write-Host "All services started successfully!" +Write-Host "Log files are stored in: $LogDir" +Write-Host "- $mcpLog" +Write-Host "- $agentLog" +Write-Host "- $uiLog" +Write-Host "==========================================" +