先把“能聊天”拆成一条可运行链路
一个 Java AI 应用从演示走向线上,通常不是再换一个更大的模型,而是把一次请求拆成几个可以独立观测和控制的环节:检索业务资料、决定是否调用工具、校验输入输出、记录调用过程,以及把结果持续推给前端。
LangChain4j 的价值在于,它把这些能力组织到 AI Service 这一层。开发者可以先用统一接口验证业务流程,再根据数据规模和安全要求替换检索器、模型或存储实现。需要注意的是,框架能提供调用链路,权限、数据脱敏、超时和审计仍然要由应用自己负责。
如果你的项目只是一次性问答,可以先从 ChatModel 开始;如果要回答企业内部资料、操作业务系统,或者承受真实用户流量,下面五个环节应当一起设计。
1. RAG:先解决“模型不知道什么”
检索增强生成(RAG)不是训练模型,而是在生成前从知识库取出相关内容,把这些内容和用户问题一起交给模型。它适合产品手册、内部制度、运维文档等需要频繁更新的资料。
一条可维护的 RAG 链路通常分为两段:
- 摄入:解析文件,切分文本,生成向量,并写入向量存储。
- 检索:把问题向量化,召回相关片段,再将结果注入当前请求。
示例可以这样写:
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();300 和 50 只是可调的起点,不是通用答案。段落很短的 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、工具调用和流式输出,再逐步替换存储和观测组件;但权限与数据治理应从第一版就纳入设计。