首页 / 文章

.NET中的提示工程与结构化输出:OpenAI、Azure、Anthropic、Gemini

适用于 .NET 的 OpenAI、Azure OpenAI、Anthropic 或 Gemini:服务地域、SDK、定价、基于 JSON Schema 的结构化输出、提示词工程以及轻量级 IChatClient。

发布者:REACTAPP.TOP阅读约 11 分钟2544 词

来源: OpenAI vs Azure OpenAI vs Anthropic vs Gemini for .NET: Provider Comparison Guide

一个直接从 ASP.NET Core 控制器调用 GPT-4o 的支持机器人或许能赢得演示机会。但随后企业客户会要求数据存储在特定地区,安全团队因处理长文档而希望使用 Claude,财务部门则想了解为何每个问题的费用是预估值的十倍。这些需求会让你倾向于选择不同的供应商,或是寻求某种能让你切换供应商的抽象解决方案。

OpenAI、Azure OpenAI、Anthropic 和 Gemini 都在销售相同的核心产品:无需托管 GPU 的托管聊天服务、工具调用功能以及嵌入功能。它们的区别在于包装形式、服务区域、合规性说明、SDK 的质量以及计费方式。对于需要运行多年的 .NET 服务而言,这些差异比上周排行榜上的最佳模型更为重要。

哪种供应商适合哪种任务

  • OpenAI平台:获取最新模型的最快途径。适用于已熟悉OpenAI数据边界的产品。
  • Azure OpenAI:当工作负载需要部署在具备企业身份认证、网络功能及区域控制功能的Azure租户中时使用。
  • Anthropic:擅长处理长上下文任务,并能精准遵循指令。适合用于处理大量文档的智能体。
  • Gemini:与Google Cloud深度集成,具备多模态处理能力,其产品与Workspace高度兼容。

无需默认选择多个服务提供商。若只有一个团队、一个区域且没有合规方面的特殊要求,使用单一提供商能减少复杂度。每增加一层抽象层都会带来额外成本。

SDK的成熟度及需安装的包

你在这里评估的应该是官方客户端库、符合习惯的异步编程模式,以及破坏性变更出现的频率。那些与HttpClient的生命周期及依赖注入机制相冲突的SDK,其带来的成本将远超令牌费用。对于这四项内容,你应该寻找原生开发或拥有良好社区支持的包,但在确定标准之前,请先查看当前的NuGet版本以及认证流程(API密钥与Entra ID)。

// Official OpenAI SDK
dotnet add package OpenAI

// Azure AI Foundry (Azure OpenAI)
dotnet add package Azure.AI.OpenAI

// Anthropic Claude (official, beta as of 2026)
dotnet add package Anthropic

// Gemini via Google Cloud Vertex AI (IChatClient-compatible, January 2026)
dotnet add package Google.Cloud.VertexAI.Extensions --prerelease

// Abstraction layer (ships with all provider adapters)
dotnet add package Microsoft.Extensions.AI

Microsoft.Extensions.AI 是将它们连接在一起的组件:每个 SDK 都会提供 IChatClient,这样应用程序的其他部分就能保持对服务提供商的无关性。

定价机制与模型路由

每个服务提供商都会对令牌收费,但具体细节会影响总费用:输入令牌和输出令牌的定价不同,缓存过的令牌更便宜,工具类服务可能会收取额外费用,而且某些套餐还有最低消费要求。长上下文处理以及需要大量工具的智能体会让费用迅速上升。Azure 通过其计量系统计费,而其他三家则通过服务提供商账户计费。

财务团队会建立成本模型,因此你的架构必须能够输出他们所信任的使用数据。应将每一次任务完成操作都置于能够记录模型类型、令牌数量及功能标志的计量系统中。预算应体现在代码中以及云服务计费警报中。按任务复杂度进行路由是成本最低的解决方案:

// Partial example — model routing based on task complexity
public class ModelRouter
{
    // Route cheap, simple tasks to the nano model
    // Route complex multi-step reasoning to the full model
    public string SelectModel(TaskComplexity complexity) => complexity switch
    {
        TaskComplexity.Classification  => "gpt-5.4-nano",   // ~20x cheaper than full
        TaskComplexity.Conversational  => "gpt-5.4-mini",   // ~5x cheaper
        TaskComplexity.Reasoning       => "gpt-5.4",         // full capability
        _                              => "gpt-5.4-mini"
    };
}

企业适配性:身份、网络与地域要求

安全问卷会淘汰那些仅优化演示延迟的项目。这些问题涉及身份验证、网络隔离、数据保留以及区域可用性。

Azure OpenAI继承了Azure的RBAC、Private Link功能以及地域选择机制。OpenAI和Anthropic提供组织级控制功能及零数据保留选项,但相关条款需仔细阅读。Gemini则遵循Google Cloud的IAM和VPC架构。

在ASP.NET Core中的身份验证方面,对于Azure OpenAI建议使用托管身份。对于API密钥提供方,应在启动时从密钥库加载密钥,并在不重新部署代码的情况下更换密钥。切勿将提供方密钥与用户JWT绑定。

法律审查必须涵盖每个已启用的提供方的训练数据使用选项、分包处理商信息以及数据处理协议条款,而不仅仅是主提供方。使用多个提供方意味着需要多份合同。

使用 JSON Schema 进行结构化输出

不同供应商对 JSON Schema 及受限解码的支持程度各不相同。建议选择那些能够为 C# 反序列化器已信任的工具参数和响应强制执行 Schema 的服务提供商;在强制机制较弱的场合,则可采用验证后重试的方式。使用 Azure OpenAI 可以让 Schema 更为严格:

// Full example — Azure OpenAI structured output with strict schema enforcement
using Azure.AI.OpenAI;
using OpenAI.Chat;
using System.ComponentModel;
using System.Text.Json;

// Define the schema type — only JSON Schema primitives are supported
public class SupportTicketAnalysis
{
    public string Category { get; set; } = string.Empty;        // e.g. "billing", "technical"
    public string Priority { get; set; } = string.Empty;        // "low", "medium", "high", "critical"

    [Description("One to two sentence summary of the issue")]
    public string Summary { get; set; } = string.Empty;

    public string[] RequiredTeams { get; set; } = [];           // which teams must be looped in

    [Description("Estimated resolution time as a human-readable string, e.g. '2 business days'")]
    public string EstimatedResolution { get; set; } = string.Empty;
}

// Build the client — structured outputs require API version 2024-08-01-preview or later
var azureClient = new AzureOpenAIClient(
    new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
    new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!),
    new AzureOpenAIClientOptions(AzureOpenAIClientOptions.ServiceVersion.V2024_08_01_Preview));

ChatClient chatClient = azureClient.GetChatClient("gpt-5.4-mini-deployment");

// Configure the request to enforce the schema strictly
var options = new ChatCompletionOptions
{
    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat<SupportTicketAnalysis>(
        "support_ticket_analysis",  // snake_case — spaces and hyphens cause 400 errors
        strict: true)               // without this, schema compliance is best-effort only
};

List<ChatMessage> messages =
[
    new SystemChatMessage("You are a support ticket classifier. Analyze the ticket and return a structured assessment."),
    new UserChatMessage("User reports: 'My invoice shows $999 but the contract says $299. Need this fixed immediately.'")
];

// If structured outputs fail with a 400, the schema has unsupported types — check for DateTime, Uri, TimeSpan
ClientResult<ChatCompletion> result = await chatClient.CompleteChatAsync(messages, options);

// JsonSerializer.Deserialize will always succeed if the API call succeeded
SupportTicketAnalysis analysis = JsonSerializer.Deserialize<SupportTicketAnalysis>(
    result.Value.Content.Text)!;

Console.WriteLine(quot;Category: {analysis.Category}");
Console.WriteLine(quot;Priority: {analysis.Priority}");
Console.WriteLine(quot;Summary: {analysis.Summary}");

该示例中有两个细节会导致大多数 400 错误:模式名称需使用 snake_case 格式,而 DateTime、Uri 或 TimeSpan 等类型并非 JSON Schema 的原生类型。若不设置 strict: true,则合规性仅能做到尽力而为。

工具调用、嵌入模型与 RAG

这四种方式都支持工具或函数的调用,但数据格式各不相同。应在边界处将它们统一转换为内部的 ToolCall DTO,以避免调度代码因供应商不同而产生差异。

选择聊天服务提供商并不等同于必须使用其嵌入模型。应在配置中明确指定嵌入模型及维度,且在更换聊天服务提供商时不要擅自更改这些设置。应积极缓存嵌入模型结果及确定的工具调用结果,但除非有相应的保留策略,否则不要缓存包含用户个人身份信息的原始聊天回复内容。

借助 IChatClient 的轻量级 .NET 抽象层

定义一个包含流式与非流式方法的简单提供者接口,将每个SDK通过适配器进行映射,并通过依赖注入的方式将其注入。将提示信息和工具置于适配器之外。避免使用那些隐藏了需要自行调整的重试机制和超时设置的“全能”框架。借助 IChatClient,注册过程会从配置中读取提供者信息,并将所有操作封装在OpenTelemetry框架中:

// Partial example — Program.cs provider registration with IChatClient
using Microsoft.Extensions.AI;
using Azure.AI.OpenAI;
using OpenAI;
using Anthropic;
using Google.Cloud.VertexAI.Extensions;

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// Read which provider to use from configuration — no code change to switch
string provider = config["AI:Provider"]!; // "azure", "openai", "anthropic", "gemini"

builder.Services.AddSingleton<IChatClient>(sp =>
{
    // Return the appropriate IChatClient implementation based on config
    return provider switch
    {
        "azure" => new AzureOpenAIClient(
                        new Uri(config["AI:Azure:Endpoint"]!),
                        new Azure.AzureKeyCredential(config["AI:Azure:Key"]!))
                    .GetChatClient(config["AI:Azure:Deployment"]!)
                    .AsIChatClient(), // extension method from Azure.AI.OpenAI

        "openai" => new OpenAIClient(config["AI:OpenAI:Key"]!)
                    .GetChatClient(config["AI:OpenAI:Model"]!)
                    .AsIChatClient(),

        "anthropic" => new AnthropicClient(config["AI:Anthropic:Key"]!)
                    .AsIChatClient(), // Anthropic SDK v10+ implements IChatClient

        "gemini" => new VertexAIChatClient(
                        projectId: config["AI:Gemini:ProjectId"]!,
                        location: config["AI:Gemini:Location"]!,
                        modelId: config["AI:Gemini:Model"]!),

        _ => throw new InvalidOperationException(quot;Unknown AI provider: {provider}")
    };
});

// Add OpenTelemetry instrumentation via pipeline decorator — applies to all providers
builder.Services.AddSingleton(sp =>
{
    var inner = sp.GetRequiredService<IChatClient>();
    return new ChatClientBuilder(inner)
        .UseOpenTelemetry(loggerFactory: sp.GetRequiredService<ILoggerFactory>(),
                          sourceName: "YourApp.AI")
        .Build();
});

特征标志应根据租户或技能选择相应的提供方。Program.cs中硬编码的单一静态客户端正是导致驻留异常演变为故障的原因。

流式处理在抽象层也需要明确的契约:取消令牌、背压机制,以及部分工具调用如何传递给调用方。不同提供方在这些方面的差异很大,以至于“直接等待字符串返回”的方式会将供应商的具体实现逻辑泄露到控制器中。

可运行的概念验证

整个思路都可以通过一个小型Web API来实现。启动项目后添加相应的包:

dotnet new webapi -n AiProviderPoc --no-openapi
cd AiProviderPoc

dotnet add package Microsoft.Extensions.AI
dotnet add package OpenAI
dotnet add package Azure.AI.OpenAI
dotnet add package Anthropic
dotnet add package Google.Cloud.VertexAI.Extensions --prerelease

所有提供者都会使用相同的提示词合同。输出类型仅限于 JSON Schema 的基本数据类型,而这些描述同时也作为对模型的指导说明:

// PromptContracts.cs — shared across all providers
using System.ComponentModel;

namespace AiProviderPoc;

// The input to every analysis request
public record AnalysisRequest(string TicketText, string? Context = null);

// The structured output — must use only JSON Schema primitives (string, int, bool, arrays)
public class TicketAnalysis
{
    [Description("Category of issue: 'billing', 'technical', 'account', 'feature-request'")]
    public string Category { get; set; } = string.Empty;

    [Description("Severity: 'low', 'medium', 'high', 'critical'")]
    public string Severity { get; set; } = string.Empty;

    [Description("One to two sentence summary of the customer's issue")]
    public string Summary { get; set; } = string.Empty;

    [Description("Suggested action for the support agent, one sentence")]
    public string SuggestedAction { get; set; } = string.Empty;

    public bool RequiresEscalation { get; set; }

    [Description("Estimated time to resolve as human-readable string, e.g. '30 minutes' or '2 business days'")]
    public string EstimatedResolution { get; set; } = string.Empty;
}

该服务将系统提示视为稳定的、有版本控制的契约,并且仅依赖 IChatClient。它采用与提供方无关的 JSON 格式;若需要在 Azure 或 OpenAI 上实施严格的架构约束,则应使用之前介绍的直接 SDK 方法。

// TicketAnalysisService.cs
using Microsoft.Extensions.AI;
using System.Text.Json;

namespace AiProviderPoc;

public class TicketAnalysisService(IChatClient chatClient)
{
    // The system prompt is the stable contract — versioned and centralized here
    private const string SystemPrompt =
        """
        You are a support ticket classifier for an enterprise software product.
        Analyze the provided support ticket and return a structured assessment.
        Be concise and accurate. Do not invent information not present in the ticket.
        """;

    public async Task<TicketAnalysis> AnalyzeAsync(AnalysisRequest request, CancellationToken ct = default)
    {
        // Build the message list — system prompt is fixed, user content is the ticket
        var messages = new List<ChatMessage>
        {
            new(ChatRole.System, SystemPrompt),
            new(ChatRole.User, request.TicketText)
        };

        // Use JSON mode via ChatOptions — provider-agnostic structured output
        // For strict schema enforcement with Azure/OpenAI, use the SDK-direct approach instead
        var options = new ChatOptions
        {
            ResponseFormat = ChatResponseFormat.Json
        };

        ChatResponse response = await chatClient.GetResponseAsync(messages, options, ct);

        // Deserialize — IChatClient normalizes the response shape across all providers
        return JsonSerializer.Deserialize<TicketAnalysis>(response.Text,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
            ?? throw new InvalidOperationException("Model returned unparseable JSON");
    }
}

Program.cs 为每个提供者分支注册一个客户端,并暴露一个单一的端点,该端点不会知晓是哪个供应商响应了请求:

using AiProviderPoc.Api;
using Anthropic;
using Azure.AI.OpenAI;
using Google.GenAI;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;          // ApiKeyCredential

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// Read provider selection from config — change "AI:Provider" to switch providers
string provider = config["AI:Provider"]
    ?? throw new InvalidOperationException("AI:Provider must be set in appsettings");

// Register IChatClient — each branch returns a different SDK's implementation
builder.Services.AddSingleton<IChatClient>(_ =>
{
    return provider.ToLowerInvariant() switch
    {
        "azure" =>
            // Azure AI Foundry: needs endpoint + key + deployment name
            new AzureOpenAIClient(
                new Uri(config["AI:Azure:Endpoint"]!),
                new ApiKeyCredential(config["AI:Azure:Key"]!))
            .GetChatClient(config["AI:Azure:Deployment"]!)
            .AsIChatClient(),

        "openai" =>
            // OpenAI direct: needs API key + model name
            new OpenAIClient(config["AI:OpenAI:Key"]!)
            .GetChatClient(config["AI:OpenAI:Model"]!)
            .AsIChatClient(),

        "anthropic" =>
            // AnthropicClient expects a ClientOptions instance (not a string API key).
            // Create a ClientOptions and pass it to the constructor.
            new AnthropicClient(
                new Anthropic.Core.ClientOptions
                {
                    ApiKey = config["AI:Anthropic:Key"]!
                })
            .AsIChatClient(),

        "gemini" =>
            // Google.GenAI: named params on Client constructor, no ClientConfig class needed.
            // Uses GOOGLE_APPLICATION_CREDENTIALS / ADC for auth; no API key required for Vertex AI.
            new Client(
                project: config["AI:Gemini:ProjectId"]!,
                location: config["AI:Gemini:Location"]!,
                vertexAI: true)
            .AsIChatClient(config["AI:Gemini:Model"]!),

        _ => throw new InvalidOperationException(quot;Unknown provider: '{provider}'. Valid: azure, openai, anthropic, gemini")
    };
});

// Register the service — depends only on IChatClient
builder.Services.AddScoped<TicketAnalysisService>();

var app = builder.Build();

// Single endpoint — provider is invisible here
app.MapPost("/analyze", async (AnalysisRequest request, TicketAnalysisService service, CancellationToken ct) =>
{
    var analysis = await service.AnalyzeAsync(request, ct);
    return Results.Ok(analysis);
});

app.Run();

更换服务提供商只需在 appsettings.json 中修改配置即可:

{
  "AI": {
    "Provider": "azure",
    "Azure": {
      "Endpoint": "https://your-resource.openai.azure.com/",
      "Key": "your-azure-key",
      "Deployment": "gpt-5.4-mini-deployment"
    },
    "OpenAI": {
      "Key": "sk-...",
      "Model": "gpt-4o-mini"
    },
    "Anthropic": {
      "Key": "sk-ant-..."
    },
    "Gemini": {
      "ProjectId": "your-gcp-project",
      "Location": "us-central1",
      "Model": "gemini-2.0-flash"
    }
  }
}

运行它并提交工单:

dotnet run

curl -X POST http://localhost:5000/analyze \
  -H "Content-Type: application/json" \
  -d '{"ticketText": "My subscription renews tomorrow but I was charged twice this month. Account #A-1234."}'

无论配置了哪种提供商,返回的结果格式都相同:

{
  "category": "billing",
  "severity": "high",
  "summary": "Customer was charged twice for their subscription and is seeking a refund before their next renewal date.",
  "suggestedAction": "Review billing history for account A-1234 and initiate a refund for the duplicate charge.",
  "requiresEscalation": false,
  "estimatedResolution": "1 business day"
}

提示词工程、评估与公平比较

在比较质量时,应固定提示词和温度参数,仅改变服务提供商和模型。否则你比较的是提示词工程的运气而非供应商的实际能力。需为提示词模板保留版本号,并独立于服务提供商适配器进行A/B测试,否则每次更换模型都会使你的提示词实验结果失效。

应在服务提供商调用过程中生成OpenTelemetry跟踪数据,将服务提供商类型、模型类型、令牌数量及结束原因作为属性。将这些在线指标与离线评估数据结合使用,这样选择更便宜的模型就能有数据支撑而非仅凭猜测。

延迟预算也各不相同。有些服务提供商能更快地输出第一个令牌,而另一些则在处理长文本时表现更佳。在确定默认选项之前,需同时测量工作负载的第一个令牌出现时间以及端到端处理时间。

在生产环境中使用多个服务提供商

  • 金丝雀模式:为每个环境保留一个黄金提示,用于确保生成的JSON符合架构规范并满足延迟性能标准;若主要服务提供商的部署违反了这些要求,则应拒绝该部署。
  • 内容过滤器:Azure内容过滤器及其他安全机制会返回不同的错误信息格式,需将其一次性映射到您自定义的异常处理规则中。
  • 故障转移:需明确哪些错误可以在备用服务提供商处重新尝试处理,而哪些错误必须直接终止处理,比如授权错误和政策拒绝情况;盲目进行故障转移可能会违背数据驻留承诺。
  • 值班人员培训:需对负责各服务提供商的人员进行账单控制面板的培训;当操作手册能明确指出具体的门户页面时,就能更轻松地处理凌晨2点出现的流量激增问题。
  • 资源回收:需禁用相关密钥,删除应用注册信息,清除包含客户文本的测试数据集,并归档成本报告。

快速决策清单

需要本地部署?从 Azure OpenAI 开始。处理长文档?试试 Benchmark Anthropic。已经在使用 GCP?评估 Gemini。需要 OpenAI 最新模型发布后立即使用?选择 OpenAI 平台。需要上述两种功能?先构建轻量级适配器及使用量统计功能。

服务提供商的选择属于企业架构决策,会影响到 SDK 使用及成本问题。需从本地部署要求、身份验证、定价透明度、结构化输出能力以及与现有系统的兼容性等方面进行比较。