做 Agent 会用到的 Node API(1):路径与文件

本系列讲实现 Agent harness 时脚下的 Node API,按场景拆篇,不当成 Node 全手册。
示例仓库:react-agent-mini
若还不熟「Agent 主循环长什么样」,可先看同仓库前作:150 行搞懂 Agent 主循环
本篇相关:代码库工具 Read/Write · Agent Memory


场景:工具的手脚落在磁盘上

Agent 要「读仓库、改文件、记偏好」,最后都会碰到两件事:

  1. 路径怎么拼、怎么防逃出工作区
  2. 文件怎么读、怎么写、写前要不要建目录

在 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 版,方便awaitTool.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+isAbsoluteresolvePathUnderCwd
约定死路径joinMemory / hooks / skills 发现
父目录dirnameWrite 前 mkdir
元信息 / 大小statRead 上限、mtime 刷新
读文本readFile(..., 'utf-8')Read、加载 AGENTS/MEMORY
写文本writeFileWrite、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。


你可以带走什么?

  1. 路径先沙箱,再读写——resolve+relative是文件类工具的安全带。
  2. promises 版 fs——和async call()同一套心智。
  3. stat再读——类型、大小、是否存在,一次问清。
  4. 写入常配mkdir(recursive)——少让模型多走一轮建目录。
  5. 预算按字节——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。