Appearance
21|只读与写操作工具:分级设计
上一篇讲了最复杂的 BashTool。运维排查里大部分工具不是 shell 命令,而是结构化的只读和写操作工具——日志查询、指标读取、配置变更、文件编辑。
这些工具比 BashTool 可控,但也有自己的设计原则。只读工具和写操作工具的风险完全不同,要分级设计。本篇讲这两类工具的设计要点。
只读工具:频率高、要快
只读工具是运维排查的主力:查日志、读指标、检索 Runbook。排查一个问题时,可能查十几次日志、几次指标,才执行一次写操作。只读工具的调用频率远高于写操作工具,它的效率直接决定 agent 的整体响应速度。
只读工具的设计重点是查询性能和返回格式。
白名单看 flag 语义。即使是结构化的只读工具,参数也要校验语义。read_log(service, lines, since) 里 lines 不能无限大、since 要是合法时间。不能只校验类型,要校验业务规则——service="nginx" 类型合法,但当前环境没有 nginx 实例就是无意义的。
返回格式统一。所有只读工具返回统一结构,agent 循环处理结果时不用为每个工具写不同解析逻辑:
python
def read_log(service: str, lines: int = 100) -> dict:
return {
"ok": True,
"data": fetch_log(service, lines),
"truncated": False, # 是否被截断
"meta": {"source": service, "lines_returned": lines},
}truncated 标记特别重要。模型需要知道"只看到了前 500 行,后面还有",否则会以为这就是全部日志。
截断标记。工具返回大段数据时要截断,但必须告诉模型截断了。truncated: True 加上"完整日志见 trace"的提示,模型才知道数据不完整、可能需要换查询条件再查。
按角色放权。只读不是全放。不同角色的 agent 看到不同的只读工具——Explore agent 只能看到搜索和查询类工具,general-purpose agent 可以看到全部。权限粒度到工具级别,不是"只读=全放行"。
只读工具的隐性风险
只读工具不是绝对安全。cat /etc/shadow 也是只读的,但读到了不该读的密码文件。只读工具的权限放开要考虑返回内容是否敏感——日志里可能含密钥、配置文件可能含密码。返回结果可能需要额外过滤,把敏感字段脱敏后再交给模型。

只读工具里加缓存是另一个陷阱。为了性能在 read_log 里加了本地缓存写入,这违反了只读的定义——并发执行时缓存状态可能不一致,调试时难以复现。只读工具应该是纯函数,缓存放外层调度器。
写操作工具:低频、要审
写操作工具是另一类:执行命令、变更配置、编辑文件。它的频率低,但风险高,设计重点是审批和可追溯。

分级审批防审批疲劳。审批机制的最大敌人不是用户不批准,是审批疲劳。每个写操作都弹审批,用户会养成全部点同意的习惯,审批就失效了。分级审批:低风险(修改临时配置文件)自动记录不实时确认,中风险弹窗确认,高风险强制等待人工复核。
python
WRITE_TOOL_RISK = {
"edit_temp_config": "low", # 自动记录
"edit_config": "medium", # 弹窗确认
"restart_service": "high", # 强制人工复核
}
def approve_write(tool_name, args, context):
risk = WRITE_TOOL_RISK.get(tool_name, "high")
if risk == "low":
log_action(tool_name, args)
return True
if risk == "medium":
return ask_user(tool_name, args)
return ask_human_review(tool_name, args) # high 强制人工审批上下文要完整。操作者需要知道"为什么模型走到这一步""前面查到了什么证据""不执行还有什么替代方案"。审批弹窗不能只显示待执行的操作,要包含完整的 trace。缺少上下文的审批是盲目审批。
拒绝信息回填。审批拒绝后不能直接结束会话。把拒绝信息回填给模型,让它基于拒绝调整策略。用户拒绝重启 Redis,模型可以转而建议检查 slowlog 和内存使用情况。直接结束会话的话,前面的排查全白费了。
FileEditTool 的约束
文件编辑是典型的写操作工具,它的设计有两个关键约束。
搜索替换模式。FileEditTool 不让模型直接给整份文件内容,而是用搜索替换:给 old_string 和 new_string,工具找到 old_string 替换成 new_string。这避免了"模型重写整个文件时丢内容"的风险。
old_string 必须唯一匹配。如果 old_string 在文件里出现多次,替换哪个不确定。要求 old_string 唯一匹配,看起来严格,但能避免"改错位置"。
不能编辑未读过的文件。模型必须先看到文件当前状态,再发起修改。这是强制性的——没读过文件就编辑,模型不知道文件里有什么,可能覆盖重要内容。工具执行前检查"这个文件是否在当前上下文里出现过",没出现过就拒绝。
python
def file_edit(file_path, old_string, new_string, context):
if file_path not in context.file_history:
return {"ok": False, "error": "未读过该文件,不能编辑"}
content = context.file_history[file_path]
if content.count(old_string) != 1:
return {"ok": False, "error": "old_string 必须唯一匹配"}
# 执行替换操作审计
写操作工具必须有完整审计。谁批准了什么操作、什么时候、基于什么上下文,都要记录。这是生产系统的底线要求,事后追溯和合规检查都靠它。
python
def audit_write(tool_name, args, approved_by, context):
record = {
"time": now(),
"tool": tool_name,
"args": args,
"approved_by": approved_by,
"session_id": context.session_id,
"trace_ref": context.trace_id,
}
audit_log.append(record)审计日志和 trace 不同——trace 记录执行过程,审计记录的是"谁授权了这个写操作"。审计日志要永久保留,不受 trace 的清理策略影响。
分级之后
只读和写操作工具的分级设计讲完了。到这里,单个 agent 的工具系统基本完整:接口声明能力边界(18)、权限体系管授权(19)、BashTool 专门防注入(20)、只读和写操作分级设计(21)。
但单个 agent 的上下文容量有限。复杂排查任务——跨多个系统、需要并行验证、涉及不同专业领域——单个 agent 容易被上下文塞满或能力不够。这时候要多 agent 协作。
下一篇讲多 Agent:什么时候该用多 agent、用哪种编排模式。