前面的课程,我们已经用C#实现了,自己的MCP Client。

下面我们一起来实现,MCP Client与LLM 对接。

一、添加依赖库

目前来说,绝大部分的大模型的API,都是遵循OpenAI的接口规范。

Microsoft.Extensions.AI 是微软官方提供的一套 统一的 AI 抽象层 ,大大简化 AI 模型在 .NET 应用中的集成。

添加依赖库:Microsoft.Extensions.AI.OpenAI,版本为:最新预发行版 9.4.4-preview.1.25259.16,添加的时候记得勾选:包括预发行版。

添加依赖库:Microsoft.Extensions.AI,版本为:9.4.4-preview.1.25259.16。

二、OpenAI 客户端实现

新增文件:ChatAIClient

2.1 初始化OpenAI客户端


初始化OpenAI客户端,并使用UseFunctionInvocation 来增强客户端, 这里启用函数调用。

备注:以下代码涉及的秘钥,记得替换为自己的。

using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
namespace MCPClient
{
    /// <summary>
    /// 表示一个用于与 AI 聊天模型交互的客户端封装类。
    /// 负责初始化聊天客户端并维护对话上下文。
    /// </summary>
    public class ChatAIClient
    {
        /// <summary>
        /// 封装后的 AI 聊天客户端接口,支持函数调用等功能。
        /// </summary>
        private IChatClient ChatClient;
        /// <summary>
        /// 存储当前会话中的所有聊天消息记录。
        /// </summary>
        private IList<ChatMessage> Messages;
        /// <summary>
        /// API 访问密钥,用于身份认证。【记得替换为自己的】
        /// </summary>
        private const string _apiKey = "6092598c-ce00-48fd-a5be-0d758088c888";
        /// <summary>
        /// AI 服务的基础请求地址。【记得替换为自己的】
        /// </summary>
        private const string _baseURL = "https://api-inference.modelscope.cn/v1/";
        /// <summary>
        /// 使用的 AI 模型标识符。【记得替换为自己的】
        /// </summary>
        private const string _modelID = "Qwen/Qwen2.5-72B-Instruct";
        /// <summary>
        /// 初始化一个新的 <see cref="ChatAIClient"/> 实例。
        /// 构造函数中自动完成聊天客户端的初始化配置。
        /// </summary>
        public ChatAIClient()
        {
            InitIChatClient();
        }
        /// <summary>
        /// 初始化内部使用的 AI 聊天客户端实例。
        /// 配置 API 凭证、服务端点,并构建具备函数调用能力的客户端。
        /// 同时初始化系统消息作为对话起点。
        /// </summary>
        private void InitIChatClient()
        {
            // 创建 API 密钥凭证
            ApiKeyCredential apiKeyCredential = new ApiKeyCredential(_apiKey);
            // 设置 OpenAI 客户端选项,如自定义服务端点
            OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
            openAIClientOptions.Endpoint = new Uri(_baseURL);
            // 创建 OpenAI 客户端并获取指定模型的聊天接口
            var openaiClient = new OpenAIClient(apiKeyCredential, openAIClientOptions)
                .GetChatClient(_modelID)
                .AsIChatClient();
            // 构建增强功能的聊天客户端(例如启用函数调用)
            ChatClient = new ChatClientBuilder(openaiClient)
                .UseFunctionInvocation()
                .Build();
            // 初始化对话历史,包含一条系统提示信息
            Messages =
            [
                // 添加系统角色消息
                new(ChatRole.System, "您是一位乐于助人的助手,帮助我们测试MCP服务器功能,优先使用中文回答!"),
            ];
        }
    }
}

2.2 处理用户的自然语言查询

ChatAIClient文件,添加如下代码,实现与 AI 模型交互,并传入 MCP 工具。

/// <summary>
/// 异步处理用户的自然语言查询,并与 AI 模型进行交互,支持 MCP 工具调用。
/// </summary>
/// <param name="query">用户的自然语言查询内容</param>
/// <param name="tools">可用的 MCP 工具列表,用于扩展 AI 的外部能力</param>
/// <returns>AI 返回的最终文本响应结果</returns>
public async Task<string> ProcessQueryAsync(string query, IList<McpClientTool> tools)
{
    // 如果消息历史为空,则初始化系统提示消息
    if (Messages.Count == 0)
    {
        Messages = 
        [
            new(ChatRole.System, "您是一位乐于助人的助手,帮助我们测试MCP服务器功能,优先使用中文回答!")
        ];
    }
    // 添加用户输入的消息到对话历史
    Messages.Add(new(ChatRole.User, query));
    // 设置请求选项,注入可用工具
    var options = new ChatOptions
    {
        Tools = [.. tools]
    };
    // 调用 AI 客户端获取响应
    var response = await ChatClient.GetResponseAsync(Messages, options);
    // 将 AI 响应加入对话历史
    Messages.AddMessages(response);
    // 输出调用的工具信息
    OutputToolUsageInfo(response);
    // 返回模型生成的文本响应
    return response.Text;
}

2.3 MCP 工具使用情况日志


ChatAIClient文件,添加如下代码,输出 AI 调用MCP 工具的情况。

 /// <summary>
    /// 辅助方法:输出 AI 在响应中调用的工具信息到控制台。
    /// </summary>
    /// <param name="response">来自 AI 的完整响应对象</param>
    private void OutputToolUsageInfo(ChatResponse response)
    {
        // 获取所有 Tool 角色的消息
        var toolUseMessages = response.Messages.Where(m => m.Role == ChatRole.Tool).ToList();
        // 判断是否调用了工具
        // 获取响应中所有角色为 Tool 的消息(即 AI 调用了哪些工具)
        var toolUseMessage = response.Messages.Where(m => m.Role == ChatRole.Tool);
        // 判断第一条消息的内容是否多于一个(通常第一个消息是用户问题,第二个是调用函数)
        if (response.Messages[0].Contents.Count > 1)
        {
            // 尝试从第一条消息的第二个内容项提取出函数调用信息
            var functionCall = (FunctionCallContent)response.Messages[0].Contents[1];
            // 设置控制台输出颜色为绿色,用于突出显示工具调用信息
            Console.ForegroundColor = ConsoleColor.Green;
            string arguments = "";
            // 如果函数调用包含参数,则拼接参数信息
            if (functionCall.Arguments != null)
            {
                foreach (var arg in functionCall.Arguments)
                {
                    arguments += $"{arg.Key}:{arg.Value};";
                }
                // 输出调用的方法名及参数信息
                Console.WriteLine($"调用方法名:{functionCall.Name};参数信息:{arguments}");
                // 遍历所有 Tool 消息,输出每个工具调用的结果
                foreach (var message in toolUseMessage)
                {
                    // 提取工具调用后的执行结果
                    var functionResultContent = (FunctionResultContent)message.Contents[0];
                    Console.WriteLine($"调用工具结果:{functionResultContent.Result}");
                }
                // 恢复控制台默认颜色(白色)
                Console.ForegroundColor = ConsoleColor.White;
            }
            else
            {
                // 如果没有参数
                Console.WriteLine("工具参数为空");
            }
        }
        else
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("本次没有调用工具");
            Console.ForegroundColor = ConsoleColor.White;
        }
    }
}

三、为LLM添加工具能力

在前面课程基础之上,在Program.cs添加代码。

代码说明:为LLM添加工具能力,并处理客户提交的内容。

// 创建聊天客户端实例
ChatAIClient chatAIClient = new ChatAIClient();
// 进入主循环,持续接收用户输入直到输入 "exit"
while (true)
{
    try
    {
        // 设置控制台文字颜色为黄色,提示用户输入问题
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.Write("\n提问: ");
        // 读取用户输入并去除前后空格,若为空则赋默认空字符串
        string query = Console.ReadLine()?.Trim() ?? string.Empty;
        // 判断用户是否输入 "exit" 以退出程序
        if (query.ToLower() == "exit")
        {
            break;
        }
        // 调用异步方法处理用户查询,并传入预定义的工具列表(listToolsResult)
        string response = await chatAIClient.ProcessQueryAsync(query, listToolsResult);
        // 设置输出颜色为黄色,显示 AI 的响应内容
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine($"AI:{response}");
        // 恢复控制台默认颜色(白色)
        Console.ForegroundColor = ConsoleColor.White;
    }
    catch (Exception ex)
    {
        // 捕获所有异常并输出错误信息,防止程序崩溃
        Console.WriteLine($"\nError: {ex.Message}");
    }
}

四、测试效果

启动项目,并输入以下内容:

抓取 https://blog.csdn.net/daremeself/article/details/147166987 的内容,并markdown格式输出

调用MCP Server的工具的情况日志。

AI响应的结果:

好了,今天就分享到这边!

下一个课程:实现自己的MCP Server。

文中示例代码: https://pan.quark.cn/s/b5b8853200f9

该专栏优先在飞书发布,欢迎收藏关注!

https://www.feishu.cn/community/article?id=7507084665509904403

- End -

推荐阅读

VS Code + Cline + 魔搭MCP Server 实现抓取网页内容。

C#实现自己的MCP Client

C#实现MCP Client 与 LLM 连接,抓取网页内容功能!的更多相关文章

  1. 5 -- Hibernate的基本用法 --4 8 外连接抓取属性

    外连接抓取能限制执行SQL语句的次数来提高效率,这种外连接抓取通过在单个select语句中使用outer join来一次抓取多个数据表的数据. 外连接抓取允许在单个select语句中,通过@ManyT ...

  2. paip.抓取网页内容--java php python

    paip.抓取网页内容--java php python.txt 作者Attilax  艾龙, EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog ...

  3. 使用Jsoup函数包抓取网页内容

    之前写过一篇用Java抓取网页内容的文章,当时是用url.openStream()函数创建一个流,然后用BufferedReader把这个inputstream读取进来.抓取的结果是一整个字符串.如果 ...

  4. Asp.Net 之 抓取网页内容

    一.获取网页内容——html ASP.NET 中抓取网页内容是非常方便的,而其中更是解决了 ASP 中困扰我们的编码问题. 需要三个类:WebRequest.WebResponse.StreamRea ...

  5. ASP.NET抓取网页内容的实现方法

    这篇文章主要介绍了ASP.NET抓取网页内容的实现方法,涉及使用HttpWebRequest及WebResponse抓取网页内容的技巧,需要的朋友可以参考下 一.ASP.NET 使用HttpWebRe ...

  6. ASP.NET抓取网页内容

    原文:ASP.NET抓取网页内容 一.ASP.NET 使用HttpWebRequest抓取网页内容 这种方式抓取某些页面会失败 不过,有时候我们会发现,这个程序在抓取某些页面时,是获不到所需的内容的, ...

  7. c#抓取网页内容乱码的解决方案

    写过爬虫的同学都知道,这是个很常见的问题了,一般处理思路是: 使用HttpWebRequest发送请求,HttpWebResponse来接收,判断HttpWebResponse中”Content-Ty ...

  8. C# 抓取网页内容的方法

    1.抓取一般内容 需要三个类:WebRequest.WebResponse.StreamReader 所需命名空间:System.Net.System.IO 核心代码: view plaincopy ...

  9. ASP.NET 抓取网页内容

    (转)ASP.NET 抓取网页内容 ASP.NET 抓取网页内容-文字 ASP.NET 中抓取网页内容是非常方便的,而其中更是解决了 ASP 中困扰我们的编码问题. 需要三个类:WebRequest. ...

  10. 爬虫学习一系列:urllib2抓取网页内容

    爬虫学习一系列:urllib2抓取网页内容 所谓网页抓取,就是把URL地址中指定的网络资源从网络中读取出来,保存到本地.我们平时在浏览器中通过网址浏览网页,只不过我们看到的是解析过的页面效果,而通过程 ...

随机推荐

  1. 数据库离程序员有多远 - cnblogs救园行动感想

    这两周,我参与了博客园的"2024救园行动",成了终身会员.说实话,当初报名的时候,我心里还挺兴奋的,想着这下能和不少老朋友在这个社区里再次相聚.毕竟,在数据库行业摸爬滚打了这么多 ...

  2. JS代码执行

  3. python excel 打开表格:表格名不知道应该怎么打开

    取所有表格名的倒数第一个就是操作的表格 import pandas as pd xl = pd.ExcelFile(xlPath) names = xl.sheet_names df = xl.par ...

  4. .NET Core 中如何实现缓存的预热?

    在构建高性能的 .NET Core 应用时,缓存是提升系统响应速度.减轻数据库压力的利器.然而,缓存并非一蹴而就,它也需要"热身"才能发挥最佳性能.这就是缓存预热的意义所在. 一. ...

  5. 写于vue3.0发布前夕的helloworld之二

    接着,继续走,来到了vm.$mount. 开始生成render函数,生成VNode,由于是第一次加载,所以patch机制为只删除前一个dom节点机制,下面都会讲到. 先到$mount: Vue.pro ...

  6. 深入理解Base64编码原理

    前言 上篇文章有涉及到Base64编码的内容,今天我们再来详细了解一下Base64的编码原理以及应用场景. 通过这篇文章你能够学习到: 什么是Base64,为什么需要Base64? Base64的编码 ...

  7. directory 用于数据泵 导入、导出创建的目录。

    1.查询directory目录 select * from dba_directories; 2.创建或者修改 directory目录 create or replace directory 目录名称 ...

  8. Oracle11g的SGA和PGA设置为多大最合适?

    Oracle官方文档推荐: MEMORY_TARGET=物理内存 x 80% MEMORY_MAX_SIZE=物理内存 x 80% 对于OLTP系统:  SGA_TARGET=(物理内存 x 80%) ...

  9. Lambda表达式的省略规则、Lambda和匿名内部类的区别--java进阶day03

    1.省略规则 2.流程讲解 主方法中调用useStringhandler,该方法的形参是接口,所以我们要给实现类对象,这里我们使用匿名内部类 use...方法进栈,形参也是变量,接收到匿名内部类(如下 ...

  10. Windows 左ctrl和左alt键互换

    reg代码 Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyb ...