当前位置: 首页 > 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.rkmt.cn/news/1419381.html

相关文章:

  • 告别付费软件!用FileZilla Server在Win10上5分钟搞定个人FTP服务器
  • MinIO分享链接太长太丑?教你一键生成带域名的短链接(CentOS 7实战)
  • AI搜索优化值不值?价格与效果真实解析
  • 基于树莓派与E-ink屏幕打造低功耗智能信息显示终端
  • 程序代码篇---多语言混合编程
  • 从Kaggle肺炎X光分类项目实战出发:5步搞定PyTorch Grad-CAM,让你的模型‘说话’
  • PAT天梯赛L2-045‘堆宝塔’:一个被低估的栈应用经典练习题
  • 差分隐私算法审计实战:DP-Auditorium原理与应用指南
  • 一文带你解锁最佳电子书阅读平台
  • PVE虚拟化实战:如何为你的虚拟机配置最佳性能参数(CPU、内存、磁盘IO避坑指南)
  • Google量子计算新动向:纠错工程化与实用应用探索
  • 读工业软件简史04行业软件
  • 为什么你的Claude系统总在边界场景崩塌?——4类反模式诊断清单及模式加固方案
  • 从电影评分到游戏排名:用Kendall‘s Tau-b实战分析‘并列排名‘数据(附Python避坑指南)
  • Mermaid Live Editor:当代码遇见视觉,如何用5行文本绘制专业图表?
  • AI赋能数据映射:从人工规则到智能推荐的决策引擎重构
  • Win10开机蓝屏提示No Bootable Device?别急着送修,先试试这5个自救方法(含详细步骤)
  • 察元AI单机版与多用户版同源 governance模块的退化方式
  • RailX架构:超大规模LLM训练的网络革新与优化
  • 避坑指南:惠普光影精灵2升级固态硬盘后,如何确保系统从新盘启动?
  • 避开这些坑!GD32F4xx定时器配置常见误区与实战排错指南
  • RuoYi-Vue + PostgreSQL实战:除了改驱动和URL,别忘了配置Quartz和修复这些Mapper坑
  • FreeRTOS任务调度“慢镜头”回放:用SystemView揪出优先级反转的元凶
  • 给老MacBook Air续命:保姆级Fedora 35安装与Wi-Fi驱动修复全记录
  • 从靶场到实战:手把手教你用Burp Suite爆破SSRF端口(CTFHub实战复盘)
  • SQuId工具实战:多语言语音合成质量自动化评估指南
  • SMUDebugTool:AMD Ryzen系统硬件调试的终极指南
  • AI时代网络安全范式转移:开发者如何应对生成式AI带来的攻防变革
  • 出差党福音:用NPS+腾讯云轻量服务器,5分钟搞定远程家里游戏主机的内网穿透
  • 程序员平均对接一个AI平台用了多少小时?比如我用QQ大模型广场对接,deepseek-v4-flash,用了大约一天时间吧。 收到SSE数据还得人工解析