Agent 不会「逛代码库」?给最简 ReAct 装上 Grep / Glob / 分段 Read
上一篇讲了 react-agent-mini 的主循环:
query()约 150 行把「模型 ↔ 工具」跑通了。
但只有 Echo 和「整文件 Read」时,Agent 其实还是个瞎子——它答得了「用 Echo 回复 hello」,却答不了「这个仓库里needsFollowUp在哪定义的?」。今天这篇讲 v1 新加的一块:只读代码库工具。主循环一个字没改,换的是「手」。
为什么要加「找文件」和「搜内容」?
Agent 探索代码通常是这个节奏:
Glob(按文件名找)
→ Grep(按内容搜)
→ Read(精读关键片段)
→ 回答用户
没有 Grep / Glob,它就只能猜文件名、整本读文件——上下文又贵又慢,也很容易读爆。
上一版扩展路线里写的第 1 步正是:
更多只读工具(Grep、Glob)—— 仍串行、auto-allow
这期把它落地了。query.ts 完全不动:工具往 getTools() 一注册,模型就能在下一轮 callModel 里看到新的 tools schema。
这本身就是 harness 分层的好处——循环稳定,能力往外挂。
改了什么?(一张表说清)
| 能力 | 变化 |
|---|---|
| Grep | 新增:cwd 内正则搜索;默认 output_mode: files_with_matches,head_limit 默认 250 |
| Glob | 新增:cwd 内按 glob 列文件,最多 100 条 |
| Read | 增强:file_path + offset/limit 分段读;输出带行号(N\t…);offset=0 视为第 1 行 |
| CLI | stderr 状态行支持 [工具] Grep: … / [工具] Glob: … |
刻意没做:写文件、Bash、权限弹窗、.gitignore 过滤、ripgrep 全参数、并发工具分区。
只读 + 结果上限,先保证「找得到、读得了、塞不爆 context」。
新增工具,两步就够
注册表现在长这样:
// src/tools/index.ts
export function getTools(): Tools {
return [EchoTool, ReadTool, GrepTool, GlobTool]
}
每加一个工具,流程固定:
- 在
src/tools/XxxTool.ts实现Tool契约(name/description/inputSchema/call) - 挂进
getTools(),写测试,CLI 状态行可选补一行
Zod inputSchema 会自动变成发给模型的 tools JSON schema,所以不用改 adapter 的核心逻辑——出站时遍历工具列表即可。
Grep:在仓库里「搜词」
入参(精简版):
| 字段 | 说明 |
|---|---|
pattern |
正则 |
path |
可选,搜索根(必须在 cwd 内) |
glob |
可选,文件名过滤(如 *.ts) |
output_mode |
可选:content / files_with_matches(默认)/ count |
head_limit |
可选,默认 250;0 表示不限 |
默认返回匹配文件列表(不是每一行):
src/query.ts
src/Tool.ts
需要行内容时显式传 output_mode: "content",才会得到熟悉的 path:line:content:
src/query.ts:89: let needsFollowUp = false
src/query.ts:107: needsFollowUp = true
实现上为什么不用系统 rg?
设计里最初考虑「优先 rg,没有再 JS 回退」。落地时选择了 纯 JS 遍历 + RegExp:
- Windows 开发环境经常没有 ripgrep
- 单测不依赖外部二进制
- 对学习向最小 Agent 够用
代价是大仓库会慢一些——用 head_limit、跳过 node_modules / .git、以及 32KB 输出上限 兜住。更重的 vendor ripgrep,留给以后对齐完整版时再考虑。
两个护栏(很重要)
- 条数上限:默认 250,避免一次
tool_result塞爆上下文 - 路径护栏:复用 Read 的
resolvePathUnderCwd,拒绝../穿越
Agent 工具里「能搜」和「搜成灾难」往往就差这两个上限。
Glob:先摸清「有哪些文件」
入参更简单:pattern(如 **/*.ts)+ 可选 path。
实现直接用 Bun 内置 API:
import { Glob } from 'bun'
const glob = new Glob(args.pattern)
for await (const match of glob.scan({ cwd: root, onlyFiles: true })) {
// …最多收 100 条
}
无新依赖,结果硬顶 100 条路径。模型常见用法是:先 Glob 缩小范围,再 Grep / Read。
Read:别整本塞给模型
大文件整读有两个问题:超过 100KB 上限直接失败;就算塞进去也浪费 token。
这里把字段名统一写成 file_path(不是 path),读起来更直观:你一眼就知道这是“文件路径”。分段语义:
offset:1-based 起始行;0视为 1limit:读多少行- 无这两个参数时,仍读整文件(≤100KB),但始终带行号
输出行号格式为 N\t内容:前面是行号,中间用一个 tab 分隔,后面是原始内容。这样模型后续如果要引用某一行,定位会更稳定:
42 export async function* query(
43 params: QueryParams,
44 ): AsyncGenerator<QueryYield, Terminal> {
典型组合拳:
Glob **/*query*
→ Grep needsFollowUp
→ Read file_path=src/query.ts offset=85 limit=40
主循环真的不用改吗?
是的。回顾上一篇的五字诀:看、停、干、限、记。
「干」这一步仍然是 runTools → runToolUse:
权限(v1 仍 auto-allow)
→ findToolByName
→ Zod 校验
→ tool.call()
→ 包装成 tool_result
Grep / Glob / Read 都是 isReadOnly: true,继续吃同一套权限策略。你要加的是工具实现,不是循环语义。
这也是面试时常考的点:
加一个只读工具,该动哪一层?
→ 工具注册表 + Tool 实现;不动queryLoop。
30 秒试一把
Mock 模式仍可验证 Echo;探索代码库更建议真实模型(需要 API Key):
# 找文件
bun run dev -- "用 Glob 列出 src/tools 下的 *.ts"
# 搜内容 + 读片段
bun run dev -- "在仓库里 Grep needsFollowUp,然后分段 Read 相关代码并解释它做什么"
CLI 上你会看到类似:
[工具] Glob: src/tools/*.ts
[工具] Grep: needsFollowUp
[工具] Read: src/query.ts
文本仍走 stdout,工具状态走 stderr——和上一篇一致。
刻意留下的坑(也是下一步)
| 还没有 | 意味着什么 |
|---|---|
| 写文件 / Bash | Agent 还不能改代码、跑命令 |
| 真实权限流水线 | 现在仍恒 allow,只适合可信 cwd |
不尊重 .gitignore |
可能扫到构建产物(可手动缩 path) |
| 工具串行 | 一轮里多个只读工具也会挨个跑 |
建议的下一刀仍然清晰:
只读工具✅- 权限流水线(替换
autoAllowCanUseTool) - REPL 多轮对话
- compact
- 写操作 / MCP
你可以从这里带走什么?
- 工具是 harness 最便宜的扩展点:实现
Tool+ 注册,循环层零改动。 - 只读工具也要有上限:条数、字节、cwd 边界——防止把 context 和磁盘一起炸掉。
- 探索链路有固定套路:Glob → Grep → 分段 Read,比「一口吞整个文件」更像真 Agent。
如果你把上一篇看成「心脏搏动」,这篇就是「长出手和眼睛」——还不会写代码、也不会问你权限,但终于能在仓库里走动了。
仓库与相关文档
- GitHub:https://github.com/jimchou-h/react-agent-mini
- 上一篇(主循环):150 行搞懂 Agent 主循环
- 架构导读:docs/architecture.md
- 工具术语表:src/tools/CONTEXT.md
欢迎 Star、Issue 和 PR。
本文基于 react-agent-mini 变更 v1-codebase-tools;Grep/Read 契约已按 v4-claude-align 校正。
更多推荐



所有评论(0)