Chuyển đến nội dung
tinAI
Quay lại

Build A Basic AI Agent From Scratch: Tools — 6 tool cơ bản cho coding agent bằng Python thuần

Bản dịch tiếng Việt của tinAI · Từ Build A Basic AI Agent From Scratch: Tools (ruxu.dev) · Ngày gốc: · Dịch ngày:

Bài gốc: Build A Basic AI Agent From Scratch: Tools (ruxu.dev)

Tác giả: Roger Oriol

Ngày đăng: Dịch ngày:

TL;DR

Phần 2 của series build AI agent từ đầu — implement 6 tool cơ bản (bash, read_file, glob_files, grep, write_file, edit_file, webfetch) bằng Python thuần với subprocess/glob/pathlib, không dùng MCP hay framework. Bài cũng show tool schema cho native tool calling và agent loop xử lý tool_calls — sau khi đọc xong bạn hiểu chính xác Claude Code/Cursor đang gọi gì dưới capot.

Ước tính đọc: 4 phút

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.


Đường dẫn nguồn

tinAI dịch bài này sang tiếng Việt từ Build A Basic AI Agent From Scratch: Tools (ruxu.dev) · Loại nguồn: web và giữ bối cảnh từ bản tin tinAI đã giới thiệu bài này .

Bản tin này có 3 bài dịch liên quan từ cùng bản tin.