猪头少年 - 云南AI专家 - 云南独立开发者

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);
}

这样可以实现 OpenAiCompatibleClientDeepSeekClientMimoClient 等不同适配器,而 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 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。

Next step

不要停在单篇文章。

沿主专题继续阅读,查看同一问题从概念到实战的完整路径。

返回「Agent 工程实战」路线