做网站月薪10万wordpress百家号采集
做网站月薪10万,wordpress百家号采集,网站建设合同表(书),番禺网站制作 优帮云Nomic-Embed-Text-V2-MoE for .NET Developers: C# Integration and ASP.NET Core Web API
最近在做一个智能文档检索的项目#xff0c;需要给海量的技术文档和用户反馈做语义搜索。试了几个开源的文本嵌入模型#xff0c;要么效果一般#xff0c;要么对中文支持不好。后来…Nomic-Embed-Text-V2-MoE for .NET Developers: C# Integration and ASP.NET Core Web API最近在做一个智能文档检索的项目需要给海量的技术文档和用户反馈做语义搜索。试了几个开源的文本嵌入模型要么效果一般要么对中文支持不好。后来发现了Nomic-Embed-Text-V2-MoE试了一下效果确实不错尤其是对混合专家架构带来的多语言和长文本理解能力印象深刻。但问题来了我们整个后端都是基于.NET技术栈的怎么把这个Python生态里常见的模型集成到C#项目里呢总不能为了一个模型服务就单独维护一套Python环境吧。经过一番摸索我找到了一套比较优雅的解决方案今天就来分享一下如何在ASP.NET Core Web API里集成Nomic-Embed-Text-V2-MoE实现一个完整的语义搜索服务。1. 方案设计为什么选择HTTP API桥接直接在想用C#里加载PyTorch模型不是不行但太折腾了。TorchSharp虽然提供了.NET绑定但生态和文档都不够完善特别是对于Nomic-Embed-Text-V2-MoE这种比较新的模型很容易踩坑。我选择的方案是模型服务化用一个轻量的Python服务来托管模型然后通过HTTP API暴露给.NET应用。这样有几个好处技术栈分离Python负责模型推理.NET负责业务逻辑各司其职。部署灵活模型服务可以单独部署、扩缩容不影响主应用。维护简单模型升级只需要更新Python服务.NET端几乎不用动。复用性强其他服务比如Java、Go写的也能调用同一个模型服务。整个架构很简单Python FastAPI服务提供/embed接口ASP.NET Core Web API通过HttpClient调用它然后把生成的向量存到数据库里对外提供搜索接口。2. 搭建模型推理服务我们先从Python端开始。这个服务要尽可能轻量只做一件事加载模型把文本变成向量。2.1 创建Python服务项目新建一个目录比如nomic-embed-service然后创建requirements.txtfastapi0.104.1 uvicorn0.24.0 torch2.1.0 transformers4.35.0 sentence-transformers2.2.2 nomic1.0.0接着创建主文件app.pyfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List import torch from sentence_transformers import SentenceTransformer import logging # 配置日志 logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) app FastAPI(titleNomic Embedding Service, version1.0.0) # 请求模型 class EmbeddingRequest(BaseModel): texts: List[str] normalize: bool True # 是否归一化向量 # 响应模型 class EmbeddingResponse(BaseModel): embeddings: List[List[float]] model: str dimensions: int # 全局模型实例 _model None def get_model(): 懒加载模型 global _model if _model is None: logger.info(正在加载 Nomic-Embed-Text-V2-MoE 模型...) # 使用sentence-transformers包装简化使用 _model SentenceTransformer(nomic-ai/nomic-embed-text-v2-moe, trust_remote_codeTrue) logger.info(f模型加载完成向量维度: {_model.get_sentence_embedding_dimension()}) return _model app.on_event(startup) async def startup_event(): 应用启动时预加载模型 # 这里只是触发加载实际推理时才会真正加载 logger.info(服务启动中...) app.get(/health) async def health_check(): 健康检查端点 return {status: healthy, model_loaded: _model is not None} app.post(/embed, response_modelEmbeddingResponse) async def create_embeddings(request: EmbeddingRequest): 生成文本嵌入向量 try: model get_model() # 生成嵌入 embeddings model.encode( request.texts, normalize_embeddingsrequest.normalize, convert_to_numpyTrue # 返回numpy数组方便序列化 ) # 转换为Python列表 embeddings_list embeddings.tolist() if hasattr(embeddings, tolist) else embeddings return EmbeddingResponse( embeddingsembeddings_list, modelnomic-embed-text-v2-moe, dimensionslen(embeddings_list[0]) if embeddings_list else 0 ) except Exception as e: logger.error(f生成嵌入时出错: {str(e)}) raise HTTPException(status_code500, detailf内部服务器错误: {str(e)}) app.get(/model/info) async def get_model_info(): 获取模型信息 model get_model() return { model_name: nomic-embed-text-v2-moe, dimensions: model.get_sentence_embedding_dimension(), max_seq_length: model.max_seq_length if hasattr(model, max_seq_length) else 8192, normalize_by_default: True }这个服务提供了三个主要端点POST /embed核心功能把文本列表转换成向量列表GET /health健康检查监控服务状态GET /model/info获取模型信息比如向量维度2.2 运行和测试服务用uvicorn启动服务uvicorn app:app --host 0.0.0.0 --port 8000 --reload启动后你可以用curl测试一下curl -X POST http://localhost:8000/embed \ -H Content-Type: application/json \ -d {texts: [Hello world, 你好世界], normalize: true}应该能看到返回的向量数据。服务跑起来后我们转到.NET部分。3. 创建ASP.NET Core Web API项目现在我们来创建主要的.NET应用。我用的是.NET 8但.NET 6/7也基本一样。3.1 项目设置和依赖创建新项目dotnet new webapi -n SemanticSearchApi cd SemanticSearchApi添加需要的NuGet包dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package Microsoft.Extensions.Http.Polly dotnet add package System.Text.Json我们主要用Entity Framework Core来存向量和元数据用HttpClient调用Python服务Polly用来处理重试。3.2 配置模型服务客户端先在appsettings.json里加配置{ EmbeddingService: { BaseUrl: http://localhost:8000, TimeoutSeconds: 30, MaxRetries: 3 }, ConnectionStrings: { DefaultConnection: Server(localdb)\\mssqllocaldb;DatabaseSemanticSearchDb;Trusted_ConnectionTrue; } }然后创建服务客户端。新建Services/EmbeddingServiceClient.csusing System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Options; namespace SemanticSearchApi.Services; public class EmbeddingServiceOptions { public string BaseUrl { get; set; } http://localhost:8000; public int TimeoutSeconds { get; set; } 30; public int MaxRetries { get; set; } 3; } public class EmbeddingRequest { [JsonPropertyName(texts)] public required Liststring Texts { get; set; } [JsonPropertyName(normalize)] public bool Normalize { get; set; } true; } public class EmbeddingResponse { [JsonPropertyName(embeddings)] public required ListListfloat Embeddings { get; set; } [JsonPropertyName(model)] public required string Model { get; set; } [JsonPropertyName(dimensions)] public int Dimensions { get; set; } } public class ModelInfoResponse { [JsonPropertyName(model_name)] public required string ModelName { get; set; } [JsonPropertyName(dimensions)] public int Dimensions { get; set; } [JsonPropertyName(max_seq_length)] public int MaxSeqLength { get; set; } } public interface IEmbeddingServiceClient { TaskListfloat GetEmbeddingAsync(string text, bool normalize true); TaskListListfloat GetEmbeddingsAsync(Liststring texts, bool normalize true); TaskModelInfoResponse GetModelInfoAsync(); Taskbool HealthCheckAsync(); } public class EmbeddingServiceClient : IEmbeddingServiceClient { private readonly HttpClient _httpClient; private readonly ILoggerEmbeddingServiceClient _logger; private readonly JsonSerializerOptions _jsonOptions; public EmbeddingServiceClient( HttpClient httpClient, IOptionsEmbeddingServiceOptions options, ILoggerEmbeddingServiceClient logger) { _httpClient httpClient; _logger logger; // 配置HttpClient _httpClient.BaseAddress new Uri(options.Value.BaseUrl); _httpClient.Timeout TimeSpan.FromSeconds(options.Value.TimeoutSeconds); // JSON序列化配置 _jsonOptions new JsonSerializerOptions { PropertyNameCaseInsensitive true, DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull }; } public async TaskListfloat GetEmbeddingAsync(string text, bool normalize true) { var embeddings await GetEmbeddingsAsync(new Liststring { text }, normalize); return embeddings.FirstOrDefault() ?? new Listfloat(); } public async TaskListListfloat GetEmbeddingsAsync(Liststring texts, bool normalize true) { try { var request new EmbeddingRequest { Texts texts, Normalize normalize }; var response await _httpClient.PostAsJsonAsync(/embed, request, _jsonOptions); response.EnsureSuccessStatusCode(); var result await response.Content.ReadFromJsonAsyncEmbeddingResponse(_jsonOptions); return result?.Embeddings ?? new ListListfloat(); } catch (HttpRequestException ex) { _logger.LogError(ex, 调用嵌入服务失败文本数量: {TextCount}, texts.Count); throw new ApplicationException(嵌入服务调用失败, ex); } } public async TaskModelInfoResponse GetModelInfoAsync() { try { var response await _httpClient.GetAsync(/model/info); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsyncModelInfoResponse(_jsonOptions) ?? new ModelInfoResponse { ModelName unknown, Dimensions 0, MaxSeqLength 0 }; } catch (Exception ex) { _logger.LogError(ex, 获取模型信息失败); throw; } } public async Taskbool HealthCheckAsync() { try { var response await _httpClient.GetAsync(/health); return response.IsSuccessStatusCode; } catch { return false; } } }这个客户端封装了所有和Python服务交互的逻辑。注意我们用了强类型的请求和响应模型这样用起来更安全。3.3 配置依赖注入在Program.cs里注册服务using SemanticSearchApi.Services; var builder WebApplication.CreateBuilder(args); // 添加服务 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 配置嵌入服务 builder.Services.ConfigureEmbeddingServiceOptions( builder.Configuration.GetSection(EmbeddingService)); // 注册HttpClient添加重试策略 builder.Services.AddHttpClientIEmbeddingServiceClient, EmbeddingServiceClient() .AddTransientHttpErrorPolicy(policy policy .WaitAndRetryAsync( retryCount: builder.Configuration.GetValueint(EmbeddingService:MaxRetries, 3), sleepDurationProvider: retryAttempt TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (outcome, timespan, retryAttempt, context) { var logger builder.Services.BuildServiceProvider() .GetServiceILoggerEmbeddingServiceClient(); logger?.LogWarning(第 {RetryAttempt} 次重试嵌入服务调用..., retryAttempt); })); // 注册其他服务后面会添加 // builder.Services.AddScopedIDocumentService, DocumentService(); var app builder.Build(); // 中间件配置 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();这里用了Polly的重试策略网络不稳定的时候会自动重试几次。4. 设计数据模型和数据库语义搜索需要存两个东西文档的原始内容或元数据和对应的向量。我们用EF Core来管理。4.1 创建实体模型新建Models/Document.csusing System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace SemanticSearchApi.Models; public class Document { [Key] public int Id { get; set; } [Required] [MaxLength(500)] public string Title { get; set; } string.Empty; [Required] public string Content { get; set; } string.Empty; [MaxLength(1000)] public string? Summary { get; set; } [MaxLength(50)] public string DocumentType { get; set; } General; // 比如Article, FAQ, Tutorial public DateTime CreatedAt { get; set; } DateTime.UtcNow; public DateTime UpdatedAt { get; set; } DateTime.UtcNow; // 导航属性 public virtual DocumentEmbedding? Embedding { get; set; } } public class DocumentEmbedding { [Key] public int Id { get; set; } [Required] public int DocumentId { get; set; } [Required] [Column(TypeName nvarchar(max))] // 存储JSON格式的向量 public string VectorJson { get; set; } string.Empty; [Required] public int Dimensions { get; set; } [Required] public string ModelName { get; set; } string.Empty; public DateTime GeneratedAt { get; set; } DateTime.UtcNow; // 导航属性 [ForeignKey(DocumentId)] public virtual Document Document { get; set; } null!; // 计算属性从JSON解析向量 [NotMapped] public Listfloat Vector { get System.Text.Json.JsonSerializer.DeserializeListfloat(VectorJson) ?? new Listfloat(); set VectorJson System.Text.Json.JsonSerializer.Serialize(value); } }这里有个设计点向量数据用JSON格式存在数据库里。虽然有些数据库比如PostgreSQL with pgvector支持原生的向量类型但SQL Server没有。用JSON的好处是通用迁移到其他数据库也方便。4.2 创建DbContext新建Data/ApplicationDbContext.csusing Microsoft.EntityFrameworkCore; using SemanticSearchApi.Models; namespace SemanticSearchApi.Data; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetDocument Documents { get; set; } public DbSetDocumentEmbedding DocumentEmbeddings { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Document配置 modelBuilder.EntityDocument(entity { entity.HasIndex(d d.DocumentType); entity.HasIndex(d d.CreatedAt); entity.HasOne(d d.Embedding) .WithOne(de de.Document) .HasForeignKeyDocumentEmbedding(de de.DocumentId) .OnDelete(DeleteBehavior.Cascade); }); // DocumentEmbedding配置 modelBuilder.EntityDocumentEmbedding(entity { entity.HasIndex(de de.ModelName); entity.HasIndex(de de.GeneratedAt); // 添加计算列SQL Server 2016 entity.Property(de de.VectorJson) .HasConversion( v v, v v) .HasColumnType(nvarchar(max)); }); } }4.3 数据库迁移和应用创建迁移dotnet ef migrations add InitialCreate dotnet ef database update这样数据库就准备好了。接下来实现核心的业务逻辑。5. 实现语义搜索服务这是最核心的部分我们要实现文档的嵌入生成和相似度搜索。5.1 创建文档服务新建Services/DocumentService.csusing Microsoft.EntityFrameworkCore; using SemanticSearchApi.Data; using SemanticSearchApi.Models; namespace SemanticSearchApi.Services; public interface IDocumentService { TaskDocument CreateDocumentAsync(string title, string content, string? summary null, string? documentType null); TaskDocument? GetDocumentAsync(int id); TaskListDocument SearchDocumentsAsync(string query, int limit 10); Taskbool GenerateEmbeddingAsync(int documentId); TaskListSearchResult SemanticSearchAsync(string query, int limit 10); } public class SearchResult { public required Document Document { get; set; } public float SimilarityScore { get; set; } public string? Highlight { get; set; } } public class DocumentService : IDocumentService { private readonly ApplicationDbContext _context; private readonly IEmbeddingServiceClient _embeddingClient; private readonly ILoggerDocumentService _logger; public DocumentService( ApplicationDbContext context, IEmbeddingServiceClient embeddingClient, ILoggerDocumentService logger) { _context context; _embeddingClient embeddingClient; _logger logger; } public async TaskDocument CreateDocumentAsync( string title, string content, string? summary null, string? documentType null) { var document new Document { Title title, Content content, Summary summary ?? GenerateSummary(content), DocumentType documentType ?? General, CreatedAt DateTime.UtcNow, UpdatedAt DateTime.UtcNow }; _context.Documents.Add(document); await _context.SaveChangesAsync(); _logger.LogInformation(创建文档成功ID: {DocumentId}, document.Id); return document; } public async TaskDocument? GetDocumentAsync(int id) { return await _context.Documents .Include(d d.Embedding) .FirstOrDefaultAsync(d d.Id id); } public async TaskListDocument SearchDocumentsAsync(string query, int limit 10) { // 简单的文本搜索用于对比 return await _context.Documents .Where(d d.Title.Contains(query) || d.Content.Contains(query)) .OrderByDescending(d d.UpdatedAt) .Take(limit) .ToListAsync(); } public async Taskbool GenerateEmbeddingAsync(int documentId) { var document await _context.Documents .FirstOrDefaultAsync(d d.Id documentId); if (document null) { _logger.LogWarning(文档不存在ID: {DocumentId}, documentId); return false; } try { // 获取模型信息 var modelInfo await _embeddingClient.GetModelInfoAsync(); // 生成嵌入向量 // 这里用标题内容也可以根据需求调整 var textToEmbed ${document.Title}\n{document.Content}; var embedding await _embeddingClient.GetEmbeddingAsync(textToEmbed); // 创建或更新嵌入记录 var documentEmbedding await _context.DocumentEmbeddings .FirstOrDefaultAsync(de de.DocumentId documentId); if (documentEmbedding null) { documentEmbedding new DocumentEmbedding { DocumentId documentId, Dimensions modelInfo.Dimensions, ModelName modelInfo.ModelName, GeneratedAt DateTime.UtcNow }; _context.DocumentEmbeddings.Add(documentEmbedding); } else { documentEmbedding.GeneratedAt DateTime.UtcNow; } // 设置向量 documentEmbedding.Vector embedding; await _context.SaveChangesAsync(); _logger.LogInformation(为文档生成嵌入成功ID: {DocumentId}, 维度: {Dimensions}, documentId, modelInfo.Dimensions); return true; } catch (Exception ex) { _logger.LogError(ex, 为文档生成嵌入失败ID: {DocumentId}, documentId); return false; } } public async TaskListSearchResult SemanticSearchAsync(string query, int limit 10) { try { // 1. 生成查询文本的嵌入向量 var queryEmbedding await _embeddingClient.GetEmbeddingAsync(query); // 2. 获取所有有嵌入向量的文档 var documentsWithEmbeddings await _context.Documents .Include(d d.Embedding) .Where(d d.Embedding ! null) .ToListAsync(); if (!documentsWithEmbeddings.Any()) { _logger.LogWarning(没有找到带嵌入向量的文档); return new ListSearchResult(); } // 3. 计算余弦相似度 var results new ListSearchResult(); foreach (var document in documentsWithEmbeddings) { if (document.Embedding null) continue; var similarity CalculateCosineSimilarity( queryEmbedding, document.Embedding.Vector); results.Add(new SearchResult { Document document, SimilarityScore similarity, Highlight ExtractHighlight(document.Content, query) }); } // 4. 按相似度排序并返回 return results .OrderByDescending(r r.SimilarityScore) .Take(limit) .ToList(); } catch (Exception ex) { _logger.LogError(ex, 语义搜索失败查询: {Query}, query); throw new ApplicationException(语义搜索失败, ex); } } private static float CalculateCosineSimilarity(Listfloat vector1, Listfloat vector2) { if (vector1.Count ! vector2.Count || vector1.Count 0) return 0; float dotProduct 0; float magnitude1 0; float magnitude2 0; for (int i 0; i vector1.Count; i) { dotProduct vector1[i] * vector2[i]; magnitude1 vector1[i] * vector1[i]; magnitude2 vector2[i] * vector2[i]; } magnitude1 (float)Math.Sqrt(magnitude1); magnitude2 (float)Math.Sqrt(magnitude2); if (magnitude1 0 || magnitude2 0) return 0; return dotProduct / (magnitude1 * magnitude2); } private static string? ExtractHighlight(string content, string query) { // 简单的关键词高亮提取 var sentences content.Split(., !, ?); var queryWords query.Split( , StringSplitOptions.RemoveEmptyEntries); foreach (var sentence in sentences) { if (queryWords.Any(word sentence.IndexOf(word, StringComparison.OrdinalIgnoreCase) 0)) { return sentence.Trim() ...; } } return content.Length 200 ? content[..200] ... : content; } private static string GenerateSummary(string content, int maxLength 200) { if (content.Length maxLength) return content; // 简单的摘要生成取第一段或前N个字符 var firstParagraphEnd content.IndexOf(\n\n, StringComparison.Ordinal); if (firstParagraphEnd 0 firstParagraphEnd maxLength) return content[..firstParagraphEnd].Trim(); return content[..maxLength] ...; } }这里有几个关键点GenerateEmbeddingAsync调用Python服务生成向量并保存SemanticSearchAsync实现语义搜索计算余弦相似度CalculateCosineSimilarity手动计算相似度因为SQL Server不支持向量运算5.2 注册文档服务回到Program.cs添加服务注册// 添加DbContext builder.Services.AddDbContextApplicationDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); // 注册文档服务 builder.Services.AddScopedIDocumentService, DocumentService();6. 创建Web API控制器最后我们创建API端点。6.1 文档控制器新建Controllers/DocumentsController.csusing Microsoft.AspNetCore.Mvc; using SemanticSearchApi.Models; using SemanticSearchApi.Services; namespace SemanticSearchApi.Controllers; [ApiController] [Route(api/[controller])] public class DocumentsController : ControllerBase { private readonly IDocumentService _documentService; private readonly ILoggerDocumentsController _logger; public DocumentsController( IDocumentService documentService, ILoggerDocumentsController logger) { _documentService documentService; _logger logger; } [HttpPost] public async TaskActionResultDocument CreateDocument( [FromBody] CreateDocumentRequest request) { try { var document await _documentService.CreateDocumentAsync( request.Title, request.Content, request.Summary, request.DocumentType); return CreatedAtAction( nameof(GetDocument), new { id document.Id }, document); } catch (Exception ex) { _logger.LogError(ex, 创建文档失败); return StatusCode(500, new { error 创建文档失败 }); } } [HttpGet({id})] public async TaskActionResultDocument GetDocument(int id) { var document await _documentService.GetDocumentAsync(id); if (document null) return NotFound(); return Ok(document); } [HttpPost({id}/generate-embedding)] public async TaskActionResult GenerateEmbedding(int id) { var success await _documentService.GenerateEmbeddingAsync(id); if (!success) return NotFound(new { error 文档不存在或生成嵌入失败 }); return Ok(new { message 嵌入生成成功 }); } [HttpGet(search)] public async TaskActionResult Search( [FromQuery] string q, [FromQuery] string? mode semantic, [FromQuery] int limit 10) { if (string.IsNullOrWhiteSpace(q)) return BadRequest(new { error 查询参数不能为空 }); try { if (mode text) { var documents await _documentService.SearchDocumentsAsync(q, limit); return Ok(new { mode, query q, results documents }); } else { var results await _documentService.SemanticSearchAsync(q, limit); return Ok(new { mode, query q, results results.Select(r new { r.Document.Id, r.Document.Title, r.Document.Summary, r.SimilarityScore, r.Highlight }) }); } } catch (Exception ex) { _logger.LogError(ex, 搜索失败查询: {Query}, 模式: {Mode}, q, mode); return StatusCode(500, new { error 搜索失败 }); } } [HttpGet(batch/generate-embeddings)] public async TaskActionResult BatchGenerateEmbeddings( [FromQuery] int batchSize 10) { try { // 获取还没有嵌入向量的文档 var documentsWithoutEmbeddings await _documentService .GetDocumentsWithoutEmbeddingsAsync(batchSize); var results new ListBatchResult(); foreach (var doc in documentsWithoutEmbeddings) { var success await _documentService.GenerateEmbeddingAsync(doc.Id); results.Add(new BatchResult { DocumentId doc.Id, Success success, Message success ? 成功 : 失败 }); // 避免请求过快 await Task.Delay(100); } return Ok(new { processed results.Count, successful results.Count(r r.Success), results }); } catch (Exception ex) { _logger.LogError(ex, 批量生成嵌入失败); return StatusCode(500, new { error 批量生成嵌入失败 }); } } } // 请求模型 public class CreateDocumentRequest { public required string Title { get; set; } public required string Content { get; set; } public string? Summary { get; set; } public string? DocumentType { get; set; } } public class BatchResult { public int DocumentId { get; set; } public bool Success { get; set; } public string Message { get; set; } string.Empty; }6.2 系统状态控制器新建Controllers/SystemController.csusing Microsoft.AspNetCore.Mvc; using SemanticSearchApi.Services; namespace SemanticSearchApi.Controllers; [ApiController] [Route(api/[controller])] public class SystemController : ControllerBase { private readonly IEmbeddingServiceClient _embeddingClient; private readonly ApplicationDbContext _context; private readonly ILoggerSystemController _logger; public SystemController( IEmbeddingServiceClient embeddingClient, ApplicationDbContext context, ILoggerSystemController logger) { _embeddingClient embeddingClient; _context context; _logger logger; } [HttpGet(health)] public async TaskActionResult Health() { try { var embeddingServiceHealthy await _embeddingClient.HealthCheckAsync(); var databaseHealthy await _context.Database.CanConnectAsync(); var modelInfo embeddingServiceHealthy ? await _embeddingClient.GetModelInfoAsync() : null; return Ok(new { status operational, timestamp DateTime.UtcNow, services new { embedding_service embeddingServiceHealthy ? healthy : unhealthy, database databaseHealthy ? healthy : unhealthy }, model modelInfo ! null ? new { modelInfo.ModelName, modelInfo.Dimensions, modelInfo.MaxSeqLength } : null, statistics new { total_documents await _context.Documents.CountAsync(), documents_with_embeddings await _context.DocumentEmbeddings.CountAsync() } }); } catch (Exception ex) { _logger.LogError(ex, 健康检查失败); return StatusCode(500, new { status degraded, error ex.Message }); } } }7. 测试和部署建议7.1 测试API启动两个服务Python模型服务cd nomic-embed-service uvicorn app:app --host 0.0.0.0 --port 8000ASP.NET Core APIdotnet run然后测试几个关键端点创建文档curl -X POST http://localhost:5000/api/documents \ -H Content-Type: application/json \ -d { title: ASP.NET Core Web API 教程, content: 本文介绍如何使用ASP.NET Core创建RESTful API..., documentType: Tutorial }生成嵌入向量curl -X POST http://localhost:5000/api/documents/1/generate-embedding语义搜索curl http://localhost:5000/api/documents/search?q.NET%20Web%20APImodesemantic7.2 性能优化建议实际用的时候你可能会遇到性能问题。这里有几个优化方向批量处理Python服务支持一次处理多个文本可以修改GenerateEmbeddingAsync支持批量生成。向量索引如果文档很多比如超过1万在内存里计算相似度会很慢。考虑用专门的向量数据库如Qdrant、Weaviate或者用PostgreSQL的pgvector扩展缓存频繁搜索相同查询可以加缓存// 在DocumentService里添加内存缓存 private readonly IMemoryCache _cache; public async TaskListSearchResult SemanticSearchAsync(string query, int limit 10) { var cacheKey $semantic_search:{query}:{limit}; if (_cache.TryGetValue(cacheKey, out ListSearchResult? cachedResults)) return cachedResults ?? new ListSearchResult(); // ... 原来的搜索逻辑 _cache.Set(cacheKey, results, TimeSpan.FromMinutes(5)); return results; }异步处理生成嵌入向量比较耗时可以用后台任务// 在控制器里 [HttpPost({id}/generate-embedding)] public async TaskActionResult GenerateEmbedding(int id) { // 立即返回后台处理 _ Task.Run(async () { await _documentService.GenerateEmbeddingAsync(id); }); return Accepted(new { message 嵌入生成任务已提交 }); }7.3 部署考虑生产环境部署要注意Python服务用gunicorn代替uvicorngunicorn -w 4 -k uvicorn.workers.UvicornWorker app:app用Docker容器化方便扩缩容加负载均衡多个实例分摊请求.NET API用Kestrel生产配置配置反向代理Nginx/IIS设置连接池和超时监控健康检查端点定期探测记录模型推理时间、搜索延迟设置警报比如服务不可用、响应时间过长8. 总结这套方案在我们项目里跑了几个月整体比较稳定。最大的好处是技术栈清晰Python专心做模型推理.NET专心做业务逻辑两边都不越界。实际用下来Nomic-Embed-Text-V2-MoE的效果确实不错特别是对长文档和多语言混合内容的处理比之前试过的几个模型都要好。虽然通过HTTP API调用多了点网络开销但在内网环境下延迟基本可以忽略不计。如果你也在.NET项目里需要用到先进的AI模型可以考虑这种服务化的思路。开始可能会觉得多维护一个服务有点麻烦但长期看解耦带来的灵活性是值得的。特别是模型更新的时候只需要更新Python服务.NET这边完全不用动。代码里还有一些可以优化的地方比如向量搜索的性能、错误处理的完善性等你可以根据实际需求调整。希望这个方案对你有帮助。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。