09 · Java 双栈:Spring Boot + LangChain4j

对应代码:server-java/** 为什么做双栈:JD 里明确出现 "基于 Java + langchain4j 框架,负责 AI Agent 核心架构设计与落地"

一、为什么同一个业务做两遍

统计 778 条岗位时发现,Java 50 条、Python 98 条,而 Java + AI 的岗位竞争反而更小。 只做 Python 会丢掉一批机会。

更重要的是------做两遍之后,我才能真正回答面试官那个问题: "这两套你为什么选它?"

二、环境准备(本机实录)

powershell 复制代码
# JDK 17(清华镜像 Adoptium)
https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jdk/x64/windows/
# 解压到 D:\zhonglin\agent_project\tools\jdk-17.0.20.1+1
setx JAVA_HOME "D:\zhonglin\agent_project\tools\jdk-17.0.20.1+1"

# Maven 3.9.9(华为云镜像)
setx MAVEN_HOME "D:\zhonglin\agent_project\tools\apache-maven-3.9.9"

mvn -B compile
# BUILD SUCCESS

踩坑:BOM 模式下 java.version 不生效

xml 复制代码
<!-- ❌ 只在用 spring-boot-starter-parent 时才生效 -->
<java.version>17</java.version>

<!-- ✅ 用 spring-boot-dependencies BOM 时必须显式声明 -->
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>

否则 maven-compiler-plugin 会退回默认的 -source 8, 然后报:-source 8 中不支持 文本块(Java 15+ 特性)。

三、核心依赖

xml 复制代码
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j</artifactId>
  <version>0.35.0</version>
</dependency>
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-open-ai</artifactId>
  <version>0.35.0</version>
</dependency>
<dependency>
  <groupId>org.xerial</groupId><artifactId>sqlite-jdbc</artifactId>
</dependency>

四、AiServices:Java 版的"声明式 Agent"

这是 LangChain4j 最妙的设计------你只写接口,实现由框架动态生成:

java 复制代码
public interface OpsAssistant {
    @SystemMessage({
        "你是企业智能运营助手(EAP)...",
        "规则:1) 只依据知识库和工具返回的真实数据作答,禁止编造数字;",
        "      2) 引用资料时在句末标注来源,如 [1];",
        "      4) 涉及推送告警、写库等高风险动作,必须先向用户确认..."
    })
    TokenStream chat(@UserMessage String message);
}

装配:

java 复制代码
OpsAssistant assistant = AiServices.builder(OpsAssistant.class)
        .chatLanguageModel(model)                       // 模型
        .contentRetriever(retrievalService.contentRetriever())  // RAG
        .tools(biQueryTool, alertTool)                  // Function Calling
        .chatMemory(memories.computeIfAbsent(threadId,  // 会话级记忆
                k -> MessageWindowChatMemory.withMaxMessages(10)))
        .build();

运行时自动完成:提示词注入 → RAG 检索 → 工具调用 → 记忆管理 → 流式输出。

小细节:我没有 用 @AiService 注解,而是手写 AiServices.builder(), 因为要做会话级记忆隔离(注解方式在按 session 隔离上不够灵活)。

五、RAG:ContentRetriever

java 复制代码
@PostConstruct
public void buildIndex() {
    // 扫描 .md → 按标题切块 → embedding → 存入向量库
    List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
    store.addAll(embeddings, segments);
}

public ContentRetriever contentRetriever() {
    return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(store)
            .embeddingModel(embeddingModel)
            .maxResults(5)
            .minScore(0.15)
            .build();
}

向量库用 InMemoryEmbeddingStore,换成 PGVector / Milvus 只改 Bean,上层零改动。

六、工具:@Tool 注解

java 复制代码
@Component
public class BiQueryTool {
    @Tool("查询 BI 数据:项目成本、缺陷趋势、交付效率等指标统计。输入中文业务问题。")
    public String query(@P("业务问题,例如:本月各项目成本排名前 5") String question) {
        String lastError = "";
        for (int attempt = 0; attempt < 2; attempt++) {
            String rawSql = model.generate(prompt + lastError);
            try {
                return execute(validate(rawSql));   // 校验 + 执行
            } catch (Exception e) { lastError = e.getMessage(); }
        }
        return "SQL 生成或执行失败:" + lastError;
    }
}

和 Python 侧一模一样:生成 → 只读校验 → 执行 → 报错回炉。 校验逻辑同样是白名单表 + 危险关键字 + 单语句 + 自动补 LIMIT。

七、流式:TokenStream + WebFlux SSE

java 复制代码
@PostMapping(value = "/api/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chat(@RequestBody ChatRequest request) {
    Sinks.Many<ServerSentEvent<String>> sink = Sinks.many().unicast().onBackpressureBuffer();
    agentService.chat(request.getThreadId(), request.getQuery())
        .onNext(token -> sink.tryEmitNext(ServerSentEvent.builder(token).event("token").build()))
        .onError(t -> sink.tryEmitNext(...event("error")...))
        .onComplete(r -> {
            sink.tryEmitNext(ServerSentEvent.builder("[DONE]").event("done").build());
            sink.tryEmitComplete();
        })
        .start();
    return sink.asFlux();
}

八、Python vs Java:到底怎么选

维度 Python(LangGraph) Java(LangChain4j)
编排 显式状态图 + 条件边,强管控 AiServices 动态代理,模型自主决策
RAG 自建混合检索(向量 + BM25 + RRF + 重排) EmbeddingStoreContentRetriever
工具 ToolRegistry + JSON Schema @Tool / @P 注解
记忆 窗口 + 摘要 MessageWindowChatMemory
流式 生成器 + SSE TokenStream + WebFlux
调试 图可视化、状态可回放 依赖日志
接入速度 慢(要设计图) 快(一个接口搞定)

我的选型结论(面试直接这么说):

"需要严格控制每一步的场景(金融、审计、有合规要求),用 LangGraph 显式建图------ 每一步都在图里,可审计、可回放。 业务边界清晰、追求接入速度的场景,用 LangChain4j 的 AiServices------ 一个接口 + 几个注解就跑起来了。 两套我都实现过。"

九、统一接口契约

同一个前端可以连两个后端(改 vite proxy 的 target):

接口 Python Java
流式对话 POST /api/chat (SSE) POST /api/chat (SSE)
检索 POST /api/knowledge/search GET /api/search
健康检查 GET /api/health GET /api/health

下一篇:10 · 踩坑记录与优化清单

相关推荐
此时不提桶,更待何时19 小时前
05-03-B-ClickHouse与OLAP面试与生产事故实战
clickhouse·面试·nosql
此时不提桶,更待何时20 小时前
03-04-B-连接池与DB治理面试与生产事故实战
mysql·面试
kaiyou202621 小时前
数据分析岗面试,如何把考证学到的知识讲成业务案例?
面试·数据挖掘·数据分析
智购科技自动售货机工厂1 天前
数字人民币硬钱包支付失败,排查发现是NFC读卡器功率不足~YH
python·面试·架构·eclipse·emacs
Sam_Deep_Thinking1 天前
如何理解java的信号量
java·后端·面试·程序员
王中阳Go1 天前
面试官问"你怎么证明它有效",200个转AI的后端没几个答得上来
人工智能·后端·面试
进击的明明1 天前
TypeScript速通笔记(上)
前端·面试·typescript
老马识码1 天前
[agent开发面试]上下文工程与压缩:Agent 的稀缺资源
人工智能·面试
时间的拾荒人1 天前
Qt 信号与槽机制详解(二):连接方式详解与面试指南
开发语言·qt·面试
我叫黑大帅2 天前
Go日志库工程选型与逃逸分析评测报告
后端·面试·go