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ợ viết code. 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-completion của Copilot (thứ 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 thích Claude Code cho phần lớn công việc của mình. Đó là công cụ đầu tiên tôi thử vào tháng 4 năm ngoái sau khi dùng Cursor suốt một năm rưỡi. Khi đó, nó còn rất cơ bản. Điều đó hoàn toàn phù hợp với workflow của tôi, vì tôi là một người đơn giản, thích công cụ đơn giản và dễ đoán. Nhưng trong 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 mà tôi không bao giờ cần đến. System prompt và các tool của nó cũng thay đổi ở mỗi bản phát hành, làm vỡ workflow của tôi và thay đổi hành vi của model. Tôi ghét điều đó. Và nó còn bị nhấp nháy.
Tôi cũng đã xây dựng nhiều agent trong những năm qua, với mức độ phức tạp khác nhau. Ví dụ, Sitegeist — agent nhỏ chạy trong browser của tôi — về cơ bản cũng là một coding agent sống trong trình duyệt. Trong suốt quá trình đó, tôi nhận ra rằng context engineering là yếu tố tối quan trọng. Việc kiểm soát chính xác những gì được đưa vào context của model giúp tạo ra output tốt hơn, đặc biệt là khi model viết code. Các harness hiện tại khiến việc này cực kỳ khó hoặc thậm chí là không thể, vì chúng âm thầm chèn thêm nội dung vào context mà bạn không hề thấy trong UI.
Nói về tính minh bạch, tôi muốn có thể kiểm tra mọi khía cạnh của tương tác giữa tôi và model. Hầu 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 thay thế trên lõi agent. Một số harness cho phép làm được phần nào, nhưng API của chúng trông như được tiến hóa hữu cơ qua nhiều năm. Những giải pháp đó tích lũy “hành lý” theo thời gian, và điều đó thể hiện rõ trong developer experience. Tôi không trách ai cả. Khi hàng nghìn người sử dụng sản phẩm của bạn và bạn phải duy trì backward compatibility, đó là cái giá phải trả.
Tôi cũng từng thử self-host, cả local lẫn trên DataCrunch. Một số harness như opencode có hỗ trợ model tự host, nhưng thường hoạt động không tốt. Chủ yếu vì chúng phụ thuộc vào các thư viện như Vercel AI SDK, thứ dường như không tương thích tốt với self-hosted model, đặc biệt là khi xử lý tool calling.
Vậy một ông già đang la hét vào Claudes sẽ làm gì? Ông ta sẽ tự viết một coding agent harness cho riêng mình, và đặt cho nó một cái tên không thể tìm thấy trên Google, để không bao giờ có người dùng. Đồng nghĩa với việc không bao giờ có issue trên GitHub. Khó đến mức nào cơ chứ?

Để làm được điều này, tôi cần xây dựng:
- pi-ai: Một unified LLM API hỗ trợ đa nhà cung cấp (Anthropic, OpenAI, Google, xAI, Groq, Cerebras, OpenRouter và mọi endpoint tương thích OpenAI), hỗ trợ streaming, tool calling với schema TypeBox, reasoning/thinking, handoff context giữa provider, tracking token và chi phí.
- pi-agent-core: Agent loop xử lý thực thi tool, validation và streaming sự kiện.
- pi-tui: Framework TUI tối giản với differential rendering, synchronized output để giảm flicker, và các component như editor có autocomplete và markdown rendering.
- pi-coding-agent: CLI thực tế kết nối tất cả lại với nhau, bao gồm quản lý session, custom tool, theme và project context file.
Triết lý của tôi rất đơn giản: nếu tôi không cần nó, tôi sẽ không xây nó. Và tôi không cần quá nhiều thứ.
pi-ai và pi-agent-core
Tôi sẽ không làm bạn chán với chi tiết API cụ thể. Bạn có thể đọc trong README.md. Thay vào đó, tôi muốn ghi lại những vấn đề tôi gặp phải khi xây dựng một unified LLM API và cách tôi giải quyết chúng. Tôi không khẳng định đây là giải pháp tốt nhất, nhưng nó đã hoạt động khá ổn trong nhiều dự án agentic và non-agentic của tôi.
Có. Tận. Bốn. API
Thực tế chỉ có bốn API bạn cần để nói chuyện với gần như mọi LLM provider: OpenAI Completions API, OpenAI Responses API, Anthropic Messages API và Google Generative AI API.
Chúng khá giống nhau về tính năng, nên việc xây abstraction bên trên không phải khoa học tên lửa. Tuy nhiên, mỗi provider có những đặc thù riêng. Điều này đặc biệt đúng với Completions API — gần như mọi provider đều nói “tiếng OpenAI”, nhưng mỗi người hiểu khác nhau về API đó.
Ví dụ: OpenAI không hỗ trợ reasoning traces trong Completions API của họ, nhưng các provider khác thì có. Điều này cũng đúng với các inference engine như llama.cpp, Ollama, vLLM và LM Studio.
Ví dụ trong openai-completions.ts:
- Cerebras, xAI, Mistral và Chutes không thích field
store - Mistral và Chutes dùng
max_tokensthay vìmax_completion_tokens - Cerebras, xAI, Mistral và Chutes không hỗ trợ role
developercho system prompt - Model Grok không thích
reasoning_effort - Mỗi provider trả reasoning content ở field khác nhau (
reasoning_contentvsreasoning)
Để đảm bảo mọi tính năng hoạt động xuyên suốt các provider, pi-ai có một bộ test khá đầy đủ, bao gồm image input, reasoning trace, tool calling và các tính năng phổ biến khác. Test được chạy trên tất cả provider và model phổ biến. Dù vậy, không có gì đảm bảo model hoặc provider mới sẽ hoạt động trơn tru ngay từ đầu.
Một khác biệt lớn khác là cách các provider báo cáo token và cache read/write. Anthropic có cách tiếp cận hợp lý nhất, còn lại thì giống miền Viễn Tây. Một số provider báo token count ngay khi bắt đầu stream SSE, một số chỉ báo khi kết thúc — khiến việc tracking chi phí trở nên không chính xác nếu request bị hủy giữa chừng.
Thêm vào đó, bạn không thể cung cấp một unique ID để correlate với billing API của họ nhằm biết user nào đã tiêu tốn bao nhiêu token. Vì vậy, pi-ai chỉ tracking token và cache theo kiểu “best effort”. Đủ cho sử dụng cá nhân, nhưng không đủ cho billing chính xác nếu bạn có user cuối tiêu thụ token qua service của mình.
Đặc biệt cảm ơn Google vì đến thời điểm này vẫn không hỗ trợ streaming tool call. Rất Google.
pi-ai cũng hoạt động được trong browser, hữu ích khi xây dựng web interface. Một số provider hỗ trợ CORS rất tốt, đặc biệt là Anthropic và xAI.
Chuyển giao context giữa các provider
Context handoff giữa các provider là một tính năng mà pi-ai được thiết kế ngay từ đầu. Vì mỗi provider có cách riêng để theo dõi tool call và thinking trace, nên việc này chỉ có thể thực hiện ở mức “best effort”.
Ví dụ, nếu bạn chuyển từ Anthropic sang OpenAI giữa một session, các thinking trace của Anthropic sẽ được chuyển thành content block trong assistant message, được bao bởi thẻ . Điều này có hợp lý hay không thì còn tùy, vì thinking trace mà Anthropic và OpenAI trả về thực ra không phản ánh chính xác những gì đang diễn ra phía sau hậu trường.
Các provider cũng chèn các blob đã được ký vào event stream mà bạn phải phát lại trong các request tiếp theo nếu chứa cùng message. Điều này cũng áp dụng khi bạn đổi model trong cùng một provider. Kết quả là abstraction và pipeline chuyển đổi phía sau trở nên khá phức tạp.
Tuy vậy, tôi khá hài lòng khi báo cáo rằng context handoff xuyên provider cũng như serialize/deserialize context hoạt động khá tốt trong pi-ai:
import { getModel, complete, Context } from '@mariozechner/pi-ai';
// Start with Claude
const claude = getModel('anthropic', 'claude-sonnet-4-5');
const context: Context = {
messages: []
};
context.messages.push({ role: 'user', content: 'What is 25 * 18?' });
const claudeResponse = await complete(claude, context, {
thinkingEnabled: true
});
context.messages.push(claudeResponse);
// Switch to GPT - it will see Claude's thinking as tagged text
const gpt = getModel('openai', 'gpt-5.1-codex');
context.messages.push({ role: 'user', content: 'Is that correct?' });
const gptResponse = await complete(gpt, context);
context.messages.push(gptResponse);
// Switch to Gemini
const gemini = getModel('google', 'gemini-2.5-flash');
context.messages.push({ role: 'user', content: 'What was the question?' });
const geminiResponse = await complete(gemini, context);
// Serialize context to JSON (for storage, transfer, etc.)
const serialized = JSON.stringify(context);
// Later: deserialize and continue with any model
const restored: Context = JSON.parse(serialized);
restored.messages.push({ role: 'user', content: 'Summarize our conversation' });
const continuation = await complete(claude, restored);
Chúng ta đang sống trong một thế giới đa model
Nói đến model, tôi muốn có một cách typesafe để chỉ định model khi gọi getModel. Vì vậy tôi cần một model registry có thể chuyển thành TypeScript type.
Tôi parse dữ liệu từ OpenRouter và models.dev (được tạo bởi team opencode — cảm ơn họ vì điều đó, rất hữu ích) vào file models.generated.ts. Dữ liệu này bao gồm chi phí token và các capability như hỗ trợ image input hoặc thinking.
Nếu tôi cần thêm model chưa có trong registry, tôi muốn type system giúp việc tạo model mới dễ dàng. Điều này đặc biệt hữu ích khi làm việc với model self-hosted, bản phát hành mới chưa có trên models.dev hoặc OpenRouter, hoặc thử nghiệm provider ít người biết:
import { Model, stream } from '@mariozechner/pi-ai';
const ollamaModel: Model<'openai-completions'> = {
id: 'llama-3.1-8b',
name: 'Llama 3.1 8B (Ollama)',
api: 'openai-completions',
provider: 'ollama',
baseUrl: 'http://localhost:11434/v1',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000
};
const response = await stream(ollamaModel, context, {
apiKey: 'dummy' // Ollama doesn't need a real key
});
Nhiều unified LLM API hoàn toàn bỏ qua khả năng hủy request (abort). Điều này là không thể chấp nhận được nếu bạn muốn tích hợp LLM vào production system. Nhiều API cũng không trả về partial result — điều này thật vô lý.
pi-ai được thiết kế từ đầu để hỗ trợ abort xuyên suốt toàn bộ pipeline, bao gồm cả tool call. Ví dụ:
import { getModel, stream } from '@mariozechner/pi-ai';
const model = getModel('openai', 'gpt-5.1-codex');
const controller = new AbortController();
// Abort after 2 seconds
setTimeout(() => controller.abort(), 2000);
const s = stream(model, {
messages: [{ role: 'user', content: 'Write a long story' }]
}, {
signal: controller.signal
});
for await (const event of s) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta);
} else if (event.type === 'error') {
console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
}
}
// Get results (may be partial if aborted)
const response = await s.result();
if (response.stopReason === 'aborted') {
console.log('Partial content:', response.content);
}
Tách kết quả tool thành hai phần có cấu trúc
Một abstraction khác mà tôi chưa thấy ở unified LLM API nào là việc tách tool result thành hai phần: một phần cho LLM và một phần cho UI.
Phần dành cho LLM thường chỉ là text hoặc JSON, nhưng điều đó không đủ cho UI hiển thị đầy đủ. Việc parse output text của tool để cấu trúc lại cho UI là cực kỳ tệ.
pi-ai cho phép tool trả về content block cho LLM và block riêng cho UI render. Tool cũng có thể trả attachment như image theo format native của provider. Tool argument được validate tự động bằng TypeBox schema và AJV, với error message chi tiết khi validate thất bại:
import { Type, AgentTool } from '@mariozechner/pi-ai';
const weatherSchema = Type.Object({
city: Type.String({ minLength: 1 }),
});
const weatherTool: AgentTool = {
name: 'get_weather',
description: 'Get current weather for a city',
parameters: weatherSchema,
execute: async (toolCallId, args) => {
const temp = Math.round(Math.random() * 30);
return {
// Text for the LLM
output: `Temperature in ${args.city}: ${temp}°C`,
// Structured data for the UI
details: { temp }
};
}
};
// Tools can also return images
const chartTool: AgentTool = {
name: 'generate_chart',
description: 'Generate a chart from data',
parameters: Type.Object({ data: Type.Array(Type.Number()) }),
execute: async (toolCallId, args) => {
const chartImage = await generateChartImage(args.data);
return {
content: [
{ type: 'text', text: `Generated chart with ${args.data.length} data points` },
{ type: 'image', data: chartImage.toString('base64'), mimeType: 'image/png' }
]
};
}
};
Điểm còn thiếu hiện tại là streaming tool result. Ví dụ như bash tool mà bạn muốn hiển thị ANSI sequence theo thời gian thực — hiện chưa hỗ trợ, nhưng có thể bổ sung khá dễ.
Việc parse JSON từng phần khi tool call đang stream là rất quan trọng cho UX tốt. Khi LLM stream argument của tool, pi-ai parse dần để bạn có thể hiển thị partial result trong UI trước khi tool call hoàn tất. Ví dụ: hiển thị diff đang stream khi agent rewrite file.
Khung agent tối giản
Cuối cùng, pi-ai cung cấp một agent loop xử lý toàn bộ orchestration: xử lý user message, thực thi tool call, feed kết quả lại cho LLM, lặp lại cho đến khi model trả response không còn tool call.
Loop cũng hỗ trợ message queue thông qua callback: sau mỗi lượt, nó hỏi có message nào đang chờ không và inject vào trước response tiếp theo. Loop emit event cho mọi thứ, giúp dễ dàng xây reactive UI.
Agent loop không cho phép cấu hình max step hay những “knob” tương tự. Tôi chưa từng thấy use case thực tế cho điều đó, vậy tại sao phải thêm?
Loop sẽ lặp cho đến khi agent nói “xong”.
Trên loop này, pi-agent-core cung cấp class Agent với những thứ thực sự hữu ích: state management, subscription event đơn giản, message queue với hai chế độ (từng cái một hoặc tất cả cùng lúc), xử lý attachment (image, document), và abstraction transport cho phép chạy agent trực tiếp hoặc qua proxy.
Tôi có hài lòng với pi-ai không? Phần lớn là có. Như mọi unified API khác, abstraction không bao giờ hoàn hảo. Nhưng nó đã được dùng trong bảy dự án production khác nhau và phục vụ tôi cực kỳ tốt.
Tại sao không dùng Vercel AI SDK? Blog của Armin phản ánh chính xác trải nghiệm của tôi. Xây dựng trực tiếp trên SDK của provider cho tôi toàn quyền kiểm soát và thiết kế API theo ý mình, với surface area nhỏ hơn nhiều. Hãy đọc blog của Armin nếu bạn muốn phân tích sâu hơn.
pi-tui
Tôi lớn lên trong thời DOS, nên terminal user interface là thứ gắn liền với tuổi thơ tôi. Từ những chương trình setup hào nhoáng của Doom cho đến các sản phẩm của Borland, TUI đã đồng hành với tôi đến cuối những năm 90. Và tôi đã cực kỳ hạnh phúc khi cuối cùng chuyển sang hệ điều hành GUI.
TUI có tính di động cao và dễ stream, nhưng mật độ thông tin thì khá tệ. Dù vậy, tôi nghĩ bắt đầu với terminal UI cho pi là hợp lý nhất. Sau này nếu cần, tôi có thể xây GUI.
Tại sao lại tự viết TUI framework? Tôi đã xem qua Ink, Blessed, OpenTUI và nhiều thứ khác. Chắc chúng đều ổn theo cách riêng, nhưng tôi không muốn viết TUI như một ứng dụng React. Blessed gần như không còn được maintain, còn OpenTUI thì rõ ràng chưa production-ready. Viết framework riêng trên Node.js nghe như một thử thách thú vị.
Hai kiểu TUI
Viết terminal UI không quá khó, nhưng bạn phải chọn “thuốc độc” của mình. Có hai cách chính:
Cách thứ nhất là chiếm toàn bộ viewport terminal và xem nó như một pixel buffer. Thay vì pixel, bạn có cell chứa ký tự với màu nền, màu chữ và style như italic, bold. Tôi gọi đây là full screen TUI. Amp và opencode dùng cách này.
Nhược điểm: bạn mất scrollback buffer, phải tự implement search. Mất scroll tự nhiên, phải mô phỏng scroll trong viewport. Không khó, nhưng bạn phải tái hiện mọi thứ mà terminal emulator đã làm sẵn. Scroll bằng chuột thường cho cảm giác không tự nhiên.
Cách thứ hai là viết vào terminal như CLI bình thường, append nội dung vào scrollback buffer, chỉ thỉnh thoảng di chuyển “rendering cursor” lên một chút để redraw spinner hoặc text editor. Claude Code, Codex và Droid dùng cách này.
Coding agent có tính chất rất phù hợp với cách thứ hai: giao diện chat tuyến tính. User viết prompt, agent trả lời, tool call, kết quả tool. Tất cả tuyến tính. Điều này phù hợp với terminal “native”. Bạn được sử dụng scroll tự nhiên và search trong scrollback buffer. Nó cũng giới hạn khả năng của TUI ở mức vừa đủ — điều tôi thấy quyến rũ vì constraint tạo ra chương trình tối giản, không thừa thãi. Đây là hướng tôi chọn cho pi-tui.
Retained mode UI
Nếu bạn từng làm GUI, bạn biết retained mode vs immediate mode. Retained mode xây cây component tồn tại xuyên frame, mỗi component biết cách render và có thể cache output. Immediate mode redraw toàn bộ mỗi frame.
pi-tui dùng retained mode đơn giản. Một Component là object có method render(width) trả về mảng string (mỗi dòng vừa viewport ngang, chứa ANSI escape code cho màu và style) và tùy chọn handleInput(data) cho keyboard input.
Một Container giữ danh sách component xếp dọc và thu thập tất cả dòng render. Class TUI là container cấp cao điều phối mọi thứ.
Khi TUI cần update màn hình, nó yêu cầu mỗi component render. Component có thể cache output: một assistant message đã stream xong không cần parse markdown lại. Container thu thập tất cả dòng từ con. TUI so sánh với buffer trước đó và chỉ redraw phần thay đổi.
Differential rendering
Dưới đây là demo đơn giản minh họa phần nào được redraw.
Thuật toán đơn giản:
- Lần render đầu: In toàn bộ dòng
- Width thay đổi: Clear toàn màn hình và render lại
- Update bình thường: Tìm dòng đầu tiên khác, di chuyển cursor đến đó và redraw từ đó xuống cuối
Nếu dòng thay đổi nằm trên viewport (user scroll lên), phải clear toàn bộ vì terminal không cho ghi vào scrollback phía trên.
Để tránh flicker, pi-tui bọc render trong synchronized output escape sequence (CSI ?2026h và CSI ?2026l), yêu cầu terminal buffer output và hiển thị atomic. Hầu hết terminal hiện đại hỗ trợ.
Trên Ghostty hoặc iTerm2 thì không flicker. Trong VS Code thì có chút tùy thời điểm. Nhưng vẫn ít hơn Claude Code.
Việc lưu toàn bộ scrollback buffer và so sánh nhiều dòng có tốn kém không? Trên máy dưới 25 năm tuổi thì không đáng kể. Vài trăm kilobyte cho session lớn. Nhờ V8. Đổi lại tôi có programming model cực kỳ đơn giản.
pi-coding-agent
pi có các tính năng cơ bản bạn mong đợi từ coding agent:
- Chạy trên Windows, Linux, macOS
- Đa provider, đổi model giữa session
- Session management: continue, resume, branching
- Project context file (AGENTS.md) load theo thứ bậc
- Slash command
- Custom slash command bằng markdown template
- OAuth cho Claude Pro/Max
- Config model/provider qua JSON
- Theme tùy chỉnh với live reload
- Editor với fuzzy search, path completion
- Message queue
- Image support
- Export HTML session
- Headless mode qua JSON streaming
- Tracking chi phí và token
System prompt tối giản
You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
Available tools:
- read: Read file contents
- bash: Execute bash commands
- edit: Make surgical edits to files
- write: Create or overwrite files
Guidelines:
- Use bash for file operations like ls, grep, find
- Use read to examine files before editing
- Use edit for precise changes (old text must match exactly)
- Use write only for new files or complete rewrites
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
- Be concise in your responses
- Show file paths clearly when working with files
Documentation:
- Your own documentation (including custom model setup and theme creation) is at: /path/to/README.md
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.
Chỉ vậy thôi. Cuối prompt sẽ inject thêm AGENTS.md của bạn.
So với Claude Code, Codex hay opencode — prompt của pi nhỏ hơn rất nhiều (dưới 1000 token). Các frontier model hiện nay đã được RL-training quá nhiều nên chúng hiểu coding agent là gì mà không cần 10.000 token system prompt.
Toolset tối giản
read
Read the contents of a file. Supports text files and images (jpg, png,
gif, webp). Images are sent as attachments. For text files, defaults to
first 2000 lines. Use offset/limit for large files.
- path: Path to the file to read (relative or absolute)
- offset: Line number to start reading from (1-indexed)
- limit: Maximum number of lines to read
write
Write content to a file. Creates the file if it doesn't exist, overwrites
if it does. Automatically creates parent directories.
- path: Path to the file to write (relative or absolute)
- content: Content to write to the file
edit
Edit a file by replacing exact text. The oldText must match exactly
(including whitespace). Use this for precise, surgical edits.
- path: Path to the file to edit (relative or absolute)
- oldText: Exact text to find and replace (must match exactly)
- newText: New text to replace the old text with
bash
Execute a bash command in the current working directory. Returns stdout
and stderr. Optionally provide a timeout in seconds.
- command: Bash command to execute
- timeout: Timeout in seconds (optional, no default timeout)
Chỉ bốn tool là đủ cho coding agent hiệu quả.
YOLO mặc định
pi chạy ở chế độ YOLO hoàn toàn. Không permission prompt. Không guardrail. Toàn quyền filesystem. Thực tế, các biện pháp bảo mật của nhiều agent khác chỉ là “security theater”. Nếu agent có thể đọc file và chạy code, game gần như kết thúc.
Giải pháp duy nhất là chặn toàn bộ network — điều đó làm agent gần như vô dụng.
No built-in to-dos
pi không và sẽ không hỗ trợ danh sách to-do tích hợp sẵn.
Theo kinh nghiệm của tôi, to-do list thường làm mô hình rối hơn là giúp ích. Nó tạo thêm trạng thái nội bộ mà model phải theo dõi và cập nhật liên tục, từ đó mở ra thêm nhiều cơ hội để mọi thứ đi sai hướng.
Nếu bạn cần theo dõi công việc, hãy làm nó có trạng thái bên ngoài (externally stateful) bằng cách ghi ra file:
# TODO.md
- [x] Implement user authentication
- [x] Add database migrations
- [ ] Write API documentation
- [ ] Add rate limiting
Agent có thể đọc và cập nhật file này khi cần. Việc dùng checkbox giúp bạn nhìn rõ việc gì đã xong và việc gì còn lại. Đơn giản, minh bạch và nằm trong quyền kiểm soát của bạn.
No plan mode
pi không và sẽ không có plan mode tích hợp.
Chỉ cần yêu cầu agent cùng bạn suy nghĩ về vấn đề, mà không sửa file hay chạy command, thường đã đủ cho nhu cầu lập kế hoạch.
Nếu bạn cần lưu kế hoạch qua nhiều session, hãy ghi nó vào file:
# PLAN.md
## Goal
Refactor authentication system to support OAuth
## Approach
1. Research OAuth 2.0 flows
2. Design token storage schema
3. Implement authorization server endpoints
4. Update client-side login flow
5. Add tests
## Current Step
Working on step 3 - authorization endpoints
Agent có thể đọc, cập nhật và tham chiếu kế hoạch này khi làm việc.
Khác với plan mode “tạm thời” chỉ tồn tại trong một session, kế hoạch lưu trong file:
-
Có thể dùng lại ở session khác
-
Có thể version cùng source code
-
Có thể chỉnh sửa trực tiếp
Điều thú vị là Claude Code hiện cũng có Plan Mode — thực chất là phân tích ở chế độ read-only, rồi cuối cùng cũng ghi ra file markdown. Và trên thực tế, bạn khó mà dùng plan mode mà không phải approve rất nhiều command, vì nếu không chạy command thì việc lập kế hoạch gần như bất khả thi.
Khác biệt của pi là tính quan sát (observability). Tôi nhìn thấy rõ agent đã đọc nguồn nào và bỏ sót nguồn nào. Trong Claude Code, Claude orchestration thường spawn một sub-agent và bạn hoàn toàn không thấy nó làm gì. Tôi thì thấy ngay file markdown được tạo. Tôi có thể chỉnh sửa nó cùng agent.
Tóm lại: tôi cần observability khi lập kế hoạch, và tôi không có điều đó với plan mode của Claude Code.
Nếu bạn thực sự muốn hạn chế agent trong giai đoạn planning, bạn có thể chỉ định tool qua CLI:
Điều này cho phép chế độ read-only để khám phá và lập kế hoạch mà không sửa file hay chạy bash. Nhưng bạn sẽ không thích đâu.
No MCP support
pi không và sẽ không hỗ trợ MCP.
Tôi đã viết khá nhiều về chủ đề này, nhưng tóm lại: MCP server là overkill cho phần lớn use case và gây tốn context không cần thiết.
Ví dụ:
-
Playwright MCP: 21 tools, ~13.7k tokens
-
Chrome DevTools MCP: 26 tools, ~18k tokens
Mỗi session bạn phải đưa toàn bộ mô tả tool vào context. Nghĩa là mất 7–9% context window ngay từ đầu. Trong khi phần lớn tool bạn sẽ không dùng đến.
Giải pháp đơn giản hơn:
-
Xây tool dạng CLI
-
Viết README mô tả cách dùng
-
Agent chỉ đọc README khi cần (progressive disclosure)
-
Dùng bash để gọi tool
Cách này:
-
Composable (pipe, chain command)
-
Dễ mở rộng (thêm script mới)
-
Tiết kiệm token
Tôi duy trì bộ tool tại:
github.com/badlogic/agent-tools
Mỗi tool là một CLI đơn giản với README để agent đọc khi cần.
Nếu bạn bắt buộc phải dùng MCP, hãy xem mcporter của Peter Steinberger — nó bọc MCP server thành CLI tool.
No background bash
Tool bash của pi chạy đồng bộ (synchronous). Không có chế độ chạy server nền, không chạy test nền, không tương tác REPL khi lệnh đang chạy.
Điều này là có chủ đích.
Quản lý background process làm tăng độ phức tạp:
-
Theo dõi process
-
Buffer output
-
Cleanup khi exit
-
Gửi input vào process đang chạy
Claude Code có background bash, nhưng:
-
Observability kém
-
Agent phải tự theo dõi process
-
Không có tool để query process
Trong các phiên bản cũ, Claude Code thậm chí quên luôn background process sau khi context bị compact, khiến bạn phải kill thủ công. Việc này đã được sửa, nhưng vấn đề thiết kế vẫn còn đó.
Giải pháp: dùng tmux.
tmux cho bạn:
-
Session tách biệt
-
Xem output realtime
-
List tất cả session
-
Thậm chí co-debug cùng agent
Không cần background bash. Claude Code cũng có thể dùng tmux. Bash là đủ.
No sub-agents
pi không có tool sub-agent riêng.
Claude Code khi xử lý task phức tạp thường spawn sub-agent. Bạn không thấy nó làm gì. Black box trong black box. Context transfer giữa agent cũng kém. Nếu sub-agent sai, debug rất đau.
Nếu muốn pi spawn chính nó, chỉ cần bảo nó chạy lại chính nó qua bash. Bạn có thể chạy trong tmux để có full observability và tương tác trực tiếp.
Quan trọng hơn: hãy sửa workflow của bạn.
Nhiều người dùng sub-agent giữa session để “tiết kiệm context”. Đúng là tiết kiệm. Nhưng đó là tư duy sai.

Nếu cần gather context:
-
Làm trong session riêng
-
Tạo artifact (file tóm tắt context)
-
Dùng artifact đó trong session mới
Cách này:
-
Không làm bẩn context
-
Có observability
-
Có steerability
Vì thực tế, model vẫn kém trong việc tự tìm đủ context để sửa bug hay thêm feature. Chúng thường chỉ đọc một phần file thay vì toàn bộ, bỏ sót chi tiết quan trọng.
Bạn có thể xem issue tracker của pi-mono — nhiều PR bị revise hoặc đóng vì agent không hiểu hết vấn đề. Không phải lỗi contributor, mà là chúng ta tin agent quá mức.
Sub-agent hợp lệ: code review
Tôi không phủ nhận hoàn toàn sub-agent. Use case tôi hay dùng nhất là code review.
Tôi bảo pi spawn chính nó với prompt review:
---
description: Run a code review sub-agent
---
Spawn yourself as a sub-agent via bash to do a code review: $@
Use `pi --print` with appropriate arguments. If the user specifies a model,
use `--provider` and `--model` accordingly.
Pass a prompt to the sub-agent asking it to review the code for:
- Bugs and logic errors
- Security issues
- Error handling gaps
Do not read the code yourself. Let the sub-agent do that.
Report the sub-agent's findings.
Tôi có thể:
-
Chọn model
-
Chỉnh thinking level
-
Lưu session
-
Hoặc tạo ephemeral session
Main agent đọc prompt rồi tự chạy lại qua bash.
Tôi không thấy toàn bộ inner working của sub-agent, nhưng tôi thấy full output của nó — điều mà nhiều harness khác không cung cấp.
Thực tế workflow của tôi là:
-
Spawn session mới
-
Review PR
-
Đưa ra nhận xét của tôi
-
Cùng agent refine đến khi ổn
Tôi không merge code rác.
Việc spawn nhiều sub-agent song song để implement nhiều feature theo tôi là anti-pattern. Nếu bạn làm vậy, codebase sẽ sớm biến thành mớ hỗn độn.
Benchmarks
Tôi đã đưa ra khá nhiều tuyên bố có phần “ngông”, nhưng liệu có bằng chứng số liệu nào cho thấy những điều trái chiều tôi nói ở trên thực sự hiệu quả không? Tôi có trải nghiệm thực tế của mình, nhưng điều đó khó truyền tải qua một bài blog — và bạn chỉ có thể tin tôi. Vì vậy tôi đã tạo một lượt chạy Terminal-Bench 2.0 cho pi với Claude Opus 4.5 và để nó cạnh tranh với Codex, Cursor, Windsurf và các coding harness khác với model native tương ứng của họ.
Tất nhiên ai cũng biết benchmark không phản ánh hoàn toàn hiệu suất thực tế, nhưng đó là cách tốt nhất tôi có thể cung cấp như một dạng bằng chứng rằng những gì tôi nói không hoàn toàn là bịa đặt.
Tôi thực hiện full run với năm lần thử cho mỗi task, đủ điều kiện để submit lên leaderboard. Tôi cũng bắt đầu một lượt chạy thứ hai chỉ chạy trong giờ CET vì tôi nhận thấy error rate (và kết quả benchmark) xấu đi khi PST bắt đầu online.
Đây là kết quả của lượt chạy đầu tiên:

Và đây là vị trí của pi trên leaderboard tính đến ngày 2 tháng 12 năm 2025:

Đây là file results.json tôi đã submit cho nhóm Terminal-Bench. Bench runner của pi có tại repository này nếu bạn muốn tái hiện kết quả. Tôi khuyên bạn nên dùng Claude plan thay vì pay-as-you-go.
Và đây là một cái nhìn sơ bộ về lượt chạy chỉ CET:

Lượt này sẽ mất thêm khoảng một ngày để hoàn tất. Tôi sẽ cập nhật bài viết khi xong.
Cũng đáng chú ý là vị trí của Terminus 2 trên leaderboard. Terminus 2 là agent tối giản của chính đội Terminal-Bench — chỉ cung cấp một phiên tmux cho model. Model gửi command dạng text vào tmux và tự parse output terminal. Không tool phức tạp, không file operation abstraction, chỉ tương tác terminal thô. Và nó vẫn cạnh tranh tốt với những agent có tooling phức tạp hơn rất nhiều. Điều này càng củng cố quan điểm rằng cách tiếp cận tối giản có thể hiệu quả tương đương.
Tổng kết
Kết quả benchmark thì thú vị thật, nhưng bằng chứng thực sự nằm ở trải nghiệm thực tế. Và trải nghiệm của tôi là công việc hàng ngày — nơi pi hoạt động rất ổn định.
Twitter đầy những bài viết về context engineering, nhưng tôi cảm thấy phần lớn các harness hiện nay không thực sự cho phép bạn làm context engineering một cách kiểm soát. pi là nỗ lực của tôi để xây một công cụ nơi tôi kiểm soát mọi thứ nhiều nhất có thể.
Tôi khá hài lòng với trạng thái hiện tại của pi. Có một vài tính năng tôi muốn thêm, như compaction hoặc tool result streaming, nhưng về cơ bản tôi không cần nhiều hơn nữa.
Việc thiếu compaction cá nhân tôi chưa thấy là vấn đề. Vì lý do nào đó, tôi có thể nhồi hàng trăm lượt trao đổi vào một session — điều tôi không làm được với Claude Code nếu không compaction.
Tôi luôn chào đón đóng góp. Nhưng như mọi dự án open-source khác của tôi, tôi có xu hướng hơi độc đoán. Bài học rút ra từ các dự án lớn trước đây. Nếu tôi đóng issue hoặc PR bạn gửi, hy vọng không có cảm giác tiêu cực. Tôi sẽ cố gắng giải thích lý do. Tôi chỉ muốn giữ dự án tập trung và maintainable.
Nếu pi không phù hợp với bạn, hãy fork nó. Thật sự. Và nếu bạn tạo ra thứ gì đó còn phù hợp với nhu cầu của tôi hơn, tôi sẵn sàng tham gia.
Tôi nghĩ nhiều bài học ở trên cũng có thể áp dụng cho các harness khác. Hãy thử và cho tôi biết kết quả.