资讯动态

Agent开发实战:技能、记忆与上下文文件的分离设计

发布时间:2026/10/18 16:03:16 来源:尧图企业网站定制
先坦白一件事我从第二版Agent开始被“对话一长就失忆、换了个会话啥都不记得、明明写好的工具却死活不触发”这三个问题折磨了大半个月。后来把“技能、记忆与上下文文件”当成三件独立的事情分别设计才彻底把这块理顺。这篇就当是系列第六篇的阶段性总结把我在代号为Hermes Agent的项目里围绕这三个模块做过的实验、踩过的坑和最终沉淀下来的方案一次性讲清楚。如果你是刚在搭Agent骨架的人或者已经遇到过“明明代码没问题但表现就是不对”的情况这篇应该能帮上忙。1. 技能、记忆与上下文文件之间的关系先划清边界再谈实现很多人把这仨概念混在一起觉得“让AI记住东西”就行了。实际上一拖三的架构注定后面要返工技能解决的是“Agent能做什么”记忆解决的是“Agent知道什么背景”上下文文件解决的是“当前这次任务的工作台长什么样”。三者职责完全不同却经常被塞进同一套prompt里或者同一个数据库表里。1.1 一个典型的“提示词膨胀”事故我最早的设计很粗暴把历史对话、用户偏好、工具说明、任务进度全部拼进system prompt然后一股脑丢给大模型。前两周看着一切正常等对话轮次超过20轮提示词突破了四千个token问题就陆续浮出来了模型开始忽略靠后面的工具定义明明结算函数就在后面它偏说自己没有这个能力用户一周前说过“我对辣菜过敏”但最新对话里没有提到模型照样推荐了麻辣锅每次增长提示词API延迟和费用都跟着涨调试时看着payload里一大坨文本根本分不清是哪条逻辑混进去的。这就是典型的“不分层就会互相污染”。提示词不是数据库也不是配置文件它是一张“当前应该关注的焦点清单”。凡是超出当前焦点的东西塞得越多模型表现得越笨。1.2 三者的职责分配短暂、持久与可复用我在Hermes Agent里把它们拆成三种完全不同的存储和处理逻辑维度技能记忆上下文文件本质可复用的动作定义跨会话的背景事实当前任务的实时状态生命周期长期固定长期累积随任务创建和销毁存储介质代码函数 注册表向量库 键值库文件系统读入方式命中时注入函数定义按相关性召回按任务阶段加载更新频率极低每次有价值的对话结束每次关键节点完成后简单类比技能是肌肉记忆记忆是大脑里沉淀下来的个人档案上下文文件是办公桌上摊开的图纸和便签。大脑不会把整本档案塞进眼前的工作区只会按需取出几页放到桌上。1.3 为什么分开处理比统一处理更稳分开处理带来的直接好处是每类信息的生命周期和读写频率不同存储方式和注入策略也可以各自优化。技能更新慢可以直接编译成注册表记忆更新频繁但召回应按相关性筛选上下文文件短小精悍只承载当前任务会话结束就归档。这样每一层都只用最合适的工具不再用一套prompt模板硬扛所有需求。2. 技能系统把“会做事”变成“能调用”的肌肉记忆技能模块的目标很简单让Agent在需要时准确调用正确的工具而不是把一堆工具说明堆在提示词里让它碰运气。2.1 技能定义的四件套名称、描述、参数与执行体我给每个技能定义四个核心字段名称简短、动词开头例如query_order_status方便意图匹配和日志检索。描述两到三句话说明这个技能的适用场景包括什么时候不用它用来减少误触发。参数模式表JSON Schema或等效结构标明哪些参数必填、哪些可选、值域范围是什么。执行体实际运行的函数或脚本以及错误返回约定。描述字段是最容易被低估的。很多人只写一句“查询订单状态”模型在识别意图时往往把“查物流”也误判到这个技能上。我在实践里给描述加了反向约束比如“仅当用户询问订单当前处理节点时使用不适用于物流轨迹查询”误触发率直接掉了三成以上。2.2 技能注册表与触发流程注册表我用一个简单的dispatch结构维护不引入复杂框架SKILL_REGISTRY {} def register_skill(name, description, params_schema): def decorator(func): SKILL_REGISTRY[name] { name: name, description: description, params_schema: params_schema, handler: func } return func return decorator运行时意图识别先经过一个闸门把用户请求和技能描述做一轮匹配得分超过阈值才让技能进入候选集然后再做参数校验缺参数的先触发追问而不是直接执行。“先匹配、再校验、最后写日志”这个顺序帮我省掉了大量无效调用排查问题时也能知道模型当时到底为什么选了这个技能。2.3 技能粒度怎么把握动作级与任务级两层拆分一开始我把“帮用户订咖啡”写成一个技能后来发现内部逻辑有三种完全不同的小动作每次小范围改动都会牵连整个技能。后来我拆成两层动作级query_balance、send_message、append_file这一类的单次操作。任务级handle_refund_request这类编排型技能内部串起多个动作级技能。动作级技能保证复用性任务级技能保证可编排性。两层之间通过内部调度函数衔接不把编排逻辑写死在提示词里。这样技能数量超过二十个时意图匹配依然能保持稳定。3. 记忆模块短期滑动窗口与长期向量检索的混搭记忆模块的目标是让Agent在跨会话场景下“记得住事”同时又不会把陈旧信息当成当前事实。3.1 短期记忆用Token预算来定窗口大小短期记忆直接使用对话历史但不全量保留。我按Token预算切窗口假设模型单次请求的上下文预算是8000个Token技能和系统提示约占2000上下文文件约占1500那短期对话窗口就剩4500左右。4500个Token按口语对话大概能装下最近七八轮这就够了。超出预算的部分不是直接删掉而是做滚动摘要把最早的两轮对话用几句话概括然后把摘要作为一条压缩消息留在窗口顶部。这样既保留关键信息又不让窗口无限膨胀。3.2 长期记忆提取、入库、召回三段式长期记忆只在两个时机写入一是任务有明显的结论或偏好出现时二是用户在对话里主动纠正了某个事实时。写入时先做一次“事实抽取”把原句转成结构化条目{ type: user_preference, content: 对辣菜过敏, source_session: session_id_xx, captured_at: 2025-06-12T10:30:00Z, importance: 0.9 }召回时用向量相似度找到最相关的若干条再按“时间衰减系数”加权排序。这不是什么复杂算法核心就是避免一条三个月前的旧结论直接压过三天前的新结论。我调了几轮参数后最终把时间半衰期设成了三十天偏好在六十天内依然有较高权重。3.3 记忆写入的三条底线去重、时效、权限这里我吃过不少亏总结成三条硬性规则去重同一条事实如果在不同会话被反复写入必须合并而不是覆盖成多条否则召回时会互相打架。时效用户明确说“我现在不怕辣了”时旧条目要标记为过期不与新条目同时出现在召回结果里。权限多用户场景必须在所有记忆条目上绑定用户ID查询时强制过滤。漏掉这个会导致A用户的偏好被B用户的会话错误召回排查成本极高。这三条规则看着简单但没有一开始固化的话等记忆表膨胀到几千条再回头清理代价就大了。4. 上下文文件为当前任务准备的“工作台”上下文文件负责承载“这次任务的具体状态”。它可以是一个或一组文件内容随任务阶段变化任务结束后归档或清理。4.1 格式选型Markdown为主、YAML为辅的双轨方案我试过纯JSON、纯Markdown和SQLite最后选择了Markdown加YAML双轨Markdown文件给人看包含任务背景、当前结论、未决事项。Agent在写这类文件时表现更好因为它能自然组织长文档结构。YAML片段给机器看保存结构化状态比如当前阶段标识、已调用过的技能列表、参数快照。有人问为什么不直接用JSON我的回答是让大模型直接生成合法的嵌套JSON失败率比生成Markdown高不少而且人工排查时JSON对人不友好。YAML作为结构化片段放在文件头的fenced block里既好解析又好读。4.2 读写时机三个阶段三个动作我把上下文文件的生命周期切成三段任务开始读取文件把Markdown正文和YAML头部状态一并注入提示词作为“工作台初始状态”。关键节点每次完成一个子任务后主动重写YAML里的阶段标识和结果摘要。注意这里是重写不是追加防止状态信息越积越乱。会话结束把最终结果写回Markdown正文同时把整份文件复制到归档目录附带会话ID和时间戳。这里面最容易忽略的是“关键节点”的判定。我的经验是任何一次技能调用成功返回都算一个关键节点必须更新状态。否则一旦对话中断下次恢复时工作台还停在老地方Agent会重复执行已完成的操作。4.3 防止上下文文件无限膨胀的压缩策略当Markdown正文超过一定阈值我设的是三千字就触发一次摘要压缩把前两个阶段的内容凝练成一小节保留最新的详细内容。这样文件始终保持“能放进上下文预算”的体量同时不丢失任务的完整脉络。压缩后的旧版本会自动归档万一需要回溯可以翻历史文件。5. 一次完整对话的三者协同从请求到落盘的过程拆解把三个模块单独讲清楚之后重点看它们如何在一段真实对话里互相配合。5.1 请求进来之后先召回记忆再匹配技能最后注入上下文文件我的处理顺序是固定的先用当前用户ID去记忆模块召回相关背景得到两三条高相关事实用用户请求去技能注册表做意图匹配得到候选技能列表读取当前任务的上下文文件提取状态和未决事项把这些内容按固定顺序拼接进系统提示词然后交给模型。这个顺序很重要。如果先注入上下文文件再召回记忆可能出现模型已经把任务状态当作全部背景了突然又插入一条长期偏好容易让模型分不清主次。先记忆、再技能、最后状态信息的主次关系最自然。5.2 执行过程中的状态更新与记忆提取模型返回技能调用指令后我先让执行体跑完拿到结果再决定是否更新上下文文件和记忆。只有两种结果值得写入长期记忆一种是用户明确表达的新偏好另一种是任务中确认的关键事实。普通问答、临时计算这类内容直接留在短期对话窗口就好不要什么都往长期记忆里塞。写入上下文文件的则是每次技能调用的结果片段。这里我加了防呆机制如果YAML头部状态和当前实际操作不一致说明中途发生过外部修改这时会暂缓写入并提示我人工确认而不是直接覆盖。5.3 会话结束后的归档与经验沉淀对话收尾时我会做一次完整归档更新Markdown正文里的最终结论和待办事项把上下文文件复制到按任务ID组织的归档目录如果这次任务里出现了一个值得固化的流程会把步骤提炼成一个新的任务级技能注册进技能表。这一步相当于把单次经验沉淀成了长期资产。越往后跑可复用的技能越多每次新任务的启动成本越低Agent行为也更稳定。6. 实测中的翻车现场与调优记录最后记录几个真实踩过的坑都是在单模块测试时没暴露、一联调就暴露的典型问题。6.1 记忆召回串味相似对话把结论带偏一次测试里用户问“上次那个项目预算是多少”模型召回了一条“预算控制在xx万元以内”的旧记忆但其实那条记忆来自另一个项目。原因是我召回时只做了向量相似度匹配没限制来源项目ID。修复方案是把记忆条目的检索键从“用户ID”扩展为“用户ID加项目ID”并给召回接口加了一个强制过滤条件。从那次之后我再没让跨项目的记忆串过味。6.2 技能误触发一个打招呼把结算技能叫出来了用户只是说了一声“你好”模型却调用了结算技能日志显示意图匹配分数刚好超过了阈值。原因是“结算”技能的描述里写了“处理与交易相关的一切问题”而“你好”与交易无关但当时的匹配算法对这个词产生了过高的相似度。我把描述改成“仅当存在具体交易记录且用户主动要求查看或操作时才触发”并在参数校验阶段加了前置条件检查没有订单号参数时禁止执行。之后误触发率恢复了正常。6.3 上下文文件并发写入两个子任务互相覆盖在一次并行执行两个子任务时两个进程同时读写同一个上下文文件后写的一方覆盖了先写方的状态更新。修复方式很简单拆成两个独立的状态文件并在写入时加了文件锁。从那以后我把“一个任务一组文件路径由任务ID隔离”立为硬性约定不再贪图省事把所有状态塞进同一个文件。6.4 关于这套结构的最后一个体会技能、记忆与上下文文件这套三层结构并不新奇但真正把它们拆开设计、按各自的生命周期管理之后Agent的可维护性和稳定性会有本质区别。我现在的习惯是新场景进来先问三个问题——这是不是一个新技能这是一段需要长期保存的事实吗这是本次任务需要逐步更新的状态吗答案分别落在哪个模块就进哪个模块坚决不混。你在自己项目里也可以按这个思路重新审视一遍现有的Agent结构大概率能发现很多“其实不该塞在提示词里”的东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑