AI 大模型与智能应用
实践与技术

专注分享 AI 大模型、Agent 开发、深度学习等技术实战与架构设计;兼具 C#/.NET 后端开发经验

113
技术文章
8
文章分类
512
技术标签
29029
累计字数
赋能企业级开发:ABP vNext 高级实践与核心功能深入解析
后端开发
2024-12-03 0k

赋能企业级开发:ABP vNext 高级实践与核心功能深入解析

赋能企业级开发:ABP vNext 高级实践与核心功能深入解析 如果你认为 ABP vNext 只是一个帮你做 CRUD 的框架,那你就太小看它了。它是一个完整的、基于最佳实践的应用开发平台。本文将带你超越 Hello World,探索其如何在真实企业级项目中大放异彩。 在上一篇入门文章后,我们了解了 ABP 的基础。现在,让我们深入其更强大的功能,看看它如何解决实际开发中的复杂场景。 一、 模块化架构:构建复杂系统的基石 ABP 的模块化不仅是代码组织方式,更是架构设计哲学。 1. 定义与依赖 每个模块都是一个独立的单元。创建一个模块非常简单: [DependsOn( // 显式声明依赖 typeof(AbpAccountApplicationModule), typeof(AbpIdentityApplicationModule), typeof(YourModuleProjectDomainModule) // 依赖自己的领域层 )] public class YourModuleApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { // 配置本模块的服务 Configure<AbpVirtualFileSystemOptions>(options => { options.FileSets.AddEmbedded<YourModuleApplicationModule>("YourCompany.YourModule"); }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { // 应用启动时的逻辑 var app = context.GetApplicationBuilder(); // ... 中间件配置 } } 2.

ABP vNext 企业级开发 模块化架构
阅读更多
模型蒸馏完全指南:从BERT到BiLSTM的新闻分类实战
人工智能
2024-11-10 0k

模型蒸馏完全指南:从BERT到BiLSTM的新闻分类实战

目录 为什么需要模型蒸馏? 核心概念:教师-学生框架 三大关键公式(面试必考) 蒸馏的完整流程 实战代码:BERT → BiLSTM 新闻标题多分类蒸馏 面试高频考点总结 常见误区与调参技巧 一、为什么需要模型蒸馏? 1.1 一句话总结 模型蒸馏 = 让一个"小学霸"(小模型)跟着"老教授"(大模型)学习,用小模型的身体继承大模型的智慧。 1.2 现实痛点 假设你用 BERT-base(1.1亿参数)训练了一个新闻标题分类器,效果很好(准确率 95%)。但当你想部署上线时,问题来了: 问题 具体表现 推理慢 BERT 单条推理约 10-30ms,高并发时扛不住 显存大 模型本身 ~440MB,加上 KV Cache 更夸张 部署难 手机端、嵌入式设备根本跑不动 蒸馏就是解决方案:训练一个 BiLSTM(可能只有几百万参数),让它学习 BERT 的"思考方式",最终得到一个又快又准的小模型。 1.3 蒸馏 vs 其他压缩方法 模型压缩技术全景: ├── 模型蒸馏(Knowledge Distillation) ← 本文重点 │ └── 用大模型的输出指导小模型训练 ├── 模型剪枝(Pruning) │ └── 去掉不重要的权重/神经元 ├── 模型量化(Quantization) │ └── FP32 → INT8,降低精度 └── 权重共享(Weight Sharing) └── 多个参数共享同一组权重 🎯 面试考点:蒸馏是唯一一种"跨架构"的压缩方法——教师和学生的模型结构可以完全不同(如 BERT → BiLSTM),而剪枝和量化通常只能压缩同架构模型。

模型蒸馏 知识蒸馏 BERT
阅读更多
架构反思:引入 Redis 统计在线人数后,JWT 是否还有存在的意义?
架构与设计
2024-10-16 0k

架构反思:引入 Redis 统计在线人数后,JWT 是否还有存在的意义?

在构建 .NET Web API 后台时,为了实现在线人数统计,我们通常会引入 Redis 来存储用户的活跃状态。 这时,一个非常敏锐且直击架构本质的问题往往会浮出水面: “既然为了统计在线人数,每次请求都要去 Redis 读写一次,那 JWT ‘无状态、不查库’ 的核心优势不就没了吗?干脆用一个短小的随机字符串(普通 Token),既能节省流量,又能节省计算资源,岂不更好?” 这是一个极好的问题。它触及了软件架构的核心——权衡 (Trade-off)。 答案是:在特定场景下,普通 Token 确实更好;但在大多数现代架构中,JWT + Redis 的组合依然具有不可替代的优势。 本文将从依赖性、性能、架构扩展性三个维度,深入剖析为什么我们依然推荐保留 JWT。 1. 核心区别:强依赖 vs. 弱依赖 这是两者最根本的区别,决定了系统的可用性 (Availability) 上限。 🔵 方案 A:普通 Token (Reference Token) + Redis 在这个方案中,Token 只是一个无意义的随机字符串(一把钥匙)。 流程: 请求到达 -> 解析 Token -> 必须请求 Redis 查找对应的 UserID -> 拿到身份 -> 执行业务。 风险: 这里 Redis 是强依赖。 如果 Redis 宕机、网络抖动或连接数耗尽,整个系统的认证模块瞬间瘫痪。

JWT Redis 认证授权
阅读更多
注意力机制是如何“注意”的?一个词看懂上下文背后的秘密
人工智能
2024-09-16 0k

注意力机制是如何“注意”的?一个词看懂上下文背后的秘密

你是否好奇,当 Transformer 模型读一句话时,是如何动态判断“当前这个词,最该参考前面哪几个词”的?这个过程并非魔法,也不是人工编写的规则,而是由 Q、K、V 三个向量 配合训练完成的一套精妙计算。 可以把整个过程想象成:当前词和前面每一个词进行一次“对话”,然后互相打分,得分高的就重点参考。具体来说,分四步。 第一步:三个关键角色登场 对于序列中的每个词,模型都会为它生成三个向量: Q(Query,查询):代表“当前词”的需求。就像在大声问:“我需要理解我自己,谁有相关信息?” K(Key,键):代表每个词的“标签”。每个词举着名牌,写着:“我主要包含这类信息。” V(Value,值):每个词真正携带的具体内容,是最终要被提取的信息。 第二步:打分——“谁对我更重要” 当要理解词 A 时,模型就用 A 的 Q 向量,去和前面每个词的 K 向量计算点积(内积),得到一个标量分数。数学上,向量方向越相近,点积越大,意味着语义越相关。 例如,“北京”的 Q 遇上“首都”的 K,会因为语义高度相关得到一个很高的分数;而遇到“的”这类虚词的 K,分数往往很低。 第三步:从分数变成概率 这些原始分数经过 Softmax 归一化,被挤压成一组在 0 到 1 之间、总和为 1 的概率值。这个概率,就是最终的“注意力权重”。 于是,“首都”可能分到 0.6,“是”分到 0.1,“的”只分到 0.05。模型就真实地“知道”了:理解“北京”时,重点看“首都”。 第四步:提取信息 最后,用这些权重对所有词的 V 向量进行加权求和。权重高的词的 V 被大量保留,权重低的则几乎被忽略。融合后的向量,就携带了丰富的上下文信息,用来完成当前词的预测或理解。 那么,这一切是由谁决定的? 答案是:数据训练。 Q、K、V 向量的生成矩阵一开始是完全随机的。模型在完成“预测下一个词”或“完形填空”的任务时,会不断犯错,再通过反向传播调整这些矩阵里的参数。经过万亿次调整,模型在无数句子中捕捉到了词与词之间的统计共现规律,涌现出这样的能力: “的”通常是结构词,不重要; “首都”对一个地点名词很重要; 动词和它的主语关系紧密; 代词往往指向前文出现过的实体…… 所以,“最该参考谁”的决定,本质上就是模型在海量文本中学到的、隐藏于 Q 和 K 互动中的语义相关性判断。所谓“多头注意力”,无非是准备很多套不同的 Q、K、V 生成矩阵,让模型能同时从语法、语义、长距离依赖等多个角度去做这样的打分。 这,就是注意力机制的核心秘密。

注意力机制 Transformer QKV
阅读更多
从机器指令到面向对象:编程思想的演进与领域驱动设计
架构与设计
2024-07-24 0k

从机器指令到面向对象:编程思想的演进与领域驱动设计

面对领域驱动设计(DDD)的火热,很多熟悉面向对象(OOP)的开发者都会有一个疑问:这和我以前学的OOP是什么关系?是全新的东西,还是旧瓶装新酒?本文将带你回顾编程思想的演进历程,理清DDD与OOP的深刻联系,并展望现代编程思想的融合之道。 一、编程思想的演进脉络:一部与“复杂性”的斗争史 编程的发展史,就是一部工程师们如何不断地抽象和封装,以应对日益增长的软件复杂性的历史。让我们用一张时间线来直观感受这一历程: timeline title 编程思想演进历程 1950年代以前 : 面向机器编程 : 主要与硬件直接交互 1950-1960年代 : 面向过程编程 : 以步骤为中心<br>结构化程序设计 1960-1970年代 : 面向对象编程兴起 : 对象为核心<br>封装、继承、多态 2000年代 : 领域驱动设计提出 : 应对业务复杂性<br>强调领域建模 2000年代至今 : 多种思想并存 : 函数式编程复兴<br>响应式编程等 下面我们来详细解读每个阶段的核心思想与突破: 面向机器与面向过程:效率与结构的初探 面向机器:在计算机诞生初期,编程直接使用机器语言或汇编语言,程序员必须深入了解硬件细节。这种方式效率极低,难以维护,是“与硬件共舞”的时代。 面向过程:随着高级语言(如Fortran, C)的出现,面向过程的思想成为主流。它将程序看作一系列线性步骤(过程),通过函数来实现每个环节,其核心方法是“自顶向下、逐步求精”。然而,当系统变得复杂时,数据和操作分离的特性使得代码的复用和维护变得异常困难,全局变量的滥用等问题凸显。 面向对象编程的兴起:将现实世界映射入代码 为了解耦数据与操作,面向对象编程(OOP) 应运而生。它以类和对象作为基本程序单位,将数据和对数据的操作封装在一起。它通过三大特性来管理复杂性: 封装:收敛内部逻辑,只暴露必要的接口。 继承:实现代码的复用和层次的抽象。 多态:允许同一接口表现出不同的行为,极大地提高了系统的灵活性。 OOP通过对象的概念将现实世界映射到软件系统,更符合人类的思维方式,是程序设计方法学上的一次巨大飞跃。 领域驱动设计的深化:聚焦业务复杂性的战略工具 到了2004年,埃里克·埃文斯(Eric Evans)在其著作《领域驱动设计》中提出了DDD。这时,软件的规模和技术复杂性已经得到较好控制,但业务逻辑本身的复杂性成为了新的挑战。 DDD的核心是建立正确的领域模型,并让这个模型成为项目成员(包括领域专家、设计人员、开发人员)之间沟通的通用语言。它在OOP的基础上,进一步强调了边界的控制,引入了限界上下文和聚合等核心模式,来应对大型、复杂业务系统的设计和拆分问题。 二、DDD与OOP:是继承与发展,而非颠覆与取代 领域驱动设计并非一个全新的编程范式,它可以看作是面向对象思想在复杂业务系统分析和设计上的进一步发展和规范化应用。 1. 核心理念的继承

编程思想 面向对象 OOP
阅读更多
Seq2Seq 与注意力机制:从机器翻译到 Transformer 架构
人工智能
2024-07-23 0k

Seq2Seq 与注意力机制:从机器翻译到 Transformer 架构

你是否好奇,当AI翻译一句话时,它是怎么做到"翻译到哪个词,就重点看哪个词"的?答案就藏在一个叫做 “专属信息包 C” 的东西里。今天,我们用最通俗的语言,一步步拆解它的诞生过程。 一、先搞懂一个问题:为什么需要"专属信息包"? 假设我们要把中文 “我 爱 深度 学习” 翻译成英文 “I love deep learning”。 在注意力机制出现之前,传统模型的做法非常粗暴: 把整句话压缩成 一个固定长度的向量 C,然后解码器拿着这同一个 C 去翻译每一个英文单词。 这就像什么呢?就像你把一本200页的小说浓缩成一句话,然后要求别人用这句话去复述每一个章节的细节——显然不可能! 翻译 “I” 的时候,需要关注"我" 翻译 “love” 的时候,需要关注"爱" 翻译 “deep” 的时候,需要关注"深度" 每个时刻需要的信息是不同的! 所以我们需要为每个时刻量身定制一个 “专属信息包”——这就是注意力机制中动态计算的 上下文向量 $C_t$。 二、认识三位主角:Q、K、V 在计算"专属信息包"之前,我们先认识三个关键角色。用一个图书馆找书的比喻来理解: 角色 全称 比喻 含义 Q (Query) 查询 你脑子里想找的那本书的"关键词" “我现在需要什么信息?” K (Key) 键 图书馆每本书封面上的"标签" “这里有什么信息?” V (Value) 值 每本书的"实际内容" “这个信息的实际内容是什么?” 当解码器要生成某个单词时,它会:

Seq2Seq 注意力机制 Transformer
阅读更多
.NET Core Web API 过滤器完全指南:类型、用途与实战
后端开发
2024-06-27 0k

.NET Core Web API 过滤器完全指南:类型、用途与实战

在 .NET Core Web API 开发中,过滤器(Filter)是实现横切关注点解耦的核心组件。它能在请求处理的不同阶段拦截逻辑,统一处理权限验证、日志记录、异常捕获等通用需求,避免重复代码。本文将详细拆解 .NET Core Web API 内置的 5 类过滤器,分析其执行机制、核心用途,并提供可直接复用的实战示例。 一、过滤器核心概念:什么是过滤器? 过滤器是 .NET Core 提供的请求拦截机制,本质是一套 “钩子(Hook)” 系统,能嵌入请求处理生命周期的关键节点。其核心价值在于: 解耦横切关注点:将日志、权限、异常处理等通用逻辑与业务逻辑分离; 统一管控:对所有接口或指定接口批量应用通用规则; 可扩展性:支持自定义过滤器,满足复杂业务场景。 所有过滤器遵循固定的执行顺序,从请求进入到响应返回,完整流程如下: 授权过滤器 → 资源过滤器 → 模型绑定 → Action 过滤器 → 执行 Action → Action 过滤器 → 结果过滤器 → 执行结果 → 结果过滤器 → 资源过滤器 → 响应返回 若任意阶段抛出异常,将直接触发异常过滤器处理。 二、5 类内置过滤器详解:用途 + 执行时机 1. 授权过滤器(AuthorizationFilter):请求的 “第一道关卡” 核心作用 验证请求是否有权限访问接口,是所有过滤器中执行最早的组件。若验证失败,直接终止请求并返回 401(未授权)或 403(禁止访问),不执行后续逻辑。 内置实现 [Authorize]:标记需要授权的 Action/Controller,依赖 IAuthorizationService 校验身份(配合 JWT、Cookie 等认证方案)。支持角色、策略等精细化授权,示例: // 仅 Admin 角色可访问 [Authorize(Roles = "Admin")] [HttpGet("admin/data")] public IActionResult GetAdminData() => Ok("管理员数据"); [AllowAnonymous]:允许匿名访问,用于公开接口(如登录、注册),会跳过授权校验: [AllowAnonymous] [HttpPost("login")] public IActionResult Login(string username, string password) => Ok("登录成功"); 关键特点 仅关注 “是否允许访问”,不处理业务逻辑;

dotnet webapi filter
阅读更多
string 类型深度解析:是值类型还是引用类型?
后端开发
2024-06-27 0k

string 类型深度解析:是值类型还是引用类型?

在 .NET 开发中,string 类型是我们日常使用最频繁的类型之一。但一个常见的疑问始终困扰着不少开发者:string 到底是值类型还是引用类型?其实答案很明确 ——string 本质是引用类型,但它兼具部分值类型的特性,这种特殊性也让它成为了 .NET 类型系统中极具代表性的存在。本文将从本质定义、特殊特性到实际验证,全面解析 string 类型的本质。 一、核心结论:string 是引用类型 从 .NET 类型系统的底层设计来看,string 完全满足引用类型的所有定义,这是不可动摇的核心事实。 1. 引用类型的核心判定依据 继承关系:string 直接继承自 System.Object(引用类型的基类),而非值类型专属的 System.ValueType; 存储位置:string 对象的实际内容存储在 托管堆 中,变量仅持有指向堆内存的 “引用地址”(类似指针); 可空性:string 变量默认支持赋值为 null(表示不指向任何堆内存对象),而值类型(如 int、DateTime)默认非空,需通过 Nullable<T>(如 int?)才能支持 null; 引用传递特性:当把一个 string 变量赋值给另一个变量时,传递的是 “引用地址”,而非字符串内容的拷贝。 2. 代码验证:引用类型的本质 // 1. 变量 a 指向堆内存中的 "hello" 对象 string a = "hello"; // 2. 变量 b 拷贝的是 a 的引用地址,而非 "hello" 内容 string b = a; // 验证:a 和 b 指向同一个堆内存对象 Console.

string 值类型 引用类型
阅读更多
RNN隐藏状态全解析:它到底是什么?传给谁?何时输出?
人工智能
2024-05-29 0k

RNN隐藏状态全解析:它到底是什么?传给谁?何时输出?

在学习循环神经网络(RNN)时,很多初学者在推导数据流时会产生一系列连环疑问: “当前时间步输出的隐藏状态,和传给下一个时间步的信息,是同一份数据吗?” “既然传给下一步了,那当前步的隐藏状态还留着干啥?” “每一个时间步都需要把隐藏状态接上 Softmax 输出预测结果吗?” 这些问题直击 RNN 的核心架构。今天,我们就把传统 RNN(Vanilla RNN)掰开揉碎,从本质、去向、输出时机三个维度,彻底理清隐藏状态(Hidden State)的前世今生。 一、 破除迷思:传给下一步的信息 和 隐藏状态 是同一个东西吗? 直接给出结论:在传统 RNN 中,它们是完全相等的,就是同一份数据(同一个张量)。 传统 RNN 的核心递推公式为: $$h_t = \tanh(W_{xh} x_t + W_{hh} h_{t-1} + b_h)$$ 在这个公式中,计算出的 $h_t$ 只有一个。它既被定义为“当前时间步的隐藏状态”,又在下一个时间步 $t+1$ 的计算中,原封不动地作为 $h_{t}$(即下一步眼中的 $h_{t-1}$)传入。不存在任何额外的变换或门控机制来区分这两者。 ⚠️ 重要区分:LSTM 不是这样 这种“输出 = 下一步输入”的特性仅适用于传统 RNN。在 LSTM 中,有两条状态线:细胞状态(Cell State, $c_t$)和隐藏状态(Hidden State, $h_t$)。$c_t$ 是真正的“记忆传送带”传给下一步,而 $h_t$ 是经过输出门过滤后的版本用于对外输出。所以在 LSTM 中,两者是不相等的。 二、 隐藏状态的“双重使命”:它到底留着干啥? 既然“传给下一步的信息”和“隐藏状态”是同一个东西,那为什么还要专门叫它“隐藏状态”?它计算出来后到底被拿去干嘛了? 这里需要破除一个思维误区:它们不是两个东西,而是“同一个东西”的两个去向。 当 $h_t$ 被计算出来后,它面临着 “一分为二”的双重使命:

RNN 隐藏状态 深度学习
阅读更多
分布式系统并发控制:从单机锁到分布式锁的深度解析
架构与设计
2024-05-06 0k

分布式系统并发控制:从单机锁到分布式锁的深度解析

引言 在分布式系统开发中,并发控制是一个永恒的话题。最近在代码评审时,我看到了这样一段代码: // 复杂操作仍需 lock public void ComplexOperation() { lock (_lockObj) { if (_count > 0 && _count < 100) { _count *= 2; // 其他复杂业务逻辑... } } } 这引发了一系列深入的讨论:如果锁里面执行的是用户下单操作,A用户下单过程中执行了1秒,B用户的请求会怎样?是直接失败还是等待?我们需要手动处理这种等待吗? 本文将围绕这个问题展开,深入探讨单机锁、细粒度锁、数据库事务和Redis分布式锁的优缺点及适用场景。 第一部分:单机锁的行为分析 锁的基本行为 当我们在C#中使用lock语句时,CLR会为我们管理一个等待队列: public void PlaceOrder(Order order) { lock (_lockObj) // 所有用户下单都串行化! { // 复杂的下单逻辑,耗时1秒... ProcessOrder(order); UpdateInventory(); // 其他业务... } } 关键结论:B用户会等待A用户执行完,而不是直接失败。整个过程是自动的,不需要人工干预。 具体执行流程 A用户进入lock块,开始执行下单操作(耗时1秒) B用户的线程到达lock语句时,发现_lockObj已被锁定 B线程会阻塞并等待,直到A线程释放锁 A用户执行完毕,释放锁 B线程获得锁,开始执行自己的下单操作 单机锁的严重问题 虽然锁机制自动处理了同步,但这种设计存在严重问题: 性能瓶颈:所有用户请求串行处理 响应时间变长:B用户需要等待A用户的1秒 + 自己的1秒 = 至少2秒 系统吞吐量下降:无法充分利用多核CPU优势 第二部分:解决方案对比 🥉 方案一:细粒度锁 private readonly Dictionary<string, object> _userLocks = new(); private readonly object _dictLock = new object(); public void PlaceOrder(string userId, Order order) { object userLock; lock (_dictLock) { if (!

分布式系统 并发控制 锁机制
阅读更多