Engineering note
配置管理与模型提供商:把密钥和模型选择移出代码
这是「Agent 工程实战」的第 9 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。
本篇要解决的问题: 建立可验证的配置边界和模型适配层,让环境切换不再依赖改源码。
本章定位:本章解决“代码里到底连的是哪个模型”的问题。你会把 API 地址、密钥和模型名称移出源码,并用提供商接口隔离不同模型服务的协议差异。
建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。配置不是把字符串放进 JSON 就结束了,还要考虑启动校验、优先级、密钥保护和测试替身。
本章导读
本章解决“代码里到底连的是哪个模型”的问题。你会把 API 地址、密钥和模型名称移出源码,并用提供商接口隔离不同模型服务的协议差异。
本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。
9.1 把硬编码移出去
建议新增:
public sealed class ModelOptions
{
public string Endpoint { get; init; } = "";
public string ApiKey { get; init; } = "";
public string Model { get; init; } = "";
public int TimeoutSeconds { get; init; } = 120;
}
密钥从环境变量读取:
var apiKey = Environment.GetEnvironmentVariable("AGENT_API_KEY")
?? throw new InvalidOperationException("未配置 AGENT_API_KEY");
开发环境可以使用未提交的 appsettings.Development.json,生产环境使用环境变量或密钥服务。
9.2 提供商接口
类名 DeepSeekStreamHandler 与实际使用的 Xiaomi Mimo 地址已经出现了概念混杂。应抽象为提供商接口:
public interface IModelClient
{
IAsyncEnumerable<ModelEvent> StreamAsync(
ModelRequest request,
CancellationToken cancellationToken = default);
}
这样可以实现 OpenAiCompatibleClient、DeepSeekClient、MimoClient 等不同适配器,而 Agent 核心不依赖具体厂商。
9.3 HTTP 客户端建议
模型请求应:
- 使用
IHttpClientFactory或长期复用的HttpClient; - 使用
ResponseHeadersRead; - 设置请求超时和取消 token;
- 检查所有非成功状态码;
- 对 429、408、5xx 做有限重试;
- 对重试设置指数退避和随机抖动;
- 记录 request id,不记录密钥和完整敏感内容。
9.4 本章交付物
ModelOptions;IModelClient;- 一个 OpenAI-compatible HTTP 客户端;
- 一个 fake model client,供测试使用;
- 启动时配置校验和清晰的错误提示。
9.5 配置的三个层次
配置通常有三个层次:代码默认值、环境配置、运行时覆盖。
代码默认值用于安全的本地开发,例如超时时间;环境配置用于 API 地址、模型名和密钥;运行时覆盖用于临时调试,例如命令行指定模型。
优先级可以规定为:
命令行参数 > 环境变量 > 配置文件 > 代码默认值
不要把 API Key 放入普通配置文件后再依赖 .gitignore。.gitignore 只能防止部分误提交,不能防止日志、备份和机器迁移中的泄露。
9.6 启动时配置校验
配置错误应该在启动时暴露,而不是等用户发送第一条消息才出现:
public void Validate()
{
if (!Uri.TryCreate(Endpoint, UriKind.Absolute, out _))
throw new InvalidOperationException("模型 Endpoint 不是有效 URL");
if (string.IsNullOrWhiteSpace(ApiKey))
throw new InvalidOperationException("未配置模型 API Key");
if (string.IsNullOrWhiteSpace(Model))
throw new InvalidOperationException("未配置模型名称");
}
错误消息要告诉开发者如何修复,但不能打印真实密钥。
9.7 Provider Adapter 的测试边界
不同模型提供商可能在字段名称、工具调用格式、reasoning 字段和错误响应上有差异。提供商适配器应有自己的协议测试,Agent Loop 只看统一的领域模型。
例如,OpenAiCompatibleClient 的测试验证 JSON 映射,AgentLoopTests 则使用 fake client 验证决策循环。这样换模型时,核心 Agent 测试不必全部重写。
9.8 练习:把当前类名改成能力名
不急着改所有代码,先定义 IModelClient,让现有 DeepSeekStreamHandler 实现它。然后在 BooAgent 中只依赖接口。完成后再考虑把 provider-specific 的类名、URL 和模型字段迁移到配置。
9.9 本章产出
本章完成后,换模型只应替换配置或适配器,而不应修改工具注册、会话和 Agent Loop。配置错误应在程序启动时一次性说明。
单篇实战作业
实践:用环境变量提供 Endpoint、Model 和 ApiKey,启动时分别测试缺失、非法 URL 和完整配置。
建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。配置不是把字符串放进 JSON 就结束了,还要考虑启动校验、优先级、密钥保护和测试替身。
章节复盘
复盘问题:切换模型提供商时,哪些差异应留在 Adapter 内,哪些差异不能泄露到 Agent 核心?
本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。
下一篇:第 10 篇《类型安全的工具注册中心:从反射便利走向工程约束》
如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。