首页
关于
Search
1
同步本地Markdown至Typecho站点
88 阅读
2
微服务
41 阅读
3
苍穹外卖
32 阅读
4
JavaWeb——后端
25 阅读
5
消息队列MQ
20 阅读
后端学习
项目
杂项
科研
论文
默认分类
登录
找到
60
篇与
zy123
相关的结果
- 第 2 页
2025-08-31
训练神经网络
🧠 神经网络训练流程基础笔记 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 控制重复多少轮。
科研
zy123
8月31日
0
4
0
2025-08-29
草稿
非常好的问题 💡 你抓住了 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 变化的数值差异。
论文
zy123
8月29日
0
6
0
2025-08-19
RAG知识库
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<br/>发送请求 Model-->>Client: 返回 tool_calls: queryConfig(...) Client->>MCPServer: 通过 MCP stdin 发送<br/>tools/call 请求 MCPServer-->>Client: 执行 Java @Tool 方法<br/>返回 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; } }
项目
zy123
8月19日
0
11
0
2025-08-11
拼团设计模式
设计模式 单例模式 懒汉 注意,单例模式的构造函数是私有的! 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(结束)。
项目
zy123
8月11日
0
8
0
2025-07-31
测试
测试 小明今天15岁了,它是天水小学上学。
后端学习
zy123
7月31日
0
4
0
上一页
1
2
3
...
12
下一页