这段时间一直在做 AI Agent 的工程化实践,系列写到第四十五篇,我越来越感觉到一个反直觉的事实:Agent 项目最难维护的不是代码,而是"行为"本身。版本控制对 AI Agent 来说,管的绝不只是代码那部分。很多从传统软件开发转过来的工程师,本能地把 Agent 项目丢进 Git 仓库,以为提交、分支、回滚就万事大吉,结果一上线就出各种诡异的问题。今天这篇就把这个问题彻底摊开聊一聊——Agent 的版本控制到底在控制什么,以及为什么只控代码根本控不住。
先讲一件我印象特别深的事。之前我们团队把一个客服类 Agent 从测试环境发到生产,代码通过了全部的单测和集成测试,Git 记录显示"干净整洁"。结果上线后,用户反馈说这个机器人"突然变得特别油腻",明明是来做售后咨询的,它却一直跟人套近乎、发表情、用感叹号。排查了大半天,最后发现根源不在代码,而是两个星期前有人微调过 system prompt 里的语气设定,把"专业克制"改成了"热情友好",这个改动一直躺在本地文件里,没有随着应用代码一起提交。那一刻我就明白了,Agent 的行为由一堆代码之外的东西共同决定,而这些东西如果不在版本控制体系里,出问题只是时间早晚。
这篇文章不适合还在跑 Demo 的人读,它更适合那些已经开始在生产环境养 Agent、被行为漂移和回滚困难折磨过的开发者。我会把 Agent 系统里所有"会变的东西"拆开来看,再给出我实践下来可落地的一套分层版本管理方案,包括单人项目和团队协作分别怎么搞。读完你至少能回答一个问题:当你说"把这个 Agent 回滚到昨天"的时候,你到底要回滚什么。
1. 一声"回滚"引发的思考:为什么传统 Git 管不住 Agent
1.1 一次失败的午夜回滚
先继续说前面那个客服 Agent 的事故。那晚我们确认了"油腻语气"是 prompt 里的一段设定引入的之后,立刻决定回滚。我当时的操作就是标准的 Git 流程——切回上一个 tag,重新构建镜像,推上去重启服务。表面上看代码"回滚成功"了,但问题并没有解决:因为出问题的 prompt 文件在我本地,它压根没提交过仓库,所以 tag 里既没有旧版本,也没有新版本,仓库里的 prompt 始终是"上一个版本",而服务实际加载的 prompt 是我本地那份"热情友好"版。
这个事故的荒谬之处在于:代码世界里,回滚是一件确定性很强的事,但在 Agent 世界里,回滚的对象根本不是代码。那一晚我们在讨论的不是"代码版本 A 还是 B",而是"到底哪些文件在决定服务行为"。最后我用了一个土办法——从聊天记录里翻出两周前的 prompt 内容,手工恢复了一份旧版本,再重启服务。那一瞬间特别讽刺,版本控制落在了一个 Word 式的手工整理行为上。
1.2 传统版本控制的三大隐含假设
我们在 Git 的世界里待了太多年,很多人已经默认了以下三条假设成立,但它们在 Agent 项目里全都不成立。
假设一:单一事实源。Git 假设"仓库里的代码就是生产环境的真身",一切变更都必须先进仓库再发到线上。但在 Agent 项目里,prompt 经常被人在线上调试台直接改,工具配置散落在一堆后台开关里,模型微调的产物可能只存在于某台训练机的挂载盘里。这些东西都不在仓库,这就意味着仓库从出生那天起就丢失了一部分"事实"。
假设二:代码是行为的唯一决定因子。传统软件的行为由代码决定,变量赋值、逻辑分支在编译或启动时就固定了。Agent 则不同,代码只是搭了一个"框架",具体怎么说话、怎么做决策,很大程度取决于一段文本(prompt)和一个权重文件(模型)。同一套代码,换一个模型版本,甚至只换 prompt 里几个形容词,行为都能变得面目全非。
假设三:版本之间的语义边界清晰。传统版本之间是可哈希、可比较的,差异就是 diff。Agent 版本很难做 diff——你改了 5 个字,但模型输出分布的变化是连续的、不可预期的,你甚至没法回答"这次行为和上次差了多少"。
这三条假设集体失效,意味着照搬 Git 管理 Agent 从一开始就走错了方向。我们需要先在脑子里建立一个新的概念:Agent 版本 = 代码 + 配置 + 数据 + 模型行为的联合快照。
1.3 从 LLM、AI 模型到 Agent:先把概念对齐
聊版本控制之前,必须先把几个高频词的关系理清楚,因为它们经常被混着用。像 DeepSeek、GPT、Claude 这类,属于AI 模型(更准确说是大语言模型 LLM)。它们是静态的推理引擎,输入一段文本,输出一段文本。而Agent是在 LLM 之上长出来的一个系统:它有一个目标拆解的循环,会调用工具,会读取记忆,会跟外部世界交互。所以当有人问你"Agent 和 LLM 有什么区别",答案很简单——LLM 是你的大脑引擎,Agent 是装了这个大脑的完整躯干,躯干还包括手(工具)、记忆(上下文存储)、神经(编排逻辑)。
这个概念对齐对版本控制至关重要。因为如果你只把 Agent 当成"一个 LLM 套壳",那你管理版本时只会盯着模型版本换没换。但真正的 Agent 系统里,工具的 Schema 改了、Prompt 里加了一句指引、评估数据集换了标注口径,这些都是版本的变更,都可能造成行为变化。换句话说,版本控制的对象不是模型本身,而是整个 Agent 系统的"定义文件"。
2. 拆箱看货:Agent 系统里到底有哪些"会变的东西"
要搞清版本控制控制什么,就得先把 Agent 系统的"可变资产"全部盘点出来。我习惯把 Agent 拆成五个层面,每层有各自的变更频率和变更方式。
2.1 工作流与编排逻辑(代码层)
这是唯一我们能拿传统 Git 思路去管的部分。包括 Agent 的主循环、状态机、任务分解策略、工具调用后的结果处理逻辑、内存读写代码等等。这一层的特点是:变更频率低,语义清晰,每次改动的影响可以被单元测试覆盖。
拿我自己的一个自动化测试 Agent 举例,编排代码里定义了一个"先看用例优先级,再决定要不要调用浏览器"的规则。这个规则改没改,可以用一个固定的输入去验证,输出是可断言的。代码层的版本控制不需要太多创新,规范的分支、PR、review、tag 流程在这层完全适用。
2.2 提示词与角色设定(文本层)
这是最容易被忽略、也是最让 Agent 工程团队头疼的一层。System Prompt、任务描述、Few-shot 示例、工具使用说明、安全护栏提示词,全都属于这一层。它的特点是:一次微小的改动就可能引发剧烈的行为变化,而且经常无法预判是变好还是变坏。
我见过一个团队管理 prompt 的方式是"把 prompt 写在约定俗成的共享文档里,谁要改就自己复制一份改完覆盖回去"。听着吓人,但在十几人的 Agent 团队里这是常态。问题在于,共享文档没有 diff、没有作者签名、没有生效版本,出了事你根本不知道是哪一次编辑引入了问题。后面我会详细讲这层的版本控制解法,现在只需要记住:文本层是 Agent 版本控制的重灾区,也是整个体系的核心。
2.3 工具定义与函数 Schema(接口层)
Agent 要调用什么东西,取决于注册给它的工具列表。每个工具有名称、描述、入参 Schema、实现代码、权限范围。工具定义变了,Agent 的"能力边界"就变了;描述变了,Agent 的"调用倾向"就变了——它可能以前从来不主动查询订单,加了句描述"当用户订单量很大时,优先分页查询"之后,就开始频繁调用这个工具了。
这一层很有意思,因为工具的实现代码属于代码层,但工具的"对外定义",尤其是 LLM 能看到的描述文本,属于文本层。也就是说,一个工具变更可能要跨两层分别纳入版本管理。很多团队只改了工具实现,忘了同步描述,结果 Agent 按照旧描述去调用,参数对不上,整个链路就断了。
2.4 模型版本与推理参数(模型层)
Agent 用哪个基础模型,哪个精调版本,这是版本控制里绕不开的一环。同一个 Agent,从 DeepSeek-V2 切到 DeepSeek-V3,哪怕代码和 prompt 完全不动,输出风格也会漂移;temperature、top_p、max_tokens 这些推理参数同样会影响行为的一致性。
这一层的控制策略其实很"老派"——锁定。生产环境用的模型必须是一个不可变的版本引用,不能是"最新版"或"推荐版"这种动态标签。否则哪天模型 API 悄悄升级了底层版本,你的 Agent 行为就变了,而你完全不知道何时变的、为何而变。此外,模型的评测报告、能接受的输入输出格式,也要跟着版本走。
2.5 评估集与标杆答案(数据层)
Agent 项目里最昂贵、最不可替代的资产其实是数据:用于评测的验证集、人工标注的标杆答案、历史会话里抽取出的优秀案例、用户反馈的负面样本。这套数据就是 Agent 的"验收标准",版本控制如果不把评估集管起来,你根本没法判断一个版本"更好"还是"更差"。
举个例子,我们一起调优了一个内容总结 Agent。上一版我们用 200 条标注数据测出来准确率 92%,这版代码和 prompt 一点没改,只是评估集换了 200 条更难的数据,结果分数直接掉到 76%。**如果评估集本身没有版本,两个结果没有任何可比性。**数据层版本控制的核心,就是把评估集和被测的 Agent 版本绑定存储,让"82%"这句话自带上下文。
2.6 五层可变资产速查表
| 层面 | 典型内容 | 变更频率 | 传统控制是否够用 |
|---|---|---|---|
| 代码层 | 编排逻辑、主循环、函数实现 | 低 | 够用 |
| 文本层 | Prompt、系统设定、Few-shot、工具描述 | 高 | 完全不够 |
| 接口层 | 工具 Schema、权限范围 | 中 | 部分够用 |
| 模型层 | 基座模型版本、推理参数 | 中 | 必须新增约束 |
| 数据层 | 评估集、标注结果、案例库 | 中 | 往往缺失 |
3. 分层版本控制怎么做:我的四层台账方案
知道了有哪些东西要管,接下来就是实操。我在实践中逐步舍弃了"一股脑全塞进 Git 仓库"的做法,转为按变更频率和影响范围分层治理,核心是四个字:台账、锁定、绑定、回放。下面按层拆开讲。
3.1 代码层:git 照常,但依赖必须锁死
代码层没什么花活,老老实实用 Git 分支管理就行。但有一个很容易忽视的细节:Agent 的依赖锁必须比普通项目更严格。
普通 Web 项目锁requirements.txt或者package-lock.json就够了,Agent 不行。因为 Agent 的运行涉及模型 API 的 SDK、工具插件的版本、向量数据库的客户端。这些依赖版本一变,Agent 的调用行为就可能变。我把依赖锁细分成三份:Python/Node 包的锁文件、模型 SDK 的版本戳、以及外部工具插件版本清单。每次发布前,这三份必须固化成一个dependency-snapshot.json跟随发布物一起打包,防止"我的机器上能跑,生产上跑出来结果不一样"。
3.2 文本层:把 Prompt 当成源码管理
文本层是整个方案的重心。最朴素的解法就是:让 Prompt 像代码一样进 Git,但给它单独开一块目录,并且和代码的提交节奏解耦。
我所在的项目里,prompts/目录是独立于src/的,各自有不同的提交历史。Prompt 的变更不走常规的 PR 流程(因为评审人不一样),而走专门的"Prompt Review"流程:必须附带一份变更说明,写明改了哪几个字、期望解决什么问题、和上一个版本的对比评估结果。
具体操作上我会做三件事:
- 一个 Agent 版本对应一个 prompt 目录,目录名就是版本号:
customer_agent_v12/system_prompt.md、few_shots.yaml。不搞final_v2_真的最终版.md这种命名。 - Prompt 变更必须绑定一个评估结果文件。哪怕是很小的改动,也要给出"旧版 92% vs 新版 93%"这样的对比,没有评估结果的 porompt 变更不允许合入。
- 保留历史版本的完整文件夹,包括所有失效的 prompt。因为它们代表了系统的"试错经验库",哪天问题复发,翻旧版本比重新推导快太多。
这里有个技巧我特别想分享:Prompt 的版本描述要写"行为意图",而不是"文字变更"。别写"把'亲爱的'改成'您好'",要写"降低开场白亲密度,以应对老年用户群体投诉"。因为改完 prompt 之后,你还得靠这句话去判断新版行为是否符合预期,而角色的文字本身无法承载这个判断。
3.3 模型层:锁定版本,但不只看版本号
模型层的版本台账至少要记录四样东西:模型名(比如 DeepSeek-V3)、具体权重版本或 API 快照 ID、推理参数(temperature 等)、以及当次变更的实测评估对比。
特别提醒一点:用 API 的公开模型,锁版本要做到"API 提供方允许的最细粒度"。很多模型服务商会提供日期后缀的快照 ID(如deepseek-chat-20250115),发布时一定要引用这个快照,而不是引用deepseek-chat这种不带日期的别名。如果提供商不提供快照,那就把一个固定日期的评测数据存下来作为替代基线。我的经验是,没有锁定的模型引用,Agent 的行为漂移是必然的,只是时间问题。
模型层版本控制还有一个容易漏的细节:Agent 的召回模型和生成模型往往是两个不同的模型(比如用一个小向量模型做检索,用一个大语言模型做生成),这两个模型要分别记录、分别锁定,不能混在一起写"模型版本:V3"。
3.4 数据层:评估集是 Agent 的验收标准,必须可回溯
评估集和标杆答案的版本控制,我的做法是做成一个独立的eval/仓库,和主应用代码完全分离。每个评估集版本有一个唯一的 ID,包含:
- 输入样例集合(最好是 jsonl 格式,每行一个用例)
- 每道题的标杆答案与评分标准
- 数据来源说明(是人工标注还是线上抽取、标注人是谁、标注日期)
- 版本变更日志(为什么加了这些用例,是为了覆盖哪类失败场景)
有了这个体系之后,"回滚"就多了一个决策依据:**出了问题,先看是代码回归、Prompt 漂移,还是评估集口径变了。**另外,我强烈建议每一条评估样例都要带"标签",比如"订单查询类"、"多轮澄清类"、"工具调用失败恢复类"。因为改了个 Prompt,可能整体分数微涨,但某一类标签的分数暴跌,只有带标签的评估集才能发现这种局部劣化。
3.5 绑定与回放:把四层台账合成一个版本号
单独的台账是碎片,必须有一个机制把它们绑定成一个可回放的整体。我会在发布时生成一个agent-version.yaml文件,四个 lock 字段:
version: v48 code: git_commit: a1b2c3d4e5f6 dependency_snapshot: dep-snap-20250603.json prompt: prompt_dir: customer_agent_v48 prompt_owner: @lihua model: primary_model: deepseek-v3-snapshot-20250512 retriever_model: text-embedding-snapshot-20250420 inference_params: { temperature: 0.2, top_p: 0.9 } eval: eval_set_id: eval-valid-v14 eval_score: { accuracy: 0.94, tool_failure_rate: 0.02 }这个文件就是 Agent 版本的"全息快照"。回滚到 v44,操作的其实是把这个 yaml 里的四个字段全部恢复:切代码、恢复 prompt 目录、切模型快照引用、挂载 v14 的评估集。到这一步,"回滚"才叫真正的回滚,因为恢复的不只是代码,是完整的行为指纹。
4. 从单人到团队:Agent 工程的提交流程与发布门禁
单人项目用好上面的台账就够了,但是一旦 Agent 项目进入团队协作甚至工业化流水线,光有台账还不够,得有配套的流程和门禁。这部分的经验来自我们转型过程中踩过的坑,分享几个已经稳定跑了一年多的实践。
4.1 分支模型:Prompt 变更独立于代码分支
我们主仓库采用 Trunk-based 模式,但prompts/目录用一套特殊的分支策略。简单来说:Prompt 的变更不要求跟代码同一个分支合并。
具体做法是:任何人要调 Prompt,先基于当前生产版本拉一个prompt-review/chinese-warmth这种分支,只改 Prompt 和相关评估文件。改完跑一遍"变更前 vs 变更后"的对比评估,把结果贴到 PR 描述里。评审人是产品负责人 + 另一个资深工程师,不是代码审查者。合并之后,这个 Prompt 并不会立即生效,而是生成一个候选版本,等下一次 Agent 发布时一起上线。
为什么这么设计?因为代码改动和 Prompt 改动根本不在一个节奏上。代码可能一周合并 20 次,Prompt 则是一个月只改 3 次,每次都要慎重。让它们拥有独立的生命周期,可以避免代码评审阻塞 Prompt 迭代,也避免 Prompt 的随意改动污染代码历史的可读性。
4.2 评估门禁:没有人 Review 的 Prompt 不要上线
我见过太多 Agent 团队死在"人人都能改 Prompt"上。客服团队觉得语气太冷可以自己偷偷改一句,运营觉得答案不接地气又改一句,三个星期后没人知道当前线上 prompt 是谁写的、为什么写成这样。所以必须给 Prompt 的变更装上门禁。我在流程里设了三道闸门:
- 可回溯:没有关联评估集版本的 Prompt 变更,一律打回。你要改一件事,就得先能证明怎么测它。
- 有对比:必须填写"旧 prompt 响应 vs 新 prompt 响应"在同一个评估集上的表现。哪怕你说这个改动纯属措辞优化,也得按同一个标杆题跑一遍告诉我看得出效果。
- 责任到人:每个 Prompt 变更都有 owner。出了行为问题,owner 负责解释"当初为什么这么改",而不是众人都说"我不知道谁改的"。
听起来有点重,但这是防止 Agent 体系失控的最低配置。我们早期没有这套门禁,靠的是人肉记忆,结果上面那个"油腻客服"的事故,我们定位了一个通宵。
4.3 发布与回滚:恢复的不只是代码
有了统一的版本台账,发布也就从"发布代码"变成"发布一个 Version Entry"。我们的发布流程如下:
- 构建产物时,自动生成
agent-version.yaml的候选版本。 - 发布系统把代码、Prompt 目录、模型快照引用、评估集 ID 捆绑,形成一个不可变的发布包。
- 生产环境部署后,开启一段"金丝雀评估期":流量切 5% 给新版本,同时把线上请求的日志采样喂给评估集自动打分。
- 效果达标后切全量,同时把这次发布对应的
agent-version.yaml打上production标签。
回滚流程也一样:不是切容器镜像,而是把上一个production标签对应的整套配置重新部署。请注意,代码可以秒回退,但模型快照引用如果没记录,回滚就没法执行。模型快照引用必须永远跟着发布包走,不能只记在谁的聊天记录里。
4.4 实验追踪:当次的 Traces 比最终答案更值钱
门禁和台账都是"事前"措施,真正能帮你在线上定位问题的是"事后"的追踪数据。我要强烈建议每一条 Agent 的请求,尤其是被评估集判定为失败的请求,完整保留运行轨迹:什么工具被调用了、调用的输入输出是什么、中间推理步骤是什么、最终回复是什么。这些 Trace 数据要按 Agent 版本号归档,而不是按时间归档。
为什么这么强调?因为 Agent 的行为是非确定性的。同一个 prompt、同一道题,跑五次可能是五种答案。你只有把当次完整的推理轨迹存下来,才能在复盘时说清楚"这个 bug 是 v47 引入的,因为它的工具调用参数格式和 v46 不同"。没有 Traces,你所谓的版本对比只能停留在"输入-输出"黑盒层面,而黑盒层面的对比无法告诉你该改哪一层。也可以理解成:评估集是考卷,Trace 是答题草稿纸。只看考卷分数你能判断涨跌,但只有草稿纸能告诉你它因为哪一步做错了而丢分。
5. 关于落地,我还想说的三件事
方法论讲得再多,落地的时候总有一些"没人提醒你、但踩了才知道"的细节。挑三个最重要的分享出来,希望能给你省掉几个通宵。
5.1 小团队别一次上太重,先补最痛的一层
如果你的团队只有三五个人,Agent 项目还在探索期,我建议不要一次把五层台账全建起来,太重了,你的业务可能三天两头推翻重来。先挑最痛的那一层下手:如果你天天被"线上的 Agent 怎么突然变了一个人"困扰,就先管好 Prompt 层;如果你天天被"这个版本效果到底好不好"困扰,就先管好评估集。等这两层跑顺了,再补模型快照和依赖锁。一个只有代码层版本管理的 Agent 项目是裸奔的,但一个只建了数据层没建模型层的项目至少还有评估集兜底,能看出变差了。渐进落地的顺序比一步到位的完美起飞靠谱得多。
5.2 平台级资产:统一的 Agent 仓库是终局
团队的 Agent 多了以后,我会强烈建议搭一个统一的"Agent 资产中心",不管是自研一个页面还是用现成的开发平台。这个中心的核心功能就一条:把 agent-version.yaml 变成一条可查询的记录,所有 Agent 的版本、评估报告、Trace 数据都能在一个界面里被检索和比对。维度包括:每个 Agent 有多少个版本,每个版本是谁在什么时间发布的,当时的评估分数是什么,生产环境当前引用的是哪个版本,上次回滚是为哪个事件。
当你的 Agent 数量超过 10 个,靠人肉维护 Excel 台账是必崩的。我们当时就是用一张共享表格硬撑,结果某天有人误删了一行,导致一个 Agent 在生产跑了一个无法追溯的版本,直接把整个发布链的信任度打没了。平台化建设不是开源的产物,是 Agent 工程化路上避不开的关口。
5.3 最终台账长什么样:一个真实的版本条目
最后给你看一条我们内部实际在用的版本台账记录,删去了所有商业敏感内容,保留了结构。你可以直接照抄这个模板建立自己的版本条目。
| 字段 | 值 |
|---|---|
| Agent 名称 | order-assistant |
| 版本号 | v48 |
| 发布时间 | 2025-06-03 14:22:08 |
| 发布人 | @lihua |
| 代码 Commit | a1b2c3d4e5f6 |
| 依赖快照 | dep-snap-20250603.json |
| Prompt 目录 | order_assistant_v48/ |
| 模型组合 | deepseek-v3-snapshot-20250512 + text-embedding-snapshot-20250420 |
| 推理参数 | temperature=0.2, top_p=0.9 |
| 评估集 | eval-valid-v14 |
| 评估结果 | accuracy=0.94, tool_failure_rate=0.02 |
| 生产状态 | 已全量,2025-06-03 15:30 切换 |
| 回滚备注 | 无 |
有了这一条记录,不管未来谁接手这个 Agent,都能在一个文件里回答四个问题:这系统由什么构成、它当时被验证过什么、它在线上表现如何、出事了我该恢复什么。
关于 Agent 版本控制这件事,我最初也天真地以为"版本控制嘛,就是 Git",直到生产环境给了我几下重锤,才慢慢梳理出"代码 + 提示词 + 工具定义 + 模型 + 评估集"这套框架。现在每次版本发布,我唯一关注的不是代码合没合,而是那个agent-version.yaml有没有完整生成——因为它才是 Agent 真正的"身份证"。如果你正在被 Agent 行为漂移、回滚失忆这些问题折磨,不妨从本周就做一件事:把你线上 Agent 正在用的 Prompt 目录整个 commit 进仓库,建一个最朴素的版本号,先把它纳入控制。后面的事,我们随时可以再聊。