在 .NET AI 聊天应用中升级到 Microsoft 代理框架

在 .NET AI 聊天应用中升级到 Microsoft 代理框架

引言

随着人工智能技术的快速发展,简单的聊天机器人已经不能满足日益复杂的业务需求。Microsoft 推出的 Agent Framework 为 .NET 开发者提供了构建智能代理的强大工具,能够实现多步骤工作流、自主决策和复杂任务编排。本文将详细介绍如何将基于 .NET AI 模板的标准聊天应用升级到 Microsoft 代理框架,利用其强大的功能和灵活性来构建更智能的应用。

正文内容

1. Microsoft 代理框架概述

Microsoft Agent Framework 是微软推出的预览框架,专为在 .NET 中构建 AI 代理而设计。它超越了简单聊天机器人的能力,提供了以下核心功能:

  • 通过多步骤工作流程进行推理和计划
  • 使用工具和函数与 API、数据库和服务交互
  • 在整个对话中维护上下文
  • 基于指令和数据做出自主决策
  • 在多代理场景中与其他代理协调

该框架建立在 .NET 开发者熟悉的模式之上,如依赖注入、中间件和遥测,并与 Microsoft.Extensions.AI 深度集成。

2. 先决条件

在开始升级前,需要准备以下环境:

  • 已安装 .NET 9 SDK
  • Visual Studio 或带有 C# Dev Kit 的 Visual Studio Code
  • 可访问 Azure OpenAI 的 Azure 账户,或使用 GitHub 模型
  • 已安装 .NET AI 应用模板
  • 基本熟悉 .NET、Blazor 和 AI 概念

3. 创建基础 AI 聊天应用

3.1 安装模板

首先使用官方 .NET AI 模板创建基线聊天应用:

dotnet new install Microsoft.Extensions.AI.Templates

3.2 创建项目

可以通过 Visual Studio 或 CLI 创建项目:

Visual Studio 方式

  1. 打开 Visual Studio 2022
  2. 选择"创建新项目"
  3. 搜索"AI Chat Web App"
  4. 配置项目名称和位置
  5. 选择 Azure OpenAI 作为 AI 提供程序
  6. 为矢量存储选择"本地磁盘"
  7. 选择 .NET Aspire 进行业务流程

Visual Studio project dialog with AI Chat Web App template

CLI 方式
使用 dotnet new 命令创建项目,配置选项与 Visual Studio 相同。

3.3 项目结构

模板生成的解决方案包含三个项目:

ChatApp20/
├── ChatApp20.Web/              # 带有聊天 UI 的 Blazor Server 应用
├── ChatApp20.AppHost/          # .NET Aspire 业务流程
└── ChatApp20.ServiceDefaults/  # 共享服务配置

主要工作将在 ChatApp20.Web 项目中进行。

Solution Explorer with project structure

4. 添加 Microsoft 代理框架

4.1 安装必要的 NuGet 包

首先将 Microsoft Agent Framework 包添加到 ChatApp20.Web.csproj:

<ItemGroup>
  <!-- 保留现有包 -->
  <PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
  <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.0-preview.1.25513.3" />
  
  <!-- 添加 Microsoft 代理框架包 -->
  <PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251009.1" />
  <PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0-preview.251009.1" />
  <PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.0.0-preview.251009.1" />
  <PackageReference Include="Microsoft.Agents.AI.Hosting.OpenAI" Version="1.0.0-alpha.251009.1" />
  <PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251009.1" />
</ItemGroup>

NuGet packages added to project

4.2 创建专用搜索功能服务

为了更好的关注点分离和可测试性,创建一个新的 SearchFunctions.cs 服务:

using System.ComponentModel;

namespace ChatApp20.Web.Services;

public class SearchFunctions
{
    private readonly SemanticSearch _semanticSearch;

    public SearchFunctions(SemanticSearch semanticSearch)
    {
        _semanticSearch = semanticSearch;
    }

    [Description("使用短语或关键字搜索信息")]
    public async Task<IEnumerable<string>> SearchAsync(
        [Description("要搜索的短语")] string searchPhrase,
        [Description("如果可能,指定文件名仅搜索该文件")] string? filenameFilter = null)
    {
        var results = await _semanticSearch.SearchAsync(searchPhrase, filenameFilter, maxResults: 5);
        return results.Select(result =>
            $"<result filename=\"{result.DocumentId}\" page_number=\"{result.PageNumber}\">{result.Text}</result>");
    }
}

4.3 在 Program.cs 中注册 AI 代理

使用 Agent Framework 的托管扩展配置 AI 代理:

// 注册使用代理框架的 AI 代理
builder.AddAIAgent("ChatAgent", (sp, key) =>
{
    var logger = sp.GetRequiredService<ILogger<Program>>();
    var searchFunctions = sp.GetRequiredService<SearchFunctions>();
    var chatClient = sp.GetRequiredService<IChatClient>();

    // 创建并配置 AI 代理
    var aiAgent = chatClient.CreateAIAgent(
        name: key,
        instructions: "你是一个有用的代理,提供简短而有趣的回答。",
        description: "帮助用户提供简短而有趣回答的 AI 代理。",
        tools: [AIFunctionFactory.Create(searchFunctions.SearchAsync)]
        )
    .AsBuilder()
    .UseOpenTelemetry(configure: c =>
        c.EnableSensitiveData = builder.Environment.IsDevelopment())
    .Build();

    return aiAgent;
});

// 注册 SearchFunctions 以便通过 DI 注入到代理中
builder.Services.AddSingleton<SearchFunctions>();

4.4 更新聊天组件

更新 Chat.razor 以使用新的 AI 代理:

@inject IServiceProvider ServiceProvider
@using Microsoft.Agents.AI

@code {
    private AIAgent aiAgent = default!;

    protected override void OnInitialized()
    {
        // 解析 Program.cs 中注册为"ChatAgent"的键控 AI 代理
        aiAgent = ServiceProvider.GetRequiredKeyedService<AIAgent>("ChatAgent");
    }

    private async Task AddUserMessageAsync(ChatMessage userMessage)
    {
        // 用代理流式处理替换 ChatClient.GetStreamingResponseAsync
        await foreach (var update in aiAgent.RunStreamingAsync(
            messages: messages.Skip(statefulMessageCount),
            cancellationToken: currentResponseCancellation.Token))
        {
            var responseUpdate = update.AsChatResponseUpdate();
            messages.AddMessages(responseUpdate, filter: c => c is not TextContent);
            responseText.Text += update.Text;
            chatOptions.ConversationId = responseUpdate.ConversationId;
            ChatMessageItem.NotifyChanged(currentResponseMessage);
        }
    }
}

5. 运行和测试增强应用

5.1 使用 .NET Aspire 运行

.NET Aspire 提供了服务发现、统一日志记录和遥测、健康检查等功能:

  1. 运行应用,Aspire 仪表板会自动在浏览器中打开
  2. 首次运行时,系统会提示配置 Azure OpenAI:
    • 选择 Azure 订阅
    • 选择或创建资源组
    • 选择或预配 Azure OpenAI 资源
    • 确保部署了聊天模型(如 gpt-4o-mini)和嵌入模型(如 text-embedding-3-small)

Aspire dashboard with running application

5.2 测试代理

应用运行后,在 Aspire 仪表板中点击 Web 端点(通常是 https://localhost:7001)进行测试:

基本对话

用户: 你好!你好吗?
代理: 嘿!我很好——充满电,就像应急救生包一样。

带有语义搜索的工具调用

用户: 应急救生包应该包括什么?
代理: 简短的救生包清单(有趣版) 急救用品——绷带、纱布、消毒剂。
      <citation filename='Example_Emergency_Survival_Kit.pdf' page_number='1'>水和食物供应</citation>

特定文件查询

用户: 告诉我关于 GPS 手表的功能
代理: GPS 手表包括...
      <citation filename='Example_GPS_Watch.pdf' page_number='2'>实时跟踪</citation>

Chat interface with documents

6. 高级场景

6.1 向代理添加更多工具

可以轻松扩展代理功能,例如添加天气查询:

public class WeatherFunctions
{
    [Description("获取某个位置的当前天气")]
    public async Task<string> GetWeatherAsync(
        [Description("城市和州/国家")] string location)
    {
        // 调用天气 API
        return $"{location}的天气: 晴天, 72°F";
    }
}

// 在 Program.cs 中
builder.Services.AddSingleton<WeatherFunctions>();

builder.AddAIAgent("ChatAgent", (sp, key) =>
{
    var searchFunctions = sp.GetRequiredService<SearchFunctions>();
    var weatherFunctions = sp.GetRequiredService<WeatherFunctions>();
    var chatClient = sp.GetRequiredService<IChatClient>();

    return chatClient.CreateAIAgent(
        name: key,
        instructions: "你可以搜索文档和查询天气...",
        tools: [
            AIFunctionFactory.Create(searchFunctions.SearchAsync),
            AIFunctionFactory.Create(weatherFunctions.GetWeatherAsync)
        ]
    ).Build();
});

6.2 多代理场景

代理框架可以轻松协调多个专用代理:

// 注册研究代理
builder.AddAIAgent("ResearchAgent", (sp, key) =>
{
    var chatClient = sp.GetRequiredService<IChatClient>();
    var searchFunctions = sp.GetRequiredService<SearchFunctions>();

    return chatClient.CreateAIAgent(
        name: "ResearchAgent",
        instructions: "你是研究专家。从文档中查找和总结信息。",
        tools: [AIFunctionFactory.Create(searchFunctions.SearchAsync)]
    ).Build();
});

// 注册写作代理
builder.AddAIAgent("WritingAgent", (sp, key) =>
{
    var chatClient = sp.GetRequiredService<IChatClient>();

    return chatClient.CreateAIAgent(
        name: "WritingAgent",
        instructions: "你是写作专家。获取信息并创建结构良好、引人入胜的内容。",
        tools: []
    ).Build();
});

// 注册协调代理
builder.AddAIAgent("CoordinatorAgent", (sp, key) =>
{
    var chatClient = sp.GetRequiredService<IChatClient>();
    var researchAgent = sp.GetRequiredKeyedService<AIAgent>("ResearchAgent");
    var writingAgent = sp.GetRequiredKeyedService<AIAgent>("WritingAgent");

    // 创建委托给其他代理的函数
    async Task<string> ResearchAsync(string topic)
    {
        var messages = new[] { new ChatMessage(ChatRole.User, topic) };
        var result = await researchAgent.RunAsync(messages);
        return result.Text ?? "";
    }

    async Task<string> WriteAsync(string content)
    {
        var messages = new[] { new ChatMessage(ChatRole.User, $"基于以下内容写文章: {content}") };
        var result = await writingAgent.RunAsync(messages);
        return result.Text ?? "";
    }

    return chatClient.CreateAIAgent(
        name: "CoordinatorAgent",
        instructions: "协调研究和写作以创建全面的文章。",
        tools: [
            AIFunctionFactory.Create(ResearchAsync),
            AIFunctionFactory.Create(WriteAsync)
        ]
    ).Build();
});

6.3 自定义代理中间件

可以添加自定义中间件进行日志记录、缓存或自定义行为:

builder.AddAIAgent("ChatAgent", (sp, key) =>
{
    var chatClient = sp.GetRequiredService<IChatClient>();
    var searchFunctions = sp.GetRequiredService<SearchFunctions>();
    var logger = sp.GetRequiredService<ILogger<Program>>();

    return chatClient.CreateAIAgent(
        name: key,
        instructions: "...",
        tools: [AIFunctionFactory.Create(searchFunctions.SearchAsync)]
        )
    .AsBuilder()
    .Use(async (messages, options, next, cancellationToken) =>
    {
        // 自定义预处理
        logger.LogInformation("代理正在处理 {MessageCount} 条消息", messages.Count());

        // 调用管道中的下一个
        var result = await next(messages, options, cancellationToken);

        // 自定义后处理
        logger.LogInformation("代理生成了包含 {ContentCount} 个内容项的响应", result.Contents.Count);

        return result;
    })
    .UseOpenTelemetry(configure: c => c.EnableSensitiveData = true)
    .Build();
});

7. 最佳实践

7.1 设计清晰的工具描述

代理工具调用的质量很大程度上取决于良好的描述:

[Description("在产品文档中搜索特定信息。" +
             "当用户询问功能、规格或产品使用方法时使用此工具。" +
             "返回带有文件名和页码的相关摘录以供引用。")]
public async Task<IEnumerable<string>> SearchAsync(
    [Description("要搜索的特定短语、关键字或问题。" +
                 "要具体并包含相关上下文。")] 
    string searchPhrase,
    [Description("可选: 要搜索的精确文件名(如'ProductManual.pdf')。" +
                 "留空则搜索所有文档。")] 
    string? filenameFilter = null)
{
    // 实现
}

7.2 测试代理行为

为代理工具创建单元测试,为代理工作流创建集成测试:

public class SearchFunctionsTests
{
    [Fact]
    public async Task SearchAsync_WithValidQuery_ReturnsResults()
    {
        // 准备
        var mockSemanticSearch = new Mock<SemanticSearch>();
        mockSemanticSearch
            .Setup(s => s.SearchAsync("test", null, 5))
            .ReturnsAsync(new List<IngestedChunk>
            {
                new IngestedChunk { DocumentId = "test.pdf", PageNumber = 1, Text = "测试内容" }
            });

        var searchFunctions = new SearchFunctions(mockSemanticSearch.Object);

        // 执行
        var results = await searchFunctions.SearchAsync("test");

        // 断言
        Assert.NotEmpty(results);
        Assert.Contains("测试内容", results.First());
    }
}

7.3 监控代理性能

使用 Application Insights 或 .NET Aspire 的仪表板监控:

  • 每次代理交互的令牌使用量
  • 工具调用模式(使用哪些工具,使用频率)
  • 代理操作的响应时间
  • 工具调用的错误率
  • 通过反馈机制获得的用户满意度

8. 性能考虑

8.1 流式与非流式

代理框架支持流式和非流式响应:

使用流式的情况

  • 构建交互式聊天界面
  • 用户期望实时反馈
  • 处理长时间运行的查询

使用非流式的情况

  • 后台处理
  • 批量操作
  • 简单 API 端点

8.2 工具调用优化

尽量减少不必要的工具调用:

// 好: 具体指令
"仅当用户询问关于文档的具体问题时使用搜索工具。
如果可以从常识中回答,则不搜索。"

// 不好: 模糊指令
"你可以访问搜索工具。"

9. 部署到 Azure

应用已准备好使用 .NET Aspire 的 Azure 预配进行部署:

# 登录 Azure
az login

# 创建 Azure 资源
cd ChatApp20.AppHost
azd init
azd up

这将:

  • 预配 Azure OpenAI 资源
  • 将 Web 应用部署到 Azure 容器应用
  • 设置 Application Insights 进行监控
  • 配置服务连接和身份验证

结论

通过本文的步骤,我们成功将标准的 AI 聊天应用升级为使用 Microsoft 代理框架的智能代理系统。这一升级带来了更清晰的架构关注点分离、更轻松的测试和内置的可观测性,同时仍然使用 .NET 开发者熟悉的模式。

Microsoft 代理框架的优势在于它不需要开发者学习全新的方式,而是建立在依赖注入、中间件和遥测等熟悉的 .NET 概念之上。无论是简单的工具调用还是复杂的多代理协调,该框架都提供了强大而灵活的功能来满足各种业务需求。

随着 AI 技术的不断发展,采用代理框架将使您的应用能够更好地适应未来需求,提供更智能、更自然的用户体验。

posted @ 2025-12-02 13:55  葡萄城技术团队  阅读(184)  评论(0)    收藏  举报