MAF快速入门(17)用户智能体交互协议AG-UI(中)

大家好,我是Edison。

最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF开发多智能体工作流,我强烈推荐你也上车跟我一起出发!

上一篇,我们初步学习了AG-UI。本篇,我们来了解AG-UI Tools 以及 实现一个前后端工具混合使用的案例。

1 什么是AG-UI Tools?

AG-UI Tools 是 AG-UI 的 工具系统,分为 Backend Tools 和 Frontend Tools,它们是AI Agent和外部世界交互的桥梁,让AI Agent能够执行实际操作,而不仅仅是生成文本。

Backend Tools

顾名思义,Backend Tools即后端工具,它在服务端执行,主要用于做一些类似数据库查询、API调用 或 敏感操作等用途,主要访问服务端资源,安全性要求较高。

何时需要使用Backend Tools?

  • 🔐 涉及敏感数据或 API 密钥

  • 💾 需要访问数据库或后端服务

  • ⚡ 计算密集型任务

  • 🔒 需要服务端验证和审计

Frontend Tools

顾名思义,Frontend Tools即前端工具,它在客户端执行,主要用于做一些类似GPS定位、剪贴板 或者 设备传感器等用途,主要访问客户端设备资源,安全性上仅需做客户端验证。

何时需要使用Frontend Tools?

  • 📍 需要访问设备功能(GPS、相机等)

  • 📋 需要读取本地资源(剪贴板、文件)

  • 🔑 涉及用户隐私数据

  • ⚡ 需要快速响应的本地操作

2 快速开始:前后端工具混合使用

假设我们有这样一个需求:我们向Agent询问"附近有什么好吃的餐厅",AG-UI前端会使用前端工具获取当前定位信息,然后将问题和定位发给AG-UI服务端,服务端又调用服务端工具搜索附近的餐馆信息,最后由大模型整合结果输出最终信息。

整个调用流程如下图所示:

接下来,我们就一步一步完成一个AG-UI Tools示例应用涉及到的Server 和 Client。

AG-UI Tools Server

首先,我们创建一个ASP.NET Web应用,安装以下NuGet包:

复制代码
Microsoft.Agents.AI.Hosting.AGUI.AspNetCore
Microsoft.Agents.AI.OpenAI
Microsoft.Extensions.AI.OpenAI

然后,就是整个示例的核心部分,我们一块一块来说:

(1)定义相关数据模型

说明:这里配置 JsonSerializerContext (JSON源生成器)主要用以提高序列化性能,确保在Native AOT环境下也能够正常序列化,即AOT友好。

复制代码
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 📦 数据模型定义
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
internal sealed class RestaurantSearchResult
{
    public string Location { get; set; } = string.Empty;
    public double SearchRadius { get; set; }
    public int TotalResults { get; set; }
    public NearbyRestaurant[] Restaurants { get; set; } = [];
}
internal sealed class NearbyRestaurant
{
    public string Name { get; set; } = string.Empty;
    public string Cuisine { get; set; } = string.Empty;
    public double Distance { get; set; }
    public double Rating { get; set; }
    public string Address { get; set; } = string.Empty;
}
internal sealed class RestaurantDetail
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string OpeningHours { get; set; } = string.Empty;
    public string PriceRange { get; set; } = string.Empty;
    public string[] RecommendedDishes { get; set; } = [];
    public string Phone { get; set; } = string.Empty;
    public bool HasParking { get; set; }
    public bool AcceptsReservation { get; set; }
}
// JSON 序列化上下文
[JsonSerializable(typeof(RestaurantSearchResult))]
[JsonSerializable(typeof(NearbyRestaurant))]
[JsonSerializable(typeof(RestaurantDetail))]
internal sealed partial class MixedToolsJsonContext : JsonSerializerContext;

(2)定义backend tools

复制代码
internal class BackendTools
{
    // 🔧 餐厅搜索工具(后端执行)
    [Description("根据位置搜索附近的餐厅。Search for nearby restaurants based on location.")]
    public static RestaurantSearchResult SearchNearbyRestaurants(
        [Description("用户当前位置描述")] string location,
        [Description("搜索半径(公里)")] double radiusKm = 2.0,
        [Description("菜系偏好(可选)")] string? cuisinePreference = null)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"\n🔍 [后端工具] SearchNearbyRestaurants");
        Console.WriteLine($"   📍 位置: {location}");
        Console.WriteLine($"   📏 半径: {radiusKm} km");
        Console.WriteLine($"   🍽️ 菜系偏好: {cuisinePreference ?? "不限"}");
        Console.ResetColor();
        // 模拟数据库查询
        var restaurants = new List<NearbyRestaurant>
    {
        new() { Name = "老北京炸酱面", Cuisine = "北京菜", Distance = 0.5, Rating = 4.7, Address = $"{location}东路100号" },
        new() { Name = "川香阁", Cuisine = "川菜", Distance = 0.8, Rating = 4.5, Address = $"{location}西路200号" },
        new() { Name = "粤味轩", Cuisine = "粤菜", Distance = 1.2, Rating = 4.8, Address = $"{location}南路300号" },
        new() { Name = "日式料理屋", Cuisine = "日本料理", Distance = 1.5, Rating = 4.6, Address = $"{location}北路400号" },
        new() { Name = "烤匠", Cuisine = "川菜", Distance = 2.5, Rating = 4.8, Address = $"{location}天府四街银泰城5楼" }
    };
        // 如果有菜系偏好,过滤结果
        if (!string.IsNullOrEmpty(cuisinePreference))
        {
            restaurants = restaurants
                .Where(r => r.Cuisine.Contains(cuisinePreference, StringComparison.OrdinalIgnoreCase))
                .ToList();
        }
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"   ✅ 找到 {restaurants.Count} 家餐厅");
        Console.ResetColor();
        return new RestaurantSearchResult
        {
            Location = location,
            SearchRadius = radiusKm,
            TotalResults = restaurants.Count,
            Restaurants = restaurants.ToArray()
        };
    }
    // 🔧 获取餐厅详情(后端执行)
    [Description("获取指定餐厅的详细信息,包括营业时间、菜单推荐等。Get detailed information about a specific restaurant.")]
    public static RestaurantDetail GetRestaurantDetail(
        [Description("餐厅名称")] string restaurantName)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"\n📖 [后端工具] GetRestaurantDetail");
        Console.WriteLine($"   🏪 餐厅: {restaurantName}");
        Console.ResetColor();
        // 模拟数据库查询
        var detail = new RestaurantDetail
        {
            Name = restaurantName,
            Description = $"{restaurantName}是一家知名的特色餐厅,拥有20年历史。",
            OpeningHours = "10:00 - 22:00",
            PriceRange = "人均 80-150 元",
            RecommendedDishes = ["招牌菜", "特色小吃", "主厨推荐"],
            Phone = "010-12345678",
            HasParking = true,
            AcceptsReservation = true
        };
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"   ✅ 获取成功");
        Console.ResetColor();
        return detail;
    }
}

(3)创建应用并注册AG-UI服务

复制代码
// Step0. Create WebApplication builder
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
// ⭐ 配置 JSON 序列化上下文 (用于数据模型的序列化)
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Add(MixedToolsJsonContext.Default));
// Step1. Register AG-UI services
builder.Services.AddAGUI();

(4)加载可用backend tools并创建Agent

复制代码
var app = builder.Build();

// Step3. Create AI Agent
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>().Value;
AITool[] backendTools =
[
    AIFunctionFactory.Create(BackendTools.SearchNearbyRestaurants, serializerOptions: jsonOptions.SerializerOptions),
    AIFunctionFactory.Create(BackendTools.GetRestaurantDetail, serializerOptions: jsonOptions.SerializerOptions)
];
Console.WriteLine("🔧 已注册后端工具:");
foreach (var tool in backendTools)
{
    Console.WriteLine($"   • {tool.Name} (服务端执行)");
}
Console.WriteLine("📝 前端工具将由客户端注册");

var agent = chatClient.AsIChatClient()
    .AsAIAgent(
        name: "MixedToolsAssistant",
    instructions: """
        你是一个智能餐厅推荐助手,具备以下能力:

        🌍 位置感知:
        - 可以获取用户的当前位置(使用 GetUserLocation 工具)
        - 当用户说"附近"、"周围"时,先获取位置

        🍽️ 餐厅推荐:
        - 使用 SearchNearbyRestaurants 搜索附近餐厅
        - 使用 GetRestaurantDetail 获取详细信息

        🎯 使用流程:
        1. 如果用户问"附近有什么餐厅",先调用 GetUserLocation 获取位置
        2. 然后调用 SearchNearbyRestaurants 搜索
        3. 如果用户想了解某家餐厅,调用 GetRestaurantDetail

        请用中文友好地回答用户。
        """,
    tools: backendTools);
Console.WriteLine("✅ AI Agent 创建成功");

(5)映射AGUI端点并启动应用:

复制代码
// Step5. Mapping AG-UI Endpoints
app.MapAGUI("/", agent);

app.Run();

AG-UI Tools Client

首先,我们创建一个控制台应用,安装以下NuGet包:

复制代码
Microsoft.Agents.AI.AGUI
Microsoft.Agents.AI

然后,定义frontend tools:

复制代码
internal class FrontendTools
{
    // 🔧 获取用户位置(前端工具 - 只有客户端能访问 GPS)
    [Description("获取用户当前的地理位置信息。这是客户端设备功能,用于获取 GPS 位置。Get the user's current location from GPS.")]
    public static string GetUserLocation()
    {
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine("\n📍 [前端工具] GetUserLocation");
        Console.WriteLine("   🔄 正在访问设备 GPS...");
        Console.ResetColor();
        // 模拟 GPS 获取延迟
        Thread.Sleep(800);
        // 模拟不同的位置(随机选择)
        string[] locations =
        [
            "北京市朝阳区三里屯",
            "上海市浦东新区陆家嘴",
            "广州市天河区珠江新城",
            "深圳市南山区科技园",
            "成都市高新区天府软件园"
        ];
        Random random = new();
        string location = locations[random.Next(locations.Length)];
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine($"   ✅ GPS 定位成功: {location}");
        Console.ResetColor();
        return location;
    }
    // 🔧 获取用户偏好设置(前端工具 - 访问本地存储)
    [Description("获取用户保存的餐饮偏好设置。Get user's saved dining preferences.")]
    public static string GetUserPreferences()
    {
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine("\n⚙️ [前端工具] GetUserPreferences");
        Console.WriteLine("   🔄 读取本地偏好设置...");
        Console.ResetColor();
        // 模拟读取本地存储
        Thread.Sleep(300);
        string preferences = "偏好菜系: 川菜、粤菜; 价位: 中等; 忌口: 无";
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine($"   ✅ 读取成功: {preferences}");
        Console.ResetColor();
        return preferences;
    }
}

随后,我们创建AG-UI Client 和 AI Agent:

复制代码
......
var serverEndpoint = config.GetValue<string>("AGUI_SERVER_URL")
    ?? "https://localhost:8443";

// 加载前端工具
AITool[] frontendTools =
[
    AIFunctionFactory.Create(FrontendTools.GetUserLocation),
    AIFunctionFactory.Create(FrontendTools.GetUserPreferences)
];
Console.WriteLine("🔧 已注册前端工具:");
foreach (var tool in frontendTools)
{
    Console.WriteLine($"   • {tool.Name} (客户端执行)");
}
Console.WriteLine("📝 后端工具由服务端提供 (SearchNearbyRestaurants, GetRestaurantDetail)");
Console.WriteLine();

// Step1. Create HTTP Client
using HttpClient httpClient = new()
{
    Timeout = TimeSpan.FromSeconds(180) // 延长超时,因为可能有多个工具调用
};

// Step2. Create AG-UI Client
var chatClient = new AGUIChatClient(httpClient, serverEndpoint);

// Step3. Create AI Agent
var agent = chatClient.AsAIAgent(
    name: "agui-client",
    description: "AG-UI 混合工具客户端",
    tools: frontendTools);  // 👈 注册前端工具

最后,准备开始对话:

复制代码
// Step4. Prepare for Conversation
var session = await agent.GetNewSessionAsync();
var messages = new List<ChatMessage>()
{
    new ChatMessage(ChatRole.System, """
        你是一个智能餐厅推荐助手。
        你可以使用多种工具来帮助用户找到合适的餐厅。
        当用户问"附近有什么餐厅"时,先获取他们的位置,再搜索餐厅。
        """)
};

Console.WriteLine("💬 开始对话(输入 :q 或 quit 退出)\n");

while (true)
{
    Console.Write("👤 用户: ");
    string? message = Console.ReadLine();

    if (string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine("⚠️ 消息不能为空,请重新输入。");
        continue;
    }

    if (message is ":q" or "quit")
    {
        Console.WriteLine("\n👋 再见!");
        break;
    }


    // 添加用户消息
    messages.Add(new ChatMessage(ChatRole.User, message));

    // 统计工具调用
    int frontendToolCalls = 0;
    int backendToolCalls = 0;
    bool isFirstUpdate = true;
    List<string> toolCallChain = [];

    Console.WriteLine();
    Console.WriteLine("━━━━━━━━━━━━━━ 开始处理 ━━━━━━━━━━━━━━━");

    // 流式接收响应
    Console.Write("🤖 助手: ");
    await foreach (var update in agent.RunStreamingAsync(messages, session))
    {
        var chatUpdate = update.AsChatResponseUpdate();

        if (isFirstUpdate)
        {
            Console.ForegroundColor = ConsoleColor.DarkGray;
            Console.WriteLine($"[🔄 Run Started - Thread: {chatUpdate.ConversationId?.Substring(0, 8)}...]");
            Console.ResetColor();
            isFirstUpdate = false;
        }

        foreach (AIContent content in update.Contents)
        {
            switch (content)
            {
                case TextContent textContent:
                    Console.ForegroundColor = ConsoleColor.Cyan;
                    Console.Write(textContent.Text);
                    Console.ResetColor();
                    break;

                case FunctionCallContent functionCall:
                    // 判断是前端还是后端工具
                    var isFrontendTool = frontendTools.Any(t =>
                        t.Name == functionCall.Name);

                    if (isFrontendTool)
                    {
                        frontendToolCalls++;
                        Console.ForegroundColor = ConsoleColor.Magenta;
                        Console.WriteLine($"\n📱 [前端工具调用] {functionCall.Name}");
                    }
                    else
                    {
                        backendToolCalls++;
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.WriteLine($"\n🖥️ [后端工具调用] {functionCall.Name}");

                        // 显示后端工具参数
                        if (functionCall.Arguments != null)
                        {
                            Console.WriteLine("   📝 参数:");
                            foreach (var kvp in functionCall.Arguments)
                            {
                                Console.WriteLine($"      • {kvp.Key}: {kvp.Value}");
                            }
                        }
                    }
                    Console.ResetColor();

                    toolCallChain.Add(isFrontendTool ? $"📱{functionCall.Name}" : $"🖥️{functionCall.Name}");
                    break;

                case FunctionResultContent functionResult:
                    // 后端工具结果显示
                    if (!frontendTools.Any(t => toolCallChain.LastOrDefault()?.Contains(t.Name) ?? false))
                    {
                        Console.ForegroundColor = ConsoleColor.Green;
                        string resultPreview = functionResult.Result?.ToString() ?? "null";
                        if (resultPreview.Length > 150)
                        {
                            resultPreview = resultPreview.Substring(0, 150) + "...";
                        }
                        Console.WriteLine($"   ✅ 结果: {resultPreview}");
                        Console.ResetColor();
                    }
                    break;

                case ErrorContent errorContent:
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine($"\n❌ 错误: {errorContent.Message}");
                    Console.ResetColor();
                    break;
            }
        }
    }

    // 显示工具调用链
    Console.WriteLine();
    Console.WriteLine("━━━━━━━━━━━━━━ 处理完成 ━━━━━━━━━━━━━━━");
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"[✅ Run Finished]");
    Console.WriteLine($"   📱 前端工具调用: {frontendToolCalls}");
    Console.WriteLine($"   🖥️ 后端工具调用: {backendToolCalls}");

    if (toolCallChain.Count > 0)
    {
        Console.WriteLine($"   🔗 调用链: {string.Join(" → ", toolCallChain)}");
    }

    Console.ResetColor();
    Console.WriteLine();
}

Console.WriteLine("👋 再见!");
Console.ReadKey();

3 测试结果

测试场景1

用户:附近有什么好吃的餐厅?

调用链路:📱GetUserLocation → 🖥️SearchNearbyRestaurants

测试场景2

用户:根据我的偏好推荐餐厅

调用链路:📱GetUserPreferences → 📱GetUserLocation → 🖥️SearchNearbyRestaurants

测试场景3

用户:烤匠这家店怎么样?

调用链路:🖥️GetRestaurantDetail

可以看到,Agent可以自动协调工具调用顺序,前后端工具能够实现协同工作,同时我们也能看到完整的工具调用链路。

4 小结

本文介绍了AG-UI Tools即AG-UI的工具系统,演示了一个结合前后端工具使用的混合模式案例,通过案例可以了解如何在AG-UI中调用工具并观察调用链。

示例源码

GitHub: https://github.com/EdisonTalk/MAFD

参考资料

圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)

Microsoft Learn,《Agent Framework Tutorials


作者:爱迪生

出处:https://edisontalk.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

相关推荐
ssshooter2 小时前
免费和付费 AI API 选择指南
人工智能·aigc·openai
多恩Stone2 小时前
【C++入门扫盲1】C++ 与 Python:类型、编译器/解释器与 CPU 的关系
开发语言·c++·人工智能·python·算法·3d·aigc
四月沐歌2 小时前
本地运行的个人AI Agent的方案设想(UI-TARS 基于视觉大模型增强OpenClaw)
agent
精神状态良好2 小时前
实战:从零构建本地 Code Review 插件
前端·llm
爱吃的小肥羊3 小时前
DeepSeek V4 细节曝光:100 万上下文 + 原生多模态
人工智能·aigc·deepseek
智泊AI3 小时前
LangChain到底是什么?LangChain的核心组件有哪些?
llm
架构师汤师爷3 小时前
一文彻底搞懂 OpenClaw 的架构设计与运行原理(万字图文)
前端·agent
DigitalOcean3 小时前
无服务器推理(Serverless Inference)是什么?与传统AI推理部署方式全面对比
aigc
马里马里奥-5 小时前
文献阅读:Next-Generation Database Interfaces: A Survey of LLM-Based Text-to-SQL
llm·nlp2sql