13 · Java 生态落地:LangChain4j 与 Spring AI

本章目标:把前 12 章的 Python 原型,翻译成你能直接搬进 Maven 工程的 Java 代码。概念你已经全部会了,这一章只是"换一门语言"。

本章你将学到

  1. LangChain4j 与 Spring AI 的选型对比,以及它们和 LangChain 的概念映射;
  2. 用 LangChain4j 实现"知识库 RAG + 工具调用"的完整客服助手;
  3. 用 Spring AI 实现 RAG 与 Function Calling 的开箱即用方案;
  4. 国产模型接入、流式、向量库选型的落地坑。

前置知识


13.1 为什么 Java 工程值得用 Java 生态

  • 团队:全员 Java,维护成本最低;
  • 基础设施:现有的配置中心、监控、部署链路都能复用;
  • 数据与交易:工具调用要打内部业务接口(订单、航班),Java 服务直连最顺;
  • 不需要额外部署 Python 服务:简单场景省一个服务。

边界(诚实说):复杂编排(Multi-Agent、复杂人工介入、图式控制流)在 Python/LangGraph 生态更成熟;Java 生态适合"单模型 + 工具 + RAG + 对话记忆"这类 80% 的业务场景。复杂到 Python 都吃力时,Java 也不该硬扛。

13.1.1 两个库怎么选

维度LangChain4jSpring AI
出身开源社区(对应 LangChain 思想)Spring 官方团队
集成轻量、API 直观与 Spring Boot / Autoconfigure 深度绑定
适合想要"即插即用、少样板"的组件库已经用 Spring Boot 的团队,要声明式接入
生态模型/向量库适配较全官方适配,跟随 Spring 版本节奏
一句话"Java 界的 LangChain""Spring 官方的 AI 支持"

如果你团队已重度使用 Spring Boot,优先 Spring AI(注册 Bean 即用);否则 LangChain4j 更灵活。下文两个都演示,你按团队现状挑一个。

13.2 概念映射:你已经在 Python 学过了

Python 概念(前 12 章)LangChain4jSpring AI
ChatOpenAIOpenAiChatModelChatModel(Bean 自动注入)
Prompt 模板PromptTemplate / 接口方法 + @UserMessagePrompt / ChatClient.prompt()
@tool + bind_tools普通方法 + @Tool,AiServices 自动装配@Bean + @Description + Function
RAG:RetrieverEmbeddingStoreContentRetrieverQuestionAnswerAdvisor
记忆:messages / CheckpointerChatMemory(MessageWindowChatMemory)ChatMemory + MessageChatMemoryAdvisor
LCEL 链AiServices 接口编排ChatClient + Advisor

13.3 LangChain4j:完整 RAG 示例

13.3.1 Maven 依赖

<dependencies>
    <!-- LangChain4j 核心 + OpenAI 兼容适配 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai</artifactId>
        <version>1.0.0</version>
    </dependency>
    <!-- 本地 Embedding 模型(离线、免费,中文够用) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
        <version>1.0.0</version>
    </dependency>
    <!-- 可选:PDF/Word 文档加载 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

版本以官方最新为准;本示例按 1.0.x 写作。

13.3.2 RAG 问答代码

package com.lantian.rag;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.DocumentSplitters;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
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;

import java.nio.file.Paths;
import java.time.Duration;

public class RagExample {

    /** 客服助手接口:方法签名就是"提示词契约" */
    interface FlightAssistant {
        String chat(String userMessage);
    }

    public static void main(String[] args) {
        // 1. 聊天模型:兼容国产端点(DeepSeek/Qwen/GLM,改 baseUrl 与模型名即可)
        ChatLanguageModel chatModel = OpenAiChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .baseUrl(System.getenv("OPENAI_BASE_URL"))   // 如 https://api.deepseek.com/v1
                .modelName(System.getenv("MODEL_NAME", "gpt-4o-mini"))
                .timeout(Duration.ofSeconds(60))
                .build();

        // 2. Embedding 模型(本地离线,注意:入库与检索必须用同一个)
        EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();

        // 3. 加载文档 → 切分 → 向量化 → 入库(对应第 5 章的步骤)
        Document document = FileSystemDocumentLoader.loadDocument(
                Paths.get("data/rules.txt"), new TextDocumentParser());
        DocumentSplitter splitter = DocumentSplitters.recursive(300, 50); // chunk_size, overlap
        var segments = splitter.split(document);

        EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
        store.addAll(embeddingModel.embedAll(segments).content(), segments);

        // 4. 检索器(对应 Python 的 as_retriever)
        ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
                .embeddingStore(store)
                .embeddingModel(embeddingModel)
                .maxResults(3)             // 相当于 k=3
                .build();

        // 5. 用 AiServices 装配:模型 + 检索 + 记忆(LangChain4j 的"LCEL")
        FlightAssistant assistant = AiServices.builder(FlightAssistant.class)
                .chatLanguageModel(chatModel)
                .contentRetriever(retriever)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .build();

        // 6. 问答(模型会先检索再回答,自动拼上下文)
        System.out.println(assistant.chat("航班起飞前4小时内退票手续费多少?"));
    }
}

和第 5 章、第 8 章对照:

  • FileSystemDocumentLoader = 文档加载;DocumentSplitters.recursive = 切分;
  • EmbeddingStore = 向量库;EmbeddingStoreContentRetriever = 检索器;
  • AiServices.builder(...) = 把模型、检索、记忆"接成一条链"——这就是 LCEL 在 Java 里的等价物;
  • 记忆 MessageWindowChatMemory = 短期记忆(第 11.2.1 节)。

13.4 LangChain4j:带工具的客服助手

LangChain4j 的 @Tool 更贴近 Java 习惯:普通类的方法加注解,AiServices 自动把工具 schema 交给模型并在模型请求时调用。

package com.lantian.agent;

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;

import java.time.Duration;
import java.util.Map;

/** 工具类:方法 + @Tool 注解,就是 Python 里的 @tool */
public class FlightTools {

    @Tool("查询航班实时状态")
    public String getFlightStatus(@P("航班号,如 CA1501") String flightNo) {
        Map<String, String> status = Map.of(
                "CA1501", "已起飞,预计 11:35 抵达",
                "MU5102", "延误约 40 分钟");
        return status.getOrDefault(flightNo.toUpperCase(), "未查询到航班 " + flightNo);
    }

    @Tool("查询经济舱票价")
    public String getFlightPrice(@P("航班号") String flightNo,
                                 @P("出行日期,YYYY-MM-DD") String travelDate) {
        return flightNo + " 在 " + travelDate + " 的经济舱票价约 780 元";
    }
}

/** 客服助手接口:AiServices 会生成实现,自动完成"决策-调工具-回填-再回答"循环 */
interface CustomerService {
    String chat(@UserMessage String userMessage);
}

public class AgentExample {
    public static void main(String[] args) {
        ChatLanguageModel chatModel = OpenAiChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .baseUrl(System.getenv("OPENAI_BASE_URL"))
                .modelName(System.getenv("MODEL_NAME", "gpt-4o-mini"))
                .timeout(Duration.ofSeconds(60))
                .build();

        CustomerService assistant = AiServices.builder(CustomerService.class)
                .chatLanguageModel(chatModel)
                .tools(new FlightTools())                       // 注册工具
                .chatMemory(MessageWindowChatMemory.withMaxMessages(20))  // 短期记忆
                .build();

        // 连续两轮:第二轮不用重复报航班号(记忆生效)
        System.out.println(assistant.chat("帮我查 CA1501 现在的状态"));
        System.out.println(assistant.chat("它的票价呢?"));
    }
}

要点:

  • @Tool + @P 注解的参数描述会自动生成工具 schema(等价于 Python 的 @tool 函数签名);
  • 执行权在你手里:模型只输出"想调哪个工具、传什么参数",AiServices 会调用你的 FlightTools 方法——查订单/改状态这类操作的安全边界就在这些方法里;
  • 多轮对话由 MessageWindowChatMemory 自动维护(等价于第 9 章的 Checkpointer 短期记忆)。

13.5 Spring AI:RAG 开箱即用

如果团队在用 Spring Boot,Spring AI 的 RAG 一条 Advisor 就搞定。

13.5.1 Maven 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    <version>1.0.0</version>
</dependency>

application.yml(兼容国产端点):

spring:
  ai:
    openai:
      base-url: ${OPENAI_BASE_URL:https://api.deepseek.com/v1}
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: ${MODEL_NAME:deepseek-chat}
          temperature: 0

13.5.2 RAG 配置:向量库 + Advisor

package com.lantian.aimall;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RagConfig {

    /** 关键 Bean:把检索器挂到所有 ChatClient 上 —— RAG 开箱即用 */
    @Bean
    public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
        return new QuestionAnswerAdvisor(
                vectorStore,
                SearchRequest.builder().topK(3).build());
    }

    /** 带 RAG 的 ChatClient:先检索知识库,再把结果拼进上下文回答 */
    @Bean
    public ChatClient ragChatClient(ChatClient.Builder builder, QuestionAnswerAdvisor advisor) {
        return builder.defaultAdvisors(advisor).build();
    }
}

13.5.3 使用

package com.lantian.aimall;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient ragChatClient) {
        this.chatClient = ragChatClient;
    }

    @PostMapping("/chat")
    public String chat(@RequestBody String question) {
        // 检索器自动生效:先查向量库,再回答(对应 Python 的 RAG chain)
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

知识库如何灌进去?写一个启动时初始化 Bean:把文档切分 → Embedding → 写入 VectorStore(PgVectorStore 表 / SimpleVectorStore 内存),逻辑与 13.3 相同,只是换用 Spring AI 的 Document 与 TokenTextSplitter。

13.6 Spring AI:Function Calling

package com.lantian.aimall;

import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.context.annotation.Configuration;

import java.util.function.Function;

@Configuration
public class ToolConfig {

    /** 输入 POJO:模型按这个 schema 填参数 */
    public record FlightQuery(String flightNo) {}
    public record FlightStatus(String state) {}

    /** 工具定义:@Description 会进入模型看到的 schema */
    @Bean
    @Description("查询航班实时状态")
    public Function<FlightQuery, FlightStatus> getFlightStatus() {
        return (query) -> {
            // 这里调用你的内部接口/服务(安全边界)
            String s = "CA1501".equalsIgnoreCase(query.flightNo()) ? "已起飞" : "延误";
            return new FlightStatus(s);
        };
    }
}
// 使用:把 FunctionCallback 注册进 ChatClient,模型即可自动调用
@Bean
public ChatClient toolChatClient(ChatClient.Builder builder,
                                 FunctionCallback getFlightStatus) {
    return builder.defaultFunctions("getFlightStatus").build();
}

Spring AI 的 Function Calling 相当于 Python 的 bind_tools + 工具注册:模型"看到"工具 schema 并决定调用,你的 Function 实现真正执行。参数 POJO(record)就是 schema 的来源。

13.7 落地必踩的坑

坑表现对策
国产模型端点差异有的端点不支持 response_format / 部分 embedding 模型baseUrl 与模型名以各家官方文档为准;先写一个"冒烟测试"验证 chat / embedding 都能通
Embedding 模型不一致入库用 A 模型、检索用 B 模型,相似度全乱全链路固定同一个 Embedding 模型(Python 第 5 章也强调过)
流式不流式体感太慢LangChain4j stream()、Spring AI .stream() 返回 Flux<String>,SSE 推给前端
向量库选型上了分布式向量库运维成本高Java 团队优先 pgvector(直接用现有 PG);数据量 > 千万级再考虑 Milvus/ES
会话状态多副本下记忆串了thread_id/会话与存储绑定(第 12.5.3 节);Spring AI 用 ChatMemory + 会话维度隔离
并发大量并发导致 LLM 限流给 LLM 调用做限流/重试(第 12.4 节),Spring AI 可配 RetryTemplate

13.8 选型建议与结论

  • 概念已经 100% 通用:你在 Python 学的 RAG 切分、检索、工具循环、记忆、评估,在 Java 里一一对应,只是 API 名字不同;
  • 简单场景(单模型 + RAG + 工具 + 记忆):用 Spring AI(已在 Spring Boot)或 LangChain4j,一个服务搞定;
  • 复杂编排(Multi-Agent、人工审批图、复杂控制流):仍是 Python/LangGraph 更成熟,Java 侧通过 HTTP 调 Agent 服务(第 12.5.1 节方案 A);
  • 混排也常见:检索在 Java 服务(贴近数据),复杂 Agent 在 Python 服务(贴近模型),中间用消息队列/HTTP 连接。

一句话:Python 教会你原理,Java 帮你上线。两手都会,你才是团队里那个"能落地 AI 的 Java 工程师"。

练习

  1. 把 13.3 的 RAG 示例改成读你团队的一份真实文档(如《业务流程与数据流转说明.md》),跑通并问一个业务问题。
  2. 给 13.4 的 FlightTools 加一个 calcFee 工具(算退票手续费),让模型完成"查价格→算手续费"的连续工具调用。
  3. 把 13.5 的 QuestionAnswerAdvisor 的 topK 改成 1 和 5,各问一次,对比回答完整性。
  4. 调研你们现有数据库:如果上 pgvector,哪个库、哪张表最合适存知识库向量?

参考答案

<details>
<summary>练习1(用真实文档跑通 RAG)</summary>

把 13.3 的 data/rules.txt 换成团队真实文档,三步走:

  1. 拷文档:把《业务流程与数据流转说明.md》复制到 data/ 下(编码务必 UTF-8,GBK 会被解析成乱码);
  2. 换加载器:Markdown/纯文本用 TextDocumentParser 即可;若是 PDF 换 ApachePdfBoxDocumentParser(依赖已加),Word 需另加 docx 解析器(langchain4j-document-parser-apache-poi);
  3. 问业务问题:assistant.chat("订单数据从哪个系统流转到哪个系统?"),验证:回答是否基于文档(而非模型臆造)、是否带检索痕迹(切分不合理时答非所问)。

常见问题排查:切分粒度不对(.recursive(300, 50) 对表格密集的文档要把 chunk 调小或按段落切);Encoding 乱码(Paths.get 读文件时确认 UTF-8);Embedding 模型与检索不一致(统一用同一个 AllMiniLmL6V2EmbeddingModel)。
</details>

<details>
<summary>练习2(FlightTools 加 calcFee,连续工具调用)</summary>

在 FlightTools 里加工具:

@Tool("计算退票手续费")
public String calcFee(@P("票价(元)") double fare,
                      @P("手续费比例,0~1,如 0.4 表示 40%") double ratio) {
    double fee = fare * ratio;
    return String.format("退票手续费 %.0f 元(票价 %.0f × %.0f%%)", fee, fare, ratio * 100);
}

问"CA1501 票价 780,起飞前 4 小时退票手续费多少"时,AiServices 生成的实现会自动完成连续调用:

  1. 先调 getFlightPrice("CA1501", "...")(若票价没给,模型会先查);
  2. 再调 calcFee(780, 0.4);
  3. 汇总两轮结果输出最终答案。

验证点:连续调用是自动完成的(你不用写循环,AiServices 内部就是这个循环——对照第 2 章手写版);两轮之间对话记忆自动保持。若模型算出的 ratio 不对,说明比例不该由模型"猜"——把分档比例表做成 getRefundPolicy(flightNo) 工具返回,让模型查了再算(与第 8 章练习 1 同理)。
</details>

<details>
<summary>练习3(topK 1 vs 5 对比)</summary>

把 13.5.2 的 SearchRequest.builder().topK(3) 改成 1 和 5,各问同一批问题,对比:

topK现象
1回答可能不完整:只命中一块,政策条目的"例外条款/分档表"常被切在不同块,答一半;偶尔答不出来
3(基准)完整度、噪音平衡
5回答更全,但无关内容增多,模型可能被带偏(引用到不相干段落),token 也涨

观察重点:不是"哪个分高用哪个",而是"回答完整率"——问 10 个问题,数"答对/答全/答偏"。结论通常落在 topK=3 附近(文档块小、规则独立时 2-3 足够;长段落合并成块的文档可能需要 5)。配合 Python 第 5 章练习 2,两边的调参逻辑完全一致。
</details>

<details>
<summary>练习4(pgvector 落库调研)</summary>

调研四件事,对着你的数据库逐项回答:

  1. 哪个库:知识库是"读多写少、量可控"的数据,优先放已有 PG 实例(不用新引存储);若现有 PG 太忙/版本低于 13(pgvector 要求 PG >= 13),单独建一个轻量 PG 实例;
  2. 哪张表:新建专用表 knowledge_chunks,不要复用业务表——字段设计参考:

    CREATE TABLE knowledge_chunks (
    id          bigserial PRIMARY KEY,
    doc_id      text NOT NULL,              -- 稳定文档标识(增量更新用)
    chunk_seq   int  NOT NULL,              -- 块序号
    tenant_id   text,                       -- 权限过滤用(第 6 章)
    content     text,
    metadata    jsonb,                      -- source/category/updated_at
    embedding   vector(768),                -- 维度必须与你的 Embedding 模型一致
    created_at  timestamptz DEFAULT now()
    );
    CREATE INDEX ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);
  3. 索引类型:HNSW(快、占用内存)vs IVFFlat(构建快、精度略降),数据 < 百万级 HNSW 无脑选;
  4. 运维成本:pgvector 只是 PG 的扩展(CREATE EXTENSION vector),不需要额外部署组件——这就是 Java 团队推荐它第一的原因。

红线:embedding 维度要和你用的 Embedding 模型对齐(AllMiniLmL6V2 是 384 维、bge-m3 是 1024 维),建表前先确认,维度不匹配插入就报错。

</details>

延伸阅读

  • LangChain4j 官方文档(dev.langchain4j,以最新版本为准)
  • Spring AI 官方文档(org.springframework.ai,以最新版本为准)
  • 第 12 章:评估、安全、成本(Java 同样适用)
  • 第 14 章:端到端系统(含 Java 侧如何调用 Python Agent 服务)