agent 该记什么,不该记什么
《ops-agent 建造笔记》第 8 篇 代码基线:v0.1.0-rc.1,提交
d897d2c95e70(2026-10-25 发布的候选版) 主要代码:agent/skills.py、agent/memory.py、agent/tools.py、agent/loop.py
0. 两类重复
我在设计排查会话时,先把容易重复的内容分成两类。下面的主机与告警都是说明用的例子,不是已经接入生产的资产清单。
第一类是流程重复。假如要排查 nginx 5xx,先看错误日志、再看上游、再核对配置改动,这种顺序不该每次都让模型重新发明。我想把确认过的步骤保留下来。
第二类是事实重复。比如“db-01 的 OS = Ubuntu 24.04”,或者经人核实过的环境约定。它们不需要长成一篇排障手册,却值得在后续主代理会话里保留。
这两类看起来像,解法其实不一样。流程要的是能复用、能改、写得讲究一点;事实要的是随手就能记下来,别给我添麻烦。它们分别长成了技能库(skills)和长期记忆(remember)。
1. 技能库按需加载
技能的形态是一份 Markdown playbook,frontmatter 只认一个 description:
---
description: nginx 返回 5xx 时的只读排查顺序
---
# 证据清单
1. ...
# 停止条件
- ...
仓库 skills/ 自带几份(nginx-5xx、disk-full、safe-service-restart、k8s-crashloop、docker-restart-loop、dns-tls、backup-recovery),~/.ops-agent/skills/*.md 同名覆盖,个人或团队自定义不用改仓库。
加载机制是模型先查目录,用时才载入正文。skill 不带 name 时返回技能名和 description,判断相关后再调用 skill {"name": "nginx-5xx"}。目录也不是 _default_system 每轮自动注入的内容,要由模型实际调用工具获取;它只比全文小得多。
catalog() 只读每个文件头部 4096 字符来提取描述,load() 对单篇正文最多读取 20001 字符,超过 20000 字符就截断并说明。名字有形状校验,不允许用路径穿越去加载任意文件;用户目录同名优先覆盖仓库版本。
为什么这么做:候选版内置 7 份 playbook,全量注入会让每次请求都为无关步骤付上下文成本。实际 token 开销还受 tokenizer 和文件长度影响;在这一版里,我先把目录与正文分开读取,并限制单篇大小。
playbook 不绕过守门,这是我给技能库划的最重要的线。skills 只提供步骤,写操作仍然走变更单流程。一份技能里写着"重启 nginx",agent 执行到这一步照样要建变更单、要确认、要审批戳。playbook 是知识,不是授权。这个边界一旦被突破,比如允许技能声明"本技能内命令免审批",技能文件本身就成了攻击面,一份被污染的技能就是一份提权说明书。
我按四件事核对内置 playbook:证据清单、停止条件、变更与审批、可重复验收。它们可能还有只读命令、回退与专门边界,不是 loader 强制执行的固定四段 schema。agent/skills.py 只解析 description 与正文;步骤质量要靠文档审查,实际命令仍由工具守门。
2. 记忆的写法约定比存储机制重要
remember 工具把事实记入 ~/.ops-agent/memory.md(0600,单条 ≤500 字符),之后在主代理与定时任务的每轮 system prompt 里自动召回。
存储机制简单:纯文本追加,按文件顺序取尾部 8000 字符作为主要召回范围,截掉残缺首行,并加一条省略说明。没有向量库、索引或结构化查询。它按文件中的新旧顺序选内容,不会理解哪条事实更重要。
但写法约定管得非常细:
主机: 属性 = 值 # db-01: OS = Ubuntu 24.04
服务 X depends_on db-01 # 依赖关系
同类事实用同一词汇
为什么管这么细?因为召回和将来按主机结构化拆分都得靠这份一致性。记忆里一会儿是"db-01 是 Ubuntu",一会儿是"Ubuntu 跑在 db-01 上",一会儿是"db-01 系统 24.04",三个月后这份文件就没法扫读了,模型和我都很难高效利用,将来想做"按主机查事实"也无从解析。写法约定就是在没有结构的地方人工维持结构。
几条配套的规矩:
- 只记结论,一次性状态不记。"磁盘现在 87%"不记,明天就过时;"这台机器磁盘常年 80% 以上,阈值已调高"可以记。
- 反复出现的排查模式建议晋升进 skills。memory 保持流水,playbook 沉淀流程。分工是 memory 记"是什么",skill 记"怎么做"。
- 过时条目直接编辑文件删除。不提供"删除工具",记忆与现状冲突时 agent 以现状为准并记录更正。
memory.add()先比较脱敏前后内容,识别出凭据就拒绝写入;通过时在锁内追加带日期的条目,收紧文件权限到 0600,并写memory-add审计。秘密识别仍是启发式,不保证识别一切格式。- 候选版又补了读侧保护:人工编辑或恢复出来的旧文件不一定经过
add(),所以recall()先对全文脱敏,再截尾。如果先截尾,PEM 的 BEGIN 标记可能已经被裁掉,剩下的私钥正文反而难以识别。原文件不因此被重写。 - 定时任务与子代理不提供
remember。主代理和定时任务的 system prompt 经_default_system()注入已有记忆;子代理用独立的SUBAGENT_PROMPT,不会自动带上这份长期记忆。需要的背景应写进明确的子任务;这和主代理“知道”的事实不是同一份隐含上下文。
3. 子代理:并行排查,只能看不能动
和技能库同属能力扩展的还有子代理(M3):通常把 2–4 个独立排查任务并行下发,例如分开取日志、配置与资源证据。实际 _subagent 接受 1–4 个非空任务,代码没有“至少两个”的限制。各自返回结论与证据,再由主代理汇总。
SUBAGENT_TOOLS 的实际集合是 bash(仅 R0)、read、skill、ops_list、events、ops_executions。没有批准、执行、回滚、记忆写入或继续派生子代理的入口;每个任务最多 8 轮,沿用父代理模型与 deadline。
这些限制每一条都有出处。只读是因为子代理在后台并行跑,没人盯着逐个确认,写操作必须回到主代理的确认链里。不能派生是防递归展开失控,一次最多 4 个任务是我在这一版定下的边界,并不代表已经测出 4 是最佳并发数。8 轮是因为子代理是来取证的,不是来研究的,8 轮拿不到结论就该把已取证的内容带回来。
子代理会话同样落在 ~/.ops-agent/sessions/,可以在 /resume 里回看排查过程。并行有个前提,就是必须可回看,否则"主代理汇总说 web1 正常"这句话没法验证。
4. 现状
知识这块仍然保持纯文本:没有向量检索、知识图谱或自动 skill 生成。长期使用样本还不足以说明这套选择能撑到什么规模。我目前保留它,是因为写入、读取和人工纠错的路径都短,后面还可以按目标拆文件。
欠账是记忆更新:条目虽然带写入日期,却没有主动过期、按目标检索或原位更新工具。冲突时遵循当前证据、追加更正,旧条目要由人编辑清理。日期不是 TTL,追加更正也不会自动删除旧事实。
本版读取记忆时的脱敏与子代理协议回归已经补上,但本版 W8 的真实环境与修复后连续 7 天观察仍未完成;我还不能用候选版的测试通过替代长期知识质量的验收。
下一篇讲收尾的那块:状态备份与恢复演练。整个系统的状态全在 ~/.ops-agent/ 一堆 JSON 文件里,这批文件自己坏了怎么办。W8 的备份、验证、恢复,以及那次 10/10 的本地隔离演练。
这一版的代码落点
- 技能目录、覆盖与读取上限:agent/skills.py
- 记忆写入与先脱敏后截尾:agent/memory.py
- 独立子代理及工具白名单:agent/tools.py
- 主代理/调度与子代理 prompt 的区别
- 导入记忆与会话的回归证据
- 内置备份恢复 playbook 的证据与停止条件