☰
C# + Agent Framework 实战:构建企业级AI代理的完整指南
2026/10/19 14:29:36 网站建设 项目流程

先用 C# 写AI代理这事,过去半年我在两个内部项目里试了又试,最终把技术栈固定在 Agent Framework 上。如果你也在纠结“怎么在 .NET 业务系统里落地 AI 代理”,这篇文章应该能省下你不少调研时间。它不是把官方文档翻译一遍,而是从项目选型、环境搭建、核心功能实现,到上线前会遇到的各种坑,按我实际做项目的顺序讲一遍。

先说结论:用 C# 和 Agent Framework 构建 AI 代理,最大的价值不是“能聊天”,而是能把现有业务逻辑封装成工具,让模型驱动这些工具去完成真实任务。适合做智能客服、工单处理、报表查询、运维辅助这类场景。下面是完整过程。

1. 整体思路:C# 世界里的AI代理到底怎么做

1.1 先搞清楚代理和聊天机器人的区别

很多团队一开始想做的其实是聊天机器人,不是代理。聊天机器人只负责“说”,代理要负责“做”。比如用户说:“帮我查一下昨天的销售数据,如果低于目标,就给相关负责人发一封提醒邮件。”聊天机器人只能给出一段“建议你这样做”的回复;代理会把这个任务拆成几步:查销售数据、和目标比较、判断是否需要发邮件、确认收件人、调用邮件工具发送,最后把结果告诉用户。

这个差别很关键,因为“拆任务”和“调用工具”才是 Agent 的核心,而不是生成文本。Agent Framework 做的事情,就是把这些环节串起来:模型负责推理,工具负责执行,框架负责调度、状态和人工介入。你可以把模型理解成一个实习生,脑子灵活但不懂业务细节;工具是公司里的各个系统入口;框架是带这个实习生的资深组长——它决定什么时候问谁、做什么、要不要停下来确认。

生活里更好理解一点:聊天机器人像前台接待,只能回答“流程是什么”;AI代理像有权限的行政助理,能拿着工单去各个部门办事,把结果办完再回来交差。前台可以外包给脚本,助理必须被纳入公司管理体系。这也是为什么代理项目通常比聊天机器人复杂,但价值也高一个量级。

1.2 为什么选 Agent Framework,而不是自己拼

老实说,直接调用模型接口、自己写一个while循环也能实现“代理”:

  • 把用户消息拼进消息列表
  • 调用模型,看看返回里有没有工具调用请求
  • 有就执行对应工具,把结果放回消息列表
  • 再调一次模型,直到模型给出最终回复

这个思路在工具只有一两个的时候完全没问题。但一旦涉及多用户、多轮对话、十几个工具、人工审批、状态持久化、异常重试,你的代码会很快失控。手动管理消息列表、判断工具调用边界、处理并发,每一项都很琐碎,而且最容易出错的地方恰恰是这些“脚手架”,不是模型本身。

Agent Framework 把这些基础设施做成了框架的一部分。它给你提供代理生命周期、会话线程、工具注册与发现、人工审批流程、可观测性钩子,并且和整个 .NET 生态天然搭配。也就是说,你不用再为了一个代理去自己造一套消息队列和状态机。对比起来:

方案优点缺点
自己写 while 循环灵活、直观状态管理难、重复代码多、容易漏边界
直接调模型 SDK简单,适合对话无法处理多步工具调用
Agent Framework内置运行时、工具、审批、状态版本迭代快,需要适应 API 变化

如果你只在做一个内部知识库问答,直接调模型就够了。但如果目标是“让代理操作业务系统”,Agent Framework 至少能帮你省掉一半的重复劳动。

1.3 这个方案适合什么样的项目

基于我实际测试的经验,不是所有项目都适合硬上 Agent Framework。适合的情况有几个特征:目标明确,比如“创建工单”“查询库存”“生成报表”;工具边界可控,每个工具只做一件事;允许在关键节点插入人工确认。反过来,如果任务是开放式创意写作,或者需要十几步全自主无人监督的链条,目前不要贸然上。

C# 背景的团队尤其适合。因为大部分 .NET 项目已经有现成的服务层、仓储层、依赖注入和日志体系。代理里的工具可以直接调用这些现有组件,而不是像 Python 方案那样需要另起一套。类型安全的优势在工具定义上非常明显——一个方法的参数类型、校验规则、返回值结构,模型和编译器都能读明白。

2. 项目初始化:环境、依赖和最小骨架

2.1 开发环境与包依赖

我用的环境是 .NET 8,VS Code 加终端,跑在 Windows 上。Agent Framework 目前还是预览阶段,NuGet 上的包名我这边看到的是Microsoft.Agent.Framework,版本号迭代很快。建议你创建项目时直接查 NuGet 最新版,然后锁死版本,不要在项目中途跟着升级,预览版的小版本之间 API 完全可能变,我踩过这个坑。

除了核心框架包,还要用到:

  • Microsoft.Extensions.Hosting:统一管理配置、依赖注入、日志和生命周期
  • Microsoft.Extensions.Configuration.Binder:把appsettings.json强类型绑定到配置类
  • 你用的模型服务对应的 SDK,或者框架自带的兼容适配器

创建项目很简单,控制台应用就可以跑通最小闭环:

dotnet new console -n AgentDemo cd AgentDemo dotnet add package Microsoft.Agent.Framework --prerelease dotnet add package Microsoft.Extensions.Hosting

如果你的 Agent 要托管在 Web API 或后台服务里,用dotnet new web或者worker模板也一样,核心代码差别不大。

2.2 用十分钟搭一个能对话的代理

我习惯把框架初始化放在Program.cs里,用 HostBuilder 统一管理。下面这段代码是简化后的骨架,不同版本 API 会有差异,但整体流程就是这样:

using Microsoft.Extensions.Hosting; using Microsoft.Agent.Framework; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddAgentFramework(options => { var aiConfig = builder.Configuration.GetSection("AI"); options.AddModel("chat", new ChatModelOptions { Endpoint = aiConfig["Endpoint"], Model = aiConfig["Model"] }); options.AddAgent("assistant", agent => { agent.WithInstructions("你是一个乐于助人的AI助理。"); agent.WithModel("chat"); }); }); var host = builder.Build(); await host.StartAsync(); var agentHost = host.Services.GetRequiredService<AgentHost>(); var assistant = await agentHost.GetAgentAsync("assistant"); var thread = agentHost.CreateThread(); Console.Write("你:"); var input = Console.ReadLine(); await foreach (var delta in assistant.ChatAsync(thread, input)) { Console.Write(delta.Text); }

这段代码做的事情很简单:配置模型、注册一个代理、创建一个会话线程、然后把用户输入发给代理并流式打印回复。别看简单,它已经把代理、线程、模型三层关系分开了。后续加工具、加审批、加多代理,都是往这个结构里填内容。

我第一次跑通的时候很兴奋,但马上发现一个问题:如果 agent 内部没有配置 API Key,框架启动时不报错,第一次对话才报认证失败。所以建议在初始化完成之后加一个简单的自检,主动发一条“ping”消息,确认链路通。

2.3 配置管理:别把密钥写死在代码里

任何涉及 AI 的项目,第一件事就是把密钥和设备隔离。Agent Framework 的配置读取完全依赖标准 .NET 配置体系,所以你可以用环境变量、用户机密、或者配置中心。我的建议是:端点、模型名、温度这些放appsettings.json,密钥走环境变量或dotnet user-secrets。

appsettings.Development.json示例:

{ "AI": { "Endpoint": "https://your-llm-endpoint.example", "Model": "your-model-name" } }

然后在配置类里绑定:

public sealed class AiOptions { public string Endpoint { get; set; } = string.Empty; public string Model { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; }

注意ApiKey那个属性名,就算配置文件里有明文,也只在环境变量里提供。我没有在配置文件里放过任何一次真实密钥,因为日志和 Git 历史会记住每一个教训。

3. 核心实现:让代理具备工具、审批和多轮记忆

3.1 设计系统指令是第一步

代理的行为,很大程度取决于你怎么写系统指令。指令写得好,工具选得准;指令写得又碎又长,模型就开始自己发明任务了。我给内部工单系统写的指令大概是这样的风格:

你是一个工单处理代理。 目标:根据用户描述创建工单,并分配到正确部门。 流程: 1. 提取用户诉求中的关键信息:标题、描述、影响范围。 2. 信息不足时,用工具查询用户所属组织,但不要猜测。 3. 创建工单前,必须让用户确认摘要。 4. 不要自行发送任何外部通知。 输出:最初回复时列一个简短的处理计划。

对比我之前写的 1000 字长指令,这样的精简版反而准确率更高。原因是模型对冗长指令的注意力会分散,越靠后的规则越容易被忽略。把最重要的约束放在前面,把“不要做”的事情放在后面,并且在指令里写“不要自行发送通知”,比只写“你可以发送通知”明确得多。

另一个技巧是:每条规则只能做一件事。比如“不要自行发送通知,也要确认收件人”这就是两条规则,拆开写。你可以在调试阶段把代理的完整指令打开,看它有没有理解错重点。

3.2 把业务能力注册成工具

代理真正干活,靠的是工具方法。Agent Framework 里注册一个工具非常简单,比如一个查库存的工具:

public class InventoryTool { private readonly IStockService _stockService; public InventoryTool(IStockService stockService) { _stockService = stockService; } [AgentTool("根据SKU查询当前可用库存数量")] public async Task<int> GetAvailableStockAsync( string sku, CancellationToken cancellationToken) { return await _stockService.GetAvailableAsync(sku, cancellationToken); } }

然后在注册代理时加上这一行:

agent.WithTool<InventoryTool>();

工具方法的描述字段是写给模型看的,不是写给开发者看的。描述里应该包含“什么时候用、参数是什么含义、有没有特殊前提”。比如说,“当用户提到库存、剩余量、可售数量时使用”,就比简单写“查询库存”好得多。

还有一个非常容易被忽略的点:工具返回值会被放进对话历史,交给模型继续推理。所以返回值格式越简单越好。如果需要返回复杂对象,最好在工具内部先转成一段简短文本,而不是把整个实体序列化后塞回上下文。模型读得越省力,下一步判断就越准。

3.3 高权限操作必须加人工审批

代理调用工具不是问题,问题是谁来为结果负责。凡是涉及发邮件、转账、删除数据、修改生产配置这种操作,我都建议在工具调用链路里加人工审批,而不是事后看日志。框架里可以这样做:

public class EmailTool { [AgentTool("发送邮件,必须获得用户确认后才能调用")] public async Task SendEmailAsync( EmailDraft draft, AgentContext context, CancellationToken cancellationToken) { var approved = await context.RequestApprovalAsync( $"确认发送邮件?收件人:{draft.To},主题:{draft.Subject}"); if (!approved) { return; // 用户拒绝 } await _emailService.SendAsync(draft, cancellationToken); } }

这个设计的关键是:审批动作发生在工具真正执行之前。如果模型自己在邮件工具里“擅自”调用,审批层会把它拦下来。你还可以根据业务规则叠加审批人:金额超过某个阈值要管理员确认,普通订单用户自己确认就行。

我在实践里还发现一个细节:审批请求的文本描述要具体,让用户一眼能看懂。否则用户看到一个模糊的“请求执行操作”,很容易顺手点拒绝,然后代理便卡住了。

3.4 多轮上下文与长度控制

代理不是无状态的。每次对话都带上历史消息,模型才能记得之前聊了什么。但历史消息也是 token 消耗的大头,尤其是工具返回结果往往又长又杂。Agent Framework 一般提供了线程(Thread)机制保存消息,但默认不一定会帮你无限保留历史。

我的做法是在每次交互后做一次上下文整理。保留最近 10 条消息,更早的内容用一段摘要代替。具体实现可以用一个中间件:

public class ContextCompactionMiddleware : IAgentMiddleware { public async Task InvokeAsync(AgentContext context, Func<Task> next) { await next(); var thread = context.Thread; thread.TrimToLastMessages(10); // 可选:如果剩余消息仍太长,做一次摘要 if (context.EstimatedTokenUsage > 12000) { var summary = await context.SummarizeAsync(thread); thread.ReplaceOldMessagesWithSummary(summary); } } }

这里EstimatedTokenUsage不是精确值,大概估算即可,目的是在上下文快爆掉之前提前处理。不要等到报错才救,模型一旦丢失关键工具结果,后续行为会变得很“蠢”。

4. 进阶编排:多代理协同与状态恢复

4.1 从单代理到多代理工作流

工具数量少的时候,一个代理能搞定一切。但当业务复杂起来,一个代理既要理解用户意图、又要调用内部服务、还要做风险判断,指令会变得臃肿,工具列表也会很长。这时候我会把职责拆到多个代理里。

以工单系统为例:前台客服代理负责收集信息和判断意图,它不直接改数据;技术执行代理负责调用内部 API 创建工单、分配负责人;审核代理对高风险动作做复核。每个代理有独立的指令和工具集合,职责边界非常清楚。这样做的好处是,模型不需要在一个上下文里同时记住“怎么说话”和“怎么操作”,准确率会提升。

4.2 用管道把多个代理串起来

编排多个代理,我用的方式不是写一堆if/else,而是用一个简单的工作流描述。先定义节点,再定义流转条件:

var flow = new AgentFlowBuilder() .Then("reception", ctx => ctx.Intent == "create_order") .Then("executor") .Then("approver");

reception负责判断用户意图,如果意图是创建订单,就流转到executor执行,最后到approver审批。这比在业务代码里硬编码逻辑更清晰,也更容易调整。

不过,多代理不是越多越好。每个代理都是一次模型调用,意味着延迟和 token 成本翻倍。如果单代理的工具选择准确率已经超过 90%,先别急着拆。只有当你定位到“一个代理内部职责冲突”的时候,拆分才有效。

4.3 会话状态持久化与可观测性

用户聊到一半应用重启,会话不能丢。框架的线程对象默认在内存里,要持久化需要自己处理。我这里直接用一个数据库表保存消息记录,每次对话后把 Thread 的消息追加进去,启动时再恢复。

持久化的字段大概是:会话ID、消息角色、消息内容、工具调用记录、时间戳。消息内容注意要对敏感信息脱敏。另一个重点是可观测性。我已经把本地日志里每一次工具调用的入参和出参都原样打出来,配合时间戳和 token 用量,排查“代理为什么自作主张”的时候非常有用。

5. 实践中遇到的问题和排查方法

5.1 工具调用不稳定,先检查这几个地方

“代理有时候调用工具,有时候不调用”,这是群里问得最多的问题。根据我的排查经验,基本都出在几个地方:

症状常见原因处理办法
工具完全不触发工具描述太泛在描述里写清楚“当用户提到XX时使用”
工具参数传错参数类型太复杂尽量用 string/int,避免深层嵌套对象
总是调用同一个工具工具数量太多或描述有倾向精简到 10 个以内,重新排序
调用后回复仍不对工具返回结果被截断降低返回内容长度,转成短文本

最坑的是工具描述过泛。比如一个GetOrderInfo工具,描述写“查询订单信息”,模型碰到任何和订单有关的话都可能去调,结果参数又不对。改完描述为“当用户提供订单号并询问订单状态、物流进度时使用”,准确率立刻上来。

5.2 上下文窗口不够用怎么办

模型上下文长度是很硬的约束。我遇到过三类情况:一是系统指令太长,占了几千 token;二是工具返回结果太大,把窗口塞满;三是多轮对话累积了大量历史。

我的处理顺序是:先压缩系统指令,删掉所有“正确的废话”;再限制工具返回内容,比如最多返回前 50 条记录,并用一句话说明总数;最后才是做历史摘要。这三步做完,上下文问题基本可控。如果还是不够,就该考虑是不是单个代理职责范围太大,需要拆代理。

5.3 并发复用导致的诡异问题

Agent Framework 里,AgentHost 一般是单例,但线程和会话要按用户隔离,不能共享。我踩过的坑是把一个自写的工具类注册成了 Singleton,工具内部又有缓存字段,结果两个用户的数据串了。解决方式很简单:工具类按请求作用域注册,数据库上下文也走作用域,不要为省事都搞成单例。

另外,模型调用本身是异步的,多个用户同时使用同一个 Agent 实例通常没事,但如果你在工具里写了共享的可变状态,比如static List<T>,那就等着线上出事故。我的原则是:工具类尽量无状态,需要用到的依赖都从构造器注入。

5.4 密钥与安全边界

安全方面有两条底线:第一,API 密钥只存在环境变量或密钥管理服务里,日志里绝不能打完整密钥;第二,代理能调用的工具,权限必须最小化。模型生成的工具参数不能默认可信,工具内部还是要做白名单校验。比如 SKU 只允许特定字符集,邮件收件人必须符合邮箱格式。模型只是帮你填参数,校验逻辑不能省。

还有一个容易被忽视的点:用户输入也可以诱导代理改变行为。虽然模型层面有指令约束,但工具入参校验是最后一道闸门。我的建议是,凡是涉及写操作的工具,入参校验都当成用户输入来对待。

6. 最后:一些很实在的建议

6.1 从最小闭环开始

不要一上来就设计多代理、复杂审批、状态持久化。先做一个只有一两个工具的代理:用户提问,代理调用工具,把结果告诉用户。把这个闭环跑通,再逐步加写操作、加审批、加第二个代理。我见过太多团队第一周就在画架构图,结果连模型工具调用都没调通。

6.2 回归测试必须做

AI 代理的随机性很强,不是改一次代码就能保证所有场景稳定。我维护了一组典型问题,每次改系统指令或者工具描述后,跑一遍这些问题,记录工具调用路径和最终结果。时间久了,你会发现很多“模型变笨”的情况,其实是工具返回了脏数据,而不是模型问题。

6.3 上线前记得留人工兜底

再好的代理也会犯错。生产环境一定要留兜底:工具调用连续失败几次就转人工;审批超时默认拒绝;异常时给用户一个可读的错误提示,而不是模型生成的乱码。代理能做多少事,取决于你允许它犯多少错。

最后再分享一个小技巧:开发阶段把每次工具调用的入参和结果原样保存在本地日志里。你会惊讶地发现,大多数“模型突然变笨”的问题,其实是工具返回了脏数据。这个技巧救了我很多次。

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

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

立即咨询