☰
Agent记忆系统设计:从上下文工程到Harness调度实践
2026/10/20 6:58:37 网站建设 项目流程

1. 从“记不住”到“会调用”:Agent 记忆系统的设计取舍

做 Agent 项目的人,迟早会撞上同一堵墙:模型本身很聪明,但一放到多轮、长周期、跨会话的任务里,就变得像个失忆的实习生。上一轮刚确认过的参数,下一轮就忘了;用户三天前说过的偏好,今天重新问一遍;同一个错误在同一个项目里反复犯。这不是模型能力问题,而是记忆架构没搭好。

我前后参与过几个 Agent 相关的项目,从早期的纯 Prompt 拼接,到后来引入向量库做检索,再到最近开始认真处理“记忆的写入、更新、遗忘”这一整套生命周期,踩过的坑基本能写一本小册子。这篇就围绕 Agent 的记忆、上下文、Skills 和 Harness 四个概念,聊聊经验到底是怎么被保存、调用和更新的。如果你正在做 Agent 应用,或者准备把某个原型推向生产,这些内容应该能帮你少走一些弯路。

先说清楚这四个词在我这里的定义,避免后面混淆。记忆(Memory)指的是 Agent 跨会话、跨任务持久保存的信息,包括用户偏好、历史决策、领域知识等。上下文(Context)是当前这一次推理调用里塞进模型窗口的内容,是记忆的一个瞬时切片。Skills是我对“可复用能力单元”的叫法,比如一个封装好的工具调用流程、一段固定的推理模板。Harness则是把这些东西串起来的那层调度框架,负责决定什么时候读记忆、什么时候写记忆、什么时候调用哪个 Skill。

这四个东西不是并列关系,而是有层次的:Harness 是骨架,Context 是血液,Memory 是长期记忆,Skills 是肌肉。搞混了它们,架构就会乱。

1.1 为什么“把所有东西塞进上下文”必然失败

早期最省事的做法,就是把所有历史对话、所有文档、所有工具说明全部拼进 Prompt。我试过,短会话没问题,一旦超过十几轮,成本和延迟就失控了。更麻烦的是,模型在超长上下文里会“注意力涣散”——关键信息被淹没在大量无关内容里,回答质量反而下降。

这里有个容易被忽略的点:上下文窗口大不等于有效利用大。实测下来,当输入超过某个阈值后,模型对中间位置信息的召回率明显下滑,这就是常说的“中间遗忘”现象。所以正确的思路不是“塞更多”,而是“塞更准”。

这就引出了记忆系统的核心矛盾:写入要全,读取要精。写入阶段尽量把有价值的信息结构化保存下来,读取阶段只挑当前任务真正需要的部分注入上下文。这两步的取舍,决定了整个 Agent 的智商上限。

1.2 记忆的三种类型与各自的存储选型

我在项目里通常把记忆分成三类,分别用不同的存储方案:

记忆类型内容举例存储方案读取时机
短期记忆当前会话的对话轮次内存队列 / Redis每轮都带
长期事实记忆用户偏好、项目背景关系库 + 向量库会话开始时检索
程序性记忆成功过的操作流程结构化文档 / 向量库任务匹配时检索

短期记忆最简单,维护一个滑动窗口就行,超出长度就丢弃最旧的。但这里有个技巧:不要无脑丢最旧的,而是按重要性打分。我一般会给每条消息打一个权重,用户明确表达的偏好权重高,寒暄权重低,窗口满了优先丢低权重的。

长期事实记忆是重点。我的做法是双写:结构化字段(比如用户 ID、偏好类型、值)存关系库,方便精确查询和更新;同时把自然语言描述存向量库,方便语义检索。两者用同一个 ID 关联。这样既能“精确查某个用户的某个偏好”,也能“模糊找和当前任务相关的历史信息”。

程序性记忆最容易被忽视,但价值极高。一个 Agent 如果能把“上次这类任务是怎么一步步做成的”存下来,下次遇到类似任务直接复用流程,效率提升非常明显。我一般把成功的操作序列抽象成模板,存成结构化文档,检索时按任务相似度匹配。

1.3 记忆写入的时机:什么时候该记,什么时候不该记

这是我在实际项目里纠结最久的问题。早期我让 Agent 每轮都总结并写入记忆,结果记忆库迅速膨胀,大量冗余和矛盾信息,检索质量直线下降。

后来我改成事件驱动写入,只在几个明确时机写:

  • 用户明确表达偏好或纠正时(“我不喜欢这种风格”“以后都用这个格式”)
  • 任务成功完成时,把整个流程抽象成程序性记忆
  • 会话结束时,做一次总结性写入
  • 检测到与已有记忆冲突时,触发更新而非新增

提示:写入前一定要做去重和冲突检测。我见过太多项目因为记忆库里有三条互相矛盾的“用户偏好”,导致 Agent 行为不稳定。

冲突检测的逻辑我一般这样设计:新记忆写入前,先检索语义最相近的几条已有记忆,如果相似度超过阈值且内容矛盾,就走更新流程而不是新增。更新时保留版本号,方便回溯。

2. 上下文工程:把对的信息在对的时机喂给模型

上下文工程这个词最近被说烂了,但真正做对的人不多。我的理解是:上下文工程的核心不是“写 Prompt”,而是“管理信息流”。它要解决的是:在有限的窗口里,如何动态组装出当前这一步推理最需要的信息。

2.1 上下文的四层结构

我在项目里习惯把上下文分成四层,从外到内依次是:

  1. 系统层:角色定义、全局约束、输出格式要求。这层基本固定,很少变。
  2. 记忆层:从长期记忆检索出来的相关内容。这层每次会话开始时组装,会话中可能动态更新。
  3. 任务层:当前任务的目标、约束、已完成的步骤。这层随任务推进不断变化。
  4. 对话层:最近的几轮交互。这层用滑动窗口管理。

分层的好处是职责清晰。系统层稳定,可以缓存;记忆层按需检索;任务层由 Harness 维护状态;对话层做窗口裁剪。每层独立优化,互不干扰。

这里有个实操细节:层与层之间要有明确的分隔标记。我一般用清晰的分节符和标题,让模型能区分“这是背景知识”和“这是当前任务”。实测下来,有明确分隔的上下文,模型对信息归属的判断准确率明显更高。

2.2 检索增强的精度问题与重排序

记忆检索最常用的方案是向量相似度。但纯向量检索有个通病:召回的相关内容里,真正有用的可能只占一小部分。比如你检索“用户对报告格式的偏好”,可能召回一堆关于报告内容的讨论,真正讲格式的只有一条。

我的解决方案是两阶段检索 + 重排序:

第一阶段用向量检索召回 Top-K(K 一般取 20 到 50),保证召回率。第二阶段用一个轻量的重排序模型,对这 K 条按与当前查询的相关性重新打分,取 Top-N(N 一般 3 到 5)注入上下文。

重排序模型不需要太大,一个小型的交叉编码器就够用。如果不想引入额外模型,也可以用规则打分:比如包含关键词的加分、时间更近的加分、被引用次数多的加分。我试过纯规则方案,在垂直领域效果也能接受。

注意:检索的查询本身也需要构造。不要直接拿用户最后一句话去检索,而是让模型先根据当前任务生成一个“检索意图”,再用这个意图去查。这一步能显著提升召回质量。

2.3 上下文压缩:长会话的续命技巧

长会话是上下文管理的噩梦。几十轮对话下来,原始内容早就超窗口了。我的做法是滚动摘要 + 关键信息抽取。

具体来说,每 N 轮(我一般设 5 到 8 轮)做一次摘要,把这几轮的核心信息压缩成一段话,替换掉原始对话。同时,从这几轮里抽取结构化信息(比如确认的参数、达成的结论),单独存到任务层。

这样对话层始终保持在一个可控的长度,而关键信息不会丢。摘要的质量很关键,我一般用模型来做,Prompt 里明确要求“保留所有决策、参数、结论,丢弃寒暄和重复内容”。

还有个技巧:给摘要分层级。近期摘要详细,远期摘要粗略。比如最近 10 轮的摘要保留细节,10 轮以前的只保留一句话概括。这样既省空间,又不丢关键脉络。

3. Skills:把可复用的能力封装成模块

Skills 这个概念,我最早是从工具调用(Tool Use)演化过来的。一开始只是给 Agent 几个 API,后来发现很多操作流程是固定的,每次都让模型重新推理一遍既浪费又不稳定,于是就把这些流程封装成 Skill。

3.1 Skill 的粒度怎么定

粒度是设计 Skill 时最头疼的问题。太细,模型要调用很多次,容易出错;太粗,灵活性差,遇到变体就抓瞎。

我的经验是按“原子业务操作”来划分。判断标准是:这个操作有没有明确的输入输出?能不能独立测试?会不会被多个任务复用?三个都满足,就值得封装成一个 Skill。

举个例子,在一个数据处理 Agent 里,“读取表格”“清洗缺失值”“生成统计摘要”是三个独立 Skill,而“完成整个数据报告”不是 Skill,是任务编排。前者可复用、可测试,后者是一次性的流程组合。

我一般会把 Skill 分成两类:工具型 Skill(封装外部调用,比如查数据库、调 API)和推理型 Skill(封装固定的推理模板,比如“按这个框架分析问题”)。前者强调稳定和错误处理,后者强调 Prompt 质量和输出格式。

3.2 Skill 的描述怎么写才能被正确调用

Skill 能不能被正确调用,八成取决于描述写得好不好。我踩过的坑是:描述写得太技术化,模型看不懂什么时候该用;或者写得太模糊,模型动不动就调用。

好的 Skill 描述应该包含四要素:

  • 做什么:一句话说清功能
  • 什么时候用:明确触发场景
  • 输入是什么:参数含义和格式
  • 输出是什么:返回结构

我一般还会加一个“不要什么时候用”的反例说明,减少误调用。比如“当用户只是询问概念时,不要调用这个查询 Skill”。

提示:Skill 描述里的触发场景要写得具体。写“处理数据相关任务”太泛,写“当用户要求计算某列的均值、中位数、标准差时”就明确多了。

3.3 Skill 的组合与编排

单个 Skill 能力有限,真正的威力在于组合。Harness 的一个重要职责就是根据任务目标动态编排 Skill 调用序列。

我的做法是维护一个 Skill 依赖图:每个 Skill 声明自己的前置条件和产出,Harness 根据当前状态和目标,规划出一条调用路径。简单场景可以用规则匹配,复杂场景可以让模型来规划,但规划结果要经过校验(比如检查前置条件是否满足)。

这里有个实用技巧:把常用的 Skill 组合固化成“复合 Skill”。比如“数据清洗 + 统计分析 + 生成图表”经常一起出现,就封装成一个复合 Skill,内部按固定顺序调用。这样既减少了编排开销,又提升了稳定性。复合 Skill 内部还可以做错误处理和重试,比让模型每次重新规划靠谱得多。

4. Harness:串起记忆、上下文与 Skills 的调度层

Harness 是我对 Agent 运行时框架的叫法。它不直接产生智能,但决定了智能能不能被稳定地组织和调用。一个设计良好的 Harness,应该让记忆、上下文、Skills 各司其职,而不是互相纠缠。

4.1 Harness 的核心循环

我设计的 Harness 主循环大致是这样的:

  1. 接收输入,更新任务状态
  2. 根据任务状态,决定检索哪些记忆
  3. 组装上下文(系统层 + 记忆层 + 任务层 + 对话层)
  4. 判断是否需要调用 Skill,如果需要,选择并执行
  5. 处理 Skill 结果,更新任务状态
  6. 判断任务是否完成,未完成则回到第 2 步
  7. 任务完成后,触发记忆写入

这个循环看起来简单,但每一步都有讲究。比如第 2 步“决定检索哪些记忆”,就需要根据任务类型和当前阶段动态调整检索策略。第 4 步“选择 Skill”,需要综合考虑任务目标、当前状态和 Skill 的适用条件。

4.2 状态管理:Harness 的骨架

Harness 最核心的部分是状态管理。Agent 在任务执行过程中会产生大量中间状态:已确认的参数、已完成的步骤、待处理的分支、遇到的错误等。这些状态必须有明确的存储和更新机制。

我一般用一个结构化的状态对象来维护,包含:

  • 任务目标与约束
  • 已完成步骤及结果
  • 待办步骤队列
  • 关键变量(参数、配置)
  • 错误记录与重试计数

这个状态对象在每一步都会被序列化进上下文,让模型始终清楚“现在在哪、要去哪、已经做了什么”。状态更新由 Harness 控制,而不是让模型自由发挥,这样能避免状态漂移。

注意:状态对象不要太大,只保留对后续决策有影响的信息。中间过程的详细日志可以存到外部,需要时再检索。

4.3 错误处理与重试策略

Agent 执行任务时出错是常态,关键是 Harness 怎么处理。我的策略是分级重试:

  • 瞬时错误(网络抖动、限流):直接重试,指数退避
  • 参数错误:让模型根据错误信息修正参数后重试
  • 逻辑错误(步骤顺序不对):回退到上一个稳定状态,重新规划
  • 无法恢复的错误:终止任务,记录现场,通知用户

重试次数要有上限,我一般设 3 次。超过就升级处理,避免无限循环烧钱。

这里有个经验:错误信息要原样传给模型,不要自己加工。模型对原始错误信息的理解往往比我们转述的更准。同时,把之前失败的原因也带上,避免模型重复同样的错误。

4.4 记忆、上下文、Skills 在 Harness 中的协同

最后说说这三者怎么在 Harness 里协同。我的设计原则是单向依赖:Harness 依赖记忆和 Skills,记忆和 Skills 之间不直接依赖,上下文是 Harness 组装出来的产物。

具体流程是:Harness 从记忆里取信息,从 Skills 里选能力,组装成上下文,交给模型推理,拿到结果后更新状态,必要时写回记忆。整个过程 Harness 是唯一的协调者,这样逻辑清晰,也方便调试。

如果让记忆和 Skills 互相调用,或者让模型直接操作记忆,很快就会乱套。我早期就犯过这个错,让模型自己决定写什么记忆,结果记忆库里全是模型的主观总结,检索时噪声极大。后来改成 Harness 控制写入,质量立刻上来了。

5. 常见问题与排查技巧实录

做 Agent 项目,问题排查占的时间比写代码还多。我把踩过的典型问题整理成一张速查表,方便对照。

问题现象可能原因排查方向解决思路
模型忘记之前确认的信息记忆未写入或检索未命中检查写入日志和检索结果补写入逻辑,优化检索查询
反复调用同一个 SkillSkill 描述触发条件太宽查看调用日志收窄触发场景,加反例说明
上下文超长导致报错对话层未裁剪检查窗口管理逻辑启用滚动摘要,限制对话层长度
记忆库膨胀检索变慢无去重和冲突检测统计记忆条数和重复率加去重、冲突更新、定期清理
任务执行到一半跑偏状态未正确维护检查状态对象更新点由 Harness 统一更新状态
输出格式不稳定系统层约束不够强检查系统 Prompt加格式示例,明确禁止项

除了表里的,还有几个我踩过的坑值得单独说。

第一个坑:检索阈值设得太死。早期我用固定的相似度阈值过滤检索结果,结果要么召回太少,要么噪声太多。后来改成动态阈值:根据当前任务的重要程度调整,重要任务放宽阈值保召回,简单任务收紧阈值保精度。

第二个坑:Skill 没有超时控制。一个外部调用卡住,整个任务就挂起。后来给每个 Skill 加了超时和降级逻辑,超时后返回一个默认结果或走备用路径,任务能继续推进。

第三个坑:记忆更新没有版本控制。用户偏好变了,旧记忆被覆盖,出问题时无法回溯。后来加了版本号和历史记录,每次更新保留旧版本,方便排查。

第四个坑:上下文组装顺序影响效果。同样的内容,放在上下文开头和结尾,模型的使用效果不一样。实测下来,把最关键的约束放在开头和结尾,中间放背景信息,效果最好。这大概和模型的注意力分布有关。

6. 一些实操心得与后续可扩展的方向

聊了这么多架构层面的东西,最后分享几个具体的实操心得,都是真金白银换来的。

关于记忆写入:宁少勿多。我现在的原则是,只有明确对后续任务有价值的信息才写入。写入前问自己一句:这条信息下次什么时候会用到?答不上来就不写。

关于上下文组装:能用结构化就不用自然语言。结构化信息模型解析更准,也更容易控制长度。比如参数用 JSON 传,比用一段话描述靠谱得多。

关于 Skill 设计:先手动跑通流程,再封装成 Skill。不要一上来就设计 Skill,而是先把任务手动做几遍,找到稳定的操作序列,再抽象成 Skill。这样封装出来的 Skill 才实用。

关于 Harness:日志要全。每一步的输入、输出、状态变化都记下来,出问题时能完整回放。我现在的 Harness 会把每次推理的完整上下文和结果存下来,排查效率提升非常明显。

这个方向后续还能扩展的地方不少。比如记忆的自动衰减机制,让久未使用的记忆逐渐降低权重;比如 Skill 的自动发现,从成功任务里自动抽象出新的 Skill;再比如 Harness 的多 Agent 协作,让多个 Agent 共享记忆和 Skills。这些我还在摸索,有新的体会再单独写。

如果你也在做类似的东西,欢迎交流踩坑经验。这个领域变化太快,一个人闷头搞容易走弯路,多看看别人怎么做的,能省不少时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询