ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

context-mode 上下文模式:多租户与多端适配的工程实践

context-mode 上下文模式:多租户与多端适配的工程实践

1. 从“context-mode”这个命名说起:它到底在解决什么问题

第一次看到“context-mode”这个词,很多人会下意识地把它当成某个框架里的配置项,或者某个编辑器插件的开关。但如果你真的在工程一线待过,就会发现这个词背后指向的是一类非常具体、非常普遍的痛点:同一个系统,在不同上下文里需要表现出不同的行为模式,而传统的做法要么写死、要么散落各处、要么靠一堆 if-else 硬撑。

我最早接触这个概念是在做一个多租户后台系统的时候。同一个接口,C 端用户调用时返回精简字段,B 端管理员调用时返回全量字段,内部服务调用时又要带上审计信息。最开始的做法是在 Controller 里根据请求头判断,然后手动拼装返回值。结果就是:每加一个调用方,就要改一次 Controller;每改一次,就要回归测试所有调用方。代码越来越臃肿,谁都不敢动。

后来我们把这种“根据上下文切换行为”的逻辑抽出来,用一个统一的模式来管理,才真正把这个问题按住。这个模式,我习惯称之为context-mode(上下文模式)。它不是某个具体库的名字,而是一种设计思路:把“当前处于什么上下文”和“在这个上下文下应该怎么做”这两件事解耦,让上下文成为一等公民,让模式切换变得可配置、可观测、可测试。

这篇文章适合谁看?如果你正在做多端适配、多租户系统、灰度发布、A/B 实验、或者任何“同一套代码要服务不同场景”的事情,那 context-mode 这套思路你大概率用得上。如果你只是写个单机小工具,那可能暂时不需要,但了解一下也没坏处,因为迟早会遇到。

下面我会从核心概念拆解开始,然后讲清楚为什么传统方案会失败,接着给出可落地的实现方案,最后分享几个我在实际项目中踩过的坑和总结出来的经验。全程说人话,不堆术语,能抄的代码直接给。

2. 拆解 context-mode 的三个核心要素:上下文、模式、切换策略

要理解 context-mode,得先把它的三个组成部分拆开看。很多人用不好这个模式,就是因为把这三个东西混在一起了。

2.1 上下文(Context):不只是请求头那么简单

上下文是什么?简单说,就是“当前这次调用所处的环境信息”。它可以是:

  • 请求来源:来自 App、Web、内部服务、第三方合作方
  • 用户身份:普通用户、VIP、管理员、系统账号
  • 租户标识:租户 A、租户 B、租户 C
  • 环境阶段:开发、测试、预发、生产
  • 实验分组:对照组、实验组 A、实验组 B
  • 时间窗口:大促期间、日常期间

这些信息有一个共同特点:它们不是业务逻辑本身,但会影响业务逻辑的执行方式。比如同样是“查询订单”这个业务动作,VIP 用户可以看到折扣明细,普通用户看不到;大促期间可能要走降级逻辑,日常期间走完整逻辑。

关键点在于:上下文必须是可采集、可传递、可序列化的。我见过很多项目把上下文存在 ThreadLocal 里,结果异步线程一切换就丢了;或者存在全局变量里,结果并发一上来就串了。这些都是坑,后面会细说。

一个设计良好的上下文对象,应该具备这些特征:

  • 不可变:一旦创建就不应该被修改,避免在传递过程中被意外篡改
  • 可合并:多个来源的上下文可以合并成一个,比如请求头里的租户信息 + 用户服务里的身份信息
  • 可降级:当某个上下文缺失时,有明确的默认值,而不是直接报错
  • 可观测:能打印出来、能打点到监控、能在日志里追踪

2.2 模式(Mode):行为的具体表现形式

模式就是“在这个上下文下,系统应该表现出什么行为”。它可以是:

  • 返回字段的集合:精简模式只返回 id 和 name,完整模式返回所有字段
  • 执行的逻辑分支:严格模式做全量校验,宽松模式只做基础校验
  • 调用的下游服务:生产模式调真实支付,沙箱模式调模拟支付
  • 限流阈值:内部调用不限流,外部调用限流 100 QPS
  • 缓存策略:热点模式缓存 5 分钟,普通模式缓存 30 秒

模式的关键在于:它应该是声明式的,而不是命令式的。什么意思?就是你应该定义“完整模式包含哪些字段”,而不是写“如果 context 是 X 则返回 A、B、C”。前者在加字段时只需要改配置,后者在加字段时要改代码。

我习惯用枚举或者配置对象来定义模式,比如:

from enum import Enum from dataclasses import dataclass class ModeType(Enum): LITE = "lite" FULL = "full" AUDIT = "audit" @dataclass(frozen=True) class ModeConfig: mode_type: ModeType fields: tuple cache_ttl: int rate_limit: int MODE_CONFIGS = { ModeType.LITE: ModeConfig(ModeType.LITE, ("id", "name"), 30, 1000), ModeType.FULL: ModeConfig(ModeType.FULL, ("id", "name", "desc", "price", "stock"), 300, 100), ModeType.AUDIT: ModeConfig(ModeType.AUDIT, ("id", "name", "desc", "price", "stock", "operator", "timestamp"), 0, 50), }

这样定义之后,加一个新模式只需要加一个配置项,不需要动业务代码。

2.3 切换策略(Switch Policy):从上下文到模式的映射规则

有了上下文和模式,还需要一个规则来决定“什么上下文对应什么模式”。这个规则就是切换策略。它可以是:

  • 基于单一维度:租户 ID 是 A 就用完整模式
  • 基于多维度组合:内部服务 + 生产环境 + 大促期间 = 审计模式
  • 基于优先级:多个规则命中时,取优先级最高的
  • 基于动态配置:从配置中心实时拉取,不重启生效

切换策略最容易出问题的地方是规则冲突和规则爆炸。我见过一个项目,切换规则写了 200 多条,最后没人敢改,因为不知道改了会影响谁。这就是典型的规则爆炸。

好的切换策略应该具备:

  • 优先级明确:规则有顺序,命中即停,避免歧义
  • 可覆盖:支持在调用级别手动指定模式,覆盖默认策略
  • 可观测:每次切换都能记录“用了哪条规则、为什么选了这个模式”
  • 可测试:规则可以单独测试,不依赖完整系统

把这三个要素理清楚之后,context-mode 的骨架就出来了:采集上下文 → 应用切换策略 → 得到模式 → 按模式执行行为。听起来简单,但每个环节都有坑。

3. 为什么“if-else 硬编码”和“全局配置”都会翻车

在讲具体实现之前,我想先说说为什么很多团队一开始都会走弯路。因为只有理解了失败的原因,才能真正理解 context-mode 的价值。

3.1 if-else 硬编码:从能用到不敢用

最直觉的做法就是在业务代码里写 if-else:

def get_order(order_id, request): order = load_order(order_id) if request.source == "app": if request.user.is_vip: return full_response(order) else: return lite_response(order) elif request.source == "admin": return admin_response(order) elif request.source == "internal": return internal_response(order) else: return default_response(order)

这段代码一开始能跑,但问题会随着时间暴露:

  • 每加一个来源,就要改这个函数:改完还要回归测试所有来源
  • 逻辑嵌套越来越深:VIP + App + 大促,又是一个分支
  • 无法复用:另一个接口也需要类似逻辑,只能复制粘贴
  • 无法测试:要测一个分支,得构造完整的 request 对象
  • 无法观测:线上出问题时,不知道走了哪个分支

我见过最夸张的一个函数,嵌套了 7 层 if-else,总共 400 多行,团队里没人敢动。这就是硬编码的终局。

3.2 全局配置:从灵活到失控

意识到硬编码的问题后,很多团队会转向全局配置。比如在配置文件里写:

order_response: app_vip: full app_normal: lite admin: admin internal: internal

这看起来好多了,至少改配置不用改代码。但新的问题来了:

  • 配置和代码脱节:配置里写了一个模式名,代码里没有对应实现,运行时才报错
  • 配置无法表达复杂逻辑:如果规则是“App + VIP + 大促期间”,配置文件很难优雅表达
  • 配置散落各处:订单有一套配置,用户有一套配置,商品有一套配置,没人知道全貌
  • 配置变更无审计:谁改的、什么时候改的、改之前是什么,全靠猜

全局配置的本质问题是:它把“策略”和“实现”分开了,但没有把“策略”本身管理好。策略应该是代码的一部分,可以测试、可以版本化、可以代码审查,而不是一堆散落的 YAML。

3.3 context-mode 的破局点:策略即代码,上下文即数据

context-mode 的核心思路是:把切换策略写成代码,把上下文当成数据,把模式定义成配置。三者各司其职:

要素形态变更频率谁来维护
上下文运行时数据每次请求都变框架自动采集
切换策略代码低开发
模式定义配置中开发 + 运维

这样分工之后,加一个新调用方只需要加一条策略规则;加一个新字段只需要改模式配置;上下文采集由框架统一处理,业务代码不用关心。

4. 落地实现:从上下文采集到模式执行的完整链路

理论讲完了,接下来是实操。我会用一个 Python 示例来演示完整链路,但思路是跨语言的,Java、Go、Node.js 都能套。

4.1 上下文采集:别让业务代码碰上下文

上下文采集最忌讳的就是让业务代码手动传参。我见过很多项目,每个函数都要传一个context参数,从 Controller 一层层传到 Service,再到 DAO。结果是:函数签名越来越长,中间任何一层忘了传就丢上下文。

正确的做法是:在入口处统一采集,通过框架机制自动传递。在 Python 里可以用 contextvars,在 Java 里可以用 ThreadLocal + 拦截器,在 Go 里可以用 context.Context。

import contextvars from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class RequestContext: source: str user_id: str tenant_id: str is_vip: bool env: str experiment_group: Optional[str] = None _current_context: contextvars.ContextVar[Optional[RequestContext]] = contextvars.ContextVar("request_context", default=None) def set_context(ctx: RequestContext): _current_context.set(ctx) def get_context() -> RequestContext: ctx = _current_context.get() if ctx is None: raise RuntimeError("Context not initialized") return ctx

在 Web 框架的中间件里统一设置:

def context_middleware(request, handler): ctx = RequestContext( source=request.headers.get("X-Source", "unknown"), user_id=request.headers.get("X-User-Id", ""), tenant_id=request.headers.get("X-Tenant-Id", "default"), is_vip=request.headers.get("X-User-Level") == "vip", env=os.environ.get("APP_ENV", "dev"), ) set_context(ctx) try: return handler(request) finally: _current_context.set(None)

这样业务代码里直接get_context()就能拿到,不用传参。注意finally里要清理,避免上下文泄漏到下一个请求。

注意:异步场景下 contextvars 是天然支持的,但如果你用的是线程池,需要手动把上下文复制过去。我踩过这个坑,线程池里的任务拿不到主线程的上下文,结果所有异步任务都走了默认模式。

4.2 模式定义:用配置对象而不是魔法字符串

模式定义要避免用字符串硬编码,比如if mode == "full"。应该用枚举 + 配置对象:

from enum import Enum from dataclasses import dataclass from typing import Tuple class Mode(Enum): LITE = "lite" FULL = "full" ADMIN = "admin" INTERNAL = "internal" @dataclass(frozen=True) class ModeSpec: fields: Tuple[str, ...] cache_ttl: int rate_limit: int audit: bool MODE_SPECS = { Mode.LITE: ModeSpec(("id", "name"), 30, 1000, False), Mode.FULL: ModeSpec(("id", "name", "desc", "price", "stock"), 300, 100, False), Mode.ADMIN: ModeSpec(("id", "name", "desc", "price", "stock", "cost", "supplier"), 0, 50, True), Mode.INTERNAL: ModeSpec(("id", "name", "desc", "price", "stock", "audit_info"), 60, 500, True), }

这样加字段只需要改MODE_SPECS,加模式只需要加一个枚举项和对应配置。业务代码里用MODE_SPECS[mode].fields来过滤字段,完全不用改。

4.3 切换策略:规则引擎的极简实现

切换策略的核心是“按优先级匹配规则,命中即停”。我用一个列表来存规则,每条规则是一个函数,返回Optional[Mode]:

from typing import Callable, List, Optional Rule = Callable[[RequestContext], Optional[Mode]] def rule_internal(ctx: RequestContext) -> Optional[Mode]: if ctx.source == "internal": return Mode.INTERNAL return None def rule_admin(ctx: RequestContext) -> Optional[Mode]: if ctx.source == "admin": return Mode.ADMIN return None def rule_vip_app(ctx: RequestContext) -> Optional[Mode]: if ctx.source == "app" and ctx.is_vip: return Mode.FULL return None def rule_default_app(ctx: RequestContext) -> Optional[Mode]: if ctx.source == "app": return Mode.LITE return None RULES: List[Rule] = [ rule_internal, rule_admin, rule_vip_app, rule_default_app, ] def resolve_mode(ctx: RequestContext) -> Mode: for rule in RULES: mode = rule(ctx) if mode is not None: return mode return Mode.LITE # 兜底

这个实现有几个好处:

  • 规则顺序就是优先级,一目了然
  • 每条规则可以单独测试,传入不同的 ctx 看返回什么
  • 加规则只需要往列表里插,不影响已有规则
  • 规则命中即停,没有歧义

如果规则特别多,可以按来源分组,或者用配置驱动。但我的经验是:规则不超过 20 条时,代码比配置更好维护。超过 20 条,说明你的上下文维度太多了,应该考虑收敛。

4.4 模式执行:在业务逻辑里优雅地应用模式

有了模式和上下文,最后一步是在业务代码里应用。关键是:业务代码只关心“当前模式是什么”,不关心“为什么是这个模式”。

def get_order(order_id: str) -> dict: ctx = get_context() mode = resolve_mode(ctx) spec = MODE_SPECS[mode] order = load_order_from_db(order_id) if spec.audit: record_audit_log(ctx.user_id, order_id, mode.value) result = {field: order[field] for field in spec.fields if field in order} return result

这样业务代码非常干净:拿上下文、解析模式、按模式过滤字段、按模式决定是否审计。加一个新模式,业务代码一行不用改。

如果某些模式下逻辑差异很大,可以用策略模式进一步抽象:

class OrderHandler: def handle(self, order: dict, spec: ModeSpec) -> dict: raise NotImplementedError class StandardHandler(OrderHandler): def handle(self, order, spec): return {f: order[f] for f in spec.fields if f in order} class AuditHandler(OrderHandler): def handle(self, order, spec): result = {f: order[f] for f in spec.fields if f in order} result["_audit"] = {"operator": get_context().user_id} return result HANDLERS = { Mode.LITE: StandardHandler(), Mode.FULL: StandardHandler(), Mode.ADMIN: AuditHandler(), Mode.INTERNAL: AuditHandler(), }

这样模式和执行逻辑的映射关系也是显式的,加新模式只需要加一个 Handler。

5. 实测中踩过的五个坑:从上下文丢失到规则冲突

这套方案我在三个项目里落地过,踩了不少坑。下面挑五个最有代表性的说说,希望能帮你省点时间。

5.1 异步任务里上下文丢失:ThreadLocal 的经典陷阱

第一个坑发生在用线程池处理异步任务的时候。主线程设置了上下文,提交任务到线程池,结果任务执行时get_context()报错说上下文未初始化。原因是 ThreadLocal(以及 Python 的 threading.local)是线程隔离的,线程池里的线程拿不到主线程的数据。

解决方案有两种:

  • 方案一:提交任务时手动把上下文作为参数传进去,在任务开头重新 set
  • 方案二:用 contextvars(Python)或 TransmittableThreadLocal(Java),它们支持上下文传递

我推荐方案二,因为方案一容易漏。Python 的 contextvars 在asyncio里是自动传递的,但在线程池里需要手动 copy:

import contextvars from concurrent.futures import ThreadPoolExecutor def submit_with_context(executor, fn, *args): ctx = contextvars.copy_context() return executor.submit(ctx.run, fn, *args)

注意:contextvars.copy_context() 会复制当前上下文,然后在目标线程里用 ctx.run 执行,这样上下文就能带过去。这个细节很多文档没写,但实际项目里必用。

5.2 规则冲突:两条规则同时命中,结果不确定

第二个坑是规则冲突。有一次加了一条新规则“大促期间走降级模式”,结果发现内部服务调用也命中了这条规则,因为内部服务调用也带了大促标记。两条规则都返回了模式,但列表顺序决定了谁生效,而列表顺序是随手写的,没人注意到。

解决方案是:给规则加显式的优先级,并且在冲突时打日志。我改成这样:

@dataclass class Rule: priority: int name: str matcher: Callable[[RequestContext], Optional[Mode]] RULES = [ Rule(100, "internal", rule_internal), Rule(90, "admin", rule_admin), Rule(80, "vip_app", rule_vip_app), Rule(10, "default_app", rule_default_app), ] def resolve_mode(ctx): matched = [] for rule in sorted(RULES, key=lambda r: -r.priority): mode = rule.matcher(ctx) if mode is not None: matched.append((rule.name, mode)) if len(matched) > 1: logger.warning(f"Multiple rules matched: {matched}") return mode return Mode.LITE

这样冲突时至少能看到日志,知道是哪两条规则撞了。

5.3 模式爆炸:从 3 个模式到 30 个模式

第三个坑是模式数量失控。一开始只有 lite、full、admin 三个模式,后来业务方不断提需求:“我要一个只返回价格的模式”“我要一个带库存的模式”“我要一个带供应商的模式”……半年后变成了 30 多个模式,配置表长得没人看得完。

解决方案是:模式应该按“行为差异”划分,而不是按“字段组合”划分。如果两个模式只是字段不同,应该合并成一个模式,用字段白名单参数控制。比如:

@dataclass(frozen=True) class ModeSpec: base_fields: Tuple[str, ...] optional_fields: Tuple[str, ...] cache_ttl: int audit: bool # 调用时可以指定额外字段 def get_order(order_id, extra_fields=()): spec = MODE_SPECS[resolve_mode(get_context())] fields = spec.base_fields + tuple(f for f in extra_fields if f in spec.optional_fields) ...

这样模式数量控制在 5 个以内,字段灵活性通过参数解决。

5.4 缓存穿透:不同模式共用缓存导致数据错乱

第四个坑是缓存。lite 模式和 full 模式查同一个订单,如果共用缓存 key,lite 模式先查,缓存里只有 id 和 name,full 模式再查时拿到的是不完整的缓存,结果返回了缺失字段。

解决方案是:缓存 key 必须包含模式标识。或者更彻底一点,缓存存全量数据,按模式过滤在应用层做。我推荐后者,因为缓存全量数据可以复用,过滤是内存操作,成本极低。

def get_order(order_id): cache_key = f"order:{order_id}" # 不带模式 order = cache.get(cache_key) if order is None: order = load_order_from_db(order_id) cache.set(cache_key, order, ttl=300) spec = MODE_SPECS[resolve_mode(get_context())] return {f: order[f] for f in spec.fields if f in order}

这样缓存只有一份,模式切换不影响缓存命中率。

5.5 观测缺失:线上出问题不知道走了哪个模式

第五个坑是观测。线上用户反馈“返回字段不对”,但日志里只记录了请求参数,没记录最终用了哪个模式、为什么用这个模式。排查起来全靠猜。

解决方案是:每次模式解析都打点,记录上下文、命中规则、最终模式。可以用结构化日志:

def resolve_mode_with_logging(ctx): for rule in sorted(RULES, key=lambda r: -r.priority): mode = rule.matcher(ctx) if mode is not None: logger.info("mode_resolved", extra={ "source": ctx.source, "user_id": ctx.user_id, "tenant_id": ctx.tenant_id, "rule": rule.name, "mode": mode.value, }) return mode logger.info("mode_resolved", extra={"rule": "fallback", "mode": "lite"}) return Mode.LITE

这样出问题时,直接按 user_id 搜日志,就能看到完整的决策链路。

6. 什么场景该用 context-mode,什么场景别硬套

context-mode 不是银弹,它有明确的适用边界。用对了事半功倍,用错了徒增复杂度。

6.1 适合用的场景:多端、多租户、灰度、实验

这几类场景是 context-mode 的主场:

  • 多端适配:App、Web、小程序、开放平台,同一套接口返回不同字段
  • 多租户系统:不同租户有不同的配置、限流、字段可见性
  • 灰度发布:按用户 ID 或租户 ID 灰度,新旧逻辑并存
  • A/B 实验:按实验分组走不同逻辑,收集数据对比
  • 内部 vs 外部:内部调用走宽松模式,外部调用走严格模式

这些场景的共同点是:上下文维度有限(通常 3-5 个),模式数量可控(通常 3-10 个),切换规则相对稳定。满足这些条件,context-mode 能显著降低代码复杂度。

6.2 不适合用的场景:一次性逻辑、强状态依赖、性能极致敏感

反过来,这些场景别硬套:

  • 一次性逻辑:只在一个地方用一次,写个 if-else 就完了,抽模式反而过度设计
  • 强状态依赖:模式切换依赖大量运行时状态,且状态之间有关联,这时候规则引擎会变得很复杂
  • 性能极致敏感:每次请求都要解析规则、查配置,如果 QPS 极高(比如 10 万+),这点开销也不能忽略。这时候可以缓存解析结果,或者用编译期确定的模式

我的经验判断标准是:如果你发现同一个业务逻辑在 3 个以上地方需要根据上下文切换行为,那就值得抽 context-mode。如果只有 1-2 个地方,先忍着,等第三次出现再抽。

6.3 渐进式落地:从一个小接口开始试点

不要一上来就全系统推广。我建议的落地路径是:

  1. 选一个上下文维度清晰、模式数量少的接口作为试点
  2. 实现上下文采集、模式定义、切换策略三件套
  3. 跑一段时间,观察日志和监控,确认没有上下文丢失、规则冲突
  4. 把试点经验整理成文档和模板
  5. 逐步推广到其他接口,每推广一个都做回归测试

这样风险可控,团队也有时间适应。我见过一个团队一上来就全量改造,结果上下文丢失导致大量请求走了错误模式,线上事故。

7. 几个让 context-mode 更好用的工程技巧

最后分享几个我在实践中总结的小技巧,都是能直接用的。

7.1 上下文快照:出问题时能完整复现

在入口处把上下文序列化存一份,出问题时可以完整复现当时的场景。我通常存在日志里,或者存到 trace 系统:

import json def snapshot_context(ctx: RequestContext) -> str: return json.dumps({ "source": ctx.source, "user_id": ctx.user_id, "tenant_id": ctx.tenant_id, "is_vip": ctx.is_vip, "env": ctx.env, "experiment_group": ctx.experiment_group, }, ensure_ascii=False)

这样排查问题时,把快照喂给测试代码,就能复现当时的模式解析结果。

7.2 模式解析结果缓存:高频接口的性能优化

如果某个接口 QPS 很高,每次请求都跑一遍规则解析有点浪费。可以按上下文的关键字段做缓存:

from functools import lru_cache @lru_cache(maxsize=1024) def resolve_mode_cached(source: str, is_vip: bool, tenant_id: str, env: str) -> Mode: ctx = RequestContext(source=source, user_id="", tenant_id=tenant_id, is_vip=is_vip, env=env) return resolve_mode(ctx)

注意缓存 key 要包含所有影响模式的字段,否则会串。另外缓存要有失效机制,规则变更时清空。

7.3 模式契约测试:防止模式定义和业务代码脱节

模式定义在配置里,业务代码在代码里,两者容易脱节。比如配置里加了一个字段,但数据库里没这个字段,运行时才报错。解决方案是加契约测试:

def test_mode_specs_match_schema(): order_schema = load_order_schema() # 从数据库或 ORM 加载 for mode, spec in MODE_SPECS.items(): for field in spec.fields: assert field in order_schema, f"Mode {mode} requires field {field} not in schema"

这样每次改模式配置,CI 会跑测试,提前发现问题。

7.4 默认模式兜底:永远不要让解析失败

无论规则怎么配,一定要有一个兜底模式。我见过因为规则没命中导致返回 500 的情况,其实完全没必要。兜底模式通常是权限最小、字段最少的那个,保证系统不崩。

def resolve_mode(ctx): for rule in sorted(RULES, key=lambda r: -r.priority): mode = rule.matcher(ctx) if mode is not None: return mode logger.warning("No rule matched, fallback to LITE") return Mode.LITE

这个兜底逻辑看似简单,但能避免很多线上事故。

7.5 规则变更的灰度:新规则先观察再生效

加新规则时,不要直接生效。可以先加一条“影子规则”,只打日志不实际切换,观察一段时间确认影响范围符合预期,再正式启用:

def rule_new_feature(ctx): if ctx.experiment_group == "new_feature": logger.info("shadow_rule_matched", extra={"user_id": ctx.user_id}) # return Mode.FULL # 先注释掉,观察日志 return None

这样新规则的影响面可以先量化,再决定是否全量。

8. 我在实际项目中的体会

context-mode 这套东西,说到底就是把“根据场景切换行为”这件事从散落各处的 if-else 里抽出来,变成一个有结构、可测试、可观测的模块。它不复杂,但需要克制:克制加模式的冲动,克制加规则的冲动,克制把上下文维度扩得太多的冲动。

我最大的体会是:上下文维度控制在 5 个以内,模式数量控制在 10 个以内,规则数量控制在 20 条以内。超过这个范围,说明你的系统复杂度已经失控了,应该先做减法,而不是继续加规则。

另外,观测比功能更重要。模式解析的日志、上下文快照、规则命中统计,这些东西在出问题时能救命。我宁愿少加一个模式,也要把观测做全。

最后再分享一个小技巧:把模式解析的结果放到响应头里返回(比如X-Mode: full),这样前端和测试同学一眼就能看出当前用的什么模式,联调效率能提高不少。当然生产环境要注意别泄露敏感信息,可以只返回模式名,不返回上下文细节。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进