实战项目:Mini Cursor(CLI 版)
摘要:用最少的工程复杂度,做出一个“能读写代码”的小 Agent。目标不是造一个完整 IDE,而是把 Tool Use + 安全护栏 + 可迭代改文件 这条链路跑通。
0. 目标与非目标
目标
- 允许用户在终端输入需求:例如“把某个函数改成支持传入数组”
- Agent 能:
- 列目录 / 搜索文件
- 读取目标文件
- 生成补丁(patch)
- 写回文件
- 重新读取并自检
非目标
- 不做 LSP、不做 AST 精确改写、不做全量测试编排(后续可加)
1. 工具设计(最小集)
建议第一版只做 4 个工具:
list_dir(path):列出目录(用于定位文件)search(pattern, path):grep 搜索(用于定位符号)read_file(path, offset?, limit?):分段读文件(控 token)write_file(path, content):覆盖写入(第一版简单粗暴)
后续升级:把 write_file 变成
apply_patch(path, patch)(更安全)
2. 最小 Agent Loop(CLI 驱动)
你可以把它拆成三个层:
- LLM 层:负责对话与 tool_calls
- Tools 层:真实执行文件系统/命令
- Loop 层:不断“问模型 -> 执行工具 -> 回传结果”直到完成
伪代码:
const tools = [list_dir, search, read_file, write_file]
const system = `你是一个代码修改助手。
规则:
- 修改前必须 read_file
- 尽量只修改必要最小范围
- 修改后必须再次 read_file 自检
- 不允许访问工作目录之外的路径
- 输出最终答复时,说明改了哪些文件、改动点是什么
`
messages = [{ role: "system", content: system }]
while (true) {
const resp = await llm({ messages, tools })
const msg = resp.choices[0].message
if (!msg.tool_calls?.length) {
print(msg.content)
break
}
messages.push(msg)
for (const call of msg.tool_calls) {
const result = await runTool(call)
messages.push({
role: "tool",
tool_call_id: call.id,
name: call.function.name,
content: JSON.stringify(result)
})
}
}
3. 关键难点 ①:Token 消耗怎么控
3.1 只读必要片段
read_file支持offset/limit- 先通过
search找到函数附近,再读局部
3.2 让模型“引用行号”
你可以让 read_file 返回带行号的内容:
120| function foo() {
121| ...
这样模型更容易做精确修改(尤其是后续 patch 化)。
3.3 限制上下文
- 只保留最近 N 轮 tool 结果
- 或把“工具输出”做摘要再放回 messages
4. 关键难点 ②:修改准确性怎么保证
第一版“覆盖写入”很危险,建议至少加三层护栏:
- 工作区沙箱:只能操作指定目录(例如 repo 根目录)
- 变更最小化约束:system prompt 强约束“只改必要范围”
- 自检回读:写完必须 read_file 校验自己改了什么
如果你要进一步提升可靠性:
- 让模型输出 unified diff
- 由你的程序做 patch apply(失败就回传错误让模型修正)
5. 推荐的输出格式(让结果可审计)
最终回答建议固定结构:
- ✅ 修改了哪些文件:
path/to/file.ts
- ✅ 改了什么:
- 改动点 1
- 改动点 2
- ✅ 如何验证:
- 运行哪些命令/测试
6. 下一步
Mini Cursor 跑通后,你就具备了构建复杂 Agent 的基础“手脚”。接下来要解决的是:
- Memory:对话长了会撞 context window,怎么让它“像记得一样”?
- Planning:任务复杂时,怎么让它先计划再执行,并能自我纠错?