前几天我在技术社区刷到一个热搜词:context-mode,点进去看了一圈,发现评论区完全是在各说各话——搞大模型的人在聊上下文窗口怎么省 token,写前端的在讨论 React Context 重渲染优化,做后端服务的在讲 Go context 的超时传递。同一个词,三种完全不同的战场。
这恰恰说明 context-mode 不是一个具体的库,也不是某个框架的独有概念,而是几乎所有复杂系统里都会遇到的一组共性问题:一段信息该活多久、该传给谁、该在哪里失效。把这组问题想清楚了,不管你是做 AI 应用、写前端框架,还是维护高并发后端,都能少踩很多坑。
这篇文章就把这几条主线全部拉通。你会看到:AI 场景下怎么管理多轮对话的上下文窗口,前端框架里 Context 的正确打开方式,后端服务中 context 元数据的传递与超时控制,以及我踩过的几个非常典型的上下文坑。平时只待在某一端的读者,读完也能理解另一端的人在解决什么问题。
1. 大家讨论 context-mode 时,到底在说哪个 context
1.1 三种最常见的 "context 阵营"
先盘点一下。我在各种技术社区观察到的 context-mode 讨论,至少有三种截然不同的语境。
第一种是 AI 应用开发者。他们口中的 context 是模型上下文窗口,本质是 token 预算。每次调用大模型接口,你传给它的系统提示词、历史对话、检索回来的资料、当前用户输入,全都挤在同一个 token 上限里。这个窗口有硬边界,超了就要丢失信息,没超也要付钱。所以他们讨论 context-mode 时,重点是"如何在有限的窗口里塞进最有用的信息"。
第二种是 Web 前端工程师。他们说的 context 是 React 的 Context API。Function Component 模式下,跨层级共享状态的首选方案就是 createContext + Provider + useContext。它解决的问题是 props drilling:你不想把用户信息一层一层从 App 传到最深层组件,于是用一个 Provider 把它放在组件树的上游,下游任何组件都可以直接取。这实际上是一个"作用域化的全局变量"。
第三种是后端和中间件工程师。他们聊的 context 是 Go 语言里的 context.Context,以及分布式链路追踪里的 trace context。前者承担超时控制、取消传播和链路元数据传递,后者通过 W3C traceparent 这样的标准在多个服务之间接力,让一次 HTTP 请求从网关到数据库的完整路径都串在一起。
1.2 拆开一看,背后是同一套信息生命周期问题
表面上是三个领域,底层逻辑却是同一个:context-mode 本质上是在管理一段"最小必要信息包"的生命周期。
先看信息包本身。无论哪种 context,你都不应该把所有东西往里塞。AI 场景里塞太多历史对话会挤爆窗口,后端 context 里塞太多业务字段会变成隐式传参通道,React 的 Context 塞太多互相无关的 state 会引发整棵组件树无意义的重渲染。所以第一步永远是筛选:什么信息值得我们放进 context?
再看生命周期。一个 context 必然经过创建、传递、失效三个阶段。React Context 的生命周期跟随组件挂载和卸载,组件树重建了,Provider 就重建了;Go context 的生命周期由 WithCancel、WithTimeout 这类函数派生出来,父级取消时子级级联取消;AI 对话的上下文生命周期则靠 token 预算约束,装不下了就把最老的对话淘汰掉。
最后看失效方式。前端和后端的 context 大多是树状结构,失效靠调用链路的取消信号;AI 的 context 是线性的消息队列,失效靠窗口滑动或者摘要压缩。这一差异决定了你会用什么手段去治理它。
1.3 一张表看透不同场景的 context 特性
| 场景 | context 实体 | 作用域 | 生命周期 | 失效方式 | 典型载体 |
|---|---|---|---|---|---|
| AI 多轮对话 | 消息序列 | 单次会话 | 会话期间持续累积 | 窗口截断 / 摘要淘汰 | messages 数组 |
| React 前端 | Context 对象 | 组件子树 | 随组件挂载/卸载 | Provider 重建或卸载 | Value 对象引用 |
| Go 后端 | context.Context | 请求调用链 | 从请求入口到响应结束 | cancel / timeout / deadline | 函数第一个参数 |
| 分布式链路 | SpanContext | 跨服务调用链 | 从网关到最终落点 | 采样策略 / trace 结束 | traceparent header |
这张表放在一起看就很有意思:AI 的 context 是最"贪心"的,它想记住一切,但物理上记不住;前端的 context 是最"宽松"的,你不太担心它泄漏,但担心它乱触发更新;后端的 context 是最"严格"的,它要求你显式传下去,否则超时和取消就断链。
2. AI 应用里的 context-mode:多轮对话与大模型上下文窗口管理
2.1 上下文窗口是一个物理壕沟
我先从当前最热的 AI 场景说起,因为这里的 context 治理直接关系到你能不能用得起大模型。
所谓上下文窗口,就是一次模型调用里能处理的 token 总量。不同模型窗口大小不一样,但不管 8K、32K 还是 128K,它都是一个物理硬边界。你塞进去的东西,除了用户当前问题,还包括系统提示词、历史对话、检索结果、工具返回的 JSON、甚至你自己拼的调试信息。这些都算 token,都会压缩你留给模型生成答案的空间。
刚接触大模型开发的人最容易犯的一个错误是:把"对话历史"理解成"把每一条消息都原样传回去"。真实业务里,一个客服机器人和用户聊了 30 轮,每轮平均 300 token,光历史就是 9000 token。再塞入一个 2000 token 的知识库检索结果和 500 token 的系统提示词,单次调用轻轻松松突破 10K token。你还没开始优化,费用已经上去了。
为什么说这是壕沟而不是护城河?因为模型看到的信息越多,不代表回答越准确。太多无关的历史反而会稀释注意力,让模型抓住一个根本不重要的细节大做文章。我见过不少案例,加了冗长日志之后,模型反而把上一轮的错别字当成重点,回了用户一通莫名其妙的内容。
2.2 一个差点打爆 token 预算的真实场景
我做过的一个人工客服机器人项目,早期版本就是这种"暴力全量"打法。每次用户发消息,代码就把整个 sessions 数组里从第一轮到当前轮的所有消息全部拼进 prompt,调用一次模型。
上线第一天就出问题。前 20 轮对话还挺正常,25 轮以后接口开始频繁报错。有的模型直接返回 context length exceeded,有的静默拒绝了超长输入。我当时的排查思路是先看日志监控:每轮请求的 prompt token 数画成曲线,可以清楚看到它在 10 轮之后进入线性爬升区间,到第 25 轮已经顶到模型窗口上限。
问题定位很清楚,但真正的深坑在后面:就算你不让它报错,把历史截断到窗口内,截断策略选择不对,模型照样表现得很蠢。我一开始简单地把最早的消息从头上裁掉,结果发现早几轮用户已经说过的"我的收货地址是北京市朝阳区XX小区"这种关键信息被无声无息地丢了。用户第 20 轮又问"刚才那个地址送得到吗",模型一脸茫然。
这个坑让我明白:上下文窗口管理不只是"总量的加减法",还是"信息优先级的筛选题"。哪些信息必须保留,哪些可以压缩,哪些可以扔掉,必须有一套规则。
2.3 三种主流的上下文构建策略对比
先放一张对比表,再逐一细说。
| 策略 | 核心原理 | 优点 | 缺点 | 最适用场景 |
|---|---|---|---|---|
| 滑动窗口 | 只保留最近 N 轮对话 | 实现简单、无额外调用成本 | 早期关键信息会丢掉 | 闲聊、短期咨询 |
| 摘要压缩 | 把早期对话用 LLM 提炼成摘要 | 信息密度高、兼顾长期记忆 | 每次压缩要额外成本、有损 | 客服、咨询、长会话 |
| 向量检索 RAG | 把知识/历史向量化,按相关性取回 | 无限记忆空间、按需取用 | 工程复杂度高、检索质量影响大 | 知识库问答、企业助手 |
滑动窗口是性价比最高的起步方案。你只需要定一个"最多保留多少轮"或者"最多保留多少 token",新消息进来,就把最老的消息挤出去。难点在于轮数不是等价的:用户说一句"好的"只占 5 个 token,但上一轮助手给出的方案占 800 token。所以按轮数裁剪很不均匀,按 token 裁剪更合理。
摘要压缩是我现在的主力方案。通过对话进行到中期时,把最早的一块历史丢给模型,要求它产出一段结构化摘要,再把摘要作为 system 消息的一部分放回去。这样做的关键技巧是:摘要必须填充具体的实体和数字,不能拍脑袋写"用户询问了配送问题"这种废话,而要写"用户地址:北京市朝阳区XX小区;配送要求:工作日 9-18 点;当前未解决问题:运费争议中"。否则摘要就是一堆没有信息量的空气,压缩了等于没压缩。
向量检索 RAG 是应对超长知识库的场景。对话历史本身不建议全部做向量化存储上来就用,因为多轮对话的实时性很强,检索召回再拼接的链路延迟不低。知识型问答适合 RAG,但连贯性强的长对话还是得靠摘要或滑窗。
2.4 可以直接抄走的滑动窗口 + 摘要压缩实现
这里给出一个最小可用的 Python 实现。我用 tiktoken 做 token 计数,用滑动窗口控制总量,在超预算时触发一个简单的压缩操作。正式项目中压缩部分要换成真正的 LLM Summarize 调用,但骨架是完全能跑的。
import tiktoken class ContextManager: """多轮对话上下文管理器:滑动窗口 + 摘要压缩""" def __init__(self, system_prompt: str, max_tokens: int = 8000): self.system_prompt = system_prompt self.max_tokens = max_tokens # 上下文窗口预算上限 self.encoder = tiktoken.get_encoding("cl100k_base") self.messages = [{"role": "system", "content": system_prompt}] self.summary = "" # 早期对话摘要,替代被裁剪的原文 def _count(self, text: str) -> int: return len(self.encoder.encode(text)) def _total_tokens(self) -> int: return sum(self._count(m["content"]) for m in self.messages) def add_message(self, role: str, content: str) -> None: self.messages.append({"role": role, "content": content}) while self._total_tokens() > self.max_tokens: if not self._compress(): break def _compress(self) -> bool: # 找出最早的一条非 system 消息 target_idx = None for i in range(1, len(self.messages)): if self.messages[i]["role"] != "system": target_idx = i break if target_idx is None: return False old = self.messages[target_idx] # 演示环境里用截断代替真正的 LLM 摘要 self.summary += f"[{old['role']}] {old['content'][:120]}...\n" removed_token = self._count(old["content"]) # 重建消息列表:system + 摘要占位 + 剩余消息 new_messages = [self.messages[0]] if self.summary: summary_msg = { "role": "system", "content": f"### 历史摘要 ###\n{self.summary}", } new_messages.append(summary_msg) for msg in self.messages[target_idx + 1:]: new_messages.append(msg) self.messages = new_messages return removed_token < self.max_tokens # 防止单条消息过大时死循环这个类的设计思路很直白:add_message 负责追加新消息,追加之后立刻检查总 token 数;一旦超预算,就压缩最早的非 system 消息,把它的内容折叠成一句摘要占位。压缩多少次取决于超标多少,全部由 while 循环驱动。
几个我在实战中会额外处理的细节:第一,压缩阈值不要设在窗口上限,建议设在 70% 到 80%。因为模型是一次性读入全部上下文的,如果每次调用都刚好压在 99% 的临界点,一旦用户这一轮输入特别长,系统直接就没空间了。给生成预留足够的 token 余量,是稳定性的关键。第二,摘要消息的 content 中不要出现换行过多或者无意义编号,尽量让模型"读起来像备忘纸条"。第三,真实项目里要在摘要生成时单独调用一次 LLM,并把它加入成本核算。
2.5 一份参考用的 token 预算分配方案
我习惯把上下文窗口按比例拆成三块:
| 区块 | 建议占比 | 说明 |
|---|---|---|
| 系统提示词 + 摘要 | 10% - 15% | 固定占用,一次性写好,润色好之后别再频繁改动 |
| 历史对话 + 检索注入 | 50% - 60% | 变量最大的部分,主要在这里做裁剪和压缩 |
| 当前输入 + 生成余量 | 25% - 30% | 必须留足,否则模型话说到一半被窗口截断,体验极差 |
这个比例不是定理,只是我踩坑之后总结的保守值。如果你的场景是"用户只发短消息、模型要输出长代码",那就应该把生成余量加大,压缩历史占比。把预算表写进代码注释里,每次调参都有据可依。
3. 软件工程里的 context 传递:从 React Context 到 Go context
3.1 我们为什么需要 context 而不是全局变量
在 AI 里我们已经看到,context 的目的是在有限空间里合理分配信息。回到软件工程领域,最早的动机其实更朴素:全局变量太难用了,但跨层级传参又太啰嗦。
全局变量的问题有三个:一是没有作用域隔离,任何代码都能改它,改完所有地方都受影响,排查困难;二是生命周期不可控,它在进程启动时创建、进程结束才销毁,但很多信息(比如一次请求的用户身份)本来只该活在请求里;三是可测试性差,测试用例之间共享一个全局变量,互相污染是常态。
context 就是"有作用域的全局变量":它在明确的范围内存在,跟随着调用链流动,链走到头它就销毁。这本质上是一种治理约束——你放弃了一点随意性,换来的是可控的生命周期和清晰的传递路径。
3.2 React Context:状态共享的正确姿态
React 的 Context API 核心用法其实不多:先 createContext 创建上下文对象,再用 Provider 包住组件子树并传入 value,最后在任意子组件调用 useContext 读取。它最典型的场景是用户信息、主题、语言包这类几乎处处要用、又不想每个层级都手动传的状态。
但很多新手会在性能上踩坑。原因在于 React 的渲染机制:父组件每次 re-render,<UserContext.Provider value={...}>的 value 对象都会被重新创建。即使你只更新了一个不相关的 state,所有消费这个 Context 的子组件都会跟着重渲染。表现形式是页面稍微复杂一点就开始卡,却找不到瓶颈。
解决办法是配合 useMemo 或拆分 Context。先看 useMemo 的做法:
const UserContext = createContext(null); function App() { const [user, setUser] = useState(null); // 只有当 user 真正变化时才创建新的 value 引用 const userValue = useMemo(() => ({ user, setUser }), [user]); return ( <UserContext.Provider value={userValue}> <Header /> <MainContent /> <Footer /> </UserContext.Provider> ); }更激进的做法是拆分。很多项目的 Context 里同时塞了用户、主题、购物车、通知列表,四类状态互相毫无关联,但因为住在一个 Provider 里,谁更新都拖累所有人。把它们拆成四个独立 Context,各自消费各自的数据,性能会立刻改善。这也是我接手老项目时最喜欢做的低成本优化。
React 场景还有一个容易忽略的原则:Provider 的位置要尽量靠近真正使用它的叶子组件,而不是无脑包在 App 顶层。包得越大,context 的作用域越宽,无关的重渲染影响面就越广。这和后端的"最小的上下文作用域"原则是同一回事。
3.3 Go context:超时、取消与元数据传递的统一出口
Go 的 context 是我最敬畏的一组标准库 API,它把并发编程里"怎么优雅地停下来"这件事变成了语言级别的约定。
核心其实四个:WithCancel 创建可取消的子 context,WithTimeout 或 WithDeadline 给子 context 设定超时,WithValue 往 context 里塞链路元数据,然后通过 Done() 通道和 Err() 感知取消原因。每个请求进来,你在入口拿到一个根 context,往下游每调用一个 goroutine 或 RPC,就基于它派生一个新 context,并确保最终 cancel 被调用。
一个非常典型的做法是设置超时并显式取消:
func handleRequest(ctx context.Context, req *http.Request) error { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() // 保证函数返回时释放资源 // 传递链路元数据 ctx = context.WithValue(ctx, "trace_id", req.Header.Get("X-Trace-ID")) // 下游 HTTP 调用,超过 2 秒自动中断 if err := callDownstream(ctx); err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Errorf("downstream timeout, trace_id=%v", ctx.Value("trace_id")) } return err } return nil }这个写法的关键点是 defer cancel() 和设置超时之间的关系。很多初学者以为 WithTimeout 会在超时到达时自动清理,所以不调用 cancel 也没关系,逻辑上没错,但带来的问题是 context 及其关联资源会一直存活到超时那一刻,而不是函数返回那一刻。想象一下每秒钟几千个请求的 QPS,每个请求的 context 都晚释放几百毫秒,内存曲线会持续向上爬。defer cancel() 是养成的习惯,不是选择题。
Go 社区对 context 有两条很明确的纪律:不要把它存进结构体里,要用就作为函数的第一个参数显式传入;不要用 nil context 作为参数,拿不准就用 context.Background() 兜底。这两条规则我在代码评审里反复提,本质都是为了让 context 的生命周期可追踪。
3.4 从单机到分布式:context 如何跨服务接力
单机内 context 传递相对简单,难题在分布式环境。一次完整的用户请求,从前端到网关,再到订单服务、支付服务、库存服务,每个服务都有自己的内存,单机 context 传到这里就断了。
业界标准做法是把 context 序列化进传输协议头部。OpenTelemetry 家族里的 W3C traceparent 头就是一个典型:网关生成一个带有 trace-id 和 span-id 的头部,每经过一个服务就更新 span-id,但 trace-id 保持不变。这样日志系统可以通过 trace-id 把分散在不同服务里的日志记录拉成一条完整链路。
我实际落地时的做法是三层配套。第一层,在网关统一为每个请求生成 trace-id 和 request-id,并写入 header;第二层,在各服务入口中间件解析 header,注入到该服务的 context 中;第三层,在日志结构化时把 context 里的 trace-id 自动带上。排查问题时一句"基于 trace-id 查全链路日志"就能搞定,不用再人肉拼时间线。
关键是要区分哪些字段应该进入分布式 context。我平时只有这三类是必放的:链路标识(trace-id、span-id)、调用方身份(client-id、user-id)、环境信息(region、env)。业务参数一概不放。为什么?因为这些字段要贯穿所有服务,越小、越稳定越好。你放一个"当前订单金额"进去,下游所有服务都能读到一个跟你完全无关的金额,维护成本直接失控。
4. 上下文模式最容易翻车的四个真实场景
4.1 场景一:无限膨胀的对话历史导致接口超时和费用失控
这是一个把 AI 场景和工程问题混在一起的典型事故。有一次我们给内部知识库助手接大模型,提示词里除了系统设定,还把员工手册、产品 FAQ 全部塞了进去,加起来一万多 token。每次问答都带着这一大坨,模型接口的响应时间从 2 秒涨到 6 秒,月账单翻了三倍。
排查过程是:先看延迟指标,发现 p99 持续恶化;再看 token 统计,发现固定提示词占了一半预算;最后做对照实验,把提示词换成一份 2000 token 的精简版之后,指标立即恢复。问题是明显的,但根因是设计阶段没人把"提示词也是上下文的一部分"当回事。现在我的习惯是:每次改 prompt,必须同步把 token 数和成本估算写进变更记录。
4.2 场景二:共享 map 引发的用户数据串号
这是我刚转后端时犯过的经典错误。当时实现一个轻量级会话服务,顺手用一个全局map[userId]SessionContext存会话。上线后诡异的事出现了:A 用户问了"我的余额多少",系统返回的是 B 用户的信息。
排查链路很磨人,最后定位到问题出在 map 的并发写:两个请求同时操作同一个 key,一个写入 value 的引用,另一个也写入同一个引用,数据就串了。更深层的原因是 SessionContext 的生命周期被错误地设置成了"进程级共享",而正确做法是每个请求创建自己的 context,用请求级存储承载会话信息。修复方案是引入真正的会话服务,每次请求从存储重建 context,彻底告别共享可变 map。这个坑教会我一件事:context 的生命周期边界,在架构设计的第一天就要画清楚,不能事后想起来才处理。
4.3 场景三:context 变成了隐式传参通道
还有一类代码让我每次做 code review 都头疼:整个 service 层方法签名清一色是func (s *Service) DoSomething(ctx context.Context) error,业务字段全都塞在 ctx 里取。比如ctx.Value("userId")、ctx.Value("orderId")散落在几十个文件里。
表面看它很方便,代码写起来短,调用方也不用显式传参。但它把接口契约彻底毁了:读代码的人根本不知道这个方法到底依赖哪些数据,编译器也检查不了,测试更是难写——你得先手工往 ctx 里塞一堆键值才能构造环境。我处理这类代码时的原则是:context 只放链路元数据,也就是 trace-id、user-id、语言这类横切关注点;订单 ID、金额、分页参数这类业务数据,必须作为显式参数传进去。区分标准就一句话——业务代码直接依赖且会变化的,就不属于 context。
4.4 场景四:异步任务里的 context 超时失效
最后一个坑是跨 goroutine 或异步队列时 context 超时悄悄丢失。我在一个消息推送服务里见过这样的代码:主请求设置了 5 秒超时,然后把"发送 Webhook"丢进一个 goroutine 异步执行,但 goroutine 里用的是context.Background(),而不是从主请求派生出来的那个带超时的 context。结果是上层 HTTP 5 秒超时报错了,底下的 Webhook 却还在傻傻地重试,拖了 3 分钟才放弃。
修复方法很直接:不要把 context 丢掉,要从入口 context 派生一个新的 timeout context 传给异步任务,并且确保异步任务内部监听了 Done 通道。即使主请求已经取消,异步任务仍然保留一个独立的超时控制,不会无限拖尾。这个场景也提醒我:context 的传播是显式行为,任何一次"偷懒不传"都可能成为线上事故的伏笔。
5. 从零构建一套适合自己的 context 治理方案
5.1 第一步:给 context 画边界和生命周期
我每次开始一个新项目,做的第一件事不是在代码里写 context,而是在设计文档里画一张生命的边界图。更准确地说,是回答三个问题:context 在哪里创建?在哪里传递?在哪里终止?
以 Go 后端为例,推荐的做法是:创建点在最外层 HTTP/gRPC 中间件,基于context.Background()派生并注入 trace-id;传递点是每一次 RPC、DB 访问、异步任务调用,严格作为第一参数;终止点是请求退出时,统一 defer cancel(),确保资源及时释放。这套约定写进团队的代码规范之后,"这个 context 哪来的、什么时候没的"就能一眼看清。
| 阶段 | 动作 | 负责层 |
|---|---|---|
| 创建 | 注入 trace-id、user-id、语言等元数据 | 网关 / 入口中间件 |
| 传递 | 超时控制、取消信号、链路信息随调用链流动 | 业务服务、异步队列 |
| 终止 | 统一 cancel、超时感知、日志落 trace 字段 | 出口逻辑、异常处理 |
| 观测 | 指标监控、日志关联、采样追踪 | 可观测性平台 |
5.2 第二步:设计上下文信息字典
context 里的字段必须集中定义,不能散落在各个服务里随手起名字。我在项目里维护一张"上下文信息字典",每条字段都有明确的所有者和值类型。
推荐的最小集合是:trace-id(链路追踪)、request-id(调试定位)、user-id(用户身份)、lang(语言环境)、env(环境标识)。再多就是多余。每个服务只消费字典里定义的字段,新字段必须经过评审才能加入。这个约束能有效防止隐式传参通道出现——你想往 context 里塞业务参数的时候,先问一下字典里有没有,没有就去改函数签名。
5.3 第三步:让上下文可观测
context 太隐形,肉眼看不见,所以必须量化。我长期监控的指标有三个:AI 场景的每轮 token 消耗曲线,超过预算 80% 就告警;后端场景的链路完整率,也就是从网关进、到 DB 落,中间所有 span 都齐的比例,低于阈值说明有 context 断链;React 场景的 Context 重渲染次数,超过阈值说明 value 没有稳定。
日志系统也要配合。每一条业务日志里都要带 trace-id,这样当用户反馈问题时,你把 trace-id 拿到手,就能把前后端、消息队列、大模型调用记录串成一条完整的链路。日志里看一眼 context 摘要的格式大致是trace_id=abc123, user_id=u_042, cost=1234tokens, status=ok,一目了然。
5.4 落地清单:五个我现在还在用的习惯
如果只想带走几个可执行的动作,那就是这五条。
第一,调用大模型之前先显式计算 token 数,超预算就压缩,绝不裸奔。第二,上下文窗口按 10/60/30 比例分配固定预算,动态部分只动历史区块。第三,业务参数显式传,链路元数据走 context,不混用。第四,所有 context 字段集中定义,新增字段过评审。第五,按时看三个指标:token 消耗曲线、链路完整率、Context 重渲染次数。这五条覆盖了我过去踩过的所有大坑,现在都变成了默认习惯。
最后说一点我自己的体会。context-mode 这个词之所以在不同的技术圈里都有热度,本质上是因为现代软件复杂度已经高到"信息流"本身就是一层必须管理的边界。无论是给模型喂上下文,还是给请求传链路信息,你做的都是同一件事:用最小的成本让最关键的信息出现在最需要它的地方。我之前做过的项目里,凡是 context 失控的,后期都在用大量加班补窟窿;凡是提前把 context 的生命周期、作用域、观测手段设计好的,后面都跑得很省心。希望这份总结能让你的下一个项目,从第一天就把这层边界画明白。