245 lines
12 KiB
Markdown
245 lines
12 KiB
Markdown
# 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
|