ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java开发者Spring AI实战教程:从零到构建文档智能问答系统!

Java开发者Spring AI实战教程:从零到构建文档智能问答系统! 1. 为什么 Java 后端需要一套文档智能问答系统很多 Java 开发者第一次接触大模型都是从写一个main方法调 Chat API 开始的。跑通那一刻确实爽但很快会遇到一个尴尬的现实模型不知道你公司内部的接口文档、产品手册、历史工单问它「订单超时补偿规则是什么」它只能一本正经地胡说。这就是文档智能问答系统要解决的问题——把私有文档喂给模型让它基于你的资料回答而不是靠训练时的记忆瞎编。我试过用纯手写 HTTP 请求的方式做这件事代码量很快就失控了文档切分、向量化、存库、检索、拼 Prompt、多轮记忆每一块都要自己粘。Spring AI 的价值就在于把这些环节抽象成 Spring 风格的 Bean 和接口你熟悉的Autowired、application.yml、ChatClient链式调用都能直接复用。对于已经写了几年 Spring Boot 的后端来说学习曲线比 LangChain 平缓得多。这套系统适合谁三类人最划算一是要做企业内部知识库的后端工程师二是想给现有 SaaS 产品加「智能客服」模块的团队三是准备面试大模型应用岗、需要一个能讲清楚的实战项目的同学。它不需要你懂 PyTorch也不需要 GPU一台能跑 Spring Boot 的机器加一个模型 API Key 就能起步。整条链路我拆成四段文档加载Reader→ 切分与向量化Embedding→ 存入向量库VectorStore→ 检索增强生成RAG。下面按这个顺序把每一步的依赖、配置、代码骨架和踩坑点都摊开讲。模型服务这块我用 TaoToken 作为统一入口来演示因为它同时提供 OpenAI 兼容协议和 Claude 系列Spring AI 的 OpenAI Starter 可以直接对接省去为不同厂商改代码的麻烦。2. Spring AI 环境搭建与 TaoToken 接入前置在写业务代码之前先把工程骨架和模型通道打通。这一步做扎实后面调 RAG 才不会因为「连不上模型」这种低级问题卡半天。2.1 依赖与版本选择Spring AI 目前迭代较快建议用 Spring Boot 3.2 搭配 Spring AI 1.0.x 的稳定版。pom.xml里核心是三个 starterOpenAI 兼容的 chat 客户端、向量库、以及文档读取。下面是我实测能跑通的依赖片段properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-simple/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-document-reader-pdf/artifactId /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意spring-ai-starter-vector-store-simple是内存向量库适合本地验证。生产环境换成 PGVector 或 Milvus 时只需替换这个 starter 并改配置业务代码基本不动这是 Spring AI 抽象带来的好处。2.2 用 TaoToken 统一模型入口Spring AI 的 OpenAI Starter 默认指向 OpenAI 官方地址我们要把它改成 TaoToken 的兼容端点。TaoToken 的 API 地址是https://taotoken.net/api它兼容 OpenAI 的/v1/chat/completions和/v1/embeddings协议所以 chat 和 embedding 可以走同一个 Key。先去控制台创建一个 API Key地址在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite创建后复制保存。然后在application.yml里配置spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.3 embedding: options: model: text-embedding-3-small这里有两个关键点。第一base-url只写到/apiSpring AI 会自动拼/v1/chat/completions如果你写成/api/v1反而会 404。第二api-key用环境变量注入别硬编码进 Git这是基本安全习惯。模型 ID 我选gpt-4o-mini做对话、text-embedding-3-small做向量化前者便宜够用后者 1536 维检索效果和成本平衡得不错。如果你更习惯用 Claude 系列做问答TaoToken 也支持把 model 换成对应的 Claude 模型 ID 即可协议层不用改。想先对比不同模型效果可以打开模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite手动试几句确认回答风格符合预期再写进配置。2.3 验证模型通道是否打通配置完别急着写 RAG先写一个最小的 ChatClient 调用确认通道正常RestController public class PingController { private final ChatClient chatClient; public PingController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/ping) public String ping() { return chatClient.prompt() .user(用一句话说明什么是向量检索) .call() .content(); } }启动后访问http://localhost:8080/ping能返回一句通顺的中文说明 Base URL、Key、Model ID 三件套都对。如果这里就报错先别往下走对照第 5 节的排查表解决。这一步花五分钟能省掉后面半小时的困惑。3. 文档加载、向量化与 VectorStore 可复制配置通道打通后进入 RAG 的核心把文档变成可检索的向量。这一节给出完整的配置片段和代码骨架你可以直接复制到项目里改。3.1 文档读取与切分Spring AI 的DocumentReader负责把 PDF、Markdown、纯文本读成Document对象。以 PDF 为例Configuration public class EtlConfig { Bean public TokenTextSplitter tokenTextSplitter() { return new TokenTextSplitter(500, 100, 10, 5000, true); } }TokenTextSplitter的五个参数分别是目标块大小 500 token、块间重叠 100 token、最小块 10 token、最大块 5000 token、是否保留分隔符。重叠很重要它保证切分点附近的语义不被割裂检索时不会因为一句话被切成两半而丢上下文。读取 PDF 的代码Service public class DocumentIngestService { private final VectorStore vectorStore; private final TokenTextSplitter splitter; public DocumentIngestService(VectorStore vectorStore, TokenTextSplitter splitter) { this.vectorStore vectorStore; this.splitter splitter; } public int ingest(Resource pdfResource) { PdfDocumentReaderConfig config PdfDocumentReaderConfig.builder() .withPagesPerDocument(1) .build(); PagePdfDocumentReader reader new PagePdfDocumentReader(pdfResource, config); ListDocument docs reader.get(); ListDocument chunks splitter.apply(docs); vectorStore.add(chunks); return chunks.size(); } }vectorStore.add(chunks)这一步内部会自动调用 Embedding 模型把每个 chunk 转成向量并存储。你不需要手动调 embedding 接口Spring AI 帮你串好了。3.2 VectorStore 参数对照不同向量库的配置差异主要在连接信息和索引参数上。下面这张表是我实际用过的三种库的对照方便你按场景选向量库Starter 依赖关键配置项适用场景SimpleVectorStorespring-ai-starter-vector-store-simple无纯内存本地验证、单元测试PGVectorspring-ai-starter-vector-store-pgvectorspring.ai.vectorstore.pgvector.dimensions1536已有 PG、数据量中等Milvusspring-ai-starter-vector-store-milvusspring.ai.vectorstore.milvus.collection-name大规模、高并发检索以 PGVector 为例application.yml配置如下spring: ai: vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1536 initialize-schema: truedimensions必须和 Embedding 模型输出维度一致text-embedding-3-small是 1536写错会在插入时报维度不匹配。initialize-schema: true会自动建表和索引生产环境建议关掉改成手动迁移避免应用启动时锁表。3.3 检索参数调优检索时最关键的两个参数是topK和相似度阈值。topK控制召回多少条太大容易引入噪声太小可能漏掉答案。我的经验是文档问答场景topK4到6比较稳配合相似度阈值 0.7 过滤掉不相关的块SearchRequest request SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.7) .build(); ListDocument docs vectorStore.similaritySearch(request);如果发现回答经常「答非所问」先把阈值调高到 0.75 试试如果经常「找不到答案」把 topK 加到 8 并降低阈值。这两个参数没有万能值要拿你的真实文档测。4. 问答链路代码骨架与检索命中验证前面把文档灌进了向量库现在把检索和生成串成一条完整的问答链路并用样例文档验证它真的能命中。4.1 RAG 问答服务骨架核心思路是用户提问 → 向量检索拿相关文档 → 把文档拼进 Prompt → 调模型生成回答。Spring AI 提供了QuestionAnswerAdvisor把这个流程封装好了Service public class RagQaService { private final ChatClient chatClient; private final VectorStore vectorStore; public RagQaService(ChatClient.Builder builder, VectorStore vectorStore) { this.vectorStore vectorStore; this.chatClient builder .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder() .topK(5) .similarityThreshold(0.7) .build()) .build()) .defaultSystem(你是一个文档问答助手只根据提供的上下文回答 上下文没有的信息就明确说不知道不要编造。) .build(); } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }QuestionAnswerAdvisor会在每次调用时自动执行检索、拼接上下文、注入 Prompt。defaultSystem里那句「不要编造」很关键它能显著降低模型在检索不到时的幻觉率。4.2 带引用来源的返回生产环境里用户往往想知道答案出自哪份文档。我们可以手动检索并返回来源public record AnswerResult(String answer, ListString sources) {} public AnswerResult askWithSources(String question) { ListDocument docs vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(5).build()); String context docs.stream() .map(Document::getText) .collect(Collectors.joining(\n---\n)); String answer chatClient.prompt() .user(u - u.text(基于以下资料回答问题\n{context}\n\n问题{q}) .param(context, context) .param(q, question)) .call() .content(); ListString sources docs.stream() .map(d - String.valueOf(d.getMetadata().get(source))) .distinct() .toList(); return new AnswerResult(answer, sources); }4.3 用样例文档验证命中准备一份测试文档比如一段产品退款政策灌进去后问几个问题。我实测下来验证要覆盖三种情况第一种文档里明确有的信息比如「退款申请后几个工作日到账」模型应该准确答出并给出正确来源。第二种文档里没有的信息比如「支持比特币退款吗」模型应该回答「资料中未提及」而不是瞎编。第三种需要跨段落综合的问题比如「超过 7 天的订单还能退吗」考察检索是否召回了多个相关块。如果第一种答错检查切分粒度是不是太粗导致关键信息被稀释如果第二种开始编造加强 system prompt 的约束如果第三种召回不全提高 topK。每次调整后重新灌文档再测别在旧索引上改参数容易混淆变量。5. 常见报错排查对照表RAG 链路长出错点分散。下面这张表覆盖了我遇到过的典型报错按现象、原因、解决三步走。报错现象根本原因解决动作401 UnauthorizedAPI Key 错误或未注入检查环境变量TAOTOKEN_API_KEY是否生效Key 是否有多余空格local proxy failed / Connection refusedbase-url 写错或网络不通确认 base-url 为https://taotoken.net/api不带/v1reading choices 时 NPE响应体为空或模型 ID 不存在打印原始响应核对 model ID 拼写向量维度不匹配embedding 维度与库配置不一致统一为 1536重建索引检索结果为空阈值过高或文档未成功入库降低 similarityThreshold查库中记录数OAuth / token 过期用了需要 OAuth 的端点改用 API Key 方式确认 Key 未过期重点说两个高频坑。第一个是base-url多写了/v1。Spring AI 的 OpenAI 客户端会在 base-url 后自动追加/v1/chat/completions你写https://taotoken.net/api/v1就变成/api/v1/v1/...直接 404。第二个是 embedding 和 chat 用了不同的 Key 或不同的 base-url导致向量化成功但对话失败或者反过来。建议 chat 和 embedding 共用同一个 TaoToken Key配置写在一起减少变量。还有一个隐蔽的坑initialize-schema: true在 PGVector 下每次启动都会尝试建表如果数据库账号没有 DDL 权限会静默失败表现为「插入成功但检索不到」。遇到这种情况手动执行建表 SQL 或给账号加权限。排查时养成看完整堆栈的习惯Spring AI 的异常信息通常包含底层 HTTP 状态码和响应体比只看最外层异常有用得多。如果确认是 Key 或额度问题去控制台https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite检查 Key 状态和用量。6. 从能跑到好用下一步怎么走到这里一个能检索、能回答、能给出处的文档智能问答系统已经跑起来了。但「能跑」和「好用」之间还有距离分享几个我踩过坑之后总结的方向。第一切分策略要按文档类型调。技术文档适合按标题层级切合同类适合按条款切纯 PDF 扫描件还得先做 OCR。TokenTextSplitter是通用方案遇到结构化文档时自定义DocumentTransformer效果更好。第二多轮对话要加记忆。现在的实现每次提问都是独立的用户追问「那第二种情况呢」时模型不知道指什么。Spring AI 的ChatMemory配合MessageChatMemoryAdvisor能解决把历史消息存进InMemoryChatMemoryRepository或 JDBC 仓库即可。第三检索质量可以加一层重排。向量检索召回的是语义相近的块但不一定最相关。引入 rerank 模型对 topK 结果二次排序能明显提升答案准确度代价是多一次模型调用。第四长期编码和 Agent 场景建议用 Coding Plan。如果你要把这套系统扩展成能调工具、能多步推理的 Agent按量计费的 API 调用成本不好控包月方案更划算具体可以看https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。最后给一个实用技巧上线前用一批真实用户问题做回归测试记录每个问题的检索命中率和回答准确率形成基线。之后每次改切分参数、换模型、调阈值都跑一遍这套测试用数据判断改动是变好还是变坏而不是凭感觉。这套方法帮我在几个项目里避免了「改一个参数修好三个问题又引入两个新问题」的循环。
返回列表