refactor: remove manual process management from start-all.ps1 and add memory architecture planning document

This commit is contained in:
SonPhung
2026-07-22 23:36:06 +07:00
parent 66385746f6
commit 2bbbff1ec9
2 changed files with 286 additions and 63 deletions

244
memory_analysis.md Normal file
View File

@@ -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<Domain> + 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<Domain> 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<Domain>` |
| 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

View File

@@ -31,71 +31,50 @@ foreach ($port in 9331, 9332, 9333) {
} }
} }
$ProcessList = @()
try { Write-Host "=========================================="
Write-Host "==========================================" Write-Host "Starting Loyalty Agent Services"
Write-Host "Starting Loyalty Agent Services" Write-Host "=========================================="
Write-Host "=========================================="
# 1. Start MCP Server # 1. Start MCP Server
Write-Host "Starting loyalty-mcp-server..." Write-Host "Starting loyalty-mcp-server..."
$mcpLog = Join-Path $LogDir "loyalty-mcp-server.log" $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 $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)"
Write-Host "loyalty-mcp-server started with PID: $($mcpProcess.Id)"
Write-Host "Waiting for loyalty-mcp-server to initialize..." Write-Host "Waiting for loyalty-mcp-server to initialize..."
while ($true) { while ($true) {
$netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9331" $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9331"
if ($netstat) { break } if ($netstat) { break }
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
} }
Write-Host "loyalty-mcp-server is up!" Write-Host "loyalty-mcp-server is up!"
# 2. Start Agent Service # 2. Start Agent Service
Write-Host "Starting loyalty-agent..." Write-Host "Starting loyalty-agent..."
$agentLog = Join-Path $LogDir "loyalty-agent.log" $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 $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 "loyalty-agent started with PID: $($agentProcess.Id)"
Write-Host "Waiting for loyalty-agent to initialize..." Write-Host "Waiting for loyalty-agent to initialize..."
while ($true) { while ($true) {
$netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9332" $netstat = netstat -ano | Select-String "LISTENING" | Select-String ":9332"
if ($netstat) { break } if ($netstat) { break }
Start-Sleep -Seconds 2 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
}
}
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-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 "=========================================="