Series 1 - Bài 2: Architecture 4 layer của CLI Agent

Một CLI Agent được tổ chức thành 4 layer với boundary rõ ràng: CLI, Agent Core, Tool, và Context. Bài này phân tích từng layer, trách nhiệm của nó, và lý do boundary tồn tại.

6 phút đọc
Tập này đang được chuẩn bị, quay lại sau nhé.

Trước khi viết một dòng code, cần phải rõ ràng về architecture. Architecture không phải diagram để đẹp — nó là tập hợp các quyết định về ai chịu trách nhiệm cái gìai được phép nói chuyện với ai.

Một CLI Agent có 4 layer với trách nhiệm rõ ràng và boundary không nên bị phá vỡ.

4 Layer Overview

┌─────────────────────────────────────────────────────────┐
│                    CLI LAYER                            │
│   Nhận input từ user, render output ra terminal         │
│   Biết về: terminal, stdin/stdout, ANSI codes           │
│   Không biết về: LLM, tools, context                   │
├─────────────────────────────────────────────────────────┤
│                   AGENT CORE                            │
│   Orchestrate toàn bộ flow: context → LLM → dispatch   │
│   Biết về: messages[], tool definitions, agentic loop  │
│   Không biết về: terminal specifics, file system       │
├─────────────────────────────────────────────────────────┤
│                   TOOL LAYER                            │
│   Thực thi actions: đọc file, chạy lệnh, tìm kiếm     │
│   Biết về: file system, shell, search APIs             │
│   Không biết về: LLM, context, conversation history   │
├─────────────────────────────────────────────────────────┤
│                  CONTEXT LAYER                          │
│   Cung cấp thông tin về môi trường hiện tại            │
│   Biết về: project structure, config files, token count│
│   Không biết về: terminal, LLM calls, tool execution  │
└─────────────────────────────────────────────────────────┘

Layer 1: CLI Layer

CLI Layer là giao diện duy nhất giữa agent và người dùng. Trách nhiệm của nó rất cụ thể và giới hạn.

                    CLI Layer

stdin ──▶ InputHandler ──▶ CommandParser
                                │
                      ┌─────────┴─────────┐
                      │                   │
               /command           regular message
                      │                   │
              handle locally      send to AgentCore

AgentCore ──▶ OutputRenderer ──▶ stdout
                │
         ┌──────┴──────┐
         │             │
    stream text    tool status
    (word by word)  indicator

InputHandler: Wrap readline, xử lý multiline input (khi user paste đoạn code dài), lưu command history để dùng phím mũi tên.

CommandParser: Phân biệt /commands (meta-commands như /clear, /context, /help) với regular messages sẽ được gửi đến agent. CLI Layer xử lý commands hoàn toàn, không pass xuống Agent Core.

OutputRenderer: Nhận chunks từ Agent Core và render ra terminal. Xử lý ANSI color codes, streaming từng word, progress indicator khi agent đang xử lý.

Boundary quan trọng: CLI Layer không biết gì về LLM hay tools. Nó chỉ biết cách hiển thị gì đó ra terminal và nhận input từ người dùng.

Layer 2: Agent Core

Agent Core là trung tâm điều phối — nơi agentic loop sống.

                  Agent Core

  User message
       │
       ▼
 ┌─────────────┐
 │ContextBuilder│ ← System prompt + file tree + history
 └──────┬──────┘
        │ messages[]
        ▼
 ┌─────────────┐
 │  LLMClient  │ ← Gọi API, parse SSE stream
 └──────┬──────┘
        │ response
        ▼
 ┌─────────────────────────┐
 │   Response type?        │
 └────┬──────────────┬─────┘
      │              │
  tool_use       text done
      │              │
      ▼              ▼
 ┌─────────┐   ┌──────────┐
 │Dispatcher│   │Return to │
 │execute   │   │CLI Layer │
 └────┬─────┘   └──────────┘
      │
  tool_result
      │
      ▼
  Add to messages[]
  Loop lại từ đầu

ContextBuilder: Mỗi vòng loop, ContextBuilder assembly messages[] từ:

  • System prompt (role, constraints, behavior)
  • Long-term context (từ Context Layer)
  • Conversation history (các turn trước)
  • Tool results (từ vòng loop trước)
  • Current user message

LLMClient: Thuần túy là HTTP client với SSE parsing. Không có business logic. Nhận messages[], trả về response chunks.

ToolDispatcher: Nhận tool_use block từ LLM response, route đến đúng tool trong Tool Layer, collect result, format thành tool_result để thêm vào messages[].

Boundary quan trọng: Agent Core không thực thi bất kỳ action cụ thể nào. Nó dispatch đến Tool Layer. Không có fs.readFile() trong Agent Core.

Layer 3: Tool Layer

Tool Layer là nơi side effects xảy ra. Đây là layer duy nhất tương tác với thế giới bên ngoài.

          Tool Layer

┌─────────────────────────────────────┐
│           Tool Registry             │
│  { name → Tool implementation }     │
└─────────────────────────────────────┘
          │
 ┌────────┼────────┬──────────┐
 │        │        │          │
 ▼        ▼        ▼          ▼

 FileTools  ShellTool  SearchTool  (custom tools)

 read_file  run_cmd    grep_files
 write_file run_test   find_symbol
 list_dir   git_status

Mỗi tool implement một interface chung:

Tool Interface:
  name: string              ← LLM dùng tên này để call
  description: string       ← LLM đọc để biết khi nào dùng
  input_schema: JSONSchema  ← LLM biết cần pass gì
  execute(input) → result   ← Application code chạy cái này

Boundary quan trọng: Tool Layer không biết về conversation history, context, hay LLM. Nó nhận structured input, thực thi action, trả structured output. Stateless hoàn toàn.

Layer 4: Context Layer

Context Layer cung cấp thông tin môi trường mà Agent Core cần để build context cho LLM.

          Context Layer

 ┌──────────────────────────────────────────┐
 │
 │  AGENT.md Loader
 │  └─ Đọc file cấu hình agent ở root project
 │     Inject vào system prompt 1 lần khi start
 │
 │  FileTree Builder
 │  └─ Scan cấu trúc thư mục hiện tại
 │     Rebuild mỗi vòng loop (project có thể thay đổi)
 │     Format gọn để không chiếm quá nhiều token
 │
 │  Token Counter
 │  └─ Estimate token count của messages[]
 │     Signal cho ContextBuilder khi gần limit
 │     Quyết định phần nào cần trim
 │
 └──────────────────────────────────────────┘

Boundary quan trọng: Context Layer không gọi LLM và không execute tools. Nó chỉ đọc và cung cấp thông tin.

Tại sao boundaries này quan trọng

Boundaries không phải bureaucracy — chúng tạo ra properties quan trọng:

Testability: Bạn có thể test Tool Layer hoàn toàn độc lập với LLM. Không cần mock API, không cần real LLM response. Tool nhận input, trả output — đơn giản.

Replaceability: Muốn đổi từ Anthropic sang OpenAI? Chỉ thay LLMClient trong Agent Core. CLI Layer và Tool Layer không đổi.

Security isolation: Tool Layer là nơi duy nhất có side effects. Muốn audit hoặc restrict gì, bạn chỉ cần focus vào đây. Không có side effects leak ra ngoài Tool Layer.

Debuggability: Khi behavior bất ngờ, bạn biết chính xác cần inspect layer nào dựa trên symptom.

Symptom → Layer cần inspect

User input bị parse sai    → CLI Layer
Context thiếu thông tin    → Context Layer
LLM gọi sai tool          → Agent Core (prompt) hoặc Tool Layer (schema)
Tool trả kết quả sai      → Tool Layer
Loop không dừng           → Agent Core (guardrails)

Violation patterns cần tránh

Anti-pattern 1: CLI Layer biết về LLM

// Sai: CLI Layer gọi LLM trực tiếp
readline.on('line', async (input) => {
  const response = await callLLM(input); // ← violation
  console.log(response);
});

Anti-pattern 2: Tool Layer đọc conversation history

// Sai: Tool cần biết context của conversation
class ReadFileTool {
  execute(input, conversationHistory) { // ← violation
    // Tool không nên cần history
  }
}

Anti-pattern 3: Agent Core thực thi file operations

// Sai: Business logic lẫn với orchestration
class AgentCore {
  async handleToolUse(toolUse) {
    if (toolUse.name === 'read_file') {
      return fs.readFile(toolUse.input.path); // ← violation
    }
  }
}

Kết luận

  • CLI Agent có 4 layer: CLI, Agent Core, Tool, Context — mỗi layer có một trách nhiệm rõ ràng.
  • CLI Layer: user interface. Agent Core: orchestration. Tool Layer: execution. Context Layer: environment info.
  • Boundaries tạo ra testability, replaceability, security isolation, và debuggability.
  • Khi một layer biết quá nhiều về layer khác, đó là signal của architecture violation.
  • Layer boundaries không phải over-engineering — chúng là investment vào maintainability dài hạn.