GitOps 实践——AI 配置的版本化管理
GitOps 实践——AI 配置的版本化管理
我们团队有一段时间的工作状态让我很不安。
Prompt 文件存在各自的服务里,不同版本的 Prompt 靠注释区分,有时候找不到"这个 Prompt 是什么时候改的"。向量数据库的 Schema 是运维同学手动改的,没有文档。模型配置(用哪个模型、温度系数、最大 Token 数)分散在各个服务的 application.yml 里,环境之间不一致——测试用的 temperature=0.7,生产悄悄改成了 0.3,两个月后才发现。
出问题的时候,我们甚至不确定是代码的问题还是配置的问题。回滚了代码,Prompt 还是新版的。这种情况非常糟糕。
真正让我下决心做 GitOps 是一次线上事故:有人在生产环境直接修改了 System Prompt,想临时修一个回复风格的问题,结果改错了,把一段关键的输出格式约束删掉了,导致下游解析 JSON 失败。这个改动没有经过 review,没有变更记录,发现问题花了两个小时,回滚花了半个小时。
GitOps 解决的核心问题就是这个:让所有环境的状态都由 Git 仓库中的声明来驱动,任何改变都必须经过 Git,任何状态都可以审计和回滚。
AI 配置的范围比你想的要广
先梳理哪些东西应该进 Git:
Prompt 模板:System Prompt、User Prompt 模板、Few-shot 示例,这些是 AI 应用的"代码",必须版本化管理,必须 review。
模型配置:使用哪个模型(model name)、temperature、top_p、max_tokens、frequency_penalty 等推理参数。这些参数对输出质量影响很大,必须追踪。
向量数据库 Schema:Collection 定义、字段类型、索引配置、Embedding 维度。这是 RAG 应用的基础设施配置,Schema 变更意味着需要重建索引,必须有变更记录。
RAG 流水线配置:Chunk 大小、Chunk 重叠度、TopK 检索数量、相似度阈值。这些参数直接影响 RAG 效果,也必须版本化。
功能开关:哪些功能默认开启、哪个用户群可以使用新功能、A/B 测试配置。
# AI 配置仓库的目录结构
ai-configs/
├── prompts/
│ ├── customer-service/
│ │ ├── system-prompt.txt
│ │ ├── intent-classification.txt
│ │ └── few-shots/
│ │ └── examples.json
│ ├── code-assistant/
│ │ ├── system-prompt.txt
│ │ └── review-prompt.txt
│ └── rag-qa/
│ ├── system-prompt.txt
│ └── query-rewrite.txt
├── model-configs/
│ ├── production.yaml
│ ├── staging.yaml
│ └── development.yaml
├── vector-db/
│ ├── schemas/
│ │ ├── knowledge-base.json
│ │ └── user-profiles.json
│ └── migrations/
│ ├── 001_initial_schema.json
│ └── 002_add_category_field.json
├── rag-pipeline/
│ ├── chunking-config.yaml
│ └── retrieval-config.yaml
└── feature-flags/
├── production.yaml
└── staging.yamlArgoCD 的工作原理
ArgoCD 是 Kubernetes 原生的 GitOps 工具。核心逻辑是:
- 你在 Git 仓库中声明了"期望状态"(比如某个 ConfigMap 的内容)
- ArgoCD 持续监听 Git 仓库的变化
- 发现 Git 仓库的状态和集群中的实际状态不一致时,自动同步(或告警等待人工审批)
对于 AI 配置,我们用 ConfigMap 和 Secret 存储,ArgoCD 负责把 Git 里的配置同步到集群。
基于 ArgoCD 的 AI 配置自动同步
第一步:把 AI 配置存为 Kubernetes ConfigMap
# ai-configs/k8s/customer-service-prompts.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: customer-service-prompts
namespace: ai-prod
labels:
app: customer-service
config-type: prompts
# 版本标签,便于追踪
version: "v2.1.0"
data:
system-prompt.txt: |
你是「XX 公司」的智能客服助手,负责处理用户的售后问题。
你的职责范围:
- 订单查询和状态跟踪
- 退款和换货申请处理
- 产品使用问题解答
你的行为准则:
- 始终保持友好、专业的语气
- 无法处理的问题转人工,不要自行给出无法兑现的承诺
- 回复必须是 JSON 格式:{"action": "...", "message": "...", "need_human": false}
当前时间:{current_time}
用户 ID:{user_id}
intent-classification.txt: |
请分析用户的意图,返回以下格式的 JSON:
{"intent": "refund|exchange|query|complaint|other", "confidence": 0.0-1.0, "entities": {}}
只返回 JSON,不要有其他文字。
---
# ai-configs/k8s/model-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ai-model-config
namespace: ai-prod
labels:
config-type: model
data:
production.yaml: |
models:
primary:
name: "gpt-4o"
temperature: 0.3
max_tokens: 2048
timeout_seconds: 30
fallback:
name: "gpt-4o-mini"
temperature: 0.3
max_tokens: 2048
timeout_seconds: 20
rag:
embedding_model: "text-embedding-3-small"
top_k: 5
similarity_threshold: 0.75
chunk_size: 512
chunk_overlap: 50第二步:ArgoCD Application 配置
# argocd/applications/ai-configs-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ai-configs
namespace: argocd
# 给 ArgoCD Application 加标签,便于按团队过滤
labels:
team: ai-platform
env: production
spec:
project: ai-platform
source:
# Git 仓库地址
repoURL: 'https://github.com/company/ai-configs.git'
# 监听 main 分支
targetRevision: main
# 只同步这个目录下的资源
path: k8s/production
# 使用 Kustomize 管理多环境差异
kustomize:
images:
- ai-configs:latest
destination:
server: 'https://kubernetes.default.svc'
namespace: ai-prod
syncPolicy:
automated:
# 自动同步:Git 有变更就自动应用到集群
prune: true # 删除 Git 里不存在的资源
selfHeal: true # 如果集群状态被人手动改了,自动恢复
syncOptions:
- Validate=true # 同步前校验 YAML 格式
- CreateNamespace=true
# 如果同步失败,等 5 分钟后重试,最多重试 5 次
retry:
limit: 5
backoff:
duration: 5m
factor: 2
maxDuration: 30m
# 健康检查:同步后检查 ConfigMap 是否确实更新
ignoreDifferences:
# metadata.annotations 经常被 K8s 自动修改,忽略这类差异
- group: ""
kind: ConfigMap
jsonPointers:
- /metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration第三步:ArgoCD Project 配置(访问控制)
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: ai-platform
namespace: argocd
spec:
description: "AI Platform 配置管理"
# 只允许从这个 Git 仓库同步
sourceRepos:
- 'https://github.com/company/ai-configs.git'
# 只允许部署到 ai-prod 和 ai-staging 命名空间
destinations:
- namespace: ai-prod
server: 'https://kubernetes.default.svc'
- namespace: ai-staging
server: 'https://kubernetes.default.svc'
# 只允许同步 ConfigMap 和 Secret,不允许同步 Deployment 等
# 这样 AI 配置仓库和应用部署仓库严格分离
clusterResourceWhitelist:
- group: ''
kind: Namespace
namespaceResourceWhitelist:
- group: ''
kind: ConfigMap
- group: ''
kind: Secret
# RBAC:只有 AI 平台团队成员有权限手动触发同步
roles:
- name: ai-platform-admin
description: "AI 平台管理员,可以手动触发同步和回滚"
policies:
- p, proj:ai-platform:ai-platform-admin, applications, *, ai-platform/*, allow
groups:
- ai-platform-team
- name: ai-developer
description: "AI 开发者,只能查看状态"
policies:
- p, proj:ai-platform:ai-developer, applications, get, ai-platform/*, allow
groups:
- ai-developers向量数据库 Schema 的版本化管理
这部分是我们做 GitOps 时最费力气的地方。
向量数据库(比如 Milvus、Qdrant)的 Collection Schema 变更,比关系型数据库的 migration 更麻烦:如果改了 Embedding 维度,整个 Collection 的数据需要重新向量化,不是改个表结构那么简单。
我们的做法是参考数据库 migration 的思路,为 Schema 变更写 migration 脚本,并在 Git 里保存历史:
# ai-configs/vector-db/migrations/003_add_metadata_field.py
"""
Migration: 为 knowledge_base Collection 添加 category 字段
Author: zhangge
Date: 2024-01-15
Reason: 支持按分类检索,减少全库扫描
影响评估:
- 是否需要重建索引:否(只是添加标量字段)
- 数据迁移量:需要为现有 1.2M 条记录补填 category 字段
- 预计执行时间:约 30 分钟
- 回滚方案:删除 category 字段(数据不丢失)
"""
from pymilvus import Collection, FieldSchema, DataType
import logging
logger = logging.getLogger(__name__)
MIGRATION_ID = "003"
COLLECTION_NAME = "knowledge_base"
def up(client):
"""执行 migration"""
collection = Collection(COLLECTION_NAME)
# 检查字段是否已存在
existing_fields = [f.name for f in collection.schema.fields]
if "category" in existing_fields:
logger.info(f"Migration {MIGRATION_ID}: category 字段已存在,跳过")
return
# 添加新字段
# 注意:Milvus 不支持直接 ALTER COLLECTION,需要创建新 Collection 并迁移数据
# 这里展示的是通过动态字段的方式添加,更安全
logger.info(f"Migration {MIGRATION_ID}: 开始为 {COLLECTION_NAME} 添加 category 字段")
# 为现有数据补填默认值
collection.load()
# 批量更新现有记录(分批处理,避免内存溢出)
batch_size = 1000
offset = 0
while True:
results = collection.query(
expr="id >= 0",
output_fields=["id"],
offset=offset,
limit=batch_size
)
if not results:
break
ids = [r["id"] for r in results]
# 用 upsert 补填 category 字段默认值
collection.upsert([
{"id": id_val, "category": "uncategorized"}
for id_val in ids
])
offset += batch_size
logger.info(f"Migration {MIGRATION_ID}: 已处理 {offset} 条记录")
logger.info(f"Migration {MIGRATION_ID}: 完成")
def down(client):
"""回滚 migration"""
logger.warning(f"Migration {MIGRATION_ID}: 回滚操作(删除 category 字段中的数据)")
# Milvus 不支持删除字段,回滚记录在文档里,需要人工操作
logger.warning("此 migration 无法自动回滚,需要人工处理")# ai-configs/vector-db/migration-history.yaml
# 记录已执行的 migration,相当于 Flyway 的 schema_version 表
migrations:
- id: "001"
description: "初始化 knowledge_base Collection"
executed_at: "2024-01-01T10:00:00Z"
executed_by: "zhangge"
status: "completed"
- id: "002"
description: "为 knowledge_base 添加 HNSW 索引"
executed_at: "2024-01-10T14:30:00Z"
executed_by: "zhangge"
status: "completed"
- id: "003"
description: "添加 category 字段"
executed_at: "2024-01-15T09:00:00Z"
executed_by: "zhangge"
status: "completed"应用如何动态加载 Git 中的配置
配置同步到 K8s 之后,应用需要能感知到 ConfigMap 的变化并热更新,不需要重启。
Spring Boot 应用可以用 Spring Cloud Kubernetes 的配置热更新:
// 应用启动时从 ConfigMap 加载 Prompt
@Configuration
@RefreshScope // 支持配置热更新
public class PromptConfiguration {
@Value("${PROMPTS_DIR:/etc/ai-prompts}")
private String promptsDir;
@Bean
@RefreshScope
public PromptTemplateRegistry promptTemplateRegistry() {
PromptTemplateRegistry registry = new PromptTemplateRegistry();
// 从挂载的 ConfigMap 目录加载所有 Prompt 模板
Path promptDir = Paths.get(promptsDir);
try (Stream<Path> files = Files.walk(promptDir)) {
files.filter(p -> p.toString().endsWith(".txt"))
.forEach(path -> {
String name = promptDir.relativize(path)
.toString()
.replace(".txt", "")
.replace("/", ".");
try {
String content = Files.readString(path);
registry.register(name, content);
log.info("Loaded prompt template: {}", name);
} catch (IOException e) {
log.error("Failed to load prompt: {}", path, e);
}
});
} catch (IOException e) {
throw new RuntimeException("Failed to load prompts from " + promptsDir, e);
}
return registry;
}
}
// Deployment 中挂载 ConfigMap
// volumes:
// - name: prompts
// configMap:
// name: customer-service-prompts
// volumeMounts:
// - name: prompts
// mountPath: /etc/ai-promptsK8s 的 ConfigMap 挂载有一个机制:当 ConfigMap 更新时,挂载到 Pod 里的文件会自动更新(通常有 1-2 分钟的延迟)。配合 @RefreshScope 和 ConfigMap 变更监听器,可以实现 Prompt 热更新而不重启服务。
GitOps 工作流程
真实的收益和成本
做完这套 GitOps 体系之后,我们团队的变化:
收益:
- 每次 Prompt 变更都有 Git commit 记录,出问题能精确到"哪次 commit 引入的"
- 不同环境的配置差异一目了然(staging.yaml vs production.yaml)
- 回滚变成了
git revert+ 等 ArgoCD 同步,不需要人工操作 - 再也不会有人在生产直接改配置了(因为就算改了,ArgoCD 的 selfHeal 会把它改回来)
成本:
- 初始搭建成本不低,ArgoCD 本身需要学习曲线
- 配置仓库需要有人维护,特别是多环境的 Kustomize 层次关系容易搞乱
- 热更新有延迟(ConfigMap 挂载更新有 1-2 分钟延迟),不适合需要秒级生效的场景
如果团队只有 3-5 人,刚开始做 AI 应用,GitOps 可能过重了。但如果有多个 AI 服务、多个环境、多人维护 Prompt,这套体系能显著降低配置管理的混乱程度。
