1. 为什么 Java 工程师转大模型开发第一步不该是背 Prompt我身边不少做 Java 后端的朋友最近都在琢磨转大模型开发。大家的路径出奇一致先看几篇 Prompt 教程再找个 LangChain 的 Python Demo 跑一跑然后发现——好像跟自己的技术栈没什么关系。Spring Boot 里那套依赖注入、事务、熔断、链路追踪到了大模型应用里突然不知道往哪放。问题出在定位上。大模型应用开发不是让你去训练模型也不是让你去写 Prompt 玄学而是把「非确定性的模型调用」当成一个外部依赖用你熟悉的工程手段把它管起来。这恰恰是 Java 工程师最擅长的事。你写过 Feign 的超时重试写过 Redis 的缓存穿透防护写过 Sentinel 的熔断降级这些能力在 RAG 场景里一个都不浪费。所以这篇不聊虚的直接给一个可交付的最小闭环用 Spring AI 和 LangChain4j 搭一个 RAG 骨架把检索、拼 Prompt、调模型、返回答案这条链路跑通并且通过 TaoToken 的统一 Key/API 通道接入模型避免在多个厂商的 Key 和 Endpoint 之间来回切换。目标很明确——你跟着做完手里有一个能启动、能提问、能返回带引用来源的问答服务而不是一个只能截图发朋友圈的 Demo。适合谁看有 Spring Boot 基础、写过 REST 接口、知道什么是 Maven 依赖和 application.yml 的 Java 工程师。不需要你懂向量数据库原理也不需要你调过模型参数。下面每一步都有可复制的配置和命令。2. TaoToken 前置统一 Key 与 API 通道怎么接在动手写代码之前先把模型通道这件事定下来。RAG 骨架里最容易被忽略、又最容易在后期返工的就是模型接入层。如果你一开始把某家厂商的 SDK 硬编码进 Service后面想换模型或者加一个备用通道就得改一堆代码。TaoToken 在这里的角色是一个统一的 API 通道你拿一个 Key通过一个兼容 OpenAI 协议的 Endpoint 去调用不同模型。对 Spring AI 和 LangChain4j 来说它们本来就支持 OpenAI 兼容的接口所以接入成本很低。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意这个 API 地址后面不加 UTM 参数直接用于代码里的 base-url。你需要提前准备两样东西一个 API Key以及确认你要用的模型名称。Key 在控制台的 API Keys 页面创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建之后复制出来后面配置里会用到。模型名称建议先用一个通用的对话模型跑通链路等骨架稳定了再换更强的模型做生成。注意Key 不要写死在代码里也不要提交到 Git。下面配置里我会用环境变量占位本地开发用 IDE 的运行配置注入线上用配置中心或容器环境变量。如果你对模型能力还没把握可以先去模型对话页面手动试几条问题感受一下响应格式和延迟地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。这一步不是必须的但能帮你在写代码前对返回结构有个预期。3. 可复制配置依赖、目录结构与 settings这一节是整篇的核心给的是能直接抄的骨架。我按 Maven 项目来写Gradle 用户把依赖换成对应写法即可。3.1 Maven 依赖Spring AI 和 LangChain4j 可以共存但为了避免版本冲突建议在骨架阶段二选一作为主链路。我的做法是用 Spring AI 做 ChatClient 和 Embedding 的抽象用 LangChain4j 的文档分割和向量存储工具做补充。下面这份依赖是实测能跑通的组合。properties java.version17/java.version spring-ai.version1.0.0-M6/spring-ai.version langchain4j.version0.35.0/langchain4j.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-all-minilm-l6-v2/artifactId version${langchain4j.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency /dependencies这里 Embedding 我用的是 LangChain4j 自带的本地 MiniLM 模型好处是不用额外调 Embedding API省一层网络开销和费用适合骨架阶段。等你要上生产再换成远程 Embedding 服务。3.2 目录结构src/main/java/com/example/rag ├── RagApplication.java ├── config │ └── AiConfig.java ├── controller │ └── QaController.java ├── service │ ├── IngestionService.java │ └── RagQaService.java └── store └── InMemoryVectorStore.java src/main/resources ├── application.yml └── docs └── handbook.mddocs目录放你要检索的原始文档骨架阶段用一个 Markdown 文件就够。InMemoryVectorStore是我自己写的一个简单内存向量存储避免引入 Redis 或 PGVector 增加启动成本。3.3 application.yml 配置server: port: 8080 spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2 embedding: enabled: false rag: chunk-size: 500 chunk-overlap: 80 top-k: 3几个关键点说明一下。base-url指向 TaoToken 的 API 地址api-key从环境变量读。temperature设成 0.2是因为 RAG 场景要的是事实一致性不是创意。embedding.enabled设为 false因为我们用 LangChain4j 的本地 Embedding不走远程。top-k是检索返回的片段数骨架阶段 3 就够太多会撑爆上下文。3.4 核心配置类Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个严谨的知识库助手只根据提供的上下文回答无法回答时明确说不知道。) .build(); } Bean public EmbeddingModel embeddingModel() { return new AllMiniLmL6V2EmbeddingModel(); } }defaultSystem里那句约束很重要它决定了模型会不会在检索不到内容时胡编。骨架阶段先把这条底线立住。4. 验证请求一次可跑的检索问答调用配置写完接下来把链路串起来。分两步先把文档灌进向量库再写问答接口。4.1 文档切分与入库Service public class IngestionService { private final EmbeddingModel embeddingModel; private final InMemoryVectorStore vectorStore; public IngestionService(EmbeddingModel embeddingModel, InMemoryVectorStore vectorStore) { this.embeddingModel embeddingModel; this.vectorStore vectorStore; } PostConstruct public void ingest() throws IOException { String raw Files.readString(Path.of(src/main/resources/docs/handbook.md)); DocumentSplitter splitter DocumentSplitters.recursive(500, 80); ListTextSegment segments splitter.split(Document.from(raw)); for (TextSegment segment : segments) { Embedding embedding embeddingModel.embed(segment.text()).content(); vectorStore.add(segment.text(), embedding.vector()); } System.out.println(已入库片段数: segments.size()); } }DocumentSplitters.recursive(500, 80)表示每段最多 500 字符相邻段重叠 80 字符。重叠是为了避免一句话被切断后语义丢失。启动时PostConstruct自动执行控制台会打印入库片段数这是第一个可验证的信号。4.2 检索加生成Service public class RagQaService { private final ChatClient chatClient; private final EmbeddingModel embeddingModel; private final InMemoryVectorStore vectorStore; public RagQaService(ChatClient chatClient, EmbeddingModel embeddingModel, InMemoryVectorStore vectorStore) { this.chatClient chatClient; this.embeddingModel embeddingModel; this.vectorStore vectorStore; } public String answer(String question) { Embedding queryEmbedding embeddingModel.embed(question).content(); ListString contexts vectorStore.search(queryEmbedding.vector(), 3); String contextBlock String.join(\n---\n, contexts); String prompt 请根据以下上下文回答问题。如果上下文没有相关信息直接回答“知识库中未找到相关内容”。 上下文 %s 问题%s .formatted(contextBlock, question); return chatClient.prompt().user(prompt).call().content(); } }vectorStore.search返回的是余弦相似度最高的 3 个片段。拼进 Prompt 时用---分隔方便模型区分不同来源。最后那句兜底指令和defaultSystem形成双重约束。4.3 Controller 与验证RestController RequestMapping(/api/qa) public class QaController { private final RagQaService ragQaService; public QaController(RagQaService ragQaService) { this.ragQaService ragQaService; } PostMapping public MapString, String ask(RequestBody MapString, String body) { String answer ragQaService.answer(body.get(question)); return Map.of(answer, answer); } }启动项目后用 curl 验证curl -X POST http://localhost:8080/api/qa \ -H Content-Type: application/json \ -d {question:手册里提到的部署流程是什么}成功的话你会看到类似这样的返回{answer:根据上下文部署流程分为三步先构建镜像再推送仓库最后滚动更新。}如果问一个手册里没有的问题应该返回「知识库中未找到相关内容」。这两个结果都出现说明检索和生成链路都通了。5. 本篇常见错排查清单骨架跑起来之后最容易卡住的地方我列一下都是实测踩过的。启动报 401 或 403先检查TAOTOKEN_API_KEY环境变量有没有真正注入到 IDE 的运行配置里。很多人是在系统环境变量里设了但 IDE 启动时没继承。最直接的办法是在AiConfig里临时打印一下System.getenv(TAOTOKEN_API_KEY)的前几位确认非空。返回内容跟文档无关大概率是 Embedding 和检索没对上。检查IngestionService里的入库片段数是不是 0如果是 0说明文档路径不对或者切分器没读到内容。另外确认top-k不要设成 1太小容易漏掉相关片段。中文检索效果差MiniLM 是英文为主的模型中文语义匹配会偏弱。骨架阶段可以接受如果要提升把 Embedding 换成支持中文的远程模型在application.yml里把embedding.enabled打开并配置对应模型。响应特别慢先看是检索慢还是生成慢。在RagQaService里给embed和chatClient.call()分别打时间戳。如果是生成慢把model换成更小的模型如果是检索慢检查向量库是不是每次请求都重新加载。Prompt 太长报 context 超限top-k调小或者把chunk-size从 500 降到 300。上下文不是越多越好无关片段反而会干扰模型。提示排错时优先看 Actuator 的/actuator/health和日志里的异常栈不要靠猜。大模型应用的错误往往藏在网络层和序列化层不在业务代码里。6. 从骨架到可交付下一步怎么走骨架跑通只是起点。真正要交付还得补三块可观测、可回滚、可降级。可观测就是在检索和生成两步埋点记录耗时和 Token 消耗可回滚是把 Prompt 模板从代码里抽出来放到配置或数据库里改错了能一键切回可降级是当模型通道抖动时返回预置的 FAQ 而不是让请求一直挂着。如果你打算把这条链路长期用下去尤其是做编码助手或者 Agent 类的应用建议了解一下 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它在长任务和批量调用上的成本结构会更友好。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有针对 Spring AI 和 LangChain4j 的对接说明遇到协议细节可以直接查。最后说一句实在的Java 转大模型开发难的不是学新框架而是接受「同样的输入可能得到不同的输出」这件事。一旦你把它当成一个需要治理的外部依赖而不是一个需要崇拜的黑盒你过去写的那些熔断、重试、缓存、监控代码全都能用上。骨架已经在你手里了接下来就是把它跑起来然后一点点加护栏。