背景

近期在做的一个项目,增加了一些AI相关的模块。在开发过程中,使用了微软最新的智能体开发框架,SemanticKernel Agent Framework(https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/?pivots=programming-language-csharp

算是有一点不成熟的经验吧,想拿出来聊聊,首先SemanticKernel(以下简称SK)是微软开源的智能体开发框架,它的定位就是企业级的AI开发框架,致力于将智能业务和本地业务结合,开发出智能化的功能模块。

更多内容大家可以查看上面给出的官方文档,这里不再赘述。

对了,笔者曾经也写过基于SK的相关博客,欢迎点击查看:

https://mp.weixin.qq.com/s/FTXTZUWwU45sefqMjq6fUw

配置服务

SK框架支持兼容OpenAI接口风格的模型,所以大部分国内的模型都是可以使用的,我这里封装了3个,分别是Kimi,Qwen和DeepSeek,其他看情况修改即可。

基本代码如下,这里返回值是一个Kernel,所以注意编写的时候引入合适的命名空间,博客篇幅有限我就不灌太多代码了。

public static Kernel CreateKernel(IConfiguration configuration)
{
    var builder = Kernel.CreateBuilder();
    var provider = configuration["AI:Provider"]!;

    var (model, key, endpoint) = provider.ToLower() switch
    {
            "qwen" => (
                configuration["AI:Qwen:Model"]!,
                configuration["AI:Qwen:ApiKey"]!,
                configuration["AI:Qwen:Endpoint"]!),
            "kimi" => (
                configuration["AI:Kimi:Model"]!,
                configuration["AI:Kimi:ApiKey"]!,
                configuration["AI:Kimi:Endpoint"]!),
            "deepseek" => (
                configuration["AI:DeepSeek:Model"]!,
                configuration["AI:DeepSeek:ApiKey"]!,
                configuration["AI:DeepSeek:Endpoint"]!),
            _ => throw new ArgumentException($"暂时还不支持的AI提供商: {provider}")
            };

    ConsoleHelper.WriteLine($"正在使用模型:{model},{DateTime.Now}", ConsoleColor.Cyan);

    // 添加 OpenAI 兼容的模型服务
    builder.AddOpenAIChatCompletion(
        modelId: model,
        apiKey: key,
        endpoint: new Uri(endpoint));

    return builder.Build();
}

即便我们在国内使用GPT,Gemini,Claude等国际模型有困难,但国内的AI行业发展的也不容小觑,因此完全没必要为此苦恼,我们可以使用国内的大模型来做我们的业务底座,性能,效果甚至可能更好。

编写插件

配置完成后,我们可以先编写服务插件,当然这个步骤不是绝对的,看个人习惯和项目情况。代码实际上比较简单,按照你项目的架构风格编写业务代码即可,需要注意的是,插件方法的头部要增加一些标记,让SK框架可以认出这些插件,看一下案例代码

[KernelFunction("query_by_project_id")] // 👈--注意这个要使用蛇形命名法
[Description("通过项目ID查询项目详细信息")] // 👈--这个说明也很重要
public async Task<string> QueryByProjectIdAsync(
    [Description("项目ID,如 720540936868229")] long projectId)
{
    ConsoleHelper.WriteLine($"=== 智能体命中插件,通过项目ID查询项目信息{DateTime.Now} ===");
    ConsoleHelper.WriteLine($"项目ID: {projectId}");

    try
    {
        if (_decProjectProvider != null)
        {
            var result = await _decProjectProvider.GetDecProjectDetail(projectId);

            if (!result.IsSuccess || result.Value == null)
            {
                return $"未找到项目ID为 {projectId} 的项目信息。";
            }

            return FormatProjectInfo(result.Value);
        }
        return "记录不存在";
    }
    catch (Exception ex)
    {
        ConsoleHelper.WriteLine($"项目ID查询失败: {ex.Message}");
        return $"查询失败: {ex.Message}";
    }
}

上面代码比较简单,就是一次简单的业务信息查询,但需要说明的是,方法体头部的KernelFunction标记,建议使用 snake_case(蛇形命名法)命名函数,以提高与主流大模型工具调用协议的兼容性。还一个要注意的是需要编写方法说明和参数说明,这个在模型执行工具调用的时候也很重要,算是明确的提示词吧,这些就当个规范记住就好。

定义智能体

这里智能体的定义,也是根据实际情况,看如何操作更加方便,我这里是定义了一个基类,在基类中先定好了智能体要定义的属性,方法等,然后所有派生类都要集成这个基类,并实现独特的智能体角色。

因此我这里定义专属智能体的代码就非常简单

public class MyBusinessAgent : ModernAgentBase
{
    public override string Name => "MyBusinessAgent";
    public override string Description => "业务智能体";

    protected override string Instructions => "你是专业的业务助手...";

    public MyBusinessAgent(Kernel kernel) : base(kernel) { }
}

比如刚刚的查询插件,我这里在这个基类的先定下,可以这样定义(部分代码)

 public class ModernProjectQueryAgent : ModernAgentBase
 {
     private readonly ProjectQueryPlugin _projectQueryPlugin;

     public override string Name => "ModernProjectQueryAgent";

     public override string Description => "现代化项目信息查询智能体,支持通过项目ID查询项目详情";

     protected override string Instructions => "你是专业的项目信息查询助手,能够根据用户提供的项目ID查询项目信息。你拥有 query_by_project_id 查询工具,支持6位以上数字的项目ID查询。请始终以专业、高效的方式为用户提供准确的项目查询服务。";

     // 注意这里的几个参数,对智能体的调用结果十分重要,大家可以自行了解一下
     protected override KernelArguments DefaultArguments => CreateStandardArguments(
         temperature: 0.3,
         topP: 0.8,
         maxTokens: 1500,
         enableFunctionCalling: true
     );

     public ModernProjectQueryAgent(Kernel kernel, IServiceProvider serviceProvider) : base(kernel)
     {
         _projectQueryPlugin = new ProjectQueryPlugin(serviceProvider);
     }
     
    /// <summary>
    /// 配置智能体插件
    /// </summary>
    protected override void ConfigurePlugins(ChatCompletionAgent agent)
    {
        AddPlugin(_projectQueryPlugin, "ProjectQuery");
    
        ConsoleHelper.WriteLine($"=== {Name} 插件配置完成 ===",ConsoleColor.Green);
        ConsoleHelper.WriteLine($"已加载插件数量: {_kernel.Plugins.Count}",ConsoleColor.Green);
        foreach (var plugin in _kernel.Plugins)
        {
            ConsoleHelper.WriteLine($"插件: {plugin.Name}", ConsoleColor.Green);
            foreach (var function in plugin)
            {
                ConsoleHelper.WriteLine($"  - 函数: {function.Name} - {function.Description}", ConsoleColor.Green);
            }
        }
    }

    /// <summary>
    /// 智能查询项目信息
    /// </summary>
    public async Task<string> SmartQueryAsync(string userInput, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(userInput))
        {
            return GetUsageHelp();
        }
        try
        {
            var chatHistory = new ChatHistory();
            var queryPrompt = $"请分析以下用户输入并执行相应的项目查询:\n{userInput}";
           
            var response = await GetResponseAsync(queryPrompt, chatHistory, cancellationToken);
            var result = response.Content ?? "查询失败,未获取到有效响应";
    
            return result;
        }
        catch (Exception ex)
        {
            return FormatErrorResponse(userInput, ex.Message);
        }
    }

     /// <summary>
    /// 执行智能体对话(获取单一响应)
    /// </summary>
    public virtual async Task<ChatMessageContent> GetResponseAsync(
        string userMessage, 
        ChatHistory? chatHistory = null, 
        CancellationToken cancellationToken = default)
    {
        // 如果没有提供历史记录,创建新的
        chatHistory ??= new ChatHistory();    
        // 添加用户消息
        chatHistory.AddUserMessage(userMessage);    
        // 获取响应
        ChatMessageContent? lastResponse = null;
        await foreach (var response in InvokeAsync(chatHistory, cancellationToken))
        {
            lastResponse = response;
            // 将助手响应添加到历史记录
            chatHistory.Add(response);
        }
    
        return lastResponse ?? throw new InvalidOperationException("智能体未返回有效响应");
    }
 }

// 关于基类的实现,这里也简单列一下
public abstract class ModernAgentBase
{
    protected readonly Kernel _kernel;
    private ChatCompletionAgent? _agent;

    public abstract string Name { get; }
    public abstract string Description { get; }
    protected abstract string Instructions { get; }
    protected virtual KernelArguments DefaultArguments => new();

    protected ModernAgentBase(Kernel kernel)
    {
        _kernel = kernel;
        InitializeAgent();
    }

    private void InitializeAgent()
    {
        var agentBuilder = new ChatCompletionAgentBuilder()
            .WithKernel(_kernel)
            .WithName(Name)
            .WithDescription(Description)
            .WithInstructions(Instructions)
            .WithArguments(DefaultArguments);

        // 允许子类扩展插件
        ConfigurePlugins(agentBuilder);

        _agent = agentBuilder.Build();
    }

    /// <summary>
    /// 子类重写此方法以注册插件(工具)
    /// </summary>
    protected virtual void ConfigurePlugins(ChatCompletionAgentBuilder builder)
    {
        // 默认不注册插件,由子类实现
    }

    /// <summary>
    /// 向 Kernel 注册插件(供子类调用)
    /// </summary>
    protected void AddPlugin(object pluginInstance, string pluginName)
    {
        _kernel.Plugins.AddFromObject(pluginInstance, pluginName);
    }

    /// <summary>
    /// 执行对话并返回最终响应(支持多轮工具调用)
    /// </summary>
    public async Task<ChatMessageContent> GetResponseAsync(
        string userMessage,
        ChatHistory? chatHistory = null,
        CancellationToken cancellationToken = default)
    {
        chatHistory ??= new ChatHistory();
        chatHistory.AddUserMessage(userMessage);

        ChatMessageContent? lastMessage = null;

        await foreach (var message in _agent!.InvokeAsync(chatHistory, cancellationToken))
        {
            lastMessage = message;
            chatHistory.Add(message);
        }

        return lastMessage ?? throw new InvalidOperationException("出现异常");
    }
}

注意啊,受篇幅限制,我不能把全部的定义都放上来,整个智能体的定义流程基本就是,定义基本属性–>注入插件–>调用服务.

上面这个例子主要实在ConfigurePlugins这个重载方法里,注入了需要的插件。实际上这么看智能体的定义也有想想我们传统的分层业务,定义仓储,开放接口,然后在宿主项目中引入,最后在开发成webapi或者页面服务,开发思路都是一样的,也是要保持单一职责,所以如果我们的智能体模块要好用,就需要多个智能体,多个插件,互相不干扰,也要保持智能体角色的单一性。

创建智能服务

前面的工作完成后,可以创建一个智能服务,来统一的接受宿主层发过来的请求,比如我们就把场景限定在一个对话框,但这个对话框不仅仅是可以聊天,还能智能识别你的聊天意图,自动的去调用不同角色的智能体来实现本地化的服务

public async Task<string> SendMessageAsync(
    string userInput, 
    string? sessionId = null, 
    CancellationToken ct = default)
{
    try
    {
        
        // 前置业务省略
        // ...

        // 智能选择智能体
        var selectedAgent = SelectAppropriateAgent(userInput);
        ConsoleHelper.WriteLine($"选择的智能体: {selectedAgent}");
        string response;
        if (selectedAgent == AgentType.ProjectQuery)
        {
            // 使用项目查询智能体
            response = await _projectQueryAgent.SmartQueryAsync(finalUserInput, ct);
        }
        else
        {
            // 使用客服智能体,提供历史上下文
            var contextInfo = ExtractContextInfo(history, referencedContent);
            response = await _customerSupportAgent.GetSmartResponseAsync(
                finalUserInput, 
                history, 
                contextInfo, 
                ct);
        }

        // 6. 只有生成了有效回复才继续存储到Redis
        if (!string.IsNullOrWhiteSpace(response))
        {
           // 保存聊天历史业务,略
        }
        return response;
    }
    catch (Exception ex)
    {
        ConsoleHelper.WriteLine($"处理消息失败: {ex.Message}");
        return "抱歉,系统暂时无法响应,请稍后再试。";
    }
}

注入容器

前面的工作完成后,就可以在宿主系统里注入服务了,这部分代码很

var kernel = ModernAgentConfiguration.CreateKernel(configuration);
services.AddSingleton(kernel);

// 注册智能体
services.AddSingleton<ModernProjectQueryAgent>();
services.AddSingleton<ModernCustomerSupportAgent>();

// 注册统一服务
services.AddSingleton<ICompatibleAgentService, ModernAgentService>();

测试效果

宿主系统完成服务注入后,就可以看一下效果怎么样了,我这边是在BlazorServer的项目里创建了一个对话框,代码就不贴了,看下执行效果吧

可以看到,智能体和常见的聊天机器人还是有区别,我们可以在定义的时候限制它的聊天能力,让它不要回答与角色无关的内容。然后问到业务相关的事情时,又能准确的识别我们的意图,给出正确的回复

这里,我打印了一些日志,仅供参考,如下

好了,至此,一个智能体就顺利集成到我们原有的项目里啦,收工!

结束语

我觉得现阶段开发的大部分的传统项目,都应该尽可能的去集成一个AI模块,即便你确实没有明确的需求也要去尝试一下。

记得前阵子看过微软CEO的一个演讲,他提到一个观点,大概是说,我们开发的软件,由于最终使用者都是人,所以需要投入大量精力在界面,交互等工作上面,即便是服务端的开发有时候也需要顺应这种交互,而在AI的时代,仅仅通过自然语言,AI就可以帮我们更高效的完成很多复杂的任务,而在将任务交给AI的时候,是不需要太多交互和界面设计的,我们只需要等一个结果就好。

当然这是一个愿景,但从AI的发展速度来看,距离这个愿景的实现可能越来越近了,作为开发者,我们也应该积极的顺应这个时代,从认知到实践,都应该积极的做出改变。可能你的项目真的不需要一个AI模块,但也要为这种即将到来的风暴变革做好准备。

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐