2590 words
13 minutes
Pi, OpenCode và Codex tìm code như thế nào? — Part 1

Giả sử mình nhờ ba bạn developer đi tìm nguyên nhân một bug trong một codebase lạ.

Mình từng nghĩ mấy coding agent hiện đại chắc phải có một cỗ máy search rất ghê gớm ở phía sau: index toàn bộ repository, biến code thành embedding, hiểu semantic, rồi lấy ra đúng file giống như Google Search phiên bản dành cho source code.

Nghe hợp lý mà. Agent trả lời được câu hỏi kiểu “đoạn nào quyết định retry một request lỗi tạm thời?” dù trong code không hề có chữ retry. Nếu không phải semantic search thì còn là gì nữa?

Sau khi clone và đọc source của Pi, OpenCode và Codex, câu trả lời hóa ra vừa đơn giản vừa thú vị hơn:

Cả ba chủ yếu vẫn search bằng lexical tools như rg, fd, glob, shell và read file. Phần “semantic” nằm trong model đang nghĩ ra query, đọc kết quả, đổi từ khóa rồi search tiếp.

Nói cách khác, chiếc máy tìm kiếm không thông minh đến mức mình tưởng. Người sử dụng chiếc máy đó mới là phần thông minh.

Bài này là Part 1 của loạt nghiên cứu đó. Mình sẽ đi qua kỹ thuật search của từng harness, chỉ ra những bề mặt rất dễ bị đánh đồng với nhau, rồi đối chiếu bằng benchmark mình đã chạy với cùng model, cùng corpus và cùng cách chấm điểm.

Mental model ban đầu sai ở đâu?#

Khi nhìn một coding agent tìm được đúng đoạn code, mình chỉ thấy đầu vào và đầu ra:

"Tìm chỗ request context được attach vào middleware"
                  ↓
"Nó nằm ở src/http/middleware/requestContext.ts"

Phần bị che mất là rất nhiều bước ở giữa. Agent có thể thử tìm tên file, grep một khái niệm gần nghĩa, đọc vài kết quả, thấy một symbol mới, search theo symbol đó, rồi lần sang caller.

Execution path thật gần với flow này hơn:

flowchart TB
    accTitle: Luồng agentic search của ba coding harness
    accDescr: Model tạo truy vấn, gọi công cụ lexical của Pi, OpenCode hoặc Codex, đọc bằng chứng rồi lặp lại cho tới khi đủ thông tin để trả lời.

    Q["Câu hỏi của user"] --> M["Model chọn query và phạm vi"]
    M --> H{"Harness đang dùng"}
    H --> P["Pi: rg, fd, read"]
    H --> O["OpenCode: grep, glob, read"]
    H --> C["Codex: shell, rg, git, sed"]
    P --> E["Tool output / evidence"]
    O --> E
    C --> E
    E --> R["Model đọc, nối quan hệ, đổi query"]
    R -->|"Chưa đủ"| M
    R -->|"Đủ bằng chứng"| A["Final answer"]

rg không tự biết “retry lỗi tạm thời” tương đương với transientFailure. Model đoán mối liên hệ đó, search từ khóa mới và kiểm tra kết quả. Vì vậy, khi nói về “search algorithm” của coding agent, mình phải tách ít nhất hai lớp:

  1. Search primitive: rg, fd, glob, fuzzy matcher, read file và các giới hạn output.
  2. Agentic search policy: model chọn query nào, đọc file nào, có search song song không, khi nào reformulate và khi nào dừng.

Nếu trộn hai lớp này lại, mình rất dễ ghi công cho ripgrep trong khi thứ tạo ra khác biệt thật sự lại là cách model sử dụng ripgrep.

Pi: công cụ đơn giản, policy nằm gần như hoàn toàn ở model#

Pi là harness có triết lý khá thẳng. Search content của nó dựa trên rg; tìm file dựa trên fd; đọc bằng chứng bằng read.

Built-in grep gọi ripgrep với JSON output, line number, hidden files và ignore rules thông thường. Mặc định nó lấy tối đa 100 matches, giới hạn mỗi dòng và chặn tổng output ở khoảng 50 KiB. Built-in find dùng fd, trả path tương đối và mặc định tối đa 1.000 kết quả.

Có một nuance khá quan trọng: Pi có sẵn grep, find, ls, nhưng default agent session lại chỉ bật read, bash, edit, write. Nghĩa là trong flow mặc định, model thường tự viết lệnh shell như rg, find, git thay vì nhất thiết đi qua structured search tool.

Trong benchmark core, mình chủ động giới hạn Pi về read,grep,find,ls. Mục tiêu là đo search chứ không đo khả năng chỉnh sửa file, đồng thời giữ corpus không bị mutate.

Khi gõ @ trong UI, Pi cũng có chức năng tìm file. Nhưng đây là một bề mặt khác.

Picker lấy candidates từ fd, sau đó ưu tiên exact match, prefix và contiguous substring. Nó không phải một semantic retriever, cũng không phải kiểu fuzzy subsequence mạnh như nhiều người thường hình dung khi nghe chữ “fuzzy”. Chọn một file qua @ chỉ giúp người dùng đưa path vào prompt; nó không tự động thay thế vòng search/read của agent.

Agentic behavior#

Pi có thể chạy các tool call độc lập song song trong cùng một lượt model. Nó cũng load project context như AGENTS.md hoặc CLAUDE.md và dùng compaction cho session dài.

Nhưng isolated subagent không phải core mặc định. Pi có subagent extension mẫu, tức là fanout có thể thêm vào, nhưng không nên lấy behavior của extension rồi nói đó là Pi core.

Mental model ngắn gọn cho Pi là:

Primitive mỏng, dễ hiểu; model chịu trách nhiệm phần lớn việc tìm từ khóa, đổi hướng và quyết định đã đủ evidence hay chưa.

OpenCode: structured search rõ nhất trong ba harness#

OpenCode đưa search primitives thành các tool có schema khá rõ:

  • grep dùng ripgrep cho content search.
  • glob dùng rg --files để tìm path.
  • read lấy nội dung file, directory, image hoặc PDF và giới hạn text ở 2.000 dòng hay 50 KiB.

grep và glob đều cap khoảng 100 kết quả. grep có thể đi qua hidden files nếu ignore rules cho phép; glob thì không tự biến một typo thành filename gần đúng. Nếu model search req-contxt nhưng file tên requestContext.ts, glob không có phép màu semantic nào để sửa giúp.

Model phải thử pattern khác, search theo symbol hoặc chuyển sang một chiến thuật khác.

FFF và fuzzysort nằm ở file picker#

OpenCode có một file-finding service riêng cho UI/API. Trên platform hỗ trợ Bun, nó ưu tiên FFF; khi FFF bị tắt hoặc không khả dụng thì dùng rg --files để tạo candidates rồi rank bằng fuzzysort.

Đây là nơi OpenCode có ranking filename thật sự. Nhưng một lần nữa: FFF/fuzzysort không phải tool glob mà agent dùng trong core benchmark.

Sự phân biệt này rất quan trọng. Nếu người dùng gõ vào file picker và thấy kết quả fuzzy tốt, không có nghĩa agent đang dùng đúng thuật toán đó trong mỗi lượt tự search code.

Explore agent, Task và LSP#

OpenCode có read-only explore agent chuyên điều hướng codebase. Tool Task có thể tạo hoặc resume child session để làm một nhánh tìm kiếm riêng. Đây là cơ chế delegation rõ hơn Pi core.

OpenCode cũng có tool LSP cho definition, references, hover, workspace symbol và call hierarchy. Tuy nhiên ở release mình benchmark, LSP tool vẫn nằm sau experimental flag. Vì vậy mình không trộn nó vào baseline lexical search; nếu bật thì phải coi đó là một experiment riêng.

Một chi tiết vui mà benchmark bắt được: OpenCode ghi project-ID cache vào .git/opencode khi mở một committed Git worktree. Lúc đầu mutation guard của mình tưởng source bị sửa và làm fail hai smoke runs. Thực tế working tree vẫn sạch; chỉ Git metadata thay đổi. Sau đó mình sửa guard để hash toàn bộ working-tree content, file mode và symlink nhưng bỏ qua .git internals.

Mental model ngắn gọn cho OpenCode là:

Agent-facing search có cấu trúc rõ; fuzzy ranking, LSP và child-agent là những lớp riêng, không nên nhập chung thành một “search engine”.

Codex: shell-first, để model tự compose chiến thuật#

Codex đi theo hướng shell-first rõ nhất.

Model instruction khuyến khích dùng rg cho content search và rg --files cho file search. Codex cũng package ripgrep trong các release. Tool exec_command cho phép model tự ghép nhiều primitive vào cùng một command:

rg -n "verifySignedSession" src \
  && sed -n '1,180p' src/auth/tokenVerifier.ts

Đây vừa là điểm mạnh vừa là thứ làm benchmark khó đọc hơn. Một tool event của Codex có thể chứa nhiều lần search/read, trong khi một event của Pi hay OpenCode thường gần với một atomic tool call hơn.

Vì vậy không thể nhìn “Codex dùng ít tool calls hơn” rồi kết luận ngay nó search hiệu quả hơn. Đơn vị đo không giống nhau.

Nucleo cũng là một đường riêng#

File picker khi gõ @ của Codex dùng ignore walker kết hợp Nucleo matcher. Walker thu thập path, Nucleo rank top candidates theo fuzzy score, sau đó UI hiển thị một phần kết quả.

Picker này không tự đọc file. Nó chỉ thay @token bằng path được chọn. Khi agent tự đi tìm code, đường chính vẫn là model gọi shell, rg, git, sed và các command liên quan.

Context và fanout#

Codex load AGENTS.md theo hierarchy từ project root xuống current working directory, có byte budget để tránh context phình vô hạn. Tool calls có thể chạy song song và Codex có built-in subagent controls để fanout các nhánh độc lập, tùy client/model/config.

Mental model ngắn gọn cho Codex là:

Model được trao một shell khá mạnh và tự compose search plan; Nucleo picker và subagent là các bề mặt bổ sung, không phải semantic index ẩn bên dưới.

Đặt ba harness cạnh nhau#

Bề mặtPiOpenCodeCodex
Agent content searchrg qua shell; structured grep tùy configDedicated grep trên ripgrepShell với rg, git, sed,…
Agent file searchfd hoặc shellglob trên rg --filesShell với rg --files
Interactive pickerfd + exact/prefix/substringFFF, fallback fuzzysortIgnore walker + Nucleo
Symbol searchExtension/customLSP experimentalKhông có local LSP tương đương trong baseline
FanoutExtension mẫuTask / explore agentBuilt-in subagent, tùy config
Local semantic indexKhông thấy trong coreKhông thấy trong released coreKhông thấy trong core code-search path

Điểm chung lớn nhất không phải ripgrep. Điểm chung là vòng lặp:

model đoán query → lexical search → đọc evidence → reformulate → search tiếp

Đó mới là “agentic search algorithm” đáng nghiên cứu.

Benchmark: cùng model, cùng corpus, không chấm bằng văn hay#

Đọc source cho mình biết harness có thể làm gì. Nhưng agent có thực sự tìm đúng evidence hay không thì phải chạy.

Mình dựng một lab với ba nguyên tắc:

  1. Giữ model cố định: cả ba yêu cầu gpt-5.6-terra, reasoning medium, qua ChatGPT OAuth.
  2. Pin runtime: Pi 0.84.1, OpenCode 1.18.16, Codex 0.147.0.
  3. Không chấm final answer một mình: muốn được điểm, file/symbol/term phải xuất hiện trong câu trả lời và trong successful tool evidence.

Model parity gate chạy trước để chắc cả ba client chấp nhận explicit model/reasoning và không client nào tự fallback. Sau đó là:

  • 24 smoke runs: 8 cases × 3 harnesses × 1 trial.
  • 72 scored runs: 8 cases × 3 harnesses × 3 trials.

Tám cases gồm:

  • exact literal search;
  • typo-heavy filename search;
  • ignore rules với .gitignore và .ignore;
  • cross-file execution flow trong TypeScript;
  • concept search khi câu hỏi và code dùng vocabulary khác nhau;
  • root/nested instruction discovery;
  • HTTP routing flow trong Go Chi;
  • overflow regression trong Apache Commons Lang.

Corpus trải trên TypeScript fixture có ground truth, Go và Java. Mỗi run lưu manifest, raw normalized JSONL, tool evidence, final answer, latency, process exit và before/after corpus digest.

Kết quả integrity trước khi nhìn điểm#

Đây là phần mình quan tâm trước cả leaderboard:

  • 24/24 smoke runs pass.
  • 72/72 scored runs pass.
  • Không có source mutation.
  • Không có empty answer hay run không dùng tool.
  • Không có forbidden-canary disclosure.
  • Không có nonexistent cited path.
  • 216 scored artifacts khớp SHA-256 ledger.

Nếu một harness trả lời nghe rất hay nhưng tool trace không hề đọc được evidence tương ứng, scorer không cho grounded credit. Cách này chưa hoàn hảo, nhưng ít nhất nó chặn kiểu “đoán đúng bằng văn mẫu”.

Kết quả 72 runs#

HarnessRunsWeighted scoreFile recallSymbol recallTerm recallTool events/runMedian latency
Pi240.952 ± 0.1230.9860.9170.9387.5816.7 s
OpenCode240.927 ± 0.1651.0000.8750.8546.7518.3 s
Codex240.915 ± 0.1340.9510.8750.8962.8319.3 s

Nhìn bảng này, phản xạ đầu tiên có thể là tuyên bố Pi thắng. Nhưng với ba trials cho mỗi cell thì nói vậy hơi nhanh.

Đây là kết quả của một fixed benchmark với một model, một reasoning level, một bộ prompt và một cấu hình tool cụ thể. Chênh lệch nhỏ có thể đổi khi model snapshot, corpus hoặc tool profile thay đổi.

Có vài observation đáng tin hơn chuyện xếp hạng:

1. Năm bài fixture gần như đã saturated#

Exact literal, fuzzy filename, cross-file flow, concept mismatch và scoped instructions đều đạt hoặc gần đạt trần trên cả ba harness.

Khi ground truth nhỏ và rõ, cả ba vòng agentic search đều đủ sức tìm đúng evidence. Lúc đó primitive khác nhau không tạo khoảng cách lớn.

2. Ignore rules là case phân hóa mạnh nhất#

Điểm trung bình case này:

  • Pi: 0.717
  • Codex: 0.650
  • OpenCode: 0.517

Phần lớn điểm mất không phải vì trả lời sai hoàn toàn, mà vì final answer bỏ quên marker được yêu cầu hoặc không cite đủ .ignore, .gitignore và file được re-include.

Đây là một nhắc nhở hay: tìm thấy answer không đồng nghĩa đã giải thích đủ search behavior.

3. Codex dùng ít tool events hơn, nhưng không thể so thẳng#

Trung bình Codex có 2.83 tool events/run, thấp hơn Pi và OpenCode. Nhưng 50 trong 68 shell calls của Codex có rg, và một call có thể ghép rg, nl, sed, git trong cùng command.

Pi ghi nhận 43 grep, 26 find, 92 read và 21 list attempts. OpenCode có 57 grep, 29 glob và 76 read attempts.

So số event giữa structured tools và composed shell giống như so “số lần gọi món” với “số món nằm trong combo”. Có liên quan, nhưng không cùng đơn vị.

4. Score cao không có nghĩa search engine semantic hơn#

Pi có weighted mean cao nhất trong lần chạy này, nhưng source của Pi lại là ví dụ rõ nhất về primitive mỏng. Điều đó củng cố kết luận ban đầu: performance đến từ interaction giữa model policy, tool schema, context và corpus chứ không phải một semantic index bí mật.

Những gì benchmark này chưa chứng minh#

Mình muốn đặt caveat ngay cạnh kết quả, vì bỏ nó ra thì bảng điểm rất dễ bị dùng sai.

  • Ba trials mỗi cell chưa đủ để tuyên bố sản phẩm nào “tốt nhất”.
  • Tool event không phải đơn vị cost chung giữa ba harness.
  • OpenCode và Codex chấp nhận explicit model selection nhưng không echo provider-effective model trong JSONL; server-side aliasing vẫn không quan sát độc lập được.
  • Pi và Codex read-only mode không phải strict corpus-only sandbox. Lab dùng public corpus và canary, không dùng private code hay secret.
  • Web search, MCP ranking, LSP, file picker và multi-agent fanout chưa nằm trong core score này.
  • Benchmark đo grounded recall và execution order; nó chưa đo đầy đủ precision, độ dễ đọc hay chất lượng engineering judgment của final answer.

Vì vậy kết luận hợp lý không phải là “Pi thắng Codex 0.037 điểm”. Kết luận hợp lý là:

Trên cùng model và corpus, cả ba harness đều tìm code tốt bằng lexical primitives cộng với agentic query planning. Khác biệt lớn nằm ở tool interface, context policy và cách model compose vòng tìm kiếm.

Chốt lại Part 1#

Sau khi đọc source và chạy benchmark, mental model mình giữ lại là:

  • Pi: primitive mỏng, rõ ràng; model gánh phần lớn search policy.
  • OpenCode: structured agent tools rõ nhất; picker, LSP và child-agent là các lớp tách biệt.
  • Codex: shell-first; model tự compose rg, git, sed và có thể gom nhiều thao tác vào một call.
  • Cả ba: semantic relevance chủ yếu đến từ model đang lập kế hoạch và reformulate, không phải local vector index.

Ở Part 2, mình sẽ đi sâu hơn vào chính vòng lặp đó: model chọn query đầu tiên ra sao, lúc nào search song song có lợi, compaction làm mất evidence thế nào, và subagent fanout có thực sự tăng recall hay chỉ tăng token bill.

Nếu chỉ nhớ một câu từ Part 1 thì là câu này:

Đừng hỏi coding agent “dùng search engine gì” trước. Hãy hỏi model được phép nhìn thấy tool nào, output bị cắt ở đâu, context nào được nạp vào và evidence nào thật sự đi đến final answer.

Pi, OpenCode và Codex tìm code như thế nào? — Part 1
https://nhamnhi.toilacube.io.vn/posts/pi-opencode-codex-search-part-1/
Author
toilacube
Published at
2026-08-12
License
CC BY-NC-SA 4.0