1. 选型起点:为什么偏要在 JVM 里做 AI 应用
1.1 一个很现实的场景:Python 方案的成本开始失控
先说个我经历过的选型现场。2025 年初,我们团队要做企业知识库问答和一套内部数据分析助手,当时摆上桌的方案有两条路:一条是正统的 Python + LangChain / LangGraph,另一条是当时还显得“小众”的 Spring AI + LangGraph4j。
团队里 Python 派和 Java 派吵得不可开交。Python 派说生态成熟、资料多、改起来快;Java 派说公司底层全是 Spring Cloud 那套东西,微服务、网关、配置中心、链路追踪全是 JVM 体系,加一个 Python 服务进来,等于在高速公路旁边修一条土路。
我当时的态度比较务实。先别聊技术情怀,算一笔费用:一个 Python 服务要接入公司已有的统一认证、灰度发布、监控告警、日志采集体系,少说一个月的集成工作量。就算用容器封装,运维那边对 Python 服务的资源占用、内存泄漏、依赖冲突照样头疼——别笑,这些在 Java 体系里被框架约束得死死的,到了 Python 那边全靠自觉。
加上 LLM 应用本身要处理的不是“单个模型调用”,而是大量的前后处理、工具调用、状态流转、RAG 检索、多轮记忆。这些逻辑如果全塞进 Python 脚本,代码组织早晚会乱;如果塞进 Java 服务,就得自己造轮子。所以在 2025 年这个时间点上,问“为什么选择 Spring AI + LangGraph4j”,本质上是在问:企业级 Java 团队想认真做 LLM 应用,现成的、有背书的路到底怎么走。
1.2 Spring AI 不是“又一个 OpenAI SDK”,而是一层基础设施
Spring AI 的定位很容易被误解。很多人第一次看到它,觉得这不就是把 OpenAI 的 Java SDK 包了一层吗?真要这么理解,后续选型十有八九要跑偏。
同样的逻辑可以参考 Spring 的历史:Spring JDBC 出现之前,Java 链接数据库的方式千奇百怪,每个团队都有自己的工具类,换数据库厂商基本等于重写数据访问层。Spring JDBC 做的事情不是发明一个新的数据库访问方式,而是把所有关系型数据库访问的共同模式抽出来,让你写一套代码,适配各种数据库。
Spring AI 干的也是这个事。它把“跟大模型对话”这件事抽象成了几个固定动作:
- 通过统一的
ChatClient接口发起对话; - 通过
ChatModel适配不同厂商(OpenAI、通义、Ollama 本地模型、Azure OpenAI 等); - 通过
EmbeddingModel做向量化; - 通过
Advisor接口把提示词增强、上下文管理、RAG 召回等横切逻辑挂到对话链路里; - 通过
ObservationHandler把每次调用纳入 Micrometer 观测体系。
这套组合拳打下来,业务代码里基本不会再出现某一家模型厂商的私有数据结构。今天接 OpenAI,明天因为合规原因换成千问,改一行配置就完事。这种“模型无关性”对于企业级项目来说,价值不是省几行代码,而是规避了绑定风险。
我自己在项目里最大的感受是:Spring AI 让 AI 调用变得像写 CRUD 一样有章法。RAG 链路里的QuestionAnswerAdvisor、工具调用里的ToolCallingManager、评估用的Evaluator,每个组件都能在 Spring 的依赖注入体系里找到自己的位置。这对习惯了 Spring 全家桶的 Java 工程师来说,几乎没有学习门槛。
1.3 选型判断的三个硬指标
后来我把自己的判断标准拆成了三条,跟团队对齐:
- 模型可替换性:项目周期至少跨一两年,期间可能有更好的私有化模型方案出现,代码不能锁死在某个厂商 SDK 上;
- 可观测性:LLM 调用是出问题重灾区,温度参数、上下文窗口、工具调用结果,每一步都要有迹可循;
- 流程可编排:真实业务里的 AI 功能很少是“一次性问答”,多轮、有条件跳转、失败重试、人工确认这类需求,必须有一个像样的流程引擎而不是写一堆 if-else。
对照这三条,当时市面上的方案里面,Spring AI 作为模型接入层完全符合第 1、2 条,但第 3 条“流程可编排”它做得不算深——直到 LangGraph4j 站了出来。所以标题里“为什么选择 Spring AI + LangGraph4j”,重点其实在后面这个加号上:它俩不是替代关系,而是互补关系。
2. Spring AI 到底在做哪一层的事
2.1 从零写 Prompt 拼接的痛,ChatClient 全接住了
不做 AI 应用的人可能很难理解,最繁琐的不是“调 API”,而是对话前的准备和对话后的处理。一个最普通的聊天机器人,背后也得做:系统提示词和历史消息拼接、限制上下文长度、把用户问题先做意图识别或改写、决定要不要调用外部工具,然后才是模型调用,拿到结果还要校验 JSON 格式、决定要不要再做一轮工具调用。
这些逻辑如果每次手工写,代码量动辄上千行,而且每个接口之间很容易出现细微差异。Spring AI 的ChatClient用链式调用把这一整套流程压成了非常直观的 DSL:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); String answer = chatClient.prompt() .system("你是一个严谨的数据库分析助手") .user("查询上个月销售额最高的三个区域") .toolNames("execute_sql", "get_table_schema") .call() .content();这段代码里的每一层,都可以在 Spring 容器里被替换或增强。想换记忆实现?换个ChatMemory的 Bean。想换向量库?换个VectorStore的 Bean。这种“面向接口编程”的舒适感,Java 老鸟一上手就懂,根本不需要说服。
2.2 一套代码跑通 OpenAI、千问和本地模型
我特意做了一个小实验来验证模型可替换性:代码里不写任何模型供应商相关类,只配置不同的依赖和application.yml片段。
连 OpenAI:
spring: ai: openai: base-url: ${OPENAI_BASE_URL} api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o连通义千问:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus连本地 Ollama:
spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:14b业务代码一行没改。这不是什么黑魔法,就是 Spring 经典的自动配置机制在起作用。对于企业项目来说,这意味着预研阶段可以用便宜的本地模型把流程跑通,上线前再切换到商业模型做效果验证——整个过程不会因为换模型而重构代码。
2.3 RAG 与工具调用没有想象中的玄乎
RAG 是当前企业落地 LLM 最有价值的方向,但很多人被网上各种花哨术语绕晕了。Spring AI 提供了完整的 RAG 链路的默认实现:
- 文档加载:
DocumentReader读取 PDF、Word、TXT; - 文档切分:
TokenTextSplitter按 Token 数切块,避免检索粒度太粗或太细; - 向量化与存储:
EmbeddingModel把文本块变向量,存入VectorStore; - 召回与重排:用户提问时向量检索 Top-K;
- 上下文注入:
QuestionAnswerAdvisor把召回内容拼进系统提示词,让模型基于资料作答。
我印象最深的是QuestionAnswerAdvisor这个组件。以前做 RAG 我要手写一套“检索 -> 拼接 -> 调用 -> 引用标注”的流程,现在只需要在构建ChatClient时加一行.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)),问题来了它会自动检索,自动拼装,最后返回结果。当然它也有局限:默认实现用的是余弦相似度或向量数据库自带的检索,遇到需要复杂重排的场景还是得自己扩展。但这不妨碍它是目前 JVM 生态里,RAG 落地成本最低的入口。
2.4 ObservationHandler 是生产环境不会撒谎的证据
埋点这件事,做 AI 应用尤其重要。因为大模型输出是概率性的,同一个问题两次的答案可能完全不一样。如果一次调用返回了错误内容,UAT 环境复现不了、日志里只有一行“调用成功”,那排查问题就只能靠猜。
Spring AI 2.0 里把可观测性做到了框架层。基于 Micrometer Observation 的ObservationHandler会把一次完整对话拆成多段观测数据:
- ChatModel 调用的耗时、Token 消耗、是否成功;
- Advisor 链路上各环节的执行情况;
- Tool 调用的入参和结果;
- RAG 检索命中了哪些文档。
这些观测数据可以透传给 Prometheus、Zipkin、SkyWalking 等现有监控体系。举个例子:业务方反馈“用户问了一个合同相关问题,机器人答非所问”。通过观测面板,我们能直接看到模型调用前 Advisor 是否成功注入了向量召回内容、工具调用是否超时、上下文是否被截断——而不是对着业务代码在那儿猜。
这也是我敢在生产环境上 Spring AI 的重要原因:它在透明度和可控性方面,明显强于普通 SDK 封装。
3. LangGraph4j 补齐的唯一短板:流程编排
3.1 单次调用和 Agent 工作流的本质差异
你可能会问:既然 Spring AI 已经把“调大模型”这件事做得这么顺手,为什么还需要 LangGraph4j?
答案在于:真实的业务场景很少是“问一句答一句”。以数据分析助手为例,用户说“帮我看看华东区这个月销量为什么跌了”,这个任务落到系统里,至少要走:
- 理解问题 -> 决定需要查哪些表;
- 获取表结构 -> 生成候选 SQL;
- 执行 SQL -> 拿到结果;
- 如果 SQL 报错或结果为空 -> 修正 SQL 重试;
- 结果汇总 -> 生成自然语言分析报告;
- 如果分析报告需要图表 -> 调一个图表生成工具。
这一套流程,每一步都可能要调用 LLM,前后步骤之间存在条件分支和循环,本质上是一个有状态的工作流。如果全部写在业务代码里,常见的做法是“用一个 while 循环 + 状态变量 + 一堆 if-else 硬扛”。一开始还能跑,等分支多起来,日志根本没法看,出了错状态不知道在哪,想重试某一环也没有手段。
LangGraph4j 解决的就是这个层面的问题:它把 Agent 工作流建模成一张有向图——节点就是各个步骤,边就是步骤之间的跳转条件,状态对象在整张图里流动,每一步都可以读写状态、决定下一步去向。
3.2 手写状态机 vs 状态图:差的不是代码量而是维护性
有人会说,不就是一个状态机吗?我自己写一个 switch-case 不就行了。
这样说的人,大概率还没遇到过带“并行分支”和“人工介入”的真实场景。LangGraph4j 的 StateGraph 给我的最大冲击在于,它把控制流从代码逻辑里“摘”了出来,变成了图结构声明:
StateGraph<DataAnalysisState> graph = new StateGraph<>(DataAnalysisState::new) .addNode("parse_intent", new ParseIntentNode()) .addNode("generate_sql", new GenerateSqlNode()) .addNode("execute_sql", new ExecuteSqlNode()) .addNode("check_result", new CheckResultNode()) .addNode("generate_report", new GenerateReportNode()) .addEdge(START, "parse_intent") .addEdge("parse_intent", "generate_sql") .addEdge("generate_sql", "execute_sql") .addEdge("execute_sql", "check_result") .addConditionalEdge("check_result", state -> state.isSqlError() ? "generate_sql" : "generate_report", Map.of("generate_sql", "generate_sql", "generate_report", "generate_report")) .addEdge("generate_report", END) .build(); GraphState state = graph.invoke(new DataAnalysisState("华东区销量为什么跌了"));四个节点是四个独立类,图结构单独声明,条件跳转单独表达。新增一个节点几乎不影响其他代码;出问题时看“当前在哪个节点”,比在一坨循环里打断点要直观得多。
有人可能觉得“这不就是 BPMN 流程引擎嘛”,但 BPMN 主要是给人走的审批流,节点类型和消息机制偏重;LangGraph4j 的图是为 LLM Agent 设计的,节点里天然就能嵌入 ChatClient 调用,状态里天然可以塞消息列表、工具调用记录、Token 消耗,并且支持流式输出——这些对 AI 应用都是刚需。
3.3 LangGraph4j 的核心概念:State、Node、Edge、Checkpointer
DalangGraph4j 里最核心的几个概念,研究过后会发现和 LangGraph(Python 版)几乎一一对应:
- State(状态):整个工作流的全局数据对象,相当于图的“内存”。每个节点执行前拿到当前状态,执行后返回更新后的状态。状态结构通常包含消息历史、LLM 输出、中间结果、错误信息等。
- Node(节点):一个节点就是一个执行单元,输入是当前状态,输出是新状态。节点内部可以调用 Spring AI 的 ChatClient,可以执行 SQL,也可以调用任意外部 API。
- Edge(边):决定节点之间的连接关系。LangGraph4j 支持普通边、条件边。条件边就是“根据当前状态里的某个字段决定下一步去哪”,这是实现 ReAct 循环的关键。
- Checkpointer(检查点):这是 LangGraph4j 最被低估的功能。它能把工作流当前状态持久化,支持断点续跑。比如一个多步骤任务执行到第三步发现模型抽风返回了错误格式,修复提示词后可以直接从第三步重跑,不需要重新执行前两步。
这四个概念组合起来,能做的事情非常多。比如你可以在图里加一个HumanInTheLoopNode,执行到某个节点时主动停下来,等人工确认后再继续 —— 这在金融、法务等强审核场景里几乎是必备能力。
3.4 断点续跑与检查点机制:LLM 场景下的“后悔药”
我为什么单独把 Checkpointer 拿出来说?因为做过 AI 应用的人都知道,LLM 的不确定性导致流程失败是常态,不是意外。一次 SQL 生成错了、一次 JSON 解析失败了、供应商 API 超时了,都可能导致整个工作流中断。
没有检查点机制的时候,重跑只能从头开始,不仅浪费时间,还可能因为历史消息已经发生变化,导致这次重跑的结果和上次不一样。
有了检查点机制,可以做到“从失败节点恢复”。LangGraph4j 的GraphState可以传入一个持久化实现(比如内存版本用于测试,后续可以接 Redis 或数据库),工作流执行过程中定期把状态存下来。中断后,通过状态恢复,上一个节点结束时的完整上下文就回来了。
这对生产环境的意义不亚于数据库的事务日志。我现在做任何 LangGraph4j 的图,第一件事就是先想清楚状态里要放什么、哪些阶段需要打检查点——这不是过早设计,而是对不确定性的敬畏。
4. Spring AI + LangGraph4j 的组合架构与实战视角
4.1 一张图看懂两者分工
说句直接的话,很多人搞不明白“为什么是 Spring AI + LangGraph4j,而不是 LangChain4j 或者只用一个”,是因为没想清楚分层。
我习惯把 AI 应用分成三层:
| 层次 | 职责 | 对应组件 |
|---|---|---|
| 接入层 | 与大模型对话、向量化、RAG 检索、工具调用协议 | Spring AI |
| 流程层 | Agent 工作流编排、状态管理、分支跳转、断点恢复 | LangGraph4j |
| 业务层 | 跟公司现有系统交互、触发订单/告警/审批等具体动作 | 自己写的 Service,图节点里调用 |
Spring AI 负责“怎么跟模型说话”,LangGraph4j 负责“什么时候跟模型说话、说完之后下一步干什么”。这两者没有重叠,也不冲突。
4.2 NL2SQL:一个典型的组合实战
热搜词里有个“spring ai alibaba nl2sql”,正好可以用来当作例子拆解。NL2SQL(自然语言转 SQL)是企业内部数据分析最常见的需求,但做成产品很难,难在 SQL 不是一次生成就一定能用的:
- 用户描述的表名、字段名和专业术语,往往跟数据库里的真实定义对不上;
- 生成的 SQL 可能有语法错误;
- SQL 能执行,但业务逻辑不对(比如时间范围、聚合口径搞错了);
- 查询结果太大,需要做 truncate 或摘要。
用 Spring AI + LangGraph4j 组合实现,流程层会非常清晰:
read_schema节点:读取相关表结构,塞进上下文(可能需要多轮,先定位哪些表相关);write_sql节点:用ChatClient生成候选 SQL;explain_sql节点:把 SQL 翻译成自然语言描述,展示给用户确认(Human-in-the-loop 的雏形);execute_sql节点:执行 SQL,如果报错则记录错误信息;repair_sql节点:如果上一步失败,把报错信息追加到状态里,回到write_sql生成修正版;answer节点:基于查询结果生成最终回答。
注意第 5 步,这是一个标准的条件边 + 循环结构。没有图编排,你要么写一个 while 循环设重试上限,要么放弃自动修正,直接报错。有了 LangGraph4j,把“重试上限 3 次”作为一个条件边判断加进去就行:超过 3 次就走give_up分支进入人工兜底。这种精细的控制流,手写代码做到后面基本都是面条代码。
4.3 RAG 和 Agent 的顺滑衔接
再来看另一个热搜词,“spring ai 2.0 rag实例”。RAG 单独用 Spring AI 就能做,但很多场景下,RAG 不是终点,而是 Agent 的一个工具。
比如企业内部知识库问答:用户问“报销政策是什么”,直接检索相关文档回答就行。但如果用户问“按报销政策计算一下我这个项目能报多少”,系统就需要先检索政策文档,从文档里抽取规则,再拿规则去执行一个计算工具——这里的流程是:“检索 -> 理解 -> 工具调用 -> 再总结”。
在 LangGraph4j 的图里,retrieve_docs节点内部调 Spring AI 的VectorStore做相似度检索,然后把检索结果写入状态;下一步extract_rules节点用 ChatClient 抽取决策点;再下一步call_calculator节点调用业务服务。每一步的产物都留在状态里,全程可追溯。
Spring AI 的QuestionAnswerAdvisor在处理“单轮检索问答”时很顺手,但一旦出现“先检索再判断是否需要额外检索”的动态流程,固定 Advisor 就限制住了。这时候把检索能力拆成节点,交给 LangGraph4j 编排,是更合理的设计。
4.4 开发调试:LangGraph4j Studio 和中文文档生态
很多人低估了调试工具在选型中的权重。LLM 应用最大的难点是“不确定性”,如果调试工具只能打印日志,效率会非常低。
LangGraph4j 配套的可视化调试工具,可以直观看到图的执行轨迹:哪个节点执行了、状态在哪个时刻长什么样、条件边为什么选择了这条路径。这种图结构可视化的调试体验,比看日志猜流程高出好几个档次。
而“langgraph4j官方中文文档”这个热搜词说明国内用的人已经不少,社区正在快速补齐中文资料。开源项目的生命力,很多时候不是看 Star 数,而是看文档质量和社区活跃度。我在项目中遇到问题,无论是 GitHub Issues 还是社区问答,都能找到对应解决方案,这比用一个小众框架然后踩坑全靠自己扛要踏实得多。
4.5 怎么和 Python 生态交互
还有一个热搜词也值得说说:“spring ai如何和python交互”。这其实是很多 Java 团队会纠结的问题,既然 Python 生态的 AI 库那么全,要不要在关键 AI 逻辑上把 Python 服务引进来?
我的建议是:先分清“调用边界”和“进程边界”。Spring AI + LangGraph4j 可以在 JVM 内完成 90% 的 LLM 应用逻辑。真正需要 Python 的场景,通常是:
- 跑一些 Java 没有好实现的数据科学算法,比如自定义 embedding 模型微调、深度排序模型;
- 复用已有的 Python 机器学习服务。
这种情况下,没必要让 Java 去“学 Python”,直接在 LangGraph4j 的某个节点里通过 HTTP/gRPC 调用 Python 服务即可。节点本身就是服务编排单元,天然支持跨进程调用。Spring AI 能管大模型,LangGraph4j 能管流程,两者组合之后,边界自然清晰——Python 只是流程里一个“被调用的人”,不需要再引入一个流程引擎来调度 Java 和 Python。
5. 落地过程中绕不开的坑
5.1 状态对象的序列化设计要提前想清楚
LangGraph4j 的状态是整个图的“全局变量”,如果后续要接 Redis/数据库做持久化,或者要做断点恢复,状态的序列化方案必须提前设计。
我踩过的一个坑是:状态里直接放了实体类对象,这些对象内部又引用了大量服务类,一序列化就报错。后来学乖了,状态对象里只放“可序列化的纯数据”,例如:
public class DataAnalysisState { private String userQuery; private List<ChatMessage> messages; // 对话历史 private String sql; private String sqlError; private Object queryResult; // SQL 查询结果(转成 JSON 字符串更稳妥) private int repairAttempts; private boolean finished; // getter/setter 省略 }简单数据组成的扁平结构,比嵌套对象难出问题得多。宁可在节点里做一个转换,也别让状态耦合业务实体。
5.2 每个节点内部要自包含,不要隐式共享状态
LangGraph4j 的节点直接可以用 lambda,很多人图省事,把业务逻辑都塞在 lambda 里,节点越写越长,图结构渐渐看不出来——这等于把代码面条症换了个姿势复发。
我后来强行立了一个规矩:每个节点一个类,输入输出都是 State 对象,内部任何数据都通过 State 传递,单测直接 new 一个 State 就能跑,不依赖 Spring 上下文。这样做的好处是:节点都是无状态的,要测某个分支逻辑直接构造对应 State,跑完断言 State 的变化,测试写起来非常舒服。
5.3 Spring AI 版本升级期的不稳定,要用适配层兜底
Spring AI 目前还处于快速迭代期,2.x 版本之间 API 变化不能说翻天覆地,但确实存在。我就遇到过升级小版本后ChatClient的 builder 方法名变化,导致全项目编译失败。
应对办法是:不要在所有业务代码里直接散用 Spring AI 的 API,而是自己包一层薄薄的适配层。
比如有个AiAssistant接口:
public interface AiAssistant { String chat(String userMessage); String chatWithContext(String userMessage, List<KeyValue> context); String callTool(String userMessage, String toolName, Object params); }实现类内部用 Spring AI 的ChatClient,业务方只知道AiAssistant。将来 Spring AI API 再怎么变,改适配层一个类就够了。这不是过度设计,而是面对快速演进框架时的基本生存技巧。
5.4 别一开始就想着“全自动”,先做一条人工兜底链路
LangGraph4j 给了很强的流程控制能力,但 AI 应用的流程控制越强,越容易让人产生“什么都能自动跑”的错觉。实际生产环境里,我认为最稳妥的结构是“自动执行 + 关键节点人工确认”,而不是“全自动一把梭”。
拿我们做的数据分析助手举例:SQL 生成后先展示给用户“我准备执行这样一段 SQL:SELECT ...”,用户点击确认才真正去跑数。看似多了一步,实际上换来了两个好处:第一,SQL 写错了用户自己会纠正,避免在错误的方向上浪费多次补救调用;第二,出了责任问题,有用户确认的日志,追责清晰。
在 LangGraph4j 里实现这一步,只需要在generate_sql和execute_sql中间加一个confirm节点,状态里加一个confirmed字段,条件边判断通过了再继续,不通过就改写 SQL 或者结束。就这么简单,但这个设计能救回大量生产事故。
5.5 流式输出与图编排的配合
还有一点容易被忽略:LangGraph4j 默认的invoke方法是拿到全量结果后返回,但真实的交互场景,用户期望的是像 ChatGPT 那样一个字一个字往外蹦。这就涉及流式输出和图编排怎么配合的问题。
我的经验是:让图负责“决策”,流式输出负责“呈现”。可以在图的节点内部调用 ChatClient 的流式方法,把 Progressive Delta 一个个推给前端;或者先在静默模式下跑完整个图,只把最后一步生成报告的过程做流式输出。第二种实现更简单,用户体验上也不会有太大差别,因为在用户眼里,前面跑 SQL、调工具都不需要逐字展示,真正需要流式的是最后那段总结。
写在最后:我对这个组合的理解
这段时间接触 Spring AI 和 LangGraph4j,最想表达的感受是:Java 生态终于有了一个“正经”做 LLM 应用的方式。
不是说之前用 Python 不行,而是对于绝大多数有 Java 技术债、有 Spring 基础设施的企业来说,让 AI 应用长在既有体系里,比另起炉灶再打通两套生态要靠谱得多。Spring AI 管住了“如何与模型对话”这一层,有效降低了各家模型 SDK 的切换成本;LangGraph4j 管住了“多步骤任务的执行顺序与状态流转”这一层,让 Agent 应用从“一堆 if-else”变成了清晰可维护的图。
如果现在有人问我该怎么选型,我的建议非常直接:单轮问答、RAG 检索这类相对固定的功能,直接用 Spring AI 就够了,别上 LangGraph4j 增加复杂度;一旦你要做的是多步骤、有判断、需要工具调用和动态重试的 Agent 类应用,再考虑引入 LangGraph4j。
最后分享一个我们团队固定下来的启动姿势:先花一两天用 Spring AI 跑通一个最小可用的“对话 + RAG + 工具调用”Demo,感受一下框架提供的抽象;再用 LangGraph4j 把同样的功能拆成三到五个节点,感受一下从“管道”到“状态图”的变化;然后你再回头看需求文档,自然就知道哪一步该用哪个框架了。这种体感上的领悟,远比我在这里干讲十条理由管用。