从 Skill Loading 到 Context Compact 再到 Memory:Agent Harness 的持续运行能力构造
从 Skill Loading 到 Context Compact 再到 Memory:Agent Harness 的持续运行能力构造
前面几个阶段里,Agent 已经具备了基础工具调用、Hook 扩展、Todo 规划、Subagent 拆任务等能力。
但做到这里还不够。
一个真正能长期工作的 Agent,还会遇到三个更现实的问题:
- 项目知识太多,不能全部塞进 system prompt;
- 上下文会越来越长,最终超过模型窗口;
- 压缩会丢失细节,新会话也无法记住用户偏好。
所以这三个章节继续增强 harness:
- s07:Skill Loading,按需加载知识;
- s08:Context Compact,压缩上下文;
- s09:Memory,长期保存重要信息。
一、s07 Skill Loading:知识不要全塞进 Prompt,要按需加载
1. 问题:system prompt 不是垃圾桶
假设项目里有很多规范文档:
- React 组件规范;
- SQL 编写规范;
- API 设计规范;
- 代码审查规范。
最直接的做法是把它们全部塞进 system prompt:
1 | SYSTEM = ( |
这样看起来简单,但问题很大。
因为 system prompt 每一轮都会被带上。哪怕 Agent 只是改一个变量名,也要背着几千行规范一起请求模型。
这会导致:
- token 浪费;
- prompt 变重;
- 无关知识干扰当前任务;
- 后续上下文更容易爆掉。
所以 s07 的核心思想是:
不要把所有知识都塞进 system prompt,而是让 Agent 知道“有哪些技能”,需要时再加载完整内容。
2. 两层 Skill 设计
s07 使用两层结构:
| 层级 | 内容 | 放在哪里 | 作用 |
|---|---|---|---|
| 第一层 | Skill 目录摘要 | system prompt | 告诉 Agent 有哪些 skill |
| 第二层 | 完整 SKILL.md 内容 | tool_result | 需要时再加载 |
也就是说,system prompt 里只放目录:
1 | Skills available: |
真正的详细内容,只有当 Agent 调用 load_skill 时才进入上下文。
3. skills/ 目录结构
每个 skill 是一个目录,里面有一个 SKILL.md:
1 | skills/ |
SKILL.md 通常会包含:
- name;
- description;
- 什么时候使用;
- 具体操作步骤;
- 相关资源路径;
- 注意事项。
4. 启动时扫描 Skill
核心代码:
1 | SKILL_REGISTRY: dict[str, dict] = {} |
这段代码做了几件事:
- 遍历
skills/目录; - 找每个子目录里的
SKILL.md; - 读取 frontmatter;
- 提取 name 和 description;
- 保存到
SKILL_REGISTRY。
SKILL_REGISTRY 就是一个技能注册表。
5. 构造 system prompt 时只注入目录
1 | def list_skills() -> str: |
这里的重点是:
system prompt 只放 skill 目录,不放完整 skill 内容。
这样 Agent 每轮都知道自己有哪些能力,但不会每轮都背完整文档。
6. load_skill 工具
当 Agent 判断当前任务需要某个技能时,会调用:
1 | load_skill("code-review") |
对应函数:
1 | def load_skill(name: str) -> str: |
注意这里不是直接按文件路径读取,而是通过 SKILL_REGISTRY 查找。
这样可以避免路径穿越问题,比如模型传入:
1 | ../../secret.txt |
因为它只能加载注册表里存在的 skill。
7. 加入工具分发系统
和前面的工具一样,load_skill 也进入工具表:
1 | TOOLS.append({ |
所以整个 agent_loop 不用改。
它仍然是:
1 | handler = TOOL_HANDLERS.get(block.name) |
这就是 harness 设计得好的地方:
新能力不是重写循环,而是新增工具。
8. s07 的核心价值
s07 解决的是知识加载问题。
以前是:
1 | 所有知识全部塞进 system prompt |
现在是:
1 | system prompt 放目录 |
它的核心价值是:
让 Agent 知道“我有什么知识”,但只在需要时付出 token 成本。
二、s08 Context Compact:上下文会爆,必须会压缩
1. 问题:messages 会无限增长
Agent 每一轮都会把内容加入 messages:
- 用户输入;
- assistant 回复;
- tool_use;
- tool_result;
- 文件内容;
- 命令输出;
- 错误日志。
如果 Agent 读了 30 个文件,运行了 20 条命令,这些结果都会堆在上下文里。
最后一定会遇到:
1 | prompt_too_long |
也就是请求超过模型上下文限制。
所以 s08 的核心思想是:
上下文一定会满,harness 必须有清理机制。
2. 总体策略:便宜的先做,贵的后做
s08 使用四层压缩:
tool_result_budget:大工具结果落盘;snip_compact:裁掉中间旧消息;micro_compact:旧 tool_result 替换为占位符;compact_history:调用 LLM 总结整个历史。
前面三层不调用模型,成本低。
最后一层需要调用 LLM,总结能力强,但成本更高。
所以原则是:
cheap first, expensive last。
3. L1:snip_compact,裁掉中间对话
代码:
1 | def snip_compact(messages, max_messages=50): |
作用是:
- 保留开头几条消息;
- 保留最近的消息;
- 删除中间旧消息;
- 用一条 placeholder 表示这里删过内容。
为什么保留头部?
因为开头可能包含初始任务、重要约束。
为什么保留尾部?
因为最近消息通常和当前任务最相关。
4. L2:micro_compact,压缩旧工具结果
代码:
1 | KEEP_RECENT_TOOL_RESULTS = 3 |
这个函数只保留最近 3 个工具结果的完整内容。
更旧的工具结果,如果内容比较长,就替换成:
1 | [Earlier tool result compacted. Re-run if needed.] |
这样做的原因是:
- 旧文件内容通常不再重要;
- 旧命令输出可能已经过期;
- 如果真的需要,可以重新执行工具。
5. L3:tool_result_budget,大结果写入磁盘
有时一个工具结果本身就很大,比如一次读取 500KB 文件。
这时只靠 micro_compact 不够,因为当前最后一条 user message 里可能已经塞爆了。
代码:
1 | def tool_result_budget(messages, max_bytes=200_000): |
它的逻辑是:
- 只检查最后一条消息里的 tool_result;
- 统计所有 tool_result 总大小;
- 如果超过 200KB,就从最大的结果开始处理;
- 把完整内容写入
.task_outputs/tool-results/; - 上下文里只保留 marker 和预览。
这样模型仍然知道:
这个结果存在,但完整内容已经被存到磁盘,需要时可以重新读。
6. 为什么执行顺序不能乱?
s08 强调执行顺序是:
1 | tool_result_budget |
尤其是 tool_result_budget 必须在 micro_compact 前面。
原因是:
tool_result_budget要保存完整大结果;micro_compact会把旧结果替换成占位符;- 如果先 micro,再 budget,完整内容可能已经没了,无法落盘。
所以正确顺序是:
1 | 先保存大结果 |
7. L4:compact_history,用 LLM 总结历史
当前三层都处理完了,但上下文还是太大,就要调用 LLM 总结。
代码:
1 | def compact_history(messages): |
它分三步:
- 先把完整对话写入
.transcripts/; - 调用 LLM 生成总结;
- 用一条 summary message 替换原来的 messages。
总结里要保留:
- 当前目标;
- 重要发现;
- 已修改文件;
- 剩余任务;
- 用户约束;
- 当前计划。
这一步会丢掉细节,但能保证 Agent 继续跑。
8. reactive_compact:出错后的紧急压缩
有时压缩触发不及时,API 直接报:
1 | prompt_too_long |
这时进入 reactive compact:
1 | def reactive_compact(messages): |
它比普通 compact 更激进:
- 保存 transcript;
- 生成 summary;
- 只保留最后 5 条消息;
- 然后重试。
同时它有重试上限,防止无限循环。
9. 整合进 agent_loop
核心流程:
1 | def agent_loop(messages): |
这说明 s08 的压缩逻辑发生在每次 LLM 调用之前。
也就是说:
每次请求模型前,harness 都先检查上下文是否需要清理。
10. compact 工具
s08 还增加了一个 compact 工具。
当模型主动调用它时,也会触发:
1 | messages[:] = compact_history(messages) |
然后返回:
1 | [Compacted. History summarized.] |
这表示压缩既可以由 harness 自动触发,也可以由模型主动触发。
11. s08 的核心价值
s08 解决的是上下文爆炸问题。
以前是:
1 | messages 无限增长,直到 prompt_too_long |
现在是:
1 | 每轮调用前先压缩,必要时总结,失败后紧急修复 |
它让 Agent 从“短会话玩具”变成“可以持续工作的系统”。
三、s09 Memory:压缩会丢细节,所以要长期记忆
1. 问题:压缩和新会话都会丢信息
s08 的 compact 可以解决上下文太长的问题。
但压缩有一个副作用:
总结是有损的。
比如用户说:
1 | 我以后写代码都用 tab,不用空格。 |
压缩后可能变成:
1 | 用户有代码风格偏好。 |
这就丢失了关键细节。
另外,新开一个会话后,之前 messages 也没了。
所以 s09 增加 Memory 层。
它解决的是:
哪些重要信息应该跨压缩、跨会话保存?
2. Memory 存在哪里?
s09 使用文件系统保存记忆:
1 | .memory/ |
每一条 memory 是一个 Markdown 文件。
格式:
1 | --- |
也就是:
- frontmatter 存元数据;
- 正文存详细内容。
3. 四种 Memory 类型
s09 把 memory 分成四类:
| 类型 | 作用 | 例子 |
|---|---|---|
| user | 用户偏好 | 用户喜欢 tab 缩进 |
| feedback | 工作方式反馈 | 不要 mock 数据库 |
| project | 项目背景 | 当前在重写认证模块 |
| reference | 索引线索 | 某类 bug 在某个系统里 |
这四种类型分别回答不同问题:
- user:用户是谁,偏好什么;
- feedback:应该怎么配合用户;
- project:当前项目发生了什么;
- reference:以后去哪找相关信息。
4. 写入 Memory 文件
核心函数:
1 | def write_memory_file(name, mem_type, description, body): |
它做了几件事:
- 把 memory 名字转成文件名;
- 写入 YAML frontmatter;
- 写入正文;
- 重建索引。
5. MEMORY.md:记忆索引
MEMORY.md 是所有 memory 的目录。
示例:
1 | - [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation |
它的作用类似 s07 的 skill catalog:
system prompt 里不放所有 memory 内容,只放 memory 索引。
这样模型知道有哪些记忆,但不会每次都加载所有详情。
6. build_system 注入 Memory 索引
每次构造 system prompt 时,读取 MEMORY.md:
1 | def build_system(): |
这样 Agent 每轮都能看到记忆目录。
但完整记忆内容不会全部进入上下文。
7. 按需加载相关 Memory
s09 有一个选择相关记忆的过程。
核心函数:
1 | def select_relevant_memories(messages, max_items=5): |
大概流程是:
- 列出所有 memory 的 name 和 description;
- 把最近对话和 memory catalog 发给 LLM;
- 让 LLM 返回相关 memory 的索引;
- 最多加载 5 条;
- 读取这些 memory 的完整内容注入上下文。
如果 LLM 选择失败,就退回关键词匹配。
8. 用户显式要求记住
第一种写入 memory 的方式很直接:
1 | 我以后都用 tab,记住这个。 |
这时 harness 可以把它写入 .memory/。
这种是用户主动记忆。
9. 每轮结束后自动提取 Memory
但用户不一定每次都说“记住”。
比如他说:
1 | 以后字符串都用单引号。 |
这其实也是长期偏好。
所以 s09 在每轮自然结束后自动提取 memory:
1 | if response.stop_reason != "tool_use": |
为什么是 stop_reason != "tool_use"?
因为这说明模型这一轮已经不需要继续调用工具,当前对话阶段自然结束。
这时适合做总结提取。
10. extract_memories:从最近对话提取记忆
核心代码:
1 | def extract_memories(messages): |
它会把最近 10 条对话发给模型,让模型判断:
- 有没有新的用户偏好;
- 有没有新的项目事实;
- 有没有新的约束条件;
- 是否已经存在类似 memory。
如果没有新内容,就返回:
1 | [] |
如果有,就写入 .memory/。
11. consolidate_memories:记忆合并
memory 文件越来越多后,会出现:
- 重复记忆;
- 过期记忆;
- 冲突记忆;
- 描述太碎。
所以 s09 有定期整理机制:
1 | CONSOLIDATE_THRESHOLD = 10 |
教学版中,只要 memory 文件数量达到阈值,就触发合并。
核心作用是:
memory 不能只增不管,否则迟早变成垃圾堆。
四、三章合起来的 Harness 构造顺序
把 s07、s08、s09 串起来,完整构造过程是这样的。
第一步:保留前面已有基础
此时 harness 已经有:
1 | agent_loop |
也就是说,Agent 已经能:
- 调模型;
- 调工具;
- 走 hook;
- 写 todo;
- 拆 subagent。
第二步:扫描 skills 目录
启动时执行:
1 | _scan_skills() |
得到:
1 | SKILL_REGISTRY = { |
第三步:构造 system prompt 时注入 skill catalog
1 | SYSTEM = build_system() |
system prompt 里只放:
1 | Skills available: |
不放完整文档。
第四步:注册 load_skill 工具
1 | TOOL_HANDLERS["load_skill"] = load_skill |
从此 Agent 可以按需加载技能详情。
第五步:每次 LLM 调用前执行压缩流水线
在 agent_loop 里,调用模型前执行:
1 | messages[:] = tool_result_budget(messages) |
如果还超:
1 | messages[:] = compact_history(messages) |
如果 API 报 prompt_too_long:
1 | messages[:] = reactive_compact(messages) |
第六步:增加 compact 工具
让模型也可以主动调用:
1 | TOOL_HANDLERS["compact"] = run_compact |
当模型意识到上下文太重时,可以主动请求压缩。
第七步:准备 memory 文件系统
创建:
1 | .memory/ |
每条长期记忆写成:
1 | .memory/xxx.md |
第八步:构造 system prompt 时注入 memory index
类似 skill catalog,memory 也只注入索引:
1 | Memory index: |
完整 memory 内容按需加载。
第九步:每轮调用前选择相关 memory
根据最近对话,从 memory catalog 里选出相关记忆:
1 | selected = select_relevant_memories(messages) |
然后读取完整内容注入当前上下文。
第十步:每轮结束后提取新 memory
当模型不再调用工具时:
1 | if response.stop_reason != "tool_use": |
这一步让 harness 能从普通对话里自动沉淀长期信息。
五、最终运行流程
最终,这个 harness 的一轮完整流程可以理解成:
1 | 用户输入 |
六、总结
这三个章节继续补齐了 Agent harness 的长期运行能力。
s07 解决知识加载问题:
技能目录常驻 system prompt,完整技能按需加载。
s08 解决上下文爆炸问题:
先用低成本结构压缩,再用 LLM 总结,最后用 reactive compact 兜底。
s09 解决长期记忆问题:
把用户偏好、项目事实、反馈和参考信息保存到文件系统,跨压缩、跨会话继续使用。
如果说前面的 harness 让 Agent “能执行任务”,那么这三章让 Agent 更像一个能长期协作的开发助手:
- Skill Loading 让它知道什么时候学;
- Context Compact 让它不会被上下文撑爆;
- Memory 让它不会每次都从零开始。
最终形成的不是一个简单聊天机器人,而是一个围绕 LLM 构造出来的运行系统。
一句话概括:
真正可靠的 Agent,不只是模型会回答,而是 harness 能管理知识、压缩上下文、保存记忆,并让模型在长期任务中持续保持工作能力。
