context-mode 的直译是“上下文模式”,我第一次接触这个概念是在做一个多端同步的办公工具项目里。同一个待办事项列表,在电脑端需要支持批量勾选、拖拽排序,在手机端需要大按钮和触控手势,在轻应用端又得走审批流和消息提醒。如果每个端都写一套 if-else 判断,代码会迅速腐烂。后来我意识到,真正的问题不是我“不会写判断”,而是没有把“判断依据”当成一个独立对象来管理——这个判断依据就是上下文,围绕上下文来组织代码运行方式,就是 context-mode。这篇文章里,我会把这个模式从原理到落地完整拆一遍,给出可以直接照抄的代码骨架,也会分享实际踩过的坑。如果你正在做多端应用、自动化流程或者 AI 应用开发,这篇文章值得花十分钟读完。
1. context-mode 到底解决什么问题:先看懂痛点再学概念
1.1 没有上下文模式的判断逻辑是如何一步步腐烂的
很多刚从“写功能”过渡到“做设计”阶段的朋友,都会有类似的经历:一开始需求很简单,无非是“用户是管理员就显示按钮 A,否则显示按钮 B”。于是代码长这样:
def handle_todo_request(request): if request.user.is_admin and request.device == "pc": return render_batch_table(request.todos) elif request.device == "mobile": return render_mobile_cards(request.todos) else: return render_simple_list(request.todos)这句代码刚写出来的时候,还挺清爽。麻烦的是后面不断的加需求:今天加一个“VIP 用户优先展示待办”,明天加一个“超过 20 个待办的移动端用户要分页”,后天再加一个“企业微信来源的请求走审批流”。每个新需求都往这个函数里塞一个条件,于是这个函数慢慢长成了十几个分支、嵌套两层以上的怪物。
更难受的是,某一天产品告诉你:“移动端 VIP 用户,待办超过 20 个时,不要分页,改用聚合视图。”这个时候你发现,改一处还不够,所有涉及同类判断的地方全部要同步调整。等这些判断散落在十个文件里,改动一个字段格式,全局都要翻一遍。我把这种状态叫做“判断腐烂”:逻辑本身不难写,难的是判断依据散落各处,导致系统整体越来越脆。
1.2 用“餐厅服务”来理解 context-mode 的定位
理解 context-mode,可以用一个餐厅的例子。假设你是一家连锁餐厅的店长,你的服务员需要根据顾客类型提供不同服务:老顾客直接上常点的菜,家庭顾客优先推荐套餐,商务宴请顾客不能催单还要额外摆盘。如果你把整套判断逻辑都塞给前台服务员,说一句他们现场查会员系统、查消费记录、查今天的预定单,那高峰期一定乱套。
好的做法是,有一个“餐桌管家”在顾客进门时就把所有信息汇总好,在菜单上贴一张便签:这是常客、忌辣、预算 200 元。服务员拿到这张便签再行动,效率比自己东查西查高得多。context-mode 就是这个“餐桌管家”:它负责把散落在不同系统的信息,统一收集、合并、加工成一个上下文对象,然后业务模块只需要基于这个对象做决策,不需要自己去查各种来源。
所以理解 context-mode,核心就一句话:把“从零散信号推导全局状态”这件事从业务模块中剥离出来,单独交给一个上下文系统处理。业务代码只面对一个已经封装好的上下文对象,而不是面对一堆参数和来源不明的标记位。
2. 为什么“上下文优先”设计成了今天的必然选择
2.1 多端适配需求把单一逻辑彻底撕碎
过去的企业软件,一个 Web 后台走天下。用户用鼠标点,屏幕是统一的宽屏,不需要考虑手指触摸面积,也不需要兼容移动网络。现在不一样了,同一个系统可能要跑在 PC 浏览器、手机 App、企业微信小程序、甚至智能平板上。不同的端不仅屏幕尺寸不同,交互习惯、权限边界、网络状态都不同。
如果每个端都各自请求接口、各自判断逻辑,很容易出现“PC 端显示了一个功能,移动端却因为少传了一个参数而报错”的现象。问题根源在于:信息没有被集中管理,而是散落在多个调用链上。而 context-mode 的做法是:由上下文层统一负责跟用户服务、设备服务、行为日志服务打交道,把获取到的信息合并成一份标准化的上下文快照,所有端共用这套快照做决策。这样既保证了一致性,又让不同端在一致的基础上做差异化处理。
打个比方,以前的系统像是每个店自己买菜自己记账,而 context-mode 像是总部统一建了一个中央厨房和一套账本。门店只需要按需取料,不用自己到处张罗。这个“中央厨房”就是上下文分层存在的意义。
2.2 自动化与智能化场景需要“全局快照”而非单点信号
我平时会折腾手机自动化,Tasker、快捷指令都玩过。早期的自动化逻辑很简单:“到了指定时间,执行指定动作”,“进入某个位置,弹出提醒”。这种单点触发规则,写多了之后很容易打架——比如你同时设了“到家开 Wi-Fi”和“晚上 11 点开启勿扰”,到家那一瞬间两个规则同时触发,后一个规则往往就被前一个挡住了。
这类自动化工具发展到今天,都在往 context-mode 迁移:系统不只依赖“当前时间”或“当前地点”单一信号,而是结合时间、位置、活动状态、上个行为、电池状态等多维度上下文,再决定要不要执行动作。同样,在企业的自动化流程里,如果只根据“表单提交”一个事件就转发审批,很容易误判。更合理的方案是把“提交人所在部门、岗位、历史报销是否合规、当前审批节点”等背景信息打包成上下文,再由流程引擎基于完整上下文去决定路由。自动化越复杂,对上下文的需求就越刚性,这是挡不住的。
2.3 context-mode 与策略模式、状态模式的边界划分
不少读者可能会问:这和策略模式、状态模式有什么区别?我用一张表说明白:
| 模式 | 核心关注点 | 解决的问题 | 典型实现 |
|---|---|---|---|
| 策略模式 | 算法可互换 | 同一接口有多种实现,运行时可切换 | 排序算法、支付渠道 |
| 状态模式 | 对象内部状态驱动行为 | 对象状态变化后行为自动变化 | 订单状态机、下载暂停恢复 |
| 观察者模式 | 事件通知与解耦 | 数据变更后通知多个对象 | 消息订阅、事件总线 |
| context-mode | 多源信息聚合后全局决策 | 行为依赖多个维度交叉判定 | 多端适配、智能路由 |
状态模式关注的是“对象自己内部的状态”,订单从“待支付”变到“已支付”,行为跟着变。而 context-mode 关注的是“外部信息的汇聚”,它不一定修改对象内部状态,而是把用户身份、设备类型、历史行为、环境信号等外部信息打包,提供给决策层使用。
我在实践中喜欢这么区分:如果一个节点只依赖对象自己的字段变化,那是状态模式;如果它需要同时看用户、设备、时间、历史等多个来源,那就是 context-mode 的范畴。大多数业务系统里的复杂问题,都是后者。
3. 从零手写一个 context-mode 系统:完整实操拆解
3.1 定义上下文对象的数据结构
概念说得再多,不如动手写代码。我拿“任务清单系统”举例,做一个支持 PC、手机、企业微信小程序三端访问的后端接口。在 context-mode 设计下,不建议直接把所有参数塞进一个字典,因为字段一旦多起来,类型和来源根本分不清。我建议用 dataclass 定义上下文结构,把上下文分成三层:设备层、用户层、业务层。
from dataclasses import dataclass, field from typing import Optional from datetime import datetime @dataclass class DeviceInfo: device_type: str # pc / mobile / mini platform: str # windows / harmony / wechat screen_width: int = 0 is_outdoor: bool = False @dataclass class UserProfile: user_id: str is_admin: bool = False is_vip: bool = False plan: str = "free" @dataclass class BizContext: current_project: Optional[str] = None todo_count: int = 0 selected_todo_ids: list[str] = field(default_factory=list) pending_audit: Optional[bool] = None @dataclass class RuntimeContext: user: UserProfile device: DeviceInfo biz: BizContext trace_id: str = "" created_at: datetime = field(default_factory=datetime.now)这个设计解决了什么问题?首先,每一类数据都有了归属,不会出现一个包罗万象的字典。其次,之后的规则判断都基于这个结构,类型安全,IDE 提示也友好。最关键的是,这个上下文对象是可序列化的——如果需要调试,你可以整个对象导出 JSON。
3.2 实现上下文收集器:三种信息采集方式
定义好结构之后,下一步是让数据进来。上下文信息一般来说有三个来源,对应的采集方式也不同。
第一个来源是请求自带的信号。比如 User-Agent 头、请求参数、IP 地址,这类信息不需要额外请求服务,直接从当前请求里提取就行。
def collect_device_info(raw_request: dict) -> DeviceInfo: ua = raw_request.get("user_agent", "").lower() platform = raw_request.get("platform", "windows") if "wxwork" in ua or "wechat" in ua: return DeviceInfo(device_type="mini", platform="wechat") if "mobile" in ua or "android" in ua or "iphone" in ua: return DeviceInfo(device_type="mobile", platform=platform) return DeviceInfo(device_type="pc", platform=platform)第二个来源是服务端主动查询。比如根据 user_id 去调用用户中心,获取账户等级、权限位、组织关系。这类信息往往是判断的“主料”,但代价是消耗网络和数据库资源,所以要注意缓存。
def collect_user_profile(user_id: str) -> UserProfile: # 这里模拟一次用户服务调用 data = user_service.get_profile(user_id) return UserProfile( user_id=user_id, is_admin=data.get("is_admin", False), is_vip=data.get("is_vip", False), plan=data.get("plan", "free"), )第三个来源是订阅式的历史行为。比如用户在最近 5 分钟点击了什么、有没有上传附件的习惯、当前操作是否打断了上一次未完成任务。这些信号来自事件流或日志存储,可以通过订阅汇总或者查询获得。三种来源在采集方式上不同,在 context-mode 里统一由上下文收集器层完再接进对象。
我这里想强调的是:收集器的设计原则是“只搬运,不决策”。收集器永远不去判断哪个信息更重要,哪个条件该生效,它只负责把能拿到的信息全部塞进对应的 dataclass。判断的事情留给路由层做。如果你把决策逻辑混进收集器,那么上下文对象就失去了中立性,后面很难排查问题。
3.3 实现路由规则与处理器:最小化规则引擎
当上下文对象构建好之后,核心的“行为切换”就发生在路由层。路由层拿着 RuntimeContext,按照规则的顺序进行匹配,命中哪个规则就交给对应的处理器执行。
from typing import Callable, Tuple, List Route = Tuple[str, Callable[[RuntimeContext], bool], Callable[[RuntimeContext], dict]] def rule_admin_pc(ctx: RuntimeContext) -> bool: return ctx.user.is_admin and ctx.device.device_type == "pc" def rule_mobile_quick(ctx: RuntimeContext) -> bool: return ctx.device.device_type == "mobile" and not ctx.user.is_admin def rule_mini_audit(ctx: RuntimeContext) -> bool: return ctx.device.device_type == "mini" and ctx.biz.pending_audit is True def handler_batch(ctx: RuntimeContext) -> dict: return {"view": "batch_table", "data": build_batch_data(ctx)} def handler_mobile_cards(ctx: RuntimeContext) -> dict: return {"view": "mobile_cards", "data": build_card_data(ctx)} def handler_audit_flow(ctx: RuntimeContext) -> dict: return {"view": "audit_flow", "data": build_audit_flow(ctx)} ROUTES: List[Route] = [ ("admin_pc_batch", rule_admin_pc, handler_batch), ("mobile_quick", rule_mobile_quick, handler_mobile_cards), ("mini_audit_flow", rule_mini_audit, handler_audit_flow), ] DEFAULT_HANDLER = handler_mobile_cards def route(ctx: RuntimeContext) -> dict: for name, rule, handler in ROUTES: if rule(ctx): return handler(ctx) return DEFAULT_HANDLER(ctx)这段代码里最关键的一点是:规则顺序是敏感的。rule_admin_pc排在最前,意味着即使管理员在手机端,他也不会先命中rule_mobile_quick。顺序就是优先级,所以每加一条规则,都要明确知道它该放在哪个位置。很多新手踩的坑是,规则之间相互覆盖,到了排查的时候才发现某条规则永远执行不到。
为了让规则优先级可管理,我习惯把规则的“权重”或者“顺序”独立出来,不隐含在列表顺序里。比如给每条规则加一个 order 字段,路由执行前排序。这样可读性更好,也方便后续用配置文件或后台去调整优先级。
3.4 加入反馈闭环:让上下文持续保鲜
context-mode 不是一次性快照。一个高效的上下文系统,必须在动作执行完之后把结果反馈回来,让后续判断能参考“刚才发生了什么”。这一步我称之为反馈闭环。
def dispatch_and_record(ctx: RuntimeContext, raw_request: dict) -> dict: ctx.biz.todo_count = len(raw_request.get("todo_ids", [])) resp = route(ctx) # 将本次行为写入最近动作列表,供下一次上下文构建时使用 ctx.biz.selected_todo_ids = raw_request.get("todo_ids", []) audit_service.record(ctx.trace_id, { "action_taken": resp.get("view"), "context": to_json(ctx), "timestamp": ctx.created_at.isoformat(), }) return resp举个例子,一个用户在手机端手动勾选了三个待办并点了“排序”,这个行为被写进 BizContext。下一次请求进来时,上下文收集器会读取这些历史行为,判断出“这是一个喜欢手动排序的用户”,于是优先展示顺序操作控件,而不是默认视图。这种能力在纯 if-else 系统里很难实现,因为每次请求都是孤立的。context-mode 把历史行为纳入上下文对象,等于给系统装了“记忆”。
实现反馈闭环时要注意异步问题:不能因为记录行为导致主流程变慢。通常做法是记录动作放到消息队列,日志异步写,或者直接交给审计系统,不让它阻塞响应。
4. 工程落地中的关键难点与排查技巧
4.1 上下文对象膨胀:从聚合根拆分入手
我第一次把 context-mode 应用到实际项目时,犯过一个很典型的错误:为了让后续“省事”,我把所有可能用到的信息都塞进上下文对象。结果两周之后 RuntimeContext 拥有将近 30 个字段,而且彼此之间的职责已经乱成一团。想查一个简单的“这个用户是不是 VIP”,都需要先搞清楚字段是 user_profile.is_vip 还是 biz.vip_flag 还是 privilege_level。
后来我学到的方法是把上下文对象按“聚合根”拆分,也即在 3.1 节里的设计——分成 DeviceInfo、UserProfile、BizContext 三个部分。每个部分在逻辑上独立演进,单独测试。路由层按需引用不同的子结构,而不是整个大对象透传。
拆分之后还有一个额外的好处:缓存粒度更精细。UserProfile 可以按 userId 缓存 10 分钟,DeviceInfo 按请求直接构建,BizContext 按会话维度查询。如果整个上下文一个大缓存,性能会很难看。
4.2 多源信息冲突时的优先级设计
上下文收集器从多个来源拿数据,在真实系统里一定会遇到冲突。典型的例子是:用户客户端写了自己的偏好“列表视图”,但后台运营配置要求“该用户属于新客组,必须看到引导弹窗”。两边打架,到底听谁的?
这个问题没有标准答案,但可以建立一个可配置的优先级表。我习惯把上下文来源划分为四个等级,从高到低排序:
| 优先级 | 来源类型 | 典型信息 | 说明 |
|---|---|---|---|
| 1 | 用户显式操作 | 用户当场选择了排序方式、点了“知道” | 用户当前意图最强烈,最高优先级 |
| 2 | 后台强规则 | 合规要求、安全限制、灰度开关 | 运营或合规强约束 |
| 3 | 用户画像偏好 | 历史行为、设置页保存的偏好 | 个性化服务基础 |
| 4 | 环境默认值 | 设备类型、时间、网络状态 | 兜底方案 |
优先级表要显式维护在配置或代码常量中,并且每条信息在写入上下文时带上来源标签。这样冲突发生时,路由层可以按照“高优先级覆盖低优先级”的规则生成最终上下文。如果两个来源同一优先级,再按时间戳取最新的。
这个设计尤其适合电商、内容平台这类强运营场景。运营配置新客弹窗时,不能覆盖用户刚提交的表单操作,又需要覆盖用户的旧偏好,所以精确的优先级比拍脑袋判断可靠得多。
4.3 可观测性:给每次决策建立 trace
上下文模式一旦在线上运行,最怕的事情是“我明明返回了 A 视图,为什么用户反馈看到了 B 内容”。如果系统里没有日志,这种问题排查起来全靠猜。
所以我从一开始就强制要求:所有针对上下文的决策都必须记录决策日志。每一次请求,生成一个 trace_id,然后把“上下文 JSON 摘要、命中的规则名、处理函数、返回结果”全部写进日志或链路追踪系统。这样出问题的时候,顺着 trace_id 一查,就能看到当时所用上下文的完整状态。
import json def trace_decision(ctx: RuntimeContext, rule_name: str, resp: dict): print(json.dumps({ "trace_id": ctx.trace_id, "rule_name": rule_name, "user_id": ctx.user.user_id, "device": ctx.device.device_type, "resolved_view": resp.get("view"), "created_at": ctx.created_at.isoformat(), }, ensure_ascii=False, default=str))排查问题的时候,我通常只看两类:一类是“所有规则都没命中,走了默认分支”,说明上下文信息没采集全;另一类是“命中了规则,但结果和预期不一致”,说明规则优先级写错。这两类问题占了 context-mode 线上故障的八成。
5. 常见问题速查表与我的五条铁律
5.1 高频问题速查表
我在带团队落地 context-mode 的过程中,整理了一份高频问题清单,大部分问题都是第一周必踩的,放在这里供你参考。
| 症状 | 根本原因 | 解决方案 |
|---|---|---|
| 所有请求都走了默认分支 | 上下文信息采集不全 | 检查收集器是否读取了请求头和用户信息,打印上下文 JSON 对比 |
| 某条规则永远不执行 | 规则优先级排错 | 确认路由列表顺序,把更具体的规则放在前面 |
| 上下文对象太多层,取字段很痛苦 | 没有做聚合根拆分 | 按设备、用户、业务拆分子对象,禁止堡垒字段 |
| 上下文中字段互相矛盾 | 多源信息未设置优先级 | 引入来源标签和优先级表,高优先级覆盖低优先级 |
| 加了新功能,修改上下文后老功能异常 | 上下文对象被多处共用 | 为上下文打版本号,增加字段时做兼容 |
| 性能下降明显 | 收集器里做了同步远程调用 | 对用户信息等做缓存,历史行为改成异步订阅 |
| 排查问题全靠猜 | 缺少决策日志 | 给每次路由加 trace_id,记录规则命中和上下文摘要 |
5.2 我在实际项目中总结的五条铁律
第一条,上下文对象一旦构建完成,就不可变。执行过程中任何人不能改上下文里的字段。如果你需要根据业务结果更新上下文,那就在反馈闭环环节重新生成一个新的上下文对象,而不是原地改。不可变上下文让并发安全和调试变得非常简单。
第二条,收集器里永远不写业务判断。收集器只负责拿数据和填字段,所有 if-else 一律放到路由规则里。这条铁律是为了保持职责分离,否则收集器越来越臃肿,最后陷入判断腐烂的老路。
第三条,规则必须显式排序,且可视。用配置文件或者常量表维护规则顺序,让产品经理也能评审:到底哪个条件优先。规则顺序一旦“隐藏”在代码列表里,迟早会乱。
第四条,必须有兜底分支,且兜底永远不应该报错。线上环境里,任何上下文缺失都不应该让服务 500。兜底分支返回一个最保守的默认行为,同时把“走了兜底”这个事件主动告警。宁可用户看到朴素页面,也不能看到错误页。
第五条,从第一天写决策日志。不要等出问题才加日志,到时候上下文已经丢失,补了日志也追溯不了。决策日志是最便宜的保险,却能在故障时候省下几小时甚至一天的排查时间。
最后再分享一点我的真实体会
重构那个多端办公工具的时候,最直观的感受其实是代码总行数并没有明显变少,但需求变更的速度变快了。以前为了一个新端适配,要改三层代码、联动测试半天;现在只需要加一个 collector、一条规则、一个 handler,影响面严格限制在新功能内部。上线半年之后,产品提了十几次差异化需求,几乎没有一次碰过别人的逻辑。这种“隔离变更”的能力,我认为才是 context-mode 最核心的回报。
如果你正准备在多端系统、自动化引擎或 AI 应用里引入类似模式,我建议不要一上来就引入重量级框架,用 dataclass 加路由函数的方式先把骨架跑起来。等量上去了,再逐步把收集器、路由、日志拆成独立服务。好的模式不应该成为额外的负担,而应该让复杂业务变得可控。希望这份经验对你有用。