开篇引子:二十年的技术变革
从当年的 Delphi、VB 到后来的 Java、C#,从微软的 .NET Framework 1.0 一路用到现在的 .NET 10,当年为了一个 HTTP 请求还要自己写 socket,现在呢?一个HttpClient搞定一切。我以为这辈子已经没什么新技术能让我激动了。
结果呢?AI 来了。
说实话,2022 年底 ChatGPT 刚出来的时候,我跟很多人一样,觉得这就是个更智能一点的搜索引擎嘛,能有多少花样?结果用了之后发现,这玩意儿是真的能"听懂人话"啊!让它写代码,它能给你写出像模像样的代码;让它解释 Bug,它能给你分析得头头是道。
作为一个老 C# 程序员,我当然想:这能不能跟我的 .NET 项目结合起来?能不能让我的程序也能调用 AI?
答案是:当然可以!而且比想象中简单得多。
这篇文章呢,就是我这几年来踩过的坑、总结的经验,手把手教你怎么用 C# 调用 OpenAI API。甭管你是刚入行的小年轻,还是跟我一样写了十几年代码的老油条,保准你能看懂、能上手。
磨刀不误砍柴工:环境准备那些事儿
API Key 怎么搞?
首先,你得有个 OpenAI 的 API Key。这玩意儿怎么获取呢?
- 打开 https://platform.openai.com/ 注册账号
- 进入 API 密钥管理页面
- 点击 “Create new secret key”
特别注意:这个 Key 只显示一次!一定要保存好,刷新页面就看不到了。我当年第一次就忘了保存,只能重新创建,浪费了好几分钟。
保存好之后,有两种方式配置到你的项目中:
方式一:环境变量(推荐)
# Linux/macOSexportOPENAI_API_KEY="sk-xxx"# Windows PowerShell$env:OPENAI_API_KEY="sk-xxx"方式二:User Secrets(ASP.NET Core 项目)
dotnet user-secretsset"OPENAI_API_KEY""sk-xxx"反正记住一点:千万别把 API Key 硬编码到代码里!我知道有些人为了图方便,直接在代码里写apiKey: "sk-xxx",这等于把你的信用卡密码贴在脑门上——分分钟被人搬空余额。
NuGet 包选哪个?
现在 C# 调用 OpenAI 有两个主流选择:
官方 SDK:包名OpenAI,版本2.11.0
dotnetaddpackage OpenAI这是 OpenAI 官方出的,基于 OpenAPI 规范自动生成,品质有保障。最新版本支持 .NET Standard 2.0+,也就是说你用 .NET Framework 4.6.1+、.NET Core 2.0+、.NET 5+ 都可以。
社区经典:包名Betalgo.Ranul.OpenAI,版本9.2.6
dotnetaddpackage Betalgo.Ranul.OpenAI这个是社区老牌库了,原名叫Betalgo.OpenAI,后来因为商标问题改的名。功能很全,文档也丰富,很多老项目都在用。
我的建议是:新项目直接用官方 SDK。毕竟官方出品,兼容性更好,有什么 API 变更也会第一时间跟进。老项目如果已经用习惯了,也不是不能继续用。
.NET 版本选择
官方示例用的是 .NET 10,但你用 .NET 8 也完全没问题。.NET 6+ 我都测过,能跑。
第一次亲密接触:Hello World 级别的调用
环境准备好了,接下来就是见证奇迹的时刻。
我们先来实现最基础的功能:调用 AI,让它回答一个问题。
同步调用
usingOpenAI.Chat;usingSystem;ChatClientclient=new(model:"gpt-4o",apiKey:Environment.GetEnvironmentVariable("OPENAI_API_KEY"));ChatCompletioncompletion=client.CompleteChat("Say 'this is a test.'");Console.WriteLine($"[ASSISTANT]:{completion.Content[0].Text}");就这么几行代码,AI 就回答了!
让我解释一下这几个关键点:
ChatClient:这是 SDK 的核心类,相当于你跟 OpenAI 服务器之间的"接线员"model:指定用哪个模型。目前主流是gpt-4o(旗舰多模态模型,上下文窗口 128K),如果想省钱可以用gpt-4o-mini,如果想用最新的可以用gpt-5.5CompleteChat:同步方法,会一直等待服务器返回结果才继续执行
异步调用
实际项目中我更推荐用异步:
usingOpenAI.Chat;usingSystem;usingSystem.Threading.Tasks;ChatClientclient=new(model:"gpt-4o",apiKey:Environment.GetEnvironmentVariable("OPENAI_API_KEY"));ChatCompletioncompletion=awaitclient.CompleteChatAsync("Say 'this is a test.'");Console.WriteLine($"[ASSISTANT]:{completion.Content[0].Text}");就多了个await,但这在 ASP.NET Core 或者桌面应用中区别可大了去了——界面不会卡死,服务器不会阻塞,线程池不会被占满。
传递对话历史
上面那个例子每次都像在问一个陌生人,没有上下文。如果你想要有上下文的对话(比如多轮对话),你需要维护一个消息列表:
List<ChatMessage>messages=new(){newSystemChatMessage("你是一个乐于助人的助手。"),newUserChatMessage("什么是 C#?"),newAssistantChatMessage("C# 是一种由微软开发的现代、面向对象的编程语言..."),newUserChatMessage("它跟 Java 有什么区别?")};ChatCompletioncompletion=awaitclient.CompleteChatAsync(messages);注意这里的消息类型:
SystemChatMessage:系统提示,告诉 AI 它的角色定位UserChatMessage:用户的问题AssistantChatMessage:AI 的回答
这样 AI 就知道之前聊过什么,回答会更加连贯。
让 AI 学会"说话":流式响应实战
为什么要流式?
不知道你们有没有这种感觉:等 AI 生成一段长文本的时候,那个加载圈转啊转的,等待的那几秒钟简直就是煎熬。
特别是做一个聊天机器人,用户打出一句话,然后看着光标愣愣地等着 AI 一点一点地把字打出来——这体验,简直了!
所以后来有了流式响应(Streaming)这个玩意儿。AI 生成一段就返回一段,像打字机一样,一个字一个字地蹦出来。用户能看到 AI 正在"思考",等待感大大降低。
流式调用实现
官方 SDK 已经封装好了流式接口,我们来看看怎么用:
usingOpenAI.Chat;usingSystem;usingSystem.ClientModel;ChatClientclient=new(model:"gpt-4o",apiKey:Environment.GetEnvironmentVariable("OPENAI_API_KEY"));CollectionResult<StreamingChatCompletionUpdate>completionUpdates=client.CompleteChatStreaming("Say 'this is a test.'");Console.Write("[ASSISTANT]: ");foreach(StreamingChatCompletionUpdateupdateincompletionUpdates){if(update.ContentUpdate.Count>0){Console.Write(update.ContentUpdate[0].Text);}}看到了吗?核心区别就是:
- 同步方法:
CompleteChat→ 返回ChatCompletion - 流式方法:
CompleteChatStreaming→ 返回StreamingChatCompletionUpdate的集合
每个update包含的是增量内容,也就是新生成的那几个字。遍历这个集合,一个字一个字地打印出来,打字机效果就出来了。
异步流式
同样支持异步版本:
usingOpenAI.Chat;usingSystem;usingSystem.ClientModel;usingSystem.Threading.Tasks;ChatClientclient=new(model:"gpt-4o",apiKey:Environment.GetEnvironmentVariable("OPENAI_API_KEY"));AsyncCollectionResult<StreamingChatCompletionUpdate>completionUpdates=client.CompleteChatStreamingAsync("Say 'this is a test.'");Console.Write("[ASSISTANT]: ");awaitforeach(StreamingChatCompletionUpdateupdateincompletionUpdates){if(update.ContentUpdate.Count>0){Console.Write(update.ContentUpdate[0].Text);}}实际项目中,Web API 返回流式响应会用到Server-Sent Events (SSE),不过那是另一个话题了。原理是一样的:AI 生成一点,前端就显示一点。
Function Calling 实战:让 AI 调用你的 C# 方法
好了,现在 AI 能回答问题了,也能流式打字了。但这些都还是"纸上谈兵"——AI 只能从它的训练数据里获取信息,无法访问实时数据,也无法执行具体操作。
Function Calling(函数调用)就是为了解决这个问题而生的。它让 AI 能够:
- 识别什么时候需要调用外部工具
- 提取用户请求中的参数
- 把参数传给你的 C# 方法
- 把方法的返回值告诉 AI
- AI 根据返回值生成最终回答
说白了,这就是让 AI 变成了你程序的"大脑",能够指挥你的代码去干活。
天气查询实战
我们来做个完整的例子:让 AI 帮你查天气。
首先,定义两个 C# 方法:
privatestaticstringGetCurrentLocation(){return"San Francisco";}privatestaticstringGetCurrentWeather(stringlocation,stringunit="celsius"){return$"31{unit}";}这只是模拟方法,实际上你可以调用天气 API 获取真实数据。
然后,定义工具(Tool):
privatestaticreadonlyChatToolgetCurrentWeatherTool=ChatTool.CreateFunctionTool(functionName:nameof(GetCurrentWeather),functionDescription:"Get the current weather in a given location",functionParameters:BinaryData.FromBytes("""{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. Boston, MA"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"The temperature unit to use."}},"required":["location"]}"""u8.ToArray()));privatestaticreadonlyChatToolgetCurrentLocationTool=ChatTool.CreateFunctionTool(functionName:nameof(GetCurrentLocation),functionDescription:"Get the user's current location");这里的 JSON Schema 就是告诉 AI:这两个函数接受什么参数、参数的类型是什么、哪些是必填的。
接下来是调用逻辑的核心部分:
List<ChatMessage>messages=[newUserChatMessage("What's the weather like today?")];ChatCompletionOptionsoptions=new(){Tools={getCurrentLocationTool,getCurrentWeatherTool}};boolrequiresAction;do{requiresAction=false;ChatCompletioncompletion=client.CompleteChat(messages,options);switch(completion.FinishReason){caseChatFinishReason.Stop:// AI 正常回答,不需要调用工具messages.Add(newAssistantChatMessage(completion));break;caseChatFinishReason.ToolCalls:// AI 要求调用工具messages.Add(newAssistantChatMessage(completion));foreach(ChatToolCalltoolCallincompletion.ToolCalls){switch(toolCall.FunctionName){casenameof(GetCurrentWeather):usingJsonDocumentargsJson=JsonDocument.Parse(toolCall.FunctionArguments);boolhasLocation=argsJson.RootElement.TryGetProperty("location",outJsonElementlocation);boolhasUnit=argsJson.RootElement.TryGetProperty("unit",outJsonElementunit);stringresult=hasUnit?GetCurrentWeather(location.GetString(),unit.GetString()):GetCurrentWeather(location.GetString());messages.Add(newToolChatMessage(toolCall.Id,result));break;casenameof(GetCurrentLocation):stringlocationResult=GetCurrentLocation();messages.Add(newToolChatMessage(toolCall.Id,locationResult));break;}}requiresAction=true;// 继续循环,让 AI 根据工具返回值生成回答break;}}while(requiresAction);这段代码看起来有点长,但逻辑其实很清楚:
- 发送用户问题和工具定义给 AI
- AI 如果说"我需要查天气",
FinishReason会是ToolCalls - 解析 AI 传来的参数,调用你的 C# 方法
- 把方法的返回值包装成
ToolChatMessage发送回去 - AI 根据返回值生成最终的人话回答
- 循环直到 AI 回答完毕,不需要再调用工具
这就是 AI Agent 的基本原理!是不是挺神奇的?
流式 Function Calling
如果你想要流式效果,处理逻辑稍微复杂一点,需要用StreamingChatToolCallsBuilder来累加工具调用增量:
StreamingChatToolCallsBuildertoolCallsBuilder=new();foreach(StreamingChatCompletionUpdateupdateinclient.CompleteChatStreaming(messages,options)){foreach(StreamingChatToolCallUpdatetoolCallUpdateinupdate.ToolCallUpdates){toolCallsBuilder.Append(toolCallUpdate);}if(update.FinishReason==ChatFinishReason.ToolCalls){IReadOnlyList<ChatToolCall>toolCalls=toolCallsBuilder.Build();// 同样处理工具调用...}}原理是一样的,就是把增量信息攒起来,攒成一个完整的工具调用再处理。
那些年踩过的坑:血泪经验总结
写代码二十年,踩过的坑比吃过的盐还多。调用 OpenAI API 这几年,我,总结了几条血泪经验,你们可得记好了。
Token 限制:别让 AI 撑坏了
OpenAI 的模型有上下文窗口限制,比如gpt-4o是 128K tokens,gpt-5已经到了 400K。听起来很多是不是?但你要是聊着聊着把整个对话历史都塞进去,很快就会超过限制。
经验之谈:
- 中文大约 1 token ≈ 1.5-2 个汉字
- 英文大约 1 token ≈ 0.75 个单词
- 用 OpenAI Tokenizer 在线计算
怎么解决?两种思路:
- 截断:只保留最近 N 轮对话,旧的丢掉
- 摘要:用另一个 AI 把历史对话压缩成摘要
错误处理:别让程序崩了
API 调用失败的场景太多了:网络抖动、服务器繁忙、API Key 过期、模型不存在……你永远不知道什么时候会出问题。
我的建议是:一定要加 try-catch,并且对不同错误码做不同处理:
try{varcompletion=awaitclient.CompleteChatAsync(messages);}catch(ClientResultExceptionex){switch(ex.StatusCode){case401:// API Key 无效Console.WriteLine("检查一下你的 API Key 是不是过期了");break;case429:// 速率超限// 等待一段时间后重试awaitTask.Delay(5000);break;case500:// 服务器错误Console.WriteLine("OpenAI 服务器抽风了,等会儿再试");break;default:Console.WriteLine($"出错了:{ex.Message}");break;}}官方 SDK 的异常继承自ClientResultException,里面包含状态码和错误信息,好好利用。
速率限制:别被封号
OpenAI 和 Azure OpenAI 都有速率限制(Rate Limits)。以 GPT-4o GlobalStandard 为例,每分钟请求数(RPM)限制是 300,每分钟 Token 数(TPM)限制是 300,000。
超过限制怎么办?服务器会返回429 Too Many Requests。
处理策略:
- 官方 SDK 内置自动重试机制,会自动处理
- 自己实现的话,用指数退避:等 1 秒、2 秒、4 秒……慢慢重试
- 看看响应头里的
Retry-After,那是服务器建议你等待的时间
成本控制:别让余额归零
这个必须重点强调!OpenAI 是按 Token 收费的,最新的 GPT-5.5 是 $5.00/M 输入 tokens、$30.00/M 输出 tokens。
我见过太多人,写demo的时候用gpt-4o,跑都没跑就直接跑了十几个小时,等发现的时候账单已经几百美元了。
省钱建议:
- 正式项目用
gpt-4o-mini,便宜量大管饱 - 减少不必要的
max_tokens设置,别让 AI 生成的太长 - 开启缓存(CacheGating)可以节省成本
- 定期查看 Usage 页面,了解消耗情况
代码考古时间:手动 HTTP vs 官方 SDK
有些老派程序员不喜欢用 SDK,觉得封装得太厚,看不到底层原理。那我们来看看如果不用 SDK,怎么直接调 HTTP 接口。
手动 HTTP 调用
usingSystem.Net.Http;usingSystem.Text;usingSystem.Text.Json;varclient=newHttpClient();client.DefaultRequestHeaders.Add("Authorization",$"Bearer{apiKey}");varrequestBody=new{model="gpt-4o",messages=new[]{new{role="user",content="Hello!"}},stream=false};varresponse=awaitclient.PostAsync("https://api.openai.com/v1/chat/completions",newStringContent(JsonSerializer.Serialize(requestBody),Encoding.UTF8,"application/json"));varresponseBody=awaitresponse.Content.ReadAsStringAsync();Console.WriteLine(responseBody);这代码看起来也不复杂是不是?但是:
- 你需要自己处理 JSON 序列化
- 流式响应要自己解析 SSE
- 错误处理要自己写
- Function Calling 的工具调用要自己解析 JSON Schema
- 每次 API 升级你可能都要改代码
而官方 SDK 呢?一个CompleteChat()搞定一切。
我的观点是:除非有特殊原因,否则能用 SDK 就用 SDK。省心省力,代码还更干净。人家微软和 OpenAI 合作写的 SDK,性能和兼容性都经过测试,比自己造的轮子强多了。
当然如果你用的是一些非标准的 OpenAI 兼容服务(比如本地部署的 LLM),那可能还是得自己写 HTTP 调用。
收工寄语:AI 开发的未来
写到这里,也该收工了。
回想这二十年,从当年的 ASP、JSP,到后来的 ASP.NET MVC、Entity Framework,再到现在的 AI 集成,不得不说咱们这行变化是真快。但不管技术怎么变,核心逻辑是一样的:用代码解决实际问题。
AI API 不是什么玄学,就是一种新的工具。学会了用它,你的程序就能"听懂人话",能帮你查资料、能帮你写代码、能帮你处理各种繁琐的任务。
这篇文章从环境配置讲到流式响应,再到 Function Calling,基本涵盖了 C# 调用 OpenAI API 的主要场景。希望能帮到你们。
至于未来会怎样?说实话我也看不清。AI 的发展速度已经超出我的预期了——去年还在用 GPT-4,今年 GPT-5 都出来了,还有什么 o1、o3 推理模型,以后会变成什么样,谁知道呢?
但有一点是肯定的:持续学习,别让自己落伍。