从零开始的 Agent 开发:灵活 or 可控?

0x00 前言

上一篇把最小的 Agent Loop 跑了起来,也加了一个简单的 Tool Call。到这一步,Agent 已经不只是生成文本了,它可以读文件、跑命令、改代码。

能力一多,问题也跟着来了:LLM 生成的是概率最高的下一段文本,但工具调用会真的影响本地环境,也即是“副作用”。fs__read 没什么问题,fs__write 就要小心乱改了,shell__exec 更是可能直接爆炸。

所以这一篇写 letcode 的工具权限系统。要回答的问题只有一个:哪些工具能自动执行,哪些工具要问用户,哪些工具应该直接拒绝?

0x01 为什么

最直接的做法是删掉危险工具,比如 shell。安全确实是安全了,那么代价呢?代价就是牺牲了灵活性。shell 的通用性是无法替代的,直接删掉终究不太现实。

那么,全部变成 Tool 呢?项目里会有各种命令,cargo checknpm testgit status、自定义脚本、临时诊断命令等。如果给每种场景都包一层专用 Tool,最后只会变成另一个复杂系统,而且仍然覆盖不全。

所以 shell 不能简单禁掉,只能管起来。

借鉴 opencode 的实现思想,把工具调用当成一个权限裁决问题。LLM 可以提出它想做什么,但执行前要经过一层判断。判断结果只有三种:Allow、Ask、Deny。

0x02 实现

letcode 里,一个 Tool Call 大致会走下面这条链路:

graph TD
    A["LLM 发起 Tool Call"] --> B["解析 JSON 参数"]
    B --> C["判断 Tool 是否在当前 scope 内"]
    C --> D["根据工具名/工具实现判断权限类别"]
    D --> E["结合当前 turn 的 directive 做限制"]

    E --> F{"PermissionPolicy 做 Allow/Ask/Deny 裁决"}

    F -->|Allow| G["执行工具"]
    F -->|Ask| H{"向用户申请授权"}
    H -->|同意| G
    H -->|拒绝| I["执行失败"]
    F -->|Deny| I

    G --> J["记录 ToolResult、ToolExecutionSummary 等"]
    I --> J

    J --> K["记录 History、生成 Evidence"]

Tool 本身是一层抽象。每个 Tool 需要声明这些东西:

  • name
  • description
  • JSON schema 参数
  • strict 标记
  • execute 实现
  • permission class

这些 Tool 会注册到 ToolRegistry。请求发给模型时,也只会把 registry 里的工具暴露出去。模型不能凭空编一个本地不存在的工具名来调用。

并且“存在”并不意味着“一定能执行”。真正执行前,还要看它的 permission_class、当前的 PermissionPolicy,以及这一轮对话里有没有额外的 ExecutionDirective

Permission Class

第一层是给工具分权限类别。

  • Read: 读操作,fs__readsearch__rggit__status
  • Preview: 预览或流程控制,不直接修改工作区,workflow__todos、子代理调研工具
  • Write: 写入操作,fs__writeedit__apply_patch
  • Command: Shell 命令,shell__exec
  • Unknown: 未知工具,保守处理

它的作用是先把大方向分出来:读操作通常安全,写操作要谨慎,命令执行单独处理,未知工具默认不信。

Permission Mode

第二层是全局权限模式。

Safe 模式最保守,所有操作要授权(Ask),危险命令直接拒(Deny)。

Default 模式(默认)平衡安全和效率:

  • Read/Preview 放行
  • Write/Unknown 要授权
  • Command 看命令风险

Solo 模式更激进。除了明显危险的命令,其他基本放行。它更适合 Agent 自主完成任务的场景,All in!

Command Risk Classification

shell__exec 不能只按工具名判断。git statusrm -rf . 都是 shell 命令,但风险完全不同。

所以 letcode 会继续对命令文本做一次分级:

1
2
3
4
5
6
pub enum CommandRisk {
ReadOnly, // git status、ls
LowRisk, // cargo check、npm test
Ask, // git commit、npm install
Deny, // rm -rf、sudo、curl | sh
}

目前的实现主要靠字符串模式匹配:

  • 破坏性操作:rm -rfmkfsdd if=
  • shell 控制语法:管道、重定向、命令链
  • 网络执行:curlwget| sh
  • 权限提升:sudoshutdown

没有做复杂的 shell parser。原因很简单,真的要完整理解 shell 语义,坑多到根本爬不出来。

现阶段只做了简单的一层拦截:明显只读的放行,明显危险的拒绝,中间地带交给用户。

比如 git status 是 ReadOnly,cargo check 是 LowRisk,git commit 会 Ask,rm -rf / 直接 Deny。

Execution Directive

第三层是临时指令,也就是 ExecutionDirective。它不改变全局模式,只限制当前这一轮或者某个子代理的行为。

  • ReadOnly: 只读,常用于探索型子代理
  • PlanOnly: 只规划,禁止写入
  • AnalyzeOnly: 只分析
  • DoNotEdit: 不编辑文件

Directive 的优先级比 Permission Mode 高。即使现在是 Solo 模式,只要当前 directive 是 ReadOnly,写文件和非只读命令都会被拦掉。

这个是为多代理考虑的,也许以后会用得到。

Tool Scope

最后还有一层 Tool Scope,也是给子代理用,目前暂定如此。

  • FullAccess: 全部工具
  • ReadOnlyExplorer: 只读工具,包括 fs__readsearch__rggit__status

0x03 设计思考

现在这套流程可以概括成:

Tool Scope → Execution Directive → Permission Mode → Command Risk

每层都有自己的事情要做:

  • Scope 管工具可见性,决定子代理能看到哪些工具
  • Directive 管临时约束,比如探索模式强制只读
  • Mode 管默认策略,平衡安全和效率
  • Risk 管具体命令,识别 rm -rf 这类危险操作

这里更多是工程取舍(屎山堆积),灵活性跟确定性始终是冲突的。

如果所有工具都 Ask,Agent 每走一步都要停下来问,体验很差。如果所有工具都 Allow,本地环境就要被模型玩弄于股掌之间。

世间安得双全法?


从零开始的 Agent 开发:灵活 or 可控?
https://www.letr7.com/2026/06/23/letcode-2/
作者
letr
发布于
2026年6月23日
更新于
2026年7月19日
许可协议