惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
IT之家
IT之家
Google DeepMind News
Google DeepMind News
罗磊的独立博客
爱范儿
爱范儿
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
U
Unit 42
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
B
Blog
博客园 - 叶小钗
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12)
.NET如何实现向量语义分析
fanly11 · 2026-05-18 · via 博客园_首页

图片

图片

     这段时间一直在做插件平台,组件插件化,还有AI Agent组件插件,还扩展支持ai 知识库,向量语义分析,还研究了语言大模型、向量大模型,还有什么归一化,点积,余弦相似度等语义算法分析匹配,之前一直用llamasharp,然后用LLamaEmbedder 做向量语义分析,但是发现匹配不准确。后面查资料LLamaEmbedder 做中文短文本语义向量,本来就不准,尤其是:现在几点 / 当前时间 / 几点了 / 查时间生成模块代码 / 数据库表结构
它不准是天生缺陷。

二、LLamaEmbedder 为什么不准?(核心原因)   

它是 LLM 大语言模型,不是向量模型,不是专门为语义向量训练的,向量质量远不如 Sentence-BERT、BGE、m3e 这类专业模型。
    中文支持极差LlamaEmbedder 原生对中文短文本、口语化问句几乎不优化。
    输出的向量没有归一化、没有对齐语义导致余弦相似度乱跳:
        同义句分数低
        无关句分数高
    速度慢、占用高对比专业向量模型,完全不适合做匹配。

二、.net 真正准的语义向量方案(中文最强)

有两种向量模型方案:
方案 1:BGE 向量模型(国内最准)
方案 2:m3e 向量模型(轻量、超快、中文专门训练)

三、如何使用BGE 向量模型做向量语义分析

 下载这个:bge-small-zh 中文向量模型

https://hf-mirror.com/Xenova/bge-small-zh-v1.5

或单独下载vocab.txt,model.onnx

https://hf-mirror.com/Xenova/bge-small-zh-v1.5/resolve/main/vocab.txt

https://hf-mirror.com/Xenova/bge-small-zh-v1.5/resolve/main/onnx/model.onnx

然后可以封装成以下代码:需要通过nuget引用下载Microsoft.ML.OnnxRuntime和Microsoft.ML.Tokenizers

  public class OnnxBgeEmbeddingService : IDisposable
  {
      private readonly InferenceSession _session;
      private readonly BertTokenizer _tokenizer; // 使用 BertTokenizer

      // 模型输入名称(动态获取或硬编码)
      private readonly string _inputNameIds;
      private readonly string _inputNameMask;
      private readonly string _inputNameTokenType; // 新增:token_type_ids 名称
      private readonly string _outputName;

      // BGE 模型配置
      private const int MaxLength = 512;
      private const int PadTokenId = 0;
      private const int TokenTypeId = 0; // BGE/BERT 单句输入通常全为 0

      public OnnxBgeEmbeddingService(string modelPath, string vocabPath)
      {
          // 1. 初始化 ONNX 会话
          var sessionOptions = new SessionOptions();
          sessionOptions.InterOpNumThreads = 4;
          sessionOptions.IntraOpNumThreads = 4;

          _session = new InferenceSession(modelPath, sessionOptions);

          // 2. 初始化分词器
          _tokenizer = BertTokenizer.Create(vocabPath);

          // 3. 获取模型输入输出名称
          var inputKeys = _session.InputMetadata.Keys.ToList();
          _inputNameIds = inputKeys.FirstOrDefault(k => k.Contains("input_ids")) ?? "input_ids";
          _inputNameMask = inputKeys.FirstOrDefault(k => k.Contains("attention_mask")) ?? "attention_mask";
          // 关键修复:查找 token_type_ids 或 token_type_ids 类似的键
          _inputNameTokenType = inputKeys.FirstOrDefault(k => k.Contains("token_type")) ?? "token_type_ids";

          if (_session.OutputMetadata.Count == 0)
              throw new InvalidOperationException("Model has no outputs.");
          _outputName = _session.OutputMetadata.Keys.First();
      }

      public float[] GenerateEmbedding(string text)
      {
          if (string.IsNullOrWhiteSpace(text))
              throw new ArgumentException("Text cannot be null or empty");

          // 1. 分词并预处理 (截断 + 填充)
          var (inputIds, attentionMask, tokenTypeIds) = EncodeAndPreprocess(text);

          // 2. 构建输入张量
          // 注意:大多数 BGE ONNX 模型接受 int64 (long),部分优化模型接受 int32 (int)
          // 如果报错 Type Mismatch,请将 DenseTensor<long> 改为 DenseTensor<int>
          var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });
          var attentionMaskTensor = new DenseTensor<long>(attentionMask, new[] { 1, attentionMask.Length });
          var tokenTypeIdsTensor = new DenseTensor<long>(tokenTypeIds, new[] { 1, tokenTypeIds.Length });

          var inputs = new List<NamedOnnxValue>
      {
          NamedOnnxValue.CreateFromTensor(_inputNameIds, inputIdsTensor),
          NamedOnnxValue.CreateFromTensor(_inputNameMask, attentionMaskTensor),
          NamedOnnxValue.CreateFromTensor(_inputNameTokenType, tokenTypeIdsTensor) // 关键修复:添加此输入
      };

          // 3. 执行推理
          using var results = _session.Run(inputs);

          // 4. 提取结果
          var rawOutput = results.First().AsTensor<float>();

          // 关键修复:正确获取维度
          // rawOutput 形状通常为 [1, seq_len, hidden_size]
          // Dimensions 是一个 int[] 数组,例如 [1, 512, 384]
          if (rawOutput.Rank != 3)
              throw new InvalidOperationException($"Expected 3D output, got {rawOutput.Rank}D");

          int seqLen = rawOutput.Dimensions.Length; // 序列长度
          int hiddenSize = rawOutput.Dimensions.Length; // 向量维度

          float[] embedding = new float[hiddenSize];

          // 5. 平均池化 (Mean Pooling)
          int validTokenCount = 0;
          for (int i = 0; i < seqLen; i++)
          {
              // 只有当 attention mask 为 1 时才参与计算
              if (attentionMask[i] == 1)
              {
                  for (int j = 0; j < hiddenSize; j++)
                  {
                      // 累加每个维度的值
                      embedding[j] += rawOutput[0, i, j];
                  }
                  validTokenCount++;
              }
          }

          // 求平均
          if (validTokenCount > 0)
          {
              for (int i = 0; i < hiddenSize; i++)
              {
                  embedding[i] /= validTokenCount;
              }
          }

          // 6. L2 归一化
          Normalize(embedding);

          return embedding;
      }

      /// <summary>
      /// 编码并预处理:截断、填充、生成 Mask 和 Token Type IDs
      /// </summary>
      private (long[] InputIds, long[] AttentionMask, long[] TokenTypeIds) EncodeAndPreprocess(string text)
      {
          // 使用 BertTokenizer 编码
          // EncodeToIds 返回 ReadOnlyMemory<int>
          int[] originalIds = _tokenizer.EncodeToIds(text).ToArray();

          int currentLen = originalIds.Length;

          // 1. 截断 (Truncation)
          if (currentLen > MaxLength)
          {
              Array.Resize(ref originalIds, MaxLength);
              currentLen = MaxLength;
          }

          // 2. 填充 (Padding)
          if (currentLen < MaxLength)
          {
              int oldLen = originalIds.Length;
              Array.Resize(ref originalIds, MaxLength);
              // 将新增部分填充为 PadTokenId (0)
              for (int i = oldLen; i < MaxLength; i++)
              {
                  originalIds[i] = PadTokenId;
              }
          }

          // 3. 生成 Attention Mask 和 Token Type IDs
          long[] attentionMask = new long[MaxLength];
          long[] tokenTypeIds = new long[MaxLength];

          for (int i = 0; i < MaxLength; i++)
          {
              // Mask: 非 Padding 位为 1,Padding 位为 0
              attentionMask[i] = (originalIds[i] != PadTokenId) ? 1 : 0;

              // Token Type IDs: BGE 单句任务通常全为 0
              tokenTypeIds[i] = TokenTypeId;
          }

          // 将 int[] 转换为 long[] 以匹配 ONNX 输入要求
          long[] finalInputIds = Array.ConvertAll(originalIds, x => (long)x);

          return (finalInputIds, attentionMask, tokenTypeIds);
      }

      private void Normalize(float[] vector)
      {
          float sum = 0;
          foreach (var v in vector)
          {
              sum += v * v;
          }
          float norm = (float)Math.Sqrt(sum);
          if (norm > 0)
          {
              for (int i = 0; i < vector.Length; i++)
              {
                  vector[i] /= norm;
              }
          }
      }

      public void Dispose()
      {
          _session?.Dispose(); 
      }
  }

以下是调用:

 var onnxBgeEmbeddingService = new OnnxBgeEmbeddingService("Models/model.onnx", "Models/vocab.txt")
 onnxBgeEmbeddingService .GenerateEmbedding("微服务是什么")

以下就是用向量语义分析检索出来的AI对话结果

2758bc00e5642d3f126761146c0a5d87