
1. 为什么我最终选了子图模式来做 AI 客服1.1 从单智能体到多智能体的真实痛点去年下半年我接了一个 AI 客服系统的项目客户是做 SaaS 的每天大概有三千到五千条用户咨询内容涵盖账单问题、功能使用、账号异常、退款流程这几大类。一开始我的思路很直接搞一个大而全的 Agent把所有工具都挂上去让它自己判断该调哪个。结果上线第一周就翻车了。问题出在几个地方。第一工具数量一多模型选错工具的概率直线上升尤其是“查账单”和“查订单”这两个工具参数结构很像模型经常混着调。第二System Prompt 越写越长为了覆盖所有场景我塞了将近两千字的指令结果模型开始“顾此失彼”处理退款的时候忘了要验证用户身份。第三调试极其痛苦一个请求进来我根本不知道它中间走了哪条路径日志打出来是一坨排查一个问题要花半小时。后来我意识到这不是 Prompt 工程能解决的问题这是架构问题。一个 Agent 承担了太多职责就像一个客服同时要懂技术、懂财务、懂法务还要记住所有流程人都会崩溃何况模型。于是我开始转向多智能体架构让每个 Agent 只负责一个垂直领域各司其职。1.2 为什么是 LangGraph 的子图模式而不是别的方案多智能体的实现方式有好几种。我试过用纯 LangChain 的 AgentExecutor 做路由也试过自己写调度逻辑但都有各自的坑。AgentExecutor 的问题是它把“决策”和“执行”耦合在一起你很难在中间插入人工审核或者条件分支。自己写调度的话状态管理会变成噩梦尤其是多个 Agent 需要共享上下文的时候。LangGraph 的子图模式吸引我的点在于它把每个子智能体封装成一个独立的 StateGraph有自己的状态定义、自己的节点和边然后通过父图来编排这些子图。这带来几个实际好处。一是隔离性子图内部的状态变化不会污染父图每个 Agent 只管自己那一亩三分地。二是可复用比如“身份验证”这个子图账单 Agent 要用退款 Agent 也要用写一次就能到处挂。三是可观测每个子图的执行路径在 LangGraph 的可视化里是分开的出问题一眼就能定位到是哪个环节。打个比方父图就像公司的前台负责判断用户来找谁子图就是各个部门的专员前台把人领到对应部门后专员按自己的流程办事办完把结果交回前台。这个模型和真实客服团队的组织结构是对应的所以落地的时候团队里没人觉得别扭。1.3 这套系统最终长什么样最终的系统架构是这样的用户消息进来先经过一个“意图识别”节点判断属于账单、功能、账号、退款中的哪一类然后路由到对应的子图。每个子图内部有自己的多轮对话逻辑比如退款子图会先验证身份再确认订单再判断是否符合退款条件最后生成退款单。子图执行完毕后结果汇总回父图由父图统一生成回复。整个系统跑下来意图识别准确率从原来的 78% 提升到了 94%工具调用错误率从 12% 降到了 2% 以下。更重要的是新增一个业务场景比如“发票申请”只需要写一个新的子图挂上去不用动现有逻辑迭代速度完全不一样了。下面我把这套系统的设计思路、核心实现、踩过的坑完整地拆一遍。如果你也在做多智能体或者 AI 客服希望能帮你少走点弯路。2. 整体架构设计与核心思路拆解2.1 父图与子图的职责边界怎么划设计多智能体系统第一个要回答的问题就是什么该放在父图什么该放在子图。我的划分原则是——父图管“路由和汇总”子图管“业务和状态”。父图只做三件事接收用户输入、判断意图、把子图的输出整合成最终回复。它不碰任何业务逻辑不知道退款要验证什么也不知道账单怎么查。这样做的好处是父图极其稳定业务怎么变它都不用改。子图则是一个完整的业务闭环。以退款子图为例它内部有自己的状态机等待身份验证 → 验证中 → 验证通过 → 确认订单 → 判断退款资格 → 生成退款单 → 完成。每个状态之间的流转条件都定义在子图内部父图完全不用关心。这里有个容易踩的坑很多人会把“身份验证”放在父图觉得这是通用能力。我一开始也这么干结果发现账单子图需要验证手机号退款子图需要验证手机号加订单号账号子图只需要验证邮箱验证逻辑根本不一样。放在父图反而要写一堆 if-else不如让每个子图自己管需要复用的部分抽成一个独立的验证子图谁需要谁调用。2.2 状态设计父子图之间传什么、怎么传LangGraph 的状态传递是这套架构里最容易出问题的地方。父图和子图各有各的 State它们之间的数据交换必须通过明确定义的字段。我的做法是定义一个ParentState里面包含几个关键字段messages对话历史、intent识别出的意图、user_id用户标识、sub_result子图返回的结果、final_response最终回复。子图有自己的SubState包含messages、user_id、以及业务相关的字段比如order_id、verified、refund_amount。关键在于子图只能读取父图传给它的字段不能直接访问父图的完整状态。这是隔离性的体现。具体实现上父图在路由到子图时会把需要的字段打包传给子图子图执行完后把结果写回sub_result父图再读取这个字段生成回复。这里有个细节值得说messages字段我用的是 LangGraph 的add_messagesreducer这样父子图之间的对话历史能自动合并不用手动拼接。但要注意如果子图内部产生了大量的中间消息比如工具调用的详细日志这些不应该全部回传给父图否则父图的上下文会爆炸。我的做法是子图只把“对用户可见的消息”写回父图中间过程留在子图内部。2.3 路由策略意图识别节点怎么设计意图识别是整个系统的入口它的准确性直接决定了后续所有环节的质量。我试过三种方案最后选了“LLM 分类 规则兜底”的混合策略。纯 LLM 分类的问题是偶尔会“幻觉”把退款意图识别成账单意图。纯规则的问题是覆盖不全用户说话的方式千奇百怪。混合策略是先用 LLM 做分类输出一个置信度如果置信度低于阈值我设的是 0.7就走规则匹配兜底规则匹配还搞不定就转人工。意图识别的 Prompt 我改了很多版最后稳定下来的结构是先给模型列出所有意图类别和每个类别的典型例子然后要求它输出 JSON 格式的结果包含intent和confidence两个字段。这里有个技巧——例子要给“边界案例”比如“我上个月的钱怎么还没退”这种既像账单又像退款给模型一个明确的归类它后面遇到类似的就不会犹豫。路由节点本身很简单就是一个条件边根据intent字段决定走哪个子图。但要注意LangGraph 的条件边需要返回一个字符串这个字符串要和你注册的节点名完全一致大小写都不能错我在这上面浪费过半小时。2.4 为什么不用 Supervisor 模式而用子图模式多智能体有个常见的模式叫 Supervisor就是一个“主管”Agent 来调度其他 Agent。我试过但放弃了。原因是 Supervisor 模式下主管 Agent 需要了解每个下属 Agent 的能力边界这又回到了“一个 Agent 知道太多”的老问题。而且 Supervisor 的调度是动态的每次都要调一次 LLM 来决定下一步延迟高、成本也高。子图模式本质上是“静态路由 动态执行”。路由是提前定义好的意图识别一次搞定执行是动态的子图内部可以多轮对话、条件分支。这样既保证了调度的确定性又保留了业务处理的灵活性。对于客服这种场景用户意图相对明确静态路由完全够用没必要上 Supervisor 增加复杂度。3. 核心细节解析与实操要点3.1 子图的独立状态定义与 reducer 选择定义子图状态的时候reducer 的选择很关键。LangGraph 默认的行为是“覆盖”也就是新值直接替换旧值。但对话历史这种字段你需要的是“追加”所以要用add_messages。我踩过的坑是一开始所有字段都用默认 reducer结果子图内部多轮对话的时候历史消息一直被覆盖模型永远只看到最后一条完全没法做多轮。正确的做法是区分对待。messages用add_messagesverified这种布尔标志用默认覆盖因为只需要最新值collected_info这种字典用自定义的合并 reducer新字段合并进旧字典而不是替换。自定义 reducer 的写法很简单就是一个函数接收旧值和新值返回合并后的值。比如def merge_dict(old: dict, new: dict) - dict: return {**old, **new}然后在 State 定义里用Annotated[dict, merge_dict]标注。这个技巧在处理“逐步收集用户信息”的场景特别有用比如退款流程里先收集订单号、再收集退款原因用合并 reducer 就不用每次手动把旧字段带上。3.2 父子图状态传递的三种方式与选型父子图之间传数据我总结下来有三种方式各有适用场景。第一种是通过父图状态字段传递。父图在路由前把数据写进 State子图读取。这种方式适合传递“路由决策相关”的数据比如user_id、intent。优点是简单直接缺点是父子图的状态定义要耦合子图得知道父图有哪些字段。第二种是通过子图调用时的参数传递。LangGraph 的add_node支持给节点传额外的参数你可以在挂载子图的时候把需要的配置传进去。这种方式适合传递“配置类”数据比如 API key、超时时间。优点是子图不需要知道父图的状态结构缺点是只能传静态数据不能传运行时产生的值。第三种是通过共享的外部存储。比如用一个 Redis 或者内存字典父子图都通过user_id去读写。这种方式适合传递“大量数据”或者“需要跨请求持久化”的数据。优点是解耦彻底缺点是要管理外部依赖而且调试的时候数据流不直观。我的选择是路由相关的小数据用第一种配置用第二种会话级的持久化数据用第三种。大部分场景第一种就够了不要过度设计。3.3 工具调用的封装与错误处理每个子图内部都会挂一些工具比如查订单、查账单、提交退款。工具调用的封装有两个要点参数校验和错误兜底。参数校验我是在工具函数内部做的用 Pydantic 定义参数模型LangGraph 会自动校验。如果校验失败会抛异常这个异常需要在子图里捕获然后引导用户补充信息。比如用户说“我要退款”但没给订单号工具调用会因为缺少order_id失败这时候子图应该回复“请提供您的订单号”而不是直接报错。错误兜底是指工具执行本身可能失败比如查订单的 API 超时了。我的做法是给每个工具包一层重试逻辑重试两次还失败就返回一个友好的错误信息同时记录日志。这里要注意不要让模型看到原始的技术错误信息否则它可能会把“Connection timeout”这种话直接说给用户听体验很差。还有一个细节工具调用的结果要格式化后再喂给模型。比如查订单返回的是一个 JSON直接丢给模型它可能理解不了我会先转成自然语言描述比如“订单号 12345金额 299 元状态已支付下单时间 3 月 15 日”这样模型处理起来准确得多。3.4 多轮对话中的状态保持与超时处理客服场景几乎都是多轮对话用户不会一句话把需求说全。这就要求子图能记住之前的对话内容并且在用户长时间不回复的时候能优雅地结束会话。状态保持靠的是messages字段的累积这个前面说过了。但有个坑是上下文不能无限增长。用户和退款子图聊了二十轮messages里堆了四十条消息模型的 token 消耗会很大而且早期的信息可能已经不重要了。我的做法是设置一个窗口只保留最近十轮对话更早的用摘要代替。摘要用一个轻量的 LLM 生成成本很低。超时处理是另一个容易被忽略的点。用户可能聊到一半就走了这时候子图的状态还挂在内存里。我设置了一个 30 分钟的超时超时后自动清理状态并且如果用户再回来重新走一遍意图识别。LangGraph 本身不提供超时机制这个要自己在状态里加一个last_active时间戳用一个后台任务定期扫描清理。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的是 Python 3.11LangGraph 的版本是 0.2.xLangChain 是 0.3.x。这两个版本搭配比较稳定再新的版本有些 API 变了网上的教程对不上容易踩坑。pip install langgraph0.2.60 langchain0.3.7 langchain-openai0.2.8 pip install fastapi uvicorn pydantic redis这里解释一下为什么选这些。LangGraph 0.2.x 是子图功能比较成熟的版本add_node挂载子图的 API 已经稳定了。LangChain 0.3.x 和它是配套的工具调用的接口没有大改。FastAPI 用来做 HTTP 服务Redis 用来做会话状态的持久化。模型我用的是 OpenAI 的 GPT-4o-mini客服场景对推理能力要求没那么高mini 版本够用成本还低。如果你要用别的模型LangChain 的ChatOpenAI换成对应的类就行接口是一样的。4.2 定义父图状态与意图识别节点先定义父图的状态。这里我用TypedDict来定义字段包括messages、intent、user_id、sub_result、final_response。from typing import TypedDict, Annotated from langgraph.graph import add_messages class ParentState(TypedDict): messages: Annotated[list, add_messages] intent: str user_id: str sub_result: dict final_response: str意图识别节点的实现核心是一个 Prompt 加一次 LLM 调用。Prompt 里我列了四个意图类别每个类别给了五个例子特别强调了边界案例的归类。输出要求是 JSON包含intent和confidence。INTENT_PROMPT 你是一个客服意图识别助手。请判断用户消息属于以下哪个类别 - billing: 账单查询、费用疑问、扣费异常 - feature: 功能使用、操作咨询、报错排查 - account: 账号登录、密码重置、绑定解绑 - refund: 退款申请、退款进度、退款条件 边界案例说明 - 我上个月的钱怎么还没退 → refund涉及退款进度 - 这个月扣了我两次钱 → billing涉及扣费异常 请输出 JSON{intent: ..., confidence: 0.0-1.0} 用户消息{message} 节点函数里我加了置信度判断。低于 0.7 的时候走一个关键词匹配的兜底逻辑。关键词匹配很简单就是维护一个词表比如“退款”“退钱”“退费”归到 refund“密码”“登录”“账号”归到 account。兜底还搞不定就返回unknown路由到人工客服子图。4.3 构建退款子图的完整状态机退款子图是这套系统里最复杂的我拿它当例子详细说。它的状态定义如下class RefundState(TypedDict): messages: Annotated[list, add_messages] user_id: str order_id: str verified: bool refund_reason: str refund_eligible: bool refund_amount: float collected_info: Annotated[dict, merge_dict]节点有六个verify_identity、collect_order、check_eligibility、collect_reason、create_refund、generate_response。边的话verify_identity之后根据verified决定是继续还是要求重新验证check_eligibility之后根据refund_eligible决定是继续还是告知不符合条件。verify_identity节点会调用一个验证工具传入user_id和用户提供的验证信息。验证通过就把verified设为 True。这里有个细节验证信息可能是手机号、邮箱、订单号中的任意一种我在工具里做了兼容只要有一种匹配就算通过。collect_order节点负责从对话中提取订单号。我用了一个简单的正则加 LLM 提取的组合正则先扫一遍扫不到再让 LLM 从对话历史里找。提取到之后写入order_id。check_eligibility节点调用退款资格查询工具返回是否符合条件以及可退金额。这个工具内部有一堆业务规则比如“超过 30 天的订单不可退”“已使用超过 50% 的服务不可退”这些规则写在工具里不暴露给模型。4.4 父子图挂载与条件路由配置子图构建好之后用add_node挂到父图上。这里的关键是给子图起一个名字这个名字就是路由时的目标节点名。from langgraph.graph import StateGraph, END parent StateGraph(ParentState) parent.add_node(intent_router, intent_router_node) parent.add_node(billing_agent, billing_subgraph) parent.add_node(feature_agent, feature_subgraph) parent.add_node(account_agent, account_subgraph) parent.add_node(refund_agent, refund_subgraph) parent.add_node(response_generator, response_node)条件路由用add_conditional_edges传入一个函数函数返回目标节点名。def route_by_intent(state: ParentState) - str: intent state.get(intent, unknown) mapping { billing: billing_agent, feature: feature_agent, account: account_agent, refund: refund_agent, } return mapping.get(intent, response_generator) parent.add_conditional_edges(intent_router, route_by_intent)每个子图执行完后都连到response_generator由它统一生成最终回复。response_generator会读取sub_result结合对话历史生成一段自然的回复文本。4.5 会话持久化与 FastAPI 接口封装状态持久化我用的是 LangGraph 的 checkpointer配合 Redis 做存储。这样即使用户中途断开下次回来还能接着之前的对话。from langgraph.checkpoint.redis import RedisSaver checkpointer RedisSaver.from_conn_string(redis://localhost:6379) app_graph parent.compile(checkpointercheckpointer)FastAPI 的接口很简单一个 POST 端点接收用户消息和session_id调用app_graph.invoke传入config{configurable: {thread_id: session_id}}这样同一个session_id的对话会自动关联到同一个状态。app.post(/chat) async def chat(req: ChatRequest): config {configurable: {thread_id: req.session_id}} result app_graph.invoke( {messages: [HumanMessage(contentreq.message)], user_id: req.user_id}, configconfig, ) return {reply: result[final_response]}这里有个性能上的注意点invoke是同步的如果并发量高要用ainvoke异步版本配合 FastAPI 的 async 端点。我实测下来异步版本在 100 并发下的 P99 延迟比同步版本低 40% 左右。5. 常见问题与排查技巧实录5.1 子图状态不更新或更新丢失这是最常见的问题表现是子图内部明明修改了某个字段但父图读到的还是旧值。原因通常是子图的输出没有正确写回父图的状态。LangGraph 的子图在作为节点执行时它的返回值会被合并到父图状态里。但合并的规则取决于父图该字段的 reducer。如果父图sub_result字段用的是默认 reducer覆盖子图返回{sub_result: {...}}就能正常写入。但如果子图返回的字段名和父图对不上就会被忽略。排查方法在子图的最后一个节点里打印一下返回值确认字段名和父图定义的一致。我踩过的坑是子图返回了result但父图字段叫sub_result白白调试了一小时。5.2 意图识别准确率上不去如果意图识别经常出错先检查 Prompt 里的例子够不够“边界”。我一开始只给了典型例子模型遇到模糊表达就懵。后来加了二十多个边界案例准确率从 82% 提到了 91%。另一个技巧是给模型“思考空间”。不要让它直接输出分类结果而是先让它用一句话解释为什么这么分类再输出 JSON。这个“解释”步骤能显著提升准确率因为模型在解释的过程中会自我校验。实测下来加了这一步准确率又提了 3 个百分点。如果还是不行考虑上 few-shot 的动态检索。把历史对话里相似的案例检索出来作为例子塞进 Prompt。这个方案成本高一些但对长尾意图效果很好。5.3 工具调用参数缺失或格式错误工具调用失败十有八九是参数问题。LangGraph 的工具调用依赖模型输出的 JSON模型有时候会漏字段或者字段类型不对。我的解决方案是三层防护。第一层在工具定义里用 Pydantic 严格定义参数类型模型输出不符合就直接报错不会带着错误参数去执行。第二层在子图里捕获参数错误引导用户补充信息而不是直接失败。第三层给模型一个“参数检查”的提示在调用工具前先确认所有必填参数都有了。还有一个实用技巧把工具的参数说明写清楚包括格式要求。比如order_id要说明“格式为 ORD 开头的 12 位字符串”模型看到这个说明输出的格式就规范多了。5.4 多轮对话中上下文丢失上下文丢失通常是因为messages字段的 reducer 配置错了或者消息没有正确传递。检查两个地方一是父图和子图的messages字段是否都用了add_messages二是子图返回时是否把新消息带上了。另一个可能的原因是消息窗口设置得太小。我一开始设的是保留最近 5 轮结果用户聊到第 6 轮的时候第 1 轮提供的关键信息比如订单号丢了。后来改成 10 轮并且对更早的消息做摘要问题就解决了。摘要的实现很简单用一个便宜的模型比如 GPT-4o-mini把早期对话压缩成一段话作为一条 SystemMessage 放在最前面。这样既保留了关键信息又控制了 token 消耗。5.5 常见问题速查表问题现象可能原因排查方法解决方案子图状态不更新字段名不匹配打印子图返回值统一父子图字段命名意图识别不准Prompt 例子不足检查边界案例补充边界案例加解释步骤工具调用失败参数缺失或格式错查看工具调用日志Pydantic 校验 引导补充上下文丢失reducer 配置错检查 messages 字段用 add_messages设窗口响应延迟高同步调用阻塞看 P99 延迟改异步 ainvoke会话状态不持久checkpointer 没配检查 Redis 连接配置 RedisSaver5.6 几个我踩过的坑和独家技巧第一个坑是子图嵌套子图。我一开始想把“身份验证”做成一个子图然后让退款子图去调用它。LangGraph 支持子图嵌套但状态传递会变得很复杂调试难度翻倍。后来我放弃了嵌套把身份验证做成一个普通节点在需要它的子图里直接调用函数。简单直接效果一样。第二个坑是条件边的返回值。LangGraph 的条件边函数必须返回一个字符串这个字符串要和节点名完全一致。我有一次返回了refund但节点名是refund_agent结果直接报错而且错误信息不直观找了半天才发现。第三个技巧是给每个子图加一个“兜底节点”。子图内部如果所有条件分支都没命中会走到一个默认节点这个节点负责生成一个“抱歉我没理解”的回复并把状态标记为需要人工介入。这样不会出现“卡死”的情况。第四个技巧是用 LangGraph 的可视化做调试。app_graph.get_graph().draw_mermaid()能生成流程图虽然我这里不画图但你在本地调试的时候可以生成出来看能直观地看到状态流转路径排查路由问题特别快。6. 性能优化与扩展方向6.1 延迟优化的几个实操手段客服系统对延迟很敏感用户等超过 3 秒就会不耐烦。我做了几件事来压延迟。第一意图识别和子图执行并行化。意图识别只需要看用户最新一条消息不需要等完整的对话历史加载完。我把这两步拆开意图识别先跑跑的同时加载历史等意图出来的时候历史也加载好了。第二工具调用加缓存。查订单、查账单这些操作同一个用户短时间内可能重复调用我在工具层加了一个 5 分钟的缓存命中率大概 30%省了不少时间。第三模型选择分级。意图识别用 mini 模型子图内的复杂推理用标准模型生成回复用 mini 模型。这样在保证质量的前提下整体成本降了 60%延迟也降了。6.2 新增业务子图的标准流程这套架构最大的好处就是扩展方便。新增一个业务场景比如“发票申请”标准流程是四步。第一步定义子图状态。参考退款子图把业务相关的字段列出来比如invoice_title、tax_number、invoice_amount。第二步实现节点和边。发票申请的流程比退款简单大概三个节点收集开票信息、校验信息、生成发票申请单。第三步挂载到父图。在父图add_node加一行在意图识别的 Prompt 里加一个类别在路由映射里加一条。第四步测试。用 LangGraph 的可视化确认路由正确然后用几个典型用例跑一遍确认状态传递没问题。整个过程熟练的话半天就能搞定不用动任何现有代码。这就是子图模式的价值。6.3 从单机到分布式的演进思路现在这套系统是单机跑的状态存在 Redis 里。如果并发量继续涨需要考虑分布式。LangGraph 本身支持分布式部署多个实例共享同一个 Redis checkpointer 就行。但分布式会带来一个新问题同一个session_id的请求可能被路由到不同实例如果两个请求同时到达会有状态竞争。解决方案是用 Redis 的分布式锁按session_id加锁保证同一会话的请求串行处理。这个锁的粒度要控制好太粗会影响并发太细会有竞争按会话加锁是比较合适的。再往后如果量特别大可以考虑把不同子图部署成独立的服务父图通过 HTTP 调用子图服务。这样每个子图可以独立扩缩容退款子图压力大就多部署几个实例。不过这是后话了大部分场景单机加 Redis 就够了。6.4 监控与可观测性建设上线之后监控比开发还重要。我埋了几个关键指标意图识别的准确率人工抽检、子图的平均执行时长、工具调用的成功率、会话的完成率用户问题是否被解决。这些指标我用 Prometheus 收集Grafana 展示。LangGraph 的每个节点执行都会产生 trace我把这些 trace 接到 LangSmith 上出问题的时候能直接看到是哪个节点、哪次调用出的错。还有一个实用的监控是异常会话告警。如果某个会话的轮次超过 15 轮还没结束大概率是卡住了系统会自动告警人工介入看一下。这个机制帮我发现了好几个 Prompt 的边界问题。7. 一些个人体会这套系统从设计到上线大概花了六周其中前两周基本都在试错试了 Supervisor 模式、试了单 Agent 大 Prompt、试了纯规则路由最后才定下来子图模式。回过头看子图模式最大的价值不是技术上的先进而是它符合人类组织协作的直觉。每个 Agent 像一个专员有自己的职责和流程父图像前台负责调度这种结构团队里任何人都能理解沟通成本极低。如果让我给正在做类似系统的朋友一个建议那就是先把业务流程图画出来再决定怎么拆子图。不要一上来就想技术架构先想清楚用户的问题有哪几类每类的处理流程是什么哪些步骤是共用的。画完流程图子图的边界自然就出来了。另外不要追求一步到位。我第一版只做了账单和退款两个子图跑通了之后再逐步加功能。每次加新子图都是一次验证架构的机会如果加得很痛苦说明架构有问题趁早调整。如果加得很顺说明方向对了继续往前走。最后分享一个小的工程习惯给每个子图写一个独立的测试脚本用几个典型用例跑一遍确认状态流转正确。这个习惯帮我省了很多联调的时间尤其是改了一个子图之后跑一遍测试就知道有没有影响到别的子图。