☰
Semantic Kernel AI 服务元数据机制详解:从 `IAIService.Attributes` 到模型驱动的服务选择器
2026/10/4 10:44:58 网站建设 项目流程

Semantic Kernel AI 服务元数据机制详解:从IAIService.Attributes到模型驱动的服务选择器

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

本篇技术指南以 Semantic Kernel 仓库中的架构决策记录(ADR)0021-aiservice-metadata.md 为核心主体,深入讲解 AI 服务元数据(AI Service Metadata)机制的由来、设计取舍与最终落地实现。你将掌握IAIService的Attributes字典如何承载模型 ID、Endpoint 等关键元数据,理解内置OrderedAIServiceSelector如何按 service id / model id / 默认顺序解析服务,并学会基于元数据编写自定义服务选择器,在真实的多模型场景中实现"按模型 ID 选路"。

一、背景:为什么需要一个 AI 服务元数据机制

1.1 问题本身:语义函数执行时"不知道"用哪个模型

在早期版本中,IAIService是一个空接口:

public interface IAIService { }

开发者虽然可以通过IKernel.GetService<T>(string? name = null)按服务类型与名称(即 service id)拿到具体服务实例,但存在两个关键痛点:

  1. prompt 创作者需要按模型 ID 检索服务。使用 OpenAI 的开发者通常会根据模型来调优自己的提示词,因此执行 prompt 时希望能"按模型 ID 把服务找出来",而不是按人为指定的 service id。
  2. 具体服务实例携带的元数据不同。例如 Azure OpenAI 服务持有的是 deployment name(由部署者任意命名,如eastus-gpt-4或foo-bar),而 OpenAI 服务持有的是必须与官方模型列表匹配的 model id。而IChatCompletion这类接口是"通用"的,不包含任何与具体连接器实例相关的属性。

这一背景下产生了两个典型的使用场景(ADR 原文列举):

  • 编写一个IAIServiceSelector,根据配置的 model id 选择要使用的 OpenAI 服务,从而在每次执行 prompt 时挑选"最优(可能也最便宜)"的模型;
  • 编写一个调用前钩子(pre-invocation hook),在 prompt 发送给 LLM 之前计算其 token 大小——而所用 token 计算库恰好需要模型 id。

1.2 决策驱动因素

ADR 明确了两个硬性要求:

  • 需要一种机制来为IAIService实例存储通用元数据,并且由具体实现(如 OpenAI、HuggingFace 服务)自行决定存储哪些相关元数据;
  • 需要能够遍历所有已注册的IAIService实例(而不仅仅是通过 id 精确查找)。

二、三个备选方案与最终决策

ADR 曾就"如何暴露元数据"给出三个候选方案,最终结论是:

Chosen option: Option #1, because it's a simple implementation and allows easy iteration over all possible attributes.

方案对比一览

方案元数据暴露方式优点 / 缺点
Option #1在IAIService上增加string? ModelId { get; }与IReadOnlyDictionary<string, object> Attributes { get; }两个属性实现简单,可方便地遍历所有属性;被最终采纳
Option #2在IAIService上增加T? GetAttributes<T>() where T : AIServiceAttributes;方法,由各实现自定义属性类类型安全,但每个连接器都要自定义类,样板代码多
Option #3在IAIService上增加只读字典Attributes+ 固定的ModelId、Endpoint、ApiVersion属性,并配套GetModelId()、GetAttribute(key)等访问方法兼顾通用字典与强类型访问,但接口定义更重

三个方案都要求扩展INamedServiceProvider(增加ICollection<T> GetServices<T>()方法)并扩展OpenAIKernelBuilderExtensions,使WithAzureXXX系列方法在可定位到具体模型时携带modelId属性。

各方案的 Selector 用法对比

Option #1 的写法(直接读属性,通过serviceProvider.GetServices<T>()遍历):

public class Gpt3xAIServiceSelector : IAIServiceSelector { public (T?, AIRequestSettings?) SelectAIService<T>(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList<AIRequestSettings>? modelSettings) where T : IAIService { var services = serviceProvider.GetServices<T>(); foreach (var service in services) { if (!string.IsNullOrEmpty(service.ModelId) && service.ModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"Selected model: {service.ModelId}"); return (service, new OpenAIRequestSettings()); } } throw new SKException("Unable to find AI service for GPT 3.x."); } }

Option #2 的写法(通过GetAttributes<AIServiceAttributes>()取模型 ID):

public class Gpt3xAIServiceSelector : IAIServiceSelector { public (T?, AIRequestSettings?) SelectAIService<T>(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList<AIRequestSettings>? modelSettings) where T : IAIService { var services = serviceProvider.GetServices<T>(); foreach (var service in services) { var serviceModelId = service.GetAttributes<AIServiceAttributes>()?.ModelId; if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"Selected model: {serviceModelId}"); return (service, new OpenAIRequestSettings()); } } throw new SKException("Unable to find AI service for GPT 3.x."); } }

Option #3 的写法(通过GetModelId()/GetAttribute(key)混合访问):

public (T?, AIRequestSettings?) SelectAIService<T>(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList<AIRequestSettings>? modelSettings) where T : IAIService { var services = serviceProvider.GetServices<T>(); foreach (var service in services) { var serviceModelId = service.GetModelId(); var serviceOrganization = service.GetAttribute(OpenAIServiceAttributes.OrganizationKey); var serviceDeploymentName = service.GetAttribute(AzureOpenAIServiceAttributes.DeploymentNameKey); if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"Selected model: {serviceModelId}"); return (service, new OpenAIRequestSettings()); } } throw new SKException("Unable to find AI service for GPT 3.x."); }

从三个例子可以看出,无论采用哪种方案,核心目标一致:让 Selector 能够遍历所有服务、读取每个服务的模型元数据,并按规则完成路由。最终选定的 Option #1 以"一个只读字典 + 一个模型 ID 属性"的最小代价实现了这一目标。

三、落地实现:源码中的IAIService.Attributes

3.1 接口现状

决策落地后,当前仓库中 IAIService.cs 的实现为:

namespace Microsoft.SemanticKernel.Services; /// <summary> /// Represents an AI service. /// </summary> public interface IAIService { /// <summary> /// Gets the AI service attributes. /// </summary> IReadOnlyDictionary<string, object?> Attributes { get; } }

相比 ADR 中 Option #1 最初设想的string? ModelId { get; }属性,最终版本只保留了只读字典Attributes。模型 ID 的获取被下沉为扩展方法(见下文),这一演化使接口更精简,也让元数据完全开放、可扩展。

3.2 元数据键与访问扩展方法

AIServiceExtensions.cs 定义了三个标准元数据键以及对应的强类型访问扩展方法:

常量键字符串扩展方法
ModelIdKey"ModelId"GetModelId()
EndpointKey"Endpoint"GetEndpoint()
ApiVersionKey"ApiVersion"GetApiVersion()

其中GetModelId()的实现非常直观:

public static string? GetModelId(this IAIService service) => service.GetAttribute(ModelIdKey);

而底层GetAttribute只是对字典的安全取值:

private static string? GetAttribute(this IAIService service, string key) { Verify.NotNull(service); return service.Attributes?.TryGetValue(key, out object? value) == true ? value as string : null; }

这意味着任何IAIService实现都可以向Attributes字典写入任意键值对,而GetModelId()/GetEndpoint()/GetApiVersion()提供了对标准键的便捷访问。

3.3 连接器如何填充元数据

以 OpenAI 连接器为例,ClientCore.cs 在构造阶段会通过内部AddAttribute方法填充元数据:

if (!string.IsNullOrWhiteSpace(modelId)) { this.ModelId = modelId!; this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } this.Endpoint = endpoint ?? httpClient?.BaseAddress; ... this.AddAttribute(AIServiceExtensions.EndpointKey, this.Endpoint.ToString()); if (!string.IsNullOrWhiteSpace(organizationId)) { ... this.AddAttribute(ClientCore.OrganizationKey, organizationId); }

AddAttribute的实现会在值为空时跳过写入:

internal void AddAttribute(string key, string? value) { if (!string.IsNullOrEmpty(value)) { this.Attributes.Add(key, value); } }

而所有 OpenAI 服务(如 OpenAIChatCompletionService.cs、OpenAITextEmbeddingGenerationService、OpenAITextToImageService、OpenAIAudioToTextService、OpenAITextToAudioService)都统一通过public IReadOnlyDictionary<string, object?> Attributes => this._client.Attributes;暴露这份共享的元数据字典。

这印证了 ADR 中"由具体IAIService实例负责存储其相关元数据(例如 OpenAI 与 HuggingFace 服务存储 model id)"的设计:接口只提供容器,具体连接器决定装什么。

四、服务的注册、遍历与选择:从GetService到GetAllServices

4.1 按 id 取单例 vs 遍历全部

ADR 中注册两个服务的示例至今仍有参考价值:

IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureChatCompletionService( deploymentName: chatDeploymentName, endpoint: endpoint, serviceId: "AzureOpenAIChat", apiKey: apiKey) .WithOpenAIChatCompletionService( modelId: openAIModelId, serviceId: "OpenAIChat", apiKey: openAIApiKey) .Build(); var service = kernel.GetService<IChatCompletion>("OpenAIChat");

这里有两个关键点:

  • Azure OpenAI 传的是 deployment name(任意命名),OpenAI 传的是 model id(必须匹配官方模型);
  • GetService<T>("OpenAIChat")只能按 service id 精确定位,无法表达"给我一个 GPT-3.x 模型"这类按元数据筛选的需求。

这正是 ADR 要求扩展INamedServiceProvider、增加"遍历所有服务"能力的原因。在今天的实现中,Kernel.cs 提供了GetAllServices<T>():

public IEnumerable<T> GetAllServices<T>() where T : class { if (this.Services is IKeyedServiceProvider) { if (this.Services.GetKeyedService<Dictionary<Type, HashSet<object?>>>(KernelServiceTypeToKeyMappings) is { } typeToKeyMappings) { if (typeToKeyMappings.TryGetValue(typeof(T), out HashSet<object?>? keys)) { return keys.SelectMany(this.Services.GetKeyedServices<T>); } return []; } } return this.Services.GetServices<T>(); }

从源码可以看出,GetAllServices在键控服务提供器(IKeyedServiceProvider,即 Microsoft.Extensions.DependencyInjection 默认实现)下,通过KernelBuilder注入的"类型→全部键"映射表遍历所有注册键;在非键控提供器下则回退到GetServices<T>()。该方法正是 ADR 中"能够遍历可用IAIService实例"这一决策驱动因素的落地实现。

五、内置选择器:OrderedAIServiceSelector的三级解析顺序

ADR 设想的自定义IAIServiceSelector是开发者扩展点;而仓库内置的默认实现是 OrderedAIServiceSelector.cs。它的TrySelect逻辑体现了完整的解析顺序:

  1. 优先使用 KernelArguments 中的 ExecutionSettings(arguments.ExecutionSettings ?? function.ExecutionSettings);
  2. 若没有任何执行设置,直接取任意已注册服务(GetAnyService);
  3. 若有执行设置,则按下述三级顺序匹配:
    • 先按 service id:遍历executionSettings的键(排除空键与默认 service id),通过IKeyedServiceProvider.GetKeyedService<T>(serviceId)精确查找;
    • 再按 model id:遍历设置中非空的ModelId,调用GetServiceByModelId在全部服务中比对GetModelId()的结果(对IChatClient则调用chatClient.GetModelId());
    • 最后回退默认:若设置了默认 service id(PromptExecutionSettings.DefaultServiceId),则取任意服务并携带默认设置。

其中"按 model id 查找"的关键代码如下:

private T? GetServiceByModelId<T>(Kernel kernel, string modelId) where T : class { foreach (var service in kernel.GetAllServices<T>()) { string? serviceModelId = null; if (service is IAIService aiService) { serviceModelId = aiService.GetModelId(); } else if (service is IChatClient chatClient) { serviceModelId = chatClient.GetModelId(); } if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId == modelId) { return service; } } return null; }

可以看到,按 model id 选路完全依赖Attributes字典中的ModelId元数据——这正是本文主题在运行时的核心价值。

5.1 选择器在函数执行链路中的位置

在 KernelFunctionFromPrompt.cs 中,prompt 函数执行时会先尝试选择IChatCompletionService,失败再尝试ITextGenerationService:

string renderedPrompt = string.Empty; // Try to use IChatCompletionService. if (serviceSelector.TrySelectAIService<IChatCompletionService>( kernel, this, arguments, out IChatCompletionService? chatService, out PromptExecutionSettings? executionSettings)) { aiService = chatService; } else if (serviceSelector.TrySelectAIService<ITextGenerationService>( kernel, this, arguments, out ITextGenerationService? textService, out executionSettings)) { ... }

由此可以推断:任何自定义IAIServiceSelector都会在此处参与每次 prompt 执行的服务解析,从而让"按模型路由"成为可能。

5.2 扩展方法与异常信息

AIServiceExtensions.cs 还提供了SelectAIService<T>扩展方法,将TrySelectAIService封装为抛出KernelException的形式。值得注意的是,当匹配失败时,异常信息会列出期望的serviceIds与modelIds(从函数的ExecutionSettings汇总而来),极大方便了排查多服务场景下的配置问题。

六、实战:编写一个按模型 ID 路由的自定义服务选择器

6.1 现代接口签名

ADR 中的示例基于早期接口(SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, ...))。当前仓库中 IAIServiceSelector.cs 的签名已演化为:

public interface IAIServiceSelector { bool TrySelectAIService<T>( Kernel kernel, KernelFunction function, KernelArguments arguments, [NotNullWhen(true)] out T? service, out PromptExecutionSettings? serviceSettings) where T : class, IAIService; }

接口从"返回元组"改为"out 参数 + bool 返回值",语义更接近现代 .NET 惯例,也让调用方可以区分"未找到服务"与"找到但无设置"两种情况。

6.2 基于元数据实现"只选 GPT-3.x"

参照 ADR 的 Option #1 思路,用当前接口实现一个按模型 ID 前缀过滤的选择器:

using System.Diagnostics.CodeAnalysis; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Services; public sealed class Gpt3xAIServiceSelector : IAIServiceSelector { public bool TrySelectAIService<T>( Kernel kernel, KernelFunction function, KernelArguments arguments, [NotNullWhen(true)] out T? service, out PromptExecutionSettings? serviceSettings) where T : class, IAIService { foreach (var candidate in kernel.GetAllServices<T>()) { var modelId = candidate.GetModelId(); if (!string.IsNullOrEmpty(modelId) && modelId.StartsWith("gpt-3", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"Selected model: {modelId}"); service = candidate; serviceSettings = null; // 可改为按需构造 PromptExecutionSettings return true; } } service = null; serviceSettings = null; return false; } }

与 ADR 示例一一对应的关键点:

  • kernel.GetAllServices<T>()对应原方案中的serviceProvider.GetServices<T>()(遍历能力由GetAllServices落地);
  • candidate.GetModelId()对应原方案中的service.ModelId(元数据读取由Attributes+ 扩展方法落地);
  • 不再throw new SKException(...),而是返回false,由调用方(如SelectAIService<T>扩展方法)决定如何报错。

6.3 使用自定义选择器

自定义选择器可以通过 KernelBuilder 配置注入(仓库中KernelBuilder支持服务集合的自定义配置,具体可参考 Kernel.cs 与 DI 相关的 KernelServiceCollectionExtensions.cs)。替换默认的OrderedAIServiceSelector后,每次 prompt 执行都会优先经过你的路由规则。

6.4 另一种用途:pre-invocation hook 中的 token 预算

ADR 提到的第二个场景(调用前计算 token 大小)同样依赖元数据:在函数过滤器或钩子中,通过IAIService.GetModelId()拿到模型 ID 后,即可选用正确的 tokenizer 估算 prompt 长度,进而决定选用更经济的模型。这与 FunctionInvocationApproval 一类示例所展示的"执行前决策"思路一脉相承,可以推断元数据机制是这类能力的地基。

七、设计要点回顾

  1. 接口最小化:IAIService只暴露IReadOnlyDictionary<string, object?> Attributes,把"存什么、怎么存"留给具体连接器,符合 ADR"由实现负责填充相关元数据"的决策;
  2. 标准键约定:ModelId、Endpoint、ApiVersion作为跨连接器约定的标准键,由 AIServiceExtensions.cs 提供强类型访问;
  3. 遍历能力:Kernel.GetAllServices<T>()使"按元数据筛选全部服务"成为可能,而不再局限于按 service id 精确查找;
  4. 选择器可插拔:IAIServiceSelector是公开扩展点,内置OrderedAIServiceSelector提供 service id → model id → 默认的三级解析顺序,且其 model id 匹配正是基于Attributes元数据;
  5. 版本演化:ADR 原文中的接口签名(如SelectAIService元组返回、AIRequestSettings类型)在后续版本中演化为TrySelectAIServiceout 参数形式与PromptExecutionSettings,但"以元数据驱动服务路由"的核心思想始终未变。

对于需要"按模型成本/能力路由"、"按模型精确计 token"或"多模型服务编排"的开发者而言,IAIService.Attributes与IAIServiceSelector正是接入这些能力的关键入口。更多相关讨论可继续阅读同目录下的 0017-openai-function-calling.md、0015-completion-service-selection.md 与 0038-completion-service-selection.md,它们共同构成了 Semantic Kernel 服务选择与调用机制的完整脉络。

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询