配置管理艺术:YAML/JSON/环境变量,配置分层与敏感信息处理
配置管理这事儿,看着简单,其实坑特别多。我见过不少团队,代码写得挺漂亮,结果上线第一天就因为配置炸了——要么是测试环境的数据库连到了生产库,要么是密钥明文写死在代码里被推到了Git仓库。嗯,这些我都经历过。
说白了,配置管理就是「把变的东西和不变的东西分开」。代码是相对稳定的,但配置不一样——开发环境一套、测试环境一套、生产环境又一套。怎么管好这些配置,是Agent工程化绕不开的一关。
配置的几种形态:YAML、JSON、环境变量
先聊聊配置的载体。目前主流的有三种:YAML、JSON、环境变量。你可能会问,为什么不用XML?我个人习惯是,XML太啰嗦了,写起来累,读起来更累。
YAML:人类可读性最强
YAML是我最推荐的配置格式,尤其是Agent的配置文件。它的缩进结构天然适合表达层级关系。举个例子:
# agent_config.yaml
agent:
name: "customer-service-bot"
version: "2.1.0"
model:
provider: "openai"
name: "gpt-4"
temperature: 0.7
memory:
type: "redis"
ttl: 3600
tools:
- name: "search_knowledge_base"
enabled: true
- name: "send_email"
enabled: false
你看,一眼就能看出结构。我在项目中遇到过最头疼的事,就是有人把YAML的缩进搞混了——Tab和空格混用,结果解析报错。所以我的建议是:统一用空格,缩进两个空格,别用Tab。
JSON:程序友好,但人读起来费劲
JSON的优势在于,几乎所有编程语言都原生支持。但说实话,人读JSON挺累的,尤其是嵌套深的时候。不过有些场景必须用JSON,比如Kubernetes的ConfigMap,或者某些API的请求体。
{
"agent": {
"name": "customer-service-bot",
"version": "2.1.0",
"model": {
"provider": "openai",
"name": "gpt-4",
"temperature": 0.7
}
}
}
嗯,看着还行,但要是再深两层,眼睛就花了。
环境变量:最灵活,也最容易被忽视
环境变量的好处是「与代码完全解耦」。你不需要修改任何文件,只需要在启动时设置一下就行。比如:
export OPENAI_API_KEY="sk-xxxx"
export DATABASE_URL="postgresql://user:pass@localhost:5432/db"
export LOG_LEVEL="debug"
但环境变量有个问题——它没有结构。所有值都是字符串,没法表达嵌套关系。所以我的做法是:环境变量只放敏感信息和环境标识,复杂的结构化配置还是用YAML。
model.temperature,你可以通过环境变量 AGENT_MODEL_TEMPERATURE=0.5 来覆盖它。这样既保留了YAML的结构化,又有了环境变量的灵活性。
配置分层:默认、开发、测试、生产
配置分层,说白了就是「不同环境用不同配置」。你想想看,开发环境连的是本地数据库,生产环境连的是云上的RDS,这两套配置怎么可能一样?
我一般这样分层:
| 层级 | 文件名 | 用途 | 示例 |
|---|---|---|---|
| 默认 | config.default.yaml | 所有环境共享的基础配置 | 模型名称、工具列表 |
| 开发 | config.dev.yaml | 本地开发环境 | localhost数据库、debug日志 |
| 测试 | config.test.yaml | CI/CD自动化测试 | 测试数据库、mock服务 |
| 生产 | config.prod.yaml | 线上正式环境 | 生产数据库、高并发参数 |
加载顺序是这样的:先加载默认配置,然后用环境特定的配置覆盖。如果某个字段在默认配置里有,在开发配置里也有,那就用开发配置的值。这就是「覆盖优先」原则。
# 伪代码示例
def load_config(env: str):
config = load_yaml("config.default.yaml")
env_config = load_yaml(f"config.{env}.yaml")
config.merge(env_config) # 环境配置覆盖默认
return config
我曾经犯过一个错:把生产环境的数据库密码写在了config.prod.yaml里,然后不小心把这个文件提交到了Git仓库。虽然马上删了,但Git历史里还留着。嗯,从那以后,我再也不在配置文件中写敏感信息了。
敏感信息处理:别让密钥裸奔
敏感信息处理,是配置管理里最容易被忽视、但后果最严重的一环。我见过有人把AWS的Access Key直接写在代码里,然后推到了公开仓库——结果第二天账户就被盗刷了几万美金。
怎么处理?我总结了三个层次:
第一层:环境变量隔离
最简单的做法,就是把敏感信息放到环境变量里。代码里只读取环境变量,不硬编码。
import os
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY 未设置")
这样至少保证了敏感信息不会出现在代码仓库里。但环境变量也有风险——如果服务器被入侵,攻击者可以轻易读取环境变量。
第二层:.env文件 + .gitignore
本地开发时,可以把敏感信息写在.env文件里,然后在.gitignore中排除它。这样既方便了本地开发,又不会提交到仓库。
# .env 文件(不要提交到Git)
OPENAI_API_KEY=sk-xxxx
DATABASE_PASSWORD=my_secret_pass
但要注意:.env文件不能加密,所以它只适合开发环境。生产环境还是得用更安全的方式。
第三层:密钥管理服务(推荐)
生产环境,我强烈建议使用专门的密钥管理服务,比如AWS Secrets Manager、HashiCorp Vault、或者Kubernetes的Secrets。这些服务会加密存储敏感信息,并提供访问控制。
# 从Vault获取密钥
import hvac
client = hvac.Client(url='https://vault.example.com', token='my-token')
secret = client.secrets.kv.v2.read_secret_version(path='openai/api_key')
api_key = secret['data']['data']['api_key']
这样做的好处是:密钥在传输和存储时都是加密的,而且可以审计谁在什么时候访问了哪个密钥。
知识体系总览
说了这么多,我画了一张图来总结配置管理的核心逻辑。你看一眼就明白了:
实战建议:一个完整的配置加载流程
最后,我分享一下我现在用的配置加载流程。这个流程经过了好几个项目的打磨,目前用着挺顺手的:
- 加载默认配置:从
config.default.yaml读取所有基础配置 - 加载环境配置:根据
APP_ENV环境变量(dev/test/prod),加载对应的config.{env}.yaml,覆盖默认配置 - 加载环境变量覆盖:读取所有以
AGENT_开头的环境变量,用它们覆盖配置中的对应字段 - 加载密钥管理服务:从Vault或Secrets Manager中拉取敏感信息,注入到配置对象中
- 校验配置完整性:检查所有必填字段是否都有值,缺少就报错,避免运行时才发现
这个流程的好处是:默认配置保证能跑,环境配置适配场景,环境变量灵活覆盖,密钥服务保证安全。四层叠加,基本覆盖了所有需求。
配置管理看起来是小事,但做不好就是大事。你想想看,一个Agent系统可能涉及十几个外部服务——LLM、数据库、缓存、消息队列、监控……每个服务都有自己的配置。如果管理不好,出问题的概率几乎是100%。
所以,花点时间把配置管理做好,绝对是值得的。