当前位置: 首页 > news >正文

搭建本地知识库系统:基于spring-ai的实战案例

做本地大模型落地时,知识库系统至少要解决以下几个问题:知识从哪里来、如何切分、如何向量化、如何检索、如何把检索结果交给模型,以及模型、数据库、配置出错时是否显式失败。

本次案例基于 Spring Boot 3.5、Spring AI 1.0.3、Ollama、PGVector 和 PostgreSQL,形成“知识文档 -> 分块 -> 向量入库 -> 相似度检索 -> 基于检索结果回答”的完整闭环。聊天模型使用sorc/qwen3.5-claude-4.6-opus:4b,向量模型使用nomic-embed-text-v2-moe,向量维度实测为768

这篇文章不再停留在原理层,而是把这次模块的目录结构、关键代码实现、生产细节和真实输出案例一起展开。

一、为什么不能只做一个聊天接口?

如果只有聊天接口,模型回答命不命中业务事实,完全取决于参数知识。这会带来三个工程问题:

  • • 回答不可控,模型可能编造不存在的表名、命令或配置。
  • • 知识无法沉淀,新的事实没有标准入库路径。
  • • 故障难排查,回答错误时很难判断是模型问题、知识缺失还是检索失败。

知识库系统要把“知识管理”和“回答生成”拆开。知识层负责把可信内容切分后写入向量库,问答层只基于检索结果回答。这样一来,行为边界、验证路径和故障定位才真正清晰。

二、技术选型与工程边界

本项目的核心依赖如下:

  • spring-ai-starter-model-ollama
    负责接入本地 Ollama 聊天模型和 embedding 模型。
  • spring-ai-starter-vector-store-pgvector
    负责把向量写入 PGVector 并做相似度检索。
  • spring-boot-starter-jdbc
    提供数据源、连接池和 JDBC 基础设施。
  • • PostgreSQL +vector扩展
    负责承载向量和文档元数据。

这里最关键的工程判断有两个:

    1. 聊天模型和 embedding 模型不能混用。sorc/qwen3.5-claude-4.6-opus:4b负责生成回答,nomic-embed-text-v2-moe负责生成向量。
    1. 敏感信息不能写入源码。数据库密码必须通过环境变量注入,配置缺失时直接失败。

三、模块目录结构

这次新增模块的真实目录结构如下:

scper-qwen-repo/├── pom.xml├── src│ ├── main│ │ ├── java/com/scper/project/backend/qwenrepo│ │ │ ├── ScperQwenRepoApplication.java│ │ │ ├── api│ │ │ │ ├── KnowledgeAskRequest.java│ │ │ │ ├── KnowledgeAnswerResponse.java│ │ │ │ ├── KnowledgeController.java│ │ │ │ ├── KnowledgeReference.java│ │ │ │ └── KnowledgeReindexResponse.java│ │ │ ├── config│ │ │ │ └── KnowledgeBaseProperties.java│ │ │ ├── service│ │ │ │ ├── KnowledgeBaseService.java│ │ │ │ ├── KnowledgeDocumentLoader.java│ │ │ │ ├── KnowledgeReindexResult.java│ │ │ │ └── KnowledgeStartupRunner.java│ │ │ └── support│ │ │ ├── KnowledgeApiErrorResponse.java│ │ │ ├── KnowledgeExceptionHandler.java│ │ │ ├── KnowledgeInfoContributor.java│ │ │ ├── KnowledgeModelInvocationException.java│ │ │ └── KnowledgeStoreOperationException.java│ │ └── resources│ │ ├── application.yml│ │ └── knowledge-base│ │ ├── 01-module-overview.md│ │ ├── 02-architecture-runbook.md│ │ ├── 03-operations-and-troubleshooting.md│ │ └── 04-production-guidelines.md│ └── test│ ├── java/com/scper/project/backend/qwenrepo│ │ ├── api/KnowledgeControllerTest.java│ │ └── service/KnowledgeDocumentLoaderTest.java│ └── resources/junit-platform.properties

这套结构的目标很明确:

  • api/只管 HTTP 契约。
  • service/只管知识加载、重建和问答主流程。
  • support/只管错误契约和可观测性。
  • resources/knowledge-base/是可控、可验证、可重复的知识源。

四、关键配置怎么落地?

application.yml文件配置如下:

spring: datasource: url: jdbc:postgresql://${SCPER_QWEN_REPO_DB_HOST:8.134.159.245}:${SCPER_QWEN_REPO_DB_PORT:5433}/${SCPER_QWEN_REPO_DB_NAME:scper_ai}?${SCPER_QWEN_REPO_DB_PARAMS:sslmode=disable} username: ${SCPER_QWEN_REPO_DB_USERNAME:scper} password: ${SCPER_QWEN_REPO_DB_PASSWORD} ai: ollama: base-url: ${SCPER_QWEN_REPO_OLLAMA_BASE_URL:http://127.0.0.1:11434} chat: model: ${SCPER_QWEN_REPO_CHAT_MODEL:sorc/qwen3.5-claude-4.6-opus:4b} options: model: ${SCPER_QWEN_REPO_CHAT_MODEL:sorc/qwen3.5-claude-4.6-opus:4b} temperature: 0.1 top-p: 0.85 num-ctx: 4096 embedding: model: ${SCPER_QWEN_REPO_EMBEDDING_MODEL:nomic-embed-text-v2-moe:latest} options: model: ${SCPER_QWEN_REPO_EMBEDDING_MODEL:nomic-embed-text-v2-moe:latest} vectorstore: pgvector: dimensions: 768 distance-type: cosine-distance index-type: hnsw initialize-schema: true schema-validation: false table-name: scper_qwen_repo_documents

这里有三个细节值得强调:

    1. dimensions必须和 embedding 模型输出一致,这次实测是768
    1. JDBC URL 默认附带sslmode=disable,因为目标 PostgreSQL 实例的握手策略不接受默认 SSL 协商。
    1. schema-validation设置为false,否则 Spring AI 1.0.3 在首启建表前会先校验表是否存在,导致首启失败。

五、控制器应该怎么设计?

HTTP 层保持极薄,控制器只做参数接收和响应组装,不承担业务判断:

@RestController@RequestMapping(path = "/api/knowledge", produces = MediaType.APPLICATION_JSON_VALUE)public class KnowledgeController { private final KnowledgeBaseService knowledgeBaseService; public KnowledgeController(KnowledgeBaseService knowledgeBaseService) { this.knowledgeBaseService = knowledgeBaseService; } @PostMapping(path = "/ask", consumes = MediaType.APPLICATION_JSON_VALUE) public KnowledgeAnswerResponse ask(@Valid @RequestBody KnowledgeAskRequest request) { return knowledgeBaseService.ask(request.question()); } @PostMapping(path = "/reindex") public KnowledgeReindexResponse reindex() { KnowledgeReindexResult result = knowledgeBaseService.reindex(); return new KnowledgeReindexResponse( result.indexedDocuments(), result.indexedChunks(), result.elapsedMs(), result.completedAt() ); }}

这个设计的好处是:

  • /api/knowledge/reindex/api/knowledge/ask的职责天然分离。
  • • 控制器逻辑稳定,适合用@WebMvcTest做快速契约测试。
  • • 所有显式失败都能统一落到异常处理器,不会在控制器里堆条件分支。

六、知识加载和分块实现

知识源不是数据库表,而是模块内 Markdown 文件。这样更适合做一个可控、可重复的工程案例。

本案例里,KnowledgeDocumentLoader做了四件事:

    1. 扫描classpath*:knowledge-base/*.md
    1. 按一级标题、二级标题拆章节
    1. 按段落优先、字符数兜底做 chunk 切分
    1. 生成稳定 UUID,写入文档元数据

核心实现如下:

private List<Document> chunkDocument(String sourceName, String content) { ParsedKnowledgeFile parsed = parseMarkdown(sourceName, content); List<Document> documents = new ArrayList<>(); int chunkIndex = 0; for (ParsedSection section : parsed.sections()) { List<String> chunks = splitIntoChunks(section.body()); for (String chunk : chunks) { String stableId = stableId(sourceName + "|" + section.heading() + "|" + chunkIndex); String text = "标题:" + parsed.title() + "\n章节:" + section.heading() + "\n内容:" + chunk; Map<String, Object> metadata = new LinkedHashMap<>(); metadata.put("source", sourceName); metadata.put("title", parsed.title()); metadata.put("section", section.heading()); metadata.put("chunkIndex", chunkIndex); documents.add(new Document(stableId, text, metadata)); chunkIndex++; } } return documents;}

七、问答主链路怎么实现

KnowledgeBaseService是整个模块的核心。它把“重建知识库”和“基于知识回答”这两个动作都收口在服务层。

1. 重建逻辑

public KnowledgeReindexResult reindex() { long startedAt = System.nanoTime(); KnowledgeDocumentLoader.LoadedKnowledgeBase knowledgeBase = documentLoader.load(); try { vectorStore.add(knowledgeBase.documents()); } catch (RuntimeException ex) { throw new KnowledgeStoreOperationException("Failed to write knowledge chunks into PGVector", ex); } return new KnowledgeReindexResult( knowledgeBase.indexedDocuments(), knowledgeBase.documents().size(), elapsedMs(startedAt), OffsetDateTime.now(ZoneOffset.UTC) );}

2. 问答逻辑

public KnowledgeAnswerResponse ask(String question) { String normalizedQuestion = normalizeQuestion(question); long retrievalStartedAt = System.nanoTime(); SearchRequest.Builder searchBuilder = SearchRequest.builder() .query(normalizedQuestion) .topK(properties.getRetrievalTopK()); searchBuilder.similarityThresholdAll(); List<Document> retrievedDocuments = vectorStore.similaritySearch(searchBuilder.build()); if (retrievedDocuments == null || retrievedDocuments.isEmpty()) { throw new KnowledgeStoreOperationException( "Knowledge repository returned no matching chunks. Reindex the repository and retry."); } String answer = chatClient.prompt() .system(properties.getSystemRole()) .user(buildUserPrompt(normalizedQuestion, retrievedDocuments)) .call() .content(); return new KnowledgeAnswerResponse( normalizedQuestion, answer.trim(), chatModelName, embeddingModelName, "pgvector", retrievedDocuments.size(), retrievalLatencyMs, elapsedMs(generationStartedAt), OffsetDateTime.now(ZoneOffset.UTC), toReferences(retrievedDocuments) );}

这段实现体现了三个生产级约束:

  • • 检索失败和模型失败要明确区分,不能统一糊成“问答失败”。
  • • 问答结果必须带引用元数据和时延,方便排查“答错了”和“答慢了”。
  • • 提示词必须明确限制模型只能依据检索片段回答,不允许自由发挥。

八、启动自举为什么很关键?

如果每次启动都要手工先导知识、再测问答,验证成本会非常高。所以模块增加了一个KnowledgeStartupRunner,启动时自动完成知识重建,必要时还会自动跑一次 demo 问答。

实现如下:

@Componentpublic class KnowledgeStartupRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { if (properties.getBootstrap().isEnabled()) { KnowledgeReindexResult result = knowledgeBaseService.reindex(); log.info("scper-qwen-repo bootstrap completed: indexedDocuments={}, indexedChunks={}, elapsedMs={}", result.indexedDocuments(), result.indexedChunks(), result.elapsedMs()); } if (properties.getDemo().isEnabled()) { KnowledgeAnswerResponse response = knowledgeBaseService.ask(properties.getDemo().getQuestion()); log.info("scper-qwen-repo demo question: {}", response.question()); log.info("scper-qwen-repo demo answer: {}", response.answer()); } }}

直到知识库真的完成了初始化。

九、异常处理和显式失败

知识库系统尤其不能靠静默兜底。因为一旦重建失败、数据库断了或者模型没返回内容,伪成功只会把错误传播到下游。

这个模块的失败策略是:

  • • 参数为空或超长,直接 400
  • • PGVector 查询或入库失败,直接 503
  • • Ollama 调用失败或返回空内容,直接 502
  • • 知识资源缺失或无有效 chunk,启动直接失败

十、怎么运行这套系统?

1. 准备环境

  • • 本地 Ollama 已安装
  • ollama pull sorc/qwen3.5-claude-4.6-opus:4b
  • ollama pull nomic-embed-text-v2-moe:latest
  • • PGVector 对应 PostgreSQL 已安装vector扩展
  • • 环境变量SCPER_QWEN_REPO_DB_PASSWORD已设置

docker环境安装pgvector

docker run -d \ --name scper-pgvector \ -e POSTGRES_DB=scper_ai \ -e POSTGRES_USER=scper \ -e POSTGRES_PASSWORD=scper123 \ -p 5432:5432 \ -v $HOME/docker-data/scper-pgvector:/var/lib/postgresql/data \ pgvector/pgvector:pg17

2. 构建

cd scper-project-backendmvn clean package -pl scper-qwen-repo -am -DskipTests=false

3. 启动

cd scper-project-backend/scper-qwen-repoSCPER_QWEN_REPO_DB_PASSWORD=你的密码 \java -jar target/scper-qwen-repo-0.1.0-SNAPSHOT.jar --server.port=18084

4. 手工触发重建

curl -X POST http://127.0.0.1:18084/api/knowledge/reindex

5. 发起问答

curl -X POST http://127.0.0.1:18084/api/knowledge/ask \ -H 'Content-Type: application/json' \ -d '{"question":"如何手工触发知识库重建?"}'

十一、几个真实输出案例

下面这些输出不是伪造示例,而是这次模块在真实环境里跑出来的结果。

案例 1:构建成功

[INFO] Reactor Summary for scper-project-backend 0.1.0-SNAPSHOT:[INFO][INFO] scper-project-backend .............................. SUCCESS[INFO] scper-qwen-repo .................................... SUCCESS[INFO] BUILD SUCCESS

案例 2:启动自举完成

2026-03-31T22:41:28.476+08:00 INFO ... KnowledgeStartupRunner :scper-qwen-repo bootstrap completed: indexedDocuments=4, indexedChunks=14, elapsedMs=2293

这条日志说明两件事:

  • • 模块启动后已经真正完成了知识文档入库
  • • 当前知识库被切成了14个 chunk,而不是只把原文硬塞给模型

案例 3:Actuator 信息输出

{ "scperQwenRepo": { "provider": "ollama+pgvector", "ollamaBaseUrl": "http://127.0.0.1:11434", "chatModel": "sorc/qwen3.5-claude-4.6-opus:4b", "resourcePattern": "classpath*:knowledge-base/*.md", "tableName": "scper_qwen_repo_documents", "embeddingModel": "nomic-embed-text-v2-moe:latest", "datasourceUrl": "jdbc:postgresql://8.134.159.245:5433/scper_ai?sslmode=disable" }}

这个输出很适合排查线上配置问题,因为它把当前运行实例到底用了什么模型、什么数据源、什么表都直接暴露出来了。

案例 4:重建接口真实返回

{ "indexedDocuments": 4, "indexedChunks": 14, "elapsedMs": 408, "completedAt": "2026-03-31T14:42:10.200316Z"}

这说明手工POST /api/knowledge/reindex已经能返回结构化结果,而不是只靠日志侧信号判断成功与否。

案例 5:问答接口真实返回

{ "question": "如何手工触发知识库重建?", "answer": "## 如何手工触发知识库重建?\n\n### 结论\n手工触发知识库重建需要通过调用 REST API 接口完成,具体为:\n```\nPOST /api/knowledge/reindex\n```", "chatModel": "sorc/qwen3.5-claude-4.6-opus:4b", "embeddingModel": "nomic-embed-text-v2-moe:latest", "vectorStore": "pgvector", "retrievedChunks": 4, "retrievalLatencyMs": 374, "generationLatencyMs": 54877, "generatedAt": "2026-03-31T14:43:05.099604Z", "references": [ { "source": "02-architecture-runbook.md", "section": "本地重建步骤" } ]}

这里有两个值得注意的点:

  • retrievedChunks=4说明系统不是裸调模型,而是真的经过了向量检索。
  • generationLatencyMs=54877说明首轮推理依然不快,本地模型冷启动和硬件规格会直接影响体验。

案例 6:启动 demo 真实回答

scper-qwen-repo demo question: 如何在本地重建 scper-qwen-repo 的知识库并验证问答已经打通?scper-qwen-repo demo answer:## 如何在本地重建 scper-qwen-repo 的知识库并验证问答已经打通### 结论1. 编译模块2. 设置 SCPER_QWEN_REPO_DB_PASSWORD3. 启动服务并启用 demo 模式4. 通过 demo 日志或 API 验证问答

这说明启动阶段不只是“服务起来了”,而是“服务起来后真的完成了知识入库,并且成功走完了一次检索问答链路”。

十二、踩过的坑

1. PGVector 首启建表失败

Spring AI 1.0.3 下,如果initialize-schema=true同时又保留schema-validation=true,首启时会先校验表存在,再尝试建表,结果直接失败。这个问题最后通过关闭 schema validation 解决。

2. JDBC 握手失败

目标 PostgreSQL 实例在默认 SSL 协商阶段直接断开连接,最后必须在 JDBC URL 上显式加sslmode=disable才能稳定连接。

3. 文档 id 不是合法 UUID

最早版本把文档 id 生成成 SHA-256 十六进制串,但 PGVector 默认按 UUID 存储 id,结果在入库时报UUID string too large。最后改成UUID.nameUUIDFromBytes(...)才和 Spring AI PGVector 约束对齐。

4. 空白 section 污染检索结果

最早的 Markdown 切分逻辑会保留空白 section,导致检索结果里混入“该章节暂无正文”。最后通过addSectionIfPresent(...)过滤空白 section 才把噪声压下去。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

http://www.jsqmd.com/news/908079/

相关文章:

  • UCL等机构研究团队如何用八万段录屏测出AI助手的“真实水平“
  • Gemini发布会后第一小时必做5件事:抓取原始SDK包、提取模型签名密钥、验证MoE专家路由逻辑、比对TensorRT-LLM兼容性、归档所有HTTP/3握手日志
  • 告别付费软件!用FileZilla Server在Win10上5分钟搞定个人FTP服务器
  • 基础不牢,AI 无用;思维到位,一行胜千行
  • MinIO分享链接太长太丑?教你一键生成带域名的短链接(CentOS 7实战)
  • 老式车载收音机改造:利用磁带通道加装外部音频输入接口
  • 告别手动计算!UE4地形导入时,那个让人头疼的Z轴缩放到底怎么算?(附自动计算工具)
  • 告别阴天废片!用Python+OpenCV实现经典颜色迁移算法,一键拯救你的旅行照片
  • AI搜索优化值不值?价格与效果真实解析
  • 【DeepSeek企业版核心功能解密】:20年AI架构师亲测的5大生产级能力与避坑指南
  • 纯电动车仿真结果不准?可能是你的AVL Cruise电池和电机模块没设对!深度解析关键参数设置逻辑
  • 程序代码篇---多语言混合编程
  • LPC9xx微控制器启动文件解析与工程实践
  • 基于树莓派与E-ink屏幕打造低功耗智能信息显示终端
  • 从Kaggle肺炎X光分类项目实战出发:5步搞定PyTorch Grad-CAM,让你的模型‘说话’
  • 别再只用t-SNE了!用UMAP在Python里给MNIST数据降维,3D可视化效果惊艳
  • Speculative RAG:基于“草稿”与并行检索的生成加速实践
  • AI如何提升内容创作效率与质量:五大核心助力点详解
  • 告别卡顿!SuperMap iDesktop 11i 倾斜摄影优化实战:从OSGB到S3M3.0的完整避坑指南
  • 2026 净化板、玻镁净化板、岩棉净化板、真金净化板、机制净化板、手工净化板厂家综合榜单:板材品质、生产工艺、防火环保多维度行业分析 - 海棠依旧大
  • Ubuntu无法识别串口ttyUSB0
  • PAT天梯赛L2-045‘堆宝塔’:一个被低估的栈应用经典练习题
  • 隐私增强技术能耗分析:从TLS到全同态加密
  • 差分隐私算法审计实战:DP-Auditorium原理与应用指南
  • ZYNQ PS端串口不够用?手把手教你用Vivado的AXI Uartlite IP核在PL端轻松拓展(附SDK与Procise联动避坑指南)
  • 别再让0.66*10=6.6000000000000005了!Java中BigDecimal处理金额的完整避坑指南
  • 告别网络焦虑!用OfflineExplorer Pro把整个技术文档站扒到本地,随时随地查资料
  • YOLOv7的Backbone设计哲学:从VoVNet、CSPNet到ELAN,看目标检测骨干网络是如何“卷”起来的
  • 用IoTBASIC打造复古可编程机器人小车:从硬件搭建到无线控制
  • 一文带你解锁最佳电子书阅读平台