先说一个很现实的问题:同一个大模型,为什么别人用着像“读心术”,你用它却像“金鱼记忆”,聊到第三轮就开始胡言乱语?大多数情况下,问题不在模型本身,而在上下文——准确说,是你根本没有一套可切换、可管理的上下文模式。
我这几年一直在做 LLM 应用开发,从简单的单轮问答,做到带检索增强的 Agent 系统,中间踩过最大的坑就是“上下文失控”。后来我把整套上下文管理沉淀成一个“context-mode”模块,才算是把这个问题彻底理顺。所以今天这篇东西,不适合想看“大模型原理”的人,更适合正在写 LLM 应用、做 Agent 开发、或者调 Prompt 调到怀疑人生的朋友。我会从模式分类、模块设计、代码实现到排查实战,把 context-mode 这件事一次讲透。
1. context-mode 到底是什么:先别急着写代码
很多人一听“上下文模式”,第一反应是“不就是把历史消息拼进 prompt 吗”。如果只停留在这一步,你迟早会被上下文折磨得没脾气。context-mode 的本质,是给对话系统设计一套可感知、可切换、可压缩、可检索的上下文管理策略,让模型在不同的任务阶段,只看到它该看到的信息,并且以最省 token 的方式看到。
1.1 从“上下文”到“上下文模式”:需求是怎么来的
先拆一个最简单的多轮对话流程。用户问“帮我总结一下上个月销售数据”,模型要回答到位,至少需要知道三样东西:历史对话决定了“上个月”指哪一段;数据库或文档决定了销售数据长什么样;系统提示词决定了回答用什么格式、什么口吻。这三样东西,都是上下文。
问题在于:上下文的来源不同、时效性不同、重要性也不同。如果一股脑全塞进模型,第一个问题是成本爆炸——所有 token 都按输入计价;第二个问题是注意力稀释——模型会分不清哪些信息更重要;第三个问题是“上下文污染”——比如上一次对话里用户随口说了句“其实那个方案不太好听”,下次开场模型可能还在纠结这句话。
于是我在实际项目里把 context-mode 拆成了四种基础模式:短上下文模式(short)、长上下文模式(long)、检索增强模式(rag)、任务隔离模式(task)。这四种模式不是凭空拍的,而是从真实需求里长出来的:
- 短上下文模式:适合闲聊、简单问答、指令清晰的任务。只需要最近几轮对话,冷启动快,token 消耗极低。
- 长上下文模式:适合需要跨多轮推理、延续性强的任务,比如写代码、写文档、做方案。历史消息要尽量保留,但需要压缩策略。
- 检索增强模式:适合知识密集型任务,比如问“我们产品哪些型号支持这个协议”。不能靠模型记忆,必须实时从库里查,然后把结果注入当前对话。
- 任务隔离模式:适合一个进程里同时跑多个独立任务的场景。比如一个客服机器人,一边在帮用户查订单,一边在记录用户反馈,两件事的上下文不能串。
这套分类,是我后来所有配置和代码的基础。
1.2 为什么说“上下文模式”不是优化项,而是地基
我见过不少团队,第一版 demo 直接“把所有历史消息无脑拼进 prompt”,测起来还挺好用,结果一上线就崩。核心原因有三个:
第一,大模型的输入长度是硬约束。上下文窗口是固定死的,超过就报错或者被静默截断。业务数据稍微一多,比如日志、文档片段、多轮语音转写文本,分分钟顶满窗口。你没有模式管理,就只能被动截断,截断哪一段由代码顺序决定,而不是由信息重要性决定——这等于把方向盘交给巧合。
第二,token 成本是线性上升的。很多人的上下文管理,本质是“越来越多”,每次请求把所有历史全带上。一轮对话可能还好,十轮、百轮之后,光输入 token 就是一笔不小的开销。我在生产环境里见过最夸张的例子,一个 8k 窗口的模型,上下文里塞了 7.5k 的历史消息,真正这次回答需要用到的信息还不到 1k。等于花了大头钱,买了最差的效果。
第三,上下文一致性是产品体验的根基。用户对“AI 助手”的信任,很大程度来自“它还记得我之前说的”。如果一个系统做上下文管理只用滑动窗口,翻几轮之后把关键约定滑掉了,用户立刻会觉得“这 AI 很不靠谱”。滑动窗口只是 context-mode 里的一个小手段,不能当作全部。
所以我在所有项目里都会立一个规矩:任何对话入口进入业务逻辑之前,必须过一层 context-mode 路由。谁来做路由?就是我们下面要讲的模式选择策略。
2. 主流上下文模式拆解:选型比写代码更重要
说实话,context-mode 不存在“哪种模式最好”,只存在“哪种模式此刻最合适”。我在不同项目里反复试过几种路线,下面把它们的适用范围、优点、代价一次讲清楚。
2.1 全局模式、局部模式、自动模式怎么选
我最早用的分法,是接口层级的分法:全局上下文、局部上下文和自动上下文。这套分法特别适合多工具协作场景。
- 全局上下文:比如系统 Prompt、用户画像、企业知识库的公共部分,任何一次请求都会注入。优点是稳定一致;缺点是消耗固定 token,如果写得又长又啰嗦,整个系统都在为这段僵尸文本买单。
- 局部上下文:某个具体任务携带的临时信息,比如“当前正在翻译的文件片段”“当前正在处理的工单内容”。优点是精准;缺点是容易漏,开发者经常忘了把该带的带上。
- 自动上下文:系统根据本次输入判断,需要多少历史就注入多少,需要查文档就自动触发检索。优点是省心;缺点是“判断”本身有延迟和不确定性,处理不好反而比固定模式更乱。
如果项目是重业务流程、数据来源清晰的,我建议尽量用“显式模式”——也就是说,开发者自己决定什么情况下切到哪种上下文,而不是让模型或者代码“猜你什么时候要什么”。自动模式我一般在 AI 编程工具的场景里才重度使用,因为它面对的问题域足够集中,推测出错也不至于太离谱。
2.2 窗口滑动、摘要压缩、关键信息锚定:三种长会话策略
长会话一直是 context-mode 的硬骨头。我在超过 20 轮连续对话的 Agent 产品里,测试过三种策略,各有套路:
策略一:窗口滑动(Sliding Window)。保持最近 N 轮消息,更早的全部丢弃。优点是实现极其简单,一个messages[-N:]就完事;缺点是没有记忆,用户第 1 轮说的“我叫王小明,帮我选一款适合油皮的面霜”,第 30 轮可能就忘了。我的经验是:这种策略只适合客服转接、临时问答这类“不需要跨轮记住事实”的场景。
策略二:摘要压缩(Summarization)。对超过阈值的早起对话,用模型生成一段摘要,然后把摘要当作一条消息放在最前面。优点是能在有限窗口里保留超长历史的核心信息;缺点是摘要一定有信息损失,而且“生成摘要”本身又要调一次模型、花一次 token、增加一次延迟。我在实践中会把摘要触发的历史长度阈值设到 8-10 轮以上,避免频繁触发。
策略三:关键信息锚定(Anchoring)。从历史消息中抽取结构化关键信息,比如用户画像、业务参数、任务进度,然后以固定格式写到系统提示词或专属锚定字段里。这个策略最惊艳:它用极少的 token 保留了“最重要的信息”,而且这些信息是结构化的,模型不容易搞混。我现在的做法是,把实体抽取、意图识别、槽位填充的结果统一维护为一个“对话档案”,每次请求自动注入档案,再配合窗口滑动的短期记忆使用。等于短期记忆靠原文,长期记忆靠档案,成本和效果都平衡得最好。
这三种策略不互斥。我在第二个正式项目里的做法是:1-6 轮用窗口滑动保原文,6-12 轮用摘要压缩过渡,所有轮次产生的关键信息都写入锚定档案。每个项目的数据特点不同,但这套基线方案值得直接抄走。
2.3 检索注入模式:把外部知识塞进上下文的正确姿势
带知识库的 LLM 应用,本质就是在做“检索增强的 context-mode”。很多人的第一版是:把知识库全量塞给模型,让它自己看。这立刻就会撞上窗口上限。正确的做法是把“检索”和“上下文注入”做成两个独立的环节。
我给一个参考链路:用户问题 → 改写出若干个检索子问题 → 向量库召回 top_k 块 → 按相关度/去重/摘要重排 → 精选出满足 token 预算的片段 → 按固定模板注入到上下文的<context>区块 → 模型基于注入内容回答。
这里有两个细节极容易翻车。第一,检索结果必须有时效性排序,不能纯按向量相似度选,否则三个月前的旧文档很可能反复抢占上下文名额。第二,注入格式必须稳定,我用的是<context> ... </context>包裹,里面每个片段用编号和来源标注。格式混乱是很多 RAG 项目幻觉率偏高的隐形原因。
2.4 隔离模式的适用场景:别让任务之间互相“传染”
我做过一个单进程多任务的智能助手,一边回答售前问题,一边处理售后工单。最开始共用一个会话上下文,结果出现过一个特别滑稽的 bug:用户在售后流程里说“我真的很生气”,结果下一个售前推荐话术里,模型居然用“我真的很生气”作为开场白。这就是任务上下文串味了。
后来我设计 task-mode:每个任务挂独立的 session_id,上下文各自管理,互不读取。任务切换时,模型收到的 prompt 完全重新组装,不掺一点别的任务的痕迹。除了分离会话,任务内部还要有独立的 token 预算和独立的检索索引,这样才不会出现 A 任务把 B 任务的上下文窗口占满的情况。如果你也在做“一个入口对应多种业务意图”的产品,我建议直接从架构上把隔离模式内置进去,别等到出问题了再补。
3. context-mode 代码落地:一个可以直接用的核心模块
概念讲再多,不如给一段能跑的代码。我下面分享的这个模块,是我在多个项目里反复精简沉淀下来的版本,核心目标是“模式可切换、逻辑可扩展、上线可监控”。你完全可以把它当成脚手架,按自己的业务改。
3.1 核心数据结构:别再用数组傻存消息
很多人存上下文就是用List[dict],比如[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]。这不是不行,只是后面你会发现问题:无法计算 token、无法给消息打标签、无法区分“系统注入的历史摘要”和“本次对话原文”。所以我用一个带元信息的消息类。
from dataclasses import dataclass, field @dataclass class CtxMessage: role: str # system / user / assistant / tool content: str tags: list = field(default_factory=list) token_count: int = 0 source: str = "" # raw / summary / anchor / retrieval timestamp: float = 0.0这个结构最大的好处是:每条消息都知道自己从哪里来。调试“模型为什么看到这句话”的时候,你不用翻日志,直接看source字段就知道是原始对话、摘要压缩、还是检索注入。
3.2 模式管理器:让切换逻辑收敛到一个类里
我设计了一个ContextModeManager类,负责维护消息队列、执行模式策略、生成最终 prompt。核心思路是:外部业务代码只需调用add_user()、add_assistant()、switch_mode(),完全不用自己处理消息裁剪和摘要。
from typing import List, Literal, Optional ModeType = Literal["short", "long", "rag", "task"] class ContextModeManager: def __init__(self, max_context_tokens: int = 6000): self.max_context_tokens = max_context_tokens self.system_template = "你是智能助手,请基于提供的上下文和对话历史回答问题。" self.history: List[CtxMessage] = [] self.mode: ModeType = "short" self.anchor: dict = {} # 关键信息锚定字段 self.retrieved_ctx: list = [] # 检索注入的文档片段 self.summary_cache: str = "" def switch_mode(self, mode: ModeType) -> None: self.mode = mode if mode == "short": self._trim_short() elif mode == "long": self._refresh_long() elif mode == "rag": self._keep_recent() elif mode == "task": self.history.clear() self.anchor = {} def add_user_message(self, content: str) -> None: msg = CtxMessage(role="user", content=content, source="raw") self.history.append(msg) self._apply_mode_policy() def apply_anchor(self, key: str, value: str) -> None: self.anchor[key] = value def set_retrieved_context(self, chunks: List[str]) -> None: self.retrieved_ctx = chunks def build_prompt(self) -> str: parts = [self.system_template] if self.anchor: anchor_text = "用户关键信息:" + ";".join( f"{k}={v}" for k, v in self.anchor.items() ) parts.append(f"<anchor>{anchor_text}</anchor>") if self.retrieved_ctx: ctx_block = "\n\n".join( f"[{i+1}] {c}" for i, c in enumerate(self.retrieved_ctx) ) parts.append(f"<context>{ctx_block}</context>") if self.summary_cache: parts.append(f"<summary>{self.summary_cache}</summary>") for m in self.history: parts.append(f"{m.role}: {m.content}") return "\n\n".join(parts)这个是简化版本,实际项目里我还加了token_count的实时统计和超限报警,但核心结构就是上面这几十行。switch_mode里的_trim_short、_refresh_long、_keep_recent是策略具体实现,我下面拆开说。
3.3 三种模式的具体策略:short 直接砍、long 压缩补、rag 保近期
short 模式:
def _trim_short(self): # 保留最近 4 轮原始消息,其余丢弃 self.history = self.history[-8:] self.retrieved_ctx = [] self.summary_cache = ""8 条消息对应 4 轮对话,适合轻量问答。注意这里我把检索上下文和摘要都清空了,因为 short 模式的定位就是“不依赖外部信息,快速响应”。
long 模式:
def _refresh_long(self): # 历史超过10条,且还没有摘要缓存,则触发摘要压缩 if len(self.history) > 10 and not self.summary_cache: old_msgs = self.history[:-8] text = "\n".join(m.content for m in old_msgs) self.summary_cache = self._call_summarizer(text) self.history = self.history[-8:]_call_summarizer在真实工程里是调用一个大模型 API,把旧消息总结成两三百字的摘要。这里有个容易踩的坑:摘要里必须保留“用户明确提出的要求”,否则用户三句话前的指令,模型转眼就忘了。我在 prompt 里会特别加一句“保留用户的所有明确要求、偏好和禁止事项”。
rag 模式:
def _keep_recent(self): # 只保留最近 6 条原文,主要信息靠 retrieved_ctx 提供 self.history = self.history[-6:]注意:rag 模式不是把检索结果直接一股脑塞进retrieved_ctx就完了,还需要在上层把知识检索和 prompt 组装串起来。我通常会在业务层先查知识库,拿回结果后调用set_retrieved_context(),再走build_prompt()。
task 模式:核心是给每个任务实例单独维护 manager,而不是在同一个 manager 里清空数据。因为清空之后你没法恢复上一个任务的状态。我一般会维护一个Dict[str, ContextModeManager],key 是 session_id,每个任务互不干扰。
3.4 和 LLM API 对接:组装 prompt 时最容易犯的错误
直接调用 OpenAI 或者国产大模型 API 时,很多人习惯直接把messages参数传给 SDK。但自建 context-mode 后,我强烈建议你用一个统一接口把“结构化消息列表”和“组装好的 prompt 文本”都生成出来。
一个常见错误是:系统提示词里写了很长的格式要求,结果 history 里的历史消息也带着一套旧的格式要求,模型会 confused。我目前的处理方式是把格式要求放进system_template,历史消息一律只放纯对话内容,检索片段放<context>标签,锚定信息放<anchor>标签。这样每个部分职责单一,模型更容易遵循。
对接时有一个参数一定要检查:max_tokens(也就是本次回答允许生成的最大长度)。很多人把max_tokens设成和模型上下文上限一样大,这是必炸的。比如模型支持 8000 窗口,你的 prompt 已经有 7500,那输出最多只能有 500,但你还是设了 8000,API 会直接报错或截断。我一般会按“输出预留 25% 窗口”的原则设置,比如窗口 8000 就设max_tokens=2000左右。
4. 上线前必须处理的四个细节:token 估算、压缩时机、成本上限、调试工具
代码跑通只是第一步。context-mode 想在生产环境稳定运行,下面四个细节值得一个个打磨。
4.1 token 估算:别等超限了才去数
模型的 tokenizer 是按字节训练的,不同语言占的 token 数不一样。中英文混合场景下,最粗暴的估算是“每 1 个汉字约 1-2 个 token,每 4 个英文字符约 1 个 token”。为了不阻塞主流程,我习惯在进入 prompt 组装之前做一次快速估算,超过预算就直接触发降级策略。
def estimate_tokens(text: str) -> int: # 粗略估算:中文按1.5字符/token,英文按4字符/token chinese_chars = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff') other_chars = len(text) - chinese_chars return int(chinese_chars / 1.5 + other_chars / 4) + 2这个函数当然比不上官方 tokenizer 精确,但作为“是否超限”的快速闸门已经足够。真正的精确计数,可以在后台任务里异步调用官方 tokenizer 做校验。我在日志里会同时记录estimate_tokens和实际usage.prompt_tokens,两者偏差超过 30% 就报警,说明估算模型需要调整。
4.2 压缩时机:摘要不是“有钱任性”,而是“精打细算”
每次触发摘要都要花一次模型调用的钱。如果每两轮就压缩一次,成本不降反升。我的做法是两段式触发:
- 第一段:消息总量超过窗口的 60%,触发规划,但只生成摘要,不改动原文;
- 第二段:消息总量超过窗口的 85%,才真正用摘要替换旧消息。
为什么分两段?因为生成摘要本身要调模型,而且它产出的文本也算 token。你在 60% 的时候就替换,摘要本身可能占 5% 的空间,等于 55% 的历史被压缩了,很划算;但如果你 80% 才触发,压缩完仍然逼近窗口上限,后面随便再来一条用户消息又超限,整体体验会很紧张。
4.3 成本上限:给上下文模式装一个“刹车”
context-mode 在省成本这件事上能做的事很多,但你不加约束,它也可能反过来烧钱。我在生产环境里会给每次对话设置三个硬性预算:
- 单次请求输入预算:比如 2000 token,超过就不允许追加历史;
- 单次会话累计预算:比如 50000 token,超过就强制切 short 模式;
- 每日总预算:接告警,超过指定金额就暂停非核心场景的 long/rag 模式。
这三个预算,分别在 manager 初始化、add_user_message 和业务层的路由逻辑里做检查。实际跑下来,很多“成本失控”案例都不是模型问题,而是历史消息无限膨胀导致的。
4.4 调试工具:把“上下文视图”导出来看看
调试 context-mode 和调试普通代码完全不同,因为你没法单步跟踪模型“为什么看到这些”。我的做法是给 manager 加一个debug_dump()方法,导出完整的上下文视图:当前模式、anchor 内容、摘要缓存、检索片段、历史消息数量、token 估测。出问题的时候,把这个视图贴进日志,几秒钟就能定位是哪个环节丢了信息。
def debug_dump(self) -> dict: return { "mode": self.mode, "anchor": self.anchor, "summary_cache": self.summary_cache, "retrieved_ctx_count": len(self.retrieved_ctx), "history_count": len(self.history), "estimated_tokens": sum(m.token_count for m in self.history), }我遇到过最典型的情况是:业务方反馈“模型怎么突然不知道用户名字了”,一查debug_dump(),发现 anchor 字典里name字段被后来一次空值覆盖了。没有这个视图,这种问题排查至少要多花一晚上。
5. 典型问题排查实录:上下文相关 bug 都在这里了
我把实际项目里遇到的上下文相关坑整理成了一张速查表,遇到类似症状可以直接对着排查。这些 bug 有一个共同特点:表面看起来是“模型出问题了”,实际上全是上下文管理策略的锅。
5.1 常见问题速查表
| 症状 | 常见原因 | 处理办法 |
|---|---|---|
| 模型答非所问,像在回应上一轮内容 | 历史消息过多,模型注意力被旧信息干扰 | 切 short 模式,只保留最近 1-2 轮 |
| 长对话后,模型忘了用户最早提出的要求 | 窗口滑动把早期指令丢掉了 | 把“用户要求”写入 anchor 字段,长期保留 |
| 明明传了文档,模型却说“没有相关信息” | 检索注入格式不规范,模型没识别出来 | 统一用<context>标签包裹,给出编号 |
| 输出经常被截断,或 API 报超限错误 | 输入 prompt 占用太多,输出空间不够 | 把 max_tokens 调到窗口的 20%-25% |
| 连续多轮后回答质量明显下降 | 历史中混入了大量中间推理过程 | 对工具调用/思考过程做“过程折叠”,只保留结论 |
| 回答上次和这次说法互相矛盾 | 两个任务共享了同一个会话上下文 | 改为 task 隔离模式,按 session 分开维护 |
| 成本突然翻倍 | 摘要压缩触发过于频繁,或历史从未清理 | 调整压缩阈值,设置会话累计预算 |
| 检索结果相关性很高,但回答还是错 | 注入的片段太多,关键段落被淹没 | 限制检索片段数量,top_k 建议控制在 3-5 |
5.2 一个印象深刻的线上事故:摘要压缩把“禁止事项”吞了
我做过一个财务问答助手,政策规则里明确写着“禁止向用户提供具体投资建议”。系统在长会话中把早期对话压缩成了摘要,摘要模型认为这句“禁止事项”跟当前问答无关,就把它丢了。结果用户在长会话末尾问“我该买哪只基金”,助手居然真的给了一条投资建议。
这事之后我给摘要压缩的 prompt 加了一句硬性要求:任何否定性、禁止性、合规性内容,必须原样保留在摘要中。并且所有摘要生成完成后,会做一个关键词扫描,如果发现原文有“禁止”“不要”“必须”“底线”这类词而摘要里没有对应内容,就拒绝替换,直接走完整历史。这个约束看起来简单,但能拦住绝大多数类似的合规风险。如果你的业务涉及合规条款、安全红线,我强烈建议你把它做进压缩策略里。
5.3 排查思路:问自己四个“在哪里”
遇到上下文相关 bug,我一般按下面四个问题定级定位:
- 信息到底在哪里丢失的?是检索环节没查到,还是注入环节没拼进去,还是压缩环节被删了?
- 模型有没有真的看到这条信息?看 debug_dump 和实际 prompt 日志,别靠猜。
- 这条信息出现在 prompt 的哪个位置?如果夹在 5000 token 中间,它的影响力远不如放在开头或结尾。
- 上下文里有没有“噪音”在干扰判断能力?比如历史里用户随口说的“随便”“差不多”,模型会当真。
这套排查思路能帮你把上下文模块的每个环节单独验证,而不是一有问题就归咎于“模型太笨”。实际上,我观察到的比例是:真正模型能力不够导致的失败,不到三成,其余都是上下文的“输入问题”。
6. 面向 AI 编程场景的 context-mode:另一个热门落地
前面讲的都是自建 LLM 应用的通用方案。现在还要提一个特别常见的场景——AI 编程工具里的 context-mode。
用过 Cursor、Copilot Chat、Codeium 这类工具的人,应该对“@文件”“@Codebase”“自动检索”不陌生。这背后其实就是一套封装好的上下文模式。我对这类工具的使用经验是:理解它的模式,比盲目堆提示词更重要。
在 AI 编程场景里,上下文来源通常分为四类:当前打开文件的局部上下文、配置文件里的持久指令(如 CLAUDE.md、.cursorrules)、代码仓库的全局索引、终端或调试器的运行时输出。高效写代码的姿势是:显式指定“本次任务关注哪个文件”,让局部上下文主导;仓库级向量检索只用来查依赖关系和历史实现,不要每次都把全库代码塞进去。
我还发现一个黄金比例:一个任务里,局部文件上下文占 60%,持久指令占 15%,检索结果占 20%,其他占 5%。超过这个比例,模型特别容易跑偏。这个比例不是精确真理,但它提醒你:上下文模式的本质,永远是“在有限窗口里做最优信息排布”。
7. 从单会话到多 Agent:context-mode 可以再往前走一步
如果你已经在单个会话里把 context-mode 跑顺了,可以尝试把它扩展到多 Agent 协作场景。在我的一个实验性项目里,多个子 Agent 各自维护独立的 ContextModeManager,它们通过一个“黑板系统”共享少量锚定信息。父 Agent 负责拆解任务、汇总结果;子 Agent 只接收和自己相关的上下文片段,干完活就释放。
这套架构的好处是:每个子 Agent 的上下文窗口都很小、很专注,token 消耗低,而且互相之间不会串味。坏处是:需要设计清晰的“上下文交接协议”,否则子 Agent 之间信息断档,父 Agent 又得反复解释背景,反而更费 token。
我给一个简单的协议例子:父 Agent 写给子 Agent 的指令必须包含 task_brief(任务说明)、input_data(输入数据引用)、constraints(约束条件)、done_criteria(完成标准)。四个字段缺一不可。这让每个子 Agent 在不看完整历史的情况下也能高质量完成任务。
如果你对 Agent 调度有兴趣,我强烈推荐先从“最小上下文交接”开始做,而不是一上来就搞复杂的消息总线。先让两个 Agent 用一份简洁协议协作,跑通后再加第三个。
回到开头那个问题:同一套模型,凭什么别人用像读心术,你用像金鱼?差别就在上下文管理。context-mode 不是什么高深算法,它是一套“让该出现的信息不缺席,让不该出现的信息不打扰”的工程纪律。我个人的体会是:把这个模块做扎实,比堆一千行 Prompt 技巧都管用。先搭一个能切换的骨架,再慢慢调策略,你会发现模型还是在那个水平,但产品体验,已经完全不同了。