知识库问答机器人:基于SpringAI+RAG的完整实现

知识库问答机器人:基于SpringAI+RAG的完整实现

一、引言

随着大语言模型的快速发展,RAG(Retrieval-Augmented Generation)技术已成为构建知识库问答系统的核心技术之一。本文将带领大家从零开始,使用Spring AI框架构建一个支持文档上传的知识库问答机器人,帮助大家深入理解RAG技术的核心原理和实践应用。

1.1 什么是RAG?
RAG(检索增强生成)是一种结合了信息检索和文本生成的技术。它的基本工作流程是:
用户提出问题
系统从知识库中检索相关信息
大语言模型基于检索到的信息生成答案

从系统设计角度触发,RAG 的核心作用可以被描述为:
在LLM调用生成响应之前,由系统动态构造一个“最小且相关的知识上下文”。

请注意两个关键词:
动态
:每次问题都不同,检索的知识也不同(比如用户问 A 产品时找 A 的文档,问 B 产品时找 B 的文档)
最小
:只注入必要信息(比如用户问 “A 产品的定价”,就只塞定价相关的片段,而非整份产品手册)

RAG可以有效的弥补上下文窗口的先天不足:不再需要把所有知识塞进窗口,而是只在需要时 “临时调取” 相关部分,既避免了窗口溢出,又减少了注意力竞争。

1.2 RAG在交互链路中的位置
接下来我们以RAG的经典应用场景——企业知识库为例,来看一下RAG在这个流程中所处的位置

在这里插入图片描述

在这个结构中,RAG主要就是在用户提问与向LLM发起请求这个中间段,用于检索关联的文档构建上下文

1.3 RAG工作原理
我们以一张图来介绍RAG的工作原理,具体的RAG详细介绍,请参照文末引用

在这里插入图片描述

二、核心实现

2.1 项目结构概览

D05-rag-qa-bot/ ├── src/main/java/com/git/hui/springai/app/ │ ├── D05Application.java # 启动类 │ ├── mvc/ │ │ ├── QaApiController.java # API控制器 │ │ └── QaController.java # 页面控制器 │ ├── qa/QaBoltService.java # 问答服务 │ └── vectorstore/ │ ├── DocumentChunker.java # 文档分块工具 │ ├── DocumentQuantizer.java # 文档量化器 │ └── TextBasedVectorStore.java # 文本向量存储 ├── src/main/resources/ │ ├── application.yml # 配置文件 │ ├── prompts/qa-prompts.pt # 提示词模板 │ └── templates/chat.html # 前端页面 └── pom.xml # 依赖配置 

2.2 项目初始化
2.2.1 Maven依赖配置

首先,我们需要在pom.xml中配置必要的依赖:
其中关于向量数据库、tika的文档解析属于核心依赖项
hanlp适用于无法直接使用EmbeddingModel的场景,在我们的示例中,会实现一个基础的文档向量化方案,其中会采用Hanlp来做中文分词
使用智谱的免费大模型来体验我们的RAG知识库问答(当然也可以基于OpenAI-Starter来切换其他的大模型,使用层面并没有改变,只需要替换依赖、api配置即可)

<dependencies><!-- 向量数据库 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-advisors-vector-store</artifactId></dependency><!-- 文档提取,使用apache-tika来实现 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-tika-document-reader</artifactId></dependency><!-- pdf文档提取,实际也可以用上面的tika来实现 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-rag</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 使用智谱大模型 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-zhipuai</artifactId></dependency><!-- 用于前端页面的支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- 中文分词,用在文档向量化 --><dependency><groupId>com.hankcs</groupId><artifactId>hanlp</artifactId><version>portable-1.8.4</version></dependency></dependencies>

这里我们引入了Spring AI的核心依赖,以及用于文档处理的Tika和PDF读取器,还特别加入了HanLP中文分词库来优化中文处理效果。

2.2.2 应用配置
在application.yml中配置API密钥和相关参数:

spring: ai: zhipuai: api-key:${zhipuai-api-key} chat: options: model:GLM-4-Flash temperature:0.1 thymeleaf: cache:false servlet: multipart: max-file-size:10MB max-request-size:50MB logging: level:org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor:debug org.springframework.ai.chat.client:DEBUG server: port:8080

2.3 自定义向量存储实现
通常RAG会使用一些成熟的向量数据库(如Pinecone、weaviate、qdrant、milvus或者es、redis等),但是考虑到安装、环境配置等成本,我们接下来会实现一个基础的自定义的文本向量库 TextBasedVectorStore,基于内存实现,无需额外的外部依赖,单纯的用来体验RAG并没有太大问题

SpringAI原生提供了一个基于内存的向量数据库SimpleVectorStore,在它的实现中,向量数据写入,依赖向量模型,因此如果有额度使用大模型厂家提供的EmbeddingModel时,直接用它进行测试即可;

当然如果你现在并没有渠道使用向量模型的,那也没关系,接下来我们将参照SpringAI的SimpleVectorStore实现的一个自定义的向量库TextBasedVectorStore,提供一套不依赖向量模型的解决方案,特别适合快速原型开发,核心实现如下(当然你也完全可以忽略它,它不是我们的重点)

2.3.1 TextBasedVectorStore - 文本匹配向量存储
在下面的实现中,重点体现了两个方法
doAdd: 将文档保存到向量数据库中(文档分片 -> 向量化 -> 存储)
doSimilaritySearch: 基于相似度的搜索
需要注意一点,文档的向量化与搜索时传入文本的向量化,需要采用同一套向量化方案

public classTextBasedVectorStoreextendsAbstractObservationVectorStore {@GetterprotectedMap<String,SimpleVectorStoreContent> store =newConcurrentHashMap();/** * 已经存储到向量库的document,用于幂等 */privateSet<String> persistMd5 = newCopyOnWriteArraySet<>();/** * 添加文档到向量数据库 * * @param documents */@OverridepublicvoiddoAdd(List<Document> documents){if(CollectionUtils.isEmpty(documents)){return;}// 创建一个新的可变列表副本List<Document> mutableDocuments = newArrayList<>();for(Document document : documents){// 过滤掉重复的文档,避免二次写入,浪费空间if(!persistMd5.contains((String) document.getMetadata().get("md5"))){ mutableDocuments.add(document);}}if(CollectionUtils.isEmpty(mutableDocuments)){return;}// 文档分片List<Document> chunkers =DocumentChunker.DEFAULT_CHUNKER.chunkDocuments(mutableDocuments);// 存储本地向量库 chunkers.forEach(document ->{float[] embedding =DocumentQuantizer.quantizeDocument(document);if(embedding.length ==0){return;}SimpleVectorStoreContentstoreContent=newSimpleVectorStoreContent( document.getId(), document.getText(), document.getMetadata(), embedding );this.store.put(document.getId(), storeContent);}); mutableDocuments.forEach(document -> persistMd5.add((String) document.getMetadata().get("md5")));}/** * 搜索向量数据库,根据相似度返回相关文档 * * @param request * @return */@OverridepublicList<Document>doSimilaritySearch(SearchRequest request){Predicate<SimpleVectorStoreContent> documentFilterPredicate =this.doFilterPredicate(request); finalfloat[] userQueryEmbedding =this.getUserQueryEmbedding(request.getQuery()); returnthis.store.values().stream().filter(documentFilterPredicate).map((content)-> content.toDocument(DocumentQuantizer.calculateCosineSimilarity(userQueryEmbedding, content.getEmbedding()))).filter((document)-> document.getScore()>= request.getSimilarityThreshold()).sorted(Comparator.comparing(Document::getScore).reversed()).limit((long) request.getTopK()).toList();} privatefloat[]getUserQueryEmbedding(String query){returnDocumentQuantizer.quantizeQuery(query);}}

2.3.2 DocumentChunker - 文档分块器
合理地将长文档分块是RAG系统的关键环节,合理的分块大小,可以有效的增加检索效率、提高准确率、减少上下文长度

在真实的RAG应用中,这一块具体的方案挺多的,比如固定尺寸(下面的方案)、地柜拆分、语义拆分、结构化拆分(如结构化的markdown文档就很适合)、延迟拆分、自适应拆分、层级拆分、LLM驱动拆分、智能体拆分等

public classDocumentChunker { privatefinalint maxChunkSize; privatefinalint overlapSize;publicDocumentChunker(){this(500,50);// 默认值:最大块大小500个字符,重叠50个字符}publicList<Document>chunkDocument(Document document){Stringcontent= document.getText();if(content ==null|| content.trim().isEmpty()){returnList.of(document);}List<String> chunks =splitText(content);List<Document> chunkedDocuments = newArrayList<>();for(inti=0; i < chunks.size(); i++){Stringchunk= chunks.get(i);StringchunkId= document.getId()+"_chunk_"+ i;DocumentchunkDoc=newDocument(chunkId, chunk, newHashMap<>(document.getMetadata())); chunkDoc.getMetadata().put("chunk_index", i); chunkDoc.getMetadata().put("total_chunks", chunks.size()); chunkDoc.getMetadata().put("original_document_id", document.getId()); chunkedDocuments.add(chunkDoc);}return chunkedDocuments;}privateList<String>splitText(String text){List<String> chunks = newArrayList<>();// 按多种分隔符分割,优先在语义边界处分割String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");StringBuildercurrentChunk=newStringBuilder();for(String sentence : sentences){if(sentence.trim().isEmpty()){continue;// 跳过空句子}if(currentChunk.length()+ sentence.length()<= maxChunkSize){// 如果当前块加上新句子不超过最大大小,就添加到当前块if(currentChunk.length()>0){ currentChunk.append(sentence);}else{ currentChunk.append(sentence);}}else{// 如果当前块为空,但是单个句子太长,需要强制分割if(currentChunk.length()==0){List<String> subChunks =forceSplit(sentence, maxChunkSize);for(inti=0; i < subChunks.size(); i++){StringsubChunk= subChunks.get(i);if(i < subChunks.size()-1){ chunks.add(subChunk);}else{ currentChunk.append(subChunk);}}}else{ chunks.add(currentChunk.toString()); currentChunk =newStringBuilder();// 添加重叠部分,如果句子长度大于重叠大小,则只取末尾部分if(sentence.length()> overlapSize){Stringoverlap= sentence.substring(Math.max(0, sentence.length()- overlapSize)); currentChunk.append(overlap); currentChunk.append(sentence);}else{ currentChunk.append(sentence);}}}}if(currentChunk.length()>0){ chunks.add(currentChunk.toString());}return chunks;}}

2.3.3 DocumentQuantizer - 文档量化器
使用HanLP进行中文分词,实现了一个简单的文档向量化工具类(同样的你也完全可以忽略它的具体实现,因为它的效果显然比使用EmbedingModel要差很多很多,但用于学习体验RAG也基本够用)

public classDocumentQuantizer { privatestaticfinalSegmentSEGMENT=HanLP.newSegment(); publicstaticfloat[]quantizeText(String text){if(text ==null|| text.trim().isEmpty()){ returnnewfloat[0];}String[] words =preprocessText(text);Map<String,Integer> wordFreq =countWordFrequency(words);// 生成固定长度的向量表示(这里使用前128个高频词)returngenerateFixedLengthVector(wordFreq,128);}/** * 将文本转换为数值向量表示(简化版) * 使用TF-IDF的基本思想,但简化为词频统计 * * @param text 输入文本 * @return 数值向量 */ privatestatic String[]preprocessText(String text){List<Term> termList = SEGMENT.seg(text);return termList.stream().filter(term ->!isStopWord(term.word))// 过滤停用词.filter(term ->!term.nature.toString().startsWith("w"))// 过滤标点符号.map(term -> term.word.toLowerCase())// 转换为小写.toArray(String[]::new);}/** * 生成固定长度的向量表示 * * @param wordFreq 词频映射 * @param length 向量长度 * @return 固定长度的向量 */ privatestaticfloat[]generateFixedLengthVector(Map<String,Integer> wordFreq,int length){float[] vector = newfloat[length];// 获取频率最高的词汇List<Map.Entry<String,Integer>> sortedEntries = wordFreq.entrySet().stream().sorted(Map.Entry.<String,Integer>comparingByValue().reversed()).limit(length).collect(Collectors.toList());// 将词频填入向量for(inti=0; i <Math.min(sortedEntries.size(), length); i++){ vector[i]= sortedEntries.get(i).getValue();}return vector;}publicstaticdoublecalculateCosineSimilarity(float[] vectorA,float[] vectorB){if(vectorA ==null|| vectorB ==null|| vectorA.length ==0|| vectorB.length ==0){ return0.0;} intminLength=Math.min(vectorA.length, vectorB.length);float[] adjustedA =Arrays.copyOf(vectorA, minLength);float[] adjustedB =Arrays.copyOf(vectorB, minLength); doubledotProduct=0.0; doublenormA=0.0; doublenormB=0.0;for(inti=0; i < minLength; i++){ dotProduct += adjustedA[i]* adjustedB[i]; normA +=Math.pow(adjustedA[i],2); normB +=Math.pow(adjustedB[i],2);} normA =Math.sqrt(normA); normB =Math.sqrt(normB);if(normA ==0|| normB ==0){ return0.0;}return dotProduct /(normA * normB);}}

2.3.4 注册向量库
接下来就是注册使用这个向量库,在配置类or启动类中,添加下面这个声明即可

@BeanpublicVectorStorevectorStore(){returnTextBasedVectorStore.builder().build();}

2.4 SpringAI向量存储
上面2.3适用于无法直接使用大模型厂家的向量模型的场景,如果可以直接使用,那么上面的全部可以直接忽略掉,直接使用下面的方式进行声明向量库即可

@BeanpublicVectorStorevectorStore(EmbeddingModel embeddingModel){returnSimpleVectorStore.builder(embeddingModel).build();}

2.5 问答服务实现
接下来我们进入核心的基于RAG的QA问答机器人的实现

2.5.1 QaBoltService - 核心问答服务
Pre. 问答服务流程
我们先从时序的角度来看一下这个问答服务的核心交互流程

在这里插入图片描述

在这个时序过程中,为了简化大家的理解,我们将文档的向量化存储与问答进行了拆分

第一步:文档向量化

这一部分包含RAG应用数据准备阶段的完整过程
数据提取
文本分割
向量化

第二步:问答
应用层响应用户提问
从向量数据库检索相似度高的文档信息
注入提示词
访问大模型,获取答案
Impl. 核心实现
接下来我们看一下具体的实现(上面的步骤分割得很清楚,但是实际使用时,用户可以在问答中上传附件,这个附件也会作为我们知识库的一部分,因此具体的实现中,你会发现这两部耦合在一起了,请不要惊讶)

step1: 初始化ChatClient

在开始之前,我们首先参照SpringAI的官方教程,通过Advisor来初始化支持RAG的ChatClient

官方文档:https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html[2]

@Service publicclassQaBoltService { privatefinal ChatClient chatClient; privatefinal ChatMemory chatMemory; privatefinal VectorStore vectorStore;@Value("classpath:/prompts/qa-prompts.pt")privateResource boltPrompts;publicQaBoltService(ChatClient.Builder builder,VectorStore vectorStore,ChatMemory chatMemory){this.vectorStore = vectorStore;this.chatMemory = chatMemory;this.chatClient = builder.defaultAdvisors(newSimpleLoggerAdvisor(ModelOptionsUtils::toJsonStringPrettyPrinter,ModelOptionsUtils::toJsonStringPrettyPrinter,0),// 用于支持多轮对话MessageChatMemoryAdvisor.builder(chatMemory).build(),// 用于支持RAGRetrievalAugmentationAdvisor.builder().queryTransformers(// 使用大型语言模型重写用户查询,以便在查询目标系统时提供更好的结果。RewriteQueryTransformer.builder().chatClientBuilder(builder.build().mutate()).build()).queryAugmenter(// ContextualQueryAugmenter 使用来自所提供文档内容的上下文数据来增强用户查询。// 默认不支持上下文为空的场景,出现之后大模型会不返回用户查询;这里调整为支持为空ContextualQueryAugmenter.builder().allowEmptyContext(true).build()).documentRetriever(VectorStoreDocumentRetriever.builder().similarityThreshold(0.50).vectorStore(vectorStore).build()).build()).build();}}

接下来就是响应问答的实现,这里分两步

step2: 文档处理

处理用户上传的附件,即上面时序图中的第一步,解析文档、切分、向量化、保存到向量库;

下面的实现中主要体现的是基于SpringAI封装的tika与pdf文档解析starter,来提取上传的文档,生成供向量数据库使用的List; 而具体的文档切分、向量化等则是在上面的TextBasedVectorStore实现

注:为了一个文档,重复进行数据处理,我们在元数据中维护了文档的 md5,这样当添加到向量库中时,就可以基于这个md5来进行去重了

privateProceedInfoprocessFiles(String chatId,Collection<MultipartFile> files){StringBuildercontext=newStringBuilder("\n\n");List<Media> mediaList = newArrayList<>(); files.forEach(file ->{try{ vardata=newByteArrayResource(file.getBytes()); varmd5=calculateHash(chatId, file.getBytes());MimeTypemime=MimeType.valueOf(file.getContentType());if(mime.equalsTypeAndSubtype(MediaType.APPLICATION_PDF)){PagePdfDocumentReaderpdfReader=newPagePdfDocumentReader(data,PdfDocumentReaderConfig.builder().withPageTopMargin(0).withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0).build()).withPagesPerDocument(1).build());List<Document> documents = pdfReader.read(); documents.forEach(document ->{ document.getMetadata().put("md5", md5);if(document.getMetadata().containsKey("file_name")&& document.getMetadata().get("file_name")==null){ document.getMetadata().put("file_name", file.getName());}}); vectorStore.add(documents); varcontent=String.join("\n", documents.stream().map(Document::getText).toList()); context.append(String.format(ATTACHMENT_TEMPLATE, file.getName(), content));} elseif ("text".equalsIgnoreCase(mime.getType())){List<Document> documents =newTikaDocumentReader(data).read(); documents.forEach(document -> document.getMetadata().put("md5", md5)); vectorStore.add(documents); varcontent=String.join("\n", documents.stream().map(Document::getText).toList()); context.append(String.format(ATTACHMENT_TEMPLATE, file.getName(), content));}}catch(IOException e){thrownewRuntimeException(e);}});returnnewProceedInfo(context.toString(), mediaList);}

step3: 问答实现

然后就是具体的问答实现,这里主要是借助 QuestionAnswerAdvisor 来封装RAG相关的信息

说明:在下面的实现中,使用了自定义的提示词模板,当然也可以直接使用SpringAI默认的方案

publicFlux<String>ask(String chatId,String question,Collection<MultipartFile> files){processFiles(chatId, files);// 自定义的提示词模板,替换默认的检索参考资料的提示词模板// 其中 <query> 对应的是用户的提问 question// <question_answer_context> 对应的是增强检索的document,即检索到的参考资料PromptTemplatecustomPromptTemplate=PromptTemplate.builder().renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()).template(""" <query> Context information is below. --------------------- <question_answer_context> --------------------- Given the context information and no prior knowledge, answer the query. Follow these rules: 1. If the answer is not in the context, just say that you don't know. 2. Avoid statements like "Based on the context..." or "The provided information...". """).build(); varqaAdvisor=QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(3).build()).promptTemplate(customPromptTemplate).build(); varrequestSpec= chatClient.prompt().system(boltPrompts).user(question).advisors(qaAdvisor).advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId));return requestSpec.stream().content().map(s -> s.replaceAll("\n","<br/>"));}

到这里,一个基于RAG的问答机器人的核心逻辑,已经全部完成,接下来我们进入体验阶段

2.5.2 控制器实现

QaApiController- API控制器 @RestController@RequestMapping("/api") publicclassQaApiController {@AutowiredprivateQaBoltService qaBolt;@GetMapping(path ="/chat/{chatId}", produces =MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>qaGet(@PathVariable("chatId")String chatId,@RequestParam("question")String question){return qaBolt.ask(chatId, question,Collections.emptyList());}@PostMapping(path ="/chat/{chatId}", produces =MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>qaPost(@PathVariable("chatId")String chatId,@RequestParam("question")String question,@RequestParam(value ="files", required =false)Collection<MultipartFile> files){if(files ==null){ files =Collections.emptyList();}return qaBolt.ask(chatId, question, files);}}

三、体验与小结

3.1 启动类

@SpringBootApplication publicclassD05Application {@BeanpublicVectorStorevectorStore(){returnTextBasedVectorStore.builder().build();}publicstaticvoidmain(String[] args){SpringApplication.run(D05Application.class, args);System.out.println("启动成功,前端测试访问地址: http://localhost:8080/chat");}}

3.2 问答提示词
在 resources/prompts/qa-prompts.pt 中维护我们的qa机器人的系统提示词(DeepSeek生成的)

## 角色设定 你是一个智能问答助手,专门负责根据用户提供的文档内容进行准确的回答和信息提取。 ## 核心任务 - 仔细阅读并理解用户上传的文档内容 - 基于文档中的信息回答用户的问题 - 提供准确、相关且基于文档的答案 - 当问题超出文档范围时,明确告知用户该信息未在文档中提及 ## 工作流程 1. 首先分析用户上传的文档,提取关键信息 2. 理解用户提出的问题 3. 在文档中查找与问题相关的信息 4. 整合相关信息并形成结构化答案 5. 如无法从文档中找到相关信息,则说明情况 ## 回答规范 - 严格基于文档内容作答,不得编造信息 - 引用文档中的具体信息时,请保持原文准确性 - 如果问题涉及多个知识点,在答案中清晰分点说明 - 对于不确定的内容,应诚实表达不确定性,而非猜测 - 保持回答简洁明了,同时确保信息完整 ## 注意事项 - 不得脱离文档内容进行回答 - 遇到模糊或不明确的问题时,可以请求用户提供更详细的信息 - 如果文档中没有相关内容,必须明确告知用户 - 保持专业、礼貌的沟通态度 

3.3 运行与测试
启动应用
:运行D05Application主类
访问页面
:打开http://localhost:8080/chat
上传文档
:选择PDF、Word或文本文件
提问测试
:在输入框中输入关于文档的问题
当然在启动时,可以在启动参数中指定大模型的ApiKey,也可以直接修改applicatino.yml,直接维护上apiKey也可以哦

3.4 核心技术要点小结
1. RAG工作流程

检索阶段
:当用户提问时,系统首先将问题转换为向量,然后在文档向量库中查找相似的文档片段
生成阶段
:将检索到的相关文档内容与用户问题一起输入大语言模型,生成最终答案
2. 文档处理优化
中文分词
:使用HanLP进行精确的中文分词,提高语义理解准确性
文档分块
:将长文档合理分块,保持语义完整性的同时便于检索
去重机制
:通过MD5哈希避免重复上传相同的文档
3. 性能优化
相似度计算
:使用余弦相似度算法计算文本相似度
缓存机制
:对已处理的文档进行缓存,避免重复处理
流式响应
:使用SSE实现答案的流式返回,提升用户体验

Read more

Ubuntu+Docker实战:手把手教你整合MyIP与cpolar实现内网穿透

Ubuntu+Docker实战:手把手教你整合MyIP与cpolar实现内网穿透

文章目录 * 前言 * 1.关于 MyIP * 2.Docker 部署 * 3.MyIP 简单使用 * 4.安装 cpolar 内网穿透 * 5. 配置公网地址 * 6. 配置固定公网地址 * 总结 前言 技术探索者们请注意!我们即将揭晓一项突破性网络技术方案——MyIP 系统!这项创新技术颠覆了传统网络部署模式,即使在缺乏固定公网地址且不依赖云端架构的前提下,您依然能够搭建个性化的 IP 控制体系。借助这一智能平台,用户将获得持续在线的远程操控权限,如同配备了全天候值守的网络运维专家。接下来,我们将详细解析在 Ubuntu 操作系统中实施部署的具体步骤,并融合 cpolar 的内网穿透技术,打造稳定高效的远程连接方案! 1.关于 MyIP MyIP 就是一个集多种网络工具于一身的小能手。它可以帮你查看本地公网 IP、查询任意 IP

By Ne0inhk
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南

在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南

想在Linux系统用Claude Code提升编程效率,却卡在系统适配门槛?想让 AI 助手深度融入 VS Code 开发流程,却不懂插件配置技巧?这篇“保姆级指南”专为你打造,从Claude Code的Linux 环境搭建到通过本地VS Code SSH远程访问服务器端Claude Code的无缝集成,每一步都配清晰操作说明,新手也能轻松上手。 本文有两个部分(干货满满!): 1. Linux安装Claude Code 2. 使用本机VS Code SSH远程访问服务器端Claude Code帮助AI编程 一、Linux安装Claude Code 1、安装必要工具 1.1 Linux端安装Node.js和Git         Node.js提供运行环境,支持 Claude Code 的 JavaScript 代码执行;Git 用于获取其代码仓库或管理版本依赖。

By Ne0inhk
Flutter 组件 mek_data_class_generator 的鸿蒙化适配实战 - 驾驭核心数据防腐大厂,实现 OpenHarmony 业务模型的不可变性与零污染自动化生成

Flutter 组件 mek_data_class_generator 的鸿蒙化适配实战 - 驾驭核心数据防腐大厂,实现 OpenHarmony 业务模型的不可变性与零污染自动化生成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 mek_data_class_generator 的鸿蒙化适配实战 - 驾驭核心数据防腐大厂,实现 OpenHarmony 业务模型的不可变性与零污染自动化生成 前言 在鸿蒙(OpenHarmony)生态全力出海的背景下,无论是车载系统、医疗平板还是重型工控终端,其核心业务逻辑的复杂度正呈指数级增长。作为架构师,我们在处理诸如 0308 批次的员工打卡模型、医院监控大宽表等数据实体流转时,最头疼的莫过于人手编写那些冗长的 copyWith、operator == 和 hashCode。 靠人手去维护这些“防手残”的基础逻辑,不仅极其枯燥,更容易引发致命的业务空隙。一旦你在给实体类加字段时忘了更新 hashCode 的对比规则,在分布式流转中就会产生难以察觉的对象识别错误。mek_data_class_generator 正是为了终结这种低级错误而生的“代码冷血机器”。它通过自动化生成线,

By Ne0inhk
【OpenClaw从入门到精通】:环境搭建全攻略——Windows/macOS/Linux三平台部署指南(2026实测)

【OpenClaw从入门到精通】:环境搭建全攻略——Windows/macOS/Linux三平台部署指南(2026实测)

【OpenClaw从入门到精通】:环境搭建全攻略——Windows/macOS/Linux三平台部署指南(2026实测) 引言 环境搭建是使用OpenClaw的第一步,也是确保系统稳定运行的基础。不同操作系统和环境配置可能会影响OpenClaw的性能和功能表现。本文将详细介绍在Windows、macOS和Linux三大主流平台上的OpenClaw部署方法,包括最佳实践、常见问题和解决方案。 通过本文的指导,你将能够根据自己使用的操作系统选择最适合的部署方案,确保OpenClaw在你的环境中稳定高效运行。 系统要求 通用要求 * Node.js: >= 22.0.0 * 内存: 最少8GB,推荐16GB以上 * 存储: 至少10GB可用空间 * 网络: 稳定的互联网连接 平台特定要求 Windows平台 * 操作系统: Windows 10 (1903+) 或 Windows 11 * 内存: 最少8GB,推荐16GB *

By Ne0inhk