1. 为什么我最终选了 Microsoft Agent Framework 而不是自己硬撸 Agent
先说结论:如果你在 .NET 技术栈里做 AI Agent 相关项目,别急着从零写一套 Agent 编排、工具调用、上下文管理的代码。我在这条路上折腾过好几轮,最后停靠在 Microsoft Agent Framework 上。它不是一个帮你调大模型 API 的玩具库,而是一套真正考虑过生产环境的 Agent 开发框架。
说下我自己的背景,方便你对号入座。我做了多年 .NET 后端,最近一年开始把业务系统接入大模型能力。一开始的需求非常朴素:让系统能根据用户的自然语言指令,自动完成一系列操作,比如查数据库、调内部 API、生成报表、发通知。最先想到的是直接调 OpenAI API,写一个循环,让模型决定调用哪个函数,然后拼上下文,再调一次模型……就这么一个"土法 Agent",我维护了大概三个月,然后彻底崩溃了。不是功能跑不通,而是每加一个工具、每换一个模型、每调一次提示词,都要动主流程代码。各种边界情况、超时、重试、工具返回格式不一致,全混在一起。
后来我看了微软在 Build 大会上关于 Agent Framework 的演讲,意识到一个事情:Agent 开发的核心难点不是"调大模型",而是"管理 Agent 的复杂度"。微软这套框架本质上把 Agent 的声明、技能、记忆、编排从业务代码里拆了出去,你只需要描述"这个 Agent 能做什么、可以用哪些工具、怎么处理多轮对话",剩下的事情框架帮你搞定。
这篇文章就围绕一个具体案例来展开:我基于 Microsoft Agent Framework 给一个内部工单系统接入了 Agent Skills,让 AI Agent 能够自动解析工单、查询关联数据、生成处理建议。整个过程从零开始,包含选型理由、环境配置、核心概念解释、Skill 编写步骤、实测效果和踩坑记录。如果你也想在 .NET 项目里接入 Agent,这篇文章应该能帮你少走不少弯路。
适合的读者:已经会 C# 和 ASP.NET Core 基础开发,想了解 Agent 到底怎么落地,对"Agent Skills"这个概念有点好奇但没空啃英文文档的同学。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Agent 与 Agent Skills 究竟是什么:先把这个概念掰清楚
聊技术之前,我特别想先掰一掰概念,因为"Agent"这个词被用得太烂了。有些人把一次带 function calling 的大模型调用就叫 Agent,有些人把 AutoGPT 那种自动循环才叫 Agent。在 Microsoft Agent Framework 的语境里,Agent 的定义其实非常清晰,而且它的设计思路和别的框架不太一样。
2.1 Microsoft Agent Framework 的核心抽象
Microsoft Agent Framework 是一个用于构建 AI Agent 的端到端开发框架,它提供了一套统一的抽象层,把模型接入、工具调用、记忆管理、多 Agent 协作、可观测性这些能力都收敛到一套 API 里。
它和 Semantic Kernel 的区别需要额外说一嘴。Semantic Kernel 更像是一个"AI 编排 SDK",你可以把 LLM、插件、记忆组合起来,解决"如何让 AI 完成任务"的问题。而 Agent Framework 更进一层,它强调"Agent 是一种具备自主决策能力的工作负载",框架提供的是 Agent 的运行环境和生命周期管理。这两个东西不是竞争关系,其实是有重叠的。微软官方也在逐步统一体验,具体到我们实际项目里,我的感受是:Semantic Kernel 适合做一个技能函数库,Agent Framework 适合把它们组装成能跑起来的 Agent 服务。
如果你用过 LangChain 或者 LangGraph,可以大致对标:Agent Framework 相当于把 LangGraph 的图编排能力 + 工具管理能力 + 会话记忆能力打包到了一起,同时针对 .NET 生态做了深度优化。
2.2 Skill 在 Agent 里的位置和意义
Agent Skills 是 Agent Framework 里的一个关键概念。把它翻译成"技能"其实有点误导,因为它不是简单的"技能库",而是一个完整的、自描述的、可复用的能力单元。
打个比方:Agent 像是一个员工,Skill 就像这个员工掌握的"工作手册+配套工具"。每个 Skill 里包含了:
- 这个技能是干什么用的描述(给 AI 看的,用来决定何时调用)
- 触发条件和参数定义(给 AI 看的,用来决定怎么调用)
- 实际的执行逻辑(给程序跑的,可以是 C# 代码、API 调用、甚至是一个子 Agent)
- 使用案例或说明(用来提示 AI 在什么场景下用)
所以你在 Agent Framework 里注册一个 Skill,本质上是在告诉 Agent:"嘿,你有一个这样的能力,当遇到这类问题时,你可以调用它,这是它的使用说明。"
这种设计的好处非常明显:技能的注册和实现是分离的。我可以把技能实现放在一个独立的类库里,然后在 Agent 配置里声明引用。以后想给 Agent 加一个新能力,只需要新增一个 Skill,不需要改动任何 Agent 主流程代码。
2.3 AI Skills 和 Agent 的区别:一个常被问的问题
热搜词里有一个"ai skills和agent的区别",我特意说说我的理解。AI Skills 是能力,Agent 是行动者。一个 Agent 可以拥有多个 Skills,然后自己决定什么时候用哪个 Skill 来完成任务。Skills 本身不决策,Agent 才决策。举例说,我的工单 Agent 拥有三个 Skills:
- 工单解析 Skill:从用户的自然语言工单里提取关键信息(标题、优先级、分类、关联系统)
- 数据查询 Skill:根据解析结果查询后台数据库,获取设备状态、历史工单、常见解决方案
- 报告生成 Skill:把查询结果整理成一段简洁的处理建议,并通知相关人员
单独看每个 Skill,它们都是"被动能力",是 Agent 决定调用它们,才形成完整的自动化流程。这个"区分"在架构设计时特别重要。你要做的是一个能决策和调度的 Agent,而不是把一堆 API 杂糅在一起。后者很容易变成"加了 AI 的 CRUD",而不是真正的 Agent。
3. 环境准备与项目搭建:从 .NET 8 到第一个 Agent 跑通
理论说得再多,不如动手跑一遍。我用的环境是 Windows 11 + .NET 8 + Visual Studio 2022,实际开发中也建议至少用 .NET 8,因为框架对性能、异步支持、依赖注入的整合都更顺。
3.1 安装框架包和创建项目结构
Microsoft Agent Framework 相关的 NuGet 包我已经在实际项目中用到的核心包拍在这里:
| 包名 | 用途 |
|---|---|
| Microsoft.Agent.Framework | Agent 运行时核心,包含 Agent、Skill、上下文管理等基础类型 |
| Microsoft.Agent.Framework.AspNet | 用于 ASP.NET Core 集成的扩展,包括中间件和端点路由 |
| Microsoft.Agent.Framework.Extensions.AI | 把 AI 服务(如 OpenAI、Azure OpenAI)注入到 Agent 的适配层 |
| Microsoft.Extensions.AI | 统一的 AI 服务抽象(这个其实是 .NET 生态的统一接口层) |
创建项目我建议用 ASP.NET Core Web API 模板作为宿主。Agent 本身可以是一个后台服务,但通过 Web API 暴露出来,方便调试和前端接入。一个最小的项目结构长这样:
code复制MyAgentProject/
├── Agents/
│ └── TicketAgent.cs
├── Skills/
│ ├── TicketParsingSkill.cs
│ ├── DataQuerySkill.cs
│ └── ReportGenerationSkill.cs
├── Models/
│ └── TicketModels.cs
├── Services/
│ └── TicketDataService.cs
└── Program.cs
创建好 Web API 项目之后,在项目文件里加上包引用。这里我用 dotnet CLI 操作:
bash复制dotnet add package Microsoft.Agent.Framework
dotnet add package Microsoft.Agent.Framework.AspNet
dotnet add package Microsoft.Agent.Framework.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
如果你是老项目升级,注意看一下目标框架,至少 .NET 8 是跑不开的。
3.2 配置模型接入:以 Azure OpenAI 为例
Agent 的大脑还是大模型,所以第一步是配模型。我用的是 Azure OpenAI 的 gpt-4o-mini,因为它在中文工单解析场景下性价比很高。配置放在 appsettings.json 里:
json复制{
"OpenAI": {
"Endpoint": "https://your-resource.openai.azure.com/",
"ApiKey": "your-api-key",
"DeploymentName": "gpt-4o-mini"
}
}
然后在 Program.cs 里注册 AI 服务。这里有个容易踩的坑:Agent Framework 的 AI 服务注册不使用 Microsoft.Extensions.AI 的传统方式,而是用框架自己的扩展方法。具体代码如下:
csharp复制builder.Services.AddOpenAIClient(builder.Configuration["OpenAI:ApiKey"],
new OpenAIClientOptions { Endpoint = new Uri(builder.Configuration["OpenAI:Endpoint"]) });
builder.Services.AddAgentFramework(options =>
{
options.DefaultModelName = builder.Configuration["OpenAI:DeploymentName"];
options.DefaultTemperature = 0.3;
});
注意 DefaultTemperature 我设置成了 0.3,工单解析场景下我们希望输出尽量稳定、可预测,温度不能太高。如果是创意文案类的 Agent,可以适当调高,但 Agent 场景一般建议低温。
3.3 创建第一个最简单的 Agent
在你的 Agents 文件夹下建一个 TicketAgent 类:
csharp复制public class TicketAgent : AgentBase
{
public TicketAgent(AgentContext context) : base(context)
{
Name = "工单助理";
Description = "负责解析工单、查询关联数据并生成处理建议";
SystemPrompt = @"你是一个工单处理助理。请根据用户提供的工单内容,
通过调用相关技能来完成工单解析、数据查询和报告生成。";
}
}
然后覆盖一个关键方法,用于注册这个 Agent 拥有的 Skills:
csharp复制protected override void ConfigureAgent()
{
UseSkill(new TicketParsingSkill());
UseSkill(new DataQuerySkill());
UseSkill(new ReportGenerationSkill());
}
这样写的好处是,Agent 的能力列表一目了然。团队新成员接手代码时,第一眼就知道这个 Agent 能干什么。
3.4 注册 Agent 到应用并暴露接口
Program.cs 里继续注册容器:
csharp复制builder.Services.AddScoped<TicketAgent>();
Agent Framework 支持从 DI 容器解析 Agent,也可以把 Agent 注册为服务,这样在 Controller 里就能注入了。下面是一个最简化的调用接口:
csharp复制[ApiController]
[Route("api/agent")]
public class AgentController : ControllerBase
{
private readonly TicketAgent _agent;
public AgentController(TicketAgent agent)
{
_agent = agent;
}
[HttpPost("run")]
public async Task<IActionResult> Run([FromBody] string userInput)
{
var response = await _agent.RunAsync(userInput, CancellationToken.None);
return Ok(response);
}
}
RunAsync 是 AgentBase 提供的方法,传入用户输入,返回 Agent 的最终回复。这里我不建议自己写大模型调用循环,因为 Agent Framework 内部已经处理了"Agent 决定调用哪个 Skill -> 执行 Skill -> 把结果反馈给模型 -> 继续决策"的整个循环。你只需要关心输入输出。
跑通这一步,你其实已经有一个最简 Agent 了,但它还不会调用任何技能,只能靠模型本身的知识回答。接下来进入重头戏:写 Agent Skills。
4. Agent Skills 的编写步骤:从工单解析到数据查询
这一节我详细讲怎么把真实的业务能力封装成 Skill。Skill 的本质是一个实现了特定接口的类。在 Agent Framework 中,Skill 的接口有三个关键方法:DescribeAsync、ExecuteAsync 和 GetParametersAsync。
DescribeAsync:返回 Skill 的用途描述,Agent 的决策模型会读这些描述来决定什么时候调用。GetParametersAsync:返回 Skill 需要的参数定义,类似 function calling 的 schema。ExecuteAsync:真正干活的方法,接收参数,返回结果。
这个设计思路和 OpenAI function calling 里的 tools 一模一样,但它有一个额外优势:类 + 描述的方式更结构化,也更容易做单元测试和静态检查。
4.1 Skill 的基础结构:以工单解析为例
工单解析 Skill 要做的事:从用户给的工单文本里提取"工单编号、报障类别、影响范围、优先级"。我在真实项目里没有用正则硬解析,因为用户输入千奇百怪。而是让大模型做结构化提取,然后我用强类型模型接住。
Skill 类长这样:
csharp复制public class TicketParsingSkill : ISkill
{
public string Name => "工单解析";
public async Task<SkillDescription> DescribeAsync()
{
return new SkillDescription
{
Description = "从工单文本中提取结构化信息,包括工单编号、报障类别、影响范围、优先级。当用户提供工单内容时使用。"
};
}
public async Task<IReadOnlyList<SkillParameter>> GetParametersAsync()
{
return new List<SkillParameter>
{
new SkillParameter("ticketText", typeof(string), "原始工单文本")
};
}
public async Task<object> ExecuteAsync(IDictionary<string, object> parameters)
{
var ticketText = parameters["ticketText"].ToString();
// 这里调用大模型做提取,也可以调用一个结构化提取函数
var parsed = await ExtractTicketInfoAsync(ticketText);
return parsed;
}
}
你可能会问,Skill 里能不能自己再调大模型?答案是可以。Skill 本质是代码,怎么实现都有自由。但我建议:解析、分类这类需要"语言理解"的 Skill 内部调模型没问题,查询数据库、调 API 这类确定性操作最好直接编码,不要再经过大模型,否则会出现"明明数据库里有这个字段,模型却给我编了个不存在的数据"这种幻觉问题。
4.2 数据查询 Skill:把业务查询封装进去
工单解析完,Agent 通常会接着查后台数据。这个 Skill 的执行逻辑不依赖大模型,纯 C# 实现:
csharp复制public class DataQuerySkill : ISkill
{
private readonly TicketDataService _dataService;
public DataQuerySkill(TicketDataService dataService)
{
_dataService = dataService;
}
public string Name => "工单数据查询";
public async Task<SkillDescription> DescribeAsync()
{
return new SkillDescription
{
Description = "根据工单编号查询关联的资产信息、历史工单、常用解决方案。当需要获取工单详情或关联数据时使用。"
};
}
public async Task<IReadOnlyList<SkillParameter>> GetParametersAsync()
{
return new List<SkillParameter>
{
new SkillParameter("ticketId", typeof(string), "工单编号")
};
}
public async Task<object> ExecuteAsync(IDictionary<string, object> parameters)
{
var ticketId = parameters["ticketId"].ToString();
var result = await _dataService.GetTicketContextAsync(ticketId);
return result;
}
}
这里有个小技巧:Skill 的构造函数也支持依赖注入。Agent Framework 在实例化 Skill 时会从容器里解析依赖,所以我可以直接把 TicketDataService 注入进来。这比手动 new 一个 service 要干净得多,也方便 mock 测试。
4.3 注册 Skill 到 Agent 和依赖注入
回到 TicketAgent,我已经写了 UseSkill(new TicketParsingSkill())。但注意 DataQuerySkill 是依赖 TicketDataService 的,如果我在 Agent 里直接 new,就需要自己构造 service。为了统一管理依赖,我建议把 Skill 也注册到 DI 容器,然后在 Agent 构造时解析。
用属性注入或者构造函数注入都行,下面是我在真实项目里用的方式:
csharp复制public class TicketAgent : AgentBase
{
private readonly IEnumerable<ISkill> _skills;
public TicketAgent(AgentContext context, IEnumerable<ISkill> skills) : base(context)
{
_skills = skills;
Name = "工单助理";
Description = "负责解析工单、查询关联数据并生成处理建议";
SystemPrompt = "你是一个工单处理助理...";
}
protected override void ConfigureAgent()
{
foreach (var skill in _skills)
{
if (skill.Name == "工单解析" || skill.Name == "工单数据查询" || skill.Name == "报告生成")
{
UseSkill(skill);
}
}
}
}
然后 Program.cs 里注册:
csharp复制builder.Services.AddScoped<TicketDataService>();
builder.Services.AddScoped<ISkill, TicketParsingSkill>();
builder.Services.AddScoped<ISkill, DataQuerySkill>();
builder.Services.AddScoped<ISkill, ReportGenerationSkill>();
builder.Services.AddScoped<TicketAgent>();
这样整个依赖链就完整了。
4.4 数据模型和返回结果的设计心得
实际做下来,Skill 的返回结果用什么类型很关键。我建议用强类型的 DTO 而不是 object 或 Dictionary<string, object>。为什么?因为 Agent 框架会把 Skill 的返回结果格式化后喂给大模型,如果返回的是一个字典,模型很难理解字段含义。我一般这么设计:
csharp复制public class TicketContext
{
public string TicketId { get; set; }
public string Title { get; set; }
public string Category { get; set; }
public string Priority { get; set; }
public AssetInfo Asset { get; set; }
public List<string> RelatedTickets { get; set; }
public List<string> Solutions { get; set; }
}
在 ExecuteAsync 里返回这个对象。Agent Framework 内部会把它序列化成 JSON,然后模型根据 JSON 来生成回复。强类型的好处是:字段名有语义、类型明确、可测试。
这里也是我最初踩坑最多的地方。一开始我图省事,直接返回一个字符串模板,结果模型经常提取不到关键信息,后来改成强类型 + JSON 序列化,输出质量立刻稳定了很多。
5. 模型决策机制与 Skill 编排:Agent 如何决定下一步做什么
Skill 写好了,Agent 怎么知道该调用哪个?这一步很多人会误解,以为框架有某种"意图识别"的独立模块。实际上 Agent Framework 的做法是:把每个 Skill 的描述和参数 schema 嵌入到系统提示词里,然后交给大模型自主决定。
它内部的决策流程大概长这样:
- 把用户的输入加到对话上下文。
- 把所有 Skill 的
DescribeAsync()和GetParametersAsync()的输出拼成一段"可用工具清单"。 - 让大模型基于当前对话和工具清单,决定是直接回答,还是调用某个 Skill。
- 如果大模型决定调用,框架会解析出 Skill 名称和参数,然后执行对应
ExecuteAsync。 - 执行结果返回给模型,模型根据结果生成下一轮输出或继续调用其他 Skill。
这就是为什么 Skill 的描述写得质量高不高,直接决定 Agent 的智能程度。我在写描述时有几条实操原则:
- 描述不要泛泛而谈,要写明"什么情况下使用"。比如"当用户提供工单编号且需要查看关联资产时使用",比"查询数据"有用得多。
- 参数名和说明要清晰。模型要靠这些生成正确的参数值。
- 如果 Skill 有依赖条件,要在描述里写清楚。比如"仅当上一步工单解析完成后才能调用"。
5.1 多轮调用和上下文管理
Agent 的一个常见场景是:用户上一句说"帮我处理工单 TICKET-001",下一句说"它关联了什么资产?"。Agent 要能记住之前的上下文,不然第二句就不知道"它"指什么。
Agent Framework 内置了对话记忆机制。你可以配置 ConversationHistoryOptions:
csharp复制builder.Services.AddAgentFramework(options =>
{
options.DefaultModelName = "...";
options.DefaultTemperature = 0.3;
options.ConversationHistory = new ConversationHistoryOptions
{
MaxTurns = 10,
StorageProvider = StorageProviders.InMemory
};
});
MaxTurns 表示记住最近多少轮对话。工单处理场景不需要太长,10 轮足够。但是注意,Agent 的记忆机制只是把历史消息塞回上下文,模型本身是没状态的。如果你有更复杂的记忆需求(比如跨会话记忆用户偏好),需要自己实现持久化,或者接外部向量库,这属于 Agent 记忆专题,后面有机会再写。
5.2 一个让人意外的坑:Skill 描述里的措辞影响决策
我在调试时发现一个有趣现象:把 Skill 描述里的"当用户提供工单文本时使用"改成"当用户输入包含工单编号或故障描述时使用",模型选择该 Skill 的准确率提升了不少。原因是模型的意图匹配依赖描述的关键词重叠,描述越贴近用户可能的表达,激活率越高。
这听起来很玄学,但实际就是这么回事。大模型的函数路由本质上就是"描述匹配 + 语义相似度"的结合。你写的不是给人看的文档,而是给模型看的导航。
我建议每个 Skill 的描述写完后,用 5~10 条不同的用户输入去测试,观察模型是否在合适的场景选择了正确的 Skill。如果不准,就调整描述里的关键词和触发条件说明。这步调优比调代码重要得多。
6. 完整运行示例:从用户输入到 Skill 调用链
理论说完,直接跑一次真实的调用链。假设用户输入是:
用户:工单 TICKET-001 报错了,页面打不开,影响线上销售系统,看看怎么回事。
Agent 收到后,内部执行链路如下:
6.1 第一步:调用工单解析 Skill
模型看到输入里有"工单 TICKET-001",而且描述匹配,于是决定调用 TicketParsingSkill。
传入参数:
json复制{
"ticketText": "工单 TICKET-001 报错了,页面打不开,影响线上销售系统,看看怎么回事。"
}
TicketParsingSkill 内部调用大模型(或结构化提取函数),返回:
json复制{
"ticketId": "TICKET-001",
"title": "销售系统页面无法打开",
"category": "页面故障",
"priority": "高"
}
6.2 第二步:模型看到解析结果,决定查询数据
Agent 拿到解析结果后,发现需要获取关联资产和历史工单,于是调用 DataQuerySkill。
参数从解析结果里提取:
json复制{
"ticketId": "TICKET-001"
}
数据服务查询返回:
json复制{
"ticketId": "TICKET-001",
"asset": {
"assetName": "销售服务-生产节点1",
"status": "异常",
"lastHeartbeat": "2025-01-12 14:23:00"
},
"relatedTickets": ["TICKET-000", "TICKET-012"],
"solutions": ["尝试重启 IIS 应用池", "检查数据库连接字符串配置"]
}
6.3 第三步:调用报告生成 Skill 或直接生成回复
到了这一步,模型可以直接生成最终回复,也可以调用 ReportGenerationSkill 生成一份格式化的处理建议。我建议把"报告生成"也做成 Skill,因为这样输出的格式是可控的,比如必须包含"基本信息、影响范围、建议操作、优先级"四段。
最终返回给用户的回复大概长这样:
工单 TICKET-001 已识别,标题为"销售系统页面无法打开",优先级评估为高。关联资产"销售服务-生产节点1"当前状态异常,最后一次心跳时间为 14:23:00。参考历史工单发现类似问题两次,常见解决方案包括:1. 重启 IIS 应用池;2. 检查数据库连接字符串配置。建议优先处理此工单并通知运维团队介入。
这个输出不是一次性大模型拍脑袋给的,而是经过了解析 -> 查询 -> 报告生成三个 Skill 的协作得出的,每个环节都有数据支撑。这正是 Agent 和单纯 function calling 的最大区别:它是目标导向的闭环,不是一问一答的点状交互。
我在真实项目里还加了一个环节:查询数据为空时,Agent 会主动问用户"工单编号是否正确?要不要试一下其他编号?"。这个行为不是我在代码里硬写的,而是模型基于 SystemPrompt 里的行为约束自主触发的。把 Agent 的"主动性"通过提示词设计出来,比写死 if else 要灵活得多。
7. 实测效果与性能分析:稳定性、延迟和成本
代码写完,最终要过性能和数据这两关。我把这套 Agent 接入内部测试环境跑了半个月,测了 500 多条真实工单数据,这里分享几个关键指标:
7.1 工单解析准确率
500 条工单里,工单编号提取准确率 98% 以上,但类别和优先级的准确性稍微低一些,大概在 85%~90% 左右。大部分错误出现在"类别划分模糊"的工单里,比如"系统慢"到底算性能问题还是网络问题,模型偶尔会分错。这个我后来通过在 SystemPrompt 里增加详细的分类定义,把准确率提升到了 93% 左右。
| 指标 | 数值 |
|---|---|
| 工单编号提取准确率 | 98.2% |
| 类别识别准确率 | 93.4% |
| 优先级判断准确率 | 90.1% |
| 平均单次调用延迟 | 2.8s |
| 超时率(15s) | 0.4% |
7.2 延迟分析
单次完整 Agent 调用,如果涉及 2~3 个 Skill,模型推理次数是 2~4 次。因为每次 Skill 调用后,模型都要再生成一次决策或者回复文本。每轮大模型调用的延迟通常在 0.8s~1.2s 左右(gpt-4o-mini),所以总延迟 2.5s~4s 是常态。
如果你的场景对延迟敏感,有几个优化思路:
- 用更小的模型做路由和解析,比如 gpt-4o-mini 代替 gpt-4o。
- 减少不必要的 Skill 调用链,比如某些简单工单只需要解析 + 回复,不需要查数据库。
- 开启流式输出,用户可以边看边等,感知延迟会好很多。
7.3 成本估算
成本主要由大模型调用次数和 token 量决定。一个包含三个 Skill 调用链的工单处理,平均消耗大概 3000~5000 token。用 gpt-4o-mini 的话,单次成本大约在 0.01~0.02 美元。如果一个月处理一万条工单,成本在 100~200 美元这个量级。对于内部效率工具来说,这个成本完全可控。
7.4 稳定性问题:超时和模型返回格式非法
实测里最常遇到的稳定性问题是:模型偶尔返回了不存在的 Skill 名,或者参数类型不对。Agent Framework 对这种问题有容错机制,但不会完全自动修复。它会把错误反馈给模型,让模型重新尝试,最多重试几次。我在测试中就遇到过一次模型连续调用一个不存在的 Skill 导致循环的情况,后来通过在 SystemPrompt 里强调"只允许调用提供的技能,不要臆造技能名称",问题就基本消失了。
另外,如果你在生产环境用,我强烈建议开启框架的日志记录和分析追踪。Agent Framework 提供了可观测性接口,可以把每次调用的决策记录、Skill 执行时间、token 消耗都输出到日志。出问题时,光靠用户反馈"AI 答得不对"很难定位,有追踪日志就容易定位是哪一步出了岔子。这算是生产环境落地绕不开的一环。
8. 踩坑与排查:必须分享的三个真实问题
最后这部分,我挑三个我在开发中遇到最典型的问题,把排查过程完整写出来,你如果遇到类似情况可以直接对照。
8.1 问题一:Skill 注册了,但 Agent 就是不调用
我刚开始接入时,满怀信心注册了三个 Skill,结果 Agent 收到工单文本后直接凭记忆回答,完全没走 Skill。排查过程:
- 先确认
UseSkill是否真的调用到了。我在ConfigureAgent里加断点,发现注册代码确实执行了。 - 再查框架有没有把 Skill 描述注入到系统提示词。翻日志发现,注入的描述只有 Name 没有 Description。原来是我的
DescribeAsync返回的 Description 字段为空,因为我把描述写到了另一个属性里,框架只认 Description 字段。 - 修正后重新测试,Agent 就开始调用 Skill 了。
排查结论:Skill 描述字段没对上,模型看不见技能说明,自然不知道怎么用。建议每次写完 Skill 后先把 DescribeAsync 的输出打印出来看一眼,确认字段完整。
8.2 问题二:Skill 执行成功,但 Agent 生成了幻觉结果
有一个阶段,DataQuerySkill 返回了正确的数据,但 Agent 最终回复里出现了一个数据里不存在的"解决方案"。我检查后发现问题出在报告生成这一步——模型在生成总结时,基于自己的知识补齐了一个合理但不存在的内容。
解决思路:
- 在 SystemPrompt 里加入强约束:"所有回复内容必须严格基于技能返回的数据,禁止补充未经确认的事实。"
- 在对话上下文中把 Skill 返回的数据明确标记为"唯一事实来源"。
- 对高确定性场景,把 Agent 的温度进一步降低到 0.2 甚至 0.1。
这样做之后,幻觉明显减少。说到底,大模型天然有"编造合理内容"的倾向,不靠提示词和流程约束,很容易出现这种问题。Agent 里不是所有环节都适合让模型自由发挥,该有的边界得有。
8.3 问题三:多并发请求导致 Agent 状态串了
刚开始我把 Agent 注册成了单例,结果 A 用户的对话状态串到了 B 用户,出现了"A 问工单 001,Agent 却在回答 B 的工单"的诡异现象。
根因是 Agent 实例里保存了对话上下文,如果多个请求共享同一个实例,上下文就会互相污染。
排查过程:
- 看日志,发现 Agent 实例的 HashCode 在所有请求里都一样,确认是单例。
- 查生命周期,发现我把 Agent 注册为
AddScoped,但 Controller 是单例的,导致 Controller 里注入的 Agent 实际上还是同一个实例。 - 修改方案:确保 Agent 生命周期是 Scoped,且 Controller 也使用 Scoped 生命周期。控制器瞬时请求时,Scoped 服务会在每个请求作用域内创建新实例。
修复后问题解决。这里必须提醒:Agent 实例管理的核心是生命周期,只要它有状态,就不能用单例。如果你需要跨请求保持对话,那应该把状态存到持久化存储,而不是让 Agent 实例常驻内存。
这三个问题,前两个偏模型层面,第三个偏工程层面,但都算 Agent 开发里非常典型的坑。写出来供你参考,能少走弯路就少走点。
9. 后续还可以怎么扩展:从单 Agent 到多 Agent 协作
目前这套工单 Agent 属于单 Agent 工作流:一个 Agent 拥有多个 Skill,自主完成一套流程。但 Agent Framework 还支持多 Agent 协作,也就是让多个专业 Agent 组成一个团队,通过调度者统一协调。这是我下一步打算做的方向。
比如把工单 Agent 拆成三个独立 Agent:
- 工单理解 Agent:负责解析工单,提取关键信息。
- 技术诊断 Agent:负责调用数据查询 Skill 和相关系统 API,进行技术分析。
- 通知协调 Agent:负责生成报告、发送通知、升级工单。
每个 Agent 只做一件事,然后由一个主调度 Agent 根据用户意图分派任务。这种架构的好处是:职责单一、易于扩展、每个模型可以配不同参数。坏处是:调用链更长、延迟更大、调试更复杂。
如果你也想往这个方向走,我建议先从"一个 Agent 多个 Skill"开始,等真实场景跑通了,再逐步拆分成多 Agent。别一上来就搞大团队,不然排查问题的难度会指数级上升。
另外一个扩展方向是把 Skill 做成动态可插拔的插件系统。比如根据不同的业务线,在数据库里配置启用哪些 Skill,这样不同租户的 Agent 行为可以不一致,也不需要改代码重新发布。这个在 Agent Framework 里实现起来并不难,因为 Skill 本来就是一个可枚举的集合,你只需要调整 ConfigureAgent 里的注册逻辑。
最后说点实在的。我在 .NET 生态里做 Agent 开发,最大的感受是:Agent 的复杂度不在"调用模型",而在如何设计能力边界和编排逻辑。Microsoft Agent Framework 的价值不是让你少写几行调用代码,而是逼着你用结构化方式思考——Agent 是什么、Skill 是什么、数据从哪来、结果怎么保证可信。想明白这些,无论你以后换什么框架,都能做出真正可用的 Agent。
如果你也在 .NET + AI 的路线上摸索,建议直接照着这篇文章搭一个最小的 Agent + Skill 跑起来,遇到问题再回头看这些经验。动手永远比看文档有效。
