1. 从手写大模型接口说起Java 开发为什么总在这件事上吃亏先说说我自己吧。之前接手一个知识库问答系统技术栈是 Java 17 Spring Boot需要对接大模型能力。团队里大多数人跟我一样第一个想法就是直接调 HTTP 接口呗。当时我花了两三天时间封装了通义千问的 HTTP 调用——拼接 URL、构建 JSON 请求体、解析 JSON 响应还要专门处理鉴权签名做流式响应的时候更麻烦得手动处理 SSEServer-Sent Events流的读和解析一边维护连接状态一边逐行处理返回内容。项目上线之后团队里任何一个人想加一条新重调整逻辑都要对着这些手写代码愁半天。这不是个例。我在身边和社区里看过太多 Java 工程师做 AI 应用时的套路拿到大模型 SDK 或者 HTTP API然后开始写耦合了大量业务逻辑的调用代码Prompt 拼接、上下文管理、文档拆分、向量检索全靠手撸代码越来越重维护越来越吃力。这里我想先点明一个核心问题Java 生态并不是没有对应这个大模型场景的成熟工具而是很多开发习惯了直接用最原始的 HTTP 方式没有意识到其实有个专门为 Java 打造的框架——LangChain4j 已经非常成熟了。简单介绍一下 LangChain4j。熟悉 Python 生态的人都知道 LangChain它把“调用大模型”和“构建 AI 应用”这件事抽象得很舒服。Java 这边长期以来都比较缺一个同等水平的抽象层LangChain4j 就是来补这个缺口的。它支持对接非常多的模型提供商通义千问就是其中之一它内置了一整套 RAG检索增强生成组件比如文档加载、文本切分、嵌入向量、向量存储、检索器、提示词模板、流式调用、AI 服务接口。换句话说我们之前手动干的事情这个框架基本一网打尽了。所以这篇文章我完整地记录一次基于 LangChain4j 通义千问实现 RAG 流式问答的全过程。会给出可以直接抄走的代码也会解释每一步为什么要这么做。适合已经会用 Java 做普通项目、想尝试或者正在尝试接入大模型的读者也适合做后端应用、Web 应用的开发朋友。不管你是第一次接触大模型应用开发还是已经有了一些手写 API 的老经验这篇文章都能让你省掉不少弯路。2. 方案选型为什么是 LangChain4j、通义千问和流式问答我其实接触过各种组合有一段时间用 Python 写 AI 服务然后在 Java 项目里通过 HTTP 调用那个 AI 服务但最终放弃了原因很现实两边维护成本高类型系统断裂调试困难生产环境排错费劲。后来我下决心在 Java 里直接实现整条链路才认真评估了当时的几个方案用原生 HTTP 封装、用 Spring 的 RestTemplate/WebClient 直接对接、或者用 LangChain4j。原生 HTTP 封装看起来最透明但实际开发中痛点很多。比如通义千问的响应结构里有好几种 role、内容和工具的字段结构需要自己写一堆实体类流式和非流式响应结构还不一样一套解析代码要写两遍上下文管理、Token 统计这些都得自己维护。写一次可以忍长期迭代确实很耗精力。用 RestTemplate/WebClient 直接对接稍微好一点至少 HTTP 层的代码可以托管但本质上还是手工造轮子。尤其是 Prompt 拼接和上下文维护如果没有任何框架约束每个人都会写出自己的一套项目一多就乱。LangChain4j 的定位就是把这些通用逻辑抽象成标准组件。它不需要你理解 LangChain 是什么你完全可以把它当作一个“Java 大模型开发工具包”来用。它有清晰的接口抽象也有默认实现不绑定某一家模型厂商切换后端只需要改配置和依赖。再说通义千问选择它有很实际的原因。首先是国内网络环境下调用非常稳定日活、并发、限流政策都相对友好。其次是它提供兼容 OpenAI 协议的接口也提供 DashScope 原生 SDK这意味着在 LangChain4j 体系里可以通过标准配置直接接入。还有一个很重要的点是它有 embedding 模型可用这个是 RAG 的核心依赖之一不需要再额外找一套专门做向量化的服务。最后说流式问答。我觉得在现在的 AI 应用里流式输出不是加分项是默认配置。想象一下用户在一个知识库里查询信息系统要思考、要检索、要生成答案每一个环节花费的时间加起来可能要好几秒。如果做非流式接口用户就要面对一个一直转圈的加载图标体验很差而流式输出可以在大模型生成第一个字的时候就把内容推给前端后面的内容逐字展示用户感知到的是“正在回答”参与感和可用性都完全不一样。除此之外流式还有个隐藏优势对于长回答可以大大降低首响应耗时这一点在超时配置比较严格的生产环境里价值很大。综合来看这个方案选的不是“最强”而是“最适合 Java 工程团队长期维护”组件化落地、国内链路稳、流式体验好、有现成的 RAG 组件可以扩展。3. 环境准备与项目初始化Maven 依赖、配置文件和基础封装这里我会给出一个完整的可运行配置大家可以直接基于这个开始。先说明一个事情不同版本的依赖库更新很快下面用的是我当时实际验证过的版本建议你以 Maven 仓库中对应时间为准但整体用法是一致的。3.1 引入 LangChain4j 核心依赖我用的是 Maven 管理项目。在 pom.xml 里加入下面这些依赖properties langchain4j.version0.36.2/langchain4j.version /properties dependencies !-- LangChain4j 核心模块 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- 通义千问集成模块 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-dashscope/artifactId version${langchain4j.version}/version /dependency !-- 简单的 Embedding 存储本地内存实现 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-easy-rag/artifactId version${langchain4j.version}/version /dependency !-- 文档解析辅助 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-document-parser-apache-pdfbox/artifactId version${langchain4j.version}/version /dependency !-- 内置 Embedding 模型库 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embedding-bge-small-zh/artifactId version${langchain4j.version}/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里解释一下几个选择。为什么没有直接用 spring-boot-starter 形式因为很多朋友的项目不一定是 Spring Boot可能只是普通的 Java 工程或者 Vert.x、Micronaut 等项目。langchain4j 的核心模块本来就不依赖 Spring保留通用性更稳妥。当然如果你的项目是 Spring Boot可以直接加 langchain4j-spring-boot-starter它能让你用属性前缀自动装配很多 Bean更方便但只要理解 LangChain4j 本身的 API手动构建也非常简单。为什么选择 BGE Small 中文 embedding 模型作为默认方案因为它体积相对小对中文支持不错可以直接在本地 CPU 运行适合开发和中小规模应用。通义千问也提供了 text-embedding-v 系列在线模型不过本地模型在离线环境、开发调试、低成本场景下优势明显所以这里先放本地。3.2 配置文件里的关键参数我习惯把配置集中在 application.properties 或 application.yml 中。用 Spring Boot 的话配置大概是这样langchain4j.dashscope.api-key${DASHSCOPE_API_KEY} # 通义千问对话模型 langchain4j.dashscope.chat-model.model-nameqwen-plus langchain4j.dashscope.chat-model.temperature0.1 langchain4j.dashscope.chat-model.max-tokens2048 # 流式问答 langchain4j.dashscope.streaming-chat-model.model-nameqwen-plus langchain4j.dashscope.streaming-chat-model.temperature0.2 # 本地 Embedding langchain4j.embedding-model.bge-small-zh.enabledtrue如果不是 Spring Boot 自动装配而是手动构建则可以在代码里写一个配置类ChatLanguageModel chatModel DashScopeChatModel.builder() .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(qwen-plus) .temperature(0.1) .build(); StreamingChatLanguageModel streamingChatModel DashScopeStreamingChatModel.builder() .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(qwen-plus) .build(); EmbeddingModel embeddingModel BgeSmallZhEmbeddingModel.builder() .build();关于 API Key 的管理有一个重要的习惯绝对不要把 Key 硬编码在代码里。生产上建议放到环境变量、KMS 或者配置中心里然后代码里只读引用。很多实战项目里出现密钥泄露往往就是因为本地调试随手写死结果提交到了代码仓库。这个小坑真的很容易踩。3.3 手动封装一个 RAG 问答服务类为了后面演示方便我们先定义一个 RAG 问答服务类它负责两件事加载外部文档到向量库接收用户问题返回对话结果。import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.output.Response; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; public class RagAssistant { private final EmbeddingStoreTextSegment embeddingStore; private final EmbeddingModel embeddingModel; private final StreamingChatLanguageModel streamingModel; public RagAssistant() { this.embeddingStore new InMemoryEmbeddingStore(); this.embeddingModel new BgeSmallZhEmbeddingModel(); this.streamingModel DashScopeStreamingChatModel.builder() .apiKey(EnvReader.getApiKey()) .modelName(qwen-plus) .build(); } public void ingest(Document document) { // 切分文档为若干小片段 DocumentSplitter splitter DocumentSplitters.recursive(300, 50); ListTextSegment segments splitter.split(document); // 为每个片段生成向量并存入向量库 ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments); } public void chatWithStream(String message, StreamResponseHandler handler) { // 配置检索器 EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.6) .build(); // 使用 AI 服务接口 Assistant assistant AiServices.builder(Assistant.class) .streamingChatLanguageModel(streamingModel) .contentRetriever(retriever) .build(); assistant.chat(message); } }这里有必要解释一个 LangChain4j 里非常好用的特性AiServices。它允许你通过接口定义 注解的方式来描述 Agent 的能力。比如interface Assistant { String chat(String userMessage); }如果改成流式可以直接用TokenStream chat(String userMessage)。当你在 AiServices 上绑定 contentRetriever 时LangChain4j 会自动把检索到的内容拼进 Prompt然后调用大模型生成答案。你不需要手工去拼接文档字符串也不需要自己维护检索逻辑框架帮你把这些细节隔离掉了。4. RAG 核心链路实现文档加载、文本切分、向量存储与检索器配置RAG 在工程上其实就是一条管道加载文档——切分文本——向量化——存储——检索——拼装 Prompt——生成答案。每一环都有不少需要细节处理的地方这里我拆开来讲。4.1 文档加载不要只想着 PDF还有 Word、Markdown、HTML很多人一想到知识库就是 PDF其实业务系统里 Word、Markdown、PPT、HTML 都有可能出现。LangChain4j 提供了一组 DocumentLoader比如 FileSystemDocumentLoader 可以加载文件系统中的文档UrlDocumentLoader 可以加载线上文档。我实际测试下来PDF 用 pdfbox parser 解析效果不错Word 文档还需要额外加 poi 相关依赖来做解析这里建议按实际文件类型补充 parser 模块。有一个当时我自己没有立刻意识到的问题PDF 本身有两种形态电子版有文字层和扫描版纯图片。text PDF 解析器只能处理前者扫描版必须接入 OCR。如果你做企业知识库扫描件占比通常不低但 LangChain4j 目前内置的 PDF parser 是不带 OCR 的别踩这个坑。先确认你的文档里的 PDF 到底是不是有文字层。文档加载示例Document pdfDocument FileSystemDocumentLoader.loadDocument(new File(rag-test.pdf));一个小细节有的 PDF 转换后会出现大量排版空格、换行错乱文本质量直接影响切分和检索效果。如果发现解析效果不好建议先做文本清洗比如把多余空白字符替换掉把行尾“非断行空格”转成普通空格再做后续处理。就因为这个细节我前几次跑出来的检索命中率一直不稳定。4.2 文本切分长度和重叠度的选择文本切分的目的是把长文档拆成许多小的语义单元每一个单元是一个 TextSegment。切分粒度太粗后续向量化不精确检索容易覆盖太多无关内容切分太细又会破坏上下文连贯性单个片段可能连一句完整的话都撑不起来。LangChain4j 提供了多种切分器我推荐用 DocumentSplitters.recursive(300, 50)。这里两个参数分别是 maxSegmentSize 和 maxOverlapSize。maxSegmentSize 是每个分片大概包含多少个字符注意是字符不是 tokenmaxOverlapSize 是相邻分片之间重叠的字符数。为什么要有重叠因为某句话如果恰好被拦腰截断他的语义主体可能在上一段也可能在下一段。重叠相当于给切分边界加了一圈“缓冲带”能让上下文信息在相邻片段里都保留一部分从而提升检索召回效果。假设你的文本是这样的这是一段关于智能客服系统的介绍。它支持多轮对话、意图识别和知识库检索。切分器在 maxSegmentSize300 时大概率不会去切这种短文本因为长度没到阈值。但对于超长文本它会在某些位置截断。重叠的 50 个字符会尽量让上一段末尾与下一段开头保持语义连续。我测试过几组参数对于大多数业务文档300/50 的组合效果比较均衡技术文档偏向更小的 200/30问答类短文本还可以 200/50。具体得根据自己文档的句子长度去调整不建议一个参数打天下。4.3 向量化与向量存储Embedding 模型决定检索效果的上限很多人容易忽略一个现实切分、存储、检索都只是工程上的处理RAG 效果的上限其实在 Embedding 模型本身。Embedding 模型的本质是把一段文本映射成一组高维向量。向量之间的余弦相似度代表语义相似度。如果 Embedding 模型对中文支持不好那么“苹果手机”和“iPhone”这种同义表达可能就检索不到。BGE Small 中文模型在不少公开评测里表现都可以但如果你要处理的是专业领域术语、代码文本或垂直业务建议用领域微调的 embedding 模型或者在线大模型提供的 embedding 接口。InMemoryEmbeddingStore 是 LangChain4j 内置的本地向量库它把所有向量装在内存里适合 Demo、小规模知识库和验收测试。缺点是内存占用高、没有持久化。一旦服务重启向量数据就丢失了。生产环境建议接 Redis 向量库、Milvus、pgvector 等LangChain4j 对这些都有对应的集成模块。我这里先讲本地实现后面有一小节专门说生产环境怎么演进。4.4 检索器的核心参数maxResults 和 minScore检索器里最关键的两个参数就是 maxResults 和 minScore。maxResults 是最终返回给大模型命中的文本片段数量minScore 是相似度阈值。我遇到一个比较常见的误区有人为了“尽可能多给模型素材”把 maxResults 设成 50。实践下来这并非好事。一方面提供的文本越多消耗的 Token 就越多成本上升另一方面检索结果会有大量噪音模型反而容易被无关内容干扰回答质量下降。我平时知识库取 3-5 条大多数场景都足够。minScore 也一样设太低无关内容混进来设太高遇到相似度普遍不高的模糊查询回答不了问题。这个没有统一标准要先跑一批测试问题统计一下检索结果的 score 分布然后再去定阈值。如果自己没法统计先给个 0.5-0.7 之间的保守值后续再调。5. 流式问答的工程实现从 TokenStream 到前端友好输出前面的链路是纯文本知识库问答的基础但既然标题里强调了流式问答我就重点展开这一节。流式问答的难点并不在于 LangChain4j 怎么提供流式接口而在于如何把流式结果优雅地接进你的业务系统让前端能收到“打字机”效果。5.1 LangChain4j 的 TokenStream 处理流程LangChain4j 中流式模型返回的是 TokenStream你可以对它注册三个阶段的事件回调onPartialResponse每次拿到部分响应时、onCompleteResponse完成时、onError异常时。这是非常典型的事件驱动模式。伪代码如下TokenStream tokenStream streamingChatModel.generate(userMessage, contextDocuments); tokenStream.onPartialResponse(partial - { // 把 partial 发送到前端 }).onCompleteResponse(completion - { // 收尾处理 }).onError(error - { // 异常处理 }).start();千万不要小看这几个回调的设计它把流式输出这件事拆得很清晰。每个 onPartialResponse 可能会被触发很多次每次拿到的 partial 只是当前这次累积响应的增量至少我在实际测试时是递增的。但同时要注意一个问题这个回调返回的是整串当前生成的文本还是本次新生成的增量我在不同版本文档里见到过差异。建议在收到 onPartialResponse 后往前端发送时不要直接覆盖而是把增量做 diff或者干脆让前端做纯增量追加以免在一些模型上出现重复文本。实践下来更好的做法是后端把每次 partial 文本原样推给前端/客户端由前端做字符串拼接。如果你用的是 WebSocket 或 SSE直接把 partial 作为一帧消息推送即可。5.2 结合消息记忆的多轮对话企业知识库问答基本是多次对话一问一答之间需要维持上下文。LangChain4j 的 MessageWindowChatMemory 可以指定记忆窗口大小例如记住最近 20 条消息。示例配置MessageWindowChatMemory chatMemory MessageWindowChatMemory.withMaxMessages(20); Assistant assistant AiServices.builder(Assistant.class) .streamingChatLanguageModel(streamingModel) .chatMemory(chatMemory) .contentRetriever(retriever) .build();动手写过上下文管理后你会很清楚这其实不容易不同用户、不同对话 session 的记忆要隔离Java 服务端多用户同时在线时很容易串线。LangChain4j 这里我用了 AiServices它默认会为每个服务实例保留一个共享 memory单用户没问题多用户场景需要给每个用户建一个独立的 Assistant 实例。当然也可以直接从 ChatMemory 接口实现把消息存储放到 Redis 里持久化 过期策略一起解决了。LangChain4j 提供了持久化 ChatMemoryStore 的接口我建议多用户系统一开始就设计成按用户维度分 Store。5.3 阿里云通义千问的流式细节我这里重点说一下通义千问的流式模式。在 DashScope API 里流式输出是通过 SSE 格式返回的每个事件里面包含一个增量 token。LangChain4j 的 DashScope 模块已经把这层协议封装好了所以你在业务代码里无感知不需要自己读 SSE 流。但有一个地方值得留意enableSearch这个参数也就是通义千问的联网搜索能力。它和 RAG 结合使用时需要注意优先级。我自己实际测试时发现开启联网搜索后模型有时会基于搜索结果回答而不是完全遵循你提供的私有知识库文档。具体业务里需要根据场景来权衡知识库问答一般建议关闭 enableSearch或者至少配置成不在 Prompt 体系里主动触发。另一个点通义千问模型对maxTokens的理解和 OpenAI 不太一样有的平台默认返回长度有上限如果你要生成很长的回答需要把 maxTokens 调大。但在 RAG 场景下我通常不建议把 maxTokens 设得过大而是尽量依靠检索质量来压缩生成内容。响应越长整体时延越高流式体验也会打折扣。6. 生产环境需要升级的四个点向量库、异步线程、安全与成本如果一个 RAG 项目只是本地跑通验证那上面讲的基本够用了。但只要你准备发布到生产环境有四件事一定要提前考虑。6.1 向量存储的持久化与选型InMemoryEmbeddingStore 最大的问题是不持久化。生产上必须换成能持久化的存储。我给大家列举几种常用选项存储方案部署方式特点适合场景Redis 向量检索已有 Redis 集群接入简单实时性好中小规模知识库pgvector已有 PostgreSQL事务与向量共存与业务数据关系密切Milvus独立集群大规模高并发百万级向量知识库OpenSearch独立集群全文向量混合检索需要结合关键词检索每个方案没什么高下之分主要看现有基础设施。我们当时选的是 Redis 方案因为团队已有比较成熟的 Redis 集群接入成本最低。如果你的知识库只有几万篇文档Redis 方案完全可以撑住但要注意内存占用和过期策略是否适合你的业务。6.2 异步化与连接管理大模型接口的响应时延明显高于普通 HTTP API这是大家能感觉得到的。如果业务请求量稍微上来一点直接用同步 Spring MVC 请求线程去等大模型返回Tomcat 默认的 200 个线程可能很快就耗尽。这里有两个优化建议。第一用 Spring 的 WebFlux 或者 Servlet 异步化处理把大模型调用放到独立线程池结合 CompletableFuture 创建异步任务。对于流式场景前端一般走 WebSocket 或 SSE服务端应保持“一连接一长任务”的模型而不是占用业务请求线程等待。第二为 HTTP 连接池设置合理的超时配置。通义千问这类平台有各自的网络要求超时太短很容易把正常的长请求误判为失败超时太长又会让故障恢复得很慢。我平时一般把连接超时设为 10 秒但读取超时要看流式周期一般给到 120 秒以上。注意Java 的 HttpClient 或 OkHttp 默认读取超时设置并不一定适合流式场景需要显式配置。LangChain4j 底层用 OkHttp 或 Java HttpClient具体的超时设置可以在模块配置里实现。这个是我自己踩过的坑之前用默认配置回答长一点就频繁触发 ReadTimeout。6.3 敏感信息过滤与合规知识库里的文档很可能包含机密数据、隐私、内部资料。检索之后大模型生成回答时你必须要确保两点一是不会被随意检索出来泄漏给无权限用户二是在 Prompt 里明确约束模型不得输出与问题无关的敏感数据。技术上可以做用户级权限过滤比如按用户角色过滤检索片段集合或者给不同的知识库分区设置独立向量库。这个 LangChain4j 不直接帮你做但你可以通过自定义 ContentRetriever 实现。建议在生产设计前期就把这部分加进去不要等出事了再补窟窿。6.4 成本控制与 Token 统计在线大模型的费用是持续发生的RAG 里每个问题都会调用一次嵌入模型把用户问题向量化和大模型生成模型。如果还开了多次重试成本会增加。我们可以做一些务实优化一是缓存用户问题完全相同的问句不走完整 RAG 链路二是按文档更新时间做增量向量化避免每次服务重启就全量重新 embedding三是把对话记忆截断保留最后 N 轮超长记忆会消耗大量 Token四是合理控制检索条数之前说的 maxResults 不要盲目调大。我用过的方案里最立竿见影的是“问题规范化 缓存”用户在短时间窗口内的相似问题可以直接命中历史答案成本几乎为零。实现也不复杂用一个 Redis Set 或 SimpleCache 记录最近的问题哈希即可。7. 常见问题速查表流式中断、解析失败、检索不到、依赖冲突这一节直接给结论都是我实际踩过的坑。我把它们整理成表格方便遇到问题时快速对照。问题现象可能原因解决思路流式输出中途停止读取超时、网络抖动、模型异常调大读取超时增加重试策略观察服务端日志PDF 文本解析失败或乱码扫描版 PDF没有文字层换 OCR 方案或改用带文字层的 PDF检索结果始终不相关Embedding 模型能力不足或文本切分不合理换专业领域 Embedding 模型调小切分长度增加重叠回答不来自知识库Prompt 约束不够、enableSearch 被开启调整 Prompt 指令关闭搜索检查检索器是否真正注入调用抛 401/403API Key 错误或失效检查密钥权限、环境变量、是否被泄露拦截依赖 jar 冲突多个 HTTP 客户端版本不一致比对 mvn dependency:tree统一版本内存占用过高InMemoryEmbeddingStore 保存向量过多换持久化向量库设置过期清理多用户上下文串号共享了同一个 ChatMemory按用户/会话维度创建 Assistant 实例再说几个开发时的独门技巧。第一个加一个“检索调试”接口。生产环境你去排查回答错误时最需要确认的是“模型为什么这么回答”。所以我在开发时期会加一个隐藏接口输入用户问题返回当前检索到的所有片段和相似度分数。这样出了问题一眼就能看到底是检索链路问题还是大模型生成问题。这个接口上线前可以加权限保护或直接去掉。第二个把 Prompt 模板单独管理不要散落在代码里。LangChain4j 支持使用资源文件作为 Prompt 模板。我会把系统提示词、用户问题模板、答案格式模板全部放到 resources 目录下用 Message 和 ChatMessage 统一加载。这样做的好处是后期调整提示词不用重新编译也可以让非研发同学协助调优。第三个建议引入测试集对 RAG 效果做回归。比如准备 20-50 条常见问题每次改完检索参数或换模型跑一遍测试集计算命中率、答案内容是否包含关键词。没有这套测试你很难知道自己改动的方向是好是坏。我自己最初没有做结果每次调参都靠感觉后来加上简单测试之后效率高非常多。8. 关于 Java RAG 效果调优的一些私人建议最后再分享一些我在生产环境里积累下来的调优体会可能不系统但每一条都是真金白银的经验。第一个体会是RAG 的效果七成在检索三成在生成。很多朋友遇到回答不准确第一反应就是换更贵的大模型或者疯狂改提示词。但实测下来很多时候问题出在知识库文档质量、切分粒度、Embedding 模型选择上。先把检索这一环调通了再去看生成否则就是本末倒置。第二个体会是不要让 Prompt 头发长、格式复杂、挤占 Model 上下文。我在调优时发现RAG 检索出来的片段本身已经包含了很多信息Prompt 里需要约束模型“只依据给定内容回答不要编造”但不要塞太多无关的约束指令。指令过长反而影响模型的注意力分配。第三个体会是在 Java 里做 AI 应用慢慢要习惯“面向接口编程”式的思维方式。不要把大模型当成一个独行侠而要把它当作你系统里的一个组件像操作数据库、缓存一样去接入。LangChain4j 这种框架的意义就是让你把这种组件化的思维方式固化到代码里。第四个体会是如果团队里没有专门做 AI 的工程师用 LangChain4j 这类框架反而比从零手写更安全。因为框架本身沉淀了很多通用问题的处理方案比如流式、上下文、错误处理底层踩过的坑已经帮你处理了一部分。手写的方案虽然灵活但也很可能把同样的问题踩一遍。写到这我可以肯定地说Java 开发直接对接大模型接口的时代已经卷到没什么必要了。与其重复造轮子不如把时间花在业务梳理、知识库内容质量、检索调优这些真正能拉开差距的事情上。这套 LangChain4j 通义千问 流式问答的方案我从开发到上线跑了不少问题整体稳定值得你直接拿去试。
阅读完成 · 觉得有帮助?