本系列讲实现 Agent harness 时脚下的 Node API,按场景拆篇,不当成 Node 全手册。
示例仓库:react-agent-mini
若还不熟「Agent 主循环长什么样」,可先看同仓库前作:150 行搞懂 Agent 主循环
本篇相关:代码库工具 Read/Write · Agent Memory
场景:工具的手脚落在磁盘上
Agent 要「读仓库、改文件、记偏好」,最后都会碰到两件事:
- 路径怎么拼、怎么防逃出工作区
- 文件怎么读、怎么写、写前要不要建目录
在 Node 里,这对应两个模块:
| 模块 | 管什么 |
|---|---|
node:path | 字符串层面的路径:拼接、解析绝对路径、算相对关系 |
node:fs/promises | 真正碰磁盘:stat/readFile/writeFile/mkdir |
本篇只讲 Agent 里高频的那一小撮,对照react-agent-mini的 Read / Write / Memory。
1.path:先把字符串变成「可信绝对路径」
常用三个:
import{isAbsolute,relative,resolve,join,dirname}from'node:path'resolve(cwd,inputPath)// 相对 → 绝对;处理 `.` / `..`relative(cwd,absolute)// 绝对相对 cwd 的相对串join(cwd,'.agents','memory','MEMORY.md')// 纯拼接片段dirname(filePath)// 父目录,给 mkdir 用Agent 里最关键的一招:cwd 沙箱
模型可能传../../etc/passwd。只靠「拼一下」不够,要校验结果仍在工作区子树内:
export function resolvePathUnderCwd( inputPath: string, cwd = process.cwd(), ): string { const absolute = resolve(cwd, inputPath) const rel = relative(cwd, absolute) if (rel.startsWith('..') || isAbsolute(rel)) { throw new Error('拒绝访问:路径必须在当前工作目录内') } return absolute }要点:
resolve会消掉..,所以必须再看relative结果rel.startsWith('..'):还在往上爬isAbsolute(rel):Windows 上相对结果有时是另一盘符绝对路径,也要拦
Read / Write / Edit / Glob / Grep 都复用这一函数——路径规则写一次,所有文件工具共用。
Memory 则用join钉死约定路径,不接受模型乱指:
join(cwd,'.agents/memory/MEMORY.md')2.fs/promises:异步读盘,别阻塞事件循环
Agent 一轮里可能连读多个文件;用 Promise 版,方便await进Tool.call:
import{readFile,writeFile,stat,mkdir}from'node:fs/promises'stat:先问「是不是文件、有多大」
constfileStat=awaitstat(filePath)if(!fileStat.isFile())thrownewError('不是普通文件')if(fileStat.size>MAX_READ_BYTES)thrownewError('文件过大')Read 在readFile之前做这件事,避免把巨型二进制整份读进内存再报错。
ENOENT(不存在)要转成对模型友好的文案,而不是把堆栈塞进tool_result:
try{fileStat=awaitstat(filePath)}catch(err){if(err&&typeoferr==='object'&&'code'inerr&&err.code==='ENOENT'){thrownewError(`文件不存在:${args.file_path}`)}throwerr}readFile:拿正文
constcontent=awaitreadFile(filePath,'utf-8')指定'utf-8',得到string。Agent 文本工具几乎总是这么读;二进制另议(你们 MCP Resource 对 blob 是占位,不塞 base64)。
writeFile+mkdir:写入与建父目录
Write 的典型顺序:
awaitmkdir(dirname(filePath),{recursive:true})awaitwriteFile(filePath,args.content,'utf-8')recursive: true:父目录多层一次性建好- Memory 启动时的
ensureMemoryDirExists也是同一个mkdir(..., { recursive: true }),方便模型直接 Write,少一轮「先建目录」
也可用stat判断「创建还是覆盖」,给模型不同成功文案——但仍是覆盖写语义。
3. 字节预算:Buffer.byteLength
截断「最多 32KB / 100KB」时,不要用string.length(那是 UTF-16 码元数)。Memory 用的是:
Buffer.byteLength(content,'utf-8')和readFile/writeFile的字节语义一致,避免中文多字节把预算算爆。
一张对照表
| Agent 需求 | Node API | 仓库里 |
|---|---|---|
| 相对路径 → 绝对 + 防穿越 | resolve+relative+isAbsolute | resolvePathUnderCwd |
| 约定死路径 | join | Memory / hooks / skills 发现 |
| 父目录 | dirname | Write 前 mkdir |
| 元信息 / 大小 | stat | Read 上限、mtime 刷新 |
| 读文本 | readFile(..., 'utf-8') | Read、加载 AGENTS/MEMORY |
| 写文本 | writeFile | Write、Edit 落盘 |
| 建目录 | mkdir({ recursive: true }) | Write、ensure memory dir |
常见坑
| 坑 | 建议 |
|---|---|
只resolve不校验 | 模型可逃出 cwd;必须relative检查 |
用existsSync再读 | 有竞态;stat/readFile捕获ENOENT更干净 |
同步fs.readFileSync塞进热路径 | 拖住整条 Agent 事件循环;工具里优先 promises |
用length当字节预算 | 多字节字符不准;用Buffer.byteLength |
| Windows 路径分隔符 | 尽量交给path;少手写/\拼接 |
和主循环的关系
主循环(query())不关心磁盘;工具层才碰path/fs。
query → tool_use: Read → resolvePathUnderCwd → stat / readFile → tool_result 文本回模型所以学 Node 文件 API,是在学Agent 的效应器,不是在学 ReAct 本身。主循环仍是前作那 150 行;本篇补的是「手脚怎么落地」。
本系列下一篇预告
(2)子进程:Bash 与 Hooks 的壳——spawn、stdout/stderr、超时杀掉、跨平台 shell。
你可以带走什么?
- 路径先沙箱,再读写——
resolve+relative是文件类工具的安全带。 - promises 版 fs——和
async call()同一套心智。 - 先
stat再读——类型、大小、是否存在,一次问清。 - 写入常配
mkdir(recursive)——少让模型多走一轮建目录。 - 预算按字节——
Buffer.byteLength,不是string.length。
仓库与延伸
- GitHub:react-agent-mini
- 本系列定位:Agent 实现向的 Node API 笔记(与 harness 设计系列分开)
- 前作主循环:150 行搞懂 Agent 主循环
- 相关实现:ReadTool.ts · WriteTool.ts · memory/load.ts
欢迎 Star、Issue 和 PR。
本文为「做 Agent 会用到的 Node API」系列第 1 篇;示例基于 react-agent-mini。