我給 AI 接上了一個 C# 運行器,結果它學會了自己上網、調試代碼
在昨天的文章中,我們介紹了我的新開源項目:C# Runner。這是一個強大的C#代碼運行器,不僅提供了前端UI,還內建了API和一個MCP服務端。
- GitHub項目地址: https://github.com/sdcb/csharp-runner
- 在線演示地址: https://csharp.starworks.cc
- MCP協議調用地址:
https://csharp.starworks.cc/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); // 如果模型還需要調用工具,則繼續循環
這里有幾個關鍵點需要注意:
- 上下文管理:所有用戶輸入、模型回復、工具調用和工具結果都必須保存在
histories列表中,確保模型在后續的每一次調用中都能理解完整的上下文。 - 循環與重試:交互是一個
do-while循環。大模型可能會連續多次調用工具(甚至為了修正錯誤而重試),直到它認為不再需要工具,可以給出最終答案為止。 - 成本計算:由于可能發生多次模型調用,會產生多個
Usage信息。你需要將它們累加,以計算總的 token 消耗和成本。 - 流式處理: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 條內容:
【編輯推薦】通過抓包,深入揭秘 MCP 協議底層通信(5/17/1090)
http://www.rzrgm.cn/sdcb/p/18995424/mcp-http-insights【最多推薦】為大模型 MCP Code Interpreter 而生:C# Runner 開源發布(8/13/537)
http://www.rzrgm.cn/sdcb/p/19003720/csharp-runner-mcp【新聞頭條】反物質量子比特首次演示(0/1/210)
https://news.cnblogs.com/n/797655/【特別頭條】博客園眾包:誠征 3D 影像景深延拓實時處理方案(預算 8-15 萬)(41/9/5584)
http://www.rzrgm.cn/cmt/p/18948571(括號內數字依次代表:評論數 / 推薦數 / 閱讀數)
這個過程生動地展示了當大模型擁有一個強大的代碼執行工具后,它如何像一個真正的程序員一樣,通過不斷試錯、調試和迭代來完成一個復雜的任務。
總結
通過將 C# Runner 接入大語言模型,我們極大地擴展了模型的能力邊界。借助 MCP 協議的標準化,這種集成為模型賦予了執行精確計算、訪問實時數據、與外部API交互等關鍵能力,有效地克服了模型的“幻覺”問題。從簡單的數學計算到復雜的網絡爬蟲,我們看到了一個更強大、更可靠的AI應用范式正在形成。
感謝閱讀,希望本文對你有所幫助!如果你有任何問題或建議,歡迎在評論區留言討論。
覺得有用的話,請給我的項目一個 Star ? 吧:
https://github.com/sdcb/csharp-runner
也歡迎加入 .NET 騷操作 QQ 群:495782587,一起交流 .NET 和 AI 的有趣玩法!

浙公網安備 33010602011771號