目录
文章目录
- 目录
- 为什么需要上下文工程
- 上下文工程的目标
- 上下文内容组成的有效模板
- 上下文工程的关键技术
- 记忆系统
- [RAG 知识库系统](#RAG 知识库系统)
- 上下文管理
- 可观测性
为什么需要上下文工程
设想一个智能客服系统。用户发来一句话: "这个咖啡机能退货吗?退货要运费吗?"
一个没有上下文工程的 Agent 只能拿着这句话去问模型,模型则只能给出一段通用的、放在任何电商平台都成立的退货说明,这对用户没有价值。
而一个做了上下文工程的 Agent 会先把三个层级的信息补齐,再去问模型:
- 即时信息:当前用户的输入内容本身。
- 历史信息:这个用户过去的交互与行为记录,例如 "3 天前购买的一台咖啡机" 、 "该用户是 VIP 会员"、"该用户说中文"。
- 外部信息:与场景相关的系统数据,例如 "这个咖啡机" 指的是哪一笔订单,需要由系统数据库补全;公共知识,例如 "该商品支持 7 天无理由退货" 、 "VIP 会员退货免运费" 需要由知识库补全。
三层材料齐备之后,模型才可能直接给出 "可以退货且免运费" 这个结论。

如上图,这三层信息并非静态地躺在某个地方等人取用,它们各有各的来源与时效,例如:
- 即时信息来自当轮输入;
- 历史信息由记忆系统沉淀;
- 外部信息由知识库检索或工具调用获取
因而上下文是被动态组装出来的,而不是被预先写好的。
上下文工程的目标
上下文工程的目标可以用一句话概括,让模型在每一轮推理中都能拿到恰好够用的材料。
- 多给并非好事 :无关材料会稀释注意力、抬高词元费用,还会把关键信息挤到模型并不擅长利用的中段位置。
- 推理速度变慢:处理更长的上下文需要更多计算,响应时间随之增加。
- 模型表现下降:尽管上下文窗口越来越大,但研究发现模型在超长上下文中检索关键信息的能力可能反而下降,关键信息若落在中段位置,被利用的概率明显低于落在首尾。
- 词元费用升高:上下文越长输入词元越多,对于需要频繁交互或处理大量文本的应用,费用会很贵。
- 少给同样不行 :模型会开始遗忘、重复提问、前后自相矛盾。
- 上下文窗口的限制导致遗忘:模型通过一个有限的上下文窗口(Context Window)处理信息,所有输入都必须进入这个窗口,一旦信息超出窗口,模型就无法再访问它,这直接导致了 "遗忘" 问题,也被称为 "窗口溢出" 问题。
- 难以处理多轮与复杂任务:对于需要跨多轮对话或执行一系列子任务的复杂任务,模型很难保持连贯的进展,因为它会不断忘记之前的步骤与决策。在 Agent 场景下这一矛盾被显著放大,因为工具的定义与工具的返回值都要占用上下文,同时由于 Agent 具有自主工作的能力,与模型的平均交互轮数也大大增加。
- 无法个性化:由于不记住特定用户的历史偏好、习惯或之前的互动,模型难以提供真正个性化的体验,每一次互动都像是第一次见面。
所以上下文工程从来不是把能给的都给上,而是在 "有限的窗口预算" 下完成一次取舍。
提示词工程解决的是这句话怎么说,上下文工程解决的是这一轮该带哪些材料、各放在什么位置。
上下文内容组成的有效模板
一个完整的上下文通常包含以下 7 项内容,括号中是它的提供方:
- 用户提示词(由用户输入):任务或问题的描述,会首先输入到模型进行意图和目标识别,作为后续内容检索的决策之一。
- 系统提示词(由系统或项目提供):对模型行为的初始说明,包括角色定义、行为规则、动作约束与示例、模型响应的结构化输出定义(JSON Schema)等。可以通过 CLAUDE.md、AGENT.md 等方式提供。
- 可用工具清单(由系统提供):模型可调用的全部函数、工具与技能定义,包括:MCP、Skills 等 name 和 description 内容。
- 工作记忆 (由短期记忆提供):关于 "当前工作" 的记忆,是当前会话中用户与模型的对话历史,虽然会为了审计持久存储虽有对话信息,但只会加载最近的 50 条(可配置)。
- 情节记忆 (由中期记忆提供):关于 "发生过什么" 的记忆,是当前会话的携带了时间、场景、具体经历的结构化事件记录,例如:某年某月的网购回忆。具有 TTL 过期时间,表示对事件的 "遗忘"。
- 语义记忆 (由长期记忆提供):关于 "是什么和如何做" 的记忆,是跨会话的客观事实记录,包括用户偏好(语言、通知方式、界面风格)、用户画像(角色、职责、常用系统、工作习惯),以及不随时间失效的业务事实(长期关注的项目、已经沉淀的结论)。没有过期时间,但可以被更新。
- 外部知识增强(由知识库、数据库、文档等提供):具有实时性、私域性、补充性的外部知识。
回到客服场景,这 7 项分别对应:
| 内容项 | 在客服场景中的具体内容 | 上下文刷新频率 |
|---|---|---|
| 用户提示词 | "这个咖啡机能退货吗?退货要运费吗?" | 每轮都变 |
| 系统提示词 | "你是一个电商客服助手,回答必须给出依据,不得编造政策条款" | 几乎不变 |
| 可用工具清单 | 查订单、查物流、发起退货单、转人工 | 几乎不变 |
| 工作记忆 | 本次会话中已经确认过的订单号、已经排除过的方案 | 每轮追加 |
| 情节记忆 | "某年某月某日购买咖啡机后的使用评价" | 跨会话稳定 |
| 语义记忆 | "该用户是 VIP 会员" 、 "偏好中文回复" | 跨会话稳定 |
| 外部知识 | 《退换货政策 v3.2》中命中的两个条款段落 | 随查询变 |
上面 7 项是面向 "业务" 的,而下面 2 项是面向 "可靠性" 的。第 8 项指示这一轮在什么现场执行,而第 9 项指示这个上下文自己经历过什么,这意味着 Agent 需要把自己做过的动作告诉模型,否则模型会在一段被悄悄截断的历史上继续推理,而它既不知道历史被截断过,也不知道中途换了模型。
- 环境与现场信息(由 Harness 提供):是本轮执行所处的现场,例如工作目录、操作系统、代码仓库的当前状态、当前时间。
- 会话自身的交代(由 Harness 提供):是这次会话里 harness 自己做过的动作,例如上一轮被用户中止、中途切换过模型、历史已经被压缩过一次。
上下文工程的关键技术

- 提示词工程:在上下文工程的具体实现中承担了 2 个关键职责。在检索发生之前,它决定是否检索、检索什么、从哪里检索、何时停止;在检索完成之后,它决定取回的材料按什么规则被使用。而这两段之间的检索执行本身(切分、索引、召回、融合)则由模型推理和工具代码完成。
- 分层记忆系统:承担了用户个人的短中长期记忆的分类、召回和存储等操作。
- RAG 知识库系统:承担了公共的外部知识补充,包括数据库、知识库等数据的访问操作。
- 上下文管理:包括上下文的窗口预算管理、优化组装、压缩、沉淀等管理工作。
- 观测与评测:包括上下文组装结果怎么看、质量怎么量化等工作。
- Agent Infra:包括 Agent SDK、记忆体、知识库、数据库、可观测性等方面的基础设施。
记忆系统
记忆系统与上下文工程的分工可以概括为仓库与调度员:
- 记忆系统是上下文的仓库:对话历史、用户偏好、摘要总结等信息存储在这里。
- 上下文工程是记忆的调度员:决定从记忆中检索哪些信息、如何检索、优化和组装给模型。需要确保提取出最相关的记忆片段。
可见,记忆决定了潜在上下文的上限,而上下文工程决定了这一轮实际用上了其中的哪些,两者缺一不可。
三层记忆
记忆系统通常会基于认知科学中的工作记忆、情景记忆、语义记忆模型,分为三个层次。分别对应短、中、长期记忆,他们之间存在数据流动,短期记忆只记录最近,其中关键事件会被中期记忆,再其中反复出现和特别说明的事实会被长期记忆。

短期记忆
短期记忆(Short-term Memory),即:短期的工作记忆。存储某个单一会话中的对话历史,虽然只有最近 50 条记录会被使用(可配置),但也会在磁盘上做全部对话历史的持久化存储,因为:
- 它是审计的依据:客服场景里尤其如此,一次退货承诺是在哪一轮给出的、依据是什么,必须可查。
- 它是回放调试的原料:可观测性中的追踪与回放都要靠原始会话数据重建现场。
- 它是长期记忆抽取的输入:异步抽取任务读取的正是这份原始记录。
落盘不等于无条件永久保留。一个值得参考的实际做法是,按 Agent 维度分库存储会话与消息,正文接近永久保留,但设一个总量上限,例如单个 Agent 10GB,超出之后从最老的会话开始真删。这样既保住了审计与回放,又让存储成本有上界。
长期记忆

长期记忆(Long-term Memory),存储跨会话的用户偏好与历史总结,使 Agent 能够随时间累积经验与知识。可再细分为:
-
中期的情节记忆 :有时间属性的、可被 "遗忘" 的(TTL)。关于 "发生过什么" 的记忆,带有时间与场景属性的具体经历,例如:"上个月他咨询过同款咖啡机的保修问题,当时的处理是走了换新"。情节记忆通过异步方式,从工作记忆中提取出来。
-
长期的语义记忆 :高维度的、永久有效的。关于 "是什么和如何做" 的记忆,不带时间与场景标记的事实性知识,例如:"该用户是 VIP 会员"。(注:和向量数据库中的 "语义检索" 不是一个含义。)语义记忆也通过异步方式,从情节记忆中提取出来的通用知识,是值得长期保留的高价值内容。

记忆的存储
记忆的主要存储介质有 3 种:
- 工作记忆:以会话为粒度,使用磁盘文件以 "类日志格式" 来存储所有对话历史。
- 情节记忆 :以会话为粒度,存储关键事件记录。记录结构例如 id、user_id、session_id、namespace、created_at、query、event 等等。
- 使用 SQLite 关系型数据库:存储结构化记录。
- 使用 Qdrant 向量数据库:存储词向量化记录。
- 语义记忆 :以用户为粒度,存储通用知识记录。记录结构例如 id、user_id、namespace、knowledge_category、knowledge_content 等等。其中知识类型包括用户偏好(语言、通知方式、界面风格)、用户画像(角色、职责、常用系统、工作习惯)、用户强调等等。
- 使用 SQLite 关系型数据库:存储结构化记录。
- 使用 Qdrant 向量数据库:存储词向量化记录。
注意,中长期记忆同时采用 SQLite 和 Qdrant 进行存储的原因主要是为了支持 "混合双路召回",可以较好的提升记忆召回的命中率。
另外,在实际生产环境中,还需特别关注记忆的安全性与隔离性。每个用户(user_id)的记忆数据应存储在独立的 namespace(命名空间)中以防止数据泄露,同时需建立完善的加密、数据备份与恢复机制,确保重要的用户偏好与历史信息不会丢失。
记忆的写入
首先需要注意的是,短期记忆的写入方式是 "类日志" 的,相对比较简单。
而中长期记忆则相对复杂,主要需要考虑 3 个关键问题:
- **哪些信息需要被长期记忆?**答案是:根据不同的场景而定。例如以下 4 种常见场景,各自的记忆要点差别很大。
- 代码助手类 :侧重记忆代码项目信息和开发偏好,包括:代码库结构(文件组织、模块关系)、命名风格(变量命名约定、代码格式)、常用的框架与库。
- 智能客服类 :侧重记忆用户咨询历史和问题解决偏好,包括:提过的问题与故障、产品使用情况和评价、服务配置与解决方案记录等。当用户第二次询问类似问题时,不必重复描述之前的细节,系统能够回忆起上次给出的建议或已经尝试过的步骤,直接切入重点。
- 个人助理类 :侧重记忆个人日程和聊天偏好,包括:健身或学习计划、经常执行的行为模式(如每周几锻炼)等。
- 推荐服务类 :侧重记忆显式反馈(点赞、明确表达不喜欢)与隐式反馈(浏览记录、点击行为、购买历史),以此构建兴趣档案和用户画像。
-
**记忆的内容是什么?**答案是由模型返回的摘要。以异步的方式,利用模型对原始对话文件中的内容进行摘要提取,继而得到中期记忆中的 event,以及得到后期记忆中的 knowledge。可见,这里就需要通过提示词工程来进行定制和约束。调教好的提示词才能得到符合预期的摘要精度。因此,随着交互增加,中长期记忆会不断适应用户,逐渐减少对显式指令的依赖。
-
**记忆写入的时机是什么?**答案是可以通过轮数或事件触发。
- 轮数触发:每隔 3~5 轮对话(可配置)自动生成摘要存入中长期记忆。
- 事件触发:在完成任务、场景转换等关键节点记录信息,例如:客服完成问题处理时保存解决方案,个人助理更新日程后写入日历。
相对的,有一类信息是明确不该进记忆库的,密钥、令牌、身份证号、银行卡号这类敏感信息,不能写入记忆。隐私数据脱敏要放在写入边界,而不是放在读取或展示环节。因为记忆库通常有向量索引、全文索引与多份备份,一条敏感信息一旦落库,后续要彻底清除需要同时处理所有副本,而召回侧的过滤只要漏一次就等于泄露。把清洗动作固定在写入这一个点上,是唯一可以被审计的做法。
此外,记忆写入的类型有以下 4 种,不能只有 "新增",还需要修改,否则会出现新结论与旧结论并存,这时就只能把判断交给模型了,但模型的判断标准并不严谨。
- 新增:库中不存在相关条目,直接写入一条新记忆。
- 修改:库中已有同一维度的条目,但内容发生了变化,应当以新内容替换旧内容,而不是再写一条。
- 删除:已有条目被新的事实否定,例如用户明确表示不再需要某项服务,应当将其标记失效。
- 不变:本轮抽取出的内容与库中已有条目实质相同,不执行任何写入,防止记忆体积膨胀。实际上,相邻几轮对话往往围绕同一件事展开,若每次记忆抽取都直接写入,就会迅速堆积大量近似重复的条目。
那么,为什么不干脆每一轮都写一次记忆呢?主要有以下 3 点原因:
- 成本:每一次抽取都是一次额外的模型调用,按轮写会让这部分成本与对话轮数成正比。
- 质量:单轮对话往往不足以支撑一条稳定的结论,隔几轮再抽取,模型能看到完整的问题与结论,抽出来的事实更可靠。
- 冗余:相邻几轮围绕同一件事展开,逐轮写入会产生大量近似重复的记忆条目,反过来污染召回。
记忆的召回

Agent 需要基于当前对话意图从记忆库中检索相关信息,主要有以下 3 种召回方式:
- 关键词检索:对应 SQLite 的全文检索能力(FTS5 扩展)。
- 向量检索:对应 Qdrant 的 retrieve 语义检索能力。
- 图关系查询:如果使用了 GraphRAG 存储记忆,还可以召回记忆实体之间的关系做多跳查询。
双路召回与融合排序
其中,关键词检索擅长定位实体,向量检索擅长语义近似。实验证明,两者结合的 "混合双路召回" 后,再使用融合排序(例如 RRF,Reciprocal Rank Fusion,倒数排名融合)合并之后,再做重排,最终效果明显好于单路。
例如:单路向量检索在面对 "咖啡机 SO-20993" 这类夹带编号的查询时召回命中率很差,因为 SO-20993 和 SO-20992 这 2 个编号在语义空间里几乎没有区分度,必须靠关键词检索来补上。
那么,一次完整的召回具体是怎么走的呢?以情节记忆为例,当客服场景中用户问 "上次那台咖啡机的保修还剩多久",系统依次执行以下 5 步:
- 按主键读取:以 user_id 标识从 SQLite 结构化存储中取出 "VIP 会员" 等用户信息。
- 元数据过滤:仍以 user_id 标识加上近 30 的时间范围(可更改),把该用户的情节记忆条目圈成一个候选集合,后两步都只在这个集合内进行。
- 关键词检索:在候选集合的全文索引上检索 "咖啡机" 与 "保修",召回工单编号为 SO-20993 的那一条记录。
- 向量检索:在同一候选集合的向量索引上检索整句查询,召回 "上个月咨询过同款咖啡机的保修,处理结果是换新" 这条记录。该记录和 3. 中的记录可能完全没有共同的关键词。
- 融合与重排:第 3 步与第 4 步的结果用 RRF 合并,再做一次重排,取前三条用于后续的上下文组装和输入推理。
关键词检索与向量检索的区别
根本差别只有一句话 ------ 前者匹配的是字面符号,后者匹配的是语义空间中的距离。
| 维度 | 关键词检索 | 向量检索 |
|---|---|---|
| 匹配依据 | 查询词项是否在片段中出现,以及它的统计权重 | 查询向量与片段向量之间的距离 |
| 对查询用词的要求 | 必须与片段用词一致,同义改写会直接落空 | 不要求用词一致,表述的意思接近即可 |
| 擅长的查询 | 编号、型号、人名、条款号、精确短语 | 口语化提问、同义改写、跨语言提问 |
| 典型失败形态 | 没有共同关键词 | 编号与长数字几乎没有区分度 |
| 结果的可解释性 | 可以指出命中了哪几个词、各自权重多少 | 只能给出一个相似度分数,无法说明相似在哪里 |
| 增量更新 | 新片段入库即可被检索到 | 新片段需先经嵌入模型计算向量 |
| 更换模型的代价 | 不涉及 | 更换嵌入模型需要全量重算向量 |
回到客服场景,三个例子足以说明两者为什么必须并用:
-
只有关键词检索能完成的查询:用户问 "SO-20993 这单到哪了" 。如果采用向量查询,那么工单编号在词向量空间里几乎没有区分度,SO-20993 与 SO-20994 的向量差别微乎其微,向量检索取回的往往是另一个用户的工单。
-
只有向量检索能完成的查询:用户问 "机器坏了能换新吗" ,而政策原文写的是 "商品出现性能故障,可在保修期内申请换货" 。两段文字除了 "换" 字之外没有共同词项,所以如果用关键词检索的话召回就会为空。
-
两者都可能失败的查询:用户问 "哪些商品不支持七天无理由退货" 。向量检索会把 "支持七天无理由退货" 的条款排在前面,因为两段文字仅差一个否定词;关键词检索同样会命中这一条,因为查询的词项它全部包含。这一类查询只能靠重排来纠正。
第 3 个例子值得单独强调,因为它说明混合双路检索并不是万能的。双路解决的是一路漏召回的问题,而不是排序错误的问题,后者属于重排的职责。
全文索引与向量索引的区别
全文索引,除了支撑关键字检索,还支持短语检索、前缀检索与邻近检索;向量索引,主要支持向量检索。
全文索引的构建分为三步:
- 分词:把片段切成词项。英文按空白与标点切分即可,中文必须经过分词器,因此分词词典的质量直接决定了专有名词能否被检索到。
- 建倒排链:为每一个词项维护一条记录,内容是该词项出现在哪些片段里、在每个片段中出现了几次、出现在什么位置。这个结构之所以称为倒排,是因为正向的关系是 "片段包含哪些词" ,而它存的是 "词出现在哪些片段里" 。
- 建统计量:记录每个片段的长度与每个词项的文档频率,供打分函数使用。
检索时取出查询各词项的倒排链,求交或求并得到候选片段,再用打分函数排序。当前主流的打分函数是 BM25(Best Matching 25),是一种关键词检索的相关性打分函数。
BM25 由三个部分相乘得到,词频饱和的程度由参数 k1 控制,长度归一的强度由参数 b 控制,原综述给出的经验区间为 k1 取 1.2 至 2 之间、b 取 0.5 至 0.8 之间。如下:
- 词频饱和:一个词在片段里出现得越多,该片段越可能相关,但增益是递减的。出现十次与出现二十次的差别远小于出现一次与出现两次的差别,因此 BM25 对词频做了饱和处理,而不是线性累加。
- 逆文档频率:一个词在整个语料里越罕见,它一旦命中就越有信息量。 "退货" 在客服知识库里几乎每篇都有,它的命中说明不了什么;而 "未拆封" 只出现在少数条款里,它的命中具有很强的指示性。
- 长度归一:长片段天然更容易包含任意一个词,因此需要按片段长度对得分做惩罚,否则排序会系统性地偏向长片段。
工程上全文索引通常不必自建,SQLite 的 FTS5、Lucene 与基于它的 ElasticSearch、PostgreSQL 的 tsvector 都是成熟实现。
向量索引的构建同样分为三步:
- 嵌入:片段经嵌入模型映射为一条固定维度的浮点向量,常见维度为 768、1024 与 1536。该映射的性质是语义接近的片段在这个空间中距离更近。
- 选定距离度量:余弦相似度、内积与欧氏距离三者选其一,入库与查询两侧必须使用同一种,否则距离没有意义。
- 建索引结构:若逐条比较查询向量与库中全部向量,代价与库的规模成正比,百万级片段的单次查询即不可接受。因此生产系统一律使用近似最近邻(ANN,Approximate Nearest Neighbor)索引,用放弃少量召回率换取查询时间的大幅下降。
常用的近似最近邻索引有以下 3 类,它们解决的问题并不相同:
- HNSW(Hierarchical Navigable Small World,分层可导航小世界图):把向量组织成一个多层图,上层节点稀疏、连边跨度大,用于快速跳跃到目标区域,下层节点稠密,用于精细搜索。查询从上层入口开始逐层贪心下行。它的召回率与查询速度都很好,代价是内存占用大、构建慢。两个关键参数是每个节点的连边数与搜索时的候选队列长度,后者是召回率与延迟之间最直接的调节旋钮。
- IVF(Inverted File,倒排文件):先对全部向量做聚类得到若干簇心,查询时只搜索距离查询最近的若干个簇。两个关键参数是簇的总数与查询时搜索的簇数。它的构建远快于 HNSW,代价是落在簇边界附近的向量容易被漏掉。
- PQ(Product Quantization,乘积量化):把一条高维向量切成若干段,每段用码本中最接近的一个码字替代,从而把一条向量从数千字节压缩到几十字节。需要注意的是,它解决的是内存占用而不是召回率,通常与 IVF 叠加使用。
注意,嵌入模型一旦更换,全部向量都要重算,因为新旧模型的向量空间不可比,混用会让距离失去意义。这一点与全文索引形成鲜明对照,后者更换分词器只影响新入库的片段。因此选定嵌入模型属于入库阶段的不可逆决策,宜在建库之前就用评测集确认。
融合排序
倒数排名融合(RRF,Reciprocal Rank Fusion)属于只做排名不给分数的排序方法。RRF 的计算方式是,对每一个片段,把它在各路召回结果中的名次代入倒数后求和,即该片段的融合得分等于各路的 1 除以(常数加名次)之和,其中常数的常见取值为 60。该常数的作用是压低头部名次之间的差距,避免某一路的第一名直接决定最终次序。
以客服场景的一次查询为例。用户问 "咖啡机未拆封能退吗" ,两路各取前五条,设:条款 A 在关键词路排第 1、在向量路排第 4;条款 B 在关键词路未进入前五、在向量路排第 1。取常数为 60,则:
- A 的融合得分为 1÷(60+1) + 1÷(60+4) ≈ 0.0164 + 0.0156 = 0.0320;
- B 的融合得分为 1÷(60+1) ≈ 0.0164。
因此 A 排在 B 之前。这个结果体现了 RRF 的两条性质:
- 只要有一路把某个片段排到前面,该片段就能进入最终结果,这保证了召回不被单路的失误抹掉;
- 而两路都排到前面的片段得分最高,这保证了被两种检索方式共同认可的片段优先。
最后需要说明 "融合排序" 与 "重排" 的分工。
- 融合排序:只用名次、不看内容,它的代价几乎为零,作用是把两路的候选收敛成一个规模可控的集合;
- 重排:则把查询与片段的正文一起送进模型打分,代价高但判断准确。
两者是先粗后精的两级,不是互相替代的两个选项。
重排
之所以需要把召回后的记忆记录再做一次重新排序的原因是:双路召回之后的融合排序只会根据各路的先后顺序做合并,它无法判断排在第一的片段是否真的回答了用户的问题,这一判断需要重排来完成。
例如查询 "不支持七天无理由退货的商品有哪些" ,向量检索很可能把 "支持七天无理由退货" 的条款排在前面,因为两段文字除了一个否定词之外几乎完全相同。
重排的基本原理是,把召回片段和用户问题一并输入到一个特定的重排模型中,例如硅基流动托管的 bge-reranker-v2-m3。这类模型通常是 Cross-Encoder(交叉编码器)结构,它能判断多个片段和问题之间的权重关系,输出一个标量相关度分数。
记忆的遗忘
记忆系统有一个容易被忽略的事实,即:记忆不只是会变多,它还会变错。一条一年前写下的 "该用户偏好电话联系" 在用户改了习惯之后就是错的,而它不会自己消失,仍然会被召回、仍然会影响回答。因此一个完整的记忆系统必须有淘汰与失效机制,对应人类记忆中的 "遗忘",而不只有写入与召回。
针对有时限的情节记忆,常见的处理手段有以下 3 类:
-
时间衰减:给记忆条目附加时间权重,越久远的条目在排序中被压低。值得注意的是,衰减的作用对象应当有区分:情节记忆适合衰减,因为上个月的那次咨询确实会随时间失去参考价值;而语义记忆中的稳定事实不适合无差别衰减,用户的姓名与会员等级不会因为写得早就变得不重要。把两类记忆放进同一条衰减曲线,是一个常见的设计错误。衰减实现有多种不同的处理方式,例如下述三者对外都叫衰减,但一个改的是数据、一个改的是排序、一个改的是生存期,选型时若不区分,很容易得到一个与预期完全不同的行为。
- MemoryBank 参照艾宾浩斯曲线的真实遗忘,条目会随时间真正删除;
- Generative Agents 以 0.995 为系数的衰减,它只作用于检索时的排序打分,并不删除任何条目;
- Mem0 过期时间设置,它是一个硬性的生存期限,到点即失效,中间没有任何衰减过程。
-
冲突覆盖:新写入的事实与已有事实冲突时,以新的为准并把旧的标记失效,而不是让两条并存。并存的后果是召回时两条都回来,模型需要自己判断该信任哪一条,这是不必要的负担。
-
容量淘汰:为每个用户的记忆设容量上限,超出时按低重要度、低命中频次与早写入这三项的组合打分淘汰。
实际上,绝大多数记忆系统都不原生提供冲突消解与入库去重的能力,而只支持新增这一种动作。因此这些都是需要开发者自己来构建的能力。
记忆的脱敏
RAG 知识库系统
记忆与知识库的区别
记忆与知识库最根本的差别不在存储技术(两者完全可以落在同一套数据存储上),而是在数据归属上。
| 维度 | 记忆(个人的) | 知识库(公共的) |
|---|---|---|
| 数据归属 | 属于某个用户或某个会话 | 跨用户共享 |
| 隔离单位 | 按用户与会话分命名空间 | 全局共享,按权限分域 |
| 写入路径 | 由会话自动沉淀,异步抽取 | 由离线入库流程建立 |
| 更新语义 | 随交互持续累积 | 以文档版本为单位替换 |
| 删除语义 | 用户可要求删除本人的全部数据 | 以文档版本为单位下线 |
两类材料真正需要协同的场景,是同一个问题既要个人背景又要公共依据。例如用户问 "这次退货能不能免运费":
- 记忆侧:取出他是 "VIP 会员" 与 "3 天前下单且未拆封" 这两条记录。
- 知识库侧:取出 "该商品当前版本的退货政策条款"。

词向量 RAG
最传统的 RAG 是词向量 RAG,本质就是对向量数据库的一个应用,通过词向量实现存储和基于语义的检索。


智能体 RAG
智能体 RAG(Agentic RAG)将自主智能体整合到 RAG 检索增强生成的整个流程中,充分利用 "反思、规划、工具使用" 等 Agentic 机制,实现了基于持续推理的动态信息检索。
- 传统 RAG:流程固定,一次召回、一次生成,轮数由代码定死;
- Agentic RAG:把检索变成模型可调用的工具,是否检索、查什么、查几轮、何时停止都由模型在推理时决定。代价是推理期词元与延迟不可预测。
Agentic RAG 利用的正是 Loop Engineering 中的 "自主执行和检验闭环" 思想,Agentic RAG 在运行时会作出的 4 项判断:
- 本轮该不该检索:Agentic RAG 利用模型能够判别出当用户问 "你好" 不需要检索,而问 "退货政策是什么" 需要检索。
- 查询该怎么改写:用户的原话往往不是一条合格的查询,其中可能含有指代、口语缩略等问题。Agentic RAG 利用模型可以将这些输入翻译为一次合格的查询。
- 取回的材料够不够:Agentic RAG 可以利用模型每轮都动态的判别信息够了没。如果模型判断手上的材料不足以支撑结论,就会据此发起下一轮检索。
- 什么时候停下来:这是最难的一项,模型既可能在材料不足时过早停止,也可能在材料已经足够时反复检索。所以必须有一项判断指向终止,否则流程不会收敛。
而在具体实现层面,只需要在传统 RAG 之上实现 3 个关键功能即可完成 Agentic 化:
- 把检索包装成一个工具:在 name 和 description 写清楚适用场景。注意,租户与权限类的过滤参数必须由代码注入,不能交给模型填。
- 反思和检验机制:在系统提示词里给出充分性判据和最大轮数,以此控制什么时候停下来,否则成本失控。又例如以 Self-RAG 为代表的自优化框架,用 "反思机制" 自评本轮该不该继续检索、检回来的材料能不能支撑当前任务。
- 检索结果溯源:注入每条检索结果时带上来源标记,比如文档标题、章节、链接、时间,作为模型判断是否继续追加检索的依据。例如:如果三条片段都来自同一篇文档的同一节,说明召回面太窄;如果取回的是旧版政策而问的是当前政策,说明需要换个查询再查一轮。结果若是一团无标注的纯文本,模型只能判断内容够不够,判断不出为什么不够、下一轮该怎么查。
Agentic RAG 的缺点就是延迟与 Token 成本的上升,而且不可预测。轮数由模型决定,同一类查询可能跑两轮也可能跑九轮,因而它很难挂上确定性的 SLA 服务等级目标,也必须配预算熔断机制,否则一个歧义查询就能把额度耗尽。
图增强 RAG
相对的,图增强 RAG(GraphRAG)改动的不是检索的次数,而是检索的单位。
GraphRAG 的检索单位从 "文本块(向量数据库)" 变成了 "实体与关系(知识图谱)"。
- 数据进入知识图谱时,需要模型抽取实体与关系三元组构图,并可选做社区检测生成分层摘要;
- 检索数据时沿关系路径做多跳,深度检索有关系的实体及其关联信息。
既然向量检索已经能覆盖语义相似,为什么还需要图检索呢?因为后者补上了向量检索的两个结构性缺陷:
- 向量检索是平面的:两个语义不相似但关系极强的实体不会被同一个查询召回。例如:查某个零件为什么反复损坏,向量检索找不到三年前那份召回公告,因为两段文字的语义相似度确实不高,但它们之间存在一条明确的关系边。
- 全局性问题无解:例如问这批文档的主要争议点是什么,答案不在任何单个文本块里,召回条数取多少都答不出来,而图的社区摘要正是为这类问题准备的。
总的来说,GraphRAG 适用于以下 3 类场景:
- 多跳查询:例如某公司的供应商的母公司注册在哪个国家,答案需要沿关系链走两跳以上。
- 需要全局概览的问题:结论不落在任何单条材料上。
- 实体密集且关系本身承载信息的领域:故障根因分析依赖服务依赖图,金融风控与反欺诈依赖股权与交易关系,医疗依赖药物、靶点与疾病的关联,法律依赖条款引用链,代码库理解依赖调用图。
而 GraphRAG 的代价主要集中在知识图谱的构建上。抽取实体与关系要逐段跑模型,全量建图的 Token 成本比单纯做嵌入高一到两个数量级;增量更新也更难,新文档进来可能改变社区划分,严格做需要重算,这正是增量索引在 GraphRAG 上成为难点而不是优势的原因。此外,图的结构设计需要领域知识,抽取质量差的图比没有图更糟。

对照与选型
首选需要注意,如果查询是单点事实查询、语料是彼此独立的文档,那么传统 RAG 加双路召回与重排就够了。
| 维度 | 智能体 RAG | 图增强 RAG |
|---|---|---|
| 代表框架 | Self-RAG、PlanRAG | GraphRAG、HippoRAG、GNN-RAG |
| 检索轮数 | 模型运行时决定 | 固定,但每轮多跳 |
| 主要成本 | 推理期词元与延迟 | 入库期词元与图维护 |
| 延迟可预测 | 否 | 是 |
| 擅长 | 需分解、需汇总的复杂问题。查询中出现比较、汇总、追问原因或多重约束条件。 | 多跳关系、全局概览。问题需要在实体之间跳转,或需要得出整体性的结论。 |
| 不适用 | 有硬性服务等级(等待时长)要求的在线问答 | 文档之间无实质关系的语料 |
| 引入的主要工作量 | 实现轮数上限、预算熔断与终止判断,以及一套能回放多轮检索过程的追踪 | 设计实体与关系的结构、实现抽取流程与一致性校验,以及增量更新时的重算策略 |
| 主要难点 | 终止条件难以写准,模型既可能过早停止也可能反复检索,二者都需要在线观测才能发现 | 抽取质量决定一切,实体对齐与同名消歧没有通用解,须按领域逐一处理 |
| 可获得的收益 | 原本一次召回无法回答的问题变为可答 | 多跳问题与全局概览类问题变为可答 |
| 收益不成立的情形 | 查询绝大多数是单点事实查询,多轮推理纯属多余开销 | 语料是彼此独立的文档,实体之间没有实质关系 |
| 对团队的要求 | 需要有可靠的成本与延迟监控,否则线上成本不可控 | 需要领域专家参与结构设计 |
客服场景中的三类查询分别落在三个不同的位置:
- 传统 RAG 即可:用户问 "咖啡机的保修期是多久" 。答案就落在保修条款的某一段里,一次召回取回该段即可,无需分解,也不涉及实体跳转。客服知识库中绝大多数查询属于这一类,这也是第三篇最终选择朴素 RAG 加双路召回的理由。
- 智能体 RAG:用户问 "我这两单一起退,运费怎么算" 。这句话里有两笔订单、两个商品类目,可能适用两条不同的退货规则,还要再叠加会员权益。系统必须先把它拆成若干子问题分别检索,再汇总判断,一次召回无论取回多少条条款都无法得出结论。
- 图增强 RAG:运营方问 "这个月退货率异常的商品,它们的供应商有重叠吗" 。答案需要沿商品、供应商、生产批次的关系链跳转,而这些关系存在于结构化数据而非条款文本中,向量检索无法表达这种跳转。需要注意的是,这一类查询的提问方是运营人员而不是消费者,因此它通常不属于在线客服机器人的职责范围。
词向量 RAG 的写入与检索
-
切分 :把文档切成可检索的片段,决定片段的大小与相邻片段的重叠量。切得太大会让一次召回携带大量无关内容,切得太小则会把一条完整的规定拆散到两个片段里,两半都不足以支撑结论。
-
索引 :为片段建立检索结构,通常同时建向量索引 与全文索引两套。这一环节的选择决定了后面能走哪几条召回路径。
- 全文索引:除了支持关键词检索,还支持短语检索、前缀检索与邻近检索。
- 向量索引:支持向量检索。
-
检索:按当轮查询取出候选片段。关键词检索与向量检索各有擅长,前者精于精确实体,后者精于语义近似,生产系统通常两路并行再做融合排序。
-
重排:用一个精度更高、代价也更高的模型对候选集重新打分,把最相关的几条排到前面。重排的输入规模远小于召回,因此它的代价是可控的。
-
组装:把最终入选的片段按第七章的规则放进输入序列的对应位置,并保留出处信息以备溯源。

出处保留与引用溯源
引用溯源是一条贯穿入库、组装与观测三个阶段的链路:
- 第一层是数据层,出处必须在切分环节产生:每个片段在入库时就要携带四项元数据(文档标识、文档版本、章节页码位置、片段在原文中的偏移)。这里的关键是产生的时机,出处必须在 RAG 切分时随片段一起写入,而不能在召回之后反查。因为召回之后手上只剩片段文本,再去原文中定位它的归属要做字符串匹配,一旦原文存在多处相似表述就会归错,而政策文档恰恰充满相似表述。
第二层是提示层,出处必须与正文一起进入模型上下文:常见形态是为每个召回片段加一个简短标识,并在系统提示词里写明引用契约,即回答中的每一条结论都要标注其依据的片段标识,没有依据时必须声明没有依据而不得作答。需要注意的是,出处不能只放在工具返回值的元数据里而不进入上下文,模型看不到没有被放进上下文的内容,这是一个在实现中很容易出现的疏漏。
第三层是回传层,模型输出的标识要能被程序还原:因此标识要简短、稳定,且不易与正文混淆。同时要把本轮召回的全部片段标识与最终被引用的标识都记录下来,缺了这份记录就无法判断检索与组装哪一侧需要改进。
企业知识库系统
企业知识库是一个系统,而不仅仅是一些组件或一个数据库。如下图所示。

上下文管理
当用户输入一个新的提示词后,Agent 的上下文工程大体会完成一下操作流程。
- 意图与目标识别:决定了上下文组成要素
- 窗口预算与规划:决定了这一轮有多少额度可用
- 多源数据检索:决定了要补充什么内容
- 优化与组装:决定了补充的内容放在哪里
- 上下文压缩:决定了放不下时舍弃谁
- 写出与沉淀:决定了本轮产生的信息哪些值得留到下一轮

意图与目标识别
把用户提示词直接输入到大模型,判别是否需要执行工具或进行补充相关数据,决定了下一轮输入的上下文组成。
窗口预算与规划
预算
不同模型 API 的上下文窗口大小不同,需要根据当前轮的实际情况和窗口余量,来预估下一轮还有多少输入额度可用,以及预估是否需要进行压缩。
- 超限之后的补救是有损的。没有窗口预算与规划能力的后果是只在上下文长度撞到窗口上限后才被发现,此时要么请求被服务端直接拒绝;要么由兜底逻辑紧急截断,但紧急截断的切点无法控制,其信息损失通常大于按既定次序的主动压缩。
- 输入侧是费用的主体。τ-bench 在成本分析中给出,每个任务每次试跑约需 200 美元的调用费用,其中 95.9% 来自输入词元,其余来自输出词元。因为 Agent 场景中最常见的就是 60k/0.5k 的场景。也就是说,管住输入长度的收益远大于优化输出长度。
例如:Claude Code 会把把窗口预算拆成四类分别记账,即:已用、空闲、缓冲与延后。缓冲的作用是让压缩在窗口真正耗尽之前就触发;延后一类说明部分占用可以推迟计入。触发判定按剩余的绝对词元量,而不是按百分比。又例如:Dsh 用 2 个比率控制,以约八成占用为触发线,并预留约一成六给摘要之后的续写。
具体实现层面,主流推理服务都提供词元计数接口,因此在真正发起 API 调用之前是可以核算本轮输入规模的。例如:
- Anthropic 的 Messages API 提供专门的词元计数端点 count_tokens,它接受与正式调用完全相同的入参,包括:系统提示、消息历史与工具定义,返回本次请求的输入词元总数。并且该 API 免费调用,只受独立的速率限制;
- OpenAI 提供输入词元计数接口,入参格式与 Responses API 一致,同样覆盖消息、工具与附件;
- Google Gemini 提供 countTokens 方法,用途同样是在发起生成请求之前先核算输入规模。
规划
规划的内容则包括三项:1)本轮各类材料的额度划分;2)检索的召回条数上限;3)是否需要把某个子任务隔离出去。
一个可作为起点的划分方式如下,具体比例需要按场景调整,此处给出的是划分的结构而非推荐值。其中,前三项先扣除,剩余的容量再按后四项分配。这个顺序不可颠倒,因为前三项是不可压缩的,若把它们留到最后处理,结果通常是在运行时才发现剩余容量不足以放下一次完整的检索召回。
| 额度项 | 性质 | 划分方式 |
|---|---|---|
| 工具定义与启用开销 | 固定,整个会话不变 | 按 6.1 节的核算结果预先扣除 |
| 系统提示 | 固定,整个会话不变 | 预先扣除,并设上限防止其逐版膨胀 |
| 输出预留 | 固定 | 按最长回答预留,不足会导致回答被截断 |
| 历史消息 | 随轮次单调累积 | 给定上限,超出后由压缩策略处理 |
| 记忆召回 | 随轮次波动 | 以召回条数而非词元数控制 |
| 知识库召回 | 随查询波动 | 以召回条数与相关度阈值双重控制 |
| 工具返回值 | 运行时产生,波动最大 | 单条设上限,超限转存,见 8.3 节 |
预算确实不够时,谁先让位?
- 最先让位的是低相关度的知识库召回片段。理由是它的让位成本最低,只需提高检索的相关度阈值,让它根本不进提示即可,不需要任何额外加工。
- 其次是较早的原始对话,用摘要替代原文。这一步有加工成本,需要一次额外的模型调用,但信息损失是可控的,关键结论会被摘要保留下来。
- 最后才处理稳定的个人偏好。理由有两点:它本身极短,删掉省不下多少预算;而它一旦缺失,回答会立刻失去个性化,用户的感知最为强烈。
也就是说,让位顺序的依据是单位预算的信息价值,而不是材料的新旧。可以具体化为两个问题:这条材料占用了多少词元,以及删掉它之后回答会差多少。两者的比值越低,越应当先让位。
多源数据检索
多源检索的关键是如何从多源信息中筛选出与当前问题最相关的内容,确保模型有正确的材料来完成任务。
第一个问题是:Agent 什么时候会触发检索?答案是要构建相关的系统提示词,因为它定义了什么算相关。例如下列这三句话决定了检索的时机、范围与空结果的处置方式。
- 写明回答政策类问题必须先检索知识库,这一句决定了本轮是否发起检索;
- 写明只受理退换货、保修与物流三类咨询,这一句把检索范围收窄到这三类文档;
- 写明不得编造政策条款,这一句要求检索结果为空时不作答而转人工。
检索数据的来源主要有记忆和知识库这 2 大类型:
-
记忆召回,例如召回这个用户的对话历史与偏好。需要说明的是,长短期记忆的召回方式并不相同:
- 短期记忆默认最近 50 条记录会按时间顺序原样进入消息历史,所以不需要经过召回;
- 长期记忆才需要被召回,因为它跨会话累积,总量远超窗口容量,无法整段带入。
-
外部知识检索:在任务所需的知识超出模型的内部知识时被激活,例如:实时数据、专业信息、私域知识。通过动态检索数据库或知识库等外部信源。
例如:用户问 "这个咖啡机能退货吗",系统需要并行发起三路检索,即:
- 从订单系统取出这个用户的购买记录,确定咖啡机具体指哪一笔交易。(外部知识检索)
- 从记忆系统取出这个用户的会员等级与沟通偏好。(记忆召回)
- 从知识库取出退换货政策中与咖啡机、未拆封、7 天相关的条款。(外部知识检索)
三路检索结果的质量需要可观测来支持,例如:一次回答失准究竟是因为没有检索到、还是检索到了但没有进入上下文、还是进入了上下文而模型没有采用等。
优化与组装
需要根据模型推理服务 API 的 "前缀缓存",以及模型对长上下文 "关注度失真" 等特点来优化并组装好一个输入就绪的上下文序列。
具体做三件事:
- 按优先级排序提取核心数据;
- 结构化数据改写成模型易于解析的自然语言表述;
- 决定每一类材料进入输入序列的哪个位置。注意,组装位置按材料的刷新频率决定,不按材料的重要程度决定,底层逻辑是充分适配推理服务的前缀缓存机制。
系统提示词
系统提示词是唯一能够稳定命中前缀缓存的那一部分。
只有以下 4 类内容应当写进系统提示词:
- 角色与身份:模型在本系统中扮演什么、面对谁、以什么立场回答。
- 行为规则:在哪些情况下必须做什么、不得做什么,以及冲突时以哪一条为准。
- 动作约束:可以调用哪些能力、调用前需要满足什么前提、哪些动作需要先确认。
- 输出格式与示例:回答的组织方式,以及一两个足以定调的示例。
相对的,有 3 类内容不应当写进系统提示:
- 每轮变化的材料:检索召回的片段、当轮的时间与状态标签都属于这一类。它们写进系统提示的后果是前缀缓存在每一轮失效,依据见第七章。
- 不可信的外部内容:从网页、文档或用户附件中取回的内容不应当以指令的形态出现在系统提示里,理由是模型难以区分哪一段是系统给的规则、哪一段是材料里夹带的伪指令。处理方式见 7.4 节。
- 大段的领域知识:政策原文、产品手册这类内容属于知识库,按轮召回其中命中的片段即可,整篇写进系统提示既浪费预算,也会把真正的规则挤到模型不擅长利用的中段位置。
判断一条内容该不该进系统提示,有一个简便的判据,即:如果它在一个几十轮的会话里会变,那它就不该在系统提示里。
系统提示词中的行为规则可以用 3 种方式写出来,三者的窗口预算占用与约束强度都不同,可以按规则的性质选择:
- 禁令:直接写明不得做什么,例如不得编造政策条款。它最短,适合边界清晰的硬约束,但它只说明了不能做什么,没有说明该做什么,因此通常需要与第 2 种配合。
- 条件分支:写明在什么条件下做什么,例如检索不到依据时,说明需要转人工确认,不要推测。它比禁令长,但能覆盖模型最容易出错的那些具体情形,是行为规则的主力写法。
- 示例:给出一两个符合要求的完整示例,必要时再给一个反例。它最长,但对格式、语气、引用方式这类难以用文字描述清楚的要求,示例的效果明显好于描述。
上述三类的应用场景可以这样考虑:
- 能用一句禁令说清的,不要写成示例;
- 凡是反复出错且错法相似的地方,把禁令升级为条件分支;
- 凡是用文字描述了三遍仍然写不出预期效果的格式要求,改为给一个示例。需要注意的是示例的窗口预算占用随示例数量线性增长,而效果并不随之线性提升,通常两个示例已经足够定调。
上述规则之间可能会冲突。例如:一条要求简洁、一条要求给出完整依据,模型在遇到两者不可兼得的情形时会任选其一,表现为行为不稳定。因此规则清单里需要显式写明优先级,例如与政策原文冲突时一律以政策原文为准。显式的优先级比把规则写得更详细更有效,因为前者消除的是歧义,后者只是增加了描述。
工具清单
它既是一份能力清单,也是一份每轮都随请求送进模型、占用固定预算的输入材料,并且它的数量存在一个明确的上限。当我们安装 Skills 和 MCP 时,并不是越多越好;越多反而会导致模型进行工具决策的模糊与混乱。公开实践与厂商文档在这一点上的口径是一致的。当可选工具增加到三十至五十个这一量级时,模型在工具选择上的准确率开始明显下降,表现为调用了职责相近但不正确的那一个,或在本不需要工具的问题上也发起调用。OpenAI 的函数调用文档则给出了一个更保守的建议值,即一轮开始时可用的函数数量应该少于二十个。

另外,按 Anthropic 公开文档中的词元计数示例,一条仅包含系统提示 "你是一位科学家" 与用户消息 "你好" 的请求,输入约为 14 个词元;而同样一条请求在附加一个查询天气的工具定义之后,输入增加到约 403 个词元。也就是说,单个工具的定义所占的词元数,比一段简短的系统提示高出一个数量级。
除了工具定义本身,启用工具调用(Function Calling)还会引入一段由服务端注入的系统提示开销,其大小随模型版本不同,公开文档给出的范围约为 286 至 804 个词元;以 Claude Sonnet 5 为例,在 "自动选择与不调用" 两种模式下约为 354 个词元,在 "强制调用与指定工具" 两种模式下约为 474 个词元。这部分开销与工具数量无关,是启用工具这件事本身的固定成本。
把这两项合起来看,一个挂了十个工具的 Agent,在用户还没说任何话之前,输入预算就已经被占掉数千词元。关键在于它每一轮都在。虽然它不像检索召回那样随轮次变化,因此它能够命中前缀缓存;但它也不会随着对话推进而被压缩或淘汰,因为工具清单在整个会话里是固定的。
组装位置与前缀缓存
上下文组装位置按刷新频率决定,而不是重要程度。这条规则,四类材料的组装位置其实已经被决定好了:
| 材料 | 刷新频率 | 能否命中前缀缓存 |
|---|---|---|
| 稳定的个人事实与偏好 | 几乎不随轮次变化 | 能 |
| 按轮检索的个人经历 | 每一轮召回的都不同 | 不能,但很短 |
| 知识库的召回片段 | 随当轮查询变化 | 不能,但很短 |
| 工具的返回值 | 运行时才产生 | 不能 |
前缀缓存的原理是把输入序列从头开始的一段稳定前缀所对应的中间计算结果保留下来,下一次请求若前缀相同即可跳过这一段的重复计算。三家主流推理服务在这件事上是一致的,差别只在参数与是否需要显式声明:
- Anthropic:输入序列的组织顺序是 "工具定义、系统提示、消息历史" ,命中要求前缀完全相同。
- OpenAI:按精确前缀匹配,输入超过约一千词元后自动启用,无需显式声明。其文档明确给出了组织输入的建议,即 "把静态内容放在前面、把变化的内容放在最后" 。并且写明应当追加新消息而不是改写较早的轮次,因为摘要、压缩或截断都会改变前缀并使缓存复用失效。
- Gemini:隐式缓存默认开启,可缓存的最小长度随模型不同,约为两千至六千词元。该服务声明命中后会自动传递成本节省,但其文档未公布具体的折扣比例。
每轮变化的内容应当追加在当轮输入的尾部,而不是插在历史的中间或前面。原因同样来自缓存。缓存命中是从序列开头逐块比对的,一旦在第 N 块的位置插入了新内容,从第 N 块开始往后的全部缓存一律失效,哪怕后面的历史对话一个字都没改。因此只有尾部连续的一段,才是推理服务可以稳定地排除在缓存前缀之外的位置。

上下文压缩
组装决定材料放在哪里,而压缩决定材料放不下时谁先让位。两者处理的都是同一批原始材料,但前者不改变材料的体积,后者会改变。常见的压缩手段包括:对话历史摘要、双层存储与基于使用频次的动态留存。
压缩会造成真实的信息损失,因此它的三类手段按损失程度递增排列:
-
截断:保留一条材料的前若干词元,其余舍弃。代价最低,无需额外的模型调用,适用于结构化程度高的材料,例如订单详情与物流轨迹,这类材料的关键字段通常集中在前部。其风险是对非结构化的长文本效果很差,关键结论可能恰好在尾部。
-
摘要 :让模型把较早的一段对话或一份长文档总结为更短的表述。通过不断总结,可以使对话记录的规模保持在较小的量级,既减少了词元数量,又只跟踪最重要的信息。其代价是每次摘要都是一次额外的模型调用,既有费用也有延迟,并且摘要本身可能丢掉后续才被证明重要的细节。
-
丢弃:直接删除整条材料。代价最高,因为信息无法恢复,适用的场景只有一类,即已经明确无用的材料,例如一次失败的工具调用及其错误信息在问题解决之后。
压缩的代价通常只被算作一次额外的模型调用,这个算法是不完整的。因为摘要、压缩或截断都会改变前缀并使缓存复用失效。每一次压缩都会使该次压缩位置之后的全部缓存失效,因此下一次请求需要为整段前缀重新付一次全额计算与全额词元费用。
这意味着压缩的次数应该得到有效的控制。即:压缩策略需要一个频率下限,实现上对应两个参数:
- 触发阈值:决定什么时候开始压缩,通常以窗口使用率或词元数表示。
- 最小清理量:决定一次压缩至少要释放多少容量。这就是频率下限的来源。
建议是宁可一次多压一些,也不要让压缩在相邻轮次之间反复触发。具体的数值与场景的单轮体量有关:单轮较短而轮数很多的场景,一次压缩应当覆盖较多轮;单轮体量很大的场景,则一次压缩覆盖两三轮即可。
写出与沉淀
决定本轮产生的信息哪些值得留到下一轮。它要处理的对象有三类:
- 会话记录:用户与模型的对话历史会被持久化,且只有最近 50 条会作为工作记忆取出。原样落盘是为了供后续的审计与回放使用。
- 抽取出的长期记忆:从会话记录中提炼出中长期记忆。
注意,上下文的写出只会影响个人记忆。而不会影响公共的 RAG 知识库。
但是如果向量个人经验沉淀为团队经验,那么此时可以有限制的进行 RAG 知识库的写入和更新。
可观测性
为什么观测是必要的
主要有以下 3 点原因:
-
所有配置错误都不报错:上下文是由系统提示、记忆召回、知识库召回、工具定义、历史消息等多个部件动态组装出来的,任何一处放错了位置、少放了一项或多放了一项,都不会产生任何异常,只表现为回答质量下降。没有观测手段时,这类问题唯一的发现途径是用户投诉。
-
故障的归因需要区分三种原因:一次回答不正确,可能是材料根本没有被检索到,可能是检索到了但没有进入上下文,也可能是进入了上下文而模型没有采用。这三者的修正或优化方式完全不同,分别对应调整检索、调整组装方式与调整提示词,而从回答的表现上看三者完全一样。
-
组装规则的效果无法从回答上观察:第七章那套按刷新频率决定组装位置的规则,其收益体现在缓存命中率与成本上,而不体现在回答质量上。也就是说规则是否真的生效,除了观测缓存指标之外没有别的验证手段。
要观测什么
观测项分成两组:
- 第一组是一次请求的完整输入与输出;
- 第二组是上下文工程各环节的运行指标。
第一组的内容已经由 OpenTelemetry 的生成式 AI 语义约定给出了标准字段,笔者建议直接沿用这套命名,而不要自定义一套,理由是它与现有的追踪系统天然对接:
| 字段 | 记录内容 |
|---|---|
| gen_ai.input.messages | 本次请求的完整消息序列,含每轮注入的材料 |
| gen_ai.output.messages | 模型的完整输出,含工具调用 |
| gen_ai.system_instructions | 本次请求使用的系统提示 |
| gen_ai.conversation.id | 会话标识,用于把多轮请求串起来 |
| gen_ai.tool.name | 本次调用的工具名 |
其中第一项与第三项合起来就是 9.1 节所说的最小必要观测,即完整的最终输入,它是其余所有排查的起点,也是唯一能够回答材料到底进没进上下文的证据。
与之配套的还有用量字段。除了常规的输入与输出词元数,缓存写入词元数与缓存读取词元数需要单独记录,它们是第七章组装规则与 8.6 节频率控制的直接验证指标。
第二组是各环节的运行指标,按前八章的环节列出:
- 检索环节:召回条数、相关度分数分布、召回为空的比例。
- 记忆环节:抽取触发次数、四类更新动作各自的发生次数、写入去重的命中次数。
- 组装环节:各类额度的实际使用率、缓存命中率。
- 压缩环节:压缩触发频次、每次释放的容量、配对完整性校验的失败次数、压缩失败的告警次数。
- 隔离与转存环节:子任务的调用次数与返回体量、转存次数与取回次数。
这里有一处必须一并处理的事项:完整输入里会包含用户的敏感信息,而 3.4 节那条写入边界脱敏的纪律只覆盖记忆库,不覆盖观测数据。观测数据的保护手段是访问控制与保留期,而不是脱敏,理由是排查与审计恰恰需要原文,掩掉之后观测就失去了意义。两者的策略不同,宜分开设计。
要评测什么
观测回答的是这一次发生了什么,评测回答的是整体做得怎么样。两者不可互相替代:观测是逐次记录,没有判定标准;评测是对一批样本给出分数,需要事先准备带标注的数据集。
检索增强场景的评测已经有一套通用指标,以 RAGAS 这类评测框架为代表,其中与上下文工程直接相关的有以下 4 项:
| 指标 | 衡量什么 | 对应的环节 |
|---|---|---|
| 上下文精确率 | 召回的若干条材料中,真正支撑了答案的占多少 | 召回与重排 |
| 上下文召回率 | 应当被召回的材料中,实际召回了多少 | 切分与索引 |
| 忠实度 | 回答中的每一条陈述是否都能在所给材料里找到依据 | 生成 |
| 答案相关性 | 回答是否切合问题本身,而不是答了相关但不同的问题 | 生成 |
这张表的价值在于它把回答不正确这一个笼统的现象分解到了具体环节上,与 9.1 节第 2 点那三种归因正好对应:上下文召回率低指向切分与索引,上下文精确率低指向召回与重排,而前两项都正常而忠实度低,则指向提示词与组装。

那么,这四项指标够不够呢?笔者认为对单轮问答基本够用,但对多轮会话不够,理由是这四项都是按单次问答计算的,而 1.2 节那四类退化全都发生在轮次之间:过早作答、答案膨胀、自我条件化与过早终止,没有一类能在单轮样本上被测出来。
因此多轮场景需要另一类评测数据集,其构造方式与单轮不同,关键差别有以下 2 点:
- 样本的单位是一整段会话而不是一个问题,并且会话中要刻意包含信息分散在多轮、中途修正前提、以及需要回溯早先约束这几种情形。
- 打分的单位同样是会话而不是轮。一个按轮打分的评测会给出很高的平均分,因为大多数轮次是正常的,而失败恰恰集中在少数关键轮次上。
至于评测集本身从哪里来,笔者的建议是从真实会话记录中挑选有代表性的样本再做人工标注,而不是让模型生成。理由是模型生成的问题分布与真实用户的问题分布差别很大,尤其缺少指代不清、前提错误与中途改口这几类,而它们正是上下文工程最容易出问题的地方。