Series 2 - Bài 6: Tool Design Principles cho LLM

Viết tool cho LLM khác viết function cho human developer. Bài này đưa ra 6 nguyên tắc thiết kế tool dựa trên cách LLM reason và ra quyết định.

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

Tool tốt cho developer và tool tốt cho LLM không giống nhau. Developer đọc docs, hỏi Stack Overflow, hỏi đồng nghiệp. LLM chỉ có tool definition trong context window.

Nếu tool design không optimize cho LLM, agent sẽ:

  • Gọi sai tool trong tình huống đúng
  • Gọi đúng tool nhưng với input sai
  • Gọi thêm tool không cần thiết
  • Không biết recover khi tool fail

Principle 1: Single Responsibility

Mỗi tool làm một việc rõ ràng. Không phải một việc và một việc khác tùy option.

Anti-pattern: Tool đa năng
  file_ops(operation: "read" | "write" | "delete", path, content?)

  Problem:
  • LLM phải biết các operation có thể có
  • Parameters conditional theo operation (content chỉ cần khi write)
  • Khó viết description rõ cho tất cả cases
  • Permission check phức tạp (read vs write vs delete ≠ cùng policy)

Better: Tách riêng
  read_file(path)
  write_file(path, content)
  delete_file(path)

  Benefits:
  • Mỗi tool có clear description
  • Parameters không conditional
  • Permission riêng lệ
  • LLM chọn đúng tool theo tên

Principle 2: Idempotency là Feature

Tool idempotent có thể gọi nhiều lần mà không thay đổi kết quả. Trong agent context, đây rất quan trọng:

Idempotent tools:
  read_file     → đọc lần n, kết quả như lần 1
  list_directory → list lần n, kết quả như lần 1
  search_files  → search lần n, kết quả như lần 1

Non-idempotent tools:
  write_file    → mỗi lần ghi là một change
  run_command   → mỗi lần chạy có thể khác nhau
  append_to_file → mỗi lần thêm content mới

Việc thiết kế: minimize non-idempotent tools. Khi không thể tránh, description phải explicit:

  description: "Write content to a file, REPLACING existing content.
                Each call overwrites the entire file.
                Use with caution — ensure you have read the
                file first to understand what will be lost."

Principle 3: Granularity phải phù hợp với task

Too fine-grained:
  read_line(path, line_number)
  read_lines(path, start, end)
  read_function(path, function_name)
  read_class(path, class_name)
  …

  Problem: LLM cần nhiều tool calls để đọc một file.
           Mỗi call = một LLM round trip. 5x latency.

Too coarse-grained:
  read_all_project_files()

  Problem: Return quá nhiều → context pollution.
           Token cost khổng lồ.

Right granularity:
  read_file(path)         ← toàn bộ 1 file
  list_directory(path)    ← entries trong 1 folder
  search_files(pattern)   ← tìm file theo pattern

  LLM tự quyết định đọc file nào sau khi list.
  Không cần grained hơn, không cần coarser hơn.

Principle 4: Result phải Actionable

LLM cần biết làm gì với result. Nếu result không actionable, LLM có thể gọi thêm tools không cần thiết.

Non-actionable:
  list_directory("/src") → "3 items"
  • LLM không biết 3 items là gì
  • Phải gọi thêm tool để biết chi tiết

Actionable:
  list_directory("/src") →
    "src/
      user.service.ts (245 lines, modified 2h ago)
      user.controller.ts (89 lines)
      user.entity.ts (34 lines)"
  • LLM biết immediately đọc file nào tiếp theo

Meta-info hữu ích trong result:
  File size / line count  → LLM biết file lớn hay nhỏ
  Last modified          → LLM biết file có recent changes không
  Exit code + duration   → LLM biết command thành công hay không

Principle 5: Fail Loudly và Specifically

Bad failure:
  run_command("npm test") → "Command failed"

  LLM không biết:
  - Exit code bao nhiêu?
  - Stderr có gì?
  - Phải làm gì tiếp?

Good failure:
  run_command("npm test") →
    "Command exited with code 1 (test failure).

     Failed tests:
       ✕ UserService > processLoan > should throw for amount=0

     Error: Expected InvalidAmountError but got undefined
       at processLoan (src/user.service.ts:45)

     Suggestion: Check processLoan at line 45 in user.service.ts"

  LLM biết:
  - Test nào fail
  - Tại sao fail
  - File nào cần đọc tiếp

Principle 6: Tool có consistent output format

LLM parse tool results theo pattern. Nếu format thay đổi giữa các tools, LLM có thỉ misinterpret:

Consistency trong error format:
  Mọi tool khi fail:
    "Error: [reason]"
    "Suggestion: [what to try next]"

Consistency trong success format:
  read_file: "[content]" (raw content)
  list_directory: "[path]/\n  [item]\n  [item]" (indented)
  run_command: "Exit code: [n]\nOutput:\n[stdout]" (labeled)

Không:
  read_file: {content: "..."}  (JSON)
  list_directory: "Item 1: ..." (prose)
  run_command: "OK"            (ambiguous)

Consistency giảm cognitive load của LLM — nó không phải learn format mới cho mỗi tool.

Trade-off: Rất hữu ích vs An toàn

Tool rất hữu ích:
  execute_arbitrary_code(code: string)
  • Agent có thể làm bất cứ điều
  • Cực kì mạnh
  • Attack surface khổng lồ
  • Một prompt injection = game over

Tool an toàn:
  run_command(command: string, allowedCommands: string[])
  • Chỉ chạy commands trong whitelist
  • ít flexible hơn
  • Attack surface rất nhỏ

Quy tắc: Design tools với minimum capability cần thiết. Không expose power không cần. Đây là principle of least privilege cho tools.

Kết luận

  • Single responsibility: mỗi tool làm một việc, tầch riêng khi nào có thể.
  • Idempotency là feature: tool idempotent an toàn hơn và dễ test hơn.
  • Granularity phải vừa đủ: không quá fine (nhiều round trips) không quá coarse (context pollution).
  • Result actionable: include meta-info để LLM ra quyết định tiếp theo mà không cần thêm tool call.
  • Fail specifically: error message với reason và suggestion giảm vòng loop.
  • Consistent format: LLM không phải learn format mới cho mỗi tool.
  • Principle of least privilege: expose minimum capability cần thiết.