Skip to content

HARRY-11 — GĐ21 — AI Agents + Tool Calling + MCP

GĐ21 — AI Agents + Tool Calling + MCP

Study note cho Frontend engineer (mạnh JS/TS) chuyển sang Backend + AI. Mỗi concept: định nghĩatại sao quan trọngcơ chếví dụ codepitfall thực tế. Ngôn ngữ: Việt, thuật ngữ giữ tiếng Anh. Code snippet Node/TS.

Bối cảnh: bạn quen viết UI gọi API qua fetch. Agent lật ngược mô hình đó — thay vì người quyết định gọi API nào, LLM tự quyết định gọi tool nào, khi nào, và với tham số gì. Backend của bạn trở thành "runtime" chạy vòng lặp đó một cách an toàn.


1. AI Agent là gì

Định nghĩa. Một AI Agent = LLM + tập tools + một vòng lặp (loop) trong đó model tự quyết định hành động tiếp theo cho tới khi đạt mục tiêu. Khác với "một prompt đơn" (one-shot: gửi prompt → nhận text → xong), agent chạy nhiều lượt (multi-turn), giữa các lượt nó gọi tool, đọc kết quả, rồi suy nghĩ tiếp.

Tại sao quan trọng. Prompt đơn chỉ biết những gì có trong training data + context bạn nhét vào. Agent thì hành động lên thế giới thực: query DB, gọi API, đọc file, chạy code. Đây là bước nhảy từ "chatbot trả lời" sang "nhân viên làm việc". Chính workspace Multica bạn đang dùng là ví dụ: agent được assign issue, comment, đổi status, chạy code — y như một teammate thật.

Cơ chế. Ba thành phần:

  • Model (bộ não): nhận messages + danh sách tool schema, sinh ra hoặc text cuối cùng hoặc "yêu cầu gọi tool" (tool call).
  • Tools (tay chân): các function backend expose cho model.
  • Loop (nhịp tim): backend đọc output của model; nếu là tool call thì thực thi và feed kết quả lại; nếu là text cuối thì dừng.

Điểm mấu chốt: model không tự chạy code. Nó chỉ đề nghị "hãy gọi search với {q: "..."}". Backend của bạn mới là bên thực sự chạy — đây là ranh giới an toàn quan trọng.

Ví dụ code (khác biệt one-shot vs agent).

ts
// One-shot: 1 lượt, không hành động
const { text } = await generateText({ model, prompt: "Thời tiết Hà Nội?" });
// → model đoán, hoặc nói "tôi không biết realtime"

// Agent: model có thể gọi tool getWeather rồi mới trả lời
const { text } = await generateText({
  model,
  tools: { getWeather },      // model được phép hành động
  maxSteps: 5,                // cho phép lặp
  prompt: "Thời tiết Hà Nội?",
});

Pitfall thực tế. Người mới hay nghĩ "agent = prompt xịn hơn". Sai. Agent là một hệ thống có vòng lặp và state, nên nó có thể loop vô hạn, tốn tiền, gọi nhầm tool. Bạn phải thiết kế điểm dừng, budget và guardrails ngay từ đầu (mục 10) — không phải add sau.


2. Tool / Function Calling

Định nghĩa. Cơ chế cho phép model yêu cầu backend gọi một function đã khai báo trước. Bạn khai báo mỗi tool bằng schema: name, description, và parameters (JSON Schema). Model đọc schema, quyết định có gọi không, và sinh ra arguments dạng JSON khớp schema.

Tại sao quan trọng. Đây là "API contract giữa model và code của bạn". Không có nó, model chỉ sinh text. Có nó, model gọi được thế giới thực một cách có cấu trúc, type-safe. Với dân FE: giống như bạn định nghĩa props của một component — description và schema là "docs" mà model đọc để biết cách dùng.

Cơ chế (vòng đời một tool call).

  1. Bạn gửi messages + danh sách tool schemas cho model.
  2. Model trả về một tool call request: { name: "getWeather", args: { city: "Hanoi" } } (chưa chạy gì cả — chỉ là "ý định").
  3. Backend nhận request, validate args, thực thi function thật.
  4. Backend feed kết quả trở lại model như một message có role tool.
  5. Model đọc kết quả → sinh text cuối, hoặc gọi tool tiếp.

Ví dụ code (Vercel AI SDK + zod).

ts
import { generateText, tool } from "ai";
import { z } from "zod";

const getWeather = tool({
  description: "Lấy thời tiết hiện tại của một thành phố",
  parameters: z.object({
    city: z.string().describe("Tên thành phố, vd 'Hanoi'"),
  }),
  execute: async ({ city }) => {           // đây là code CỦA BẠN chạy
    const r = await fetch(`https://api.weather/${city}`);
    return await r.json();                  // kết quả feed lại cho model
  },
});

const { text } = await generateText({
  model, tools: { getWeather }, maxSteps: 5,
  prompt: "Trời Hà Nội có mưa không?",
});

z.object(...) được SDK convert sang JSON Schema gửi cho model — bạn không viết JSON Schema tay.

Pitfall thực tế. (1) Đừng tin args mù quáng: model có thể sinh args sai kiểu hoặc độc hại (SQL injection nếu bạn nối chuỗi). Luôn validate (zod) + parametrize query. (2) Description mơ hồ → model gọi sai tool hoặc bịa args. (3) Tool trả về object khổng lồ → tốn token và làm model lạc. Trả về đúng thứ model cần.


3. Agent Loop / ReAct Pattern

Định nghĩa. ReAct = Reason + Act. Mẫu vòng lặp: model reason (suy nghĩ nên làm gì) → act (gọi tool) → observe (đọc kết quả tool) → lặp lại, cho tới khi model đưa ra final answer hoặc chạm giới hạn.

Tại sao quan trọng. Đây là "engine" thực sự của agent. Hiểu loop = hiểu vì sao agent làm được task phức tạp (chia nhỏ, thử, sửa) và cũng vì sao nó nguy hiểm (loop vô hạn, tốn tiền).

Cơ chế (một vòng).

messages = [system, user]
lặp (tối đa maxSteps lần):
  out = model(messages, tools)
  nếu out là text cuối  → return out        // điều kiện dừng 1: final answer
  nếu out là tool call:
    result = execute(out.tool, out.args)
    messages.push(tool_call, tool_result)   // observe: nhét kết quả vào history
// điều kiện dừng 2: hết maxSteps → dừng cưỡng bức

Hai điều kiện dừng bắt buộc: final answer (model tự nói xong) và max steps (chặn loop).

Ví dụ code (tự viết loop thô, để hiểu bản chất).

ts
let messages = [{ role: "user", content: prompt }];
for (let step = 0; step < 8; step++) {              // maxSteps = 8
  const res = await model.call(messages, toolSchemas);
  if (!res.toolCalls?.length) return res.text;      // dừng: final answer
  for (const call of res.toolCalls) {
    const result = await tools[call.name](call.args);
    messages.push({ role: "assistant", toolCalls: [call] });
    messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify(result) });
  }
}
throw new Error("Đạt max steps mà chưa xong");       // dừng cưỡng bức

Pitfall thực tế. (1) Quên maxSteps → agent lặp mãi khi tool luôn trả lỗi → cháy budget. (2) Loop rung (model gọi lại y hệt tool với y hệt args vì không "học" được từ kết quả) — thường do error message vô nghĩa (xem mục 4). (3) History phình to mỗi vòng → vượt context window; cần cắt/summary history khi loop dài.


4. Tool Design Tốt

Định nghĩa. Nghệ thuật thiết kế tool sao cho model dùng đúng, dùng an toàn: mô tả rõ, tham số chặt, trả lỗi có ý nghĩa, idempotent khi có thể, giới hạn quyền hạn.

Tại sao quan trọng. Model chỉ "thấy" tool qua schema + kết quả trả về. Tool tồi = agent tồi, bất kể model mạnh cỡ nào. 80% chất lượng agent nằm ở tool design, không phải prompt.

Cơ chế / nguyên tắc.

  • Description rõ: nói khi nào dùngkhi nào KHÔNG dùng. "Search knowledge base cho câu hỏi nội bộ; KHÔNG dùng cho toán."
  • Tham số chặt: enum thay vì free string, min/max, format rõ. Càng chặt model càng ít bịa.
  • Lỗi có ý nghĩa cho model: đừng throw stack trace. Trả { error: "city 'Xyz' không tồn tại, hãy thử tên tiếng Anh" } — để model tự sửa ở vòng sau.
  • Idempotency: tool ghi (create/charge) nên nhận idempotencyKey, vì agent có thể gọi lại do retry/loop → tránh tạo trùng, charge trùng.
  • Quyền hạn (least privilege): mỗi tool chỉ làm đúng 1 việc với scope tối thiểu. Đừng expose "runSQL(anything)".

Ví dụ code (lỗi có ý nghĩa + enum + idempotency).

ts
const createTicket = tool({
  description: "Tạo support ticket. Dùng khi user báo lỗi cần theo dõi.",
  parameters: z.object({
    priority: z.enum(["low", "high"]),               // enum, không free string
    title: z.string().max(120),
    idempotencyKey: z.string().describe("UUID để tránh tạo trùng"),
  }),
  execute: async (a) => {
    const exist = await db.ticket.findByKey(a.idempotencyKey);
    if (exist) return { id: exist.id, note: "đã tồn tại, không tạo lại" };
    try { return { id: (await db.ticket.create(a)).id }; }
    catch (e) { return { error: `Tạo thất bại: ${e.message}. Kiểm tra title < 120 ký tự.` }; }
  },
});

Pitfall thực tế. Trả throw ra ngoài loop → agent chết thay vì tự sửa. Ngược lại, nuốt lỗi im lặng (return {}) → model tưởng thành công, đi tiếp trên dữ liệu rác. Luôn trả lỗi dạng dữ liệu model đọc được, không phải exception.


5. Memory: Short-term vs Long-term

Định nghĩa.

  • Short-term memory = conversation history (mảng messages) truyền vào model mỗi lượt. Sống trong context window, mất khi hết session.
  • Long-term memory = kiến thức lưu ngoài (thường là vector store), truy hồi lại khi cần bằng semantic search (RAG). Bền qua nhiều session.

Tại sao quan trọng. Context window có hạn (tokens) và tốn tiền theo độ dài. Không thể nhét cả lịch sử 6 tháng + toàn bộ docs công ty vào mỗi request. Long-term memory cho phép agent "nhớ" chọn lọc: chỉ kéo về phần liên quan.

Cơ chế.

  • Short-term: bạn append message vào mảng, gửi lại mỗi vòng. Khi quá dài → summary (nhờ model tóm history cũ thành 1 đoạn ngắn) hoặc cắt cửa sổ (giữ N message gần nhất).
  • Long-term: khi có info cần nhớ → tạo embedding (vector) → lưu vào vector DB (pgvector, Pinecone…). Khi cần → embedding của câu hỏi → tìm k vector gần nhất → nhét text đó vào prompt.

Ví dụ code (retrieve long-term rồi đưa vào short-term).

ts
// Lưu:
await vdb.upsert({ id, vector: await embed(fact), text: fact });
// Truy hồi khi trả lời:
const hits = await vdb.query({ vector: await embed(userMsg), topK: 4 });
const context = hits.map(h => h.text).join("\n");
const messages = [
  { role: "system", content: `Kiến thức liên quan:\n${context}` }, // long-term → short-term
  ...history,                                                        // short-term
  { role: "user", content: userMsg },
];

Khi nào cần long-term? Khi thông tin (a) nhiều hơn context window, (b) phải bền qua session, hoặc (c) là kiến thức riêng model không được train. Chat 3 câu hỏi đáp thì short-term là đủ — đừng over-engineer vector DB.

Pitfall thực tế. (1) Nhét quá nhiều "memory" vào prompt → model nhiễu, latency tăng, tiền tăng. (2) Vector search trả về đoạn không liên quan (embedding kém / chunk sai) → agent tự tin nói sai. (3) Coi vector DB như source of truth cho dữ liệu chính xác (số dư, giá) — sai; realtime/chính xác thì gọi API/DB qua tool, không truy hồi vector.


6. Agent Frameworks

Định nghĩa. Thư viện lo sẵn phần "loop + tool calling + message plumbing" để bạn không tự viết. Phổ biến: Vercel AI SDK (generateText + tools, gọn cho TS/Node/Next), LangChain / LangGraph (nhiều tích hợp, LangGraph mô hình agent thành graph state machine).

Tại sao quan trọng. Loop + parse tool call + quản history + streaming + retry là boilerplate dễ sai. Framework chuẩn hoá, đổi được model provider (OpenAI/Anthropic) mà không đổi code loop.

Cơ chế / trade-off.

  • Tự viết loop: kiểm soát tối đa, ít magic, dễ debug, không lệ thuộc. Nhưng bạn tự lo streaming, parallel tool calls, retry, provider differences.
  • Vercel AI SDK: API nhỏ gọn (generateText, streamText, tool), hợp app TS. maxSteps lo loop giúp bạn. Ít abstraction thừa. Hợp phần lớn app web.
  • LangChain/LangGraph: mạnh khi flow phức tạp (nhiều nhánh, cycle, human-in-the-loop, checkpoint state). Đổi lại abstraction dày, learning curve cao, hay "magic".

Ví dụ code (cùng 1 việc, Vercel AI SDK lo loop).

ts
const { text, steps } = await generateText({
  model, tools: { getWeather, createTicket },
  maxSteps: 6,                    // framework tự lặp reason→act→observe tới 6 lần
  prompt: userMsg,
});
console.log(steps.length, "bước đã chạy");

Chọn thế nào? App TS đơn giản/vừa → Vercel AI SDK. Flow nhiều nhánh/state phức tạp, cần checkpoint & resume → LangGraph. Muốn hiểu bản chất & control tuyệt đối, ít tool → tự viết (như mục 3). Quy tắc: bắt đầu đơn giản, chỉ lên framework nặng khi thực sự cần.

Pitfall thực tế. Chọn LangChain "vì phổ biến" cho một app 2 tool → nuốt abstraction, khó debug khi lỗi nằm sâu trong framework. Ngược lại tự viết cho flow 20 nhánh có cycle → tự bịa lại một framework tồi hơn. Match độ phức tạp framework với độ phức tạp bài toán.


7. MCP (Model Context Protocol) là gì

Định nghĩa. MCP là một chuẩn mở (Anthropic khởi xướng) để kết nối AI app ↔ tools/data theo một giao thức chung. Ví như "USB-C cho AI": một cổng chuẩn để cắm bất kỳ tool nào vào bất kỳ AI app nào.

Tại sao cần chuẩn hoá? Không có MCP: mỗi AI app (Claude Desktop, IDE, agent của bạn) phải tự viết tích hợp riêng cho mỗi tool (GitHub, Postgres, Slack…). N app × M tool = N×M tích hợp custom, không tái dùng được. Có MCP: mỗi tool viết một MCP server, mọi MCP client dùng được → thành N + M. Đây là lý do MCP bùng nổ: viết tool một lần, chạy mọi nơi.

Cơ chế (client–host–server).

  • Host: ứng dụng AI người dùng chạy (Claude Desktop, IDE, agent runtime của bạn). Chứa LLM.
  • Client: bộ nối bên trong host, giữ kết nối 1–1 tới một server.
  • Server: chương trình expose tools/data theo chuẩn MCP (vd: "github server" expose tool tạo issue). Chạy local (stdio) hoặc remote (HTTP).

Luồng: host phát hiện server → hỏi "bạn có tool gì?" → server trả danh sách schema → host đưa schema cho LLM → LLM quyết định gọi → host gửi lệnh gọi qua client → server thực thi → trả kết quả.

Ví dụ (khái niệm, cấu hình host trỏ tới server).

jsonc
// host config: khai báo 1 MCP server chạy qua stdio
{ "mcpServers": {
    "my-tools": { "command": "node", "args": ["./my-mcp-server.js"] }
} }

Liên hệ Multica. Multica là ví dụ sống của "agent-as-teammate": agent nhận issue, comment, chạy code. Nhiều tool trong hệ này (và trong Claude Code bạn đang chạy) được expose qua MCP — bạn thấy loạt mcp__... tools chính là các MCP server đã cắm vào host.

Pitfall thực tế. Nhầm MCP với "một API khác". MCP không thay REST — nó là lớp chuẩn hoá cách LLM khám phá và gọi tool. Và: MCP server chạy với quyền của bạn; cắm server lạ = cho code lạ chạy trên máy bạn với token của bạn. Chỉ cắm server tin cậy.


8. MCP Primitives & Transport

Định nghĩa. MCP định nghĩa 3 primitive (loại capability server có thể cung cấp) và các transport (cách client–server nói chuyện).

Ba primitive.

  • Tools: hành động model gọi (có side effect): createIssue, runQuery. — model-controlled.
  • Resources: dữ liệu để đọc vào context (file, DB row, doc), định danh bằng URI. Giống GET, không side effect. — thường app/user chọn đưa vào.
  • Prompts: template prompt tái dùng do server cung cấp, user kích hoạt (vd "review PR này"). — user-controlled.

Phân biệt then chốt: tool = làm gì đó, resource = đọc gì đó, prompt = template có sẵn.

Transport.

  • stdio: server chạy local như child process, giao tiếp qua stdin/stdout. Đơn giản, nhanh, hợp tool local (đọc file, DB local). Không qua network.
  • HTTP / SSE (Streamable HTTP): server chạy remote, giao tiếp qua HTTP + Server-Sent Events để stream. Hợp server dùng chung nhiều người, deploy trên cloud.

Tại sao quan trọng. Chọn đúng primitive giúp model dùng đúng: đừng biến "đọc file" thành tool nếu nó chỉ là resource. Chọn đúng transport: local dev → stdio; production shared → HTTP.

Ví dụ (khai báo resource + tool trong server).

ts
server.resource("file", "file:///docs/{name}", async (uri) => ({
  contents: [{ uri: uri.href, text: await readFile(uri) }],   // đọc, không side effect
}));
server.tool("createIssue", schema, async (a) => { /* side effect */ });

Pitfall thực tế. Nhét mọi thứ thành tool → model bị ngập lựa chọn, gọi lung tung. Dữ liệu chỉ-đọc nên là resource. Về transport: stdio server viết log ra stdout sẽ phá giao thức (stdout dành cho JSON-RPC) — log phải ra stderr. Đây là bug MCP kinh điển.


9. Xây một MCP Server đơn giản (TS SDK)

Định nghĩa. Dùng @modelcontextprotocol/sdk để viết một server expose 1–2 tool, rồi cắm vào một host (Claude Desktop / IDE / agent) để gọi.

Tại sao quan trọng. Đây là kỹ năng biến "code có sẵn của bạn" thành capability mọi AI app dùng được. Với BE engineer: giống viết một microservice, nhưng "khách hàng" là LLM.

Cơ chế. (1) Tạo server, khai báo tool (name, schema, handler). (2) Chọn transport (stdio cho local). (3) connect. (4) Khai báo server trong config của host. (5) Host gọi tool.

Ví dụ code (server stdio expose 2 tool).

ts
// my-mcp-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "math-tools", version: "1.0.0" });

server.tool("add", { a: z.number(), b: z.number() },
  async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }));

server.tool("now", {},
  async () => ({ content: [{ type: "text", text: new Date().toISOString() }] }));

const transport = new StdioServerTransport();   // giao tiếp qua stdin/stdout
await server.connect(transport);
// LƯU Ý: KHÔNG console.log ra stdout — sẽ phá JSON-RPC. Dùng console.error (stderr).

Cách host gọi vào. Trong config host (vd Claude Desktop claude_desktop_config.json):

jsonc
{ "mcpServers": {
    "math-tools": { "command": "node", "args": ["/abs/path/my-mcp-server.js"] }
} }

Host khởi động server như child process, hỏi danh sách tool (add, now), đưa schema cho LLM. Khi bạn chat "cộng 3 và 4", LLM gọi add({a:3,b:4}), server trả 7, LLM đọc rồi trả lời.

Pitfall thực tế. (1) Log ra stdout phá giao thức (nói ở mục 8). (2) Đường dẫn tương đối trong config — host chạy từ cwd khác → không tìm thấy file; dùng absolute path. (3) Handler throw không bắt → server chết, host mất kết nối; bọc try/catch trả lỗi dạng content. (4) Quên build TS → JS; host chạy node file.js, không chạy .ts trực tiếp (trừ khi dùng tsx).


10. Orchestration & Guardrails

Định nghĩa. Tập cơ chế kiểm soát agent để nó an toàn và trong ngân sách: giới hạn số bước, timeout, chi phí (token/tiền), human-in-the-loop cho hành động nguy hiểm, và sandbox cho code/tool rủi ro.

Tại sao quan trọng. Agent tự quyết định → có thể quyết định sai với hậu quả thật: xoá dữ liệu, charge tiền, gửi email nhầm, loop cháy budget. Guardrails là ranh giới giữa "trợ lý hữu ích" và "sự cố production". Chính CLAUDE.md của workspace này có guardrail "human approval gate": agent chỉ được tạo issue ở backlog, không tự đẩy sang todo — con người mới bấm nút.

Cơ chế.

  • Max steps: chặn loop vô hạn (mục 3).
  • Timeout: mỗi tool + toàn run có deadline; treo tool không được block mãi.
  • Budget: đếm token/tiền, dừng khi vượt ngưỡng.
  • Human-in-the-loop (HITL): tool nguy hiểm (xoá, chi tiền, gửi mail) không tự chạy — trả về "cần xác nhận", chờ người duyệt rồi mới thực thi.
  • Sandbox: chạy code/tool không tin cậy trong môi trường cô lập (container, VM) — không cho đụng filesystem/network thật.
  • Allowlist quyền: agent chỉ được gọi tập tool được phép cho task đó.

Ví dụ code (HITL cho hành động phá huỷ).

ts
const deleteUser = tool({
  description: "Xoá user. HÀNH ĐỘNG NGUY HIỂM.",
  parameters: z.object({ userId: z.string(), confirmed: z.boolean().default(false) }),
  execute: async ({ userId, confirmed }) => {
    if (!confirmed)
      return { status: "NEEDS_CONFIRMATION", message: `Xác nhận xoá ${userId}? Gọi lại confirmed=true.` };
    await requireHumanApproval(userId);        // chặn: chờ người bấm duyệt
    await db.user.delete(userId);
    return { status: "deleted" };
  },
});

Pitfall thực tế. (1) HITL "giả": chỉ hỏi model confirmed=true — model tự set true là xong, không có người thật. Xác nhận nguy hiểm phải chặn ở con người/hệ thống, không phải tin field do model điền. (2) Không timeout tool gọi API ngoài → một tool treo khoá cả run. (3) Chạy code do LLM sinh thẳng trên máy prod không sandbox → RCE.


11. Observability cho Agent

Định nghĩa. Khả năng quan sát những gì agent làm: trace từng bước (mỗi tool call, args, kết quả, token dùng, latency, chi phí), gom thành một cây theo run. Công cụ: Langfuse, OpenTelemetry (chuẩn tracing chung), LangSmith…

Tại sao agent khó debug (và cần observability). Agent không tất định: cùng input, hai lần chạy có thể khác đường đi. Lỗi có thể ở: prompt, schema tool, kết quả tool, hay quyết định của model. Không có trace, bạn chỉ thấy "output cuối sai" mà không biết bước nào hỏng. Trace biến hộp đen thành hộp kính.

Cơ chế. Mỗi run = một trace. Mỗi bước = một span (LLM call span, tool call span) lồng nhau, ghi: input, output, tokens, latency, cost, lỗi. Bạn xem lại "phim quay chậm" của agent: nó reason gì, gọi tool nào, tool trả gì, vì sao rẽ nhánh đó.

Ví dụ code (trace thủ công + Langfuse ý niệm).

ts
const trace = langfuse.trace({ name: "support-agent", input: userMsg });
for (const call of res.toolCalls) {
  const span = trace.span({ name: call.name, input: call.args });
  const t0 = Date.now();
  const result = await tools[call.name](call.args);
  span.end({ output: result, metadata: { ms: Date.now() - t0 } });   // latency mỗi tool
}
trace.update({ output: finalText, usage: res.usage });               // tokens/cost cả run

(Vercel AI SDK có experimental_telemetry xuất OpenTelemetry để Langfuse nuốt tự động.)

Pitfall thực tế. (1) Chỉ log output cuối → mù hoàn toàn khi lỗi nằm ở bước giữa. (2) Log cả PII/secret vào trace không mask → rò rỉ. (3) Không đo token/cost per run → phát hiện cháy tiền qua hoá đơn cuối tháng thay vì qua dashboard. Bật observability trước khi lên production, không phải sau khi có sự cố.


12. Multi-Agent (tóm tắt)

Định nghĩa. Thay vì một agent làm tất, chia thành nhiều agent chuyên môn, thường có một orchestrator (điều phối) giao việc cho các sub-agent (researcher, coder, reviewer…) rồi tổng hợp.

Tại sao / khi nào chia. Chia khi: (a) task tách được thành sub-task rõ ràng, ít phụ thuộc lẫn nhau (chạy song song được); (b) mỗi phần cần "persona"/tool set khác hẳn; (c) một agent với 40 tool trở nên lú (quá nhiều lựa chọn → chọn sai). Chia giúp mỗi agent context gọn, tool ít, chuyên biệt → chính xác hơn.

Cơ chế. Orchestrator nhận mục tiêu → phân rã thành sub-task → spawn sub-agent (mỗi cái là một agent loop riêng, tool riêng) → thu kết quả → tổng hợp/ra quyết định. Chính bạn (subagent này) là ví dụ: được orchestrator giao đúng một việc, với path và acceptance criteria rõ.

Ví dụ (khái niệm).

ts
const research = await runAgent({ role: "researcher", tools: [search], task });
const draft    = await runAgent({ role: "writer", tools: [], task: `Viết dựa trên: ${research}` });
// orchestrator tổng hợp research → draft

Pitfall thực tế. Multi-agent thường bị lạm dụng: chia nhỏ khi một agent là đủ → tăng độ phức tạp, latency, chi phí, và lỗi "tam sao thất bản" khi truyền context giữa các agent. Quy tắc: mặc định một agent; chỉ multi-agent khi có lý do rõ (song song thật, tách persona, tool quá nhiều). File ownership giữa agent phải rõ để tránh ghi đè nhau.


Thực hành

Mục tiêu: viết một agent có 2–3 tool, chạy được vòng lặp thật, có guardrail và trace.

  1. Scaffold (Node/TS): npm i ai zod @ai-sdk/openai (hoặc anthropic). Tạo agent.ts.
  2. Tool 1 — search RAG (long-term memory): một hàm searchDocs(q) embed câu hỏi, query vector store (pgvector/local), trả top-k đoạn text. (Mục 5.)
  3. Tool 2 — call API: getOrderStatus(orderId) gọi REST API thật (hoặc mock trả JSON), validate orderId bằng zod. (Mục 2, 4.)
  4. Tool 3 — action có guardrail: refundOrder(orderId, confirmed) — nếu !confirmed trả NEEDS_CONFIRMATION; có idempotencyKey; log ra stderr. (Mục 4, 10.)
  5. Loop: dùng generateText({ tools, maxSteps: 6 }) (hoặc tự viết loop mục 3). Đặt maxSteps, timeout mỗi tool.
  6. Observability: log mỗi bước (tool name, args, ms, tokens) — thủ công hoặc Langfuse. (Mục 11.)
  7. Test 3 kịch bản: (a) câu hỏi cần RAG, (b) câu hỏi cần call API, (c) yêu cầu refund → phải dừng ở NEEDS_CONFIRMATION, không tự chạy.
  8. (Optional) MCP server nhỏ: tách 1 tool (vd getOrderStatus) thành MCP server stdio (mục 9), cắm vào Claude Desktop, gọi thử từ chat. Nhớ: log ra stderr, absolute path trong config.

Done khi

  • [ ] Agent chạy loop reason→act→observe, tự chọn đúng tool cho mỗi trong 3 kịch bản.
  • [ ] Mỗi tool có description rõ, params validate bằng zod, trả lỗi dạng dữ liệu (không throw ra loop).
  • [ ] maxSteps + timeout hoạt động: chứng minh loop dừng khi tool luôn lỗi (không cháy vô hạn).
  • [ ] Hành động nguy hiểm (refund) không tự chạy — chặn ở NEEDS_CONFIRMATION / human approval.
  • [ ] Có trace: đọc lại được từng bước (tool, args, kết quả, latency, token) của một run.
  • [ ] RAG tool truy hồi đúng đoạn liên quan; dữ liệu chính xác (order status) lấy qua API, KHÔNG qua vector.
  • [ ] (Optional) MCP server expose được ≥1 tool, host gọi vào chạy đúng, không phá stdout.
  • [ ] Giải thích được: khi nào một agent là đủ vs khi nào cần multi-agent (không lạm dụng).

Câu hỏi chưa chốt (tự trả lời khi thực hành)

  • Vector store nào cho long-term memory: pgvector (đã có Postgres) hay managed (Pinecone)?
  • Provider model: OpenAI hay Anthropic? Ảnh hưởng cú pháp tool calling nhẹ, SDK che phần lớn.
  • Ngưỡng budget cụ thể (token/USD mỗi run) cho môi trường của bạn là bao nhiêu?

Học bằng cách build. Chứng minh, đừng tin.