云选科普

LangChain4j 如何把 RAG 应用带到生产环境

从私有文档检索到工具调用、输入输出护轨、调用观测与 SSE 流式响应,梳理 Java 团队构建生产级 AI 应用时的关键链路与选型注意事项。

先把“能聊天”拆成一条可运行链路

一个 Java AI 应用从演示走向线上,通常不是再换一个更大的模型,而是把一次请求拆成几个可以独立观测和控制的环节:检索业务资料、决定是否调用工具、校验输入输出、记录调用过程,以及把结果持续推给前端。

LangChain4j 的价值在于,它把这些能力组织到 AI Service 这一层。开发者可以先用统一接口验证业务流程,再根据数据规模和安全要求替换检索器、模型或存储实现。需要注意的是,框架能提供调用链路,权限、数据脱敏、超时和审计仍然要由应用自己负责。

如果你的项目只是一次性问答,可以先从 ChatModel 开始;如果要回答企业内部资料、操作业务系统,或者承受真实用户流量,下面五个环节应当一起设计。

1. RAG:先解决“模型不知道什么”

检索增强生成(RAG)不是训练模型,而是在生成前从知识库取出相关内容,把这些内容和用户问题一起交给模型。它适合产品手册、内部制度、运维文档等需要频繁更新的资料。

一条可维护的 RAG 链路通常分为两段:

  1. 摄入:解析文件,切分文本,生成向量,并写入向量存储。
  2. 检索:把问题向量化,召回相关片段,再将结果注入当前请求。

示例可以这样写:

Document document = FileSystemDocumentLoader.loadDocument(
    "docs/product-guide.pdf",
    new ApachePdfBoxDocumentParser()
);

DocumentSplitter splitter = DocumentSplitters.recursive(300, 50);
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();

EmbeddingStoreIngestor.builder()
    .documentSplitter(splitter)
    .embeddingModel(embeddingModel)
    .embeddingStore(store)
    .build()
    .ingest(document);

ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
    .embeddingStore(store)
    .embeddingModel(embeddingModel)
    .maxResults(5)
    .minScore(0.75)
    .build();

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .contentRetriever(retriever)
    .build();

30050 只是可调的起点,不是通用答案。段落很短的 FAQ 可能需要更小的切片,长篇规范则要保留标题、章节和权限等元数据。InMemoryEmbeddingStore 适合本地验证,服务重启后数据会消失;线上应使用持久化向量库,并为租户、文档版本和访问权限设计过滤条件。

当简单向量召回不够时,可以在检索阶段加入查询改写、多路检索、混合检索或重排序。判断 RAG 是否可用,也不要只看最终回答,要分别检查“正确片段有没有召回”和“模型有没有依据片段作答”。

2. Tool Calling:让模型调用受控的 Java 能力

RAG 解决的是“读资料”,工具调用解决的是“做事情”。模型根据方法描述决定是否调用工具,框架负责把参数传给 Java 方法,再将执行结果交回模型生成最终回复。

public class WeatherTools {
    @Tool("查询指定城市的天气")
    String getWeather(@P("城市名") String city) {
        return weatherService.query(city);
    }
}

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .tools(new WeatherTools())
    .build();

工具描述应当写清楚用途、参数格式、返回内容和失败条件。更重要的是,工具不能直接等同于权限:查询工具可以只读,写入、删除、发消息、执行命令等操作需要在服务端再次做身份校验、参数校验和幂等控制。高风险动作还应加入人工确认或审批,不要因为模型“看起来理解了”就跳过业务授权。

3. Guardrail:把输入输出校验放在模型边界

护轨适合处理模型边界上的规则:输入是否越权,输出是否满足 JSON 结构,是否包含不允许的内容,以及不符合要求时是否重新生成。输入护轨应在模型调用前阻断请求;输出护轨则在结果返回业务层前校验。

@OutputGuardrails(
    value = JsonOutputGuardrail.class,
    maxRetries = 2
)
public interface Assistant {
    String chat(String userMessage);
}

重试次数不能无限增加:每次重试都可能增加延迟和模型费用。生产环境应记录失败原因,并为超时、连续失败和人工兜底设置明确策略。护轨也不是完整的安全方案,提示词注入可能来自用户输入、检索文档或工具返回值,因此还要配合数据分级、最小权限、输出脱敏和审计日志。

4. 可观测性:至少回答四个问题

上线后,不能只记录“成功或失败”。一次 AI 请求至少要能回答:调用了哪个模型、消耗了多少 token、耗时在哪个环节、最终答案是否命中了正确资料。

开发阶段可以打开请求和响应日志,生产环境则应避免把密钥、完整隐私文本和敏感业务数据直接写入日志。更稳妥的做法是使用监听器或指标组件记录请求次数、延迟、错误类型、token 用量和检索结果数量,并给每次请求附上 trace ID。

建议把指标分成三层:

  • 模型层:请求量、首 token 延迟、总耗时、输入输出 token、错误率。
  • RAG 层:召回数量、最低相似度、空召回比例、文档版本和权限过滤结果。
  • 业务层:工具调用成功率、人工接管率、用户反馈和任务完成率。

这样出现“答案变差”时,才能区分是资料没有更新、检索没有召回,还是模型生成偏离了上下文。

5. SSE:用流式响应改善等待体验

流式输出适合聊天、代码生成和长文本回答。后端以 text/event-stream 持续发送增量内容,前端可以边接收边渲染;它不等于更快生成,只是让用户更早看到结果。

LangChain4j 可以通过 TokenStream 接收增量回调,Spring MVC 则可用 SseEmitter 转发给浏览器:

@GetMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chat(@RequestParam String message) {
    SseEmitter emitter = new SseEmitter(60_000L);

    executor.submit(() -> assistant.chat(message)
        .onPartialResponse(partial -> safeSend(emitter, partial))
        .onCompleteResponse(response -> emitter.complete())
        .onError(error -> emitter.completeWithError(error))
        .start()
    );

    return emitter;
}

实际部署时要处理客户端断开、网关超时、线程池上限、重复重连和异常格式。若前端需要恢复中断的回答,应设计请求 ID 和事件序号;如果只是单向推送文本,SSE 通常比 WebSocket 更容易落地,但并不代表所有场景都适用。

按项目阶段选择实现方式

  • 本地验证:内存向量库、简单切分、一个只读工具、基础日志即可。
  • 小团队内测:改用持久化向量库,增加权限过滤、超时、错误兜底和 token 统计。
  • 生产服务:把检索、工具、模型调用和业务接口分层,补齐审计、指标、限流、重试和人工确认。
  • 高风险业务:先定义可执行的安全边界,再开放工具;对检索文档、工具参数和输出分别做校验。

一套 AI 应用是否值得上线,关键不在于把所有能力一次接满,而在于每个环节都能解释、能限制、能回滚。对 Java 团队来说,可以先用 AI Service 串起 RAG、工具调用和流式输出,再逐步替换存储和观测组件;但权限与数据治理应从第一版就纳入设计。

继续浏览

还想继续看,可以再看这些文章

适合想继续在同一主题下横向阅读、对比不同切入角度的用户。