Agent 工程体系 · 第 50/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Semantic Kernel Agent:Plugin、Process、Memory、编排和服务集成

Semantic Kernel(以下简称 SK)不是一个“替模型自动完成所有事情”的黑盒 Agent 产品,而是一组把模型调用、函数调用、状态管理、流程执行和服务连接组合起来的 SDK。它的核心价值在于:将已有代码和外部服务包装成模型可以理解的函数,再由应用程序决定这些函数如何被调用、如何被审计,以及失败后如何恢复。

官方文档将 SK 描述为一个面向 C#、Python 和 Java 的轻量级开源开发套件。它通过 Kernel 统一组织 AI 服务、插件、依赖注入、提示词执行和中间件能力。(learn.microsoft.com)

本文中的“Agent 工程体系”需要先区分五个层次:

  1. Plugin:Agent 可以调用的能力契约。
  2. Process:按照事件、步骤和状态推进的业务流程。
  3. Memory:Agent 可持续访问的上下文和外部知识。
  4. 编排:一个或多个 Agent 之间的协作控制结构。
  5. 服务集成:模型、向量库、数据库、HTTP API、身份认证、日志和观测系统的连接方式。

它们不是同义词,也不是平级替换关系:

flowchart TD
    User[用户请求] --> Agent[Agent]
    Agent --> Kernel[Semantic Kernel]
    Kernel --> Model[Chat Completion / LLM]
    Kernel --> Plugin[Plugin / Function]
    Kernel --> Memory[Thread / Retrieval / Vector Store]
    Kernel --> Service[外部服务与连接器]
    Agent --> Orchestration[Agent 编排]
    Orchestration --> AgentA[Agent A]
    Orchestration --> AgentB[Agent B]
    Process[Process] --> Step1[Step 1]
    Step1 --> Step2[Step 2]
    Step2 --> Step3[Step 3]
    Step1 -.事件.-> Step2
    Step2 -.事件.-> Step3

其中,Agent 负责“根据目标决定下一步”,Plugin 提供可执行能力,Memory 提供信息,Process 负责确定性的业务状态迁移,编排负责多个执行主体之间的协作,Kernel 则是把这些对象装配起来的运行时。


一、先理解 Kernel:它不是 Agent,而是 Agent 的运行时边界

1. Kernel 的组成

在 SK 中,Kernel 通常包含两类对象:

  • Services:AI 服务以及应用运行所需的其他服务;
  • Plugins:可被提示词或 AI 服务调用的函数集合。

AI 服务可以是聊天补全、文本生成、Embedding 等;其他服务可以是日志记录器、HTTP 客户端、数据库访问对象或自定义领域服务。插件则通过这些依赖执行实际工作。(learn.microsoft.com)

可以把 Kernel 抽象成:

K=(S,P,F,M)K = (S, P, F, M)

其中:

  • SS 是服务集合;
  • PP 是插件集合;
  • FF 是经过注册后的函数集合;
  • MM 是运行时元数据,例如函数描述、参数 Schema、过滤器和观测信息。

模型并不能直接访问 Kernel 中的任意对象。模型只能看到应用程序暴露给它的函数描述:

VisibleTools=Filter(F,tenant,agent,risk,version)\text{VisibleTools} = \operatorname{Filter}(F, \text{tenant}, \text{agent}, \text{risk}, \text{version})

这条公式非常重要。把对象注册到 Kernel,不等于把它授权给模型调用。

一个最小的 C# Kernel 装配示例:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.Core;

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")!,
    endpoint: Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!,
    apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY")!);

builder.Services.AddLogging(logging =>
{
    logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Information);
});

builder.Plugins.AddFromType<TimePlugin>("time");

Kernel kernel = builder.Build();

这个过程包含四步:

  1. 创建 KernelBuilder
  2. 注册聊天补全服务;
  3. 注册日志和其他依赖;
  4. 将插件添加到 Kernel;
  5. Build() 后获得不可变程度更高、可注入运行的 Kernel 实例。

实际项目中,Kernel 通常不应在每次用户请求中随意创建。更常见的方式是将服务连接器、HTTP 客户端、日志、配置和插件工厂注册到依赖注入容器,再按租户、Agent 或请求创建受限的执行上下文。


二、Plugin:模型可以调用的能力契约

1. Plugin 不只是一个函数集合

在 SK 中,Plugin 是一组可以暴露给 AI 应用或服务的函数。函数通常对应已有代码、API 或业务操作。SK 使用函数调用机制,将函数名称、描述和参数 Schema 发送给模型;模型返回函数调用请求后,SK 再把请求分派到真实代码。(learn.microsoft.com)

一个可被 Agent 使用的函数至少包含以下信息:

Plugin 名称
Function 名称
功能描述
参数名称
参数类型
参数是否必填
参数语义
返回值类型
副作用
权限要求
幂等性
错误模型

因此,Plugin 的真正形式更接近:

PluginFunction=(name,description,inputSchema,outputSchema,sideEffects,policy)\text{PluginFunction} = (\text{name}, \text{description}, \text{inputSchema}, \text{outputSchema}, \text{sideEffects}, \text{policy})

只写一个方法名是不够的。模型需要知道这个函数“什么时候应该调用”“传什么参数”“是否会产生副作用”。

2. 一个带有副作用边界的 Plugin

下面的插件提供两个能力:

  • 查询订单;
  • 取消订单。
using System.ComponentModel;
using Microsoft.SemanticKernel;

public sealed class OrderPlugin
{
    private readonly IOrderService _orders;

    public OrderPlugin(IOrderService orders)
    {
        _orders = orders;
    }

    [KernelFunction("get_order")]
    [Description("查询指定订单的当前状态、金额和商品摘要。只读操作,不会修改订单。")]
    public async Task<OrderSummary> GetOrderAsync(
        [Description("订单编号,例如 ORD-1001")] string orderId)
    {
        if (string.IsNullOrWhiteSpace(orderId))
        {
            throw new ArgumentException("orderId 不能为空", nameof(orderId));
        }

        return await _orders.GetSummaryAsync(orderId);
    }

    [KernelFunction("cancel_order")]
    [Description("取消一个尚未发货的订单。该操作会修改订单状态,调用前必须确认订单属于当前用户。")]
    public async Task<CancelOrderResult> CancelOrderAsync(
        [Description("订单编号,例如 ORD-1001")] string orderId,
        [Description("取消原因")] string reason)
    {
        if (string.IsNullOrWhiteSpace(orderId))
        {
            throw new ArgumentException("orderId 不能为空", nameof(orderId));
        }

        if (string.IsNullOrWhiteSpace(reason))
        {
            throw new ArgumentException("reason 不能为空", nameof(reason));
        }

        return await _orders.CancelAsync(orderId, reason);
    }
}

这里的 IOrderService 是领域服务,不是模型服务:

public interface IOrderService
{
    Task<OrderSummary> GetSummaryAsync(string orderId);
    Task<CancelOrderResult> CancelAsync(string orderId, string reason);
}

这种分层的因果关系是:

模型决定是否提出调用请求
        ↓
SK 根据函数名和参数 Schema 解析请求
        ↓
应用过滤器检查身份、租户、权限和风险
        ↓
Plugin 调用领域服务
        ↓
领域服务执行事务、幂等和外部 API 调用
        ↓
结构化结果返回模型

模型只负责提出意图,不能替代订单服务中的授权、状态校验和事务。

3. 函数调用的完整循环

启用自动函数调用后,典型流程如下:

  1. SK 将可用函数及其参数序列化为 JSON Schema;
  2. 将聊天历史和函数定义发送给模型;
  3. 模型返回普通消息,或者返回一个或多个函数调用;
  4. SK 解析函数名称和参数;
  5. 调用 Kernel 中对应函数;
  6. 将函数结果写回聊天历史;
  7. 再次请求模型;
  8. 直到模型输出最终消息,或达到最大迭代次数。(learn.microsoft.com)

设第 ii 轮上下文为 HiH_i,工具集合为 TT,模型输出为 rir_i

ri=LLM(Hi,T)r_i = LLM(H_i, T)

如果:

ri=ToolCall(t,a)r_i = \text{ToolCall}(t, a)

则应用执行:

oi=t(a)o_i = t(a)

并构造下一轮上下文:

Hi+1=Hi{assistant(ri),tool(oi)}H_{i+1} = H_i \cup \{\text{assistant}(r_i), \text{tool}(o_i)\}

如果模型直接返回文本:

ri=FinalTextr_i = \text{FinalText}

则本次 Agent 调用结束。

函数调用不是“模型执行了代码”,而是“模型生成了结构化调用请求,运行时决定是否执行代码”。

4. Plugin 名称是命名空间

SK 会使用 Plugin 名称对函数进行命名空间隔离。例如两个插件都拥有 search 函数时,可以形成:

knowledge-search
ticket-search

官方文档指出,插件名称会参与函数命名空间,因此应使用清晰且具有业务语义的名称,避免重复或无意义的 PluginService 后缀。(learn.microsoft.com)

这对“Agent 工具注册表”有直接影响。注册表中的一条工具记录不应只有函数名,还应至少包括:

{
  "plugin": "order",
  "function": "get_order",
  "version": "2",
  "tenant": "tenant-a",
  "risk": "read",
  "input_schema_hash": "sha256:...",
  "enabled_for": ["customer-support-agent"]
}

Agent 任务分解时,子任务的前置条件可以映射到工具约束:

子任务:确认订单状态
前置条件:用户身份已确认、订单编号已提取
可用工具:order.get_order:v2
产物:OrderSummary
验收:订单属于当前租户且状态字段完整

这样,任务分解不是只生成自然语言步骤,而是生成可校验的执行计划。

5. 只读函数与副作用函数必须区别处理

Plugin 函数大致可以分为两类:

数据检索函数

例如:

search_policy
get_customer_profile
find_similar_tickets

它们通常用于 RAG、上下文补充和事实查询。

任务自动化函数

例如:

cancel_order
issue_refund
send_email
deploy_service

它们会改变系统状态,通常需要更严格的授权、审批、幂等和审计。

一个常见反例是把“退款”写成普通的自然语言工具:

[KernelFunction]
public Task<string> Refund(string orderId)

这个定义存在多个问题:

  • 没有金额或币种约束;
  • 没有明确退款状态;
  • 没有幂等键;
  • 没有说明是否需要人工审批;
  • 返回字符串,模型无法可靠区分成功、处理中和失败;
  • 没有说明用户与订单的归属关系。

更合理的返回值应是结构化结果:

public sealed record RefundResult(
    string RequestId,
    string OrderId,
    decimal Amount,
    string Currency,
    string Status,
    bool RequiresApproval,
    string? ErrorCode);

函数返回 Schema 越明确,模型越不需要猜测字段含义。官方文档也建议向模型提供清晰的返回类型 Schema,以减少因模型自行推断字段而导致的调用错误。(learn.microsoft.com)


三、Plugin 注册表:能力发现、租户过滤、版本和动态装配

Plugin 注册表不是 SK 的单一内置对象,而是 Agent 系统通常需要在 Kernel 之上补充的治理层。

1. 为什么不能把所有 Plugin 都注册给所有 Agent

假设系统有四个插件:

knowledge.search
order.get_order
order.cancel_order
deployment.restart

客服 Agent 只能使用前三者中的查询能力,不能使用部署重启。若把全部函数发送给模型,会产生三个风险:

  1. 能力越权:模型可能提出不适合当前角色的调用;
  2. 提示上下文膨胀:函数描述占用模型上下文;
  3. 决策歧义:多个相似函数会降低工具选择准确性。

因此,运行时应先计算工具集合:

Tagent=TallTtenantTroleTversionTpolicyT_{agent} = T_{all} \cap T_{tenant} \cap T_{role} \cap T_{version} \cap T_{policy}

其中:

  • TallT_{all}:系统已注册工具;
  • TtenantT_{tenant}:当前租户允许的工具;
  • TroleT_{role}:当前 Agent 角色允许的工具;
  • TversionT_{version}:兼容当前任务契约的版本;
  • TpolicyT_{policy}:通过风险、审批和环境策略的工具。

2. 动态装配的正确边界

动态装配不是让模型动态加载任意程序集。安全的动态装配过程应是:

请求进入
  ↓
解析 tenantId、agentId、environment
  ↓
查询工具注册表
  ↓
检查版本兼容性和策略
  ↓
从受信任目录加载 Plugin 工厂
  ↓
通过 DI 创建 Plugin
  ↓
只将过滤后的函数加入本次执行 Kernel
  ↓
调用模型

插件加载器可以抽象为:

public interface IPluginFactory
{
    string PluginName { get; }
    Version Version { get; }

    object Create(IServiceProvider services);
}

生产环境还应校验:

程序集来源
签名或哈希
插件版本
依赖版本
权限声明
租户范围
环境范围
输入输出契约
回滚版本

不要把模型输出直接作为程序集名称、类名或 URL。以下做法是不安全的:

var pluginName = modelGeneratedText;
LoadAssembly(pluginName);

模型输出是非可信输入,不能直接决定代码加载、网络访问或权限扩大。

3. 版本兼容不是字符串比较

工具版本至少需要区分:

  • 函数名称是否兼容;
  • 输入 Schema 是否向后兼容;
  • 输出 Schema 是否向后兼容;
  • 副作用语义是否变化;
  • 权限和审批要求是否变化。

例如,order.cancel_order:v1 接受:

{
  "orderId": "ORD-1001",
  "reason": "重复下单"
}

如果 v2 增加了必填的 idempotencyKey,它就不一定兼容旧的任务计划。任务系统应将工具契约哈希或版本写入任务产物,避免恢复任务时加载到语义不同的函数。


四、Memory:不是一个“记忆开关”

1. Agent 中至少有三种状态

“Memory”在 Agent 系统里经常被过度简化。实际应区分:

状态类型 作用 典型存储
对话状态 保存当前会话消息 Chat History、Agent Thread
业务状态 保存流程进度和事务事实 数据库、Process State
语义记忆 按相似性查找知识或历史内容 向量库、全文检索、混合检索

这三者不能互相替代。

例如:

“用户刚才说订单号是 ORD-1001”

更适合放在当前线程或结构化槽位中。

“订单 ORD-1001 当前状态为已发货”

应从订单服务实时查询,而不是依赖语义记忆。

“公司退款政策第 4.2 条”

适合进入文档索引和检索系统。

一个关键原则是:

事实新鲜度>向量相似度\text{事实新鲜度} > \text{向量相似度}

订单状态、库存、账户余额等动态事实不能因为“历史文本相似”就被当作当前事实。

2. Agent Thread 与 Memory 的区别

SK 的 Agent 架构使用 AgentThread 抽象会话或对话状态。不同 Agent 对线程的管理方式不同:

  • 有些 Agent 要求应用程序保存完整聊天历史;
  • 有些服务端 Agent 将会话状态存储在服务侧,通过线程 ID 操作;
  • 状态型 Agent 通常需要匹配的线程实现,例如 Azure AI Agent 对应 AzureAIAgentThread。(learn.microsoft.com)

因此,Thread 更接近:

“这次对话发生了什么”

而不是:

“系统长期知道什么”

如果将全部聊天历史永久当作 Memory,会导致:

  • 隐私保留时间过长;
  • 过时信息持续污染上下文;
  • Token 成本增加;
  • 租户隔离变得困难;
  • 删除和纠错难以实现。

3. 向量记忆的数学基础

给定文本 xx,Embedding 模型生成向量:

e(x)Rde(x) \in \mathbb{R}^{d}

查询文本 qq 生成:

e(q)Rde(q) \in \mathbb{R}^{d}

常见的余弦相似度是:

sim(q,x)=e(q)e(x)e(q)e(x)\operatorname{sim}(q,x) = \frac{e(q)\cdot e(x)} {\|e(q)\|\|e(x)\|}

系统取相似度最高的前 kk 条记录,再将它们转换为上下文:

Ck(q)=TopKxDsim(q,x)C_k(q) = \operatorname{TopK}_{x \in D} \operatorname{sim}(q,x)

但实际检索还应加入元数据过滤:

D={xDx.tenant=tenantIdx.aclprincipalx.versioncompatibleVersions}D' = \{x \in D \mid x.tenant = tenantId \land x.acl \supseteq principal \land x.version \in compatibleVersions \}

然后才执行相似度搜索:

Ck(q)=TopKxDsim(q,x)C_k(q) = \operatorname{TopK}_{x \in D'} \operatorname{sim}(q,x)

如果先全库向量搜索,再在应用层过滤租户,可能已经把其他租户的记录取出并暴露到内存、日志或模型请求中。

4. SK Vector Store 的抽象

SK 的 Vector Store 抽象包含三个主要层次:

Vector Store
  └── Collection
        └── Record

可以理解为:

  • Vector Store:数据库实例;
  • Collection:记录集合以及索引;
  • Record:单条数据记录。(learn.microsoft.com)

记录通常包括:

{
  "id": "policy-4-2",
  "tenantId": "tenant-a",
  "content": "退款政策……",
  "embedding": [0.012, -0.031, 0.044],
  "source": "policy.pdf",
  "version": "2026-01",
  "acl": ["support"],
  "updatedAt": "2026-02-01T10:00:00Z"
}

SK 还可以将 Vector Store 搜索包装成 Text Search,再暴露为 Plugin,用于 RAG 或函数调用。官方文档给出的路径是:取得记录集合、用 VectorStoreTextSearch 包装,再转换为 Plugin。(learn.microsoft.com)

需要特别区分规范保证和版本状态:SK 的 Vector Store 功能在官方文档中仍标为 Preview,底层抽象和连接器可能发生破坏性变化;内存向量连接器适合原型或进程内高速操作,不适合作为生产持久化存储。(learn.microsoft.com)

5. 检索结果不是事实证明

一个常见错误是:

检索到了相似文档
      ↓
直接相信文档内容
      ↓
执行高风险操作

正确路径应是:

检索政策文档
      ↓
提取相关条款
      ↓
检查文档版本和租户范围
      ↓
将政策作为决策依据
      ↓
调用订单或财务服务重新验证当前状态
      ↓
必要时进入审批

反例:

向量库中保存了“订单已退款”

这不能证明当前订单仍然已退款,因为退款状态可能在写入向量库后发生变化。动态状态必须由权威业务服务确认。


五、Process:把 Agent 行为放入可审计的业务流程

1. Process 与 Agent 的根本区别

Agent 的典型问题是:

下一步我应该做什么?

Process 的典型问题是:

当前状态是什么?
哪些步骤已经完成?
哪个事件可以触发下一步?
失败后从哪里恢复?

Process 是一个为业务目标组织的活动集合。SK Process Framework 将流程拆成 Process、Step 和 Pattern:

  • Process:为了完成业务目标的一组步骤;
  • Step:有明确输入和输出的活动;
  • Pattern:决定步骤如何执行的结构。(learn.microsoft.com)

可以用状态机表示:

P=(Q,Σ,δ,q0,F)P = (Q, \Sigma, \delta, q_0, F)

其中:

  • QQ:流程状态集合;
  • Σ\Sigma:事件集合;
  • δ\delta:状态迁移函数;
  • q0q_0:初始状态;
  • FF:完成状态集合。

例如订单退款流程:

Created
  ↓ PaymentVerified
Eligible
  ↓ ApprovalRequired
WaitingForApproval
  ↓ Approved
RefundExecuting
  ↓ RefundSucceeded
Completed

失败路径不能被省略:

RefundExecuting
  ├── RefundSucceeded → Completed
  ├── Timeout         → RetryScheduled
  ├── ProviderError   → Failed
  └── UnknownResult   → ReconciliationRequired

2. Step 不应只返回自然语言

不可靠的 Step:

“请检查订单并决定是否退款”

可靠的 Step 应有结构化输入、输出和验收条件:

输入:
- orderId
- principalId
- tenantId

输出:
- eligibility
- reasonCode
- refundAmount
- requiresApproval

验收:
- orderId 已确认属于 tenantId
- 退款金额来自订单服务
- eligibility 只能取 ALLOWED、DENIED、REVIEW

Agent 可以参与 Step 内部的分析,但 Process 负责约束状态迁移。也就是说:

Agent:建议“可以退款”
Process:检查订单状态、金额、审批和幂等性后决定是否进入退款步骤

这能防止模型的一次错误输出直接变成不可逆业务动作。

3. 事件驱动的数据流

SK Process Framework 使用事件驱动模型,Step 可以通过 Kernel Functions 执行任务,并根据事件推进流程。官方文档同时明确指出,该 Process Framework 仍处于实验阶段,接口可能变化。(learn.microsoft.com)

一个完整流程的运行记录应类似:

{
  "processId": "refund-20260901-0001",
  "state": "WaitingForApproval",
  "events": [
    {
      "type": "RefundRequested",
      "at": "2026-09-01T09:00:00Z"
    },
    {
      "type": "EligibilityChecked",
      "payload": {
        "orderId": "ORD-1001",
        "amount": 99.00,
        "currency": "CNY",
        "requiresApproval": true
      }
    }
  ],
  "pending": "HumanApproval"
}

恢复时,系统不应重新让模型从头猜测,而应从持久化状态恢复:

读取 processId
  ↓
恢复 state = WaitingForApproval
  ↓
读取未完成事件
  ↓
等待 Approved 或 Rejected
  ↓
继续后续 Step

4. Process 的并发和幂等

如果一个退款流程收到两次相同的 Approved 事件,系统必须避免执行两次退款。

常见做法是引入幂等键:

idempotencyKey=hash(processId,stepName,businessObjectId)idempotencyKey = hash(processId, stepName, businessObjectId)

执行前检查:

若 idempotencyKey 已成功执行:
    返回已有结果
否则:
    创建执行记录
    调用外部服务
    保存结果

但仅靠“先查询再执行”仍可能发生竞态:

请求 A 查询:不存在
请求 B 查询:不存在
请求 A 执行退款
请求 B 执行退款

因此,幂等记录需要数据库唯一约束或原子插入,而不是只依赖应用层判断。


六、Agent 编排:控制多个 Agent 如何协作

1. 编排不等于多 Agent

编排是对多个 Agent 的消息传递、任务分派、执行顺序和结果合并进行控制。

SK Agent Framework 的基础抽象包括:

  • Agent:Agent 的统一抽象;
  • AgentThread:线程或对话状态;
  • Agent Orchestration:多个 Agent 的协作结构。(learn.microsoft.com)

官方列出的编排模式包括:

  • Concurrent;
  • Sequential;
  • Handoff;
  • Group Chat;
  • Magentic。

这些 Agent Orchestration 能力目前仍属于实验阶段,可能发生较大变化。官方同时提示,旧的 AgentGroupChat 已不再维护,新的实现应关注 GroupChatOrchestration。(learn.microsoft.com)

2. 五种编排模式的因果差异

Sequential:顺序流水线

需求分析 Agent
      ↓
资料检索 Agent
      ↓
方案撰写 Agent
      ↓
审核 Agent

适合前置条件明确、后一步依赖前一步产物的任务。

形式化表示:

A1(x)=y1A_1(x) = y_1

A2(y1)=y2A_2(y_1) = y_2

A3(y2)=y3A_3(y_2) = y_3

反例是把互不依赖的三个检索任务也强制串行,会增加延迟。

Concurrent:并行执行

           ┌── 数据库检索 Agent ──┐
用户请求 ──┼── 文档检索 Agent   ──┼── 汇总 Agent
           └── API 查询 Agent   ──┘

适合子任务之间没有写冲突,且都只依赖同一个输入的场景。

必要条件可以写成:

ij,WiWj=\forall i \neq j,\quad W_i \cap W_j = \varnothing

其中 WiW_i 是 Agent ii 的写集合。如果两个并发 Agent 都会更新同一订单状态,就不能简单使用并发模式。

Handoff:责任转移

入口 Agent
  ├── 技术问题 → 技术 Agent
  ├── 退款问题 → 财务 Agent
  └── 投诉问题 → 人工服务 Agent

Handoff 的核心不是广播消息,而是把后续处理责任交给另一个 Agent。适合路由和专业分工,但需要定义:

何时转交
转交时携带哪些上下文
原 Agent 是否继续监听
转交失败如何回退

Group Chat:共享会话协作

多个 Agent 在同一个会话中发言,由选择器或规则决定下一位发言者。

它适合讨论、评审和观点对比,但容易出现:

  • 重复发言;
  • 角色边界模糊;
  • 无人负责最终提交;
  • Token 消耗随轮次增长。

因此,Group Chat 必须有终止条件,例如:

达到最大轮数
出现结构化 FinalAnswer
审核 Agent 输出 Approved
主持 Agent 判定所有验收项完成

Magentic:动态规划式协作

Magentic 类模式更适合任务目标不完全确定、需要动态拆解和调整的场景。但动态性越强,越需要额外的:

预算限制
最大步骤数
工具白名单
结果验收器
失败回退策略

否则它可能形成“模型不断提出新子任务”的循环。

3. 编排中的数据变换

多个 Agent 不应只传递一段未经约束的文本。更可靠的方式是定义中间产物:

{
  "taskId": "refund-review-001",
  "orderId": "ORD-1001",
  "facts": [
    {
      "name": "order_status",
      "value": "SHIPPED",
      "source": "order-service",
      "observedAt": "2026-09-01T09:00:00Z"
    }
  ],
  "decision": "REVIEW",
  "reasonCode": "SHIPPED_ORDER",
  "nextAction": "HUMAN_APPROVAL"
}

数据变换函数:

TAB:OutputAInputBT_{A \to B}: Output_A \to Input_B

其职责是:

  • 转换字段名称;
  • 丢弃不必要的上下文;
  • 校验 Schema;
  • 注入租户和追踪标识;
  • 拒绝缺少前置条件的结果。

如果 Agent A 输出自然语言,Agent B 再从中抽取字段,就增加了一次不必要的不确定性。更好的做法是让 A 输出结构化数据,再由 B 消费结构化数据。


七、服务集成:AI 服务、业务服务和基础设施服务

1. 三类服务不要混淆

SK 的服务集成可以分成三类。

AI 服务

例如:

Chat Completion
Text Generation
Embedding
Text-to-Image
Speech-to-Text

业务服务

例如:

订单服务
CRM
工单系统
支付系统
库存系统
审批系统

基础设施服务

例如:

日志
指标
Tracing
HTTP 客户端
缓存
身份认证
密钥管理
数据库连接池

SK 官方文档提供了多种 AI 服务连接器,包括 Azure OpenAI、OpenAI、Google、Mistral、Ollama、Amazon Bedrock 等,并支持 C#、Python 或 Java 的不同组合。(learn.microsoft.com)

服务抽象的价值是将 Agent 逻辑与具体模型解耦:

IChatCompletionService chatService

上层只依赖聊天补全能力,而不是直接依赖某一家模型 SDK。更换模型时,主要修改连接器和配置,而不是重写 Plugin 和流程。

2. 一个完整的聊天 Agent 调用路径

以“查询订单并回答用户”为例:

HTTP 请求
  ↓
认证与租户解析
  ↓
加载 Agent 配置
  ↓
创建受限 Kernel
  ↓
注册 order.get_order 与 policy.search
  ↓
加载当前 Agent Thread
  ↓
模型判断是否调用工具
  ↓
Function Invocation Filter 校验权限
  ↓
OrderPlugin 调用订单服务
  ↓
结果回写模型
  ↓
生成最终答案
  ↓
持久化 Thread、审计事件和指标

这个流程中至少有三个边界:

  1. 模型边界:模型不能直接访问数据库;
  2. Plugin 边界:Plugin 不能绕过领域服务;
  3. 流程边界:高风险动作不能只由自然语言决定。

3. OpenAPI 与 MCP 的集成方式

SK 支持从本地原生代码、OpenAPI 规范或 MCP Server 导入插件。原生 Plugin 适合在同一代码库中复用依赖;OpenAPI 和 MCP 更适合跨语言、跨团队或跨进程共享能力。(learn.microsoft.com)

三种方式的区别如下:

方式 适合场景 主要优点 主要风险
Native Plugin 本地代码和领域服务 类型安全、依赖注入方便 与应用耦合
OpenAPI Plugin 已有 HTTP API 跨语言、契约清晰 API 描述可能不完整
MCP Server 跨应用共享工具 工具可被多个客户端使用 权限、网络和版本治理更复杂

导入 OpenAPI 并不会自动解决安全问题。OpenAPI 描述的是接口契约,不一定包含:

当前用户能否访问资源
租户范围
调用频率
副作用等级
审批要求
数据脱敏规则

因此,OpenAPI Plugin 仍然需要在应用侧叠加授权和审计。


八、过滤器:把安全控制放在函数真正执行之前

SK 提供函数调用过滤器、提示词渲染过滤器和自动函数调用过滤器。过滤器可以用于检查权限、记录调用、修改上下文或阻止不允许的函数执行。(learn.microsoft.com)

1. 为什么不能只在系统提示词中写权限规则

下面的提示词规则不够可靠:

你不能为其他租户查询订单。

原因是提示词属于模型输入,不是强制安全边界。模型可能:

  • 错误理解租户;
  • 忽略规则;
  • 误用函数参数;
  • 在函数返回结果中获得越权数据。

权限必须在代码路径上执行:

模型提出 order.get_order(orderId)
       ↓
过滤器从服务端上下文读取 tenantId
       ↓
检查 orderId 是否属于 tenantId
       ↓
允许或拒绝函数执行

2. 过滤器应该检查什么

至少包括:

主体身份 principalId
租户 tenantId
Agent 身份 agentId
函数版本
资源归属
操作风险
审批状态
请求幂等键
速率限制
参数范围

伪代码如下:

public async Task<object?> InvokeAsync(
    FunctionInvocationContext context,
    Func<FunctionInvocationContext, Task<object?>> next)
{
    var function = context.Function;

    if (!policy.Allowed(
            principalId: requestContext.PrincipalId,
            tenantId: requestContext.TenantId,
            agentId: requestContext.AgentId,
            plugin: function.PluginName,
            functionName: function.Name))
    {
        throw new UnauthorizedAccessException(
            $"Function denied: {function.PluginName}.{function.Name}");
    }

    if (riskCatalog.IsHighRisk(function.PluginName, function.Name))
    {
        await approvalService.RequireApprovalAsync(context);
    }

    return await next(context);
}

需要注意,过滤器只负责通用的执行前控制;订单是否可退款、库存是否足够等业务规则仍应由领域服务负责。


九、一个端到端示例:客服 Agent 查询订单

下面用一个简化示例串起 Kernel、Plugin、模型服务和 Thread。示例重点是数据流,不依赖某个特定模型名称。

1. 定义服务和插件

public sealed record OrderSummary(
    string OrderId,
    string Status,
    decimal Amount,
    string Currency);

public sealed class OrderPlugin
{
    private readonly IOrderService _orders;

    public OrderPlugin(IOrderService orders)
    {
        _orders = orders;
    }

    [KernelFunction("get_order")]
    [Description("查询当前用户有权访问的订单状态和金额,只读。")]
    public Task<OrderSummary> GetOrderAsync(
        [Description("订单编号")] string orderId)
    {
        return _orders.GetSummaryAsync(orderId);
    }
}

2. 装配 Kernel

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: config["AzureOpenAI:Deployment"]!,
    endpoint: config["AzureOpenAI:Endpoint"]!,
    apiKey: config["AzureOpenAI:ApiKey"]!);

builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.AddSingleton<IAuthorizationPolicy, AuthorizationPolicy>();

builder.Plugins.AddFromType<OrderPlugin>("order");

Kernel kernel = builder.Build();

这里的前置条件是:

  • 已创建 Azure OpenAI 部署;
  • 配置了 Endpoint、部署名和密钥;
  • OrderService 能够访问订单系统;
  • AuthorizationPolicy 能够获取当前请求主体和租户。

3. 调用聊天完成服务

using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var chatService =
    kernel.GetRequiredService<IChatCompletionService>();

var history = new ChatHistory();

history.AddSystemMessage("""
你是客服 Agent。
只能查询当前用户有权限访问的订单。
如果用户没有提供订单编号,应先要求订单编号。
不要猜测订单状态、金额或币种。
""");

history.AddUserMessage("帮我查一下订单 ORD-1001 的状态");

var settings = new OpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var response = await chatService.GetChatMessageContentAsync(
    history,
    executionSettings: settings,
    kernel: kernel);

Console.WriteLine(response.Content);

模型可能产生如下调用循环:

用户:
帮我查一下订单 ORD-1001 的状态

模型:
调用 order.get_order({ "orderId": "ORD-1001" })

Plugin:
{
  "orderId": "ORD-1001",
  "status": "SHIPPED",
  "amount": 99.00,
  "currency": "CNY"
}

模型:
订单 ORD-1001 当前状态为“已发货”,订单金额为 99.00 元。

代码能否直接编译,取决于目标 SDK 的具体版本和连接器包;SK 的 Agent、Process、Vector Store 等模块更新较快,实际项目应根据目标语言版本锁定包版本,并以对应 API 文档和编译器结果为准。本文不把实验性 API 当成稳定契约。

4. 为什么查询结果必须由服务返回

不要让模型自己生成:

{
  "orderId": "ORD-1001",
  "status": "SHIPPED"
}

模型生成的是意图和文本,不是订单系统的事实。正确结果必须来自:

OrderPlugin
  → OrderService
    → Order Database / Order API

如果订单服务返回超时,模型不应把“上一次已发货”当成当前状态。此时应返回明确的失败状态:

{
  "status": "UNKNOWN",
  "errorCode": "ORDER_SERVICE_TIMEOUT",
  "retryable": true
}

Agent 可以向用户解释“暂时无法查询”,但不能把未知状态伪装成已知状态。


十、错误处理:模型错误、工具错误和业务错误必须分层

1. 模型错误

包括:

模型不可用
限流
上下文过长
函数调用格式错误
模型返回无法解析的参数

处理方式通常是:

  • 重试可重试的网络错误;
  • 限制最大函数调用轮次;
  • 对参数做 Schema 校验;
  • 对超长上下文执行裁剪或摘要;
  • 记录模型请求和响应元数据。

2. 工具错误

包括:

HTTP 连接失败
数据库连接失败
外部服务返回 500
认证过期
函数不存在
插件版本不兼容

工具错误不应被直接拼接成自然语言再交给模型。应先归一化:

public sealed record ToolError(
    string Code,
    string Message,
    bool Retryable,
    string? CorrelationId);

模型只需要知道可解释的业务结果:

{
  "success": false,
  "error": {
    "code": "ORDER_SERVICE_UNAVAILABLE",
    "retryable": true
  }
}

3. 业务错误

业务错误并不表示系统故障:

订单已发货,不能取消
订单不属于当前用户
退款金额超过可退金额
需要人工审批

这类结果应作为正常领域结果返回,而不是抛出无法区分的 Exception

{
  "success": false,
  "decision": "DENIED",
  "reasonCode": "ORDER_ALREADY_SHIPPED",
  "requiresApproval": false
}

这样 Agent 可以正确解释结果,Process 也可以根据 reasonCode 选择状态迁移。


十一、可观测性:记录“为什么调用”,而不只是“调用了什么”

一个 Agent 请求至少应关联:

traceId
requestId
tenantId
principalId
agentId
threadId
processId
pluginName
functionName
functionVersion
modelId
promptVersion
latency
tokenUsage
retryCount
resultCode

SK 的 Kernel 和过滤器机制适合在模型调用、提示词渲染和函数执行边界加入日志与遥测。官方文档将可观测性、过滤器和安全列为企业组件的一部分。(learn.microsoft.com)

仅记录:

order.get_order 调用成功

是不够的。生产诊断还需要回答:

为什么这个 Agent 能看到这个函数?
为什么模型选择了这个函数?
函数参数来自用户、Memory 还是另一个 Agent?
调用时使用了哪个版本?
返回结果是否被过滤或脱敏?

一个完整的调用事件可以是:

{
  "traceId": "tr-001",
  "tenantId": "tenant-a",
  "agentId": "support-agent",
  "plugin": "order",
  "function": "get_order",
  "version": "2",
  "decision": "allowed",
  "inputHash": "sha256:...",
  "resultCode": "OK",
  "latencyMs": 84
}

高敏感字段不应直接写入日志。可以记录哈希、字段存在性和结果代码,而不是完整订单内容。


十二、Semantic Kernel 与 CrewAI 的边界差异

CrewAI 文档将 Agent、Crew 和 Flow 作为主要抽象,并强调 Flow 可以通过 start、listen、router 等步骤编排任务、管理状态、持久化执行并恢复长流程;Tasks & Processes 则用于定义顺序、层级或混合流程。(docs.crewai.com)

这与 SK 的关注点有相似之处,但抽象重心不同:

维度 Semantic Kernel CrewAI
核心运行时 Kernel、AI Service、Plugin Agent、Crew、Flow
工具模型 Plugin / Kernel Function Tool
流程模型 Process Framework Flow、Tasks、Processes
Agent 协作 Agent Orchestration Crew / Flow
服务集成 多语言连接器、DI、OpenAPI、MCP 工具、集成和 Flow
当前稳定性重点 Process 和 Orchestration 需关注实验状态 具体能力以其版本文档为准

这不是“谁更强”的比较,而是工程边界不同:

  • 如果已有 .NET、Java 或 Python 服务,希望把模型和企业 API 接入现有应用,SK 的 Kernel、Plugin 和连接器更自然;
  • 如果主要目标是快速组织多个角色型 Agent 和任务流程,CrewAI 的 Crew、Task、Flow 抽象可能更直接;
  • 无论使用哪个框架,租户过滤、工具版本、幂等、审批、状态持久化和审计都不能由框架名称自动解决。

十三、常见误解与反例

误解一:注册 Plugin 后,Agent 就能安全使用它

错误。注册只是装配,安全使用还需要:

工具发现
租户过滤
角色授权
参数校验
资源归属校验
风险审批
审计

误解二:Memory 可以替代数据库

错误。Memory 适合提供相关上下文,不能替代订单、支付、库存等权威系统。

向量检索:找到“可能相关”的信息
数据库查询:确认“当前真实”的状态

误解三:Process 就是把多个 Prompt 串起来

错误。Process 需要状态、事件、输入输出、迁移和恢复。Prompt 只是某个 Step 内部可能使用的实现方式。

误解四:多 Agent 一定比单 Agent 更好

错误。多个 Agent 会增加:

消息传递
上下文复制
Token 消耗
调度复杂度
失败路径
一致性问题

如果一个 Agent 加两个清晰的 Plugin 就能完成任务,引入五个 Agent 反而会降低可诊断性。

误解五:模型返回了函数调用,就应该自动执行

错误。函数调用是请求,不是授权。尤其是写操作,必须经过应用层策略和领域服务校验。

误解六:把所有工具描述都放进上下文更保险

错误。工具越多,模型选择空间越大,描述成本越高,越容易误选。应根据 Agent、租户、任务类型和风险等级生成最小工具集合。


十四、如何为一个生产 Agent 组织这些组件

一个可落地的分层可以是:

接口层
  └── 认证、限流、租户解析、请求校验

Agent 层
  └── 目标、系统指令、Thread、响应格式

能力层
  └── Plugin、工具注册表、版本、权限和动态装配

流程层
  └── Process、Step、事件、状态、重试、恢复

知识层
  └── 检索、Vector Store、文档版本、ACL 过滤

领域层
  └── 订单、支付、库存、审批等确定性服务

基础设施层
  └── AI Connector、数据库、HTTP、日志、Tracing、密钥

一次任务从进入到结束,可以表示为:

RequestTask DecompositionCapability SelectionAgent ReasoningPolicy CheckPlugin ExecutionProcess TransitionAcceptance\text{Request} \rightarrow \text{Task Decomposition} \rightarrow \text{Capability Selection} \rightarrow \text{Agent Reasoning} \rightarrow \text{Policy Check} \rightarrow \text{Plugin Execution} \rightarrow \text{Process Transition} \rightarrow \text{Acceptance}

其中最后一步不能省略。每个子任务都应有验收条件:

子任务:查询订单
前置条件:
- principalId 已确认
- tenantId 已确认
- orderId 已提取

调用:
- order.get_order:v2

产物:
- OrderSummary

验收:
- 订单属于当前租户
- status 非空
- observedAt 存在
- 结果来源为订单服务

这比“让 Agent 自己完成订单查询”更容易测试、恢复和审计。


十五、版本与稳定性判断

截至 2026 年 9 月,使用 SK 时应特别关注以下边界:

  • Kernel、Plugin 和基础 AI 服务连接器属于核心能力,但具体包名、配置类和函数调用 API 仍应以目标语言 SDK 版本为准;
  • Process Framework 官方文档仍标记为 Experimental;
  • Agent Orchestration 官方文档仍标记为 Experimental;
  • AgentGroupChat 已不再维护,新的实现应关注 GroupChatOrchestration
  • Vector Store 抽象和内存连接器官方文档标记为 Preview;
  • 连接器对不同语言和服务的支持矩阵并不完全相同。(learn.microsoft.com)

因此,生产发布时应同时固定:

SDK 版本
连接器版本
模型部署版本
Plugin 契约版本
Prompt 版本
Vector Store Schema 版本
Process 状态版本
Agent 编排配置版本

当任务需要长时间运行时,还应保存版本信息:

{
  "taskId": "task-001",
  "agentVersion": "support-agent-3",
  "pluginContractVersion": "order.v2",
  "promptVersion": "support-prompt-12",
  "processVersion": "refund-process-4"
}

恢复任务时,不能只加载“当前最新版本”。否则同一个任务可能在恢复前后使用不同的输入 Schema、工具语义或流程状态定义。


Semantic Kernel Agent 的核心不是某个单独的 Agent 类,而是几条明确的边界:

  • Plugin 把能力描述为模型可以请求、应用可以控制的函数;
  • Memory 提供上下文和知识,但不取代权威业务状态;
  • Process 将步骤、事件、状态和恢复机制固化为可审计流程;
  • 编排 决定多个 Agent 如何顺序、并行、转交或协作;
  • 服务集成 把模型、业务 API、数据库、身份和观测系统接入同一个运行时;
  • Kernel 负责装配和执行,但不自动替应用承担授权、事务和业务正确性。

当这几层被正确分开,Agent 才能从“会调用工具的聊天程序”变成可测试、可恢复、可审计的业务执行系统。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。