Phần 1 của series đã build agent harness cơ bản nhất: kết nối model, nhận input user, lưu context conversation, loop chạy liên tục. Agent đó không hữu ích lắm — chỉ trả lời dựa trên kiến thức bên trong. Để agent làm được việc thật, cần cho nó cách hành động trong môi trường. Đó là tools.
Tool là gì
Tool là program hoặc function bạn expose cho LLM để nó tự gọi. Tool có thể đơn giản như một function Python nằm cùng code agent, hoặc phức tạp như MCP server gọi HTTP API đọc/ghi database. (MCP không cover trong phần này, sẽ có ở phần sau.)
Agent dùng tool thế nào
LLM output text, làm sao gọi tool? Implementation đầu tiên dùng cách suggest LLM output text dạng Action: web_fetch rồi agent harness parse và chạy function. Không tin cậy lắm vì model đôi khi không tuân format.
LLM hiện đại có native tool calling — fine-tuned để output JSON structured tool request. Implementation native này có validation sẵn, giảm hallucination và tăng độ tin cậy khi gọi tool.
Implement 6 tool cơ bản
Trong code Python, tạo submodule tools chứa tất cả tool và schema của chúng.
Tool 1: bash
def run_bash(command: str) -> str:
"""Run a bash command and return its output."""
result = subprocess.run(
command, shell=True, text=True, capture_output=True
)
output = result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
return output or "(no output)"
Tool mạnh nhất. Cho agent chạy bash command nghĩa là cho nó làm bất cứ gì trên máy. Đỡ phải tự viết tool riêng cho mỗi chương trình LLM đã biết — nhưng cũng là tool nguy hiểm nhất. Phần sau sẽ làm chặt security.
Tool 2: read_file
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
"""Read lines from a file, with optional offset and limit."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
lines = p.read_text(errors="replace").splitlines()
selected = lines[offset - 1: offset - 1 + limit]
return "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
Đọc file trên máy. Hữu ích cho coding agent đọc codebase.
Tool 3: glob_files
def glob_files(pattern: str, path: str = ".") -> str:
"""Find files matching a glob pattern inside a directory."""
matches = glob_module.glob(f"{path}/**/{pattern}", recursive=True)
matches += glob_module.glob(f"{path}/{pattern}")
unique = sorted(set(matches))
return "\n".join(unique) if unique else "(no matches)"
Tìm file theo pattern. Cần để agent explore và xem file nào có trước khi đọc.
Tool 4: grep
def grep(pattern: str, path: str = ".", include: str = "*") -> str:
"""Search file contents for a regex pattern, optionally filtering by filename glob."""
results = []
for filepath in glob_module.glob(f"{path}/**/{include}", recursive=True):
fp = Path(filepath)
if not fp.is_file():
continue
try:
for i, line in enumerate(fp.read_text(errors="replace").splitlines(), 1):
if re.search(pattern, line):
results.append(f"{filepath}:{i}: {line}")
except OSError:
pass
return "\n".join(results) if results else "(no matches)"
Search content bằng regex, return matching line với file path và line number. Bổ trợ glob_files: đầu tiên tìm file nào tồn tại, sau đó search bên trong. Tham số include giới hạn search theo filename pattern — tránh search binary hoặc tập trung vào một ngôn ngữ.
Tool 5: write_file
def write_file(path: str, content: str) -> str:
"""Write content to a file, creating it if it does not exist."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
Tạo file mới và ghi nội dung. Tự tạo parent directory nếu chưa tồn tại — agent không phải lo về directory structure. Cần cho bất cứ agent nào sinh output, sinh code, hoặc save kết quả.
Tool 6: edit_file
def edit_file(path: str, old_string: str, new_string: str) -> str:
"""Replace the first occurrence of old_string with new_string in a file."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
original = p.read_text()
if old_string not in original:
return f"Error: string not found in {path}"
p.write_text(original.replace(old_string, new_string, 1))
return f"Edited {path}"
Khác write_file ở chỗ replace từng đoạn string thay vì ghi đè cả file. An toàn hơn nhiều khi agent chỉ cần sửa nhỏ trong file đã có, tránh overwrite content chưa đọc. Go-to tool cho coding agent patch line cụ thể.
Tool 7: webfetch (bonus)
def webfetch(url: str) -> str:
"""Fetch a URL and return its full plain-text content (up to 2 MB)."""
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return f"Error fetching {url}: unsupported scheme '{parsed.scheme}'."
req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
raw = b"".join(...).decode(charset, errors="replace")
soup = BeautifulSoup(raw, "html.parser")
text = soup.get_text(separator="\n", strip=True)
return re.sub(r"\n{3,}", "\n\n", text).strip()
Fetch web page và return plain text. BeautifulSoup strip HTML markup nên model chỉ nhận text đọc được — context sạch, token-efficient. Giới hạn http/https và cap 2 MB để không flood context window.
Tool schema cho native tool calling
Sau khi implement tool, phải cho agent biết chúng tồn tại. Agent cũng cần biết mỗi tool làm gì và nhận parameter nào. Tool schema là cách định nghĩa cho model:
def get_tool_schemas():
return [
{
"type": "function",
"function": {
"name": "run_bash",
"description": "Run a bash command on the user's machine and return the output.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute.",
}
},
"required": ["command"],
},
},
},
# ... schema cho 6 tool còn lại theo cùng pattern
]
Mỗi schema gồm tên tool, description, parameter types và list required.
Agent loop xử lý tool_calls
def handle_tool_calls(tool_calls, messages):
for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f" [tool] {name}({args})")
if name not in TOOL_REGISTRY:
result = f"Error: unknown tool '{name}'"
else:
result = TOOL_REGISTRY[name](**args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
def agent_loop(client):
messages = [{
"role": "system",
"content": "You are a helpful assistant with tools to read/write files, search the file system, and fetch web pages."
}]
while True:
user_input = input("You: ")
if user_input.lower() == "\\exit":
break
messages.append({"role": "user", "content": user_input})
while True:
response = client.chat.completions.create(
model="gemma4",
messages=messages,
tools=TOOL_SCHEMAS,
temperature=0.7,
)
message = response.choices[0].message
messages.append(message)
if message.tool_calls:
handle_tool_calls(message.tool_calls, messages)
else:
print(f"Assistant: {message.content}")
break
Full code có ở GitHub repo của series.
Test thử
Với 7 tool này, agent đã xử lý được task như fetch web page rồi ghi markdown file:
$ python agent.py
You: Read the frontpage of ruxu.dev and list all the articles in a markdown file ruxu.md
[tool] webfetch({'url': 'https://ruxu.dev'})
[tool result] Blog | Roger Oriol ...
[tool] write_file({'path': 'ruxu.md', 'content': '# Articles on ruxu.dev\n\n- Build a Basic AI Agent From Scratch\n...'})
[tool result] Wrote 375 bytes to ruxu.md
Assistant: Done — I created `ruxu.md` with the article list from the front page of ruxu.dev.
Đã build được gì
Giờ bạn có tool-calling agent đã khá mạnh. Có thể dùng nó như coding agent hoặc assistant và work thật. Vẫn thiếu nhiều feature so với Claude Code hay Hermes Agent, nhưng đang dần đến đó.
Phần tiếp theo
Dùng agent hiện tại một lúc sẽ thấy nó dùng tool không có plan dài hạn, hay đứt giữa task phức tạp. Phần tiếp theo sẽ bổ sung planning và task management tool để agent xử lý được long-running task.