Series 1 - Bài 4: Agent Core, nơi orchestration xảy ra
Agent Core là trung tâm điều phối của CLI Agent — nơi agentic loop sống, context được build, và tool dispatch xảy ra. Bài này phân tích từng component và sequence diagram của một complete turn.
Nghe bài viết này dưới dạng podcastNếu CLI Layer là giao diện và Tool Layer là tay chân, thì Agent Core là não. Nhưng đây là não theo nghĩa traffic controller — không phải nơi business logic sống, mà là nơi điều phối để đúng thứ xảy ra vào đúng thời điểm.
Hiểu sai Agent Core là nguồn gốc của nhiều architecture mistake phổ biến nhất trong agent system.
Một complete turn nhìn từ Agent Core
User message: "Refactor hàm processLoan trong loan.service.ts"
Turn bắt đầu:
┌─────────────────────────────────────────────────────────┐
│ 1. ContextBuilder.build() │
│ ├─ Load system prompt │
│ ├─ Load file tree từ Context Layer │
│ ├─ Attach conversation history │
│ └─ Append user message │
│ → messages[] ready │
│ │
│ 2. LLMClient.call(messages[]) │
│ ├─ POST /v1/messages với tool definitions │
│ ├─ Parse SSE stream │
│ └─ Collect response blocks │
│ → response: tool_use | text │
│ │
│ 3. Response routing: │
│ │
│ If tool_use: │
│ ├─ ToolDispatcher.execute(tool_use) │
│ ├─ Append tool_use to messages[] │
│ ├─ Append tool_result to messages[] │
│ └─ GOTO step 2 (loop) │
│ │
│ If text: │
│ └─ Return text to CLI Layer (turn ends) │
└─────────────────────────────────────────────────────────┘
ContextBuilder: Assembling the window
ContextBuilder có một job: tạo ra messages[] array đúng format để gửi đến LLM.
messages[] structure sau khi ContextBuilder chạy:
[
{
role: "user",
content: [
{ type: "text", text: "[SYSTEM]\n" + systemPrompt },
{ type: "text", text: "[PROJECT]\n" + fileTree },
]
},
// ... conversation history ...
{
role: "user",
content: "Refactor hàm processLoan..."
}
]
Thứ tự trong messages[] quan trọng. LLM attend mạnh hơn vào phần đầu và phần cuối. System prompt và current user message nên ở những vị trí này.
ContextBuilder cũng là nơi enforce token budget. Trước khi build, nó estimate tổng token count. Nếu quá limit, nó trim conversation history cũ — nhưng không bao giờ trim system prompt hay current message.
Token budget strategy:
Priority 1 (không bao giờ trim): System prompt
Priority 2 (không bao giờ trim): Current user message
Priority 3 (trim nếu cần): Recent tool results
Priority 4 (trim đầu tiên): Old conversation history
LLMClient: Thin HTTP wrapper
LLMClient cố tình thin. Không có business logic, không có retry với backoff phức tạp, không có caching. Một job: gọi API và parse response.
LLMClient.call(messages[]):
1. Serialize request:
{
model: "claude-sonnet-4-5",
max_tokens: 8096,
stream: true,
tools: [...tool definitions...],
messages: messages[]
}
2. POST đến /v1/messages
3. Parse SSE stream:
event: content_block_start → bắt đầu block mới
event: content_block_delta → thêm chunk vào block
event: content_block_stop → block hoàn thành
event: message_stop → response hoàn thành
4. Collect và return:
[
{ type: "text", text: "..." },
{ type: "tool_use", name: "read_file", input: {...} }
]
Streaming là bắt buộc cho UX. Nếu không stream, user phải đợi toàn bộ response trước khi thấy gì. Với response dài 2000 words, đó có thể là 5-10 giây chờ trong im lặng.
Với streaming, user thấy từng word xuất hiện — trải nghiệm responsive hơn nhiều dù total time như nhau.
ToolDispatcher: Security boundary
ToolDispatcher là component dễ bị underestimate nhất. Nhìn qua thì chỉ là routing — LLM muốn gọi tool nào thì route đến đó. Nhưng dispatcher là security boundary duy nhất giữa LLM decision và actual execution.
ToolDispatcher.execute(tool_use):
1. Validate tool exists trong registry
Nếu không → ToolNotFoundError (LLM có thể hallucinate tên tool)
2. Validate input schema
Nếu không match → ValidationError với expected schema
(Trả về LLM để nó biết sửa input)
3. Check permissions
Tool có trong allowed list không?
Input path có nằm trong allowed directories không?
(Security gate)
4. Nếu tool là destructive → Request confirmation từ CLI Layer
User confirm? → proceed
User deny? → return cancellation result
5. Execute tool
6. Wrap result:
{
type: "tool_result",
tool_use_id: tool_use.id,
content: result
}
Tại sao validation ở đây? Vì LLM không phải luôn trả về valid input. Đặc biệt khi tool definitions phức tạp, LLM có thể misunderstand parameter names hoặc formats. Validation ở Dispatcher cho phép return clear error về LLM để nó retry với correct input.
Tại sao permission check ở đây? Đây là single point of truth. Nếu permission check nằm trong từng tool implementation, một tool mới bị quên implement permission check sẽ tạo ra security hole. Centralize ở Dispatcher đảm bảo không có bypass.
Guardrails: Loop không chạy mãi
Agent Core manage guardrails — không phải CLI Layer, không phải tools:
Loop state:
┌─────────────────────────────────────────────────────┐
│ iteration_count: 0 (max: 20) │
│ total_tokens: 0 (max: 100,000) │
│ start_time: now (timeout: 5 minutes) │
│ consecutive_errors: 0 (max: 3) │
└─────────────────────────────────────────────────────┘
Trước mỗi vòng loop:
if iteration_count >= max → stop, báo cáo
if total_tokens >= max → stop, báo cáo
if elapsed > timeout → stop, báo cáo
if consecutive_errors >= max → stop, báo cáo
Guardrails không phải pessimism — chúng là engineering cho failure case. Mọi production system cần chúng.
Trade-off: Fat Core vs Thin Core
Fat Agent Core:
Business logic trong Core
Tool-specific handling trong Core
Formatting trong Core
→ Easier để start
→ Harder để test (phụ thuộc vào LLM)
→ Harder để change (thay đổi một thứ ảnh hưởng nhiều thứ)
Thin Agent Core:
Chỉ orchestration
Business logic trong Tools
Formatting trong CLI Layer
→ Harder để start (cần discipline)
→ Easier để test từng phần
→ Easier để change và extend
Luôn chọn Thin Core. Khi gặp temptation để add business logic vào Core, đó là signal để tạo tool mới hoặc move logic xuống Tool Layer.
Kết luận
- Agent Core có 4 component: ContextBuilder, LLMClient, ToolDispatcher, Guardrails.
- ContextBuilder assembly messages[] theo priority order — system prompt và current message không bao giờ bị trim.
- LLMClient là thin HTTP wrapper với SSE streaming — không có business logic.
- ToolDispatcher là security boundary: validate, authorize, execute, wrap result.
- Guardrails prevent infinite loops và runaway cost — không phải optional.
- Thin Core là đúng — business logic sống trong Tools, không trong Core.
Chưa có bình luận
Để lại bình luận
Bình luận sẽ được phê duyệt trước khi hiển thị.