Xây dựng coding agent tối giản: Bài học thực tế

Nghe bài viết:

Những điều tôi học được khi xây dựng một coding agent tối giản và có quan điểm rõ ràng

Trong ba năm qua, tôi đã sử dụng LLM để hỗ trợ lập trình. Nếu bạn đang đọc bài này, có lẽ bạn cũng đã trải qua quá trình tiến hóa tương tự: từ việc copy & paste code vào ChatGPT, sang auto-complete của Copilot (vốn chưa bao giờ thực sự hiệu quả với tôi), rồi đến Cursor, và cuối cùng là thế hệ coding agent mới như Claude Code, Codex, Amp, Droid và opencode – những công cụ đã trở thành “daily driver” của chúng ta trong năm 2025.

Tôi dùng Claude Code cho phần lớn công việc. Đó là công cụ đầu tiên tôi thử vào tháng 4 sau khoảng một năm rưỡi dùng Cursor. Khi đó nó còn khá cơ bản – điều này cực kỳ hợp với workflow của tôi, vì tôi thích công cụ đơn giản và dễ đoán. Nhưng vài tháng gần đây, Claude Code đã biến thành một con tàu vũ trụ với 80% tính năng tôi không bao giờ dùng đến. System prompt và tool cũng thay đổi mỗi lần release, làm vỡ workflow và thay đổi hành vi model. Tôi ghét điều đó. Và nó còn bị flicker nữa.

Tôi cũng đã xây dựng khá nhiều agent qua nhiều năm với mức độ phức tạp khác nhau. Ví dụ, Sitegeist – agent dùng browser của tôi – về cơ bản là một coding agent sống trong trình duyệt. Qua quá trình đó, tôi nhận ra rằng context engineering là yếu tố tối quan trọng. Kiểm soát chính xác những gì được đưa vào context của model giúp đầu ra tốt hơn, đặc biệt khi viết code. Các harness hiện tại khiến việc này cực kỳ khó hoặc bất khả thi, vì chúng lén inject những thứ bạn không hề thấy trong UI.

Tôi muốn kiểm tra mọi khía cạnh của tương tác với model. Gần như không có harness nào cho phép điều đó. Tôi cũng muốn một định dạng session được tài liệu hóa rõ ràng để có thể xử lý hậu kỳ tự động, và một cách đơn giản để xây dựng UI khác trên lõi agent. Dù một phần có thể làm được với các giải pháp hiện tại, API của chúng giống như sản phẩm của quá trình tiến hóa tự nhiên – tích tụ “hành lý” theo thời gian, và điều đó thể hiện rõ trong trải nghiệm developer.

Tôi cũng thử self-host, cả local lẫn trên DataCrunch. Một số harness như opencode có hỗ trợ self-hosted model, nhưng thường không hoạt động tốt. Phần lớn do chúng phụ thuộc vào thư viện như Vercel AI SDK – vốn không thân thiện với self-hosted model, đặc biệt trong phần tool calling.

cl-260219020121

Vậy một ông già đang la hét với Claude sẽ làm gì? Tự viết harness coding agent của riêng mình và đặt cho nó một cái tên không thể tìm kiếm trên Google, để chẳng bao giờ có người dùng. Đồng nghĩa với việc không bao giờ có issue trên GitHub. Nghe hợp lý mà.

Để làm được điều này, tôi xây dựng:

  • pi-ai: API LLM thống nhất, hỗ trợ nhiều provider (Anthropic, OpenAI, Google, xAI, Groq, Cerebras, OpenRouter…), streaming, tool calling với TypeBox schema, reasoning, chuyển context giữa provider, tracking token và chi phí.
  • pi-agent-core: Vòng lặp agent xử lý thực thi tool, validation và streaming event.
  • pi-tui: Framework TUI tối giản với differential rendering, synchronized output để giảm flicker, editor có autocomplete và markdown rendering.
  • pi-coding-agent: CLI kết nối mọi thứ với quản lý session, custom tool, theme và file context dự án.

Triết lý của tôi rất đơn giản: nếu tôi không cần, tôi sẽ không xây. Và tôi không cần quá nhiều thứ.

pi-ai và pi-agent-core

Tôi không đi sâu vào API chi tiết ở đây. Thay vào đó, tôi muốn nói về những vấn đề khi xây dựng một unified LLM API và cách tôi xử lý chúng.

Chỉ có bốn API thực sự quan trọng

Bạn thực ra chỉ cần nói chuyện với bốn API để tương tác với hầu hết provider LLM:

  • OpenAI Completions API
  • OpenAI Responses API
  • Anthropic Messages API
  • Google Generative AI API

Chúng khá giống nhau về tính năng, nhưng mỗi provider có “dị biệt” riêng. Ví dụ:

  • Một số provider không thích field store
  • Một số dùng max_tokens thay vì max_completion_tokens
  • Grok không thích reasoning_effort
  • Field reasoning trả về khác nhau giữa các provider

Token tracking và cache reporting thì đúng kiểu “miền Viễn Tây”. Có provider chỉ trả token ở cuối stream, khiến việc tính chi phí chính xác gần như bất khả thi nếu request bị abort.

pi-ai xử lý token và cache tracking theo kiểu best-effort – đủ dùng cho cá nhân, nhưng không nên dùng cho billing chính xác.

Chuyển context giữa provider

pi-ai hỗ trợ chuyển context giữa provider ngay từ đầu. Ví dụ bạn bắt đầu với Claude, rồi chuyển sang GPT, rồi sang Gemini trong cùng session. Các thinking trace sẽ được convert thành block khi cần.

Context có thể serialize thành JSON và khôi phục sau này với bất kỳ model nào.

Thế giới đa model

Tôi xây dựng registry model để có thể định nghĩa model một cách type-safe trong TypeScript. Dữ liệu được parse từ OpenRouter và models.dev.

pi-ai cũng hỗ trợ abort request và trả về partial result – điều mà nhiều unified LLM API không làm được.

Tách kết quả tool cho LLM và UI

Tool trong pi-ai có thể trả về hai phần:

  • Phần cho LLM (text/JSON)
  • Phần cho UI (structured data)

Tool cũng có thể trả về ảnh ở định dạng native của provider. Argument được validate bằng TypeBox và AJV.

Agent loop tối giản

pi-ai có agent loop xử lý orchestration đầy đủ: nhận message, thực thi tool, feed lại vào model, lặp cho đến khi model hoàn tất.

Tôi không thêm max steps hay giới hạn phức tạp khác. Agent loop chỉ chạy cho đến khi model nói “xong”.

pi-tui

Tôi lớn lên trong thời DOS, nên TUI là thứ quen thuộc với tôi. pi-tui sử dụng cách tiếp cận retained mode UI và differential rendering – chỉ redraw những dòng thay đổi.

Thuật toán rất đơn giản:

  1. Render lần đầu: in toàn bộ
  2. Nếu thay đổi chiều rộng: clear và render lại
  3. Bình thường: tìm dòng thay đổi đầu tiên và redraw từ đó

Để giảm flicker, pi-tui dùng synchronized output escape sequence.

pi-coding-agent

pi có đầy đủ các tính năng cơ bản của coding agent:

  • Chạy trên Windows, Linux, macOS
  • Hỗ trợ multi-provider
  • Quản lý session (resume, branch)
  • AGENTS.md cho context dự án
  • Slash command
  • Theme tùy chỉnh
  • Tracking token và chi phí

System prompt tối giản

System prompt của pi dưới 1000 token. Không có 10.000 token hướng dẫn phức tạp như các harness khác. Frontier model hiện tại đã đủ được RL-train để hiểu coding agent là gì.

Chỉ bốn tool

  • read
  • write
  • edit
  • bash

Vậy là đủ.

YOLO mặc định

pi chạy full YOLO mode: không permission prompt, không guardrail. Agent có toàn quyền filesystem và bash.

Nếu agent đã có thể đọc file và chạy code, thì security theater cũng chẳng giúp gì nhiều. Muốn an toàn, hãy chạy trong container.

Không có to-do built-in

Thay vì built-in task system, chỉ cần một file TODO.md. Agent có thể đọc và cập nhật.

Không có plan mode

Nếu cần kế hoạch dài hạn, viết vào PLAN.md. File-based plan giúp version control và observability tốt hơn.

Không có MCP

MCP server thường dump hàng chục nghìn token vào context mỗi session. Tôi chọn cách dùng CLI tool + README thay vì MCP.

Không background bash

Dùng tmux thay vì background bash phức tạp. Quan sát tốt hơn, đơn giản hơn.

Không sub-agent built-in

Nếu cần sub-agent, spawn chính nó qua bash. Tốt nhất là tách session riêng cho context gathering.

Benchmark

Tôi chạy Terminal-Bench 2.0 với Claude Opus 4.5. pi cạnh tranh với Codex, Cursor, Windsurf và các harness khác.

Kết quả cho thấy một approach tối giản có thể cạnh tranh ngang ngửa các harness phức tạp.

Tổng kết

Benchmark chỉ là con số. Bằng chứng thật sự là công việc hằng ngày của tôi. pi cho tôi kiểm soát hoàn toàn context và workflow.

Tôi vẫn muốn thêm một vài tính năng như compaction hay tool result streaming, nhưng nhìn chung tôi hài lòng.

Nếu pi không phù hợp với bạn, hãy fork nó. Nếu bạn làm thứ gì đó tốt hơn, tôi sẵn sàng tham gia.

Một số bài học ở đây có thể áp dụng cho các harness khác. Hãy thử và tự rút ra kết luận của bạn.