Custom AI Tools: Build CLI nhỏ bằng Python gọi LLM API cho công việc hàng ngày
7/19/2026 · 13p đọc
title: "Custom AI Tools: Build CLI nhỏ bằng Python gọi LLM API cho công việc hàng ngày"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 1 — AI-Native Coding & Tooling"
order: 10
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags: ["cli", "python", "llm-api", "automation", "ai-native", "developer-tooling", "internal-tools"]
Custom AI Tools: Build CLI nhỏ bằng Python gọi LLM API cho công việc hàng ngày
Một Pull Request 47 file thay đổi, +1200/-380 dòng, nằm chờ review từ sáng. Bạn mở tab diff, cuộn qua từng file, cố ghép lại trong đầu: cái này sửa bug, cái kia refactor không liên quan, cái kia nữa — không rõ có phải leftover debug code hay không. 20 phút sau bạn mới đủ ngữ cảnh để bắt đầu review thật sự. Nhân với số PR bạn review mỗi tuần, đây là một khoản chi phí lặp lại, âm thầm, không ai tính vào velocity report nhưng ai cũng cảm nhận được.
Chín bài trước trong Season 1 nói về việc dùng AI có sẵn: prompt tốt hơn, đọc code base nhanh hơn, debug bằng stack trace, review OWASP. Tất cả đều là AI người khác build sẵn — bạn gõ prompt vào giao diện chat hoặc IDE, nhận output, dán ra ngoài. Cách này đủ tốt cho phần lớn công việc. Nhưng có một lớp việc lặp lại, đặc thù quy trình của riêng team bạn — tóm tắt diff trước khi review, sinh commit message chuẩn từ git diff, rà soát changelog trước khi release, phân loại ticket theo mức độ ưu tiên dựa trên nội dung — mà không giao diện chat nào vừa vặn, và mua một SaaS chuyên dụng cho một việc 30 dòng code thì không đáng.
Bài này là bước ngoặt của Season 1: từ tư duy "dùng AI tool có sẵn" sang "tự build AI tool cho chính mình". Đây chưa phải AI Agent theo nghĩa đầy đủ (Season 3 sẽ đào sâu phần đó — agent có state, có tool-calling, có vòng lặp tự sửa lỗi) — ở đây chỉ là một CLI script gọi LLM API một lần, ăn input, trả output có cấu trúc. Nhưng chính bước nhỏ này là ranh giới giữa "người dùng AI" và "kỹ sư build hạ tầng AI" — tư duy nền tảng của AI-Augmented Solutions Architect.
Vấn đề
Vì sao những việc như "tóm tắt diff trước khi review" không có công cụt có sẵn nào vừa vặn?
1. Nó quá đặc thù để có SaaS chung, quá nhỏ để đáng mua SaaS riêng. Có sản phẩm AI code review thương mại (tích hợp GitHub/GitLab, chấm điểm rủi ro, gợi ý fix) — nhưng chúng nhắm vào một quy trình review đầy đủ, không phải một bước "tóm tắt nhanh trước khi mình tự đọc". Trả tiền license cho cả team, xin duyệt security review cho một third-party tool, tích hợp SSO — tất cả chi phí đó để đổi lấy một bước tóm tắt 30 giây, thường không đáng.
2. Giao diện chat (ChatGPT, Claude.ai, Copilot Chat) không phù hợp cho việc lặp lại có cấu trúc input cố định. Mỗi lần cần tóm tắt diff, bạn phải: mở tab chat, gõ lại prompt (hoặc nhớ paste từ note), copy nguyên diff dán vào, chờ, đọc, copy kết quả ra chỗ khác dùng. Không có cách nào gắn bước này vào git alias, pre-commit hook, hay CI step. Giao diện chat được thiết kế cho hội thoại tương tác, không phải cho một thao tác lặp đi lặp lại có input/output cố định.
3. Prompt hay đến mấy cũng không tự động hoá được nếu nó chỉ tồn tại trong đầu bạn hoặc trong một ghi chú. Prompt template tốt (như các bài 3, 7, 8 đã bàn) giải quyết được chất lượng output, nhưng không giải quyết được việc phải tự tay chạy lại nó mỗi lần. Một CLI tool đóng gói prompt đó thành lệnh gõ một dòng — pr-summarizer --diff HEAD~1 — biến kỹ thuật prompt thành hạ tầng dùng lại được, chia sẻ được cho cả team qua một file script trong repo.
Đây chính là điểm chuyển tư duy: một Solutions Architect không chỉ hỏi "prompt nào tốt cho việc này" mà còn hỏi "việc này có đáng được đóng gói thành một công cụ nội bộ không, và nếu có thì kiến trúc tối thiểu của nó là gì".
Kỹ thuật cốt lõi
Một CLI tool gọi LLM API — dù nhỏ đến đâu — luôn có 4 khối chức năng, theo đúng thứ tự dữ liệu chảy qua:
flowchart LR
A["1. Đọc input<br/>(file / stdin / argv / subprocess)"] --> B["2. Xây prompt<br/>(template có tham số hoá)"]
B --> C["3. Gọi LLM API<br/>(có timeout, retry, xử lý lỗi)"]
C --> D["4. Format output<br/>(cấu trúc hoá, không in thô)"]
style A fill:#59c,stroke:#333,stroke-width:1px
style B fill:#5a5,stroke:#333,stroke-width:1px
style C fill:#c93,stroke:#333,stroke-width:1px
style D fill:#c55,stroke:#333,stroke-width:1px
Giải thích từng khối, vì sao nó cần tồn tại độc lập thay vì gộp chung:
| Khối | Vai trò | Vì sao tách riêng |
|---|---|---|
| Đọc input | Lấy dữ liệu thô từ nhiều nguồn: file diff export sẵn, output của git diff qua subprocess, hoặc stdin để pipe từ lệnh khác |
Tool càng linh hoạt về nguồn input càng dễ nhét vào quy trình có sẵn (alias, hook, CI) mà không phải sửa code |
| Xây prompt | Nhét dữ liệu vào template cố định, có chỗ tham số hoá (ngôn ngữ output, mức độ chi tiết, ngữ cảnh dự án) | Tách prompt khỏi logic gọi API giúp sửa prompt (thứ hay đổi nhất) mà không đụng phần gọi API (thứ ổn định) |
| Gọi LLM API | Gửi request, xử lý timeout, retry khi lỗi mạng/rate limit, không để một lần fail làm crash cả tool | Đây là điểm duy nhất phụ thuộc network — cần xử lý lỗi cẩn thận hơn phần còn lại vốn chạy local |
| Format output | Parse kết quả trả về thành cấu trúc hữu ích (markdown có heading, hoặc JSON để pipe tiếp vào tool khác), không in nguyên văn response thô | Output thô từ LLM thường lẫn lời dẫn dòng ("Dưới đây là bản tóm tắt...") không cần thiết khi dùng trong pipeline tự động |
Điểm khác biệt quan trọng so với việc gõ prompt vào chat: tool CLI là code, nên nó được version control, review, và cải tiến như mọi phần code khác trong repo — không phải một prompt nằm rải rác trong lịch sử chat của từng người. Đây là lý do build CLI tool nội bộ, dù nhỏ, luôn đáng giá hơn "mỗi người tự nhớ prompt của mình" khi việc đó được lặp lại đủ nhiều.
Thực hành
CLI tool cụ thể: pr-summarizer
Dưới đây là toàn bộ code — chạy được ngay sau khi cài pip install anthropic (hoặc SDK của provider bạn dùng) và điền API key qua biến môi trường. Tool nhận diff từ file hoặc trực tiếp từ git diff, gửi tới LLM API, in ra bản tóm tắt theo format chuẩn: Tóm tắt — File thay đổi chính — Rủi ro cần review kỹ.
#!/usr/bin/env python3
"""
pr-summarizer.py — Tóm tắt diff của một PR trước khi review.
Cách dùng:
export ANTHROPIC_API_KEY="sk-ant-..." # KHÔNG hardcode key trong code
python pr_summarizer.py # tự lấy diff của commit gần nhất
python pr_summarizer.py --base main # diff so với nhánh main
git diff main | python pr_summarizer.py --stdin # đọc diff qua pipe
"""
import argparse
import os
import subprocess
import sys
import time
import anthropic # pip install anthropic
MODEL = "claude-sonnet-4-5"
MAX_RETRIES = 3
PROMPT_TEMPLATE = """Bạn là một senior engineer đang giúp đồng nghiệp chuẩn bị review PR.
Đọc đoạn diff dưới đây và trả lời NGẮN GỌN theo đúng 3 mục sau, không thêm lời dẫn:
## Tóm tắt
(2-3 câu: PR này làm gì, thay đổi theo hướng nào)
## File thay đổi chính
(liệt kê tối đa 5 file quan trọng nhất, mỗi dòng: tên file — thay đổi gì)
## Rủi ro cần review kỹ
(liệt kê cụ thể: logic nghiệp vụ nhạy cảm, thiếu xử lý lỗi, thay đổi schema/API
breaking change, hoặc "Không phát hiện rủi ro rõ ràng" nếu thực sự không có)
Diff:
---
{diff}
---
"""
def get_diff_from_git(base: str) -> str:
"""Lấy diff qua subprocess thay vì tự tay copy-paste."""
result = subprocess.run(
["git", "diff", f"{base}...HEAD"],
capture_output=True, text=True, check=True,
)
return result.stdout
def call_llm(diff: str) -> str:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
sys.exit("Lỗi: chưa set biến môi trường ANTHROPIC_API_KEY.")
client = anthropic.Anthropic(api_key=api_key)
prompt = PROMPT_TEMPLATE.format(diff=diff[:15000]) # cắt bớt nếu diff quá dài
for attempt in range(1, MAX_RETRIES + 1):
try:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
except anthropic.APIStatusError as e:
if attempt == MAX_RETRIES:
sys.exit(f"Gọi API thất bại sau {MAX_RETRIES} lần: {e}")
wait = 2 ** attempt # backoff: 2s, 4s, 8s
print(f"Lỗi API (lần {attempt}), thử lại sau {wait}s...", file=sys.stderr)
time.sleep(wait)
def main():
parser = argparse.ArgumentParser(description="Tóm tắt diff PR bằng LLM.")
parser.add_argument("--base", default="main", help="Nhánh gốc để so sánh diff")
parser.add_argument("--stdin", action="store_true", help="Đọc diff từ stdin thay vì git")
args = parser.parse_args()
diff = sys.stdin.read() if args.stdin else get_diff_from_git(args.base)
if not diff.strip():
sys.exit("Không có thay đổi nào để tóm tắt.")
print(call_llm(diff))
if __name__ == "__main__":
main()
Vài điểm cần chú ý khi đọc code trên (đúng 4 khối đã nói ở phần trước):
- Đọc input (
get_diff_from_git/--stdin): hai đường nhập liệu — subprocess gọigit difftrực tiếp cho dùng nhanh, và đọcstdinđể pipe từ lệnh khác (git diff main | python pr_summarizer.py --stdin) khi cần linh hoạt hơn, ví dụ trong CI. - Xây prompt (
PROMPT_TEMPLATE): tham số hoá bằng.format(diff=...), tách khỏi phần gọi API — sửa format output (thêm mục, đổi ngôn ngữ) chỉ cần sửa string này, không đụng logic network. - Gọi API (
call_llm): có retry với backoff cấp số nhân (2s → 4s → 8s) cho lỗi tạm thời (rate limit, timeout mạng), và thoát rõ ràng bằngsys.exitvới thông báo cụ thể thay vì để traceback khó hiểu văng ra. - Format output: prompt ép model trả về đúng 3 mục cố định — đây là "format hoá ở tầng prompt" cho một script nhỏ. Nếu cần dùng output này tiếp trong pipeline khác (ví dụ tự động post comment lên PR), bước tiếp theo hợp lý là yêu cầu model trả JSON (
response_formathoặc structured output tương ứng của provider) và parse bằngjson.loadsthay vì markdown tự do.
Chạy thử
# Cài dependency (một lần)
pip install anthropic
# Set API key qua biến môi trường — KHÔNG BAO GIỜ hardcode vào file
export ANTHROPIC_API_KEY="sk-ant-xxxxxxxxxxxx"
# Chạy trên PR hiện tại, so với nhánh main
python pr_summarizer.py --base main
Output mẫu (minh hoạ hình dạng, không phải kết quả cố định — nội dung thật phụ thuộc diff đầu vào):
## Tóm tắt
PR thêm endpoint tìm kiếm khách hàng theo tên công ty và refactor lại
CustomerRepository để dùng QueryBuilder thay cho raw SQL.
## File thay đổi chính
- customers.service.ts — thêm searchByCompanyName(), đổi raw query sang QueryBuilder
- customers.controller.ts — thêm route GET /customers/search
- customers.module.ts — đăng ký thêm provider
## Rủi ro cần review kỹ
- Endpoint search chưa thấy giới hạn theo workspace_id trong đoạn diff —
cần kiểm tra kỹ khả năng lộ dữ liệu cross-tenant.
- Không thấy rate limit hoặc validate độ dài input cho companyName.
Biến thể nhanh: đổi thành "commit-message-writer"
Vì kiến trúc 4 khối giữ nguyên, đổi mục đích tool chỉ cần đổi PROMPT_TEMPLATE và cách lấy input — phần còn lại (gọi API, retry, argparse) tái dùng nguyên vẹn:
COMMIT_MSG_TEMPLATE = """Viết commit message theo chuẩn Conventional Commits
(feat/fix/refactor/chore + mô tả ngắn gọn, tiếng Anh) dựa trên diff sau.
Chỉ trả về đúng commit message, không giải thích thêm:
{diff}
"""
Đây chính là lý do "viết CLI tool nhỏ" đáng đầu tư thời gian ban đầu hơn một prompt dùng một lần: khung sườn (đọc input, gọi API có retry, argparse) tái sử dụng được cho hàng chục việc tương tự trong quy trình của team.
Cạm bẫy thường gặp
1. Hardcode API key trực tiếp trong code. Việc dễ mắc nhất khi viết script nhanh cho bản thân dùng: gõ thẳng api_key = "sk-ant-abc123..." vào code cho tiện, định "sửa lại sau". Rủi ro thật: commit nhầm lên git (kể cả private repo — key vẫn nằm trong lịch sử commit mãi mãi trừ khi rewrite history), chia sẻ script cho đồng nghiệp kèm luôn key cá nhân, hoặc paste code lên đâu đó để hỏi debug. Luôn dùng biến môi trường (os.environ.get(...)) hoặc file .env được .gitignore, không có ngoại lệ "chỉ là script nội bộ, không sao đâu".
2. Over-engineering một script 40 dòng thành một "framework nội bộ". Cám dỗ tự nhiên khi một kỹ sư quen làm hệ thống lớn: thêm config file YAML, thêm plugin system để hỗ trợ nhiều LLM provider, thêm class abstraction cho "input source" và "output formatter", viết unit test đầy đủ cho một script chỉ mình bạn dùng. Với công cụ nội bộ giải quyết một việc cụ thể, hẹp — argparse + vài hàm thuần là đủ. Chỉ nâng cấp kiến trúc (tách module, thêm test, viết docs) khi tool đã được từ 2-3 người khác trong team dùng thật và có nhu cầu mở rộng rõ ràng, không nâng cấp trước vì "sau này có thể cần".
3. Không xử lý trường hợp input rỗng hoặc quá lớn, để LLM API tự "gánh". Diff của một PR khổng lồ (refactor toàn bộ module, vài nghìn dòng) có thể vượt context window hoặc làm response chậm/tốn phí bất thường; diff rỗng (chạy tool nhầm lúc chưa có thay đổi) gửi request vô nghĩa. Code mẫu trên xử lý phần này ở mức tối thiểu (diff[:15000] cắt bớt, kiểm tra rỗng trước khi gọi API) — với dùng thật, nên cân nhắc thêm: cảnh báo khi diff bị cắt bớt (đừng âm thầm bỏ qua phần cuối), hoặc chia nhỏ theo từng file nếu cần tóm tắt đầy đủ diff lớn.
🧭 Góc nhìn Solutions Architect
Một script 40 dòng gọi API trực tiếp là điểm khởi đầu hợp lý — nhưng khi nào nó cần trở thành một service có thể gọi lại, có log, có giới hạn chi phí per-user? Nếu ba người trong team đều tự viết ba bảnpr-summarizerkhác nhau, vấn đề không còn là kỹ thuật viết CLI nữa mà là thiếu một nơi tập trung để chia sẻ tool nội bộ — bạn có đang âm thầm cần một "internal tool registry" không, hay ba bản khác nhau vẫn ổn ở quy mô hiện tại?
🔗 Bài viết liên quan
- Advanced Prompting: Chain-of-Thought & ReAct — kỹ thuật xây prompt template có cấu trúc, nền tảng cho phần prompt trong mọi CLI tool ở bài này.
- Security & Code Scanning: AI phát hiện lỗ hổng OWASP — một ví dụ khác về prompt có cấu trúc, có thể đóng gói thành chính CLI tool tương tự
pr-summarizer.
Kết Season 1
Hành trình 10 bài đi từ việc cấu hình một môi trường dev AI-native (Bài 1), qua context engineering để AI hiểu đúng codebase (Bài 2), prompting có cấu trúc (Bài 3), debug và refactor có AI hỗ trợ (Bài 4-5), làm việc với legacy code và viết test tự động (Bài 6-7), review bảo mật và thiết kế schema (Bài 8-9), và giờ là bước tự xây công cụ riêng thay vì chỉ dùng công cụ có sẵn (Bài 10). Điểm chung xuyên suốt: AI không thay thế tư duy kỹ thuật, nó thay đổi điểm chi phí thấp nhất để áp dụng tư duy đó — sớm hơn, rẻ hơn, lặp lại được nhiều hơn.
Nhưng một CLI tool gọi API một lần, ăn input cố định, trả output rồi kết thúc — vẫn là công cụ, không phải hệ thống. Câu hỏi tự nhiên tiếp theo: điều gì xảy ra khi công cụ cần tự quyết định bước tiếp theo dựa trên kết quả bước trước, khi nhiều AI cần phối hợp với nhau, khi hệ thống cần được thiết kế ở tầng kiến trúc chứ không chỉ tầng script? Đó là nội dung của Season 2 — AI-Augmented Architecture & System Design: nâng tầm tư duy từ "viết code với AI" lên "thiết kế hệ thống có AI là một thành phần kiến trúc".
Bài trước: AI for Database Schema Design