上一篇《别在业务代码里裸调大模型》把大模型装进了 Spring Cloud。后台问得最多的是同一句话:"问它公司内部的事,它就一本正经地胡说八道,怎么破?"
这篇就解决它。先说结论:不是模型不行,是你喂饭的方式不对。
一、把文档直接塞进 Prompt,是新手必踩的第一个坑
最直觉的方案:用户提问,把相关文档全文拼进 Prompt 一起发给模型。Demo 一跑效果惊艳,上生产四种死法:
- 装不下——一套产品手册几百页,再大的上下文窗口也塞不进去;
- 太贵——输入 token 按量计费,每问一句都重发整本手册,月底账单让你怀疑人生;
- 太慢——模型先"读完"五万字才开始答题,首字延迟几十秒,用户早跑了;
- 不更新——文档今天刚改,模型根本不知道,这就是幻觉的根源。
二、RAG 一句话:让模型开卷考试
不许背全书,只发三页相关资料。
离线把文档切片、向量化、存进向量库;在线把用户问题向量化,捞出最相关的几段,连同问题一起交给模型,让它照着资料作答。
本篇打通左半场(文档入库),下一篇讲右半场(检索与生成)。
文档 → 切片 → Embedding → 向量库 问题 → Embedding ↓ 相似度检索 相关片段 + 问题 → 大模型 → 答案三、三步把文档灌进知识库(Spring AI + Redis)
第 1 步,依赖和配置,复制就能用:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-tika-document-reader</artifactId> <!-- PDF/Word/Markdown 通吃 --> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-redis</artifactId> </dependency>spring: ai: openai: api-key: ${LLM_API_KEY} base-url: ${LLM_BASE_URL} # 国产兼容模型换地址即可 embedding: options: model: text-embedding-3-small vectorstore: redis: index: knowledge-index prefix: "doc:"向量库用 Redis Stack(普通 Redis 不支持向量索引),Docker 一行起:
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack:latest第 2 步,入库流水线,核心就十几行:
@Service public class KnowledgeIngestService { private final VectorStore vectorStore; public KnowledgeIngestService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public int ingest(String fileUrl, String docName, String tenantId) { // 1. 加载:Tika 自动识别 PDF/Word/Markdown 并抽取文本 List<Document> rawDocs = new TikaDocumentReader(fileUrl).get(); // 2. 切片:600 token 一片,相邻片重叠 100,防止语义被拦腰斩断 TokenTextSplitter splitter = TokenTextSplitter.builder() .chunkSize(600).chunkOverlap(100).build(); // 3. 打元数据:来源、租户、更新时间——下期权限过滤全靠它 List<Document> chunks = splitter.apply(rawDocs).stream() .map(d -> Document.builder().text(d.getText()) .metadata(Map.of("source", docName, "tenantId", tenantId, "updatedAt", LocalDate.now().toString())) .build()) .toList(); // 4. 向量化 + 入库一步到位 vectorStore.add(chunks); return chunks.size(); } }提醒一句:入库是离线批处理,别挂在用户请求链路里。文档上传后丢给 MQ 或异步线程池,否则 Embedding 批处理能把接口拖死。
第 3 步,先验证检索,再谈生成:
@GetMapping("/search") public List<String> search(@RequestParam String q) { SearchRequest request = SearchRequest.builder() .query(q) .topK(4) // 召回最相关的 4 片 .similarityThreshold(0.7) // 相似度门槛,滤掉牛头不对马嘴的 .build(); return vectorStore.similaritySearch(request).stream() .map(d -> d.getText() + "【来源:" + d.getMetadata().get("source") + "】") .toList(); }拿十几个真实业务问题打一遍:检索结果不对,模型再强也白搭。这半场验收通过,再进下一篇的生成环节。
四、切片的三个坑,我替你踩过了
- 别按字数硬切。每 500 字一刀,代码被腰斩、表格被劈成两半,向量化出来的全是残次语义。正确姿势:先按标题/章节结构切,再用长度兜底。
- 一定要留重叠。相邻两片保留 10%~20% 重叠,否则正好从一句话中间切开,两片都读不懂。
- metadata 第一天就要打全。来源、租户、密级、更新时间……很多人上线后才发现没法做权限过滤,只能全量重建。下一篇的多租户隔离,靠的就是今天打进去的
tenantId。
五、Embedding 和向量库,各给你一句话结论
Embedding 两条纪律:
- 一个知识库固定一个 Embedding 模型,换模型 = 全量重新入库;
- 文档更新只重算变化的切片(用内容哈希判增量),别无脑全量重建。
向量库一句话选型:百万级以内 Redis 足够(你们本来就有);要做 SQL 级权限过滤上 pgvector;真到千万级再请出 Milvus。别为了简历优化架构。
六、写在最后
下一篇是 RAG 的灵魂:怎么把召回片段拼成 Prompt、怎么让模型"不知道就说不知道"、怎么给答案挂引用溯源,以及最容易出数据泄露事故的多租户权限过滤——A 公司员工绝不能搜出 B 公司的文档。
如果这篇帮你少踩了坑,点赞 + 收藏就是最大的鼓励。评论区聊聊你们知识库的量级(几百份还是几万份文档?),我按评论区的规模决定下一篇选型讲多深。
系列导航:(一)别在业务代码里裸调大模型 → (二)RAG 文档入库(本篇)→ (三)检索增强与多租户权限
每周四更新,关注不迷路。
运行环境:JDK 17 + Spring Boot 3.3 + Spring AI 1.0.x + Redis Stack,代码实测可跑。