猪头少年 - 云南AI专家 - 云南独立开发者

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 或无限运行。

最稳妥的演进顺序是:

  1. 删除通用命令工具,先实现具体的 search_code
  2. 如果确实需要通用命令,只允许固定 executable;
  3. 把参数解析为数组或 DTO;
  4. 设置工作目录、环境变量、超时和输出限制;
  5. 对高风险命令要求人工审批。

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 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。

Next step

不要停在单篇文章。

沿主专题继续阅读,查看同一问题从概念到实战的完整路径。

返回「Agent 工程实战」路线