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 工程体系”需要先区分五个层次:
- Plugin:Agent 可以调用的能力契约。
- Process:按照事件、步骤和状态推进的业务流程。
- Memory:Agent 可持续访问的上下文和外部知识。
- 编排:一个或多个 Agent 之间的协作控制结构。
- 服务集成:模型、向量库、数据库、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 抽象成:
其中:
- 是服务集合;
- 是插件集合;
- 是经过注册后的函数集合;
- 是运行时元数据,例如函数描述、参数 Schema、过滤器和观测信息。
模型并不能直接访问 Kernel 中的任意对象。模型只能看到应用程序暴露给它的函数描述:
这条公式非常重要。把对象注册到 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();
这个过程包含四步:
- 创建
KernelBuilder; - 注册聊天补全服务;
- 注册日志和其他依赖;
- 将插件添加到 Kernel;
Build()后获得不可变程度更高、可注入运行的 Kernel 实例。
实际项目中,Kernel 通常不应在每次用户请求中随意创建。更常见的方式是将服务连接器、HTTP 客户端、日志、配置和插件工厂注册到依赖注入容器,再按租户、Agent 或请求创建受限的执行上下文。
二、Plugin:模型可以调用的能力契约
1. Plugin 不只是一个函数集合
在 SK 中,Plugin 是一组可以暴露给 AI 应用或服务的函数。函数通常对应已有代码、API 或业务操作。SK 使用函数调用机制,将函数名称、描述和参数 Schema 发送给模型;模型返回函数调用请求后,SK 再把请求分派到真实代码。(learn.microsoft.com)
一个可被 Agent 使用的函数至少包含以下信息:
Plugin 名称
Function 名称
功能描述
参数名称
参数类型
参数是否必填
参数语义
返回值类型
副作用
权限要求
幂等性
错误模型
因此,Plugin 的真正形式更接近:
只写一个方法名是不够的。模型需要知道这个函数“什么时候应该调用”“传什么参数”“是否会产生副作用”。
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. 函数调用的完整循环
启用自动函数调用后,典型流程如下:
- SK 将可用函数及其参数序列化为 JSON Schema;
- 将聊天历史和函数定义发送给模型;
- 模型返回普通消息,或者返回一个或多个函数调用;
- SK 解析函数名称和参数;
- 调用 Kernel 中对应函数;
- 将函数结果写回聊天历史;
- 再次请求模型;
- 直到模型输出最终消息,或达到最大迭代次数。(learn.microsoft.com)
设第 轮上下文为 ,工具集合为 ,模型输出为 :
如果:
则应用执行:
并构造下一轮上下文:
如果模型直接返回文本:
则本次 Agent 调用结束。
函数调用不是“模型执行了代码”,而是“模型生成了结构化调用请求,运行时决定是否执行代码”。
4. Plugin 名称是命名空间
SK 会使用 Plugin 名称对函数进行命名空间隔离。例如两个插件都拥有 search 函数时,可以形成:
knowledge-search
ticket-search
官方文档指出,插件名称会参与函数命名空间,因此应使用清晰且具有业务语义的名称,避免重复或无意义的 Plugin、Service 后缀。(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 只能使用前三者中的查询能力,不能使用部署重启。若把全部函数发送给模型,会产生三个风险:
- 能力越权:模型可能提出不适合当前角色的调用;
- 提示上下文膨胀:函数描述占用模型上下文;
- 决策歧义:多个相似函数会降低工具选择准确性。
因此,运行时应先计算工具集合:
其中:
- :系统已注册工具;
- :当前租户允许的工具;
- :当前 Agent 角色允许的工具;
- :兼容当前任务契约的版本;
- :通过风险、审批和环境策略的工具。
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 条”
适合进入文档索引和检索系统。
一个关键原则是:
订单状态、库存、账户余额等动态事实不能因为“历史文本相似”就被当作当前事实。
2. Agent Thread 与 Memory 的区别
SK 的 Agent 架构使用 AgentThread 抽象会话或对话状态。不同 Agent 对线程的管理方式不同:
- 有些 Agent 要求应用程序保存完整聊天历史;
- 有些服务端 Agent 将会话状态存储在服务侧,通过线程 ID 操作;
- 状态型 Agent 通常需要匹配的线程实现,例如 Azure AI Agent 对应
AzureAIAgentThread。(learn.microsoft.com)
因此,Thread 更接近:
“这次对话发生了什么”
而不是:
“系统长期知道什么”
如果将全部聊天历史永久当作 Memory,会导致:
- 隐私保留时间过长;
- 过时信息持续污染上下文;
- Token 成本增加;
- 租户隔离变得困难;
- 删除和纠错难以实现。
3. 向量记忆的数学基础
给定文本 ,Embedding 模型生成向量:
查询文本 生成:
常见的余弦相似度是:
系统取相似度最高的前 条记录,再将它们转换为上下文:
但实际检索还应加入元数据过滤:
然后才执行相似度搜索:
如果先全库向量搜索,再在应用层过滤租户,可能已经把其他租户的记录取出并暴露到内存、日志或模型请求中。
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)
可以用状态机表示:
其中:
- :流程状态集合;
- :事件集合;
- :状态迁移函数;
- :初始状态;
- :完成状态集合。
例如订单退款流程:
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 已成功执行:
返回已有结果
否则:
创建执行记录
调用外部服务
保存结果
但仅靠“先查询再执行”仍可能发生竞态:
请求 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
适合前置条件明确、后一步依赖前一步产物的任务。
形式化表示:
反例是把互不依赖的三个检索任务也强制串行,会增加延迟。
Concurrent:并行执行
┌── 数据库检索 Agent ──┐
用户请求 ──┼── 文档检索 Agent ──┼── 汇总 Agent
└── API 查询 Agent ──┘
适合子任务之间没有写冲突,且都只依赖同一个输入的场景。
必要条件可以写成:
其中 是 Agent 的写集合。如果两个并发 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"
}
数据变换函数:
其职责是:
- 转换字段名称;
- 丢弃不必要的上下文;
- 校验 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、审计事件和指标
这个流程中至少有三个边界:
- 模型边界:模型不能直接访问数据库;
- Plugin 边界:Plugin 不能绕过领域服务;
- 流程边界:高风险动作不能只由自然语言决定。
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、密钥
一次任务从进入到结束,可以表示为:
其中最后一步不能省略。每个子任务都应有验收条件:
子任务:查询订单
前置条件:
- 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 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:CrewAI:Agent、Task、Crew、Flow、状态和生产边界
- 下一篇:LlamaIndex Agent:Workflow、Tool、Context、RAG 和多 Agent
- 延伸:Agent 工具注册表:能力发现、租户过滤、版本和动态装配
- 延伸:Agent 任务分解:目标、子任务、前置条件、产物和验收
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论