
1. 从context-mode这个词说起它到底在解决什么问题第一次看到context-mode这个说法很多人会以为是某个具体框架里的配置项或者某个库的开关参数。但如果你在工程一线待过几年尤其是在做AI应用、对话系统、或者任何需要记住上下文的项目时你会发现这个词其实指向一个非常朴素又非常要命的问题同一份数据在不同上下文里应该有不同的行为模式而大多数项目把这件事做砸了。举个最直观的例子。你做一个客服机器人用户说我要退货。这句话在订单刚下单五分钟的上下文里正确响应是您可以自助取消在订单已发货三天的上下文里正确响应是我帮您走退货流程在用户已经连续三次咨询同一个问题的上下文里正确响应可能是我直接帮您转人工。同样一句话上下文不同处理模式完全不同。如果你把所有逻辑都塞进一个巨大的 if-else 或者一个万能提示词里系统很快就会变成一团乱麻。context-mode 要做的就是把这层上下文决定行为模式的逻辑显式地抽出来变成一个可配置、可切换、可测试的独立层。它不是某个具体产品的名字而是一种架构思路——把上下文从隐式状态提升为显式模式。这个思路在对话系统、推荐引擎、工作流引擎、甚至前端状态管理里都反复出现只是叫法不同。我之所以想认真聊这个话题是因为过去两年我参与过三个不同规模的对话类项目每一个都在上下文管理上栽过跟头。第一个项目把上下文全塞进 session结果并发一上来就串数据第二个项目用提示词硬编码上下文规则改一个场景要动整个 prompt回归测试根本跑不动第三个项目终于学乖了把 context-mode 单独抽成一层后面迭代速度直接翻倍。这篇文章就把这套思路完整拆开讲包括它为什么有效、怎么落地、以及我在实操中踩过的那些坑。适合读这篇的人正在做对话系统、智能助手、工作流引擎的工程师被上下文越写越乱困扰的开发者以及任何想理解状态与行为分离这个经典设计思想在 AI 时代怎么落地的人。不需要你有很深的 AI 背景但最好写过一点业务代码不然有些坑你感受不到痛。2. context-mode 的核心机制上下文如何决定行为模式2.1 把上下文和模式拆成两个独立维度很多人一上来就把 context-mode 理解成根据上下文切换模式这个理解对了一半但漏掉了最关键的一点上下文和模式必须是两个可以独立演化的维度。如果你把它们耦合在一起比如当用户等级是 VIP 且订单状态是已发货时进入模式 A那每加一个上下文条件模式定义就要改一次很快就爆炸了。正确的拆法是两层上下文层context负责收集和归一化当前场景的所有相关信息。它只回答现在是什么情况不回答该怎么办。比如用户身份、历史交互轮次、当前任务阶段、外部系统状态、时间窗口等等。模式层mode负责定义在这种情况下应该采用什么行为策略。它只回答该怎么办不关心这些情况是怎么来的。这两层之间用一个映射规则连接。映射规则可以简单到一张表也可以复杂到一棵决策树但它的职责非常单一给定上下文快照输出当前应该激活的模式。我习惯用一个生活化的类比来解释上下文就像医院的检查报告模式就像治疗方案映射规则就是诊断逻辑。检查报告只描述身体状况治疗方案只描述怎么治诊断逻辑负责把两者对上。你不会把检查报告直接写进治疗方案里也不会让治疗方案去重新做检查。这个边界一旦清晰整个系统的可维护性就上来了。2.2 模式本身应该包含哪些要素一个设计良好的 mode不是简单的一个字符串标签而应该是一个结构化的策略包。根据我的经验它至少包含四个要素要素作用举例触发条件什么上下文下激活该模式用户连续两轮表达不满行为策略该模式下系统怎么响应优先安抚主动提供补偿方案资源约束该模式的边界和限制最多追问一次超时转人工退出条件什么时候切出该模式用户情绪缓和或问题解决这四个要素缺一不可。我见过太多项目只定义了触发条件和行为策略结果模式一旦进入就出不来用户被卡在某个状态里反复被安抚体验极差。退出条件和触发条件同等重要这是我在第二个项目里用血泪换来的教训。另外要强调一点模式之间应该是平级且互斥的尽量不要设计嵌套模式。嵌套模式听起来很优雅实际上会让状态机变得极难调试。如果确实需要组合行为用模式 修饰器的方式而不是模式套模式。2.3 映射规则为什么不能写成巨型 if-else这是我最想强调的一点。很多团队一开始会用 if-else 或者 switch 来写映射规则前十个场景没问题到第三十个场景就开始失控。原因有三个第一条件组合爆炸。假设你有 5 个上下文维度每个维度 3 种取值理论组合就是 243 种。你不可能为每种组合手写一条规则只能靠嵌套判断而嵌套判断的可读性会随着层数指数下降。第二规则冲突无法检测。当规则多到一定程度你根本不知道哪条规则会先命中改一条可能悄悄影响另一条。这种 bug 极难复现因为依赖具体的上下文组合。第三无法做回归测试。if-else 逻辑藏在代码里你没法单独对映射规则做单元测试只能端到端跑成本极高。我的做法是把映射规则数据化用一张配置表或者一个规则文件来描述上下文特征 - 模式的映射代码只负责执行匹配。这样规则可以独立测试、独立版本管理甚至可以让非工程角色参与维护。规则引擎不需要多复杂一个按优先级排序的匹配列表就够了关键是规则和代码分离。# 映射规则的数据化示例伪代码 rules [ {priority: 100, when: {sentiment: negative, rounds: 2}, mode: soothe}, {priority: 90, when: {task_stage: confirm, user_level: vip}, mode: fast_track}, {priority: 50, when: {task_stage: confirm}, mode: standard}, {priority: 0, when: {}, mode: default}, # 兜底 ]注意最后那条兜底规则优先级为 0条件是空。任何映射系统都必须有兜底模式否则一旦上下文出现未预期组合系统就会没有行为可执行直接报错。这个兜底模式不需要多聪明但必须存在且稳定。3. 落地 context-mode 的完整实操路径3.1 第一步定义上下文快照的结构落地 context-mode 的第一件事不是写模式而是定义上下文快照context snapshot的结构。这一步做不好后面全是返工。上下文快照应该是一个扁平的、可序列化的结构字段命名要统一取值要归一化。我建议按来源给上下文字段分组但存储时保持扁平。常见的分组有会话维度当前轮次、会话时长、历史意图序列用户维度用户等级、历史行为标签、偏好设置任务维度当前任务阶段、已完成步骤、待确认项环境维度时间、渠道、设备类型字段取值一定要归一化。比如用户等级不要一会儿是数字 1/2/3一会儿是字符串 vip/normal统一成枚举。归一化的好处是映射规则写起来干净不会因为取值格式不一致导致匹配失败。我踩过这个坑上游系统传了个level: VIP规则里写的是level: vip大小写不一致结果 VIP 用户全走了普通模式线上跑了半天才发现。上下文快照还要考虑时效性。有些字段是实时计算的比如当前情绪有些是缓存的比如用户等级。快照生成时最好标注每个字段的新鲜度避免用过期的上下文做决策。这个细节在低频场景里无所谓在高频对话里非常关键。3.2 第二步设计模式的粒度模式粒度是个需要反复权衡的问题。太粗一个模式管太多场景内部又要写分支等于没抽离太细模式数量爆炸维护成本高。我的经验法则是一个模式对应一种用户可感知的行为差异。如果两种行为在用户看来是一样的就不该拆成两个模式。比如标准应答和礼貌应答如果只是措辞微调用户感知不到区别那就合并成一个模式用参数控制措辞。反过来如果两种行为在用户看来明显不同——比如主动追问和直接给答案——那即使触发条件很接近也应该拆成两个模式。因为它们的退出条件、资源约束都不一样混在一起会让状态机变脏。模式数量控制在多少合适根据我做过项目的经验初期 5 到 8 个模式足够覆盖 80% 的场景。超过 15 个模式就要警惕了大概率是粒度太细或者职责重叠。我见过一个项目定义了 40 多个模式结果维护的人自己都记不清哪个是哪个最后退化成了一个大模式加一堆 if。3.3 第三步写映射规则并做冲突检测规则写完之后一定要做冲突检测。所谓冲突就是同一个上下文快照可能命中多条规则且这些规则指向不同模式。如果规则是按优先级排序的冲突不一定是 bug高优先级覆盖低优先级是预期行为但你必须知道哪些规则之间存在覆盖关系。我的做法是写一个简单的检测脚本枚举上下文空间的所有组合或者采样跑一遍映射统计每个模式被命中的次数以及每条规则被覆盖的次数。如果某条规则从来没被命中过要么是条件写错了要么是被更高优先级的规则完全覆盖了两种情况都值得检查。# 冲突检测的简化思路 from itertools import product def detect_conflicts(rules, context_space): hit_count {r[mode]: 0 for r in rules} shadowed {i: 0 for i in range(len(rules))} for ctx in context_space: matched [i for i, r in enumerate(rules) if match(r[when], ctx)] if matched: hit_count[rules[matched[0]][mode]] 1 for i in matched[1:]: shadowed[i] 1 return hit_count, shadowed这个脚本不复杂但能帮你提前发现大量问题。规则系统的可观测性比规则本身写得多漂亮更重要。一个你无法观测的规则系统迟早会变成黑盒。3.4 第四步模式切换的时机与副作用处理模式切换不是免费的。每次切换都可能带来副作用上下文需要重新加载、用户可能感知到行为突变、正在进行的任务可能被打断。所以切换时机要谨慎。我的原则是在任务边界切换而不是在任务中途切换。比如用户正在填写退货表单这时候即使检测到情绪变化也不要立刻切模式而是等表单提交或取消后再切。中途切换会让用户觉得系统精神分裂。如果确实需要中途切换比如检测到严重不满必须立刻安抚那要做好状态保存和恢复。把当前任务的进度存下来切到安抚模式安抚完再切回来继续。这个切回来的逻辑很容易被忽略导致用户安抚完发现之前的进度没了体验更差。副作用处理还有一个细节模式切换要幂等。同一个上下文快照重复计算应该得到同一个模式不能因为计算了两次就切换了两次。这个在分布式环境下尤其重要因为快照可能被多个节点处理。4. 我在三个项目里踩过的 context-mode 坑4.1 坑一把上下文当全局变量用第一个项目我们图省事把上下文直接挂在全局 session 对象上任何模块都能读写。刚开始跑得挺顺直到并发测试。两个用户同时操作A 的上下文被 B 覆盖了B 的订单状态出现在了 A 的对话里。这种 bug 在测试环境几乎复现不了因为测试时通常只有一个用户。根因是上下文没有做隔离。上下文快照必须是每个请求独立的不能共享可变状态。修复方案是把上下文快照改成不可变对象每次请求生成一份新的模块之间通过参数传递而不是全局读取。改完之后并发问题消失但代码改动量不小因为之前到处都在直接读全局。这个坑的教训是上下文快照从第一天起就要设计成不可变、请求级隔离的。不要等到出问题再改改起来伤筋动骨。4.2 坑二模式定义散落在代码各处第二个项目模式的概念是有的但没有集中定义。A 模块里写了一段如果是 VIP 就走快速通道B 模块里又写了一段VIP 用户优先处理两段逻辑不一致导致 VIP 用户在不同环节体验割裂。根因是模式没有单一事实来源single source of truth。模式的定义、触发条件、行为策略必须集中在一处所有模块都从这里读取不能各自实现。我们后来把所有模式定义抽到一个配置文件里代码只负责执行问题才解决。这个坑还暴露了一个更深的问题当模式定义分散时你无法回答系统当前支持哪些模式这个问题。这对新人和排查问题都是灾难。集中定义之后一份文档就能说清楚所有模式维护成本大幅下降。4.3 坑三忽略模式的退出条件导致用户被卡住第三个项目我们设计了一个挽留模式当检测到用户可能流失时激活主动提供优惠。触发条件写得很灵敏但退出条件忘了写。结果用户领了优惠之后系统还在不停地推优惠用户觉得被骚扰反而加速流失。根因是只设计了进入逻辑没设计退出逻辑。模式本质上是一个状态状态机必须有完整的进出条件。我们后来给每个模式强制要求定义退出条件并且在代码层面做了校验没有退出条件的模式不允许上线。这个坑的教训是模式设计要像设计状态机一样严谨进入、停留、退出三个环节都要想清楚。尤其是退出条件它决定了用户能不能从某个状态里出来直接影响体验。4.4 坑四映射规则没有版本管理这个坑比较隐蔽。我们改了映射规则但没做版本管理线上出问题时无法回滚到上一个版本只能靠记忆手动改回去改的过程中又引入了新问题。根因是规则被当成了代码的一部分而不是独立的配置资产。规则应该像配置一样做版本管理每次变更都有记录可以灰度、可以回滚。我们后来把规则抽成独立文件纳入版本控制每次变更走 review 流程问题才可控。这四个坑有一个共同点都是因为把 context-mode 当成顺手写写的东西而不是一个需要认真设计的独立层。一旦你把它当成独立层来对待这些问题大部分会在设计阶段就被规避掉。5. context-mode 的测试与可观测性建设5.1 映射规则的单元测试怎么写映射规则数据化之后测试就变得非常直接给定一个上下文快照断言输出的模式。这种测试写起来快、跑起来快、覆盖率高是性价比最高的测试。我通常会准备一组黄金用例每个模式至少有一个正向用例应该命中和一个反向用例不应该命中。正向用例保证模式能被激活反向用例保证模式不会被误激活。这两类用例加起来基本能覆盖映射规则的主要逻辑。def test_soothe_mode_activated(): ctx {sentiment: negative, rounds: 3} assert resolve_mode(ctx) soothe def test_soothe_mode_not_activated_on_first_round(): ctx {sentiment: negative, rounds: 1} assert resolve_mode(ctx) ! soothe除了单点用例还要有边界用例。比如rounds正好等于阈值时应该命中还是不应该命中这种边界最容易出问题。我习惯在规则里明确写还是并且在测试里覆盖这个边界。5.2 线上如何观测模式分布规则测试只能保证逻辑正确线上实际跑起来是什么样还得靠观测。我建议至少埋三个指标模式命中分布每个模式被激活的次数占比。如果某个模式占比异常高或异常低说明规则可能有问题。模式切换频率单位会话内的切换次数。切换太频繁说明规则不稳定用户会感知到行为抖动。模式停留时长每个模式平均停留多久。停留过短说明触发条件太敏感停留过长说明退出条件没生效。这三个指标配合起来看基本能定位大部分线上问题。我遇到过一次线上告警就是因为某个模式的命中率突然从 5% 涨到 40%一查发现是上游传的上下文字段格式变了导致规则误匹配。如果没有这个指标这个问题可能要等用户投诉才发现。5.3 模式切换日志怎么记才有用日志不是记得越多越好而是要记能回答问题的信息。模式切换日志至少应该包含切换前后的模式、触发切换的上下文快照、命中的规则 ID、切换时间戳。关键是上下文快照要完整记录不能只记触发字段。因为排查问题时你往往需要看完整的上下文才能理解为什么命中了这条规则。我见过只记了模式名的日志出问题时完全没法定位只能加日志重新复现效率极低。另外日志要能按会话聚合。把同一个会话的所有模式切换日志串起来就是一条完整的用户行为轨迹对理解用户体验非常有帮助。这个聚合视图在排查用户为什么被卡住这类问题时特别好用。6. 从 context-mode 延伸出的几个设计思考6.1 上下文与模式分离本质是关注点分离context-mode 这个思路说到底就是软件工程里最经典的关注点分离separation of concerns。上下文负责描述世界模式负责决定行动映射规则负责连接两者。三者职责清晰各自可以独立演化。这个思路不只适用于对话系统。推荐系统里用户画像是上下文推荐策略是模式工作流引擎里流程状态是上下文处理节点是模式前端状态管理里应用状态是上下文视图渲染逻辑是模式。凡是涉及根据情况决定行为的场景都可以套用这个框架。我甚至觉得能不能把上下文和模式清晰地分开是判断一个系统架构成不成熟的重要标志。混在一起能跑但跑不远分开了才能持续迭代。6.2 模式不是越多越好而是越正交越好前面提过模式数量要控制这里再强调一个更本质的原则模式之间应该正交。所谓正交就是每个模式解决一个独立的问题模式之间不重叠、不依赖。正交的模式组合起来能覆盖很大的场景空间而且每个模式可以独立测试、独立修改。非正交的模式则会互相干扰改一个影响一片。判断模式是否正交有个简单的方法问自己这个模式能不能在不影响其他模式的前提下被删除。如果能说明它相对独立如果不能说明它和其他模式耦合了需要重新设计。6.3 给非工程角色留一个配置入口这是我最近一个项目的新体会。context-mode 的映射规则如果完全由工程师维护业务方每次想调整策略都要提需求、排期、等发版效率很低。如果能把规则的配置入口开放给业务方当然要有审核和灰度机制迭代速度会快很多。具体做法是把规则配置做成一个可视化的表格或者简单的 DSL业务方可以自己改条件、改模式改完提交审核审核通过后灰度发布。工程师只负责维护规则引擎本身不参与具体规则的调整。这个分工在规则频繁变化的场景下特别有效。当然开放配置入口的前提是规则引擎足够健壮能处理各种非法输入并且有完善的回滚机制。否则业务方改错一条规则可能影响一大片用户。这个平衡需要根据团队情况把握。7. 一些实操中的零散心得关于上下文快照的字段设计我有个习惯每个字段都要能回答如果这个字段缺失系统会怎样。如果某个字段缺失会导致系统无法决策那它就必须有默认值或者兜底逻辑。我见过太多因为某个上下文字段为空导致整个映射失败的案例加个默认值就能解决但往往要等到线上出问题才想起来。关于模式命名我建议用动词或动宾短语而不是名词。比如用soothe_user而不是soothing用fast_track而不是vip_mode。动词命名的好处是它直接描述了这个模式要做什么读代码时一目了然。名词命名容易让人困惑于这个模式是什么状态。关于规则优先级我的经验是留出足够的间隔。不要用 1、2、3 这样的连续优先级用 10、20、30 甚至 100、200、300。这样以后想在两条规则之间插入新规则时不用重新编号所有规则。这个习惯能省掉很多维护麻烦。关于模式切换的用户感知有个小技巧在切换点加一个自然的过渡语。比如从标准模式切到安抚模式时先说一句我理解您的心情再进入安抚逻辑。这样用户不会觉得系统突然变了个人。过渡语不需要多复杂但能显著提升体验的连贯性。最后说一个我反复强调的点context-mode 的价值不在于它多复杂而在于它把混乱的逻辑变得可管理。如果你的项目现在还在用一堆 if-else 处理上下文不妨试试把它抽成独立的一层。刚开始可能觉得多此一举但等到场景变多、规则变复杂时你会感谢当初做了这个决定。我在第三个项目里最庆幸的事就是坚持把 context-mode 抽了出来后面加场景的速度比前两个项目快了不止一倍。