用 whatsmars 读懂 LangChain4j:多 Agent 编排、高级 RAG、Guardrails,一文探索 Java AI 高级能力
前面的博文里,我们用 spacecloud 展示了 Spring AI 2.0 的全栈能力,用 babi 探索了三种框架的选型差异。这次轮到 LangChain4j——whatsmars-ai 模块在两天内完成了六大批量更新,从基础对话一跃成为涵盖多 Agent 编排、高级 RAG 管线、输入输出护栏、结构化输出、多模态、可观测性的全功能 AI 应用。
如果说 Spring AI 2.0 的关键词是"全栈",那 LangChain4j 的关键词就是"高级"——它不追求覆盖面,但在 Agent 编排和 RAG 管线上做得更深。
一、更新概览
先看本次新增了哪些能力:
| 能力 | 核心类/模块 | 说明 |
|---|---|---|
| Agentic 多 Agent 编排 | AgenticConfig + 11 个 Agent 接口 | 5 种编排模式:基础、顺序、循环、并行、Supervisor |
| Guardrails 护栏 | ProfanityInputGuardrail / QualityOutputGuardrail | 输入侧敏感词拦截 + 输出侧质量检查 + reprompt |
| 高级 RAG 管线 | 5 阶道流水线 | 查询压缩 → 智能路由 → 检索 → LLM 重排 → 注入 |
| 结构化输出 | StructuredOutputAssistant | AI 方法直接返回 Java record |
| 多模态 | Vision / Image / Audio | 视觉理解 + 文生图异步 API + 语音合成 |
| ConditionalOnMcpStatusUp | 自定义 @Conditional | TCP 端口探测,MCP Server 在线才加载 Bean |
| ChatModelListener | LoggingChatModelListener | Token 用量 + 耗时 + 消息分布全监听 |
下面逐个展开,每个模块都有代码片段和设计思路解析。
二、Agentic:五种 Agent 编排模式
这是本次更新最大的模块——13 个文件,11 个 Agent 接口,覆盖了 LangChain4j AgenticServices 的全部编排能力。
Agent 接口:声明式定义
所有 Agent 都是声明式接口,用 @Agent + @SystemMessage + @UserMessage 注解定义:
public interface ResearchAgent {
@Agent("研究指定主题并生成详细的研究报告")
@SystemMessage("""
你是一个专业的技术研究员。...
关于网络搜索工具的使用原则:
- 仅当主题涉及近期事件、实时数据或你知道截止日期之后的变化时才使用搜索工具
- 对于已有充分知识的成熟技术概念和框架特性,直接基于自身知识撰写报告即可
""")
@UserMessage("请研究以下主题:{{topic}}")
String research(@V("topic") String topic);
}
流式变体只需把返回类型从 String 改为 TokenStream:
public interface StreamingResearchAgent {
@Agent("研究指定主题并生成详细的研究报告(流式)")
TokenStream research(@V("topic") String topic);
}
返回结构化对象的 Agent 也很直观——QualityReviewerAgent 直接返回 record:
public interface QualityReviewerAgent {
@Agent(name = "质量审查专家", description = "评审技术文档的质量")
@UserMessage("请评审以下技术文档:\n\n{{document}}")
QualityReview reviewDocument(@V("document") String document);
record QualityReview(double score, String feedback, String strengths) {}
}
这种声明式风格是 LangChain4j 的核心设计:Agent 的定义就是接口 + 注解,不需要写实现类,框架在运行时自动生成代理。
模式一:顺序工作流
Research → Summarize → Translate,前一个 Agent 的输出通过 AgenticScope 自动传递给下一个:
@Bean
public UntypedAgent sequentialWorkflow(ChatModel chatModel, WebSearchTool webSearchTool) {
var research = AgenticServices.agentBuilder(ResearchAgent.class)
.chatModel(chatModel).tools(webSearchTool)
.outputKey("researchResult").build();
var summarize = AgenticServices.agentBuilder(SummarizerAgent.class)
.chatModel(chatModel).outputKey("summary").build();
var translate = AgenticServices.agentBuilder(TranslatorAgent.class)
.chatModel(chatModel).outputKey("translation").build();
return AgenticServices.sequenceBuilder()
.subAgents(research, summarize, translate)
.outputKey("translation")
.build();
}
每个 Agent 通过 outputKey 声明自己的输出存入 AgenticScope 的哪个 key,后续 Agent 可以从 scope 中读取。最后的 outputKey("translation") 指定整个工作流的最终输出取 translation。
模式二:流式顺序工作流
中间步骤用同步 ChatModel,最后一步换成 StreamingChatModel,最终结果通过 SSE 流式输出:
var translate = AgenticServices.agentBuilder(StreamingTranslatorAgent.class)
.streamingChatModel(streamingChatModel)
.outputKey("translation")
.build();
这个设计很巧妙:用户只需要最终翻译结果流式输出,中间的研究和摘要步骤不需要流式。这样可以减少不必要的 LLM 调用开销,同时保证用户体验——一气呵成地看到最终结果。
模式三:循环工作流
Writer 写文档 → QualityReviewer 评审,score >= 0.7 退出,最多 3 轮:
@Bean
public UntypedAgent loopWorkflow(WriterAgent writerAgent, QualityReviewerAgent qualityReviewerAgent) {
return AgenticServices.loopBuilder()
.subAgents(writerAgent, qualityReviewerAgent)
.outputKey("document")
.exitCondition(scope -> {
QualityReviewerAgent.QualityReview review =
(QualityReviewerAgent.QualityReview) scope.readState("review");
return review != null && review.score() >= 0.7;
})
.maxIterations(3)
.build();
}
exitCondition 接收一个 Predicate<AgenticScope>,从 scope 中读取上一步的 review 结果,检查评分是否达标。maxIterations(3) 是安全阀——如果 LLM 一直无法达到质量标准,3 轮后强制退出。
Controller 层的流式循环更复杂——手动编排写-审循环,每轮发送 SSE 事件:iteration(状态:writing/reviewing)、stream(写作 token 流)、review(评分+反馈)、document(最终文档)。这让用户能实时看到"AI 正在写第几稿"和"评审打了几分"。
模式四:并行工作流
三个代码审查 Agent(安全、性能、最佳实践)并行执行,通过 output 回调聚合结果:
return AgenticServices.parallelBuilder()
.subAgents(securityReviewer, performanceReviewer, bestPracticeReviewer)
.outputKey("fullReview")
.output(scope -> {
String security = (String) scope.readState("securityReview");
String performance = (String) scope.readState("performanceReview");
String bestPractice = (String) scope.readState("bestPracticeReview");
return """
=== 综合代码审查报告 ===
【安全审查】%s
【性能审查】%s
【最佳实践】%s
""".formatted(security, performance, bestPractice);
})
.build();
并行模式下,所有子 Agent 同时执行,output 回调在全部完成后被调用,从 scope 中读取各个 Agent 的输出并合并为最终报告。
模式五:Supervisor 编排
Supervisor 是最灵活的模式——它不是预定义的流程,而是一个"主管 Agent",根据用户问题动态决定调用哪些子 Agent、以什么顺序:
return AgenticServices.supervisorBuilder()
.chatModel(chatModel)
.subAgents(researcher, summarizer, securityReviewer,
performanceReviewer, bestPracticeReviewer)
.contextGenerationStrategy(SupervisorContextStrategy.CHAT_MEMORY_AND_SUMMARIZATION)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.supervisorContext("你是一个技术顾问主管。根据用户的问题,合理调度专家团队。...")
.build();
两个关键策略:CHAT_MEMORY_AND_SUMMARIZATION 让 Supervisor 保持对话记忆并在上下文过长时自动摘要;SUMMARY 让 Supervisor 在所有子 Agent 执行完后汇总结果而非直接转发。Supervisor 模式最接近真实的"AI 团队"——你给它一群专家,它自己决定怎么用。
AgenticServices 的设计哲学
回顾这五种模式,可以发现 LangChain4j 的编排设计有一个清晰的思路:把"Agent 的定义"和"Agent 的编排"分离。Agent 的定义是声明式接口——@Agent + @SystemMessage + @UserMessage,每个 Agent 只关心自己的职责。Agent 的编排是命令式 Builder——sequenceBuilder()、loopBuilder()、parallelBuilder()、supervisorBuilder(),编排逻辑在配置类中集中管理。
这种分离的好处是:同一个 Agent 可以被多种编排模式复用。比如 ResearchAgent 既在顺序工作流中做第一步(研究 → 摘要 → 翻译),也可以在 Supervisor 模式中作为一个子 Agent 被动态调度。Agent 接口不需要知道自己在哪种编排模式中,它只负责"收到输入 → 返回输出",编排逻辑由 Builder 层处理。
另一个值得注意的细节是 AgenticScope 的角色。它是一个运行时上下文,在 Agent 间传递数据——每个 Agent 通过 outputKey 把结果写入 scope,后续 Agent 通过 scope.readState() 读取。这比把所有数据塞进 ChatMemory 更清晰——ChatMemory 是对话历史,AgenticScope 是工作流数据。两者职责分离,互不干扰。
对比 Python 生态,LangChain4j 的 AgenticServices 大致对应 LangGraph 的 StateGraph——都是基于状态传递的图编排。但 LangChain4j 的声明式接口(@Agent 注解的 Java 接口)比 LangGraph 的函数定义更类型安全,IDE 支持也更好。代价是灵活性低一些——LangGraph 可以自由定义图的边和条件分支,LangChain4j 目前只提供了五种预设模式。
三、Guardrails:输入输出护栏
AI 应用的安全问题不需要过多解释——用户可能输入不当内容,LLM 也可能输出低质量回答。LangChain4j 提供了 InputGuardrail 和 OutputGuardrail 两个接口,通过注解绑定到 AI Service 上。
输入护栏:敏感词拦截
public class ProfanityInputGuardrail implements InputGuardrail {
private static final String[] BLOCKED_WORDS = {"fuck", "shit", "damn", "ass"};
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
String text = userMessage.singleText().toLowerCase();
for (String word : BLOCKED_WORDS) {
if (text.contains(word)) {
log.warn("检测到敏感词汇: {}", word);
return failure("消息包含不当内容,请重新表述您的问题");
}
}
return success();
}
}
实现很直接——关键词匹配,命中就 failure()。Controller 层捕获 InputGuardrailException 返回拦截提示。
输出护栏:质量检查 + reprompt
更有意思的是输出侧——如果 AI 回复太短(<50 字),不是直接拒绝,而是触发 reprompt() 让 LLM 重新回答:
public class QualityOutputGuardrail implements OutputGuardrail {
private static final int MIN_LENGTH = 50;
@Override
public OutputGuardrailResult validate(AiMessage responseFromLLM) {
String text = responseFromLLM.text();
if (text == null || text.length() < MIN_LENGTH) {
log.warn("AI 回复过短 ({} 字符),触发 reprompt",
text == null ? 0 : text.length());
return reprompt(
"回复过于简短,请提供更详细、更有价值的回答",
"请用至少 3-4 句话详细回答用户的问题,确保提供有价值的信息。"
);
}
return success();
}
}
reprompt() 是 LangChain4j 的一个精巧设计——它不是简单地把 LLM 的短回答丢弃,而是把"回复太短"这个反馈和"请详细回答"的指令一起发给 LLM,让它基于自己的上一轮回答重新生成。这比简单的拒绝-重试更智能,因为 LLM 知道自己上次答得不好,会调整输出。
注解绑定
Guardrail 不是在 Config 里绑定的,而是直接注解在 AI Service 接口上:
public interface GuardrailsAssistant {
@SystemMessage("你是一个专业的 Java 技术专家,回答要详细、准确、有深度。")
@InputGuardrails(ProfanityInputGuardrail.class)
TokenStream chat(String message);
}
这种声明式风格和 Agent 接口一致——安全策略跟着接口定义走,而不是散落在配置类里。
四、高级 RAG:五阶段流水线
之前 whatsmars-ai 的 RAG 是简单版:embed → search → inject → chat。这次升级为完整的五阶段管线:
用户查询 → 查询压缩 → 智能路由 → 检索 → LLM 重排 → 注入 → LLM → SSE 响应 + sources
阶段一:查询压缩
用户提问往往很口语化,包含寒暄和冗余。CompressingQueryTransformer 用 LLM 把冗长提问压缩为检索关键词:
String compressed = chatModel.chat("""
将以下用户查询压缩为简洁的检索关键词,用于在知识库中搜索。
要求:只保留核心概念和技术术语,去除寒暄、语气词和冗余表达。
直接输出关键词,不要任何解释,不超过15个字。
用户查询: %s
""".formatted(original));
比如 “你好,我想问一下 Spring Boot 有哪些核心特性?” 会被压缩为 “Spring Boot 核心特性”。短查询(<=20 字)直接跳过压缩。这个设计的价值在于:embedding 模型对简洁的技术术语比对长句更敏感,压缩后检索质量更高。
阶段二:智能路由
不是所有问题都需要检索知识库——"你好"和"讲个笑话"不需要 RAG。SmartQueryRouter 用 LLM 做分类:
private boolean classify(String query) {
String response = chatModel.chat("""
判断以下用户查询是否需要从知识库检索资料来回答。
如果查询涉及技术知识、概念解释、原理说明、最佳实践、框架用法等,回复 KNOWLEDGE
如果是问候、闲聊、与知识库无关的通用问题,回复 CHAT
只回复 KNOWLEDGE 或 CHAT,不要其他内容。
用户查询: %s
""".formatted(query));
return response != null && response.trim().toUpperCase().contains("KNOWLEDGE");
}
判定为 CHAT 时路由到空检索器(跳过检索),判定为 KNOWLEDGE 时路由到实际的内容检索器。这减少了不必要的 embedding 计算和向量搜索开销。
阶段三:自定义检索器
CustomContentRetriever 在标准检索基础上,把相似度分数写入 metadata,供后续阶段使用:
EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(maxResults)
.minScore(minScore)
.build();
// ... 把 match.score() 写入 Document metadata
meta.put("score", String.valueOf(match.score()));
minScore 设为 0.5——相似度低于 0.5 的文档直接过滤,避免低质量检索结果干扰 LLM。
阶段四:LLM 重排
这是最复杂的阶段。ReRankingContentAggregator 收集所有检索结果,去重后让 LLM 打分(0-10),取 Top-3:
sb.append("请对以下候选资料与原始问题的相关性进行评分(0-10分)。\n");
sb.append("原始问题: ").append(queryToContents.keySet().iterator().next().text()).append("\n\n");
sb.append("候选资料:\n");
for (int i = 0; i < allCandidates.size(); i++) {
String snippet = allCandidates.get(i).textSegment().text();
if (snippet.length() > 150) snippet = snippet.substring(0, 150) + "...";
sb.append(i + 1).append(". ").append(snippet).append("\n");
}
LLM 打分后,解析分数(正则 (\d+)\.\s*(\d+)),按分数降序排列取前 3 条。同时把 source 信息(文本、文件名、原始相似度分数、LLM 重排分数)存入 RetrievalContext(ThreadLocal),供 Controller 在 SSE 响应中推送 sources 事件。
这个设计的核心思想是:向量相似度 ≠ 语义相关性。两段文本可能在向量空间接近,但实际内容不直接回答用户问题。LLM 重排基于对问题的真正理解来排序,比纯向量相似度更准。
阶段五:注入与来源引用
var contentInjector = DefaultContentInjector.builder()
.promptTemplate(new PromptTemplate("""
{{userMessage}}
基于以下参考资料回答用户问题。
回答时请引用资料来源编号(如 [1]、[2])。
参考资料: {{contents}}
"""))
.metadataKeysToInclude(List.of("file_name", "score"))
.build();
注入模板让 LLM 在回答时标注来源编号,同时把文件名和相似度分数也注入到上下文中。Controller 在 SSE 流完成后推送一个 sources 事件,包含每条引用的 SourceInfo(text, fileName, score, reRankScore)——前端可以展示"这段回答引用了哪些文档,各自相似度多少"。
管线组装
整个管线通过 DefaultRetrievalAugmentor.builder() 手动组装:
var retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.queryTransformer(queryTransformer)
.queryRouter(queryRouter)
.contentAggregator(contentAggregator)
.contentInjector(contentInjector)
.build();
注意:retrievalAugmentor 不暴露为 Spring Bean,而是直接传给 AiServices.builder()。这是刻意的设计——如果暴露为 Bean,Spring 会把它自动注入到所有 @AiService 标注的接口中,导致不需要 RAG 的 AI Service 也被污染。后面会详细讲这个架构决策。
为什么高级 RAG 需要这么多阶段?
简单 RAG(embed → search → inject)在原型阶段够用,但生产环境会遇到一系列问题:用户提问太口语化导致 embedding 质量差(需要查询压缩);闲聊问题也触发检索浪费算力(需要智能路由);向量相似度高但语义不相关(需要 LLM 重排);用户不知道答案从哪来(需要来源引用和分数展示)。每个阶段解决一个具体的"RAG 质量天花板"问题。
当然,不是所有 RAG 应用都需要全套管线。对于简单的文档问答,Spring AI 的 QuestionAnswerAdvisor 一行搞定,开发效率高得多。whatsmars 展示的是"把 RAG 做到极致"的可能性,不是"所有 RAG 都应该这么做"的模板。实际项目中,通常从简单 RAG 开始,遇到具体问题再加对应阶段——检索质量差就加查询压缩,准确率低就加重排,响应慢就加智能路由跳过无关检索。
五、结构化输出:AI 方法返回 Java Record
LLM 返回的是自然语言文本,但实际应用中我们经常需要结构化数据。LangChain4j 的 AI Service 支持方法返回 Java record——框架自动指导 LLM 输出 JSON 并反序列化:
public interface StructuredOutputAssistant {
@SystemMessage("""
你是一个信息提取助手,请从用户描述中提取个人信息。
你必须严格以 JSON 格式回复,包含字段: name(string), age(integer),
email(string), occupation(string), hobbies(string数组)。
只返回 JSON,不要包含任何其他文字。
""")
@UserMessage("请从以下描述中提取用户信息:{{message}}")
UserInfo extractUserInfo(@V("message") String message);
record UserInfo(String name, Integer age, String email,
String occupation, List<String> hobbies) {}
}
四个示例覆盖了典型场景:信息提取(UserInfo)、情感分析(SentimentResult 带 enum)、任务列表(TodoList 嵌套 List<TodoItem>)、SQL 生成(SqlResult 带 List<String>)。调用方拿到的是类型安全的 Java 对象,不需要自己做 JSON 解析。
六、多模态:视觉、文生图、语音
这三个能力和 spacecloud 的实现思路一致,但用 LangChain4j 的 API 实现。
视觉理解使用 LangChain4j 的 ImageContent + TextContent 组合:
UserMessage userMessage = UserMessage.from(
TextContent.from(prompt),
ImageContent.from(imageUrl)
);
ChatResponse response = multimodalChatModel.chat(userMessage);
文生图和语音合成同样需要自定义适配 DashScope 原生 API——DashScopeImageModel 实现异步提交 + 轮询,DashScopeTtsModel 调用 SpeechSynthesizer 端点返回 MP3。这和 spacecloud 的实现完全一致,说明 DashScope 的 API 差异不因框架不同而改变——无论 Spring AI 还是 LangChain4j,都需要适配。
七、ConditionalOnMcpStatusUp:MCP 依赖的优雅处理
whatsmars-ai 的 MCP Client 依赖 whatsmars-mcp(端口 8886)先启动。如果 MCP Server 没启动,Client 会连接失败导致应用启动报错。解决方案是自定义 Spring 条件注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnMcpStatusUpCondition.class)
public @interface ConditionalOnMcpStatusUp {
String host() default "localhost";
int port() default 8886;
int timeout() default 2000;
}
OnMcpStatusUpCondition 通过 TCP socket 探测端口:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), timeout);
log.info("MCP Server 端口检测通过: {}:{},加载 MCP 相关 Bean", host, port);
return true;
} catch (Exception e) {
log.warn("MCP Server 端口检测失败: {}:{},跳过 MCP 相关 Bean", host, port);
return false;
}
}
MCP Server 在线时加载 MCP Client 相关 Bean,不在线时跳过——应用依然能启动,只是 MCP 功能不可用。这比 @DependsOn 或启动顺序控制更优雅,因为 MCP Server 可能是独立部署的,不在同一个应用上下文中。
八、架构决策:手动构建 AiService
这次更新的一个重要架构变化:全部 AI Service 从 @AiService 注解改为 AiServices.builder() 手动构建。
// 之前:注解驱动,Spring 自动注入
// @AiService
// public interface RetrievalAssistant { ... }
// 现在:手动构建
@Bean
public RetrievalAssistant retrievalAssistant(StreamingChatModel streamingChatModel, ...) {
return AiServices.builder(RetrievalAssistant.class)
.streamingChatModel(streamingChatModel)
.retrievalAugmentor(retrievalAugmentor)
.build();
}
原因在前文提到过:如果用 @AiService 注解,且 Spring 上下文中存在 RetrievalAugmentor Bean,LangChain4j 会把它自动注入到所有 @AiService 接口中。这意味着 SimpleAssistant、StreamingAssistant 这些不需要 RAG 的 AI Service 也会被挂上检索管线——每次对话都先去知识库检索一番,完全不合理。
手动构建的好处是精确控制:只有 RetrievalAssistant 拿到 retrievalAugmentor,其他 AI Service 不受影响。代价是配置类变长了,但这种显式控制对于功能复杂的项目是值得的。
九、可观测性:ChatModelListener
LoggingChatModelListener 实现了 ChatModelListener 接口,监听所有 LLM 调用:
@Override
public void onResponse(ChatModelResponseContext responseContext) {
ChatResponse chatResponse = responseContext.chatResponse();
TokenUsage usage = chatResponse.tokenUsage();
String tokenInfo = String.format("prompt=%d, completion=%d, total=%d",
usage.inputTokenCount(),
usage.outputTokenCount(),
usage.totalTokenCount());
long elapsed = System.currentTimeMillis()
- (long) responseContext.attributes().getOrDefault(ATTR_START_TIME, System.currentTimeMillis());
log.info("[LLM Response] provider={}, tokens=[{}], elapsed={}ms", ...);
}
三个回调点:onRequest(记录开始时间 + 消息分布)、onResponse(Token 用量 + 耗时)、onError(异常 + 耗时)。这在多 Agent 编排场景下特别有价值——一次用户请求可能触发 5-10 次 LLM 调用(各子 Agent 各调一次 + 重排打分 + 查询压缩 + 查询路由分类),有了 Listener 就能看清每次调用的 Token 消耗和耗时,快速定位性能瓶颈。
十、写在最后
如果把这波更新和 spacecloud、babi 放在一起看,三个项目恰好展示了 Java AI 生态的三个维度:
spacecloud 展示了 Spring AI 2.0 的全栈能力——从对话到生成、从 RAG 到 MCP,强调的是覆盖面和微服务集成。whatsmars 展示了 LangChain4j 的高级能力——多 Agent 编排、高级 RAG 管线、Guardrails,强调的是深度和编排能力。babi 则是三框架选型对比——用 AgentScope、LangGraph4j、Spring AI 三种框架实现同一个 Coding Agent,强调的是框架差异和选型。
LangChain4j 在 Agent 编排上的优势很明显:AgenticServices 提供的五种编排模式(顺序、循环、并行、Supervisor)覆盖了大部分多 Agent 场景,声明式接口让 Agent 定义极简,AgenticScope 提供了 Agent 间数据传递的统一机制。相比之下,Spring AI 2.0 目前在 Agent 编排方面还没有对标方案——它的优势在 MCP 协议和微服务集成。
高级 RAG 管线是另一个亮点。LangChain4j 的 RetrievalAugmentor 设计把查询压缩、智能路由、重排、注入拆成可插拔的组件,组装方式自由度很高。Spring AI 的 QuestionAnswerAdvisor 更简洁但灵活性低一些——它是一个 Advisor,内部封装了检索和注入,不好拆开定制。
Guardrails 和结构化输出则是 LangChain4j 独有的能力——Spring AI 目前没有内置的输入输出护栏机制,结构化输出也还在发展中。这些是 LangChain4j 作为 LangChain Python 生态 Java 移植的"先发优势"。
相关博文:
git clone https://github.com/javahongxi/whatsmars
更多推荐


所有评论(0)