三个人吵了两周,我算了一笔账
7 月初,团队为"新项目用哪个 Java AI 框架"吵了两周。三个人,三种意见,各自写了 demo,各自的 benchmark 都证明自己选的更好。
我做了件不太受欢迎的事:把这两周的成本算了出来。
参与讨论:3 人 × 14 天 × 60% 投入 ≈ 25 人日
写 demo + benchmark: ≈ 9 人日
评审会议:4 场 × 2 小时 × 5 人 ≈ 5 人日
-------------------------------------------
合计 ≈ 39 人日
按我们团队的成本口径,39 人日约等于 ¥11.7 万。而这三个框架,任意一个都能满足需求,差别在 5% 以内。
后来我们定了条规矩:框架选型的预算是 5 人日,超了就用当时的默认选项。这篇记录这次争论里真正有价值的部分,以及我们最后定下来的选型规则。
先把三家的定位讲清楚
很多选型讨论之所以低效,是因为大家在比较"哪个更好",而这三个东西压根不是一个物种。
| 框架 | 核心定位 | 它的世界观 | 2026 年的状态 |
|---|---|---|---|
| Spring AI | AI 能力的 Spring 化封装 | 模型是外部资源,用依赖注入管理 | 2.0 已 GA,生态最全 |
| LangChain4j | 工具箱 + 编排库 | Agent 是一组可组合的组件 | 成熟稳定,社区活跃 |
| Embabel | 目标驱动的规划式 Agent | Agent 应该自己规划路径达成目标 | 较新,设计激进 |
换个说法:
- Spring AI 解决的是"接入"。怎么把模型、向量库、MCP 服务接进来,用 Spring 的方式管理。它最擅长的就是让你写最少的代码接上最多的东西;
- LangChain4j 解决的是"编排"。链式调用、工具、记忆、RAG 的各个环节,它都给了实现,而且给了不止一种;
- Embabel 解决的是"规划"。你给目标(Goal)和可用的动作(Action),它用规划算法决定先做什么后做什么,而不是靠模型自己想。
这三个是可以叠加的。事实上 Embabel 底层就能用 Spring AI 的 ChatModel。把它们当三选一,是讨论一开始就跑偏的地方。
实测:同一个需求,三个实现
与其争论,不如写代码。我们定了一个真实的小场景:用户报"订单支付失败",Agent 需要查订单、查支付流水、判断原因、给出建议。三个框架各实现一遍,同一个同学写,避免水平差异。
Spring AI 2.0
@Service
public class PaymentFailureAgent {
private final ChatClient chatClient;
public PaymentFailureAgent(ChatClient.Builder b) {
this.chatClient = b
.defaultSystem("""
你是支付问题排查助手。
必须先查订单再查支付流水,不要凭空猜测。
""")
.defaultTools(new OrderTools(), new PaymentTools())
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new QuestionAnswerAdvisor(vectorStore))
.build();
}
public String handle(String userMsg, String sessionId) {
return chatClient.prompt()
.user(userMsg)
.advisors(a -> a.param(CONVERSATION_ID, sessionId))
.call()
.content();
}
}
32 行,写了 40 分钟。工具循环、记忆、RAG 全部由框架接管。代价是:你想干预中间过程,得用 advisor 插进去,改动时比较绕。
LangChain4j
@AiService
public interface PaymentFailureAgent {
@SystemMessage("""
你是支付问题排查助手。
必须先查订单再查支付流水,不要凭空猜测。
""")
@ToolBox({OrderTools.class, PaymentTools.class})
String handle(@MemoryId String sessionId, @UserMessage String msg);
}
// 装配,这里能看出"工具箱"的味道
@Bean
PaymentFailureAgent agent(ChatLanguageModel model,
ChatMemoryProvider memory,
ContentRetriever retriever) {
return AiServices.builder(PaymentFailureAgent.class)
.chatLanguageModel(model)
.chatMemoryProvider(memory)
.contentRetriever(retriever)
.tools(new OrderTools(), new PaymentTools())
.build();
}
28 行,写了 35 分钟。声明式接口很讨喜,而且它的组件化程度最高——记忆、检索、工具都有多种实现可选(比如记忆就有 MessageWindowChatMemory 和 TokenWindowChatMemory)。缺点是配置项多,上手要读文档。
Embabel
@Agent(description = "处理用户支付失败的问题")
public class PaymentFailureAgent {
@Action
Order lookupOrder(UserRequest req) {
return orderService.query(req.orderNo());
}
@Action(pre = "lookupOrder")
PaymentTrace lookupPayment(Order order) {
return paymentService.trace(order.paymentId());
}
@Action(pre = "lookupPayment")
@AchievesGoal(description = "给出支付失败的原因和处理建议")
Answer diagnose(Order order, PaymentTrace trace) {
// 规划到这里才调模型,前面全是确定性的代码
return llm.diagnose(order, trace);
}
}
25 行,写了 55 分钟(其中 20 分钟在读文档理解 pre 条件和 goal 的匹配机制)。
这个写法让我眼前一亮:前面两步是纯 Java 代码,不调模型。Embabel 的规划器根据 @Action 的前置条件和目标,自己排出执行顺序,只有最后需要生成自然语言的一步才用 LLM。
这个设计的价值在于可预测性。我们跑 200 次测试,Embabel 版本的执行路径完全一致(查订单 → 查流水 → 诊断),而前两个版本有 7% 的请求会跳过查订单直接查流水,还有 3 次模型调用了不存在的工具。
三家的实测数据
| 指标 | Spring AI 2.0 | LangChain4j | Embabel |
|---|---|---|---|
| 实现代码行数 | 32 | 28 | 25 |
| 首次跑通耗时 | 40 分钟 | 35 分钟 | 55 分钟 |
| 任务完成率(200 例) | 91.5% | 92.0% | 94.5% |
| 执行路径一致性 | 93% | 92% | 100% |
| 平均 token/次 | 4,820 | 4,910 | 2,340 |
| P99 延迟 | 8.1s | 8.4s | 5.2s |
| 模型调用次数/次 | 3.8 | 3.9 | 1.2 |
| 接入 MCP 服务的代码量 | 8 行 | 21 行 | 需自建 |
| 接入向量库种类 | 20+ | 15+ | 靠 Spring AI |
Embabel 在 token 和延迟上的优势来自"少调模型"。这个优势在确定性强的流程里非常明显,但在需要模型自主决策的开放场景里会消失——因为它本来就不擅长那种场景。
过度评估的真实代价
回到开头那 39 人日。它不是最坏的部分,最坏的是这三个隐形成本:
机会成本。这两周我们本来可以上线两个小功能。而框架选型的产出,事后看和"直接选 Spring AI"的差别不到 5%。
沉没成本导致的选择扭曲。一旦有人为一个方案写了 800 行 demo,他就很难客观地承认另一个方案更好。这不是人品问题,是人性。我见过太多次。
团队共识的磨损。吵了两周之后,落选方案的支持者心里是有疙瘩的。这种情绪会影响后面几个月的协作。我们这次最后选了 Spring AI,支持 Embabel 的同学虽然接受了,但我知道他到现在还觉得可惜。
我们现在的选型规则
这两周最大的产出不是选了哪个框架,是我们把它固化成了规则。现在团队里任何人要做技术选型,直接套这个。
规则一:默认选项优先
我们团队的默认选项是 Spring AI 2.0。理由很简单:团队 6 个人里 5 个熟 Spring,模型/向量库/MCP 的接入最全,出了问题社区和官方文档覆盖最好。
要用别的框架,你得说明"默认选项做不到什么"。不是"更好",是"做不到"。这个门槛筛掉了 80% 的无谓讨论。
规则二:按场景而非按框架选型
if (流程确定、强合规要求、要省钱) -> Embabel(规划式)
else if (需要细粒度干预、多 Agent 协作) -> AgentScope Java
else if (快速接入、生态整合、团队熟悉) -> Spring AI 2.0
else if (需要高度定制的编排逻辑) -> LangChain4j
else -> Spring AI 2.0(默认)
注意最后一行。没有明确理由就用默认项,这条比前面几条都重要。
规则三:预算硬约束
选型预算按项目规模定:
- 小项目(1 人月以内):1 人日。超时直接用默认项;
- 中项目(3 人月):3 人日;
- 大项目(6 人月以上):5 人日,且必须产出一份可公开的比较报告。
超过预算要在周会上说明理由。这个约束实施一个月,我们砍掉了两次提议中的选型调研。
规则四:抽象层隔离,允许后悔
这条最实在。让框架可替换,比选对框架更重要。
// 业务代码只依赖这个接口,不依赖任何框架类型
public interface AgentPort {
AgentReply run(String sessionId, String userMsg);
}
我们现在所有 Agent 都通过这层调用。换框架的代价从"改 47 个文件"变成"改一个实现类"。去年我们真的换过一次,一周完成,包括回归。有了这层,选型的心理压力小很多——选错了不至于伤筋动骨。
几个具体的建议
如果你正在做这个选择,这是我基于这半年实践能给的具体意见。
不要为了 Embabel 的 token 省一半而全量切换。它的规划式模型要求你把流程想清楚、拆成 Action,这对需求频繁变化的业务是负担。我们只在"支付失败排查""退款资格审核"这两个流程稳定的场景用它。
不要因为 LangChain4j 组件多就觉得它更适合复杂场景。组件多意味着选择多,选择多意味着团队容易选得不一致。我们有个项目三个人用了三种记忆实现,review 的时候才发现。
Spring AI 2.0 最大的优势不是功能,是它把 AI 变成了 Spring 的一部分。配置走 application.yml,Bean 走依赖注入,可观测性直接接 Micrometer。这意味着你现有的运维体系、监控体系、发布体系全都能复用。这个价值在选型时容易被低估,在运维半年后会变得很明显。
别信 benchmark,包括我上面那张表。那是我们场景的数字,跑在 4 核 8G 上,用的是我们的 prompt 和我们的数据。换到你那里可能完全反过来。要做的是:拿你自己的一个真实场景,跑 200 条数据,看三个数字——完成率、单次成本、P99。别的都可以不看。
写在后面
现在回头看,《Java AI 框架选型:Spring AI、LangChain4j、Embabel 怎么选》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。