昨天的文章中,我们介绍了我的新开源项目:C# Runner。这是一个强大的C#代码运行器,不仅提供了前端UI,还内建了API和一个MCP服务端。

大家可能知道,MCP (Model Context Protocol) 是由 Anthropic 公司推出的一个协议,旨在让大语言模型(LLM)能够以一种更通用的方式调用外部工具。我们的 C# Runner 正好实现了MCP协议,这使得任何大模型都能通过API来调用并执行C#代码,从而获得精确、可靠的外部能力。

今天,我们就来深入探讨如何将这个强大的C#运行器接入到大模型中,让AI拥有执行代码的“超能力”。

大模型“幻觉”的困境

通常,我们可能是这样通过 OpenAIClient 调用聊天API的:

// 注意:需要安装 OpenAI 的 NuGet 包
var api = new OpenAIClient(new ApiKeyCredential(Util.GetPassword("azure-ai-key")), new OpenAIClientOptions
{
Endpoint = new Uri($"https://{Util.GetPassword("azure-ai-resource")}.openai.azure.com/openai/v1?api-version=preview"),
});
ChatClient cc = api.GetChatClient("gpt-4.1"); await foreach (StreamingChatCompletionUpdate delta in cc.CompleteChatStreamingAsync(
[
new SystemChatMessage("你是人工智能助理"),
new UserChatMessage("1234567除以7654321=?(需要精确到5位小数)"),
]))
{
if (delta.ContentUpdate.Count > 0)
{
Console.Write(delta.ContentUpdate[0].Text);
}
}

对于这个数学问题,大模型的输出可能如下:

用计算器直接计算:

$$\frac{1234567}{7654321} \approx 0.16128$$

精确到小数点后5位答案是:

0.16128

模型声称它“使用计算器”了,但实际上,这个结果(0.16128)是基于其内部的概率推理得出的,并非精确计算。我们知道,正确答案其实是 0.16129。这种在需要精确计算时产生的“幻觉”,正是我们需要外部工具来解决的痛点。

通过MCP协议赋予大模型C#执行能力

为了解决上述问题,我们可以将 C# 运行器作为工具接入大模型。第一步是让我们的应用程序了解这个工具有什么功能。通过 MCP 协议,我们可以轻松获取这些信息。

首先,安装 ModelContextProtocol.Core NuGet 包,然后用下面的代码获取工具的定义:

// 安装 NuGet 包: ModelContextProtocol.Core
IMcpClient mcpClient = await McpClientFactory.CreateAsync(new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://csharp.starworks.cc/mcp"),
})); await foreach (var tool in mcpClient.EnumerateToolsAsync())
{
Console.WriteLine($"""
Name: {tool.Name}
Schema: {tool.JsonSchema.ToString()}
Description: {tool.Description}
""");
}

运行后,你会得到类似下面的输出,它描述了工具的名称、功能和参数:

Name: run_code
Schema: {"type":"object","properties":{"code":{"type":"string"},"timeout":{"type":"integer"}},"required":["code"]}
Description: Run C# code in a sandboxed environment, default timeout: 30000(ms)……

有了这些信息,我们就可以将其转换为 OpenAI Chat Completion API 所要求的工具格式。

var cco = new ChatCompletionOptions();
await foreach (McpClientTool tool in mcpClient.EnumerateToolsAsync())
{
cco.Tools.Add(ChatTool.CreateFunctionTool(
tool.Name,
tool.Description,
BinaryData.FromString(tool.JsonSchema.GetRawText())));
}

构建大模型与工具的交互循环

当大模型决定使用工具时,整个交互过程并非一次完成,而是一个循环。模型可能会多次调用工具,直到它认为问题已经解决。我们需要构建一个循环来处理这个过程,并将每一次的对话、工具调用请求和工具返回结果都保存起来。

下面是这个交互循环的逻辑骨架:

// 历史消息,包含系统指令和用户初次提问
var histories = new List<ChatMessage>
{
new SystemChatMessage("你是人工智能助理,请结合已有工具(如果存在)回复用户的需求,如果工具错误,请尽量解决错误并重试"),
new UserChatMessage("1234567除以7654321=?(需要精确到5位小数)")
}; ChatFinishReason? finishReason = null;
do
{
// 异步流式获取模型响应
await foreach (StreamingChatCompletionUpdate delta in cc.CompleteChatStreamingAsync(histories, cco))
{
// ... 处理流式响应 ... // 1. 收集模型发出的工具调用请求
// (需要将流式返回的Delta片段拼接成完整的工具调用) // 当模型确认需要调用工具时
if (delta.FinishReason == ChatFinishReason.ToolCalls)
{
// 2. 将模型的工具调用请求添加到历史记录中
// 3. 调用MCP客户端,执行C#代码
// 4. 将工具的执行结果添加到历史记录中
} // ... 输出模型的最终文本回复 ... finishReason = delta.FinishReason;
}
} while (finishReason == ChatFinishReason.ToolCalls || finishReason == null); // 如果模型还需要调用工具,则继续循环

这里有几个关键点需要注意:

  1. 上下文管理:所有用户输入、模型回复、工具调用和工具结果都必须保存在 histories 列表中,确保模型在后续的每一次调用中都能理解完整的上下文。
  2. 循环与重试:交互是一个 do-while 循环。大模型可能会连续多次调用工具(甚至为了修正错误而重试),直到它认为不再需要工具,可以给出最终答案为止。
  3. 成本计算:由于可能发生多次模型调用,会产生多个 Usage 信息。你需要将它们累加,以计算总的 token 消耗和成本。
  4. 流式处理:OpenAI 的工具调用同样支持流式输出。你需要正确地将流式返回的 delta 片段聚合成一个或多个完整的工具调用请求。

完整示例:精确计算与代码纠错

让我们把所有部分串联起来,看看一个完整的、能够工作的例子。

安装 NuGet 包:

  • OpenAI (2.2.0 或更高)
  • ModelContextProtocol.Core (0.3.0-preview.3 或更高)
// --- 完整代码 ---
var api = new OpenAIClient(new ApiKeyCredential(Util.GetPassword("azure-ai-key")), new OpenAIClientOptions
{
Endpoint = new Uri($"https://{Util.GetPassword("azure-ai-resource")}.openai.azure.com/openai/v1?api-version=preview"),
});
var cc = api.GetChatClient("gpt-4.1"); // 1. 初始化 MCP 客户端
IMcpClient mcpClient = await McpClientFactory.CreateAsync(new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://csharp.starworks.cc/mcp"),
})); // 2. 获取工具定义并配置到 OpenAI 客户端
var cco = new ChatCompletionOptions();
await foreach (McpClientTool tool in mcpClient.EnumerateToolsAsync())
{
cco.Tools.Add(ChatTool.CreateFunctionTool(tool.Name, tool.Description, BinaryData.FromString(tool.JsonSchema.GetRawText())));
} var jso = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
var histories = new List<ChatMessage>
{
new SystemChatMessage("你是人工智能助理,请结合已有工具(如果存在)回复用户的需求,如果工具错误,请尽量解决错误并重试"),
new UserChatMessage("1234567除以7654321=?(需要精确到5位小数)")
}; // 3. 开始交互循环
ChatFinishReason? finishReason = null;
do
{
var toolCalls = new Dictionary<int, FunctionArgs>();
await foreach (StreamingChatCompletionUpdate delta in cc.CompleteChatStreamingAsync(histories, cco))
{
foreach (StreamingChatToolCallUpdate tool in delta.ToolCallUpdates)
{
byte[] argsDelta = tool.FunctionArgumentsUpdate.ToArray();
if (toolCalls.TryGetValue(tool.Index, out FunctionArgs? toolCall))
{
toolCall.Args.AddRange(argsDelta);
}
else
{
toolCalls.Add(tool.Index, new FunctionArgs(tool.ToolCallId, tool.FunctionName) { Args = argsDelta.ToList() });
}
} if (delta.FinishReason == ChatFinishReason.ToolCalls)
{
histories.Add(new AssistantChatMessage(toolCalls.Values.Select(x => ChatToolCall.CreateFunctionToolCall(x.Id, x.Name, BinaryData.FromBytes(x.Args.ToArray())))));
foreach (FunctionArgs func in toolCalls.Values)
{
// 调用 MCP 工具执行代码
Console.WriteLine("--- C# Code to Run ---");
Console.WriteLine(JsonSerializer.Deserialize<JsonObject>(func.Args.ToArray())!["code"]!.ToString()); CallToolResult result = await mcpClient.CallToolAsync(func.Name, BinaryData.FromBytes(func.Args.ToArray()).ToObjectFromJson<Dictionary<string, object>>()!); Console.WriteLine("--- Execution Result ---");
Console.WriteLine(result.StructuredContent); // 将结果添加回历史记录
histories.Add(ChatMessage.CreateToolMessage(func.Id, JsonSerializer.Serialize(result.StructuredContent, jso)));
}
} if (delta.ContentUpdate.Count > 0)
{
Console.Write(delta.ContentUpdate[0].Text);
} if (delta.Usage != null)
{
Console.WriteLine($"\n--- Usage: {delta.Usage.TotalTokenCount} tokens ---");
if (finishReason != null) break;
}
finishReason = delta.FinishReason;
}
} while (finishReason == ChatFinishReason.ToolCalls || finishReason == null); public record FunctionArgs(string Id, string Name)
{
public List<byte> Args { get; set; } = [];
}

运行上述代码,你会看到这样的输出:

--- C# Code to Run ---
double result = 1234567.0 / 7654321.0;
return Math.Round(result, 5); --- Execution Result ---
{"kind":"end","result":0.16129,"elapsed":150} --- Usage: 1487 tokens ---
1234567 ÷ 7654321 = 0.16129(精确到小数点后5位)。
--- Usage: 1538 tokens ---

看!大模型首先生成了一段C#代码,然后通过我们的C#运行器执行,得到了精确的结果 0.16129,并最终给出了正确的答案。这个过程涉及两次对大模型的调用,一次用于生成代码,一次用于总结答案,因此产生了两次 Usage 记录。

更多有趣的骚操作

1. 计算真实的SHA256哈希值

如果你直接问 GPT-4.1 “C#” 的SHA256值是什么,它可能会“猜”一个答案:

错误示范(模型猜测)

ecddf76be50b529b129c5602778b0a8ddc52ae688ef31fa8c7c3d776b2115747

这显然不是一个真实计算出的哈希值。但当我们接入C#运行器后,模型会选择编写并执行代码:

--- C# Code to Run ---
using System.Text;
using System.Security.Cryptography; string input = "C#";
using (SHA256 sha256 = SHA256.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = sha256.ComputeHash(inputBytes);
// Convert to hex string
StringBuilder sb = new StringBuilder();
foreach (var b in hashBytes)
sb.Append(b.ToString("x2"));
return sb.ToString();
} --- Execution Result ---
{"kind":"end","result":"040228846ead4a4195145fe089343cb0894d00a9380176a41a8f6c5ee70b4824","elapsed":354} --- Usage: 1563 tokens ---
字符串 "C#" 的 SHA256 哈希值是:
040228846ead4a4195145fe089343cb0894d00a9380176a41a8f6c5ee70b4824
--- Usage: 1664 tokens ---

我们完全有理由相信,这一次,0402...4824 才是通过代码坚实计算出的真实哈希值。

2. 实时网络爬虫:获取博客园头条

这是一个更有挑战性的任务。没有工具的大模型无法访问实时互联网数据,当你问它“今天博客园有哪些头条”时,它只能抱歉地表示无能为力。

但有了C#运行器这个唯一的工具,事情就变得有趣了。我使用了 o3 模型(一个代码能力更强的模型),并向它发出了同样的提问。接下来发生了一系列非常精彩的“自主调试”:

  • 第1次尝试:模型编写了爬虫代码,但使用了 System.Web.HttpUtility,这在 .NET Core 环境中不存在,导致编译错误。
  • 第2次尝试:模型接收到错误反馈,自动修正了代码,改用 System.Net.WebUtility。这次编译通过了,但因为HTML结构定位不准,没有抓到内容。
  • 第3次尝试:模型决定先看看网页原始HTML长什么样,于是写代码获取了前1500个字符。
  • 第4次到第7次尝试:基于对HTML结构的观察,模型不断调整它的正则表达式和字符串定位逻辑,期间还遇到了几次自己写的正则转义错误。每一次失败,它都根据错误信息进行调整。
  • 第8次尝试成功! 模型终于编写出了正确的代码,成功提取了头条标题和链接。
  • 第9次尝试:模型对第8次的结果做了最后的美化和过滤,然后输出。

最终,模型给出了一份格式优美的报告:

今天博客园头条区块显示的最新 4 条内容:

  1. 【编辑推荐】通过抓包,深入揭秘 MCP 协议底层通信(5/17/1090)

    https://www.cnblogs.com/sdcb/p/18995424/mcp-http-insights

  2. 【最多推荐】为大模型 MCP Code Interpreter 而生:C# Runner 开源发布(8/13/537)

    https://www.cnblogs.com/sdcb/p/19003720/csharp-runner-mcp

  3. 【新闻头条】反物质量子比特首次演示(0/1/210)

    https://news.cnblogs.com/n/797655/

  4. 【特别头条】博客园众包:诚征 3D 影像景深延拓实时处理方案(预算 8-15 万)(41/9/5584)

    https://www.cnblogs.com/cmt/p/18948571

(括号内数字依次代表:评论数 / 推荐数 / 阅读数)

这个过程生动地展示了当大模型拥有一个强大的代码执行工具后,它如何像一个真正的程序员一样,通过不断试错、调试和迭代来完成一个复杂的任务。

总结

通过将 C# Runner 接入大语言模型,我们极大地扩展了模型的能力边界。借助 MCP 协议的标准化,这种集成为模型赋予了执行精确计算、访问实时数据、与外部API交互等关键能力,有效地克服了模型的“幻觉”问题。从简单的数学计算到复杂的网络爬虫,我们看到了一个更强大、更可靠的AI应用范式正在形成。


感谢阅读,希望本文对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论。

觉得有用的话,请给我的项目一个 Star 吧:

https://github.com/sdcb/csharp-runner

也欢迎加入 .NET 骚操作 QQ 群:495782587,一起交流 .NET 和 AI 的有趣玩法!

我给 AI 接上了一个 C# 运行器,结果它学会了自己上网、调试代码的更多相关文章

  1. 在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用

    由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET M ...

  2. ZeroMQ接口函数之 :zmq_recv – 从一个socket上接收一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq_recv zmq_recv(3)        ØMQ Manual - ØMQ/4.1.0 Name zmq_r ...

  3. ZeroMQ接口函数之 :zmq_send – 在一个socket上发送一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-send zmq_send(3)              ØMQ Manual - ØMQ/4.1.0 Name ...

  4. ZeroMQ接口函数之 :zmq_recvmsg – 从一个socket上接收一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-recvmsg zmq_recvmsg(3)         ØMQ Manual - ØMQ/4.1.0 Nam ...

  5. ZeroMQ接口函数之 :zmq_sendmsg – 从一个socket上发送一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-sendmsg zmq_sendmsg(3)        ØMQ Manual - ØMQ/4.1.0 Name ...

  6. ZeroMQ接口函数之 :zmq_send_const – 从一个socket上发送一个固定内存数据

    ZeroMQ API 目录 :http://www.cnblogs.com/fengbohello/p/4230135.html ——————————————————————————————————— ...

  7. 我需要在Web上完成一个图片上传的功能

    我需要在Web上完成一个图片上传的功能. 这个页面需要能从手机中选择图片上传. 首先,这个页面是从微信上面触发的,所以修改了微信的的入口地址,增加了身份识别号作为传参. 跳转到页面的时候,页面先检查身 ...

  8. 在 Linux 上配置一个 syslog 服务器

    syslog服务器可以用作一个网络中的日志监控中心,所有能够通过网络来发送日志的设施(包含了Linux或Windows服务器,路由器,交换机以及其他主机)都可以把日志发送给它. 通过设置一个syslo ...

  9. 【网站开发】在新浪SAE上搭建一个博客

    概述 在新浪SAE上搭建一个博客 1.访问新浪SAE站点 http://sae.sina.com.cn/ 2.注册新浪SAE 3.选择应用仓库 4.选择WordPress 5.安装WordPress ...

  10. 使用python在SAE上搭建一个微信应用,使用有道翻译的api进行在线翻译

    1. 准备,先在使用python一步一步搭建微信公众平台(一)中基本实现自动回复的功能后,接着在有道词典上申请一个key,http://fanyi.youdao.com/openapi?path=da ...

随机推荐

  1. centos 7 安装 netcoresdk 和Nginx 并发布netcore

    微软官网的yum安装: 打开linux终端程序 netcore sdk 地址https://dotnet.microsoft.com/download/linux-package-manager/ce ...

  2. JAVA经典算法分析

      算法分析是对一个算法需要多少计算时间和存储空间作定量的分析. 算法(Algorithm)是解题的步骤,可以把算法定义成解一确定类问题的任意一种特殊的方法.在计算机科学中,算法要用计算机算法语言描述 ...

  3. Client-go的四种客户端的简单使用

    Client-go的四种客户端使用 我们知道kubectl是通过命令行交互的方式与Kubernetes API Server进行交互的,Kubernetes还提供了通过编程的方式与Kubernetes ...

  4. 2025年最值得关注的效率神器排行榜,HR和运营人都在偷偷用

    在职场节奏越来越快的当下,效率工具已经成为打工人提升工作表现的"刚需".无论你是HR.运营.市场.产品经理,还是独立工作者,选对一款好用的效率神器,往往能省下大量时间.提升工作成就 ...

  5. Win10环境安装Anaconda(3-2021.05)+Tensorflow(2.6)

    Win10环境安装Anaconda(3-2021.05)+Tensorflow(2.6) ​ 在学习机器学习的过程中会用到许多Python库,例如tensorflow.pandas等,用到的时候单独去 ...

  6. 数栈干货放送!babel-plugin-import最全源码详解

    ​ 本文将带领大家解析babel-plugin-import 实现按需加载的完整流程,解开业界所认可 babel 插件的面纱. 首先供上babel-plugin-import插件 一.初见萌芽 首先 ...

  7. 「CF798E」 Mike and code of a permutation

    \(O(n^2)\)做法 让第\(i\)个点向\(p_j(p_j>p_i)\)的点连边 首先\(i\)肯定能连向\(a_i\),若当\(a_i==-1\),那么当前所有没打过标记的点向\(i\) ...

  8. Spring Boot 集成 ShardingSphere-JDBC 配置示例

    概述 Apache ShardingSphere‐JDBC 旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库. 关系型数据库当今依然占有巨大市场份额,是 ...

  9. Golang基础笔记六之流程控制

    本文首发于公众号:Hunter后端 原文链接:Golang基础笔记六之流程控制 本篇笔记介绍 Golang 里流程控制相关的一些语法,以下是本篇笔记目录: 条件语句 循环语句 1.条件语句 1. if ...

  10. SQL Server 删除重复行,查询重复,多记录匹配保留最小行(如何删除 SQL Server 表中的重复行)

    https://support.microsoft.com/zh-cn/topic/%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4-sql-server-%E8%A1%A8% ...