Cursor 编辑器界面 ┌────────────────────────────────────────────┐ │ 顶部菜单栏（File / 编辑 / 选择 / 查看...）│ ├────────────────────────────────────────────┤ │ 左侧侧边栏（文件树 / 项目 / Git 等） │ ├────────────────────────────────────────────┤ │ 中间主编辑区（代码编辑） │ ├────────────────────────────────────────────┤ │ 右侧 AI 面板（Cursor 智能助手 / Chat） │ ├────────────────────────────────────────────┤ │ 底部状态栏（分支 / 文件类型 / 行列信息等） │ └────────────────────────────────────────────┘ 如何使用 Cursor 的 AI 功能 功能 作用 快捷键（默认） 🧠 Inline Edit 选中一段代码 → Ctrl + K（或右键 “Ask Cursor”）→ 输入自然语言指令（如“优化正则表达式”） Ctrl + K 🤖 Agent 面板 打开一个“聊天窗口”，直接和 Cursor 对话（如“帮我写个登录验证函数”） Ctrl + I ✨ Tab 补全 在代码中输入时，Cursor 自动预测后续内容 Tab 🔁 重新生成建议 当灰色提示出现但不理想时，按 `Alt + ``（或右键 “Regenerate”） `Alt + `` 选择一段代码+CTRL+K 修改指定的代码，直接在代码编辑区弹出修改窗口调用大模型。CTRL+ENTER 或者鼠标点击 应用修改 CTRL+N 或者 **鼠标点击 **拒绝修改 CTRL+I 1）图中设置的Auto表示自动选择模型； 2）@ 可以添加文件、文件夹到当前对话上下文，如果你在代码编辑区选择一段代码再 CTRL+I 或者 CTRL+L，也会自动将它添加进来。还可以直接拖拽资源管理器中的文件到AI对话框中。 3）+Browser 模式可以让模型看到整个项目文件 任务 你可以直接问 Cursor 找函数定义 “verifyPassword 函数定义在哪个文件？” 查找引用 “verifyCode 被调用了多少次？” 理解架构 “帮我总结整个项目的模块结构。” 改代码 “把所有 JS 文件里的 var 改成 let。” 生成文档 “根据这个项目代码生成一份 README。” 4）对话模式 Agent模式，默认的对话模式，功能最全，AI 不仅能回答，还会“主动思考、规划和执行”。可以理解为：“帮我干活”的模式。 Plan 模式，它可以浏览整个项目文件，但不会直接改代码，而是会：把你的需求拆分成多个步骤；告诉你“下一步该干什么”；有时生成一份修改计划或脚本方案。 Ask 模式：问答模式，和网页版使用大模型一样。 TAB 一般注释起手，如果出现灰色预测文字时，直接 Tab 就能接受 AI 自动补全 ，按ESC可以拒绝这个建议；否则就是普通的缩进。 触发逻辑 传统 IDE（比如 VS Code、IntelliJ） 的补全是词法或语法级触发： 你输入 “pri” → 提示 “print()”； 它只是基于关键字匹配。 而 Cursor 的补全 是“语义级”的： AI 根据上下文 推断你下一步可能在做什么。 它会综合考虑： 当前光标前后的代码； 注释 / 函数签名； 文件名； 项目中的其他定义； 甚至最近你在这个 Tab 里做的事。 阶段 触发动作 AI 响应 检测阶段 监听输入变化（按键、换行、注释等） 分析上下文语义树（AST） 预测阶段 生成“意图向量” 模型推断用户下一步目标 展示阶段 在光标后显示灰色 ghost text 等待你按 Tab 接受 触发时机 类型 触发条件 示例 行为 函数触发 输入 function / def function check... 预测参数和函数体 控制语句触发 输入 if / for 等 if (phone) 预测条件或逻辑块 注释触发 输入 //、#、/** // 校验邮箱格式 按注释生成实现 模板触发 class / import / try class User 预测类体、导入内容 上下文模仿 类似已有代码块 第二个函数定义 模仿风格 文件结构触发 同名模块、utils utils. 补出函数名 空行预测 新行、缩进后 空一行 猜测下一个合逻辑语句 Index&Docs Codebase Indexing（代码索引）： Cursor 会扫描整个项目文件夹； 提取文件路径、函数定义、类名、依赖关系； 生成语义向量（embeddings）并存储（只保存索引，不上传源代码）。 界面说明：索引完成度100%；已被索引的文件数量为 78； Index New Folders： 当你创建或添加新文件夹时，Cursor 是否自动索引它。 Ignore Files in .cursorignore： 告诉 Cursor 哪些文件不要被索引。类似.gitignore Docs（文档索引）： 可以手动添加官方文档、API 文档、开发指南，让它当作参考知识。 添加一个 README.md 或内部 API 规范； 或添加外部文档（如一个 SDK 的文档网站）； 之后在 Chat 里用 @doc 引用 如何撤销修改 在AI编辑窗口提问，应用修改，点击Accept 应用完之后，若想回退版本，可以点击提问右边的回撤箭头。 整体思路（重要） 1）先打开项目，然后用browser模式，让AI对这个项目生成一个 .cursorignore 文件，自动忽略哪些文件无需添加进上下文。 2）打开设置，Index&docs，重新索引一下。 3）把项目相关的需求文档、接口文档、技术文档，全部录入到docs中，作为知识库以作备用。这与项目代码文件夹解耦的。 4）在项目根目录下创建 .cursorrules 文件，不需要 @ 引用，自动生效。 .cursorrules 就是 Cursor 的 AI 指令与代码风格规则文件，用来告诉 AI：在这个项目里写代码、命名、注释、格式化时都按照这些规则来做。是一个 系统级提示词 。 eg: ai_behavior: - 所有回答必须使用中文 - 代码中添加必要的注释 style: indentation: 4 spaces naming_convention: variables: camelCase AI 背后接收到的系统指令：你是一个 Java 开发助手。 遵循以下项目规则： 所有回答必须使用中文。 代码中添加必要的注释。 使用 4 空格缩进。 变量名使用 camelCase。 用户请求： 「写一个计算阶乘的 Java 函数」。
DeepWalk DeepWalk 的最终目标是—— 把图中的每个节点，用一个低维向量表示（embedding）出来，使得结构相似的节点在向量空间中也接近。 也就是说，如果两个节点在图上经常“一起出现”（相连、同社群等），那它们的 embedding 也应该相似。 主要思想 将图结构转化为“序列语料”： 在图上做 随机游走（random walks），生成顶点序列，类似于自然语言中的“句子”。 将随机游走得到的序列输入 Skip-Gram 模型，学习节点的低维嵌入表示。 学到的嵌入能捕捉到 邻居相似性、社区结构，可用于分类、聚类、链路预测等任务。 输入 & 输出 输入：图结构 $G=(V,E)$，随机游走参数（步长 $t$，次数 $\gamma$，窗口大小 $w$），嵌入维度 $d$。 输出：节点嵌入矩阵 $\Phi \in \mathbb{R}^{|V|\times d}$。 关键公式与目标 随机游走生成“句子”： 从某个顶点 $v_i$ 出发，采样一个长度为 $t$ 的随机游走序列： $$ W_{v_i} = (v_i, v_{i+1}, \dots, v_{i+t}) $$ 生成的序列 $W_{v_i}$ 就是一句“话”，用来训练 Skip-Gram 模型。在这些序列中，如果两个节点经常在相邻位置出现，就表示它们关系密切。 ["A", "B", "C", "D"] Skip-Gram 目标函数： 现在我们用 Word2Vec 的思想：对于一句话 ["A", "B", "C", "D"]，假设窗口大小 $w = 1$，也就是每个节点只看它的邻居。那么训练样本是这样的： 中心节点 它要预测的邻居 A B B A, C C B, D D C 模型要学的是： “给我节点 B，我要能预测出 A 和 C。” “给我节点 C，我要能预测出 B 和 D。” 于是就有这个公式，给定中心节点 $v_i$，最大化其上下文窗口内邻居节点的条件概率： $$ \max_{\Phi} \sum_{i=1}^{|V|} \sum_{u \in \mathcal{N}_w(v_i)} \log Pr(u \mid \Phi(v_i)) $$ 其中 $\mathcal{N}_w(v_i)$ 表示在随机游走序列中，窗口大小 $w$ 内的上下文节点。 我希望模型能学到这样一种 embedding：中心节点 $v_i$ 能够高概率预测出它周围的邻居 $u$。 优化形式（负对数似然）： 就是把上面的最大化目标，改写成一个最小化损失函数。 最大化“好事”（邻居出现的概率）= 最小化“坏事”（邻居预测错误的概率） $$ \min_{\Phi} - \log Pr\big( {v_{i-w},\dots,v_{i+w}} \mid \Phi(v_i) \big) $$ 概率建模（Hierarchical Softmax 或 Negative Sampling）： $$ Pr(u \mid \Phi(v_i)) = \frac{\exp\big( \Phi(u) \cdot \Phi(v_i) \big)}{\sum_{v \in V} \exp\big( \Phi(v) \cdot \Phi(v_i) \big)} $$ 这相当于用 Softmax 把所有节点的相似度转成概率分布。 其中： 分子：中心节点和邻居节点的相似度（点积越大越相似）； 分母：所有节点的相似度和（用来归一化成概率）。 ​ 如果 $u$ 是 $v_i$ 的邻居，那么 $\Phi(u)$ 与 $\Phi(v_i)$ 的点积就应更大。 例子 想象你是个“社交分析师”： 你观察到小明每天都和小红、小刚一起出现； 而小明几乎从不和小李在一起； 那你自然会说：小明和小红、小刚的关系更近。 DeepWalk 就是通过“随机游走 + 预测邻居”这个机制， 自动学出“谁跟谁关系近”的数值表示。 训练逻辑 1）加载 10+1 张图 A_list = [A100, A101, ..., A110] 我们取前 10 张作为输入，最后一张作为真实标签。 2）对每一个时间步的图做 DeepWalk for t in range(100, 110): emb_t = DeepWalk(A_t) 每张图 $A_t$ 都独立训练一个 节点 embedding（比如 400×64）。 400是节点数，64是嵌入维数 embedding 就是每个节点在这一时刻的“语义坐标”。 训练目标是：让在图里常相邻的节点在 embedding 空间也靠得近。 可以理解成： DeepWalk 把图结构变成“节点的向量表示”。 3）拼接成时间序列特征（10步） 对每对节点 (i, j)，你把过去10个时间步的 embedding 拿出来，计算每步的 Hadamard积（元素乘）： feats = [emb100[i]*emb100[j], emb101[i]*emb101[j], ..., emb109[i]*emb109[j]] X_ij = concatenate(feats) 每个节点对 (i, j) → 一个 10×64 = 640 维的特征； 这 640 维包含了它俩“过去10步的相似度变化轨迹”。 白话理解： 这个特征就像是“节点对关系的时间切片”。 如果两人（节点）经常靠得近 → 向量相似； 如果逐渐靠近 → 特征会呈现变化趋势。 4）第11步图作为真实答案（标签） A_target = A110 y_ij = 1 if 节点i和j在第110步相连 else 0 每个节点对 (i, j) 的标签就是第11步的真相； 如果在第11步有边 → 正样本； 没边 → 负样本。 5）平衡样本（1:1） 因为稀疏图中没连边的太多： 原来是 9,237 个正样本 vs 70,563 个负样本； 现在我们随机保留同样数量的负样本； 所以训练时正负样本平衡：9,237 : 9,237。 ✅ 这样模型不会再“全猜没边”。 6）训练 MLP 来做预测 MLP(X) → 输出每个节点对在第110步连边的概率 输入：每个节点对的 640 维特征； 输出：一个 [0,1] 概率； Loss：二分类交叉熵（BCELoss）； 优化器：Adam。 🧠 白话解释： MLP 在学“什么样的历史关系轨迹会导致下一步产生连接”。 7）用真实 A110 对照预测结果 模型预测完后： 它给每对节点一个连边概率； 我们用阈值 0.5 分成「有边/无边」； 然后与真实的 $A_{110}$ 比较，得到： 指标 含义 AUC=0.8279 模型能区分哪些节点会连边、哪些不会（非常好） ACC=0.7223 有约 72% 的节点对被正确分类（相对平衡后的准确率） （1）静态图版本 你只有一张图，比如社交网络的当前关系： DeepWalk 学出 embedding； 然后用 MLP 或逻辑回归，预测那些“目前没边但 embedding 很接近”的节点对； 这些就是「潜在朋友」→ 即未来可能建立连接的边。 📘 所以静态链路预测其实是： 用当前结构的 embedding 相似度，预测将来是否可能连边。 （2）动态图版本 你有多帧图，比如第100~109步（连续的10个时间片）： 每一步都用 DeepWalk 得到 embedding； 然后把每个节点对的“10帧特征”拼起来； 喂给 MLP 预测第110步是否连边。 📘 动态版本其实是： 把「节点关系的历史轨迹」作为输入， 预测下一个时间步的关系变化。 DeepWalk 的本质： 是一个 图嵌入算法（Graph Embedding Method）， 输入图结构，输出节点的向量表示（embedding）。 也就是说： 每次运行 DeepWalk，相当于重新学习节点的 embedding； embedding 本身就是最终结果，不需要“恢复模型状态”去推理； 它不具备像神经网络那样“泛化到新数据”的功能。 所以： ✅ DeepWalk 的输出 = checkpoint ✅ DeepWalk 的 embedding = 可直接保存 / 复用的结果 ❌ 不需要保存训练中间的模型权重
移动模型 RWP（Random Waypoint，随机路标） 空间与时间： 在一个正方形活动区域 $[0,R]\times[0,R]$ 内，离散为 steps 个时间步。 节点运动规则： 初始位置：每个节点在区域内均匀随机选一个起点。 选目标点：从区域内均匀随机再选一个目标点（下一路标）。 速度：对该段路程，从区间 $[v_{\min}, v_{\max}]$ 均匀采样一个速度（以“每步移动的距离”计）。 运动轨迹：用直线匀速插值从当前点走到目标点，按需要的步数填充每个时间步的坐标。 停留：到达目标后，停留 pause_steps ~ U[pause_min, pause_max]（整数步）。 循环：停留结束后再选一个新的随机目标点，重复 3–5。 输出轨迹： 把每个时间步所有节点的 $(x,y)$ 位置拼成矩阵 $Y\in\mathbb{R}^{\text{steps}\times 2N}$。 邻接生成（通讯图）： 对每个时间步，任意两节点距离 $\le$ 通讯半径 comm_radius 则记为一条无向边（1），否则为 0，得到一组对称的 0/1 邻接矩阵序列。 要点：RWP 的目标点在区域内+“可停留”，因此轨迹呈 跳点—直线—驻留的节奏。 RW（Random Walk，随机游走） 空间与时间： 活动区域仍是边长 $R$ 的正方形（或你实现里也支持圆形采样作为初始点）。离散 steps。 节点运动规则： 初始位置：在活动区域内随机取一个点作为起点。 目标点：每次从一个随机边界点或区域内随机点中挑一个当目标（你的实现是 5 种可能：四条边上的均匀点或区域内随机点）。 速度：每段从 $[v_{\min}, v_{\max}]$ 均匀采样一个速度。 运动轨迹：按直线匀速插值走向目标。 无停留：到达目标立即选择下一目标继续走（不驻留）。 循环：重复 2–4。 邻接生成： 与 RWP 相同，以 comm_radius 做阈值逐步形成 0/1 邻接矩阵。 要点：RW 版本不设停留，且目标点常在边界，轨迹更“连续流动”，在边界附近来回穿梭的概率更高。 RD（Random Direction，随机方向） 空间与时间： 同样是正方形区域与离散时间。 节点运动规则： 初始位置：从边界上均匀随机选一个起点（四条边等概率）。 目标点：每段都从边界再均匀随机选一个新的边界点当目标（边界→边界）。 速度：每段速度 $[v_{\min}, v_{\max}]$ 均匀采样。 运动轨迹：直线匀速插值到目标。 停留：到达目标后停留 pause_steps ~ U[pause_min, pause_max] 步。 循环：继续从目标（边界点）出发，选下一个边界点为新目标。 邻接生成： 同上，按 comm_radius 阈值逐步构造邻接矩阵序列。 要点：RD 强化了边界—边界往返与停留，节点会频繁在边缘聚集、驻留，再切换到另一边。 Gaussian Markov Mobility（高斯-马尔科夫移动模型） 空间与时间： 在矩形区域 $[x_{\min}, x_{\max}] \times [y_{\min}, y_{\max}]$ 内，离散为多个时间步。 节点运动规则： 初始状态：每个节点随机生成位置 $(x,y)$，速度 $v \sim U(0.5, v_{\max})$，方向 $\theta \sim U(0,2\pi)$。 速度更新： $v_t = \alpha v_{t-1} + (1-\alpha)\mu_v + \sqrt{1-\alpha^2},\varepsilon_v,$ 其中 $\mu_v$ 是全局平均速度，$\varepsilon_v \sim \mathcal{N}(0,\sigma_v^2)$。 方向更新： $\theta_t = \alpha \theta_{t-1} + (1-\alpha)\mu_\theta + \sqrt{1-\alpha^2},\varepsilon_\theta,$ 其中 $\mu_\theta$ 是平均方向，$\varepsilon_\theta \sim \mathcal{N}(0,\sigma_\theta^2)$。 位置更新： $x_t = x_{t-1} + v_t \cos\theta_t, \quad y_t = y_{t-1} + v_t \sin\theta_t$ 边界处理：若节点越界，则进行反射，即位置镜像回区域内，同时方向在对应维度取反。 循环：重复步骤 2–5，生成连续轨迹。 邻接生成： 同前，设定通信半径 comm_radius，每个时间步任意两节点距离 ≤ 半径则连边，得到动态邻接矩阵序列。 要点： 参数 $\alpha$ 决定轨迹的平滑度/惯性： $\alpha \to 1$：路径接近直线，有明显惯性； $\alpha \to 0$：路径接近随机游走，更“抖动”。 和 RWP / RW / RD 不同，这里没有“目标点”概念，节点轨迹完全由马尔科夫更新的速度与方向决定，生成的运动更自然连续。 维度 RWP（随机路标） RW（随机游走，你实现：边界/内点混合目标、无停留） RD（随机方向，你实现：边界→边界+停留） GMM（高斯-马尔科夫） 空间密度 中心更密集（运动段偏向中心），若有停留：总体=中心偏移 + 均匀停留的混合 经典“各向同性反射随机游走”→近似均匀；但你的实现中目标多在边界，会带来轻微“边界增密” 边界显著增密（目标与停留都在边界，停留时间把概率质量“挂”在边缘） 近似均匀（无目标点偏好、速度/方向马尔科夫更新 + 反射），边界处可能有很弱的反射效应 速度（时间加权） 若每段速度 ~ U[v_min, v_max]：时间加权后的稳态速度分布偏向慢速（慢速段逗留时间更长 → 采样更容易采到），称“速度衰减/偏置” 同 RWP 的现象（段速均匀抽样 + 时间加权 → 偏慢） 同 RWP；另外停留会拉高“0 速度”的占比 受 α 与噪声控制，接近截断的高斯/稳态马尔科夫分布；无“路标段速”偏置 运动连续性 “跳点—直线—停留”—再跳点，方向变化较大 无停留、持续移动，方向由目标决定（混合边界/内点），变化连贯 “边界—直线—边界—停留”，方向常大幅改变；停留让轨迹呈块状 最平滑（速度/方向自相关），拐弯更自然 节点度/连边分布（给定 comm_radius） 中心密度更高 → 中心平均度更大，边缘更稀疏；有停留则在路标点附近形成“团” 近似均匀（你的实现可能边界稍高）；总体度分布较均衡 边界平均度更高（节点停在边上形成热点），内部相对稀疏 近似均匀；由于轨迹平滑，同伴保持时间更长，度变化更缓 链路时长（LET）/SNR波动 停留时链路更稳定；运动段因频繁“折返”方向，LET 方差较大 持续移动且无停留→链路更易“断续”，LET 略短、波动更频繁 停留导致边界链路长时存在，离开边界后链路更快变化，两极分化 速度/方向相关性高 → 链路更平稳、LET 较长；SNR 梯度/方差更小 相遇/再相遇（contact / inter-contact） 中心高密 → 相遇更频繁；停留会带来重入热点 近似均匀；若边界目标占比高 → 边界再相遇概率升高 边界热点相遇/再相遇概率高；内部穿越时短暂接触 平滑惯性 → 更连续的相遇时段、再相遇概率↑（沿相近方向“并走”的时间更长） 链路预测效果： RWP：有停留 → 形成稳定链路，便于预测。中心偏置 → 形成热点区域，链路模式较明显。 RW：持续移动，没有停留 → 链路变化更快，能训练出短时预测能力。效果稍差 RD：节点停留在边界时，链路超稳定 → 预测很容易。 GMM：轨迹平滑，链路断开/形成有更明显的惯性。LET 分布更长，预测任务难度适中（比 RW 稳定，比 RWP 更连续）。最贴近“真实连续移动”的模型。
Kubernetes (K8S) Kubernetes 是一个开源的容器编排平台，用于自动化部署、扩缩容（Pod 数量多了/少了自动调整）和自愈（坏掉的 Pod 会被自动重建） 基本架构 一个 Kubernetes 集群 (Cluster) = 控制平面 + 工作节点 的组合。要使用K8S，要先搭建集群，deployment.yaml 和 service.yaml 要提交到 K8S 集群，由它调度和运行。 控制平面 (Control Plane) 负责管理整个集群，不直接跑应用。 组件 作用 比喻 API Server 接收命令（kubectl）、提供统一接口 前台服务员 etcd 存储集群所有状态（配置、Pod 列表等） 账本/数据库 Scheduler 决定 Pod 跑在哪个 Node 上 领班 Controller Manager 保证实际状态符合预期（补 Pod、重建失败的 Pod） 经理 工作节点 (Worker Nodes) 实际跑应用容器。 组件 作用 Kubelet 负责跟 API Server 沟通，并启动/管理容器 容器运行时 运行容器（Docker、containerd） Kube-Proxy 管理网络规则，实现 Service 的负载均衡 graph TD A[控制平面] --> B[Node 1] A --> C[Node 2] A --> D[Node 3] B --> E[Pod] B --> F[Pod] C --> G[Pod] D --> H[Pod] K8S 核心概念 Pod 最小运行单元，一个 Pod 里可以有 1 个或多个容器。 共享网络和存储空间 Deployment 管 Pod 的「副本数」和「版本」。 支持滚动更新、回滚 Service 提供稳定的网络入口（因为 Pod IP 会变）。 负载均衡到一组 Pod。 ConfigMap & Secret ConfigMap：存储配置信息（非敏感）。 Secret：存储敏感信息（密码、Token），会加密。 基本使用方式 Deployment（定义 Pod 模板 + 副本数） apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 # 要 3 个 Pod selector: matchLabels: app: my-app template: # Pod 模板 metadata: labels: app: my-app #给 Pod 打标签，Service 用 selector 找到这些 Pod 做负载均衡 spec: containers: - name: my-app #Pod名 image: my-registry/my-app:v1 # ← 指向 CI 推送后的镜像（仓库/名称:标签） ports: - containerPort: 8080 #容器对外提供服务的端口 Pod名可能是：my-app-6f7c9d7c5d-abcde，它里面的容器名：my-app 映射关系： kind: Deployment → 创建一个「Pod 管理器」 replicas: 3 → 保证始终有 3 个 Pod template → Pod 的样子（运行哪个镜像、开哪个端口） Service（提供统一访问入口） apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: my-app # 找到所有带 app=my-app 标签的 Pod ports: - port: 80 # Service 入口端口 targetPort: 8080 # Pod 里容器的端口 type: LoadBalancer # 暴露给外部访问 映射关系： selector → 绑定到 Deployment 创建的 Pod，根据 Pod 的标签（my-app） ports → 用户访问 80 端口，会转发到 Pod 的 8080 type: LoadBalancer → 提供一个外部 IP 供用户访问 典型工作流程 sequenceDiagram 开发者->>Git: 提交代码 Git->>CI/CD: 触发构建 CI/CD->>镜像仓库: 构建Docker镜像 CI/CD->>K8S: kubectl apply K8S->>K8S: 创建/更新Pod K8S->>K8S: 健康检查 K8S->>用户: 服务可用 sequenceDiagram 开发者->>API Server: kubectl apply -f xxx.yaml API Server->>etcd: 写入期望状态 API Server->>Scheduler: 请求调度 Scheduler->>Node: 分配 Pod 到合适的节点 Node->>Kubelet: 启动容器 Controller Manager->>Cluster: 监控状态并修复 Service->>用户: 提供统一访问入口 👉 过程总结： 提交 YAML → API Server 接收并存到 etcd。 Scheduler 决定 Pod 去哪个 Node。 Node 上的 Kubelet 把容器启动。 Controller Manager 盯着数量，缺了就补。 Service 统一出口，用户不用关心 Pod IP。 常用操作 前提是1：集群必须已经搭好（minikube/kind/k3s/云厂商的 K8S）。 2：kubectl 已经配置好 kubeconfig 3：镜像能拉取到 如果是公有镜像（比如 Docker Hub），直接写就行。 如果是私有仓库，需要配 imagePullSecrets。 # 部署应用 kubectl apply -f deployment.yaml kubectl apply -f service.yaml # 查看集群资源 kubectl get pods -o wide kubectl get deployments kubectl get services # 扩缩容 kubectl scale deployment/my-app --replicas=5 # 滚动更新 kubectl set image deployment/my-app my-app=my-registry/my-app:v2 # 回滚 kubectl rollout undo deployment/my-app
CI/CD 自动部署项目 使用Gitea Actions 1）启用Gitea Actions： 编辑你的Gitea服务器配置文件 app.ini（通常位于 /etc/gitea/或 Gitea 安装目录），添加： [actions] ENABLED = true 然后重启Gitea服务。 2）在部署服务器上安装Runner： # 1. 下载并安装 act_runner (需要sudo) sudo wget https://gitea.com/gitea/act_runner/releases/download/v0.2.10/act_runner-0.2.10-linux-amd64 -O /usr/local/bin/act_runner sudo chmod +x /usr/local/bin/act_runner # 2. 为Runner创建专用工作目录（推荐使用绝对路径） sudo mkdir -p /opt/gitea-runner sudo chown $USER:$USER /opt/gitea-runner # 将目录所有权给当前用户 cd /opt/gitea-runner # 3. 注册Runner（重要：必须添加labels标签） act_runner register --no-interactive \ --instance https://你的gitea域名.com \ --token <你的令牌> \ --name my-runner \ --labels self-hosted,linux # 这个标签必须与工作流文件中的runs-on匹配 # 4. 启动Runner（后台运行） nohup act_runner daemon > runner.log 2>&1 & # 5. 查看运行状态和日志 tail -f runner.log ps aux | grep act_runner 3）令牌获取： 如果你想给整个实例用 → 站点管理员设置 → Actions → Runners。 如果只想给某个仓库用 → 进入该仓库 → Settings → Actions → Runners。 点击 New Runner，系统会生成一个 TOKEN。 例如：abcdef1234567890 还是设置整个实例好！！ 4）将act_runner注册为服务自启动 sudo vim /etc/systemd/system/act_runner.service 写入以下内容： [Unit] Description=Gitea Actions Runner After=network.target [Service] ExecStart=/usr/local/bin/act_runner daemon WorkingDirectory=/opt/gitea-runner User=zy123 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ExecStart 指向你安装的 act_runner 路径（你已经放在 /usr/local/bin/ 了）。 WorkingDirectory 可以写你打算存放 Runner 缓存的目录（比如 /home/zy123/act_runner，你已经在这里 register 过了）。 User=zy123 保证 Runner 以你当前用户运行，而不是 root。 重新加载 systemd 配置： sudo systemctl daemon-reexec 启动并开机自启: sudo systemctl enable --now act_runner 查看运行状态: systemctl status act_runner 5）在后端仓库根目录下，新建目录和文件： .gitea/workflows/deploy.yml 写入以下内容： name: Deploy Backend on: push: branches: [ "master" ] # master 分支推送时触发 jobs: build-and-deploy: runs-on: self-hosted steps: - name: Checkout code uses: actions/checkout@v3 - name: Create application-local.yml run: | mkdir -p src/main/resources/ cat << 'EOF' > src/main/resources/application-local.yml ${{ secrets.APPLICATION_LOCAL_YML }} EOF - name: Build and restart smile-picture run: | cd docs/tags/picture-v1.0 docker compose -f docker-compose-app-v1.0.yml build smile-picture-backend docker compose -f docker-compose-app-v1.0.yml up -d smile-picture-backend - name: Cleanup dangling images run: docker image prune -f 特别注意,application-local没有用git管理，把它存在了Actions的密钥中： 然后deploy.yml中可以把里面的内容复制过去。 6）工作原理 1.Runner 接收任务 当你 push 到 main 分支时： Gitea 会触发 workflow（.gitea/workflows/deploy.yml）。 服务器上的 act_runner 会接收到这个任务，并开始执行 jobs.build-and-deploy。 2.actions/checkout@v3 这一行的作用是：它会把你刚刚 push 的最新代码 完整克隆 到 Runner 的工作目录。 /home/<你的用户>/actions-runner/_work/<repo名>/<repo名>/ 所以，这时候你的项目在 Runner 的临时目录里已经是最新代码了，不需要 git pull。 3.后续 cd docs/tag/group-buy-v3.0 都是在 这个临时目录 里执行的。 相当于你在服务器上的临时副本中： 用 docker compose build 构建镜像 用 docker compose up -d 启动/更新容器 4.Runner 每次执行 workflow 时，工作目录都会 重新 checkout 最新代码。 默认不会自动清理旧目录，但可以在 workflow 里加 actions/checkout 的参数来控制，比如 clean: true。. 7）git push之后
🧠 神经网络训练流程基础笔记 1. 数据集（Dataset） 数据集包含了 样本 (sample) 和 标签 (label) 在你的场景里： 样本 = 一个时间步的图（节点特征矩阵 + 邻接矩阵） 标签 = 每个节点是否掉线（0/1） 2. Batch（批量） Batch = 一次训练用到的一小部分样本 batch_size 表示 每次梯度更新时用多少个样本 例如： 数据集有 1000 个样本 batch_size=8 → 每次训练只拿 8 个样本来算梯度 这样能减少显存占用，也让模型更新更频繁 3. Epoch（轮次） Epoch = 把整个数据集完整训练一遍 如果： 数据集有 1000 个样本 batch_size=8 那么 1 个 epoch 需要 1000 / 8 = 125 次迭代，才能用完所有样本 训练 20 个 epoch，就相当于把数据重复“看”了 20 遍 4. 训练循环流程 一次完整训练过程一般是这样： 初始化模型参数（随机权重） 重复若干 epoch 打乱训练数据 按 batch_size 切成小批次 对每个 batch： 前向传播 (forward) 把输入样本丢进模型 得到预测结果 计算损失 (loss) 预测 vs 真实标签 的差距 反向传播 (backward) 自动求导，算出梯度 参数更新 (step) 用优化器（SGD/Adam）更新参数，让模型预测更接近标签 最终得到训练好的模型 5. 为什么要用 batch？ 全量训练 (batch_size=1000) 优点：精确 缺点：显存占用大，更新慢 小批量训练 (batch_size=8~32) 优点：节省显存，更新更频繁，收敛快 缺点：梯度有点“噪声”（但通常能帮助模型泛化） 👉 所以主流方法是 小批量训练 + 多个 epoch。 6. 直观理解 把训练比作 学生背单词： 样本 = 单词 batch_size = 一次背多少个单词（8 个单词一组，还是 50 个单词一组） epoch = 把词表完整过一遍（1000 个单词都背一遍） 多个 epoch = 重复背很多遍（记得更牢） 7. 在你现在的任务中 数据：1000 时间步 batch_size=8 → 一次训练 8 个时间步的图 epochs=20 → 每个时间步都要用到，整个过程重复 20 遍 总共更新参数的次数 = 1000/8 * 20 = 2500 次 📌 一句话总结： 训练神经网络 = 前向传播（算预测） + 损失函数（算误差） + 反向传播（算梯度） + 参数更新（优化器） 重复这个过程，按 batch 分批喂数据，按 epoch 控制重复多少轮。
非常好的问题 💡 你抓住了 FMI 的一个关键弱点：它对类不平衡（尤其是“大簇 vs 小簇”）敏感。 我给你一个非常直观的小例子（带数字），让你一步就理解为什么大簇会“主导”FMI 的结果。 🧩 一、背景 FMI 衡量“延迟聚类是否正确识别真实的同簇节点对”。 它在计算时看的是： $\text{Precision} = \frac{TP}{TP + FP}, \quad \text{Recall} = \frac{TP}{TP + FN}$ 其中 TP、FP、FN 都是在节点对级别计算的。 而节点对的数量和簇大小密切相关！ 一个簇有 $n$ 个节点，它内部的节点对数量是： n(n−1)2\frac{n(n-1)}{2} 也就是说，大簇里的“同簇节点对”远远多于小簇。 所以 大簇错误一点点，FMI 会掉很多；小簇错一大片，FMI 变化反而不大。 🧮 二、具体例子：两种簇结构 假设我们有 10 个节点： 簇类型 节点 簇大小 内部节点对数 大簇 A, B, C, D, E, F, G, H 8 $\frac{8×7}{2} = 28$ 小簇 I, J 2 $\frac{2×1}{2} = 1$ 总同簇节点对 — — 29 对 可见： 👉 只有 1/29 ≈ 3.4% 的“真实同簇节点对”来自小簇。 🚀 三、聚类情况对比 我们比较两种延迟聚类结果： ✅ 情况 1：只错了小簇 延迟聚类： (A–H 在一起)，(I)、(J) 也就是： 大簇 8 个节点保持完美； 小簇被拆散成两个单独节点。 统计： TP（正确识别的同簇节点对）= 28（全部来自大簇） FN（漏掉的同簇节点对）= 1（小簇 I–J） FP（错误合并的节点对）= 0 计算： $Precision = 28/(28+0)=1.0, \quad Recall = 28/(28+1)=0.9655$$FMI = \sqrt{1.0 × 0.9655} ≈ 0.9826$ → FMI ≈ 0.98，非常高！ 尽管你其实完全错了小簇。 ❌ 情况 2：大簇错了一点 延迟聚类： (A–E 在一起)，(F–H)、(I,J) 也就是： 大簇被拆成两部分； 小簇正确。 统计： 大簇内部正确识别的节点对： A–E：$10$ 对 F–H：$3$ 对 实际大簇原有 $28$ 对 → 漏掉 $15$ 对 TP = 10 + 3 + 1(小簇)=14 FN = 15（漏掉的大簇内部关系） FP = 0（没有错误合并） 计算： $Precision = 14/(14+0)=1.0, \quad Recall = 14/(14+15)=0.4827$$FMI = \sqrt{1.0 × 0.4827} ≈ 0.694$ → FMI ≈ 0.69，骤降！ 虽然你只是把大簇分裂了一点点。 📊 四、比较结果 情况 错误类型 FMI 实际错误影响 ✅ 情况 1 小簇完全错 0.98 几乎没影响 ❌ 情况 2 大簇拆错一半 0.69 FMI 急剧下降 💡 五、结论直觉化 观察 含义 大簇内部有很多节点对（比如 28 对） → 错一部分会影响大量 TP/FN 计数 小簇内部节点对极少（1 对） → 即使错了也几乎不影响 FMI FMI 是基于“所有节点对”的全局比例 → 大簇权重大，小簇权重极小 所以我们说： FMI 对类不平衡（簇大小差异）敏感。 FMI 的高低主要反映“大簇聚类的一致性”，小簇的错误被“淹没”。 ✅ 六、一句话总结 在 FMI 中，大簇内部节点对的数量多得多， 导致它们在计算中权重更大。 因此 FMI 更能反映“大簇聚类的好坏”， 而忽略“小簇是否聚错”。 是否希望我帮你扩展这个例子写成一段 Python 演示代码？ 我可以打印出每种情况下的 TP/FP/FN/FMI，让你直接运行看到这两个场景 FMI 变化的数值差异。
Spring AI + Ollama 1. Ollama 定位：本地/容器化的 大模型推理服务引擎 作用：负责加载模型、执行推理、返回结果 支持的模型：Qwen、DeepSeek、nomic-embed-text（embedding 模型）等 交互方式：REST API（默认 http://localhost:11434） ollama run qwen2.5:7b-instruct 2. Spring AI 定位：Spring 官方推出的 AI 接入框架 作用：统一封装对各种 AI 服务的调用逻辑，让开发者更容易接入（OpenAI、Ollama 等） 关键点 OllamaChatModel：封装了调用 Ollama 的对话接口 OllamaOptions：指定模型、参数（温度、上下文大小等） PgVectorStore：对接向量数据库（Spring AI 提供的默认向量数据库适配器。） TokenTextSplitter：默认的文本切分器 （Spring AI 内置逻辑） 3.pgVectorStore pgVectorStore 是 Spring AI 提供的向量存储接口实现，底层基于 PostgreSQL + pgvector 插件。它在项目中的作用： 1）存储向量和元数据： pgVectorStore.accept(splits); 这行代码会： 1.将 splits 中每个 Document 的 text → 调用 embedding 模型 → 生成向量 2.将生成的向量、原始文本、以及文档元数据统一存入数据库表（默认表名：vector_store）； embedding：存储生成的向量； content：存储原始文本内容； metadata：以 JSONB 形式存储元数据。 2）检索时自动生成 SQL： List<Document> documents = pgVectorStore.similaritySearch(request); 在执行检索时，pgVectorStore 会自动生成 SQL 查询， 解析查询请求中的文本； 生成查询向量； 自动构造 SQL（结合 pgvector 的 <-> 向量相似度运算符）； 按相似度排序返回最相关的文档列表。 进一步地，结合 filterExpression（例如 metadata->>'knowledge' = 'xxx'）可在相似度计算的基础上进一步进行条件过滤。 配置 ai: ollama: base-url: http://ollama:11434 #告诉 Spring AI 去 http://ollama:11434 调用模型接口。 embedding: options: num-batch: 512 #批处理参数 model: nomic-embed-text #指定使用的 embedding 模型 rag: embed: nomic-embed-text 注意，需要提前下载模型： ollama pull deepseek-r1:1.5b ollama pull qwen2.5:7b-instruct ollama pull nomic-embed-text ollama list Docker如何开启GPU加速大模型推理 巨坑！ 默认是CPU跑大模型，deepseek1.5b勉强能跑动，但速度很慢，一换qwen2.5:7b模型瞬间就跑不动了，这才发现一直在用CPU跑！！ 如何排查？ 1.查看ollama日志： load_backend: loaded CPU backend from /usr/lib/ollama/libggml-cpu-alderlake.so 2.本地cmd命令行输入，查看显存占用率。 nvidia-smi | 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A | | N/A 58C P0 24W / 110W | 2261MiB / 8188MiB | 6% Default | 设备状态： RTX 4060显卡驱动（576.02）和CUDA 12.9环境正常 当前GPU利用率仅6%（GPU-Util列） 显存占用2261MB/8188MB（约27.6%） 可见模型推理的时候压根没有用GPU！！！ 解决 参考博客：1-3 Windows Docker Desktop安装与设置docker实现容器GPU加速_windows docker gpu-CSDN博客 1）配置WSL2，打开 PowerShell（以管理员身份运行），执行以下命令： wsl --install -d Ubuntu # 安装 Linux 发行版 wsl --set-default-version 2 # 设为默认版本 wsl --update # 更新内核 重启计算机以使更改生效。 2）检查wsl是否安装成功 C:\Users\zhangsan>wsl --list --verbose NAME STATE VERSION * Ubuntu Running 2 docker-desktop Running 2 这个命令会列出所有已安装的Linux发行版及其状态。如果看到列出的Linux发行版，说明WSL已成功安装。 3）安装docker desktop默认配置： 4）保险起见也启用一下开启 Windows 的 Hyper-V 虚拟化技术： 搜索“启用或关闭Windows功能”，勾选：Hyper-V 虚拟化技术。 5） 命令提示符输入 nvidia-smi C:\Users\zhangsan>nvidia-smi Tue Aug 19 21:18:11 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 576.02 Driver Version: 576.02 CUDA Version: 12.9 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Driver-Model | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A | | N/A 55C P4 12W / 129W | 2132MiB / 8188MiB | 33% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ 若显示GPU信息（如型号、显存等），则表示支持。 6）Docker Desktop配置**（重要）** 打开 Docker 设置 → Resources → WSL Integration → 启用 Ubuntu 实例。 进入 Docker 设置 → Docker Engine，添加以下配置： { "experimental": true, "features": { "buildkit": true }, "registry-mirrors": ["https://registry.docker-cn.com"] // 可选镜像加速 } 保存并重启 Docker。 7）验证 GPU 加速 docker run --rm -it --gpus=all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled. > Windowed mode > Simulation data stored in video memory > Single precision floating point simulation > 1 Devices used for simulation MapSMtoCores for SM 8.9 is undefined. Default to use 128 Cores/SM MapSMtoArchName for SM 8.9 is undefined. Default to use Ampere GPU Device 0: "Ampere" with compute capability 8.9 > Compute 8.9 CUDA device: [NVIDIA GeForce RTX 4060 Laptop GPU] 24576 bodies, total time for 10 iterations: 17.351 ms = 348.102 billion interactions per second = 6962.039 single-precision GFLOP/s at 20 flops per interaction 成功输出应包含 GPU 型号及性能指标 [NVIDIA GeForce RTX 4060 Laptop GPU] 8）Ollama验证： 在容器中也能显示GPU了，说明配置成功了！！！ version: '3.9' services: ollama: image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/ollama:0.5.10 container_name: ollama restart: unless-stopped ports: - "11434:11434" volumes: - ./ollama:/root/.ollama runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=all 什么原理？ GPU 驱动仍运行在 Windows 主机上；WSL2 作为 Linux 内核子系统，共享访问该 GPU 驱动的底层接口；Docker Desktop 在 WSL2 内运行（作为 Linux 虚拟环境）， 通过 NVIDIA Container Runtime (nvidia-container-toolkit) 把 GPU 设备挂载进容器； 最终容器内程序（如 PyTorch、Ollama）能直接使用 GPU 计算。 RAG（检索增强生成） postgre向量数据库 PostgreSQL也是结构化数据库（关系型数据库）！！！ 所有数据都遵守固定的结构（Schema）； 每一列（Column）定义了数据类型； 每一行（Row）表示一条记录； 表结构： @Bean public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel).build(); } PgVectorStore 会自动检测并在首次使用时创建所需的向量表，默认表名是 vector_store；默认表结构是固定格式（id, content, metadata, embedding）。 PgAdmin软件下：ai-rag-knowledge -> 架构 -> public -> 表 -> vector_store id → 每条数据的唯一标识（主键/uuid）。 content → 存放 chunk 的原始文本。 metadata → 存放 JSON 格式的额外信息（文件名、路径、知识库标签等）。 embedding → 存放向量，类型是 vector(N)（N 由 Embedding 模型决定，比如 768）。 所有文件切分出来的 chunk 都存在这一张表里，每条记录就是一个 chunk。 表的主要用途 1）相似度检索（Retrieval）：用embedding <-> 查询向量 进行向量距离计算，找出与输入文本语义最相近的片段。其中<-> 是 pgvector 的距离运算符 2）结果展示（Content Retrieval）：检索后，取出距离最近的若干条记录的 content，作为模型回答时的上下文。 3）溯源/过滤：通过 metadata 字段中的信息（如文件名、标签、知识类别等）进行结果筛选或来源追踪，例如：其中 ->> 从 JSON 对象中取出某个字段的值，并以文本（text）形式返回。 INSERT INTO vector_store (content, metadata) VALUES ('公司年度财报分析', '{"knowledge": "finance", "source": "report.pdf"}'), ('销售策略优化', '{"knowledge": "marketing", "source": "plan.docx"}'), ('财务合规指引', '{"knowledge": "finance", "source": "policy.pdf"}'), ('技术架构设计', '{"knowledge": "engineering", "source": "design.md"}'); 现在你想只查出属于 “财务领域（finance）” 的内容，可以这样写： SELECT id, content, metadata FROM vector_store WHERE metadata->>'knowledge' = 'finance'; 查询 同一般的SQL语句。 vector_dims(embedding)是内置函数，返回维度大小。 还有 metadata 和embedding 列显示不下。 A. 索引/入库（Ingestion） 这里用了SpringAI提供的：TikaDocumentReader 和 pgVectorStore 文档读取器 用 TikaDocumentReader 把上传的 PDF、Word、TXT、PPT 等文件解析成 List<Document>。 每个 Document 包含 text 和 metadata（比如 page、source）。 相当于是“把二进制文件 → 转成纯文本段落”。 TikaDocumentReader documentReader = new TikaDocumentReader(resource); List<Document> documents = documentReader.get(); 清洗过滤 剔除空文本，避免无效数据进入后续流程。 List<Document> docs = new ArrayList<>(documents); docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty()); if (docs.isEmpty()) { log.warn("文件内容为空，跳过处理: {}", normalizedPath); return; } 文档切分器 用 TokenTextSplitter 把每个 Document 再切成 chunk（默认 800 tokens 一段，可配置）。 目的是避免超长文本超过 Embedding 模型的输入限制。 @Bean public TokenTextSplitter tokenTextSplitter() { return TokenTextSplitter.builder() .withChunkSize(600) // 每段最多 600 token .withMinChunkSizeChars(300) // 每段至少 300 字符 .withMinChunkLengthToEmbed(10) .withMaxNumChunks(10000) .withKeepSeparator(true) .build(); } //使用，省略依赖注入 List<Document> splits = tokenTextSplitter.apply(docs); withMinChunkSizeChars(300): 如果某个切分块的文本少于 300 个字符，TokenTextSplitter 会避免直接拆分它，而是尝试 合并它与下一个切分块，直到符合字符数要求。 打元数据 给原始文档和 chunk 都加上 knowledge（ragTag）、path、original_filename 等信息。 方便后续检索时追溯来源。 docs.forEach(doc -> { doc.getMetadata().put("knowledge", ragTag); doc.getMetadata().put("path", normalizedPath); doc.getMetadata().put("original_filename", originalFilename); }); splits.forEach(doc -> { doc.getMetadata().put("knowledge", ragTag); doc.getMetadata().put("path", normalizedPath); doc.getMetadata().put("original_filename", originalFilename); }); Embedding 模型 对 每个 chunk 调用 Ollama 的 nomic-embed-text，生成一个固定维度的向量（如 768 维）。 注意：这是对 chunk 整体嵌入，不是对单个 token。 向量存储 //配置类 @Bean public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel).build(); } //使用，省略依赖注入 pgVectorStore.accept(splits); 用 pgvector 存储 [embedding 向量 + metadata + 原始文本]。 向量维度由 Embedding 模型决定，pgvector 表的维度必须保持一致（如 768/1024/1536）。 B. 检索/回答（Query） 接收用户问题 输入用户的问题文本 message。 相似度检索 使用同一个 Embedding 模型（如 nomic-embed-text）将问题转为查询向量。 在向量库中用 余弦相似度 / 内积 搜索相似 chunk。 可附加条件过滤：如 knowledge == 'xxx'，只在某个知识库范围内查。 常用参数： topK：取最相似的前 K 个结果（例如 5~8）。 minSimilarityScore（可选）：过滤低相关度结果。 // 1) 相似度检索（带 ragTag 过滤） List<Document> documents = pgVectorStore.similaritySearch( SearchRequest.builder() .query(message) .topK(8) .filterExpression("knowledge == '" + ragTag + "'") .build() ); List<Document> documents = pgVectorStore.similaritySearch(request); 拼装文档上下文 把检索到的文档片段拼接成系统提示中的 DOCUMENTS 部分。 可以在拼接时附带 metadata（如文件名、页码），方便溯源。 String documentContent = documents.stream() .map(doc -> "[来源:" + doc.getMetadata().get("original_filename") + "]\n" + doc.getText()) .collect(joining("\n\n---\n\n")); 构造提示词（Prompt） 使用 SystemPromptTemplate 注入 DOCUMENTS 内容。 System Prompt 应该放在 用户消息之前，确保模型优先遵循规则。 Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT) .createMessage(Map.of("documents", documentContent)); List<Message> messages = new ArrayList<>(); messages.add(ragMessage); // 先放系统提示 messages.add(new UserMessage(message)); 调用对话模型（流式返回） return ollamaChatModel.stream(new Prompt( messages, OllamaOptions.builder().model(model).build() )); 优化 1.优化分词逻辑 这块比较复杂，要切的恰到好处...切的不大不小。 2.更新向量嵌入模型 MCP服务 简介 - 模型上下文协议 MCP 是“模型与外部世界沟通的桥梁”。它让模型不仅能聊天，还能调用本地文件系统、数据库、网络 API 等服务，实现“有工具可用的 AI”。 基础介绍 角色 说明 举例 🖥️ MCP Server（服务端） 提供“工具能力”，比如操作文件系统、调用系统命令、执行数据库查询等。 @modelcontextprotocol/server-filesystem、mcp-server-computer.jar 🤖 MCP Client（客户端） 嵌入在应用（你的 Spring Boot）中，让模型能访问这些工具。 由 spring-ai-mcp-client-webflux-spring-boot-starter 实现 快速使用 1）引入依赖（POM） <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId> </dependency> 2）配置 MCP 服务（Server） 创建配置文件：resources/config/mcp-servers-config.json { "mcpServers": { "filesystem": { "command": "npx.cmd", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "D:/folder/MCP-test", "D:/folder/MCP-test" ] }, "mcp-server-computer": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "D:/folder/study/apache-maven-3.8.4/mvn_repo/edu/whut/mcp/mcp-server-computer/1.0.0/mcp-server-computer-1.0.0.jar" ] } } } filesystem：定义了一个 MCP 文件系统服务； 通过 npx 启动； 调用包 @modelcontextprotocol/server-filesystem； 并将本地路径 D:/folder/MCP-test 暴露给模型访问。 mcp-server-computer：定义了一个自定义的 Java MCP 服务； 使用 java -jar 启动； 通过标准输入输出（stdio）与主程序通信。 Spring配置文件（application.yml）配置 MCP 服务加载路径 spring: ai: mcp: servers-config: classpath:config/mcp-servers-config.json 3）安装并启用 Filesystem MCP 服务 需要提前下载该文件服务，官网: https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem （创建、移动目录，读取、写入文件...） 先装 Node.js，配置环境变量 node -v npm -v 安装 MCP 文件系统服务 npm install -g @modelcontextprotocol/server-filesystem 4）配置客户端 Spring AI 通过 ChatClient 调用大模型，如果你使用多个模型（如 Ollama、OpenAI），可以通过不同的 ChatModel Bean 实例来区分。 @Bean public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi) { return OllamaChatModel.builder() .ollamaApi(ollamaApi) .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); } @Bean public ChatClient.Builder chatClientBuilder(OllamaChatModel ollamaChatModel) { return new DefaultChatClientBuilder( ollamaChatModel, ObservationRegistry.NOOP, (ChatClientObservationConvention) null ); } 注意，ollamaChatModel()是基础模型，创建与Ollama服务交互的底层Chat模型；chatClientBuilder()是客户端构建器，提供高级功能封装（如工具调用） 5）模型调用示例 ollamaChatModel 是连接本地 Ollama 服务的客户端； 具体使用哪个模型（deepseek-r1:1.5b、qwen2.5:7b-instruct 等）是在调用时通过 OllamaOptions 决定的: ChatResponse response = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build() .prompt("你好，介绍一下你自己") .call(); 6）测试 MCP 调用 查看当前可用工具 @GetMapping("/tools") public Object listTools() { return Arrays.stream(tools.getToolCallbacks()) .map(cb -> Map.of( "name", cb.getName(), "description", cb.getDescription() )) .toList(); } 自动调用工具（含 MCP） @GetMapping("/test-workflow") public String testWorkflow(@RequestParam String question) { var chatClient = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); return chatClient.prompt(question).tools(tools).call().chatResponse().toString(); } 当模型发现某个问题需要使用外部工具（如文件读写、计算服务）时，会自动调用注册在 MCP 中的工具进行操作。 有时候大模型不会去调用Tools，可能是模型能力不够。 注意，docker部署后，这个路径还没改！！！自然查不到可用的工具，因为不是在本地windows中了。 自定义MCP服务 目录结构 mcp-server-demo/ ├─ src/ │ └─ main/ │ ├─ java/…/DemoApplication.java │ ├─ java/…/NoteService.java // 你的工具：@Tool 方法 │ └─ resources/application.yml // 仅需几行 └─ pom.xml POM 依赖 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> 关键配置 spring: application: name: mcp-server-demo ai: mcp: server: name: ${spring.application.name} version: 1.0.0 # 让它安静点，避免把 MCP JSON 混进正常日志 logging: level: org.springframework.ai.mcp: INFO pattern: console: server: main: banner-mode: off # 非 Web 进程（可选） # web-application-type: none 入口 & 工具 DemoApplication.java @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } 启动配置。 NoteService.java @Service public class NoteService { @Tool(description = "把输入转成大写并附带字符数") public Map<String, Object> toUpper( @ToolParam(name = "text", description = "要转换的文本") String text) { if (text == null) text = ""; return Map.of( "upper", text.toUpperCase(), "length", text.length() ); } } 注意，你的服务方法必须配置 @Tool(description = "获取电脑配置") 多一个注解，就向外多提供一个工具！！！ 生成Jar包 在客户端项目中引用 { "mcpServers": { "mcp-server-demo": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "C:/path/to/target/mcp-server-demo-1.0.0.jar" ] } } } 注意上一步生成的jar包要放到这个"C:/path/to/target/mcp-server-demo-1.0.0.jar"中。 底层调用逻辑 一、启动客户端 假设你运行的客户端配置里包含： "mcpServers": { "filesystem": {...}, "mcp-server-computer": {...} } 当客户端启动时，会做以下几步： 1）客户端启动子进程 客户端发现你声明了两个 MCP 服务器，于是准备分别启动它们。 npx.cmd -y @modelcontextprotocol/server-filesystem D:/folder/MCP-test D:/folder/MCP-test java -Dspring.ai.mcp.server.stdio=true -jar D:/folder/.../mcp-server-computer-1.0.0.jar 这两个命令都会启动新的独立进程，“挂在”客户端进程下面，通过标准输入/输出通信。 2）客户端 → 服务端 “握手”（initialize） 客户端通过每个子进程的 stdin 写入 MCP 初始化请求： { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientInfo": {"name": "MyClient", "version": "1.0"}, "capabilities": {} } } 3）服务端 → 客户端 回应握手结果 你的 mcp-server-computer.jar在内部收到这个 JSON-RPC 请求后，返回： { "jsonrpc": "2.0", "id": 1, "result": { "serverInfo": {"name": "mcp-server-computer", "version": "1.0.0"}, "capabilities": {"tools": true} } } 客户端读取这条 JSON（从 stdout），于是握手成功 4）客户端请求工具清单 紧接着客户端发： { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} } 服务端返回： { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "queryConfig", "description": "获取电脑配置", ... } ] } } 客户端现在就知道：“这个 MCP Server 提供一个叫 queryConfig 的工具”。 二、用户发起请求 1）用户发起问题：“请读取这台电脑的配置”。 2）客户端构造 Chat 请求 将用户消息 + 系统消息 + 工具的 JSON Schema（名称、入参、描述）一起发给大模型。 请求里不仅包含用户输入，还要告诉模型——“你可以调用这些工具，这些工具的参数长这样。” 在 Spring AI 中，这一步通常由 ChatClient 或底层 ChatModel 完成 @GetMapping("/test-workflow") public String testWorkflow(@RequestParam String question) { log.info("Workflow测试请求接收 - 问题: {}", question); var chatClient = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); ChatResponse response = chatClient .prompt(question) .system("你必须使用提供的工具完成任务，禁止凭空回答；若没有调用任何工具，不要返回答案。") .tools(tools) .call() .chatResponse(); log.info("工具调用情况: {}", tools.getToolCallbacks()); log.info("模型响应: {}", response); return response.toString(); } 实际发送给模型的底层 payload 类似这样： { "model": "qwen2.5:7b-instruct", "messages": [ { "role": "system", "content": "你必须使用提供的工具完成任务..." }, { "role": "user", "content": "在 D:/folder/MCP-test 下创建配置.txt..." } ], "tools": [ { "type": "function", "function": { "name": "queryConfig", "description": "获取电脑配置（操作系统、用户信息、Java环境等）", "parameters": { "type": "object", "properties": { "computer": { "type": "string", "description": "电脑名称，可为空" } } } } }, { "type": "function", "function": { "name": "write_file", "description": "创建一个文件并写入内容", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"} } } } } ], "tool_choice": "auto" } 3）模型分析任务 → 决定调用哪个工具 大模型在接收到 prompt 后，会“思考”： “要回答这个问题，我需要外部信息吗？” “有一个叫 queryConfig 的工具能获取电脑配置，那我就调用它。” 现代指令模型（包括 Qwen、GPT-4o、Claude、Llama）都经过一种专门的微调（finetune）： 当输入里包含 "tools"、"parameters"、"function" 这样的字段时， 模型学会： 识别哪些任务需要调用工具； 生成标准 JSON 格式的 tool_calls； 不胡编乱造工具名； 遵守 schema 中参数名的拼写和结构。 于是它返回结构化响应（OpenAI function-calling 格式）： { "id": "chatcmpl-01", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "tool_calls": [ { "id": "call_1", "type": "function", "function": { "name": "queryConfig", "arguments": "{\"computer\": \"DESKTOP-01\"}" } } ] } } ] } 4）客户端执行工具调用（MCP 调用链） Spring AI 的 DefaultToolCallingManager 会检测到（框架自动实现）： if (response.hasToolCalls()) { executeToolCalls(response.getToolCalls()); } 客户端解析出模型请求调用的工具名 queryConfig，然后通过 MCP 协议向对应的 MCP Server（你的 mcp-server-computer）发送调用请求（通过 stdin）： { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "queryConfig", "arguments": { "computer": "DESKTOP-01" } } } 注意，默认的工具名就是函数名！！！ 你的 MCP Server（Spring Boot 进程）接收后，由框架自动路由到 @Tool 方法： @Service public class ComputerService { @Tool(description = "获取电脑配置") public ComputerFunctionResponse queryConfig( @ToolParam(name = "computer", description = "电脑名称") String computer ) { log.info("获取电脑配置信息: {}", computer); // 读取系统属性 + systeminfo 等命令 ... return response; // 返回 JSON 对象 } } 6）客户端将结果反馈回模型（第二轮） 客户端收到结果后，会把它作为新的上下文输入发回模型： { "role": "tool", "tool_call_id": "call_1", "name": "queryConfig", "content": "{\"osName\": \"Windows 11\", ... }" } 模型在这时再进行一轮“总结性思考”： “我得到了电脑配置结果，可以组织成自然语言回答。” 最终返回： { "role": "assistant", "content": "你的电脑运行的是 Windows 11，架构为 x64，用户名为 Jerry。" } sequenceDiagram participant User participant Client participant Model participant MCPServer as MCP Server User->>Client: "请读取这台电脑的配置" Client->>Model: 封装 Prompt + Tool Schema 发送请求 Model-->>Client: 返回 tool_calls: queryConfig(...) Client->>MCPServer: 通过 MCP stdin 发送 tools/call 请求 MCPServer-->>Client: 执行 Java @Tool 方法 返回 JSON 结果 Client->>Model: 将工具结果再发给模型 Model-->>Client: 生成自然语言回答 Client-->>User: 输出最终文本 如果有长的调用链 当模型需要调用多个工具（例如：create_directory → queryConfig → write_file）， Spring AI 会自动进入一个「循环式调用流程」，直到模型自己不再请求调用工具为止。 框架就会自动: 1.发现模型要调用工具； 2.执行工具； 3.把结果回灌给模型； 4.模型如果再要求调用下一个工具，再执行； 5.一直到模型返回纯文本。 while (true) { ChatResponse response = model.call(currentPrompt); if (response.hasToolCalls()) { // 1. 模型请求调用工具 for (ToolCall call : response.getToolCalls()) { Object result = executeTool(call.name, call.arguments); // 2. 把结果封装成“tool_result”消息 currentPrompt.addToolResult(call.id, call.name, result); } } else { // 没有工具调用 = 模型给出最终自然语言 return response; } }
设计模式 单例模式 懒汉 注意，单例模式的构造函数是私有的！ public class LazySingleton { private static volatile LazySingleton INSTANCE; private LazySingleton() {} public static LazySingleton getInstance() { if (INSTANCE == null) { // 第一次检查 synchronized (LazySingleton.class) { if (INSTANCE == null) { // 第二次检查 INSTANCE = new LazySingleton(); } } } return INSTANCE; } } 第一次检查：防止重复实例化、以及进行synchronized同步块（防止每次加锁的开销）。 第二次检查：防止有多个线程同时通过第一次检查，然后依次进入同步块后，创建N个实例。 volatile：防止指令重排序。 instance = new LazySingleton(); 正确顺序是： 1️⃣ memory = allocate(); // 分配内存 2️⃣ ctorInstance(memory); // 调用构造函数，初始化对象 3️⃣ INSTANCE = memory; // 将对象引用赋值给 INSTANCE 由于 JVM 和 CPU 都可能对指令进行优化重排，步骤 ② 和 ③ 的顺序可能被交换！ 1️⃣ memory = allocate(); // 分配内存 3️⃣ INSTANCE = memory; // 把引用赋给 INSTANCE（但未初始化完） 2️⃣ ctorInstance(memory); // 调用构造函数 线程 A刚执行完赋值（3），但没调用构造函数；此时线程B来了，发现 INSTANCE != null，直接返回这个未初始化完成的对象！！！ 饿汉 public class EagerSingleton { // 类加载时就初始化实例 private static final EagerSingleton INSTANCE = new EagerSingleton(); // 私有构造函数 private EagerSingleton() {} // 全局访问点 public static EagerSingleton getInstance() { return INSTANCE; } } 工厂模式 简单工厂 // 产品接口 interface Product { void use(); } // 具体产品A class ConcreteProductA implements Product { @Override public void use() { System.out.println("使用产品A"); } } // 具体产品B class ConcreteProductB implements Product { @Override public void use() { System.out.println("使用产品B"); } } class SimpleFactory { // 根据参数创建不同的产品 public static Product createProduct(String type) { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); default: throw new IllegalArgumentException("未知产品类型"); } } } public class Client { public static void main(String[] args) { // 通过工厂创建产品 Product productA = SimpleFactory.createProduct("A"); productA.use(); // 输出: 使用产品A Product productB = SimpleFactory.createProduct("B"); productB.use(); // 输出: 使用产品B } } 缺点：添加新产品需要修改工厂类（违反开闭原则） 抽象工厂 抽象工厂模式是一种创建型设计模式，它提供一个接口用于创建相关或依赖对象的家族，而不需要明确指定具体类。 // 抽象产品接口 interface Button { void render(); } interface Checkbox { void render(); } // 具体产品实现 - Windows 风格 class WindowsButton implements Button { @Override public void render() { System.out.println("渲染一个 Windows 风格的按钮"); } } class WindowsCheckbox implements Checkbox { @Override public void render() { System.out.println("渲染一个 Windows 风格的复选框"); } } // 具体产品实现 - MacOS 风格 class MacOSButton implements Button { @Override public void render() { System.out.println("渲染一个 MacOS 风格的按钮"); } } class MacOSCheckbox implements Checkbox { @Override public void render() { System.out.println("渲染一个 MacOS 风格的复选框"); } } // 抽象工厂接口 interface GUIFactory { Button createButton(); Checkbox createCheckbox(); } // 具体工厂实现 - Windows class WindowsFactory implements GUIFactory { @Override public Button createButton() { return new WindowsButton(); } @Override public Checkbox createCheckbox() { return new WindowsCheckbox(); } } // 具体工厂实现 - MacOS class MacOSFactory implements GUIFactory { @Override public Button createButton() { return new MacOSButton(); } @Override public Checkbox createCheckbox() { return new MacOSCheckbox(); } } // 客户端代码 public class Application { private Button button; private Checkbox checkbox; public Application(GUIFactory factory) { button = factory.createButton(); checkbox = factory.createCheckbox(); } public void render() { button.render(); checkbox.render(); } public static void main(String[] args) { // 根据配置或环境选择工厂 GUIFactory factory; String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("win")) { factory = new WindowsFactory(); } else { factory = new MacOSFactory(); } Application app = new Application(factory); app.render(); } } 观察者模式 是一种行为型设计模式，定义对象之间一对多的依赖关系，当一个对象（被观察者 / Subject）状态发生变化时 ，会自动通知所有依赖它的对象（观察者 / Observer），实现事件通知机制。 典型应用场景： GUI 事件监听（按钮点击通知多个监听器） 消息订阅/发布系统（发布者-订阅者） Spring 的事件机制（ApplicationEvent） 数据模型变化 → 界面自动更新（MVC、MVVM） 模板方法 核心思想： 在抽象父类中定义算法骨架（固定执行顺序），把某些可变步骤留给子类重写；调用方只用模板方法，保证流程一致。 如果仅仅是把重复的方法抽取成公共函数，不叫模板方法！模板方法要设计算法骨架！！！ Client ───▶ AbstractClass ├─ templateMethod() ←—— 固定流程 │ step1() │ step2() ←—— 抽象，可变 │ step3() └─ hookMethod() ←—— 可选覆盖 ▲ │ extends ┌──────────┴──────────┐ │ ConcreteClassA/B… │ 示例： // 1. 抽象模板 public abstract class AbstractDialog { // 模板方法：固定调用顺序，设为 final 防止子类改流程 public final void show() { initLayout(); bindEvent(); beforeDisplay(); // 钩子，可选 display(); afterDisplay(); // 钩子，可选 } // 具体公共步骤 private void initLayout() { System.out.println("加载通用布局文件"); } // 需要子类实现的抽象步骤 protected abstract void bindEvent(); // 钩子方法，默认空实现 protected void beforeDisplay() {} protected void afterDisplay() {} private void display() { System.out.println("弹出对话框"); } } // 2. 子类：登录对话框 public class LoginDialog extends AbstractDialog { @Override protected void bindEvent() { System.out.println("绑定登录按钮事件"); } @Override protected void afterDisplay() { System.out.println("focus 到用户名输入框"); } } // 3. 调用 public class Demo { public static void main(String[] args) { AbstractDialog dialog = new LoginDialog(); dialog.show(); /* 输出： 加载通用布局文件 绑定登录按钮事件 弹出对话框 focus 到用户名输入框 */ } } 要点 复用公共流程：initLayout()、display() 写一次即可。 限制流程顺序：show() 定为 final，防止子类乱改步骤。 钩子方法：子类可选择性覆盖（如 beforeDisplay）。 策略模式 核心思想： 将可以互换的算法或行为抽象为独立的策略类，运行时由**上下文类（Context）**选择合适的策略对象去执行。调用方（Client）只依赖统一的接口，不关心具体实现。 ┌───────────────┐ │ Client │ └─────▲─────────┘ │ has-a ┌─────┴─────────┐ implements │ Context │────────────┐ ┌──────────────┐ │ (使用者) │ strategy └─▶│ Strategy A │ └───────────────┘ ├──────────────┤ │ Strategy B │ └──────────────┘ // 策略接口 public interface PaymentStrategy { void pay(int amount); } // 策略A：微信支付 @Service("wechat") public class WechatPay implements PaymentStrategy { public void pay(int amount) { System.out.println("使用微信支付 " + amount + " 元"); } } // 策略B：支付宝支付 @Service("alipay") public class Alipay implements PaymentStrategy { public void pay(int amount) { System.out.println("使用支付宝支付 " + amount + " 元"); } } // 上下文类 public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void execute(int amount) { strategy.pay(amount); } } // 调用方 public class Main { public static void main(String[] args) { PaymentContext ctx = new PaymentContext(new WechatPay()); ctx.execute(100); ctx = new PaymentContext(new Alipay()); ctx.execute(200); } } 下面有更优雅的策略选择方式！ Spring集合自动注入 在策略、工厂、插件等模式中，经常需要维护**“策略名 → 策略对象”**的映射。Spring 可以通过 Map<String, 接口类型> 一次性注入所有实现类。 @Resource private final Map<String, IDiscountCalculateService> discountCalculateServiceMap; 字段类型：Map<String, IDiscountCalculateService> key—— Bean 的名字 默认是类名首字母小写 (mjCalculateService) 或者你在实现类上显式写的 @Service("MJ") value —— 那个实现类对应的实例 Spring 机制： 启动时扫描所有实现 IDiscountCalculateService 的 Bean。 把它们按 “BeanName → Bean 实例” 的映射注入到这张 Map 里。 你一次性就拿到了“策略字典”。 示例： // 上下文类：自动注入所有策略 Bean @Component @RequiredArgsConstructor public class PaymentContext { // key 为 Bean 名（如 "wechat"、"alipay"），value 为策略实例 private final Map<String, PaymentStrategy> paymentStrategyMap; public void pay(String strategyKey, int amount) { PaymentStrategy strategy = paymentStrategyMap.get(strategyKey); if (strategy == null) { throw new IllegalArgumentException("无匹配支付方式: " + strategyKey); } strategy.pay(amount); } } // 调用方示例 @Component @RequiredArgsConstructor public class PaymentService { private final PaymentContext paymentContext; public void process() { paymentContext.pay("wechat", 100); // 输出：使用微信支付 100 元 paymentContext.pay("alipay", 200); // 输出：使用支付宝支付 200 元 } } 模板方法+策略模式 本项目的价格试算同时用了策略模式 + 模板方法模式： 策略模式（Strategy）： IDiscountCalculateService 是策略接口；ZKCalculateService、ZJCalculateService ...是可替换的折扣策略（@Service("ZK") / @Service("ZJ") 作为选择键）。外部可以根据活动配置里的类型码选哪个实现来算价——这就是“运行时可切换算法”。 模板方法模式（Template Method）： AbstractDiscountCalculateService#calculate(...) 把共同流程固定下来（先进行人群校验 → 计算优惠后价格），并把“真正的计算”这一步延迟到子类通过 doCalculate(...) 实现。 责任链 应用场景：日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方，都非常适合责链模式。 场景：员工报销审批 组长审批 报销单先到组长这里。 组长要么通过，要么驳回；如果通过，就传递给下一个。 部门经理审批 组长通过后，报销单自动流转到部门经理。 部门经理再看金额和合理性，要么通过，要么驳回；如果通过，就继续往下。 财务审批 部门经理通过后，单子来到财务。 财务校验发票、预算，要么通过，要么驳回；如果通过，就继续。 总经理审批 如果金额超过某个阈值（比如 5 万），最后需要总经理签字。 总经理通过后，整个审批链结束。 典型的责任链模式要点： 解耦请求发送者和处理者：调用者只持有链头，不关心中间环节。 动态组装：通过 appendNext 可以灵活地增加、删除或重排链上的节点。 可扩展：新增处理逻辑只需继承 AbstractLogicLink 并实现 apply，不用改动已有代码。 单实例链 可以理解成“单向、单链表式的链条”：每个节点只知道自己的下一个节点（next），链头只有一个入口。 你可以在启动或运行时动态组装：head.appendNext(a).appendNext(b).appendNext(c); T / D / R 是啥？ T：请求的静态入参（本次请求的主要数据）。 D：动态上下文（链路里各节点共享、可读写的状态容器，比如日志收集、校验中间结果）。 R：最终返回结果类型。 1）接口定义：ILogicChainArmory<T, D, R> 提供添加节点方法和获取节点 // 定义了“链条组装”的最小能力：能拿到下一个节点、也能把下一个节点接上去 public interface ILogicChainArmory<T, D, R> { // 获取当前节点的“下一个”处理者 ILogicLink<T, D, R> next(); // 把新的处理者挂到当前节点后面，并返回它（方便链式 append） ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next); } 2）ILogicLink<T, D, R> 继承自 ILogicChainArmory<T, D, R>，并额外声明了核心方法 apply // 真正的“处理节点”接口：在具备链条组装能力的基础上，还要能“处理请求” public interface ILogicLink<T, D, R> extends ILogicChainArmory<T, D, R> { R apply(T requestParameter, D dynamicContext) throws Exception; } 3）抽象基类：AbstractLogicLink，提供了责任链节点的通用骨架，（保存 next、实现 appendNext/next()、以及一个便捷的 protected next(...)，这样具体的节点类就不用重复这些代码，真正的业务处理逻辑仍然交由子类去实现 apply(...)。 // 抽象基类：大多数节点都可以继承它，避免重复写“组装链”的样板代码 public abstract class AbstractLogicLink<T, D, R> implements ILogicLink<T, D, R> { // 指向“下一个处理者”的引用 private ILogicLink<T, D, R> next; @Override public ILogicLink<T, D, R> next() { return next; } @Override public ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next) { this.next = next; return next; // 返回 next 以便连续 append，类似 builder } /** * 便捷方法：当前节点决定“交给下一个处理者” */ protected R next(T requestParameter, D dynamicContext) throws Exception { // 直接把请求丢给下一个节点继续处理 // 注意：这里假设 next 一定存在；实际项目里建议判空以免 NPE（见下文改进建议） return next.apply(requestParameter, dynamicContext); } } 子类只需要继承 AbstractLogicLink 并实现 apply(...)： 能处理就处理（并可选择直接返回，终止链条）。 不处理或处理后仍需后续动作，就 return next(requestParameter, dynamicContext) 继续传递。 4）实现子类 @Component public class AuthLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { if (!ctx.isAuthenticated()) { // 未认证：立刻终止；也可以在这里构造一个标准错误响应返回 throw new UnauthorizedException(); } // 认证通过，继续下一个环节 return next(req, ctx); } } @Component public class LoggingLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { System.out.println("Request received: " + req); return next(req, ctx); } } @Component public class BusinessLogicLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { // 业务逻辑... return new Response(...); } } 5）组装链 @Configuration @RequiredArgsConstructor public class LogicChainFactory { private final AuthLink authLink; private final LoggingLink loggingLink; private final BusinessLogicLink businessLogicLink; @Bean public ILogicLink<Request, Context, Response> logicChain() { return authLink .appendNext(loggingLink) .appendNext(businessLogicLink); } } 示例图： AuthLink.apply └─▶ LoggingLink.apply └─▶ BusinessLogicLink.apply └─▶ 返回 Response 这种模式链上的每个节点都手动 next()到下一节点。 多实例链1 以上是单例链，即只能创建一条链；比如A->B->C，不能创建别的链，因为节点Bean是单例的，如果创别的链会导致指针引用错误！！！ 如果想变成多例链： 1）节点由默认的单例模式改为原型模式： @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class A extends AbstractLogicLink<Req, Ctx, Resp> { ... } 2）组装链的时候注明不同链的bean名称： /** 全局唯一链：A -> B -> C */ @Bean("chainABC") public ILogicLink<Req, Ctx, Resp> chainABC() { A a = aProvider.getObject(); B b = bProvider.getObject(); C c = cProvider.getObject(); return a.appendNext(b).appendNext(c); // 返回链头 a } /** 全局唯一链：A -> C */ @Bean("chainAC") public ILogicLink<Req, Ctx, Resp> chainAC() { A a = aProvider.getObject(); C c = cProvider.getObject(); return a.appendNext(c); // 返回链头 a（另一套实例） } 多实例链2 前面是每个节点自己要维护下一个节点。 authLink.appendNext(loggingLink).appendNext(businessLogicLink); 而这里，节点只管apply，链路的遍历由容器（双向链表）来维护。 /** * 通用逻辑处理器接口 —— 责任链中的「节点」要实现的核心契约。 */ public interface ILogicHandler<T, D, R> { /** * 默认的 next占位实现，方便节点若不需要向后传递时直接返回 null。 */ default R next(T requestParameter, D dynamicContext) { return null; } /** * 节点的核心处理方法。 */ R apply(T requestParameter, D dynamicContext) throws Exception; } /** * 业务链路容器 —— 双向链表实现，同时实现 ILogicHandler，从而可以被当作单个节点使用。 */ public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{ public BusinessLinkedList(String name) { super(name); } /** * BusinessLinkedList是头节点，它的apply方法就是循环调用后面的节点，直至返回。 * 遍历并执行链路。 */ @Override public R apply(T requestParameter, D dynamicContext) throws Exception { Node<ILogicHandler<T, D, R>> current = this.first; // 顺序执行，直到链尾或返回结果 while (current != null) { ILogicHandler<T, D, R> handler = current.item; R result = handler.apply(requestParameter, dynamicContext); if (result != null) { // 节点命中，立即返回 return result; } //result==null，则交给那一节点继续处理 current = current.next; } // 全链未命中 return null; } } /** * 链路装配工厂 —— 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList 中。 */ public class LinkArmory<T, D, R> { private final BusinessLinkedList<T, D, R> logicLink; /** * @param linkName 链路名称，便于日志排查 * @param logicHandlers 节点列表，按传入顺序链接 */ @SafeVarargs public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) { logicLink = new BusinessLinkedList<>(linkName); for (ILogicHandler<T, D, R> logicHandler: logicHandlers){ logicLink.add(logicHandler); } } /** 返回组装完成的链路 */ public BusinessLinkedList<T, D, R> getLogicLink() { return logicLink; } } //工厂类，可以定义多条责任链，每条有自己的Bean名称区分。 @Bean("tradeRuleFilter") public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) { // 1. 组装链 LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory = new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter); // 2. 返回链容器（即可作为责任链使用） return linkArmory.getLogicLink(); } 示例图： BusinessLinkedList.apply ←─ 只有这一层在栈里 while 循环： ├─▶ 调用 ActivityUsability.apply → 返回 null → 继续 ├─▶ 调用 UserTakeLimit.apply → 返回 null → 继续 └─▶ 调用 ... → 返回 Result → break 链头拿着“游标”一个个跑，节点只告诉“命中 / 未命中”。 这里无需把节点改为原型模式，也可以实现多例链，因为由双向链表BusinessLinkedList 负责保存链路关系和推进执行，而ILogicHandler节点本身不再保存 next 指针，所以它们之间没有共享可变状态。 本项目中使用多实例链2，有以下场景： 一、拼团「锁单前」校验链 目标：在真正锁单前把“活动有效性 / 用户参与资格 / 可用库存”一口气校清楚，避免后续回滚。 1.活动有效性校验 ActivityUsability （当前时间是否早于活动截止时间） 2.用户可参与活动次数校验 UserTakeLimitRuleFilter（默认用户只可参与一次拼团） 3.剩余库存校验 TeamStockOccupyRuleFilter（可能同时有多人点击参与当前拼团，尝试抢占库存，仅部分人可通过校验。） 校验通过方可进行真正的锁单。 二、交易结算校验链 1.渠道黑名单校验 SCRuleFilter：某签约渠道下架/风控拦截，禁止结算。 2.外部单号校验 OutTradeNoRuleFilter：查营销订单；不存在或已退单（CLOSE）→ 不结算。 3.可结算时间校验 SettableRuleFilter：结算时间必须在拼团有效期内（outTradeTime < team.validEndTime），比如发起 拼团一个小时之内要结算完毕。 4.结束节点EndRuleFilter：整理上下文到返回对象，作为结算规则校验的产出。 检验通过方可进入真正的结算。 三、交易退单执行链 1.数据加载 DataNodeFilter：按 userId + outTradeNo 查询营销订单与拼团信息，写入上下文。 2.重复退单检查 UniqueRefundNodeFilter：订单已是 CLOSE → 视为幂等重复，直接返回。 3.退单策略执行 RefundOrderNodeFilter：依据“拼团态 + 订单态”选用具体退单策略 IRefundOrderStrategy，执行退款/解锁/改库并返回成功结果。 本身就是完整的退单流程。 规则树流程 结构：节点之间呈树状关系，一个节点可以路由到多个不同的“下一跳”。 执行规则：根据 上下文数据（request + context），选择不同的分支继续执行。 责任链一开始就组装成链，依次执行；规则树每个节点中的get() 方法会根据上下文选择不同的下一跳。 场景：风控规则树（用户支付时校验） RootNode：进入风控 RiskScoreNode （风险评分节点） 如果分数 < 30 → 通过节点（直接放行支付） 如果分数 30 ~ 60 → 人工审核节点（挂起订单，通知客服人工确认） 如果分数 > 60 → 拒绝节点（直接拦截交易） 整体分层思路 分层 作用 关键对象 通用模板层 抽象出与具体业务无关的「规则树」骨架，解决 如何找到并执行策略 的共性问题 StrategyMapper、StrategyHandler、AbstractStrategyRouter<T,D,R> 业务装配层 基于模板，自由拼装出 一棵 贴合业务流程的策略树 RootNode / SwitchNode / MarketNode / EndNode … 对外暴露层 通过 工厂 + 服务支持类 将整棵树封装成一个可直接调用的 StrategyHandler，并交给 Spring 整体托管 DefaultActivityStrategyFactory、AbstractGroupBuyMarketSupport 通用模板层：规则树的“骨架” 角色 职责 关系 StrategyMapper 映射器：依据 requestParameter + dynamicContext 选出 下一个 策略节点 被 AbstractStrategyRouter 调用 StrategyHandler 处理器：真正执行业务逻辑；apply 结束后可返回结果或继续路由 节点本身 / 路由器本身都是它的实现 AbstractStrategyRouter<T,D,R> 路由模板：① 调用 get(...) 找到合适的 StrategyHandler;② 调用该 handler 的 apply(...);③ 若未命中则走 defaultStrategyHandler 同时实现 StrategyMapper 与 StrategyHandler，但自身保持 抽象，把细节延迟到子类 业务装配层：一棵可编排的策略树 RootNode -> SwitchNode -> MarketNode -> EndNode ↘︎ OtherNode ... 每个节点 继承 AbstractGroupBuyMarketSupport（业务基类） 实现 get(...)：决定当前节点的下一跳是哪一个节点 实现 apply(...)：实现节点自身应做的业务动作（或继续下钻） 组合方式 路由是“数据驱动”的：并非工厂把链写死，而是节点在运行期根据 request + context 决定下一跳（可能是ERROR_NODE或END_NODE），灵活插拔。 对外暴露层：工厂 + 服务支持类 组件 主要职责 DefaultActivityStrategyFactory (@Service) 仅负责把 RootNode 暴露为 StrategyHandler 入口（交由 Spring 管理，方便注入）。 AbstractGroupBuyMarketSupport 业务服务基类：封装拼团场景下共用的查询、工具方法；供每个节点继承使用 本项目执行总览： 调用入口：factory.strategyHandler() → 返回 RootNode（实现了 StrategyHandler）。 执行流程： apply(...)：模板入口，先跑 multiThread(...) 预取/并发任务，再跑 doApply(...)。 doApply(...)：每个节点自己的业务；通常在末尾调用 router(...) 继续下一个节点（return router(request, ctx);）。也可以在某些节点“短路返回”，不再路由。 router(request, ctx)：内部调用当前节点的 get(...) 来挑选下一节点next，若存在就调用 next.apply(...) 递归推进；若不存在（或是到达 EndNode），则收束返回。 RootNode 校验必填参数：userId/goodsId/source/channel。 合法则路由到 SwitchNode；非法直接抛 ILLEGAL_PARAMETER。 SwitchNode（总开关、不区分活动，做总体的降级限流） 调用 repository.downgradeSwitch() 判断是否降级；是则抛 E0003。 调用 repository.cutRange(userId) 做切量；不在范围抛 E0004。 通过后路由到 MarketNode。 MarketNode multiThread(...) 中并发拉取： 拼团活动配置 GroupBuyActivityDiscountVO 商品信息 SkuVO 写入 DynamicContext doApply(...) 读取配置 + SKU（Stock Keeping Unit库存量单位），按 marketPlan 选 IDiscountCalculateService，计算 payPrice / deductionPrice 并写回上下文。 路由判定： 若配置/商品/deductionPrice 有缺失 → ErrorNode 否则 → TagNode TagNode（业务相关，部分人不在本次活动范围内！） 若活动没配置 tagId → 视为不限定人群：visible=true、enable=true。 否则通过 repository.isTagCrowdRange(tagId, userId) 判断是否在人群内，并据此更新 visible/enable。 路由到 EndNode。 EndNode 从 DynamicContext 读取：skuVO / payPrice / deductionPrice / groupBuyActivityDiscountVO / visible / enable； 构建并返回最终的 TrialBalanceEntity，链路终止。 ErrorNode 统一异常出口；若无配置/无商品，抛 E0002；否则可返回空结果作为兜底； 返回后走 defaultStrategyHandler（结束）。
测试 小明今天15岁了，它是天水小学上学。