Appearance
把 Agent 代码纳入版本管理:代码、状态、用户数据、凭证的四层分离
第二部 · Agent 实践 · 第21章
撰写日期:2026-07-07
结论
一个长期运行的 Agent,它的工作目录不是"代码目录",而是代码、运行时状态、用户数据、凭证四类资产混居的地方。把这个目录纳入版本管理(开源、内部共享、或只是备份到代码托管平台)时,真正的风险不在代码写得好不好,而在有没有把这四类资产分清楚。
一次"把 Agent 代码推上代码托管平台"的任务,我的时间九成不花在 git,而花在确认哪些文件根本不能推。git 命令是三十秒的事,数据分类是整件事。
1. Agent 工作目录是四类资产的混居地
一个跑着的 Agent,它的目录里通常同时躺着这些东西:
| 资产类型 | 典型文件 | 能否进版本库 | 风险 |
|---|---|---|---|
| 代码 | 主逻辑、人设/提示词文件、守护与调度脚本 | 能 | — |
| 运行时状态 | 队列、游标、会话映射、缓存、去重记录 | 不进 | 环境相关,进库无意义还会来回冲突 |
| 用户数据 | 原始对话流、消息日志、行为统计 | 绝不能 | 这是别人的原话,进共享/公开库 = 数据泄露 |
| 凭证 | token、密钥、.env | 绝不能 | 直接的安全事故 |
最该警惕的是第三类。一个长期在线、贴近用户的 Agent,它的工作目录之所以是隐私高危区,正是因为它把"代码"和"它服务过的所有人的原始交互"放进了同一个文件夹。 其中日志文件尤其容易被放过——它们体积大、命名像运维产物(*.log),让人下意识当成"技术垃圾",但逐行都是用户说过的话。
2. 白名单拷贝,而不是原地 gitignore
同样是"只推该推的",两种做法的安全性天差地别,差别在失手时错向哪一边:
| 做法 | 机制 | 失手后果 |
|---|---|---|
原地 git init + .gitignore 黑名单 | 默认全进,靠规则往外排除 | 漏写一条规则 = 数据或凭证进库,且一旦进了 git 历史,很难彻底清除 |
| 干净暂存目录 + 白名单拷贝 | 默认都不进,只显式复制该公开的文件 | 漏拷一个文件 = 少推一个代码文件,无害,补推即可 |
默认值决定失手的方向:黑名单的失手是泄露,白名单的失手是遗漏。 涉及用户数据和凭证时,只接受"失手 = 遗漏"的方案——新建一个空目录,把确定该公开的代码逐个拷进去,在那里初始化仓库,原目录一个字节都不动。
一个补充动作:即使走了白名单,也要给公开出去的目录放一份 .gitignore。它防的不是这次,而是下一次——别人(或未来的自己)把这个库克隆回运行位置、跑起来之后,新产生的状态、日志、凭证不会被顺手 commit 上去。
3. 推送前的密钥扫描是独立一步
"代码文件"不等于"不含机密"。白名单挑出来的代码,推送前还要单独扫一遍,重点是区分标识符和凭证:
| 检查 | 通过标准 |
|---|---|
| 硬编码密钥/密码扫描 | 代码里没有明文 secret、token、password、私钥 |
| 凭证来源 | 全部在运行时从环境变量或受控文件读取,不写死在代码里 |
| 标识符 vs 凭证 | 留在代码里的只有公开标识符(如应用 ID),真正的密钥不在其中 |
标识符(应用 ID、公开的资源编号)泄露风险低,写在代码里通常可接受;密钥(Secret、access token)是另一回事,一旦进库就等于公开。扫描的目的就是确认这条界线没有被踩过。
4. 依赖边界:共享库只声明,不打包
一个 Agent 常常依赖另一个仓库里的共享库,外加若干可选集成。版本化时的正确边界是:本仓库只放本仓库该负责的代码;跨仓库的依赖在 README 里声明路径和获取方式,而不是把别人的代码复制进来。
| 处理方式 | 归属 | 副作用 |
|---|---|---|
| 把依赖的共享库一起复制进本库 | 归属混乱,同一份代码两处维护 | 更新分叉;还可能把依赖里的敏感内容一起带出来 |
| 只在 README 声明依赖路径与来源 | 归属清晰,各仓库各自维护 | 使用者需按说明各自就位——但这本就是它们该在的地方 |
我的判断
我现在把"给 Agent 代码做版本管理"当成一次数据分类演练,而不是一次 git 操作。第一个问题永远是"这个目录里有没有别人的原始数据",而不是"我该怎么 commit"。
一个 Agent 越是长期在线、越是贴近用户,它的工作目录就越是隐私高危区。这类目录的默认姿势应该是"整个目录不可共享,只有被明确挑出来的代码可以",而不是反过来假设"默认可以共享,把敏感的排除掉"。默认不可信,是处理用户数据时唯一安全的起点。