如何使用SpringAI来实现一个RAG应用系统

RAG原理

大模型没有本地私有知识,所以用户在向大模型提问的时候,大模型只能在它学习过的知识范围内进行回答,而RAG就是在用户在提问的时候 将本地与问题相关的私有知识连同问题一块发送给大模型,进而大模型从用户提供的私有知识范围内进行更精确的回答。

核心技术栈

  • SpringAI
  • MybatisPlus
  • Chroma
  • Elasticsearch
  • MySQL

核心步骤

文本分块向量化

将文本切分成多个文本块,作者使用markdown来存储文本内容,markdown格式的文本相对来说是比较容易且分的,将文本切分之后 请求向量化接口进行文本向量化,最后将向量的结果写入到原本的数据块中 存储到向量数据库

向量数据库

  • Elasticsearch 混合检索使用,知识召回准确度比较高
  • Chroma 本地测试 或者小数据集使用 也能混合检索 但是无法像es那样可以模糊混合检索

向量检索

将用户的问题进行向量化,然后调用向量数据库的检索

实现

文本分块存储到向量数据库

@Service("docMarkdownFileParseService") public class DocMarkdownFileParseServiceImpl implements DocFileParseService { @Override public List<Document> parse(MultipartFile file,Integer kdId) { // 初始化markdown配置 MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder() .withHorizontalRuleCreateDocument(true) .withIncludeCodeBlock(true) .withIncludeBlockquote(true) .withAdditionalMetadata("knowledgeDocId", kdId) .build(); MarkdownDocumentReader reader = new MarkdownDocumentReader(file.getResource(), config); // 文档切分读取 return reader.get(); } }

分块的时候会涉及一些metadata,metadata用来存储数据块的元数据,也可以存储一些自定义字段,可以更好的为混合检索提供支持! 这里我存储了知识文本的ID

MarkdownDocumentReader

我在SpringAI的基础上扩展了MarkdownDocumentReader,主要是将markdown各级标题提取出来组合成titleExpander,最终形成 一级标题-二级标题-三级标题-当前标题 这样的格式,进而为后续的混合检索提供支持

SpringAI默认提供的类没有对表格解析做支持,所以我也支持了表格的解析,所有源码都粘贴到下面

package cn.dataling.rag.application.reader; import org.commonmark.ext.gfm.tables.*; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.*; import org.commonmark.parser.Parser; import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentReader; import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import java.io.IOException; import java.io.InputStreamReader; import java.util.*; /** * Reads the given Markdown resource and groups headers, paragraphs, or text divided by * horizontal lines (depending on the * {@link MarkdownDocumentReaderConfig#horizontalRuleCreateDocument} configuration) into * {@link Document}s. * * @author Piotr Olaszewski */ public class MarkdownDocumentReader implements DocumentReader { /** * The resource points to the Markdown document. */ private final Resource markdownResource; /** * Configuration to a parsing process. */ private final MarkdownDocumentReaderConfig config; /** * Markdown parser. */ private final Parser parser; /** * Create a new {@link MarkdownDocumentReader} instance. * * @param markdownResource the resource to read */ public MarkdownDocumentReader(String markdownResource) { this(new DefaultResourceLoader().getResource(markdownResource), MarkdownDocumentReaderConfig.defaultConfig()); } /** * Create a new {@link MarkdownDocumentReader} instance. * * @param markdownResource the resource to read * @param config the configuration to use */ public MarkdownDocumentReader(String markdownResource, MarkdownDocumentReaderConfig config) { this(new DefaultResourceLoader().getResource(markdownResource), config); } /** * Create a new {@link MarkdownDocumentReader} instance. * * @param markdownResource the resource to read */ public MarkdownDocumentReader(Resource markdownResource, MarkdownDocumentReaderConfig config) { this.markdownResource = markdownResource; this.config = config; this.parser = Parser.builder() .extensions(Collections.singletonList(TablesExtension.create())) .build(); } /** * Extracts and returns a list of documents from the resource. * * @return List of extracted {@link Document} */ @Override public List<Document> get() { try (var input = this.markdownResource.getInputStream()) { Node node = this.parser.parseReader(new InputStreamReader(input)); DocumentVisitor documentVisitor = new DocumentVisitor(this.config); node.accept(documentVisitor); return documentVisitor.getDocuments(); } catch (IOException e) { throw new RuntimeException(e); } } /** * A convenient class for visiting handled nodes in the Markdown document. */ static class DocumentVisitor extends AbstractVisitor { private final List<Document> documents = new ArrayList<>(); private final List<String> currentParagraphs = new ArrayList<>(); private final MarkdownDocumentReaderConfig config; private Document.Builder currentDocumentBuilder; /** * 存储各级标题的文本内容,用于构建层级title * 数组索引对应标题级别(1-6) */ private final String[] headingLevels = new String[7]; /** * 用于构建表格内容的构建器 */ private final StringBuilder tableBuilder = new StringBuilder(); /** * 是否正在处理表格 */ private boolean inTable = false; /** * 当前表格的列数,用于生成分隔行 */ private int tableColumns = 0; /** * 是否正在处理表头 */ private boolean inTableHeader = false; DocumentVisitor(MarkdownDocumentReaderConfig config) { this.config = config; } /** * Visits the document node and initializes the current document builder. */ @Override public void visit(org.commonmark.node.Document document) { this.currentDocumentBuilder = Document.builder(); super.visit(document); } @Override public void visit(Heading heading) { buildAndFlush(); // 更新当前级别的标题文本(在visit(Text)中设置) // 这里先设置当前级别及更高级别保持不变,清除更低级别的标题 int level = heading.getLevel(); for (int i = level; i < headingLevels.length; i++) { headingLevels[i] = null; } super.visit(heading); } @Override public void visit(ThematicBreak thematicBreak) { if (this.config.horizontalRuleCreateDocument) { buildAndFlush(); } super.visit(thematicBreak); } @Override public void visit(SoftLineBreak softLineBreak) { translateLineBreakToSpace(); super.visit(softLineBreak); } @Override public void visit(HardLineBreak hardLineBreak) { translateLineBreakToSpace(); super.visit(hardLineBreak); } @Override public void visit(ListItem listItem) { translateLineBreakToSpace(); super.visit(listItem); } @Override public void visit(Image image) { String alt = image.getDestination(); // 注意:这里应为getTitle()或getFirstChild()获取alt文本 String url = image.getDestination(); String title = image.getTitle(); // 将图片信息格式化后添加到当前段落中 String imageInfo = String.format("![%s](%s \"%s\")", alt, url, title); this.currentParagraphs.add(imageInfo); super.visit(image); } @Override public void visit(BlockQuote blockQuote) { if (!this.config.includeBlockquote) { return; } translateLineBreakToSpace(); this.currentDocumentBuilder.metadata("category", "blockquote"); super.visit(blockQuote); } @Override public void visit(Code code) { this.currentParagraphs.add(code.getLiteral()); this.currentDocumentBuilder.metadata("category", "code_inline"); super.visit(code); } @Override public void visit(FencedCodeBlock fencedCodeBlock) { if (!this.config.includeCodeBlock) { return; } translateLineBreakToSpace(); String literal = fencedCodeBlock.getLiteral(); Integer openingFenceLength = fencedCodeBlock.getOpeningFenceLength(); Integer closingFenceLength = fencedCodeBlock.getClosingFenceLength(); StringJoiner literalJoiner = new StringJoiner(""); literalJoiner.add("\n"); // 构建开头的代码块标记,包含语言标识 for (int i = 0; i < openingFenceLength; i++) { literalJoiner.add(fencedCodeBlock.getFenceCharacter()); } // 添加语言标识(如果有) String language = fencedCodeBlock.getInfo(); if (language != null && !language.trim().isEmpty()) { literalJoiner.add(language); } literalJoiner.add("\n"); literalJoiner.add(literal); // 构建结尾的代码块标记 for (int i = 0; i < closingFenceLength; i++) { literalJoiner.add(fencedCodeBlock.getFenceCharacter()); } literalJoiner.add("\n"); this.currentParagraphs.add(literalJoiner.toString()); this.currentDocumentBuilder.metadata("category", "code_block"); this.currentDocumentBuilder.metadata("lang", language); // 同时保存在元数据中 super.visit(fencedCodeBlock); } @Override public void visit(CustomBlock customBlock) { if (customBlock instanceof TableBlock tableBlock){ inTable = true; inTableHeader = false; tableBuilder.setLength(0); // 清空表格构建器 tableColumns = 0; // 设置元数据 this.currentDocumentBuilder.metadata("category", "table"); super.visit(tableBlock); // 继续访问表格子节点 // 表格处理完成 if (tableBuilder.length() > 0) { this.currentParagraphs.add(tableBuilder.toString()); } inTable = false; inTableHeader = false; } else { super.visit(customBlock); } } @Override public void visit(CustomNode customNode) { if (customNode instanceof TableBody tableBody){ inTableHeader = false; super.visit(tableBody); } else if (customNode instanceof TableRow tableRow){ if (inTable) { // 处理表格行 int columnCount = 0; StringBuilder rowBuilder = new StringBuilder("|"); // 遍历行中的所有单元格 Node child = tableRow.getFirstChild(); while (child != null) { if (child instanceof TableCell) { columnCount++; String cellContent = extractCellContent((TableCell) child); rowBuilder.append(cellContent).append("|"); } child = child.getNext(); } // 如果是表头行,记录列数并添加分隔行 if (inTableHeader && tableColumns == 0) { tableColumns = columnCount; tableBuilder.append(rowBuilder).append("\n"); // 添加分隔行 tableBuilder.append("|"); tableBuilder.append("---|".repeat(Math.max(0, tableColumns))); tableBuilder.append("\n"); } else { tableBuilder.append(rowBuilder).append("\n"); } } super.visit(tableRow); } else if (customNode instanceof TableCell tableCell){ // 单元格内容在visit(Text)中处理,这里直接继续访问 super.visit(tableCell); } else if (customNode instanceof TableHead tableHead){ inTableHeader = true; super.visit(tableHead); } else { super.visit(customNode); } } @Override public void visit(Text text) { if (text.getParent() instanceof Heading heading) { int level = heading.getLevel(); String currentTitle = text.getLiteral(); // 存储当前级别的标题 headingLevels[level] = currentTitle; // 构建层级title String hierarchicalTitle = buildHierarchicalTitle(level); this.currentDocumentBuilder.metadata("category", "header_%d".formatted(level)) .metadata("title", currentTitle) .metadata("titleExpander", hierarchicalTitle); } else if (!inTable) { // 如果不是在表格中,才添加到当前段落 this.currentParagraphs.add(text.getLiteral()); } // 表格中的文本在extractCellContent方法中处理 super.visit(text); } /** * 构建层级标题 * @param currentLevel 当前标题级别 * @return 层级标题字符串,如 "一级标题 - 二级标题 - 三级标题" */ private String buildHierarchicalTitle(int currentLevel) { List<String> titleParts = new ArrayList<>(); // 从1级标题开始,收集到当前级别为止的所有标题 for (int i = 1; i <= currentLevel; i++) { if (headingLevels[i] != null && !headingLevels[i].trim().isEmpty()) { titleParts.add(headingLevels[i].trim()); } } // 用 " - " 连接所有标题部分 return String.join(" - ", titleParts); } /** * 提取表格单元格内容 */ private String extractCellContent(TableCell tableCell) { StringBuilder cellBuilder = new StringBuilder(); Node child = tableCell.getFirstChild(); while (child != null) { cellBuilder.append(extractNodeText(child)); child = child.getNext(); } // 清理内容:移除首尾空格,将内部多个空格/换行替换为单个空格 String content = cellBuilder.toString().trim(); content = content.replaceAll("\\s+", " "); // 如果单元格内容为空,添加一个空格 if (content.isEmpty()) { content = " "; } return content; } /** * 递归提取节点文本 */ private String extractNodeText(Node node) { if (node instanceof Text) { return ((Text) node).getLiteral(); } else if (node instanceof Code) { return ((Code) node).getLiteral(); } else if (node instanceof StrongEmphasis) { // 加粗文本 return extractChildrenText(node); } else if (node instanceof Emphasis) { // 斜体文本 return extractChildrenText(node); } else if (node instanceof Link) { // 链接 - 提取链接文本 return extractChildrenText(node); } else { // 其他节点类型,递归提取子节点文本 return extractChildrenText(node); } } /** * 提取所有子节点的文本 */ private String extractChildrenText(Node node) { StringBuilder result = new StringBuilder(); Node child = node.getFirstChild(); while (child != null) { result.append(extractNodeText(child)); child = child.getNext(); } return result.toString(); } public List<Document> getDocuments() { buildAndFlush(); return this.documents; } private void buildAndFlush() { if (!this.currentParagraphs.isEmpty() || (inTable && tableBuilder.length() > 0)) { String content; if (inTable && tableBuilder.length() > 0) { // 如果正在处理表格,使用表格内容 content = tableBuilder.toString(); } else { // 否则使用段落内容 content = String.join("\n", this.currentParagraphs); } Document.Builder builder = this.currentDocumentBuilder.text(content); this.config.additionalMetadata.forEach(builder::metadata); Document document = builder.build(); this.documents.add(document); this.currentParagraphs.clear(); tableBuilder.setLength(0); } this.currentDocumentBuilder = Document.builder(); } private void translateLineBreakToSpace() { if (!this.currentParagraphs.isEmpty() && !inTable) { this.currentParagraphs.add(" "); } } } }

表格支持还需要添加一下依赖

<dependency> <groupId>org.commonmark</groupId> <artifactId>commonmark-ext-gfm-tables</artifactId> <version>0.22.0</version> </dependency>

下面是接受前端上传的markdown文件,以及所选择的知识库ID,然后做文本切块 向量化存储

public List<Document> embeddingDocumentsForMarkdown(Integer kdId, MultipartFile file) { String fileExtension = getFileExtension(file); // 文档切分读取 List<Document> documents = switch (fileExtension) { case "md" -> docFileParseServiceMap.get("docMarkdownFileParseService").parse(file, kdId); case "pdf" -> docFileParseServiceMap.get("docPdfFileParseService").parse(file, kdId); case "docx", "doc" -> docFileParseServiceMap.get("docWordFileParseService").parse(file, kdId); default -> throw new ExceptionCore("不支持的文件类型"); }; if (CollectionUtils.isEmpty(documents)) { return Collections.emptyList(); } vectorStoreComponent.getVectorStore().add(documents); return Collections.emptyList(); }

向量数据库

存储文本向量 为向量检索提供支持

package cn.dataling.rag.application.provider; import cn.dataling.rag.application.properties.ChromaProperties; import cn.dataling.rag.application.properties.ElasticsearchProperties; import cn.dataling.rag.application.util.JsonUtils; import cn.dataling.rag.application.vectorstore.ChromaVectorStore; import cn.dataling.rag.application.vectorstore.ElasticsearchAiSearchFilterExpressionConverter; import cn.dataling.rag.application.vectorstore.ElasticsearchVectorStore; import cn.dataling.rag.application.vectorstore.SimpleVectorStore; import com.google.common.collect.Lists; import org.springframework.ai.chroma.vectorstore.ChromaApi; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; /** * 向量存储提供者 */ public final class VectorStoreProvider { /** * 获取向量存储 * * @param vectorStoreType 向量存储类型 * @param embeddingModel 嵌入模型 * @param jsonConfig 配置 */ public static VectorStore getVectorStore(String vectorStoreType, EmbeddingModel embeddingModel, String jsonConfig) { VectorStoreProviderEnum vectorStoreProviderEnum = VectorStoreProviderEnum.valueOf(vectorStoreType); switch (vectorStoreProviderEnum) { case ELASTICSEARCH: ElasticsearchProperties elasticsearchProperties = JsonUtils.toObject(jsonConfig, ElasticsearchProperties.class); elasticsearchProperties.setSimilarity(ElasticsearchVectorStore.SimilarityFunction.cosine); return elasticsearchVectorStore(embeddingModel, elasticsearchProperties); case SIMPLE: return simpleVectorStore(embeddingModel); case CHROMA: ChromaProperties chromaProperties = JsonUtils.toObject(jsonConfig, ChromaProperties.class); return chromaVectorStore(embeddingModel, chromaProperties); default: throw new RuntimeException("vectorStoreType not support"); } } /** * 获取ES向量存储 * * @param embeddingModel 嵌入模型 * @param elasticsearchProperties es配置 */ public static VectorStore elasticsearchVectorStore(EmbeddingModel embeddingModel, ElasticsearchProperties elasticsearchProperties) { return ElasticsearchVectorStore.builder(elasticsearchProperties, embeddingModel) .withFilterExpressionConverter(new ElasticsearchAiSearchFilterExpressionConverter()) .batchingStrategy(docs -> Lists.partition(docs, elasticsearchProperties.getBatchSize())) .build(); } /** * 获取内存向量存储 * * @param embeddingModel 嵌入模型 */ public static VectorStore simpleVectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel) .batchingStrategy(docs -> Lists.partition(docs, 100)) .build(); } /** * 获取Chroma向量存储 * * @param embeddingModel 嵌入模型 * @param chromaProperties chroma配置 */ public static VectorStore chromaVectorStore(EmbeddingModel embeddingModel, ChromaProperties chromaProperties) { ChromaApi chromaApi = ChromaApi.builder() .baseUrl(chromaProperties.getBaseUrl()) .build(); return ChromaVectorStore.builder(chromaApi, embeddingModel) .collectionName(chromaProperties.getCollectionName()) .tenantName(chromaProperties.getTenantName()) .batchingStrategy(docs -> Lists.partition(docs, chromaProperties.getBatchSize())) .databaseName(chromaProperties.getDatabaseName()) .initializeSchema(true) .initializeImmediately(true) .build(); } /** * 向量存储提供者枚举 */ public enum VectorStoreProviderEnum { ELASTICSEARCH("ES"), SIMPLE("内存"), CHROMA("Chroma"), ; private final String value; VectorStoreProviderEnum(String value) { this.value = value; } public String getValue() { return value; } } }

RAG检索增强

public Flux<AssistantMessage> chatWithRag(ChatWithRagDTO data) { // 查询知识文档 KnowledgeDoc knowledgeDoc = knowledgeDocService.getKnowledgeDocById(data.getKnowledgeDocId()); if (ObjectUtils.isEmpty(knowledgeDoc)) { return Flux.just(new AssistantMessage("知识库不存在")); } // 获取知识文档的提示词 Integer promptId = knowledgeDoc.getPromptId(); PromptInfo promptInfo = promptInfoMapper.selectById(promptId); // 查询模型信息 Model model = modelMapper.selectById(data.getChatModelId()); // 获取对话客户端 ChatClient chatClient = chatClientProvider.getChatClient(model.getProvider(), model.getName(), model.getApiUrl(), model.getApiKey()); String delimiterToken = ObjectUtils.isEmpty(promptInfo) ? "{}" : promptInfo.getDelimiterToken(); StTemplateRenderer stTemplateRenderer = ObjectUtils.isEmpty(delimiterToken) ? StTemplateRenderer.builder().startDelimiterToken('{').endDelimiterToken('}').build() : StTemplateRenderer.builder().startDelimiterToken(delimiterToken.charAt(0)).endDelimiterToken(delimiterToken.charAt(1)).build(); // 构建提示词 同时将工具信息添加到提示词模板中 PromptTemplate promptTemplate = ObjectUtils.isEmpty(promptId) ? defaultPromptTemplate : PromptTemplate.builder() .template(promptInfoService.getPromptInfoById(promptId).getContent()) // 自定义模板分隔符(避免与 JSON 冲突 ) 默认分隔符 {} 可能与 JSON 语法冲突,可修改为 <> .renderer(stTemplateRenderer) .variables(Map.of("tools", getMcpToolsDefinition())) .build(); VectorStore vectorStore = vectorStoreComponent.getVectorStore(); RetrievalAugmentationAdvisor augmentationAdvisor = RetrievalAugmentationAdvisor.builder() // 阶段一:优化用户问题 将单个查询扩展为多个相关查询 .queryExpander(query -> data.getQueryExpander() ? queryExpander(chatClient, query.text()) : List.of(query)) // 阶段二: 根据查询检索相关文档 根据扩展后的查询进行检索 默认会使用线程池并行查询 .documentRetriever(query -> similaritySearch(data.getTopK(), data.getSimilarityThreshold(), query.text(), data.getKnowledgeDocId(), vectorStore)) // 阶段三:合并来自多个查询结果 合并多查询/多数据源的检索结果,去重 .documentJoiner(new ConcatenationDocumentJoiner()) // 阶段四:对检索到的文档进行后置处理 对检索到的文档进行后处理,如重排序 .documentPostProcessors((query, documents) -> data.getRerank() ? documentRerank(documents, query.text()) : documents) // 阶段五:查询增强阶段 将检索到的文档上下文融入原始查询 生成最终的prompt prompt中要包含 context 和 query 分别代表上下文和查询 .queryAugmenter(ContextualQueryAugmenter.builder() .documentFormatter(documents -> documents.stream() .map(e -> { String temp = """ 标题: %s 内容: %s """; Map<String, Object> metadata = e.getMetadata(); String titleExpander = CollectionUtils.isEmpty(metadata) ? "无标题" : (metadata.containsKey("titleExpander") ? metadata.get("titleExpander").toString() : "无标题"); return String.format(temp, titleExpander, e.getText()); }) .reduce((a, b) -> a + "\n\n" + b) .orElse("未检测到相关知识")) // 允许空上下文 如果为true的话 当上下文为空 模型会跳过上下文 使用自己的知识进行回答 .allowEmptyContext(false) .emptyContextPromptTemplate(emptyContextPrompt) .promptTemplate(promptTemplate) .build()) .build(); return chatClient.prompt() .user(data.getText()) .toolCallbacks(toolCallbackProvider) .advisors(MessageChatMemoryAdvisor.builder(jdbcChatMemory).build(), augmentationAdvisor) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, data.getConversationId())) .stream() .chatResponse() .map(e -> e.getResult().getOutput()) .takeWhile(assistantMessage -> IS_STREAM.getOrDefault(data.getConversationId(), true)) .onErrorResume(throwable -> Flux.just(AssistantMessage.builder().content(String.format("模型调用异常 %s", throwable.getCause().getMessage())).build())) .doFinally(d -> IS_STREAM.remove(data.getConversationId())); }

最后成品


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1164071.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

环保与水务行业PLC设备远程诊断与维护解决方案

水务行业的泵站、污水处理厂&#xff0c;环保行业的监测站、除污设备等&#xff0c;通常分布广泛、地处偏远。一旦PLC控制系统出现程序故障或参数异常&#xff0c;需要派遣专业工程师长途跋涉现场处理&#xff0c;响应周期长、差旅成本高&#xff0c;且故障期间可能导致工艺中断…

储能系统绝缘监测的技术突破:微电流传感器在直流侧的应用与优化

引言 随着全球储能市场的爆发式增长&#xff0c;系统安全性成为行业关注的焦点。根据中国电力科学研究院数据&#xff0c;2025年储能电站因绝缘故障导致的事故占比高达15%&#xff0c;其中直流侧漏电流监测不足是主要诱因。微电流传感器&#xff08;如基于磁通门或高精度霍尔原…

巴菲特的股东回报政策:股息与回购的平衡

巴菲特的股东回报政策&#xff1a;股息与回购的平衡关键词&#xff1a;巴菲特、股东回报政策、股息、股票回购、平衡策略摘要&#xff1a;本文深入探讨了巴菲特所奉行的股东回报政策&#xff0c;着重分析股息与股票回购之间的平衡关系。通过对相关核心概念的阐述、背后算法原理…

2026年网络安全就业前景怎么样?网络安全工程师多少钱一个月?

前言 网络安全工程师是当今互联网行业中备受瞩目的职业之一。随着网络安全问题的不断增加&#xff0c;对于网络安全专业人才的需求也日益增长。然而&#xff0c;网络安全工程师的薪资水平各地区存在一定的差异。那么&#xff0c;网络安全就业前景如何呢&#xff1f; 一、市场需…

了解串口通信

文章目录 前言一、问题及发展总结 前言 串口通信主要针对抗干扰问题进行的演变 一、问题及发展 串口道信最开始使用TTL电平&#xff0c;抗干扰能力低——>RS232&#xff08;提升电平大小&#xff0c;增加抗干扰能力&#xff09;——>RS485&#xff08;差分线形式继续增…

快速构建您的小程序+APP+H5商城源码系统,并邀请商户入驻

温馨提示&#xff1a;文末有资源获取方式在数字经济蓬勃发展的今天&#xff0c;一个能够同时支撑自营、平台化和技术服务业务的电商系统&#xff0c;是开拓多元收入流的强大引擎。我们隆重介绍一款专为商业模式创新而设计的电商源码系统&#xff0c;它不仅功能完备&#xff0c;…

飞牛 NAS 远程访问卡顿?这份终极配置指南 ,让你的影音库真正“移动”起来

目录你真正要解决的&#xff0c;不是“能不能连上”配置示例&#xff1a;共享影音目录&#xff08;可直接照抄&#xff09;1&#xff09;找到配置文件2&#xff09;写入配置&#xff08;注意缩进&#xff09;安全与回滚&#xff08;建议读完再开&#xff09;3&#xff09;保存并…

AI智能问数系统:让业务人员玩转数据的技术底层

以前业务同事查数据&#xff0c;得求着技术写SQL&#xff0c;一等就是大半天。现在对着AI说句“查下上周各区域销售额Top3”&#xff0c;秒级就出结果带图表——这背后不是AI“猜透了心思”&#xff0c;全靠一套硬核技术在撑场面。作为落地过多个问数系统的产品经理&#xff0c…

leetcode 困难题 871. Minimum Number of Refueling Stops 最低加油次数-内存100

Problem: 871. Minimum Number of Refueling Stops 最低加油次数 解题过程 内存100%&#xff0c;状态数组标记是否被使用&#xff0c;每次从当前能到达的最远的地方&#xff0c;期间所有的加油站选择油最多的站点&#xff0c;不停循环直到可以到达目的地 Code using pr pair&…

大学生未来想要从事网络安全,不知道先学什么应该从哪开始?(末尾附学习路线图)

网络安全从技术层面上主要分为web安全和二进制安全两个大方向&#xff0c;方向不同学习内容也不同的。如果是零基础的话建议从web安全开始。 &#xff08;一&#xff09;Web安全学习内容 1、学习一种或几种编程语言。 网络安全也属于计算机范畴&#xff0c;涉及到IT行业的&…

leetcode 872. Leaf-Similar Trees 叶子相似的树-耗时100

Problem: 872. Leaf-Similar Trees 叶子相似的树 解题过程 耗时100%&#xff0c;前序遍历的&#xff0c;拿到叶子节点&#xff0c;顺序默认从左到右&#xff0c;判断两者是否相等 Code /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNo…

C# winform部署yolo26-seg实例分割的onnx模型演示源码+模型+说明

yolo26已经正式发布了&#xff0c;因此使用C#代码实现YOLO26-seg实例分割部署&#xff0c;首先看yolov11-seg网络结构&#xff0c;发现输出shape是1x116x8400 再来看看yolo26-seg网络结构输出&#xff0c;输出shape是1x300x38 可见yolo11和yolo26输出是不一样的是不能共用代码。…

解读GB/T4857.5跌落测试标准 助力医药包装NMPA注册合规

在医疗器械、生物制药、敷料、疫苗等行业&#xff0c;产品的运输安全直接关系到临床使用效果与患者生命健康&#xff0c;而运输包装作为产品的“防护屏障”&#xff0c;其耐冲击性能至关重要。GB/T4857.5-92《包装 运输包装件 跌落试验方法》作为国内运输包装冲击测试的核心标准…

适合PPT汇报的扁平化图片素材哪里找?10个优质网站推荐!

很多小伙伴在准备PPT汇报时&#xff0c;都会为找不到合适的图片素材而头疼。太复杂的图片容易分散观众注意力&#xff0c;太普通的又显得缺乏专业感。而扁平化设计的图片凭借简洁的线条、明快的色彩和极简的风格&#xff0c;正好能解决这个问题——它们既能突出重点&#xff0c…

2026精选10个商业海报背景图网站:设计师必藏!

对于设计师来说&#xff0c;找一张合适的商业海报背景图简直是日常工作中的“小难题”——既要高清美观&#xff0c;又要符合品牌调性&#xff0c;最好还能免费商用。尤其是在商业场景下&#xff0c;背景图的选择直接影响海报的传播效果和品牌形象。今天&#xff0c;我们就来盘…

基于MATLAB Simulink R2015b平台的三相感应电机动态仿真建模与性能分析

Three_Phase_Induction_Motor&#xff1a;基于MATLAB/Simulink的三相感应电机动态数学建模仿真模型。 仿真条件&#xff1a;MATLAB/Simulink R2015b最近在实验室被三相感应电机的启动电流问题折腾得够呛。这玩意儿空载启动时电流能飙到额定电流的5-7倍&#xff0c;直接把我给整…

VP引导定位软件-平移九点标定

VP引导定位软件-平移九点标定 一 确定通讯协议 Calib,X,Y,第几个点Calib,140,10,1 Calib,140,-10,2 Calib,140,-30,3 Calib,160,10,4 Calib,160,-10,5 Calib,160,-30,6 Calib,180,10,7 Calib,180,-10,8 Calib,180,-30,9二 定义全局变量/// <summary>/// 软件模式/// Loca…

吐血推荐8个一键生成论文工具,研究生轻松搞定论文写作!

吐血推荐8个一键生成论文工具&#xff0c;研究生轻松搞定论文写作&#xff01; 论文写作的救星&#xff0c;AI 工具如何改变研究生的学术生活 在当今信息爆炸的时代&#xff0c;研究生们面对的不仅是繁重的课程任务&#xff0c;还有论文写作带来的巨大压力。传统的写作方式不仅…

救命神器9个AI论文网站,专科生毕业论文格式规范+写作神器推荐!

救命神器9个AI论文网站&#xff0c;专科生毕业论文格式规范写作神器推荐&#xff01; AI 工具让论文写作不再难 对于专科生来说&#xff0c;毕业论文不仅是学业的终点&#xff0c;更是能力的考验。面对格式规范、内容逻辑、语言表达等多重挑战&#xff0c;很多同学感到无从下手…

阿德勒《自卑与超越》深度解读:于自卑中寻路,在合作中超越

阿德勒《自卑与超越》深度解读&#xff1a;于自卑中寻路&#xff0c;在合作中超越阿尔弗雷德・阿德勒&#xff0c;作为与弗洛伊德、荣格并称的现代心理学三大奠基人&#xff0c;是个体心理学的创始人、人本主义心理学的先驱。不同于弗洛伊德理论的艰深晦涩&#xff0c;他的经典…