# Sub-Agent Architecture — Deep Dive (5 câu hỏi)

Dựa trên feedback và khảo sát codebase MCP server + loyalty-agent.

---

## 1. MCP Tool Design cho Loyalty Multi-Module

### Hiện trạng

```
loyalty-mcp-server/
  └── tool/
       ├── CampaignTools.java      (7 tools: search, count, getById, getByIds, generateId, checkId, create)
       └── CampaignRuleTools.java  (7 tools: search, count, getById, getByIds, generateId, checkId, create)
```

Tổng: **14 tools**, tất cả expose qua 1 MCP SSE endpoint duy nhất (`/sse`). Agent nhận hết 14 tool definitions → context window bị ăn ~1000+ tokens chỉ cho tool schema.

### Loyalty có nhiều module hơn

Nhìn vào `client/api/` đã có sẵn `MemberTierApi.java`, `PointPoolApi.java`. Tương lai sẽ có:

```
┌─────────────────────────────────────────────────────────┐
│                 LOYALTY CORE MODULES                     │
├──────────────┬──────────────┬──────────────┬────────────┤
│   Reward     │   Member     │ Transaction  │  Partner   │
│              │              │              │            │
│ • Campaign   │ • Profile    │ • TxnCode    │ • Merchant │
│ • Rule       │ • Tier       │ • TxnHistory │ • Store    │
│ • Formula    │ • Card       │ • Counter    │ • Offer    │
│ • Schedule   │ • Segment    │ • Statement  │            │
│ • Counter    │ • Point Pool │ • Batch      │            │
│ • Alert      │ • Enrollment │              │            │
└──────────────┴──────────────┴──────────────┴────────────┘
```

Nếu mỗi module có 5-7 tools → **tổng 30-50 tools**. 1 agent không thể xử lý tốt.

### Đề xuất: Tool Groups + Agent Scoping

**Phương án A — Grouped Tools trong 1 MCP Server (Đơn giản nhất)**

Giữ 1 MCP server, nhưng tools tổ chức theo package/group. Agent-side filter theo tên prefix:

```
loyalty-mcp-server/
  └── tool/
       ├── reward/
       │    ├── CampaignTools.java       → tool names: reward_searchCampaigns, reward_getCampaignById...
       │    └── CampaignRuleTools.java   → tool names: reward_searchRules, reward_getRuleById...
       ├── member/
       │    ├── MemberTools.java         → tool names: member_searchMembers, member_getMemberById...
       │    └── TierTools.java           → tool names: member_searchTiers...
       └── transaction/
            └── TransactionTools.java    → tool names: txn_searchTransactions...
```

Agent-side filtering:

```java
// Trong QueryAgent — chỉ load read tools của reward module
List<ToolCallback> tools = toolInterceptor.getWrappedTools(sessionId, messageId)
    .stream()
    .filter(t -> {
        String name = t.getToolDefinition().name();
        return name.startsWith("reward_") && !name.contains("create") && !name.contains("generate");
    })
    .toList();
```

> [!TIP]
> **Ưu điểm**: Không cần đổi MCP server architecture. Chỉ rename tools + filter ở agent.
> **Nhược điểm**: Tất cả tool definitions vẫn load vào memory agent, chỉ filter khi truyền cho ChatClient.

---

**Phương án B — Multi-MCP Endpoint (Mạnh hơn, phức tạp hơn)**

Mỗi domain module là 1 MCP endpoint riêng. Agent chỉ connect tới endpoint cần thiết:

```yaml
# application.yml — loyalty-agent
spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            reward:
              url: http://localhost:9331/sse      # Reward tools only
            member:
              url: http://localhost:9331/member/sse  # Member tools
            transaction:
              url: http://localhost:9331/txn/sse     # Transaction tools
```

Nhưng Spring AI MCP client hiện tại auto-discover **tất cả** connections. Để chọn lọc, cần custom `ToolCallbackProvider` filtering.

> [!NOTE]
> **Khuyến nghị cho bước đầu**: Dùng **Phương án A** (1 MCP server, tool name prefix, agent-side filter). Đơn giản, hiệu quả, không cần thay đổi infrastructure.

---

**Phương án C — MCP Server per Module (Micro-MCP)**

Tách thành nhiều MCP server process:

```
┌───────────────┐  ┌───────────────┐  ┌───────────────┐
│  reward-mcp   │  │  member-mcp   │  │   txn-mcp     │
│  :9331/sse    │  │  :9332/sse    │  │  :9333/sse    │
│  Campaign*    │  │  Member*      │  │  Transaction* │
│  Rule*        │  │  Tier*        │  │  Counter*     │
└───────────────┘  └───────────────┘  └───────────────┘
```

> [!WARNING]
> Overkill cho giai đoạn hiện tại. Chỉ hợp lý khi mỗi module có team riêng hoặc deploy riêng.

### Tóm tắt: Phương án nào?

| Phương án | Độ phức tạp | Khi nào dùng |
|---|---|---|
| **A. Prefix + Filter** | ⭐ Thấp | **Nên bắt đầu từ đây** — 5-20 tools |
| **B. Multi-MCP Endpoint** | ⭐⭐ Trung bình | 20-50 tools, cần isolation |
| **C. Micro-MCP Server** | ⭐⭐⭐ Cao | 50+ tools, multi-team |

---

## 2. Shared Memory & Context Injection cho Creator Agent

### Vấn đề

User flow thực tế khi tạo campaign + rule:

```
User: "danh sách chiến dịch"               → QueryAgent, searchCampaigns
User: "xem thể lệ Happy Birthday"          → QueryAgent, searchRules
User: "tạo thêm 1 rule mới tương tự"       → CreatorAgent — CẦN BIẾT context trước đó!
      Cần biết: campaignId, rule template,
      formula type, existing rules...
```

CreatorAgent cần:
1. **Chat history** — biết user đang nói về campaign nào
2. **Domain context** — query các thông tin liên quan (pools, tiers, counters...)
3. **Validation data** — kiểm tra ID hợp lệ, trùng lặp...

### Kiến trúc Memory

```
┌──────────────────────────────────────────────────────────────┐
│                    SHARED MEMORY LAYER                        │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │           InMemoryChatMemory (existing)                 │ │
│  │           Key: conversationId (sessionId)               │ │
│  │           Shared across ALL agents                      │ │
│  └─────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │           SessionContext (NEW)                          │ │
│  │           Key: sessionId                                │ │
│  │           Stores: activeCampaignId, lastQueryResult,    │ │
│  │                   workflowState, userPreferences         │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
         │                    │                    │
    ┌────┴────┐         ┌────┴────┐         ┌────┴────┐
    │  Chat   │         │  Query  │         │ Creator │
    │  Agent  │         │  Agent  │         │  Agent  │
    │         │         │  writes │         │  reads  │
    │         │         │  context│         │ context │
    └─────────┘         └─────────┘         └─────────┘
```

**Chat history (InMemoryChatMemory)** đã shared — tất cả agent dùng chung `conversationId` làm key. Khi user chuyển từ QueryAgent sang CreatorAgent, history vẫn giữ nguyên.

**SessionContext** mới — lưu structured data:

```java
public class SessionContext {
    private String activeCampaignId;        // Campaign đang được nói tới
    private String activeCampaignName;      // Tên để inject vào prompt
    private String lastToolResult;          // Kết quả tool gần nhất (compressed)
    private Map<String, Object> metadata;   // Flexible key-value
}
```

### Context Enrichment cho Creator Agent

Khi Creator Agent bắt đầu tạo rule, nó cần query nhiều thông tin. Có 2 cách:

**Cách 1: LLM tự gọi tools (hiện tại)**

Để LLM quyết định gọi tools nào để lấy thông tin. Vấn đề: model nhỏ hay quên hoặc gọi sai.

**Cách 2: Code-driven Context Injection (khuyên dùng)**

```java
public class CreatorAgent implements Agent {

    @Override
    public void execute(ChatRequest request, String sessionId) {
        SessionContext ctx = sessionContextStore.get(sessionId);

        // Auto-enrich: trước khi gọi LLM, query sẵn thông tin cần thiết
        StringBuilder enrichedContext = new StringBuilder();

        if (ctx.getActiveCampaignId() != null) {
            // Auto-fetch campaign details
            Campaign campaign = campaignService.findById(ctx.getActiveCampaignId());
            enrichedContext.append("Campaign hiện tại: ")
                .append(campaign.getName())
                .append(" (ID: ").append(campaign.getId()).append(")\n");

            // Auto-fetch existing rules
            List<CampaignRule> existingRules = ruleService.findByCampaignId(campaign.getId());
            enrichedContext.append("Rules hiện có: ").append(existingRules.size()).append("\n");

            // Auto-fetch available pools
            List<PoolDefinition> pools = poolService.getAll();
            enrichedContext.append("Point pools: ")
                .append(pools.stream().map(PoolDefinition::getName).toList()).append("\n");
        }

        chatClient.prompt()
            .system(CREATOR_SYSTEM_PROMPT)
            .user(enrichedContext + "\n\n" + request.prompt())
            ...
    }
}
```

> [!IMPORTANT]
> **Code-driven enrichment** tốt hơn "LLM tự gọi tool" vì:
> 1. Deterministic — không phụ thuộc vào khả năng reasoning của model nhỏ
> 2. Nhanh hơn — gọi API trực tiếp, không qua LLM → tool → LLM loop
> 3. Đầy đủ hơn — developer biết rõ cần context gì

---

## 3. Multi-Ollama Endpoints (Thinking vs Non-Thinking)

### Setup

```
┌──────────────────┐    ┌──────────────────┐
│   Máy A (hiện tại)│    │   Máy B (mới)    │
│   192.168.99.10   │    │   192.168.99.XX   │
│                    │    │                    │
│   qwen3.5:4b      │    │   qwen2.5:3b      │
│   (thinking ON)    │    │   (non-thinking)   │
│   GPU: ...         │    │   GPU: ...         │
│                    │    │                    │
│   Dùng cho:        │    │   Dùng cho:        │
│   - Query Agent    │    │   - Chat Agent     │
│   - Creator Agent  │    │   - Formatter      │
└──────────────────┘    └──────────────────┘
```

### Spring AI Config — Model Registry

```yaml
# application.yml
spring:
  ai:
    ollama:
      # Default (thinking model)
      base-url: http://192.168.99.10:11434
      chat:
        options:
          model: qwen3.5:4b
          temperature: 0.3
          num_ctx: 32768

# Custom — non-thinking endpoint
agent:
  models:
    chat:
      base-url: http://192.168.99.XX:11434
      model: qwen2.5:3b
      temperature: 0.7
    query:
      base-url: http://192.168.99.10:11434
      model: qwen3.5:4b
      temperature: 0.3
    creator:
      base-url: http://192.168.99.10:11434
      model: qwen3.5:4b
      temperature: 0.2
```

### Implementation

```java
@Configuration
public class MultiModelConfig {

    @Bean
    public OllamaApi thinkingOllamaApi() {
        return OllamaApi.builder()
            .baseUrl("http://192.168.99.10:11434")
            .build();
    }

    @Bean
    public OllamaApi fastOllamaApi() {
        return OllamaApi.builder()
            .baseUrl("http://192.168.99.XX:11434")
            .build();
    }

    @Bean("chatAgent.chatClient")
    public ChatClient chatAgentClient(
            @Qualifier("fastOllamaApi") OllamaApi api,
            ToolCallingManager toolCallingManager) {
        var model = OllamaChatModel.builder()
            .ollamaApi(api)
            .defaultOptions(OllamaChatOptions.builder()
                .model("qwen2.5:3b")
                .temperature(0.7)
                .build())
            .toolCallingManager(toolCallingManager)
            .build();
        return ChatClient.builder(model).build();
    }

    @Bean("queryAgent.chatClient")
    public ChatClient queryAgentClient(
            @Qualifier("thinkingOllamaApi") OllamaApi api,
            ToolCallingManager toolCallingManager) {
        var model = OllamaChatModel.builder()
            .ollamaApi(api)
            .defaultOptions(OllamaChatOptions.builder()
                .model("qwen3.5:4b")
                .temperature(0.3)
                .build())
            .toolCallingManager(toolCallingManager)
            .build();
        return ChatClient.builder(model).build();
    }
}
```

> [!TIP]
> **Thay model dễ dàng**: Khi có model tốt hơn (ví dụ `qwen3:8b-no-think`), chỉ cần đổi trong config YAML, không sửa code.

---

## 4. Latency Optimization — Tránh Chain Agents

### Vấn đề

Chain 2 agents nối tiếp → latency gấp đôi:

```
BAD: Query Agent (10s) → Formatter Agent (8s) = 18s tổng
```

### Giải pháp: KHÔNG chain — mỗi agent tự hoàn chỉnh

```
GOOD: Query Agent (10s) — tự format trong cùng 1 prompt = 10s tổng
```

**Cách thực hiện**: Thay vì Formatter Agent riêng, nhúng formatting instruction vào `agent_instruction` (đã có sẵn!):

```java
// Trong CampaignService.java (MCP server) — ĐÃ CÓ SẴN
return Result.of(campaigns, """
    Trả lời tự nhiên bằng tiếng Việt. Không dùng markdown table.
    Hiển thị mỗi chiến dịch theo dạng danh sách đánh số:
      **[Tên]** (`[campaignId]`) — Owner: [ownerName]...
    """);
```

`agent_instruction` pattern hiện tại **đã là formatter** — nằm trong tool result, không tốn thêm LLM call. Rất thông minh.

### Nơi nào thực sự cần parallel/async

```
┌─────────────────────────────────────────────────────────────┐
│  Creator Agent — Tạo Rule Flow                              │
│                                                              │
│  1. User: "tạo rule mới"                                    │
│                                                              │
│  2. Context Enrichment (PARALLEL, code-driven, ~500ms):     │
│     ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│     │ getCampaign() │  │ getRules()   │  │ getPools()   │   │
│     └──────┬───────┘  └──────┬───────┘  └──────┬───────┘   │
│            └─────────────────┼─────────────────┘            │
│                              ▼                               │
│  3. LLM Call (SINGLE, ~8-12s):                              │
│     System: Creator prompt + enriched context               │
│     User: "tạo rule mới"                                    │
│     → LLM hỏi thêm hoặc confirm                            │
│                                                              │
│  Total: ~9-13s (vs 17s+ hiện tại vì LLM phải tự gọi tool)  │
└─────────────────────────────────────────────────────────────┘
```

**Parallel context fetching** (Java `CompletableFuture`):

```java
// Context enrichment — chạy song song, mất ~500ms thay vì 3 LLM calls
CompletableFuture<Campaign> campaignFuture = CompletableFuture.supplyAsync(
    () -> campaignService.findById(campaignId));
CompletableFuture<List<CampaignRule>> rulesFuture = CompletableFuture.supplyAsync(
    () -> ruleService.findByCampaignId(campaignId));
CompletableFuture<List<PoolDefinition>> poolsFuture = CompletableFuture.supplyAsync(
    () -> poolService.getAll());

// Wait all (parallel) — ~500ms
CompletableFuture.allOf(campaignFuture, rulesFuture, poolsFuture).join();

// Inject into prompt
String context = buildContext(campaignFuture.get(), rulesFuture.get(), poolsFuture.get());
```

### Tóm tắt chiến lược latency

| Kỹ thuật | Tiết kiệm | Áp dụng |
|---|---|---|
| Tắt thinking cho Chat Agent | 17s → 2-3s | Chat Agent |
| Formatter = `agent_instruction` | Bỏ 1 LLM call (~8s) | Query Agent |
| Parallel context fetch | 3 API calls: 1.5s → 0.5s | Creator Agent |
| Code-driven enrichment | Bỏ 2-3 tool-call loops (~20s) | Creator Agent |

---

## 5. On-Premise Fallback Strategy (Không dùng Cloud)

### Constraint: Bảo mật dữ liệu → chỉ chạy on-premise

Bỏ hoàn toàn phương án cloud. Thay vào đó:

### Strategy: Multi-Tier On-Premise Models

```
┌─────────────────────────────────────────────────────────────┐
│                    ON-PREMISE MODEL TIERS                     │
│                                                              │
│   Tier 1 (Fast, Light)          Tier 2 (Strong, Slow)       │
│   ┌──────────────────┐         ┌──────────────────┐         │
│   │ Máy B             │         │ Máy A (GPU mạnh) │         │
│   │ qwen2.5:3b        │         │ qwen3.5:4b       │         │
│   │ non-thinking      │         │ thinking          │         │
│   │ ~2-3s/response    │         │ ~8-15s/response   │         │
│   │                    │         │                    │         │
│   │ Dùng cho:          │         │ Dùng cho:          │         │
│   │ - Chat Agent       │         │ - Query Agent      │         │
│   │ - Simple Q&A       │         │ - Creator Agent    │         │
│   │ - Validation       │         │ - Complex reasoning│         │
│   └──────────────────┘         └──────────────────┘         │
│                                                              │
│   Tương lai: Tier 3 (nếu có GPU mạnh hơn)                  │
│   ┌──────────────────┐                                      │
│   │ Máy C (A100/H100) │                                      │
│   │ qwen3.5:14b       │                                      │
│   │ cho critical tasks │                                      │
│   └──────────────────┘                                      │
└─────────────────────────────────────────────────────────────┘
```

### Khi nào escalate từ Tier 1 → Tier 2?

Không cần "confidence-based routing" phức tạp. Dùng **Intent-based routing** đơn giản:

```java
// Router quyết định dựa trên intent, không dựa trên confidence
switch (intent) {
    case CONVERSATION → chatAgent;      // Tier 1 (fast model)
    case CAMPAIGN_QUERY,
         RULE_QUERY,
         UNKNOWN → queryAgent;          // Tier 2 (thinking model)
    case CAMPAIGN_CREATE,
         RULE_CREATE → creatorAgent;    // Tier 2 (thinking model)
}
```

### Retry Strategy (nếu model trả kết quả kém)

Thay vì fallback lên cloud, **retry cùng model với prompt khác**:

```java
// Nếu tool result rỗng hoặc LLM trả "tôi không biết"
if (isLowQualityResponse(response)) {
    // Retry 1: thêm few-shot example vào prompt
    // Retry 2: simplify câu hỏi
    // Retry 3: báo user "Không tìm thấy, vui lòng thử cách khác"
}
```

> [!IMPORTANT]
> **Không retry bằng model khác** — retry bằng **prompt khác** trên cùng model hiệu quả hơn cho small LLMs. Thêm 1 ví dụ cụ thể vào prompt thường giải quyết 80% trường hợp model trả sai.

---

## Tổng kết — Bước tiếp theo

```
┌──────────────────────────────────────────────────────────────┐
│                   KIẾN TRÚC ĐỀ XUẤT CUỐI                    │
│                                                              │
│   Máy B (fast)                 Máy A (strong)                │
│   ┌────────────┐               ┌────────────┐               │
│   │ qwen2.5:3b │               │ qwen3.5:4b │               │
│   └─────┬──────┘               └─────┬──────┘               │
│         │                             │                      │
│         ▼                             ▼                      │
│   ┌──────────┐              ┌──────────────────┐            │
│   │ChatAgent │              │  QueryAgent      │            │
│   │(no tools)│              │  (read tools,    │            │
│   │          │              │   prefix filter) │            │
│   └──────────┘              └──────────────────┘            │
│                              ┌──────────────────┐            │
│                              │  CreatorAgent    │            │
│                              │  (write tools,   │            │
│                              │   code-driven    │            │
│                              │   enrichment)    │            │
│                              └──────────────────┘            │
│                                                              │
│   ┌─────────────────────────────────────────────┐            │
│   │        Shared Memory Layer                  │            │
│   │  InMemoryChatMemory + SessionContext        │            │
│   └─────────────────────────────────────────────┘            │
│                                                              │
│   ┌─────────────────────────────────────────────┐            │
│   │        MCP Server (1 process)               │            │
│   │  Tools: reward_*, member_*, txn_*           │            │
│   │  Agent-side filtering by prefix             │            │
│   └─────────────────────────────────────────────┘            │
└──────────────────────────────────────────────────────────────┘
```

Nếu anh thấy hướng này ổn, mình có thể tạo change proposal (`/opsx-propose`) để bắt đầu implement từng phase:

1. **Phase 1**: Agent interface + ChatAgent (tắt thinking, model riêng) — cải thiện "hello" ngay
2. **Phase 2**: Tool prefix naming + QueryAgent (filtered tools) — giảm tool confusion
3. **Phase 3**: CreatorAgent + SessionContext + code-driven enrichment — nâng chất lượng tạo campaign/rule
4. **Phase 4**: Multi-Ollama config + ModelRegistry — hoàn thiện infra
