<![CDATA[我的飛鳥集]]>https://fmcf.cchttps://api.fmcf.cc/api/v2/objects/avatar/q4yxd5smqxro8lfm69.png我的飛鳥集https://fmcf.ccMagnetoTue, 17 Mar 2026 11:37:17 GMTTue, 17 Mar 2026 11:37:17 GMT<![CDATA[融合大核归纳偏置与正交空间感知的轻量化特征重构范式]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/posts/life/CoordDWConvPro

如果你还在为边缘计算下"既要又要"而犯愁,那么这篇文章或许值得一看。

Abstract

在轻量级卷积神经网络的设计中,如何在有限的 FLOPs 下平衡“局部感受野”与“全局空间感知”始终是一个核心难题。

传统的 $3 \times 3$ 卷积受限于感受野大小,而常规的 SE-Block 注意力机制则因全局池化操作导致了空间位置信息的坍塌,为此特地开发了一种新型算子。该结构创新性地融合了 $5 \times 5$ 大核深度卷积 与 坐标注意力机制(Coordinate Attention),并通过 强制残差策略与 GroupNorm 优化,成功构建了一种对硬件友好且具备鲁棒位置编码能力的特征提取范式。

1.设计动机与理论背景

在分析代码之前,我们需要理解该模块试图解决的三个核心痛点:

  • 有效感受野(ERF)的局限性:传统轻量级网络过度依赖 $3 \times 3$ 卷积堆叠。根据研究,深层网络的实际有效感受野往往呈高斯分布且随深度衰减,难以捕捉大尺度语义目标。
  • 空间语义的错位(Spatial Misalignment):标准的 SE 模块通过 Global Average Pooling 将特征图压缩为 $1 \times 1 \times C$,虽然强化了通道依赖,却彻底丢失了物体的空间坐标信息。
  • 微批次统计的不稳定性(Micro-Batch Instability):在端侧设备进行迁移学习或微调时,受限于显存,Batch Size 往往极小(如 2 或 4),此时 BatchNorm 的统计量估计将产生巨大偏差,导致训练发散。

    而本次构建的注意力融合卷积核正是基于上述理论背景提出的解决方案。

2.核心架构拆解

该模块并非简单的层级堆叠,而是一个精心设计的特征重构闭环。以下结合代码逻辑,分步骤进行深度剖析:

2.1大核深度卷积带来的归纳偏置

代码实现:

self.dw_conv = nn.Conv2d(c1, c1, kernel_size=5, stride=s, padding=2, groups=c1, bias=False)

设计:将卷积核从 $3 \times 3$ 提升至 $5 \times 5$。从信息论角度看,这增加了单个神经元的“可视区域”。

理论优势: $5 \times 5$ 卷积的感受野面积是 $3 \times 3$ 的 $25/9 \approx 2.78$ 倍。在轻量级网络(如 MobileNetV3)中,这种大核深度卷积 能有效模拟 Transformer 中的 Token Mixer 行为,增强对纹理和形状的捕捉能力,且 NCNN 等推理框架对 $5 \times 5$ DW 算子已具备极高的 Winograd 算法优化支持。


2.2 正交特征分解与坐标注意力

这是本模块的“灵魂”。不同于 SE 的全局池化,该模块利用两个正交的 1D Global Pooling 操作将空间信息分解。

步骤 I:正交投影

x_h = self.pool_h(feat)  # Output: (N, C, H, 1)
x_w = self.pool_w(feat)  # Output: (N, C, 1, W)
  • 数学表征:输入张量 $X$ 被分别沿水平坐标 $X$ 和垂直坐标 $Y$ 进行聚合。这种操作生成了两个方向感知特征图,使得网络能够捕捉沿着一个空间方向的长距离依赖,同时保存另一个方向的精确位置信息。

步骤 II:跨维度交互与降维

y = torch.cat([x_h, x_w], dim=2)
y = self.conv_pool(y)
y = self.gn(y) # GroupNorm for stability
  • 优化策略: 此处引入了 reduction=16 的瓶颈层以降低模型复杂度。

  • 改进: GroupNorm 的引入是点睛之笔。在注意力分支的中间层,特征通道被压缩,且往往伴随着极小的 Batch Size。GN 将通道分组进行归一化,其统计量不依赖于 Batch Size,从而解决了在微调任务中 BN 层导致的“统计量漂移”问题。

步骤 III:注意力重校准

a_h = self.conv_h(x_h).sigmoid()
a_w = self.conv_w(x_w).sigmoid()
out = identity_feat * a_w * a_h
  • 特征融合: 最终的输出特征图是通过原始特征与两个方向的注意力图进行 Hadamard Product 得到的。这相当于为特征图上的每一个像素点 $(i, j)$ 赋予了一个基于全局上下文计算的“重要性权重”。

2.3 强制残差流

if self.use_res:
    return x + out
  • 梯度流保护: 注意力机制本质上是一种“Soft Gating”。在训练初期,注意力权重可能接近于零。强制残差连接构建了一条 恒等映射 通路,确保了在最坏情况下(注意力层失效),该模块退化为标准的卷积层,从而保证了深层网络梯度的有效反向传播,避免了梯度消失。

3. 详细执行流与张量演化

为了更清晰地展示该模块内部的数据流转,我们将 Forward 过程形式化为以下详细步骤:

  1. 空间特征提取:
  • 输入 $X \in \mathbb{R}^{N \times C_1 \times H \times W}$。

  • 经过 $5 \times 5$ DWConv $\rightarrow$ $1 \times 1$ PWConv $\rightarrow$ BN $\rightarrow$ Hardswish。

  • 输出中间特征 $F \in \mathbb{R}^{N \times C_2 \times H \times W}$。

  1. 坐标信息编码:

    • H-Pooling: 将 $F$ 压缩为 $Z^h \in \mathbb{R}^{N \times C_2 \times H \times 1}$。
    • W-Pooling: 将 $F$ 压缩为 $Z^w \in \mathbb{R}^{N \times C_2 \times 1 \times W}$。
  2. 变换与激活 :

  • 拼接 $Z^h$ 与 $Z^w$ 并通过 $1 \times 1$ 卷积降维至 $C_{mid}$。
  • 应用 GroupNorm(1, mip) 进行归一化(此处 Group=1 等效于 LayerNorm,但针对通道维度)。
  • 应用 Non-linear 激活函数。
  1. 解码与重加权:
  • 将特征张量重新切分为空间感知的权重向量 $A^h$ 和 $A^w$。
  • $Y = F \odot A^h \odot A^w$ (其中 $\odot$ 表示广播机制下的逐元素相乘)。
  1. 特征重构 (Reconstruction):
  • 最终输出 $O = X + Y$ (若满足残差条件)。

4. 实验验证与数据可视化

为了验证 该卷积核 在真实场景中的有效性,我们在受控环境下进行了严格的对比实验。

实验设置:

  • 数据集:自定义检测数据集(包含警棍、手电筒、刀具等高相似度类别)。

  • 训练策略:SGD 优化器,Cosine LR 调度,训练周期为 5000 Epochs(以确保模型完全收敛)。

Baseline:仅将本模块替换为标准的 3x3 DWConv,其余网络架构保持完全一致。

4.1 总体性能评估:计算量与精度的权衡 (Trade-off Analysis)

COMMON ENHANCE 数据解读: 如上表所示,该卷积核 在参数量几乎不增加、FLOPs 仅增加 0.05G(几乎可忽略不计)的前提下,实现了 mAP@50-95 惊人的 10% 绝对增长。这证明了该模块并非通过简单的堆砌参数换取性能,而是通过更高效的空间特征建模提升了模型的表征能力。

4.2 难例挖掘与细粒度分类

在测试中“类间相似性” 是最大的挑战。

例如,长条形的“警棍”与“手电筒”在低分辨率下极难区分。

我们提取了模型在这些特定类别上的 Top-1 准确率 (Accuracy) 进行对比分析: COMMON ENHANCE

5. Conclusion

该算子展示了一种极具前瞻性的轻量级网络设计思路。

  1. 通过 $5 \times 5$ 卷积 引入了更强的空间归纳偏置。
  2. 通过 坐标注意力 解决了标准 CNN 缺乏位置感知能力的问题。
  3. 通过 GroupNorm 和 Hardswish 展现了优秀的工程落地意识,使其在 小样本微调 和端侧推理 场景下具有极高的实用价值。

该模块不仅是一个即插即用的组件,更为后续的轻量级检测网络设计提供了一个标准的空间-通道解耦 范式。

看完了?说点什么呢

]]>
https://fmcf.cc/posts/life/CoordDWConvProhttps://fmcf.cc/posts/life/CoordDWConvProSun, 18 Jan 2026 04:50:08 GMT
<![CDATA[童年回忆]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/19

《七子之歌》和《北京欢迎你》是我童年记忆最深的两首歌,至今会把我仍会哼唱这两首歌,它们像一根丝线,牵引我回到童年。

2008年,盛大的奥运对于小小的我不能理解它的意义,我只知道是我的父亲去了北京与大家共襄盛举,是“我家大门常打开,开怀容纳天地”的余音绕梁,是大家都爱着自己,是每一个身边的人脸上都洋溢着幸福的笑容。我至今想来还是后悔在开幕式的时候睡着了,错过了那个最充满希望的一次奥运。

再长大一岁,是母亲每个夜晚的哭泣,父母亲带我去到很多医院求医,他们跑遍大江南北,带我去北京上海,最后在西京医院治好了,想来这或许也是他们后来在陕西定居的原因吧。第一次住进二三十层的高楼正是在医院的病房,父亲指着楼下川流不息的车流说“你看那些车像不像蚂蚁一样”,是啊人生如蝼蚁,可我今天还是想要追寻蝼蚁的意义。手术的事情我一点也记不起来了,全麻醉是什么样的感觉我也不记得。医生嘱咐以后几年都不能剧烈活动,可是我还是跑跳着走出医院,父母亲没有阻拦我只是笑,小时候只是乐于终于离开一个困住我的地方,现在仔细想来是他们悬在心里的那颗石头终于落地了。

再长大,那个时候我的家境很好,父亲只是问我喜欢哪所学校,就把我送去了那所学校上学,我在学校音乐课上一遍一遍唱“你可知Macau不是我真姓,我离开你太久了母亲”,小孩子是不知道什么是祖国统一的,只知道我离不开我的母亲,那个因我而日夜哭泣的母亲。

母亲不会带我吃肯德基,但是每到春节回家总是可以吃到每年唯一一次肯德基,小时候的我能吃到肯德基就是最大最大的满足,我可以开心一整个星期。现在想来,童年的自己真的很好满足。

童年很幸运,很幸福。幸运那时候大家都很富有,幸运那时候家里很富有才能治好我的病,幸运我有这样爱我的父母亲,幸运有一群爱我的家人,幸运那个时代每个人的脸上都洋溢着希望,正是因为这样的幸运我才会幸福,正是以前的幸运和幸福我才怀念,怀念的是那个可以很简单就可以满足的自己,最天真和无邪的自己。

母亲总说“不要看你爸没个正形,他可是一步一步走出大山,白手起家才有今天这样的成就的。”

再到后来的世界就变了,复杂、猜忌、质疑,父亲因为做项目被骗血本无归,后来又趟了一滩浑水,背了几千万的债务,那些所谓的“朋友”也不再帮助父亲,甚至吓跑了我哥他女朋友一家人,我们一家人过上了拮据的日子。好在家里的大家都齐心协力,用十年时间走出阴影,只是父亲不再年轻。

我有叛逆,但他们溢于言表的爱和童年那段幸福快乐的时光,和在那时不愿放弃我的父母亲,让我爱着这个家,爱这个世界,支撑我在日后最艰难的日子里一步一步走出阴影,和父亲一样。

我爱这个世界,想温柔地爱所有人。

看完了?说点什么呢

]]>
https://fmcf.cc/notes/19https://fmcf.cc/notes/19Mon, 22 Dec 2025 16:20:02 GMT
<![CDATA[春门鼹影]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/posts/life/Chunmen_mole_shadow

这是一首诗 送给一个人

鼹鼠迈进春的门廊
你正梳着苇草的发梢  
河面的波涛,都长出童话的绒毛
  
絮是柳林不肯收信的邮票
  影是野树精心编织的舞蹈
    
摊开掌心——  
  那颗玻璃珠似的月亮,
  映出甲虫壳里沉睡的梦想
  
爱人啊,航线是水鼠未唱完的歌谣  
  在所有风声经过的地方  
  请与我歌唱
  陶罐里会长出铃铛,壶中总有花茶香

看完了?说点什么呢

]]>
https://fmcf.cc/posts/life/Chunmen_mole_shadowhttps://fmcf.cc/posts/life/Chunmen_mole_shadowSat, 20 Dec 2025 14:39:21 GMT
<![CDATA[双系统趣事]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/18

用双系统 Windows 和 Ubuntu 的同学其实遇到过一个很有意思的问题,双系统时间不同步,从 Linux 切换回 Windows 后恰好慢了 8 个小时

其实这是因为 Windows 和 Linux 对硬件时钟(RTC)时间的解读方式不同。 我们的主板上会有一颗时钟芯片,存储着物理世界的时间,但 Windows 认为硬件时钟里存储的时间应该是本地时间(比如,你的时区是东八区,硬件时钟就应该是北京时间)。 Linux/Ubuntu 认为硬件时钟里存储的时间应该是 UTC(协调世界时)时间。

假设你的时区是 东八区(UTC+8,北京时间)。真实世界的时间是 北京时间(本地时间)下午4点(16:00),对应的 UTC 时间就是 上午8点(08:00)。

第一步:在 Windows 中

1.正在使用 Windows,系统时间正确显示为 16:00(下午4点)。

2.Windows 认为硬件时钟应该存储本地时间。所以,它会把 16:00这个时间写入硬件时钟。

第二步:重启,进入 Ubuntu

1.启动电脑,进入 Ubuntu。

2.Ubuntu 去读取硬件时钟,它默认认为硬件时钟上存储的是 UTC 时间。于是它读到了 16:00。

3.Ubuntu 心想:“哦,硬件时钟上说 UTC 时间是 16:00。而我所在的时区是 UTC+8,那么本地时间应该是 UTC 时间加上 8小时。”

4.计算:16:00(它以为的UTC) + 8小时 = 第二天凌晨00:00。

5.Ubuntu 自动通过互联网进行网络时间校准(NTP)。当系统启动联网后,它发现:“咦?我根据硬件时钟算出来的本地时间(00:00)和网络标准时间(16:00)对不上,差了8小时。”

于是,Ubuntu 做了一件聪明事:

  • 它自动把系统时间纠正为了正确的网络时间(16:00)。
  • 同时,为了保持未来的一致性,它用这个纠正后的 UTC 去更新硬件时钟
  • 此时,硬件时钟里的数字被 Ubuntu 从 16:00改为了 08:00。

第三步:重启,切回 Windows(时间错误)

再次重启电脑,回到 Windows。

  • Windows 去读取硬件时钟,它依然认为硬件时钟上存储的是本地时间。于是它读到了 08:00。
  • 当 Windows 无法在启动时自动联网校准时,则需要手动校准,因此直接把这个时间显示为本地时间上午8点。

看完了?说点什么呢

]]>
https://fmcf.cc/notes/18https://fmcf.cc/notes/18Sat, 01 Nov 2025 11:54:57 GMT
<![CDATA[又一列车]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/16

这列车出现在海岸,自京城而来,身披大地母亲由北至南的尘土,风尘仆仆。 大地与海洋这两位母亲,护送它前往天涯海角……

看完了?说点什么呢

]]>
https://fmcf.cc/notes/16https://fmcf.cc/notes/16Tue, 07 Oct 2025 01:59:56 GMT
<![CDATA[Build Gaussian Splatting Trainer for Ubuntu 24 & New CUDA SDK Version]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/posts/technology/Build-Gaussian-Splatting

::: gallery https://api.fmcf.cc/api/v2/objects/icon/3yy0ne2i6e03ayluxk.webp :::


https://github.com/graphdeco-inria/gaussian-splatting


YouTube Video:

https://www.youtube.com/watch?v=T_kXY43VZnk

Preface

Introduction

3D 高斯溅射是时下最新的一项计算机视觉/计算机图形学/深度学习技术,它是“神经渲染”领域的最新的成果。它引入了三项关键技术,使得在保证画质的同时,也能实现实用的训练时间,并能在 1080p 分辨率下达到实时渲染

Fundamental Theories

技术核心

  • NeRF

    • 传统NeRF是把场景表示为一个连续函数 $F(\mathbf{x}, \mathbf{d})$,输入3D空间点 $\mathbf{x}$ 和视角方向 $\mathbf{d}$
    • 但NeRF渲染需要体积积分,训练和推理都很慢。
  • SfM

    • 并没有直接在稠密体素上优化,而是先通过COLMAP/SfM得到稀疏点云。
    • 每个点都变成一个"高斯球体"来近似场景。
  • Gaussian Splatting

    • 每个场景点被建模为一个3D Gaussian Ellipsoid,它们带有协方差矩阵 $\Sigma$
    • 渲染时并未逐点绘制,而是把这些 Ellipsoid 投射到2D图像上,得到平滑的 Splat
  • Differentiable Rasterization

    • 渲染可微,使得通过反向传播来优化位置、形状、颜色、透明度等参数
    • 用的是类似 OpenGL 的快速 Rasterizer

基础数学模型

  • 单个高斯定义

一个高斯分布中心在 $\mu \in \mathbb{R}^{3}$,协方差矩阵 $\Sigma \in \mathbb{R}^{3 \times 3}$:

$$ G(\mathbf{x}) = \exp\left(-\frac{1}{2}(\mathbf{x} - \mu)^T \Sigma^{-1}(\mathbf{x} - \mu)\right) $$

这里:

  • $\mu$:高斯的中心(3D点的位置)
  • $\Sigma$:协方差矩阵,控制形状(球形/椭球)
  • $G(\mathbf{x})$:某点在该高斯下的"贡献"


  • 投影到 2D 图像平面

通过相机投影矩阵 $\Pi$,将 $\mu$ 和 $\Sigma$ 映射到图像平面:

$$ \mu{2D} = \Pi(\mu), \quad \Sigma{2D} = J \Sigma J^{T} $$

其中 $J$ 是投影的雅可比矩阵。

这使得 3D 高斯在屏幕上变成一个 2D 高斯斑点(一个模糊的小椭圆)。


  • 颜色与透明度合成

每个高斯还带颜色 $\mathbf{c}$ 和透明度 $\alpha$。

图像上的最终像素值是多个高斯 splat 的加权混合:

$$ C(\mathbf{p}) = \sum{i} T{i} \alpha{i} \mathbf{c}{i} $$

其中 $T{i} = \prod{j<i} (1-\alpha_{j})$ 表示前面高斯的透射率(类似体积渲染公式)。


  • 优化目标

与真实照片对比,最小化差异(MSE、SSIM、LPIPS 等损失)。

通过反向传播,更新 $\mu,\Sigma,\alpha,\mathbf{c}$ 等参数,使得渲染结果与真实图像一致。

Build Step

Device & System Preparation

  • NVIDIA GPU & GPU Drive
  • ANY X86_64 CPU(High computing power better)
  • Ubuntu 24
  • Anaconda OR Miniconda
  • Python 3.9(important)

About CUDA Version

  • Use CUDA Version == 12.8

The default CUDA version on the Ubuntu24 system is 12.9, and latest version is 13.

However, using newer versions of CUDA Toolkit + PyTorch may cause various issues, such as requiring higher Python versions that are not supported by Gaussian Splatting.

Install

Review the Environment.yml before starting, as it contains all the modules required for use.

If you are using servers or computers located within China, please configure a network proxy or set up a download mirror in advance to avoid download failures.

Please ensure that your system has Git installed before executing the command.

Step 1: Git Clone

git clone https://github.com/graphdeco-inria/gaussian-splatting --recursive

Note:--recursive must be included because the project has submodules.

Step 2: Create Conda Environment

conda create -n gaussian_splattingpython=3.9 ipython
conda activate gaussian_splatting

Step 3: Install CUDA Tookit

Please check if your CUDA Toolkit version is 12.8. If so, proceed to execute the final command in the "Third Step."

nvcc --version

Note: The CUDA version displayed by the nvidia-smi command refers to the driver version, not the CUDA Toolkit version.

If not, you will need to uninstall the current version and reinstall it. This guide will skip the uninstallation steps and focus solely on the installation process for version 12.8.

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-8
conda install -c nvidia cuda-toolkit=12.4

Conda will install CUDA Toolkit 12.4, which remains backward-compatible with CUDA 12.8 instructions.

Step 4: Install the C++ Compilation Environment

sudo apt-get update 
sudo apt install build-essential ninja-build

Step 5: Install Additional Dependencies

conda install -c conda-forge plyfile
conda install tqdm

and important step:

pip install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1 --index-url https://download.pytorch.org/whl/cu128

Installation will be aborted if your Python version is not 3.9.

pip install opencv-python joblib

Finally, ensure numpy==1.22. If not, please uninstall the current numpy using the pip and reinstall version 1.22.

Step 6: Modify Modules

First, navigate to and enter your project directory. For example, my directory is ~/Code/gaussian-splatting.

cd ~/Code/gaussian-splatting

and edit file to include float.h: #include <float.h>

vim submodules/simple-knn/simple-knn.cu

Step 7: Compile Modules

pip install submodules/diff-gaussian-rasterization 
pip install submodules/simple-knn
pip install submodules/fused-ssim

Final Step

After saving the dataset, you can execute the command to begin training.

python train.py -s ./xxx

看完了?说点什么呢

]]>
https://fmcf.cc/posts/technology/Build-Gaussian-Splattinghttps://fmcf.cc/posts/technology/Build-Gaussian-SplattingSun, 24 Aug 2025 05:38:29 GMT
<![CDATA[我独自等待]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/15

“祝福未来的你幸福,以后结婚记得给我发请帖~(微笑)再见~”

今天是你的生日,一年前也是,我清晰的记得,一举一动、一颦一笑都深深刻在我的心底,因这件事吵架,更因这件事离别,谁曾想这是最后一次见面,或许你已经开始奔赴新的生活,但我依旧妄图在这里独自等待。

你是我的天使,我的希望,曾经。当我曾坠入谷底,是你一点一点帮助我,我喜欢坐在你身边,听你讲故事,看着你弹琴,也喜欢和你在一起散步,畅想我们的未来。你也是我的挚友,我们在最美的中学时代相遇,陪伴彼此走过点滴,计算机和摄影是你曾最爱的话题,我追逐你的脚步,一点又一滴,彼此进步,勇往直前。

你是我的梦魇,我的绝望,现在。你最终还是将我推入深渊,我一次又一次恳求你,渴求你不要扔掉我,即使是远远的看着你,你也狠心把我抛弃。我从未如此爱一个人,甚达后无来者之境地,我未曾想过如此深厚的友谊甚至是爱情,最终要如此收尾。

我曾认为时间会冲淡一切,但在爱你这件事上,仍未改变。我常常夜里梦见你,你在我的梦里还是那样温柔,热情的与我相拥。

“我还以为再也见不到你了!抱抱!!(扑上去)”,可再也见不到了,这是梦,即使不愿醒来,不愿相信,但梦醒了,我还是要面对如此的事实。

生日快乐,允中。即使你可能再也不会,也不愿看到我的祝福。

看完了?说点什么呢

]]>
https://fmcf.cc/notes/15https://fmcf.cc/notes/15Sun, 17 Aug 2025 12:16:20 GMT
<![CDATA[使用 Ncatbot 构建多模态对话插件,对接 OpenAI 与 Ollama 的实现]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin

0. 前言

项目已在 Github 上开源,这里是一些基础的技术讲解。

https://github.com/ouyangyanhuo/ModelChat

1. 结构设计:插件化工程

在项目中,采用插件化架构进行系统构建,利用 Ncatbot 的插件模式,可以很好的将主程序与功能隔离开。

插件架构的优势在于模块边界清晰、依赖隔离度高、系统可拓展性强。标准的插件化极大程度上将代码之间解耦,无论是开发还是部署都是十分友好的。

项目系统目录结构如下:

ModuleChat/
├── main.py            # 插件主入口,负责指令注册与调度逻辑
├── chat.py            # 模型适配层,封装本地和云端模型的调用
├── config.yml         # 配置文件,集中控制模型参数与启用选项
├── requirements.txt   # 依赖库
└── cache/
    └── history.json  # 聊天历史记忆文件

chat.py 是系统的核心模块,主程序 main.py 负责接收、解析指令,并且路由消息到模型模块处理。在开发中这是十分友好的一种结构,使得专注于单个功能模块的开发与调试,极大降低了维护复杂度。

而配置项集中于 config.yml,进一步提升了灵活性与环境适配能力。

采用临时 json 文件的方式,记录指令的调用与回复,并且传入 API 接口,可以使得大模型可以获得一定程度上的短期记忆功能,但一定程度上来说,这对系统并不友好,我认为更优解是采用数据库的形式,但使用数据库很大程度上增大了系统的复杂性,所以使用 json 文件是一个很好的代替选择。

2. 插件主程序:指令解耦与路由中枢

main.py 是插件的主入口,通过 register_user_func 方法注册两个指令:/chat/clear chat_history,分别对应聊天功能和历史记录清除。

此外,主程序支持对图像消息的自动识别,提取其中的图像 URL 并传递给 chat_model_instance.recognize_image 方法,自动获取视觉描述。

            if image_url and self.chat_model.get('enable_vision', True) and not self.chat_model.get('use_local_model'):
                # 使用图像识别功能
                image_description = await chat_model_instance.recognize_image(image_url)
                user_input = f"用户发送了一张图片,图片描述是:{image_description}。用户说:{user_input}"
            elif image_url and not self.chat_model.get('enable_vision', True):
                # 图像识别功能未开启,但检测是否是本地模型
                if self.chat_model.get('use_local_model'):
                    user_input = f"用户发送了一张图片,但用户使用的是本地模型,无法进行图像识别。用户说:{user_input}"
                else:
                    user_input = f"用户发送了一张图片,但图像识别功能未开启。用户说:{user_input}"

获取到视觉描述后,再转到语言大模型来进行输出,其实这是一种很好的解决方法,在当前使用场景下,更多的是需要对图片进行识别后对内容进行分析处理,而不是处理图像本身,这可以很大程度上减少 API 调用,提高缓存命中率,减少 TOKEN 使用,从而降低 API 调用成本,并且可以使用云端大模型来进行识别后,再交给本地大模型回答,近一步压缩成本。

错误处理方面,try...except 包裹了整个聊天逻辑,避免了图像解码失败或 API 异常造成主流程崩溃,保持插件健壮性。

整合来看,main.py 是典型的“轻控制器”模式,仅协调各组件而不承担业务逻辑细节,使整个插件具备良好的工程可读性。

3. 模型适配模块:多模型封装与语义一致性

chat.py 是插件的核心逻辑。它负责处理模型调用、聊天历史记忆、图像识别等任务。为了兼容多种模型接口(如 OpenAI API 与 Ollama 本地服务),采用了统一封装接口的策略,使外部调用者无需关注模型细节,只需通过 useCloudModel()useLocalModel() 两个方法即可完成对话。

    async def useLocalModel(self, msg: BaseMessage, user_input: str):
        """使用本地模型处理消息"""
        try:
            # 构建消息列表,包含历史记录
            messages = self._build_messages(user_input, msg.user_id if hasattr(msg, 'user_id') else None)
            response: ChatResponse = chat(
                model=self.config['model'],
                messages=messages
            )
            reply = response.message.content.strip()

            # 保存当前对话到历史记录
            if hasattr(msg, 'user_id'):
                self._update_user_history(msg.user_id, {"role": "user", "content": user_input})
                self._update_user_history(msg.user_id, {"role": "assistant", "content": reply})
        except Exception as e:
            reply = f"请求出错了:{str(e)}"
        return reply

值得注意的是,由于 OpenAI 接口调用和 Ollama 调用存在些许不同,并且某些模型的参数并不完全,所以相比起来,使用 OpenAI 接口,调用云端模型其实可以获得更好的体验,例如我们可以控制模型的 temperature 使它更具想象力亦或者是更注重实时,减少幻觉。

所有用户历史被存储在 cache/history.json 文件中,这是一种持久化保存的方案,并且可以具备一定程度的可追溯性,历史记录通过 _update_user_history 方法动态更新,控制在配置文件设定的最大轮数之内。这种方式防止了上下文过大带来的性能问题,同时确保模型能理解连续上下文,提升回答质量,使得即使对接接口也可以拥有近似记忆的能力。

类中还集成了 OpenAI 的图像识别模型,通过 _build_vision_messages 构建多模态消息结构。并且设计上我将图像处理、消息构造、异常处理、调用模型等函数分离,使得开发中可以更快定位问题所在,开源后也更利于其他开发者阅读。

4. 云端模型对接(OpenAI):标准化封装

云端模型的调用主要通过 openai 官方库封装,利用 chat.completions.create 方法完成上下文构建与回复生成。在每一次调用中都通过 _build_messages() 方法构造完整的对话上下文,加入系统提示词和使用 cache/history.json 保存的历史记录,实现多轮记忆式对话。

    def _build_messages(self, user_input: str, user_id: str = None):
        """构建消息列表"""
        messages = []
        
        # 添加系统提示词
        system_prompt = self.config.get('system_prompt', "你是一名聊天陪伴机器人")
        messages.append({"role": "system", "content": system_prompt})

        if user_id:
            history = self._get_user_history(user_id)
            messages.extend(history)
        
        # 添加当前用户输入
        messages.append({"role": "user", "content": user_input})
        return messages

调用逻辑中封装了 temperature 参数,支持通过配置文件灵活控制模型输出的随机性。

当遇到 Bug report 时,统一使用 return 将报错反馈到用户面,这样可以减少大量的报错开发处理,并且将常见的由于配置失误导致的问题,更清晰的反馈出来,即统一模型+规则处理的两种方法来反馈运行时遇到的问题。

                if "401" in str(fallback_error) or "Unauthorized" in str(fallback_error):
                    raise Exception("模型API认证失败,请检查配置文件")
                raise Exception(f"图像识别出错: {str(e)}, 备用方法也失败: {str(fallback_error)}")

返回结果后,会将本轮问答同步到用户历史缓存中,并保存到本地文件中,确保下一轮能正常取回上下文。这样可以降低内存依赖、增强缓存命中率,同时为后续调试与行为复现提供依据。

5. 本地模型调用(Ollama):轻量化推理与接口统一

本地模型的调用通过 ollama.chat() 完成,复用了 _build_messages() 的上下文构建逻辑,以确保调用逻辑与云端一致,保持接口一致性。

这种本地推理机制的优点非常明显:在无网络或私有部署环境下依旧能使用智能对话功能,极大增强了插件的部署灵活性和安全性。即便是在隐私敏感场景中,也可以做到本地化部署与运行。

设计上保持本地与云端调用接口一致(都封装为 use*Model()),外部调用方无需判断模型来源,从而降低复杂度。除此之外,同样实现了历史记录更新、异常捕获机制,使本地模型具备与云端一致的功能完整度与稳定性。

6. 图像识别处理逻辑:多模态输入的语义增强策略

图像识别功能是本插件的一大亮点。插件支持识别图像消息并通过 OpenAI 视觉模型进行处理。整个流程如下:

    1. 从图像消息中提取 URL;

      for segment in msg.message:
         if isinstance(segment, dict) and segment.get("type") == "image":
             image_url = segment.get("data", {}).get("url")
             break
      
    1. 通过 HTTP 请求获取图像内容并进行 Base64 编码;
    1. 构造视觉输入格式(包括 image_urltext prompt);
            response = requests.get(image_url)
            response.raise_for_status()
            return base64.b64encode(response.content).decode('utf-8')
    1. 调用视觉模型完成图像描述;
            # 获取并编码图片
            image_data = self._encode_image_from_url(image_url)
            
            # 构建消息
            messages = self._build_vision_messages(image_data, prompt)
            
            # 调用视觉模型
            response = self.vision_client.chat.completions.create(
                model=self.config.get('vision_model'),
                messages=messages,
                temperature=self.config.get('model_temperature', 0.6),
                stream=False,
                max_tokens=2048
            )
    1. 将图像描述拼接到用户输入中,提升上下文语义完整性。

这一机制有效解决了图文混合输入场景中的信息不对称问题,同时通过分级调节调用,可以仅利用云端高算力处理复杂问题,再交给本地处理简单化后的问题,极大程度上减少了 TOKEN 使用率。

异常处理方面,我们设计了两级降级策略:如主调用失败则尝试纯文本 fallback prompt;若仍失败,则提示用户检查 API 密钥或模型状态。这种容错设计使插件能在部分失败时依旧保持服务不中断。

7. 聊天历史系统:记忆窗口控制

聊天历史存储在 cache/history.json 文件中,按用户维度管理。该设计使系统可同时服务多用户,并为每个用户维护独立上下文。通过 _get_user_history_update_user_history 方法,插件能自动在每轮对话中注入历史信息,实现类“记忆式”问答体验。

我们对历史记录长度做了窗口限制(默认10轮),以控制上下文规模并避免模型处理压力过大、过度消耗 TOKEN。缓存更新为同步写入的操作,确保在系统崩溃、断电等异常情况下不会造成信息丢失。

    async def clear_user_history(self, user_id: str):
        """清除指定用户的历史记录"""
        user_id = str(user_id)
        if user_id in self.history:
            del self.history[user_id]
            self._save_history()
            reply = "已清空聊天记录"
        else:
            reply = "没有找到用户的聊天记录"
        return reply

此外,还支持通过指令 /clear chat_history 主动清除用户历史,为隐私或重新对话提供了便利。这种机制让插件既具备持久性,又保留用户主动控制空间。

8. DEBUG & LOG

在调试处打断点、print 标志 这都是一个很好的测试习惯,我还从微信开发学到了一招 —— print("FUCK"),在长期运行时偶尔会出现崩溃的情况,这时候事实上可以在 log 中输出指定的字符,当查看日志定位问题的时候,可以直接搜索字符串来迅速定位,FUCK 无疑是一种有趣的方式。

看完了?说点什么呢

]]>
https://fmcf.cc/posts/technology/ncatbot-multimodal-pluginhttps://fmcf.cc/posts/technology/ncatbot-multimodal-pluginMon, 04 Aug 2025 09:25:02 GMT
<![CDATA[一直切,你的刀还会利吗?]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/14

民航局发布禁止无 3C 充电宝带上飞机是一件十分正常的事情,毕竟飞机在飞行过程中发生爆炸事件是非常恐怖的事情。

地方的高铁站跟着凑什么热闹?? 甚至铁路局都说过很多次,不会禁止充电宝上高铁,今天依旧看到有高铁站在拦截无 3C 充电宝。

有些人就这么害怕承担责任吗?就这么害怕自己的乌纱帽掉下来吗?

一直切,你的刀还会利吗?

看完了?说点什么呢

]]>
https://fmcf.cc/notes/14https://fmcf.cc/notes/14Mon, 07 Jul 2025 05:21:21 GMT
<![CDATA[《我与地坛》]]>
该渲染由 MX-SPACE API 生成,可能存在排版问题,最佳体验请前往:https://fmcf.cc/notes/13

要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏。不能说,也不能想,却又不能忘。它们不能变成语言,它们无法变成语言,一旦变成语言就不再是它们了。它们是一片朦胧的温馨与寂寥,是一片成熟的希望与绝望,它们的领地只有两处:心与坟墓。


但是太阳,它每时每刻都是夕阳也都是旭日。当它熄灭着走下山去收尽苍凉残照之际,正是它在另一面燃烧着爬上山巅布散烈烈朝辉之时。那一天,我也将沉静着走下山去,扶着我的拐杖。有一天,在某一处山洼里,势必会跑上来一个欢蹦的孩子,抱着他的玩具。当然,那不是我。但是,那不是我吗?宇宙以其不息的欲望将一个歌舞炼为永恒。这欲望有怎样一个人间的姓名,大可忽略不计

看完了?说点什么呢

]]>
https://fmcf.cc/notes/13https://fmcf.cc/notes/13Mon, 23 Jun 2025 16:42:11 GMT