这两年做 Agent 项目,最深的体感是:模型能力本身已经不是主要瓶颈,真正拉开差距的是上下文工程。同一个底座模型,有人搭出来的 Agent 像个靠谱的实习生,有人搭出来的就是个复读机。差别不在 prompt 写得多花哨,而在于你喂给模型的上下文——怎么存、怎么筛、怎么排序、怎么在正确的时间放进窗口里。
这篇文章不会讲那些“教你写个 system prompt”的入门内容。我想聊的是从记忆存储到智能决策的完整链路:长期记忆怎么建模、向量检索怎么和上下文窗口配合、token 预算怎么分配、决策路由怎么避免抖来抖去,以及最终怎么把这套东西搭成一条可观测、可回滚的流水线。如果你正在做一个要长期服务真实用户的 Agent,或者你团队已经开始被“上下文塞不下”“记忆串场”“决策不稳定”这类问题困扰,那这篇内容应该对你有用。
1. 上下文工程:Agent 能力的隐形天花板
1.1 为什么上下文工程才是 Agent 的核心竞争力
很多人一开始做 Agent,第一反应是选最强的基础模型,第二反应是把 prompt 写得事无巨细。这两件事重要,但远远不够。打个比方,模型的参数是员工的智商,prompt 是入职手册,而上下文是员工每天实际看到的工单、聊天记录、项目资料。一个聪明员工如果每天只看到过时信息,照样会把事情搞砸。
我在业务里见过太多类似案例。有个客服类 Agent,系统 prompt 里写了完整的服务规范,但用户问“我上次那个退款为什么还没到账”,Agent 因为拿不到用户的订单历史和退款进度,只能凭空给出通用话术。这不是模型不会,而是上下文里缺了最关键的记忆。反过来,另一个团队把所有历史对话一股脑拼进 prompt,结果用户问了个简单问题,上下文里却塞了两万 token 无关记录,模型被噪声带偏,回答又臭又长。
所以我把上下文工程定义为:管理“模型该看到什么、以什么顺序看到、看到多少”的一整套工程手段。它连接存储系统、检索系统、窗口管理器和决策逻辑,是 Agent 从 demo 走向可用的必由之路。理解了这件事,才会理解为什么记忆存储和智能决策本是同一件事的两端。
1.2 全栈流水线的整体架构拆解
既然叫全栈流水线,我就按数据流动的方向把它切成四层:
- 记忆存储层:负责把交互历史、用户画像、业务事实沉淀为可检索的结构化数据。
- 上下文编排层:负责在每次请求时,从记忆层捞出候选内容,做重排、裁剪、压缩,组装成最终送进模型的 prompt。
- 决策执行层:负责基于组装好的上下文,决定直接回复、调用工具、询问用户或走某个工作流。
- 反馈回写层:负责把决策结果和用户反馈再写回记忆存储层,形成闭环。
这四层不是四个独立模块,而是同一条流水线上的四个工位。决策执行层输出质量上限,由前两层提供的上下文质量决定;记忆存储层的数据能否沉淀成长期价值,又取决于反馈回写层是否把结果有效记录。任何一层掉链子,Agent 都会出现“记不住、找不到、用不上”的问题。
我习惯用一张请求时序图来推演整条链路:用户消息进入后,先做意图识别和实体抽取,再基于这些信号触发记忆检索,检索结果经过重排和 token 预算裁剪后,和最近对话历史、系统指令拼装成完整上下文,模型决策后如果要调工具,工具结果会再次进入上下文,最终生成回复,同时异步触发记忆写入和更新。这个时序稳定跑通之后,Agent 的能力边界才真正由你自己掌控。
2. 记忆存储层:给 Agent 装一套“外置大脑”
2.1 记忆的分类与建模:短期、长期、工作记忆
记忆不是简单地把对话历史存起来。我习惯把它分成三类来建模。
短期记忆是当前会话内的上下文,比如用户刚才提的问题、你上一轮给出的回复。它时效性强,通常不落库,直接放在会话状态里。长期记忆是跨会话持久存在的用户事实和业务信息,比如用户偏好、历史订单、项目背景、之前确认过的结论。工作记忆则更像当前任务栈,比如多轮工具调用的中间结果、正在执行的分步计划。
很多 Agent 翻车,就是因为三类记忆混为一谈。把短期对话全量写入长期库,导致记忆库迅速膨胀;把长期事实当成短期上下文,每次请求都从头检索一遍,浪费 token 还容易漏。我的做法是为长期记忆设计一个简单通用的记忆单元结构,至少包含四个字段:实体主体(谁)、事实描述(什么事)、可信度/来源(怎么知道的)、时间戳(什么时候知道的)。在此基础上,再区分“事实”和“事件”:事实是相对稳定的偏好和属性,事件是一次性发生的事情。比如“用户偏好简洁回复”和“用户昨天发起了一笔退款”就是两类东西,检索权重和更新策略都应不同。
2.2 向量存储选型与数据管线设计
选型阶段,我踩过不少坑,也总结出几条经验。记忆检索的核心是语义召回,所以向量存储基本躲不掉,但不要为了向量化把整个存储体系推倒重来。完全可以搞双轨:结构化信息放常规数据库,用元数据过滤过滤候选集;文本信息用向量索引做语义召回。两者通过实体 ID 关联。
具体选型上,我的判断标准就三条:第一,能否按用户 ID 做分区或过滤,这决定了多租户隔离能不能做好;第二,写入吞吐能不能扛住 Agent 的异步记忆写入;第三,能否原生配合常见的 embedding 接口做向量索引。市面上大多数主流向量方案都满足,真正拉开差距的是 schema 设计。我在项目里给记忆表设计了分区键和二级索引,分区键通常是用户 ID,二级索引包括记忆类型、时间戳、访问频率,这样检索时可以先用元数据把候选集压到几百条,再算向量相似度,速度和准确率都会好很多。
数据管线方面,我的建议是:记忆写入单独放在一条异步链路上,不要阻塞主请求。对话结束后,由后台服务对本次交互做摘要、抽取实体、生成记忆单元,再写入存储。这样做有几个好处:用户不会有额外延迟;记忆写入失败不影响主流程;重要程度判断可以用更大的模型慢慢做。
2.3 记忆写入、更新与遗忘机制
记忆写入不能“捡到篮子里都是菜”。我见过最多的问题,是把每一轮对话原样写入记忆库,最后检索出来全是碎句子,既占窗口又没信息量。合理的写入策略应该带一个“值得记性”的判断:包含新的用户事实、新的偏好、明确决定或关键事件片段才值得写入;寒暄、重复表达、无信息量问答直接丢弃。
更新机制同样要小心。用户说“我改主意了,以后用邮件联系”,这条新事实应该覆盖旧事实,而不是和旧事实并存。我会给记忆单元加一个版本号和当前生效标记,新事实写入时把同主体的旧事实标记为失效。检索时默认只取生效中的记忆,避免模型看到互相矛盾的信息。
遗忘机制容易被忽略。长期记忆如果只增不减,检索质量会持续下降。我常用的策略是给记忆单元维护一个重要性分数和最近访问时间,某个记忆长时间未被命中,重要性再高也要降级或归档。更简单实用的做法是定期做一次离线清理:把互相重复的记忆合并、把低价值事件压缩成摘要、把所有确认失效的内容物理删除或移入冷存储。
3. 上下文编排层:窗口不是越大越好
3.1 上下文窗口的预算管理与分配策略
很多人以为上下文窗口越大,Agent 就越聪明,这是今年最贵的错觉。窗口再大,检索结果噪声也多,模型注意力会被无关信息稀释。上下文编排的核心是预算管理,就像装修房子:面积再大,功能分区不规划,住着也难受。
我习惯给上下文做一个预算表,按 token 分配比例。假设窗口上限是 128k,我会预留 20% 给输出 token 和工具定义,剩下 80% 再分给系统指令、检索到的相关记忆、近期对话历史和当前消息。一个通用模板是:系统指令占 5%~10%,检索记忆占 30%~40%,历史对话占 20%~30%,当前输入和工具中间结果占 20%~25%。这些比例不是拍脑袋定的,核心原则是:检索记忆是当前决策的主要依据,必须给足空间;历史对话是辅助信息,保留最近几轮回合即可;系统指令要精简,否则会挤占真正有用的记忆空间。
预算确定后还要做动态裁剪。模型输出的最大长度要留够,比如 max_tokens 设为 4096,缓冲区再预留 2048,实际可用上下文就是窗口上限减掉这 6k。检索结果超预算时,不能粗暴截断,而应该按重排后的分数截断,保证留下的是最高质量内容。这套逻辑完全可以写成函数:输入用户维度、会话维度、系统维度,输出一份已经裁剪到位的上下文。
3.2 上下文压缩与结构化摘要
对话一旦超过预算,就要触发压缩。压缩不是把旧内容丢掉,而是把旧内容“变成”更小的新内容。你可以把它理解为记账:长期流水太大,就按月做一份汇总,明细归档,但汇总里必须保留与未来决策相关的关键数字和结论。
压缩有两种常见形态。第一种是对话级压缩:当最近窗口超过阈值,调用一次模型,把较早的往返对话总结成 300 字以内的摘要,保留当时的决定、用户诉求和遗留问题,然后替代那些原始对话进入窗口。第二种是记忆级沉淀:压缩后的摘要不是只为了腾出空间,而是要顺着记忆写入链路,提炼成永久记忆。比如用户连续问了一周“帮我对比三款设备的续航”,这个偏好应该沉淀成“用户正在评估设备续航,关注续航数据”,下次再问就直接命中。
压缩有一个大家容易忽略的坑:摘要不能无限套娃。旧的摘要再被摘要,信息损耗会指数级放大。我的对策是压缩时保留一个“不可压缩字段”清单,包括用户 ID、金额、日期、明确结论等关键事实,即使做摘要也必须原样保留。宁可摘要长一点,也不能把关键数字丢掉。
3.3 混合检索策略:让相关记忆“自己浮上来”
记忆检索最怕两种极端:只靠关键词,语义近似但表述不同的记忆被漏掉;只靠向量,精确实体信息反而排到后面。所以我最终采用的是混合检索加两层排序。
第一层是召回。同时跑向量相似度检索和全文关键词检索,各自取 top N 个候选,做并集。向量负责语义相关,关键词负责实体精确匹配。第二层是融合排序。候选集合并后,把原始分数归一化,再叠加几个业务因子:时间衰减因子、重要性分数、记忆类型权重。时间衰减我常用一个指数函数,比如 score 乘以 0.95 的 n 次方,n 是距离今天的天数,这样昨天的事件比三个月前的事件自然排前。
如果你有充足算力,还可以加第三层重排序:把召回的 20~30 条候选喂给一个专门的重排序模型,让它基于和当前问题的相关性,输出一个精排后的列表。这一层效果非常明显,但成本也高,我建议先在离线实验里测一测收益,只有召回精确率确实不足时再引入。
4. 智能决策层:从上下文到行动的临门一脚
4.1 决策路由:判断该用什么工具、该不该问
决策层的核心任务,是把组装好的上下文转化为行动。最常见的行动有三种:直接生成答案、调用工具获取新信息、向用户提问澄清。很多 Agent 的问题在于无脑直接生成,或者无脑调用工具,缺少路由判断。
我习惯在决策层里塞一个显式路由步骤。不是写一个大 prompt 让模型自由发挥,而是先让模型输出结构化判断:当前问题是否需要额外数据?如果需要,需要哪一类数据?是否已经掌握足够信息?然后根据判断走不同分支。这个路由可以用一个小的模型甚至规则做,成本低、稳定性高。举例来说,用户问“我的订单到哪了”,路由层应该先识别出“需要查询订单状态”,再调用订单接口;用户问“你们什么时候发货”,如果 Agent 已经知道订单信息和发货规则,就直接回答,没必要再调一遍接口。
“该不该问”同样重要。我见过不少 Agent 自作主张,在信息不足时硬答,最后被用户揪着打脸。我的策略是:当关键信息缺失且检索不到时,宁可多问一句。同时给模型设置一个置信度阈值,低于阈值就触发澄清分支。这个阈值不是固定不变的,要根据业务容错率调整:容错率越低,阈值设得越高。
4.2 规划与验证:让决策可解释可回滚
自动化决策最怕不可控。决策可以快,但不能黑盒。我要求所有决策过程必须输出一张决策轨迹卡片,里面至少包含:路由判断结果、命中的记忆条目标识、调用的工具和参数、模型推理的置信度。这样做有两个好处:用户投诉和模型出错时,可以回查是哪一步出了问题;积累一段时间后,能拿到真实数据去优化路由规则和检索权重,而不是靠玄学调 prompt。
对于涉及核心业务的操作,我还会加一道验证闸门。工具调用不能只靠模型自觉,执行前要过一遍校验器,检查参数格式、上下文是否匹配、操作是否命中风险规则。某些高影响操作可以直接做成“需要用户确认”模式,比如退款、发送消息、修改数据,模型只能生成建议,最终确认必须由用户或上层系统完成。这是最朴素也最有效的防呆设计。
回滚机制同样不能少。如果工具调用返回异常,或最终回复与检索到的事实冲突,决策层要有明确的降级路径:先重试一次;重试失败就换备选工具;如果备选也没有,就明确告诉用户“暂时无法完成”,而不是硬编一个答案。
4.3 决策反馈闭环:把结果写回记忆
决策层不是终点。Agent 答得好不好,最终要用结果数据来修正记忆和检索。这是很多团队忽略的一环,也是记忆系统越用越聪明的关键。
具体做法是:每次决策完成后,记录一个反馈事件。事件里包括用户问题、决策类型、检索出的记忆、模型回复、用户后续行为(是否纠正、是否满意、是否中断)。这些反馈经过异步处理后,一部分用来更新已有记忆的重要性分数,比如被模型采纳且用户确认有效的检索结果,权重上调;被模型忽略或用户纠正的内容,权重下调。另一部分用来沉淀新记忆,比如用户对某个回答做了纠正,说明这里存在一条未被记录的偏好,应该写入。
反馈闭环不是一次性的,而是要持续跑。我会定期基于这些反馈事件做回归分析:哪些类型的查询总是检索不到正确记忆?哪些记忆即使检索到了也被模型忽略?这些问题最终会回到 2.3 节的写入策略和 3.3 节的检索策略,形成一条完整的持续优化路径。
5. 全栈链路实操:从零搭建一个记忆增强的决策 Agent
5.1 整体技术模块与数据流
讲完分层设计,我直接给一个可落地的参考实现。假设我们要搭一个带长期记忆的客服助理 Agent,可以回答产品问题、查询订单状态、记录用户偏好。技术模块大概长这样:
- 服务入口:接收用户消息,管理会话状态。
- 记忆服务:负责长期记忆的写入、检索、更新。
- 检索与重排服务:负责混合召回和排序。
- 上下文构建器:负责 token 预算分配和上下文组装。
- 决策服务:负责路由判断、工具调用、生成回复。
- 反馈处理器:异步消费对话日志,更新记忆。
数据流是:用户消息 → 上下文构建器请求记忆服务检索 → 记忆候选经过重排和裁剪 → 连同系统指令、会话历史组装 → 决策服务判断并生成 → 如果工具调用,工具结果追加进上下文再生成 → 最终响应返回 → 反馈处理器异步写记忆。整套链路里,唯一同步阻塞的路径是用户请求到响应返回的那一段,其他全部异步,尽量让主链路延迟可控。
5.2 上下文构建的关键代码与参数配置
这是整套系统里最值得自己手写一遍的模块。下面给一个简化但能直接用的上下文构建器骨架:
def build_context(user_id, session_id, user_message): # 1. 计算 token 预算 window_limit = 128_000 output_reserved = 6_000 context_budget = window_limit - output_reserved system_prompt = load_system_prompt(session_id) budget_detail = { "system": int(context_budget * 0.08), "memory": int(context_budget * 0.38), "history": int(context_budget * 0.27), "current": int(context_budget * 0.27), } # 2. 混合检索记忆 candidates = retrieve_memories( user_id, user_message, top_k=20, time_decay=0.96, ) # 3. 按预算截断记忆 memory_block = pack_block(candidates, budget_detail["memory"]) history_block = trim_history(session_id, budget_detail["history"]) # 4. 组装最终上下文 messages = [ {"role": "system", "content": system_prompt}, *memory_block, *history_block, {"role": "user", "content": user_message}, ] return messages, budget_detail说明几个参数:top_k 取 20 是兼顾召回率和重排成本的经验值;时间衰减 0.96 表示每过一天,记忆分数乘 0.96,约 17 天后权重减半。token 预算比例不是硬编码死,而是要从日志统计里调,比如发现历史对话总是超预算,就该提高 history 比例或压缩得更狠。
记忆检索函数这里值得看更多细节。我习惯把检索目标拆成一个查询对象,而不是只拿原始用户问句去向量化。比如用户问“我之前说过我过敏,你能记住吗”,直接向量化效果很差,更好的做法是多路查询:原始问题一路,抽取出的关键实体一路,最近对话的关键意图一路。各路检索结果合并时做去重,保留分数最高的。
5.3 观测指标与调优方法
没有任何上下文工程可以在不观测的情况下做对。我会在整套链路里埋三类指标:
- 检索质量指标:记忆召回率、重排后 Top5 命中率、检索平均延迟、空结果比例。
- 上下文使用指标:每次请求的 token 消耗、各分区预算占比、超时截断率、压缩触发频率。
- 决策质量指标:工具调用成功率、用户纠正率、兜底触发率、端到端满意度打分。
调优顺序非常关键。先调记忆写入,保证该存的都存了;再调检索排序,保证该找的都能找到;最后调上下文预算分配,保证找到的内容被合理地送进窗口。很多团队一上来就调 prompt,忽略前两层,结果怎么调都隔靴搔痒。我用过一个很原始但有效的方法:在日志里随机抽 100 个真实用户问题,人工标记“理想记忆是什么”,然后跑离线检索看 Top5 命中率。这个数低于 70%,就说明记忆写入或检索策略还有问题,先别碰决策层。
6. 在真实业务中踩过的坑与排查建议
6.1 记忆混淆与串扰
记忆混淆是记忆系统最常见的事故,而且特别隐蔽。比如测试环境里用户 A 的订单信息出现在用户 B 的上下文中;或者同一用户在同一会话里,新旧偏好互相冲突。排查这类问题,我会先看检索请求里的过滤条件是否生效。最常见的原因是向量检索的过滤和相似度搜索耦合得太紧,有些实现里 filter 逻辑没真正下推,导致候选集跨越了租户边界。
另一个坑是记忆更新的并发问题。用户同时在两个会话里修改自己的偏好,两个异步写入线程各自创建了新版本,旧版本没有被正确置为失效,最终库里同一个实体出现两条“当前生效”的记忆。解决方法是给记忆单元的版本更新加一个基于用户 ID 和实体 ID 的分布式锁,或者用乐观锁加生效版本号的机制。除此之外,写入侧还要做字段级合并,把“回复偏好在变”和“收货地址没变”这类互补信息合并成一条完整记忆,而不是各写一条。
6.2 检索噪声与上下文污染
检索质量和最终回复质量之间的关系不是线性的。把 top_k 从 5 调到 20,召回率上升了,但噪声也进来了。在上下文里混入 15 条不相关记忆,比完全不检索还要糟——模型会把无关信息当成推理依据。
我踩过最典型的一次:Agent 回答用户“订单预计送达时间”时,检索结果里混进了一条“用户偏好环保包装”,模型居然在回复里专门解释了一句“我们会在送达时使用环保包装”。检索噪声污染让回答显得很蠢。解决办法有三层:第一层,给重排模型设一个最低相关性阈值,低于阈值的候选直接丢弃;第二层,检索关键词和各类检索路返回的分数做加权融合,避免某路召回低质内容过多;第三层,在组装上下文时,给记忆块加一个显式说明,告诉模型“以下是有可能相关的历史记忆,如果无关请忽略”,给模型一个拒绝依据。
6.3 决策抖动与自动化风险
决策抖动是指同样的问题,Agent 在不同时间给出不一致的答案。原因通常不是模型随机性,而是记忆和上下文不稳定:昨天检索到了一条偏好记录,今天那条记录被新版本覆盖了,Agent 的行为就变了;或者 top_k 里混入了一条噪声记忆,直接把决策带偏。
我应对决策抖动有一套流程化检查方法。先固定会话和时间维度,复现问题,看日志里的记忆候选是否有变化;如果候选稳定但回复变了,那就是模型温度带来的,考虑对关键场景降低温度,甚至引入确定性采样;如果候选本身就在波动,就回到检索排序策略找原因。最后还会在关键业务场景加一个“回复一致性校验器”,把本次回复的关键结论和检索到的记忆做一次语义比对,发现明显冲突就触发重生成。
自动化风险更是不能马虎。决策层一旦接入真实业务,工具调用的权限边界必须从源头收紧。我的原则是:只开放最小工具集,每次调用前都要经过预算和风险判定,高影响操作一律加入确认环节。宁可牺牲一些“自动化程度”,也不能让 Agent 在无人确认的情况下执行不可逆动作。
我在实际项目里把这些机制逐步落地之后,最直观的变化并不是单轮回答变聪明,而是 Agent 连续用上一两个月后,记忆越用越准确,用户不需要反复解释自己的背景和偏好。这让我相信,上下文工程的本质就是把“一次性智能”雕刻成“可积累的智能”。如果你现在正卡在 Agent 上线前的某道坎上,不妨回到这条流水线上,逐一检查存储、检索、编排和决策。坑我已经替你踩过不少,剩下的,就是你自己的实测和迭代了。