545 lines
26 KiB
Markdown
545 lines
26 KiB
Markdown
# 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
|