1. 为什么我劝你放弃自研 RAG,先跑通这条最小链路
企业知识库问答这件事,坑不在“大模型会不会答”,而在“文档怎么进、检索怎么准、模型怎么稳、接口怎么统一”。我见过太多团队一上来就自研:先搭向量库,再写文档解析,再调检索权重,再适配三四家模型接口,最后还要做对话记忆和容错。小团队两个月起步,上线后一改需求就牵一发动全身。
更现实的问题是模型侧接入。今天用 A 家的对话模型,明天想换 B 家的重排模型,后天老板说要用某个新出的向量模型,每换一次就要改一遍 SDK、改一遍鉴权、改一遍超时重试。真正吃掉工期的往往不是 RAG 算法,而是这些模型接入的胶水代码。
这篇要讲的是另一条路:用 SpringBoot3 做服务骨架,把 RAG 检索链路和大模型问答能力串起来,模型侧统一走 TaoToken 的 Key/API 通道,一套 Base URL + 一个 Key 就能切换不同模型。你不需要先成为 RAG 专家,先把“上传文档 → 命中答案”这条链路跑通,再逐步加检索优化。
适合谁看:有 Java 后端基础、想给公司内部做知识库问答的开发者;正在评估自研还是接入的团队负责人;以及已经写过 demo 但检索效果不稳定、想补齐工程化配置的人。下面所有配置和代码都可以直接复制,重点是让你今天就能跑出一个能问答的最小系统。
2. TaoToken 前置准备:统一 Key 与模型通道怎么配
在动手写 SpringBoot3 代码之前,先把模型侧通道打通。这一步做对了,后面换模型、加模型都只是改配置。
TaoToken 在这里扮演的角色是“统一模型入口”:你拿到一个 API Key,通过一个兼容 OpenAI 标准的 Base URL 去调用对话、向量、重排等不同模型。对 SpringBoot3 项目来说,好处是 LangChain4j 或任意 OpenAI 兼容客户端只需要配一次地址和 Key,模型 ID 作为参数传入即可。
先到官网注册并进入控制台创建 Key:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 控制台创建 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API Keys 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
创建完 Key 后,先别急着写代码,用一条 curl 验证通道是否通。这一步能帮你排除 90% 的“后面报 401 但不知道哪错了”的问题:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer 你的API_KEY" \ -d '{ "model": "你的对话模型ID", "messages": [ {"role": "user", "content": "用一句话说明什么是RAG"} ] }'如果返回里有choices字段和正常文本,说明 Key 和通道都没问题。注意 API 地址是https://taotoken.net/api,不带任何查询参数,鉴权走标准的Authorization: Bearer。
这里有个容易踩的点:很多人把 Base URL 写成带/v1结尾还是不带搞混。在 OpenAI 兼容客户端里,Base URL 通常填https://taotoken.net/api,客户端自己会拼/v1/chat/completions;如果你直接手写 HTTP 请求,就写完整路径https://taotoken.net/api/v1/chat/completions。两种写法对应两种场景,别混用。
模型 ID 从哪来?在模型对话页可以直接试跑并看到可用模型列表:
- 模型对话体验:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
建议你在这里先把“对话模型”和“向量模型”各试一次,确认这两个模型 ID 都能正常返回,再写进 SpringBoot3 配置。因为 RAG 链路里对话和向量是两类调用,任何一个不通,问答都会失败。
如果你后面要做长期编码或 Agent 类任务,可以了解 Coding Plan:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
前置准备的核心就三件事:拿到 Key、确认 Base URL、确认对话和向量两个模型 ID。这三样齐了,下面进入代码环节。
3. 可复制配置:SpringBoot3 依赖、application.yml 与 RAG 参数
这一节是全文最该抄的部分。我按“能直接跑”的标准给配置,路径和字段名保持一致,你替换 Key 和模型 ID 即可。
先看 Maven 依赖。SpringBoot3 要求 Java 17,LangChain4j 用 1.x 版本,向量库用 PostgreSQL + pgvector:
<properties> <java.version>17</java.version> <spring-boot.version>3.4.0</spring-boot.version> <langchain4j.version>1.0.0</langchain4j.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-pgvector</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-document-parser-apache-tika</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> </dependencies>然后是application.yml。这里把 TaoToken 的 Base URL、Key、对话模型、向量模型、重排模型都集中配置,后面换模型只改这里:
server: port: 8080 spring: datasource: url: jdbc:postgresql://localhost:5432/knowledge username: postgres password: postgres data: redis: host: localhost port: 6379 taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat-model: 你的对话模型ID embedding-model: 你的向量模型ID rerank-model: 你的重排模型ID rag: chunk-size: 500 chunk-overlap: 80 top-k: 5 vector-threshold: 0.7 keyword-threshold: 0.3 rerank-top-n: 3注意api-key用环境变量注入,别硬编码进仓库。启动前设置:
export TAOTOKEN_API_KEY=你的API_KEY接着是 Java 配置类,把 LangChain4j 的对话模型和向量模型 Bean 建出来,统一指向 TaoToken:
@Configuration public class ModelConfig { @Value("${taotoken.base-url}") private String baseUrl; @Value("${taotoken.api-key}") private String apiKey; @Value("${taotoken.chat-model}") private String chatModel; @Value("${taotoken.embedding-model}") private String embeddingModel; @Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(chatModel) .temperature(0.2) .timeout(Duration.ofSeconds(60)) .build(); } @Bean public EmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(embeddingModel) .build(); } }这里三件套齐了:Base URL 是https://taotoken.net/api,Key 走环境变量,Model ID 从配置读。温度设 0.2 是为了知识库问答更稳定,别用默认的高温。
向量库初始化用 pgvector,建表语句:
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS knowledge_embedding ( id UUID PRIMARY KEY, content TEXT, metadata JSONB, embedding VECTOR(1536) ); CREATE INDEX ON knowledge_embedding USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);维度 1536 要和你选的向量模型输出维度一致,不一致会插入失败。这是新手最容易忽略的报错来源。
RAG 参数解释一下:chunk-size500 是每块字符数,chunk-overlap80 是相邻块重叠,避免句子被切断;top-k5 是召回数量;vector-threshold0.7 是向量相似度门槛,低于这个值不采纳;rerank-top-n3 是重排后保留的条数。这些值不是固定的,文档密度大就调小 chunk,问答不准就调高 threshold。
4. 验证请求:从上传文档到命中答案的完整动作
配置写完,跑起来验证。这一节给你完整的接口和验证步骤,确保链路真的通。
先写文档入库服务,用 Tika 解析后分块、向量化、写库:
@Service public class KnowledgeIngestService { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public KnowledgeIngestService(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } public void ingest(InputStream inputStream, String fileName) throws Exception { DocumentParser parser = new ApacheTikaDocumentParser(); Document document = parser.parse(inputStream); DocumentSplitter splitter = DocumentSplitters.recursive(500, 80); List<TextSegment> segments = splitter.split(document); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments); } }上传接口:
@RestController @RequestMapping("/api/knowledge") public class KnowledgeController { private final KnowledgeIngestService ingestService; public KnowledgeController(KnowledgeIngestService ingestService) { this.ingestService = ingestService; } @PostMapping("/upload") public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) { try { ingestService.ingest(file.getInputStream(), file.getOriginalFilename()); return ResponseEntity.ok("入库成功: " + file.getOriginalFilename()); } catch (Exception e) { return ResponseEntity.status(500).body("入库失败: " + e.getMessage()); } } }用 curl 上传一个 PDF:
curl -X POST http://localhost:8080/api/knowledge/upload \ -F "file=@员工手册.pdf"返回“入库成功”后,去数据库确认向量真的写进去了:
SELECT id, LEFT(content, 50) AS preview FROM knowledge_embedding LIMIT 5;如果表里有数据,说明解析、分块、向量化、写库这条链路通了。如果表是空的,看日志里有没有 embedding 调用报错。
然后是问答接口,检索 + 重排 + 生成:
@RestController @RequestMapping("/api/chat") public class ChatController { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; private final ChatLanguageModel chatModel; public ChatController(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore, ChatLanguageModel chatModel) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; this.chatModel = chatModel; } @PostMapping("/ask") public Map<String, Object> ask(@RequestBody Map<String, String> req) { String question = req.get("question"); Embedding queryEmbedding = embeddingModel.embed(question).content(); List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(queryEmbedding, 5, 0.7); String context = matches.stream() .map(m -> m.embedded().text()) .collect(Collectors.joining("\n\n")); String prompt = """ 你是企业知识库助手,只根据下面的资料回答问题。 如果资料里没有答案,直接说“资料中未提及”,不要编造。 资料: %s 问题:%s """.formatted(context, question); String answer = chatModel.generate(prompt); return Map.of( "answer", answer, "sources", matches.stream() .map(m -> m.embedded().metadata().getString("source")) .toList() ); } }验证请求:
curl -X POST http://localhost:8080/api/chat/ask \ -H "Content-Type: application/json" \ -d '{"question":"员工年假有多少天?"}'成功结果长这样:
{ "answer": "根据员工手册,入职满一年的员工每年享有5天年假。", "sources": ["员工手册.pdf"] }看到answer里有基于文档的具体内容、sources能溯源到文件名,这条“上传文档 → 命中答案”的链路就算跑通了。如果answer是“资料中未提及”,说明检索没召回,去调top-k或降低vector-threshold;如果answer是编的,说明 prompt 约束不够,把“不要编造”那句加强。
想更直观地对比不同模型的回答效果,可以在模型对话页手动试同一段 prompt:
- 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来,遇到直接对号入座。
401 Unauthorized。最常见。原因通常是 Key 没注入、Key 写错、或者Authorization头格式不对。检查三处:环境变量TAOTOKEN_API_KEY是否真的 export 了;application.yml里是不是写成了${TAOTOKEN_API_KEY}而不是硬编码空值;curl 里 Bearer 后面有没有多余空格。还有一种情况是 Key 被复制时带了换行,肉眼看不出来,重新复制一次。
local proxy failed / connection refused。这个报错说明请求根本没发出去,通常是 Base URL 写错或本地网络配置问题。确认 Base URL 是https://taotoken.net/api,不要带多余路径。如果你本地配了某些网络工具导致请求被拦截,先关掉再试。注意这里不要用任何非官方的转发地址,直接用官方 API 地址最稳。
reading choices 相关报错。典型表现是Cannot read field "choices"或返回体里没有choices字段。原因一般是模型 ID 写错,或者调用的是向量接口却按对话接口解析。检查taotoken.chat-model和taotoken.embedding-model有没有填反。对话模型返回choices,向量模型返回data,两者结构不同,解析代码不能混用。
OAuth / token 过期类报错。如果你用的是某些需要 OAuth 流程的客户端,报错会提示 token invalid。TaoToken 走的是标准 API Key 鉴权,不需要 OAuth 流程。如果你在某个工具里看到 OAuth 配置项,说明那个工具默认走了别的鉴权方式,改成 API Key 模式即可。Claude Code 这类工具接入时,配置里要写全三件套:Base URL、API Key、Model ID,缺一个都会报鉴权或模型不存在。
向量维度不匹配。报错类似expected 1536 dimensions, not 1024。这是建表时VECTOR(1536)和你实际向量模型输出维度不一致。解决办法:查你选的向量模型输出维度,改表结构,重新入库。改完记得清空旧数据,否则新旧维度混在一起还会报错。
入库成功但问答召回为空。表里有数据,但findRelevant返回空列表。先确认查询用的 embedding 模型和入库时是同一个,不同模型向量空间不通用。再确认vector-threshold是不是设太高,0.7 对某些模型偏严,可以先降到 0.5 测试。最后确认分块大小,chunk 太大导致语义分散,也会召回不准。
排障时建议打开 LangChain4j 的请求日志,把实际发出的 URL、模型 ID、请求体打出来,对照官方文档核对:
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
6. 把模型通道固定下来,RAG 才敢往生产走
跑通最小链路后,下一步不是急着加知识图谱,而是把模型通道固定成可运维的形态。我自己的做法是:所有模型调用都收敛到ModelConfig这一层,业务代码只依赖ChatLanguageModel和EmbeddingModel接口,不直接碰 HTTP。这样换模型、加备用模型、做熔断降级,都只改配置层。
具体来说,给对话模型加一个降级包装:主模型超时或报错时,自动切到备用模型。因为 TaoToken 是统一入口,你只需要在配置里多写一个模型 ID,不用改任何业务代码:
@Bean public ChatLanguageModel chatLanguageModel() { ChatLanguageModel primary = OpenAiChatModel.builder() .baseUrl(baseUrl).apiKey(apiKey) .modelName(primaryModel).timeout(Duration.ofSeconds(30)) .build(); ChatLanguageModel fallback = OpenAiChatModel.builder() .baseUrl(baseUrl).apiKey(apiKey) .modelName(fallbackModel).timeout(Duration.ofSeconds(30)) .build(); return new FallbackChatModel(primary, fallback); }FallbackChatModel自己实现ChatLanguageModel接口,在generate里 try-catch,主模型失败就调备用。这就是“统一 Key 接入”的真正价值:容错逻辑写一次,所有模型通用。
另一个生产必备是链路追踪。每次问答记录:问题、召回条数、重排后条数、模型耗时、最终答案。这些数据攒一周,你就能看出是检索拖后腿还是模型拖后腿。检索慢就优化向量索引,模型慢就换更快的模型 ID,都是配置级操作。
最后提醒一句:知识库问答的准确率,七分靠文档质量,三分靠检索参数。文档本身结构混乱、扫描件没 OCR、表格没解析好,再好的 RAG 也救不回来。先把文档入库质量盯住,再调chunk-size和top-k,顺序别反。
如果你要长期跑编码或 Agent 类任务,Coding Plan 那条通道可以单独了解:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
到这里,SpringBoot3 + RAG + 大模型的企业知识库问答最小系统就跑通了。接下来你要做的,是把公司真实文档丢进去,看哪些问题答不准,然后针对性调参数。这个过程没有捷径,但有了统一模型通道,至少你不用再为换模型改代码了。