Engineering note
文件与命令工具:Agent 的能力从哪里开始失控
这是「Agent 工程实战」的第 7 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。
本篇要解决的问题: 文件读写和命令执行让 Agent 真正产生影响,也让安全边界变得不可省略。
本章定位:本章以文件和命令工具为例,讨论 Agent 如何触碰真实计算机。你会看到工具的业务语义、安全边界、同步异步和副作用控制必须同时设计。
建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。这是本书第一个安全重点章节。任何能写文件或启动进程的能力,都应在本章原则下继续开发。
本章导读
本章以文件和命令工具为例,讨论 Agent 如何触碰真实计算机。你会看到工具的业务语义、安全边界、同步异步和副作用控制必须同时设计。
本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。
7.1 文件工具做了什么
FileTool 提供三项能力:扫描目录、写文件、读文件。路径会和 Environment.CurrentDirectory 拼接。
这是一个很好的学习工具,因为它展示了 Agent 如何操作真实环境;也是一个高风险工具,因为模型一旦拥有写文件能力,就可能修改不应修改的内容。
7.2 命令工具做了什么
CommandTool.Execute 接收程序名和参数,创建 ProcessStartInfo,重定向标准输出,等待进程结束,并将输出包装成 AgentToolResult。
它证明了 Agent 可以从“会回答”升级到“会执行”,但也引入了命令注入、无限运行、资源消耗、敏感信息泄露和破坏性操作风险。
7.3 原型阶段的最小安全原则
在没有权限系统之前,至少遵守:
- 只允许访问一个明确的 workspace 根目录;
- 拒绝
..穿越; - 拒绝 workspace 外的绝对路径;
- 命令必须使用 allowlist;
- 禁止 shell 字符串拼接;
- 设置超时、输出长度和退出码;
- 记录每次工具调用。
不要因为项目运行在个人电脑上就跳过这些规则。Agent 的风险来自“模型能够组合动作”,而不是来自工具代码有多长。
7.4 本章小结
文件和命令工具是最能体现 Agent 价值的工具,也是最先需要权限边界的工具。功能完成和安全可用是两件不同的事。
7.5 文件工具的几个隐藏语义
GetFiles 使用 SearchOption.TopDirectoryOnly,这意味着它只扫描当前目录,不会递归子目录。模型如果把“扫描项目”理解成递归扫描,就可能得到不完整的结果。
WriteFile 直接使用 File.WriteAllTextAsync,会覆盖已有文件。工具描述如果只写“将内容写到文件中”,模型未必意识到这是覆盖操作。
ReadFile 没有指定编码、最大读取长度和文件类型。遇到二进制文件或超大文件时,直接读入字符串会造成异常或上下文膨胀。
这几个例子说明,工具的业务语义需要显式写进代码和 schema,不能依赖调用者猜测。
7.6 命令工具的输入边界
ProcessStartInfo(exe, arguments) 比把整串内容交给 shell 好一些,但它仍然不是完整的安全策略。程序名可能指向任意路径,参数可能读取敏感文件,子进程可能启动新的 shell 或无限运行。
最稳妥的演进顺序是:
- 删除通用命令工具,先实现具体的
search_code; - 如果确实需要通用命令,只允许固定 executable;
- 把参数解析为数组或 DTO;
- 设置工作目录、环境变量、超时和输出限制;
- 对高风险命令要求人工审批。
7.7 工具实现的同步与异步
当前 GetFiles 声明为 async Task<AgentToolResult>,但方法内部没有 await。它会产生编译警告,也会让读者误以为目录扫描已经异步化。
如果操作本身是同步且很快,可以直接返回 AgentToolResult;如果需要统一异步接口,就应明确将耗时操作放入合适的异步 API,而不是为了接口形式添加空的 async。
7.8 练习:把文件工具变成只读安全版本
先暂时移除 WriteFile,只保留读取。为 ReadFile 增加:相对路径限制、文件大小限制、UTF-8 读取、文件不存在错误码和相对路径返回值。完成后再通过审批机制恢复写入能力。
7.9 本章产出
本章的成果应是一组“能力小而明确”的工具,而不是一个可以随意操作系统的超级工具。Agent 越强,工具越要窄。
7.10 工具应该以业务意图为中心
假设用户想搜索代码。暴露 CommandTool.Execute("rg", "BooAgent Agent"),等于把命令语法、参数转义和进程策略全部交给模型;暴露 SearchCode(query, path),则可以由程序固定使用 rg,统一处理路径、输出长度和错误。
后者的好处是模型只需要理解业务意图,工具可以在内部替换实现。例如未来从 rg 换成索引服务,模型和会话协议都不需要改变。
7.11 工具描述也需要版本
工具行为变更时,旧会话可能还带着旧 schema。建议在定义中保留版本或兼容策略:
filesystem.read_file.v1
filesystem.read_file.v2
如果只是增加可选参数,可以保持同名;如果改变路径语义、返回结构或副作用,最好建立新版本,待旧会话和评测迁移后再下线旧工具。
7.12 本章复盘问题
完成文件和命令工具后,逐项回答:
- 如果模型要求读取用户目录,哪个组件拒绝?
- 如果命令输出 100 MB,哪个组件截断?
- 如果写文件过程中程序被杀死,文件处于什么状态?
- 如果工具名称发生变化,已有评测如何处理?
- 如果工具内部抛异常,用户和模型分别看到什么?
这些问题的答案就是工具系统的工程成熟度。
下一阶段:把原型变成工程。 从下一篇起,开始为最小闭环补齐可靠性、安全、配置、会话、观测与测试。
单篇实战作业
实践:把 ReadFile 限制在 workspace 内,写出绝对路径、..、超大文件和不存在文件四个测试。
建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。这是本书第一个安全重点章节。任何能写文件或启动进程的能力,都应在本章原则下继续开发。
章节复盘
复盘问题:工具带来的价值是否值得它的权限和副作用?能否用更窄的业务工具替代通用命令执行?
本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。
下一篇:第 8 篇《先修复 Agent 循环:从 Demo 走向可控内核》
如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。