.NET Core应用集成SmallThinker-3B-Preview:C#调用AI模型服务全解析
.NET Core应用集成SmallThinker-3B-Preview:C#调用AI模型服务全解析
最近在做一个内容审核的后台服务,需要处理大量用户生成的文本,判断其中是否包含不当信息。手动审核肯定不现实,于是就想到了接入一个AI模型来帮忙。SmallThinker-3B-Preview这个模型在文本理解和分类任务上表现不错,而且提供了标准的HTTP API,正好可以用.NET Core来调用。
如果你也在用.NET技术栈,想把类似的能力集成到自己的Web应用或者后台服务里,那这篇文章就是为你准备的。我会带你走一遍完整的流程,从创建一个干净的Web API项目开始,到如何优雅、可靠地调用外部AI服务,最后还会聊聊怎么处理批量任务和应对网络不稳定的情况。整个过程用的都是.NET生态里那些成熟、好用的库,代码写起来会很顺手。
1. 项目准备与环境搭建
咱们先从创建一个新项目说起。打开你的终端或者Visual Studio,准备开始。
1.1 创建ASP.NET Core Web API项目
我习惯用命令行来创建项目,感觉更清晰一些。打开终端,导航到你存放代码的目录,然后运行下面这条命令:
dotnet new webapi -n AITextProcessor --framework net8.0这条命令会创建一个名叫AITextProcessor的新项目,并且指定使用.NET 8。.NET 8是长期支持版本,用起来比较放心。创建完成后,用你喜欢的IDE(比如Visual Studio 2022、Rider或者VS Code)打开这个项目。
1.2 添加必要的NuGet包
我们的项目需要和HTTP API打交道,还要处理JSON数据,所以得引入几个关键的包。你可以通过IDE的包管理器,或者直接在项目文件(.csproj)里添加,也可以运行下面的命令:
dotnet add package Microsoft.Extensions.Http dotnet add package System.Text.Json dotnet add package Polly dotnet add package Polly.Extensions.Http我来简单说一下这几个包是干什么的:
Microsoft.Extensions.Http:这是.NET Core内置的,用来创建和管理HttpClient实例,非常重要。System.Text.Json:同样是内置的,但我们需要确保它被引用,用来序列化和反序列化JSON数据,也就是把C#对象变成JSON字符串,或者反过来。Polly和Polly.Extensions.Http:这两个是“神器”,专门用来处理调用外部服务时可能出现的各种问题,比如网络短暂失败、服务暂时不可用等,能让我们的应用更健壮。
1.3 准备模型服务的配置信息
在调用SmallThinker-3B-Preview的API之前,你得先有它的访问地址和密钥(如果需要的话)。通常这类服务会提供一个基础URL和一个API Key。我们把这些配置信息放在appsettings.json文件里,这样以后修改起来也方便。
打开appsettings.json文件,添加一个配置节:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "SmallThinkerConfig": { "BaseUrl": "https://api.example-ai-service.com/v1/", // 替换为实际的API地址 "ApiKey": "your-api-key-here", // 替换为你的API密钥 "TimeoutSeconds": 30, "MaxRetryAttempts": 3 }, "AllowedHosts": "*" }这里我定义了一个SmallThinkerConfig节点,里面放了基础地址、密钥、超时时间和重试次数。记得把注释里的占位符换成你实际的信息。
2. 核心服务层设计与实现
配置好了,接下来我们就要写代码了。一个好的做法是把调用AI服务的逻辑封装成一个独立的服务类,这样业务代码用起来干净,以后想换模型或者改调用方式也容易。
2.1 定义配置模型和API请求/响应模型
首先,我们创建几个类来对应配置和API的数据结构。在项目里新建一个Models文件夹。
第一个是配置类,对应appsettings.json里的内容:
// Models/SmallThinkerOptions.cs namespace AITextProcessor.Models; public class SmallThinkerOptions { public const string SectionName = "SmallThinkerConfig"; public string BaseUrl { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; public int TimeoutSeconds { get; set; } = 30; public int MaxRetryAttempts { get; set; } = 3; }然后,定义调用AI模型API时,我们发送的请求数据和它返回的响应数据。这需要根据SmallThinker-3B-Preview API的实际文档来定。假设它的文本生成端点接收一个prompt字符串,返回一个包含generated_text的JSON。
// Models/TextGenerationRequest.cs using System.Text.Json.Serialization; namespace AITextProcessor.Models; public class TextGenerationRequest { [JsonPropertyName("prompt")] public string Prompt { get; set; } = string.Empty; // 你可以根据API文档添加更多参数,例如: // [JsonPropertyName("max_tokens")] // public int MaxTokens { get; set; } = 100; }// Models/TextGenerationResponse.cs using System.Text.Json.Serialization; namespace AITextProcessor.Models; public class TextGenerationResponse { [JsonPropertyName("generated_text")] public string GeneratedText { get; set; } = string.Empty; [JsonPropertyName("model")] public string Model { get; set; } = string.Empty; // 可能还有其他字段,如耗时、token数量等 }JsonPropertyName这个特性(Attribute)来自System.Text.Json,它告诉序列化器,C#属性GeneratedText对应JSON里的字段名generated_text。这样命名风格就统一了。
2.2 实现AI模型服务客户端
这是最核心的部分,我们创建一个服务类来封装所有HTTP调用细节。
// Services/SmallThinkerService.cs using System.Net.Http.Headers; using System.Text; using System.Text.Json; using AITextProcessor.Models; using Microsoft.Extensions.Options; namespace AITextProcessor.Services; public interface ISmallThinkerService { Task<string> GenerateTextAsync(string prompt, CancellationToken cancellationToken = default); } public class SmallThinkerService : ISmallThinkerService { private readonly HttpClient _httpClient; private readonly SmallThinkerOptions _options; private readonly ILogger<SmallThinkerService> _logger; private readonly JsonSerializerOptions _jsonOptions; public SmallThinkerService( HttpClient httpClient, IOptions<SmallThinkerOptions> options, ILogger<SmallThinkerService> logger) { _httpClient = httpClient; _options = options.Value; _logger = logger; // 配置JSON序列化选项,比如不区分大小写 _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; // 设置HttpClient的基础地址和默认请求头 _httpClient.BaseAddress = new Uri(_options.BaseUrl); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (!string.IsNullOrWhiteSpace(_options.ApiKey)) { // 假设API使用Bearer Token认证,根据实际情况调整 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey); } _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); } public async Task<string> GenerateTextAsync(string prompt, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(prompt)) { throw new ArgumentException("Prompt cannot be null or empty.", nameof(prompt)); } var requestBody = new TextGenerationRequest { Prompt = prompt }; var jsonContent = JsonSerializer.Serialize(requestBody, _jsonOptions); var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); _logger.LogInformation("Sending request to SmallThinker API with prompt: {Prompt}", prompt); try { // 这里假设API端点路径是 /generate,请根据实际文档修改 var response = await _httpClient.PostAsync("generate", httpContent, cancellationToken); response.EnsureSuccessStatusCode(); // 如果状态码不是2xx,会抛出异常 var responseString = await response.Content.ReadAsStringAsync(cancellationToken); var result = JsonSerializer.Deserialize<TextGenerationResponse>(responseString, _jsonOptions); _logger.LogInformation("Successfully received response from SmallThinker API."); return result?.GeneratedText ?? string.Empty; } catch (HttpRequestException ex) { _logger.LogError(ex, "HTTP request failed when calling SmallThinker API."); throw; // 或者返回一个默认值/错误信息,根据业务需求决定 } catch (JsonException ex) { _logger.LogError(ex, "Failed to deserialize the response from SmallThinker API."); throw; } catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { _logger.LogInformation("The request to SmallThinker API was cancelled."); throw; } catch (TaskCanceledException ex) { // 这很可能是超时引起的 _logger.LogError(ex, "The request to SmallThinker API timed out."); throw new TimeoutException("Request to AI service timed out.", ex); } } }这个服务类做了几件关键的事情:
- 依赖注入:通过构造函数接收配置好的
HttpClient、配置选项和日志器。 - 初始化配置:在构造函数里设置
HttpClient的基础地址、认证头和超时时间。 - 核心方法:
GenerateTextAsync方法接收一个提示词,构造请求体,发送POST请求,然后解析响应。 - 错误处理:用
try-catch块包裹核心调用,对网络错误、JSON解析错误、超时等常见问题进行了记录和处理。 - 日志记录:关键步骤都打了日志,出问题时方便排查。
2.3 配置依赖注入和HTTP客户端
服务类写好了,怎么让它在整个应用里可用呢?我们需要在Program.cs里进行配置。
打开Program.cs文件,添加以下代码:
// Program.cs using AITextProcessor.Models; using AITextProcessor.Services; using Polly; using Polly.Extensions.Http; var builder = WebApplication.CreateBuilder(args); // 添加服务到容器 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 1. 配置选项模式,绑定 appsettings.json 中的配置 builder.Services.Configure<SmallThinkerOptions>( builder.Configuration.GetSection(SmallThinkerOptions.SectionName)); // 2. 配置一个命名的HttpClient,并应用Polly重试策略 builder.Services.AddHttpClient<ISmallThinkerService, SmallThinkerService>(client => { // 基础配置在SmallThinkerService构造函数中已完成,这里可以放一些通用配置 }) .AddPolicyHandler(GetRetryPolicy(builder.Configuration)); // 添加重试策略 // 3. 注册我们自己的服务 builder.Services.AddScoped<ISmallThinkerService, SmallThinkerService>(); // 如果需要后台处理,可以添加HostedService // builder.Services.AddHostedService<TextProcessingBackgroundService>(); var app = builder.Build(); // 配置HTTP请求管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); // 定义Polly重试策略的方法 static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(IConfiguration configuration) { var options = configuration.GetSection(SmallThinkerOptions.SectionName).Get<SmallThinkerOptions>(); var maxRetryAttempts = options?.MaxRetryAttempts ?? 3; // 定义一个策略:针对网络错误(5xx状态码,请求超时等)进行重试 // 等待时间呈指数级增长(第一次等2秒,第二次等4秒...) return HttpPolicyExtensions .HandleTransientHttpError() // 处理5xx和408(请求超时) .Or<TaskCanceledException>() // 处理超时异常 .WaitAndRetryAsync( retryCount: maxRetryAttempts, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (outcome, timespan, retryAttempt, context) => { // 重试时记录日志 var logger = context.GetLogger(); logger?.LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt); }); }这段代码是集成的关键:
Configure<SmallThinkerOptions>:把配置文件里的内容绑定到我们的SmallThinkerOptions类。AddHttpClient<ISmallThinkerService, SmallThinkerService>:这是精髓。它做了两件事:1) 注册了一个专门给SmallThinkerService用的HttpClient;2) 将这个HttpClient的生命周期与SmallThinkerService关联起来。.AddPolicyHandler(GetRetryPolicy(...))则为这个HttpClient挂载了我们用Polly定义的重试策略。AddScoped<ISmallThinkerService, SmallThinkerService>:注册服务接口和实现,这样在控制器里就可以通过构造函数注入了。
Polly策略GetRetryPolicy定义了当遇到暂时性故障(如网络波动、服务端返回5xx错误)时,自动进行重试,并且重试间隔会逐渐变长(指数退避),避免对下游服务造成压力。
3. 在控制器中使用AI服务
服务层和配置都搞定了,现在让我们在Web API的控制器里用它。假设我们有一个处理文本分析的控制器。
// Controllers/TextAnalysisController.cs using AITextProcessor.Services; using Microsoft.AspNetCore.Mvc; namespace AITextProcessor.Controllers; [ApiController] [Route("api/[controller]")] public class TextAnalysisController : ControllerBase { private readonly ISmallThinkerService _aiService; private readonly ILogger<TextAnalysisController> _logger; public TextAnalysisController( ISmallThinkerService aiService, ILogger<TextAnalysisController> logger) { _aiService = aiService; _logger = logger; } [HttpPost("generate")] public async Task<IActionResult> GenerateText([FromBody] GenerateRequest request) { if (request == null || string.IsNullOrWhiteSpace(request.Prompt)) { return BadRequest("Prompt is required."); } try { _logger.LogInformation("Received request to generate text for prompt: {Prompt}", request.Prompt); var generatedText = await _aiService.GenerateTextAsync(request.Prompt); return Ok(new { generatedText }); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while generating text."); // 根据异常类型返回更精确的错误信息 return StatusCode(500, "An error occurred while processing your request."); } } // 可以添加更多端点,例如批量处理、内容审核等 // [HttpPost("batch")] // public async Task<IActionResult> ProcessBatch([FromBody] BatchProcessRequest request) { ... } } // 请求模型 public class GenerateRequest { public string Prompt { get; set; } = string.Empty; }这个控制器很简单,它暴露了一个POST /api/TextAnalysis/generate的端点。当收到请求时,它调用我们之前写好的SmallThinkerService,然后将AI生成的结果返回给客户端。所有的HTTP通信、重试、错误处理逻辑都被封装在服务层,控制器这里看起来很清爽。
4. 进阶:后台任务与批量处理
很多时候,我们可能需要异步地、或者批量地处理文本,而不是实时响应一个HTTP请求。比如,每天晚上定时处理当天积累的用户评论。这时候,后台任务就派上用场了。
.NET Core提供了BackgroundService基类,可以很方便地创建长时间运行的后台服务。我们来创建一个后台服务,从数据库(或队列)里获取待处理的文本,调用AI服务处理,然后保存结果。
首先,假设我们有一个简单的仓储接口和模型:
// Models/TextItem.cs namespace AITextProcessor.Models; public class TextItem { public int Id { get; set; } public string OriginalContent { get; set; } = string.Empty; public string? ProcessedResult { get; set; } public DateTime CreatedAt { get; set; } public DateTime? ProcessedAt { get; set; } public string Status { get; set; } = "Pending"; // Pending, Processing, Completed, Failed } // Services/ITextItemRepository.cs namespace AITextProcessor.Services; public interface ITextItemRepository { Task<IEnumerable<TextItem>> GetPendingItemsAsync(int batchSize, CancellationToken ct); Task UpdateItemAsync(TextItem item, CancellationToken ct); }然后,实现后台服务:
// Services/TextProcessingBackgroundService.cs using AITextProcessor.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace AITextProcessor.Services; public class TextProcessingBackgroundService : BackgroundService { private readonly ILogger<TextProcessingBackgroundService> _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly int _batchSize = 10; // 每次处理的批次大小 private readonly TimeSpan _processingInterval = TimeSpan.FromMinutes(5); // 处理间隔 public TextProcessingBackgroundService( ILogger<TextProcessingBackgroundService> logger, IServiceScopeFactory scopeFactory) { _logger = logger; _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Text Processing Background Service is starting."); // 延迟启动,等待主机构建完成 await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { _logger.LogDebug("Starting a new processing cycle."); await ProcessBatchAsync(stoppingToken); } catch (Exception ex) when (ex is not OperationCanceledException) { // 捕获非取消异常,记录日志,避免后台服务崩溃 _logger.LogError(ex, "An error occurred during batch processing."); } // 等待一段时间后再次执行 _logger.LogDebug("Waiting for next processing cycle."); await Task.Delay(_processingInterval, stoppingToken); } _logger.LogInformation("Text Processing Background Service is stopping."); } private async Task ProcessBatchAsync(CancellationToken stoppingToken) { // 重要:在后台服务中,对于Scoped服务(如DbContext、我们的AIService), // 需要创建独立的作用域(Scope)。 using var scope = _scopeFactory.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService<ITextItemRepository>(); var aiService = scope.ServiceProvider.GetRequiredService<ISmallThinkerService>(); var pendingItems = await repository.GetPendingItemsAsync(_batchSize, stoppingToken); if (!pendingItems.Any()) { _logger.LogDebug("No pending items to process."); return; } _logger.LogInformation("Processing {Count} pending text items.", pendingItems.Count()); foreach (var item in pendingItems) { if (stoppingToken.IsCancellationRequested) { break; } try { item.Status = "Processing"; await repository.UpdateItemAsync(item, stoppingToken); // 调用AI服务处理文本 // 这里可以根据业务需求构造不同的prompt var prompt = $"请分析以下文本内容:{item.OriginalContent}"; var result = await aiService.GenerateTextAsync(prompt, stoppingToken); item.ProcessedResult = result; item.Status = "Completed"; item.ProcessedAt = DateTime.UtcNow; await repository.UpdateItemAsync(item, stoppingToken); _logger.LogDebug("Successfully processed item {ItemId}.", item.Id); } catch (Exception ex) { _logger.LogError(ex, "Failed to process item {ItemId}.", item.Id); item.Status = "Failed"; // 可以选择记录错误信息到 item.ProcessedResult await repository.UpdateItemAsync(item, stoppingToken); } // 可选:在处理每个项目之间添加短暂延迟,避免对AI服务造成过大压力 await Task.Delay(TimeSpan.FromMilliseconds(200), stoppingToken); } } }这个后台服务会每隔5分钟运行一次,每次从仓储中获取一批状态为“Pending”的文本项,然后逐个调用AI服务进行处理,并更新处理状态和结果。使用IServiceScopeFactory来正确解析具有Scoped生命周期的服务(如数据库上下文和我们的AI服务)是关键。
最后,别忘了在Program.cs中取消注释或添加这行代码来注册这个后台服务:
builder.Services.AddHostedService<TextProcessingBackgroundService>();5. 总结
走完这一整套流程,一个能在.NET Core应用里集成SmallThinker-3B-Preview模型服务的架子就搭起来了。回顾一下,我们主要做了这几件事:
首先是项目的基础搭建,引入了必要的NuGet包,并把API配置信息放到了appsettings.json里,这样管理起来方便。然后,我们设计并实现了一个核心的服务类SmallThinkerService,它把调用外部HTTP API的细节,比如构造请求、处理响应、错误处理都封装了起来,业务代码用起来很干净。
为了让这个服务更可靠,我们用了HttpClientFactory来管理HttpClient的生命周期,避免了手动管理带来的资源问题。还通过Polly库添加了重试策略,这样遇到网络波动或者服务端暂时性错误时,应用能自己尝试恢复,提高了整体的韧性。
在控制器里使用这个服务就很简单了,依赖注入进来,直接调用对应的方法就行。如果需求更复杂,比如要处理大量数据,我们还介绍了如何利用BackgroundService来创建后台任务,进行异步的批量处理,这样不会阻塞主请求线程。
整个过程中,像配置绑定、依赖注入、日志记录这些.NET Core的基础设施我们都用上了,代码结构比较清晰,也易于测试和维护。你可以根据SmallThinker-3B-Preview API的具体文档,调整请求和响应的模型,也可以根据业务需求,扩展后台任务的功能,比如从消息队列里消费任务,或者处理更复杂的AI调用逻辑。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
