Memory、Skills 与上下文工程
这不是一个"给 Agent 多加几行配置"的简单升级。我们要做的,是让 Agent 从一个"每次对话都失忆的工具人"进化成一个 记住用户偏好、按需调用领域知识、自动压缩长对话 的智能助手。 整个过程基于前两篇文章的项目,增量改动集中在
agents.ts和index.ts,同时新增了AGENTS.md和skills/目录。
目录
- 回顾:前两篇文章结束时 Agent 能做什么?
- Memory:让 Agent 拥有"长期记忆"
- Skills:给 Agent 装上"领域知识库"
- Context Engineering:对话太长怎么办?
- Multimodality:多模态能力边界
- 三个新测试:验证 Memory 和 Skills
- 回顾与展望
1. 回顾:前两篇文章结束时 Agent 能做什么?
| 阶段 | 能力 | 不足 |
|---|---|---|
| 第一篇 | 计算器、报时、流式输出、多轮对话、Extended Thinking | 每次重启就失忆,不知道用户是谁 |
| 第二篇 | 文件读写、搜索、执行 shell 命令 | 只有通用能力,没有领域知识 |
到了第三篇,我们要解决三个核心问题:
- 失忆 — Agent 每次启动都是一张白纸,不知道用户偏好
- 无知 — Agent 只有通用工具,不了解产品细节(FAQ、退款政策等)
- 窗口爆炸 — 对话越来越长,上下文窗口迟早不够用
2. Memory:让 Agent 拥有"长期记忆"
2.1 Memory vs Checkpointer:两种"记忆"的区别
在第二篇文章中,我们已经用过 MemorySaver(checkpointer)。它能在同一个 session 内记住多轮对话。但它有一个致命缺陷——进程重启就丢了。
MemorySaver(checkpointer) Memory(AGENTS.md)
┌────────────────────────┐ ┌────────────────────────┐
│ 存储在进程内存中 │ │ 存储在磁盘文件中 │
│ 进程重启 → 丢失 │ │ 进程重启 → 仍在 │
│ 记住"这轮对话说了什么" │ │ 记住"用户是谁、偏好什么" │
│ 自动累积,不需要配置 │ │ 手动维护,按需更新 │
│ 短期记忆(对话级) │ │ 长期记忆(用户级) │
└────────────────────────┘ └────────────────────────┘
就像人的记忆一样:MemorySaver 是工作记忆(当前正在做的事),Memory 是长期记忆(你是谁、你喜欢什么)。
2.2 创建 AGENTS.md
AGENTS.md 是一个 Markdown 文件,里面写着用户的偏好和项目上下文。Agent 启动时自动加载它,内容被注入系统提示词。
# Agent 持久记忆
## 用户偏好
- 回复语言:中文
- 代码风格:TypeScript,使用 ES Module
- 命名规范:camelCase 变量/函数名,PascalCase 类/接口名
- 注释风格:中文行内注释,解释"为什么"而非"是什么"
## 项目上下文
- 项目类型:LangChain + TypeScript 学习项目
- 包管理器:pnpm
- 运行方式:`pnpm dev`(tsx 直接运行)
## 编码约束
- 不要使用 any 类型,除非处理 LangChain 消息流的 content 字段
- 工具函数必须有 Zod schema 描述
- 错误信息用中文返回
文件路径:workspace/AGENTS.md(在 Backend 的 rootDir 下)。
2.3 代码变化:一行配置
在 agents.ts 中,createDeepAgent 新增 memory 参数:
export const agent = createDeepAgent({
model,
tools: [calculatorTool, getCurrentTimeTool],
backend,
// 持久记忆:启动时加载 AGENTS.md,内容注入系统提示词
memory: ['/AGENTS.md'],
// ...
});
就这么一行。deepagents 内部的 createMemoryMiddleware 会在 Agent 启动时:
- 从 Backend 读取
/AGENTS.md的内容 - 将内容注入系统提示词(system prompt)
- Agent 的每次回复都会"参考"这些偏好
生产建议:
memory支持多个路径,比如['~/.deepagents/AGENTS.md', './.deepagents/AGENTS.md'],按顺序加载,后面的覆盖前面的。适合全局配置 + 项目级配置的组合。
3. Skills:给 Agent 装上"领域知识库"
3.1 为什么需要 Skills?
Agent 有了 Memory 之后知道"用户喜欢中文、TypeScript",但如果用户问:
"我买了一个产品用了 2 次,现在想退款能退多少?"
Agent 完全不知道退款政策是什么。它只有通用工具(计算器、文件操作),没有领域知识。
Skills 就是解决方案——把领域知识写成 Markdown 文件,Agent 按需加载。
3.2 Skills 的文件结构
workspace/
├── AGENTS.md # 持久记忆
└── skills/
├── product-faq/SKILL.md # 产品 FAQ
├── refund-policy/SKILL.md # 退款政策
└── code-review/SKILL.md # 代码审查规范
每个 Skill 是一个目录,里面放一个 SKILL.md 文件。SKILL.md 需要一段 YAML frontmatter 声明名称和描述:
---
name: product-faq
description: 产品常见问题解答知识库,涵盖功能介绍、使用方法、技术架构等高频问题
---
# 产品 FAQ
## Q: 这个项目是做什么的?
这是一个 LangChain + TypeScript 的 AI Agent 学习项目...
## Q: 支持哪些工具?
内置工具包括:calculator、get_current_time、文件系统工具...
frontmatter 中的 name 和 description 是关键——deepagents 用它们注册 Skill 元数据,LLM 根据 description 判断什么时候需要读取这个 Skill 的完整内容。
3.3 Skills 的工作机制
用户问:"退款能退多少?"
↓
LLM 看到系统提示词中列出了 3 个 skill 描述:
- product-faq: 产品常见问题解答
- refund-policy: 退款政策与售后服务规范 ← 匹配!
- code-review: 代码审查规范
↓
LLM 决定读取 refund-policy skill
→ Agent 调用 read_file("/skills/refund-policy/SKILL.md")
→ 拿到完整退款政策内容
↓
LLM 根据退款政策回答用户问题
这就是按需加载——不是把所有 Skill 内容都塞进系统提示词(那会浪费 token),而是只注入描述,LLM 需要时再读取完整内容。
3.4 代码变化:一行配置
export const agent = createDeepAgent({
model,
tools: [calculatorTool, getCurrentTimeTool],
backend,
memory: ['/AGENTS.md'],
// 领域知识:扫描 /skills/ 下所有子目录
// 每个含 SKILL.md 的子目录被注册为一个 skill
skills: ['/skills/'],
// ...
});
skills 参数接受路径数组。deepagents 的 createSkillsMiddleware 会自动:
- 扫描
/skills/下的所有子目录 - 找到每个含
SKILL.md的子目录 - 解析 frontmatter 提取
name和description - 将 skill 元数据注册到 Agent 中
| Skill 名称 | 描述 | 触发场景 |
|---|---|---|
product-faq | 产品常见问题解答 | 用户问"怎么用"、"支持什么" |
refund-policy | 退款政策与售后规范 | 用户问退款、售后、客服 |
code-review | 代码审查规范 | 用户问代码质量、命名规范 |
进阶(S3.2):当 Skill 数量超过 20 个时,"全量加载描述"策略会消耗太多 token。这时候可以用 Embedding + 向量检索做预筛选——把用户问题向量化,只加载语义最相关的几个 Skill。
deepagents目前还没内置这个功能,但架构上支持通过自定义 middleware 扩展。
4. Context Engineering:对话太长怎么办?
4.1 Token 限制与上下文窗口(S3.1)
LLM 的上下文窗口是有限的。我们的模型配置了 maxTokens: 10000,但整个上下文窗口的组成是:
上下文窗口 = 系统提示词 + Memory + Skill 描述 + 工具定义 + 历史消息 + 新消息 + 模型回复
────────────── 共享一个固定大小的 token 预算 ──────────────
对话 10 轮后,历史消息可能已经占掉了 50% 的窗口。对话 50 轮后,新消息可能被截断。
4.2 摘要压缩(Summarization)
deepagents 提供了 createSummarizationMiddleware 来解决这个问题:
import { createSummarizationMiddleware } from 'deepagents';
const summarizationMiddleware = createSummarizationMiddleware({
backend, // 复用已有的 LocalShellBackend
// 当消息数超过 20 条时触发摘要压缩
trigger: { type: 'messages', value: 20 },
// 压缩后保留最近 10 条消息的原文
keep: { type: 'messages', value: 10 },
});
工作原理:
消息 1~50(触发条件:消息数 > 20)
↓
LLM 将消息 1~40 压缩成一段摘要
↓
保留消息 41~50 的原文
↓
上下文变成:[摘要] + [消息 41~50]
trigger 支持三种触发条件:
| 类型 | 含义 | 示例 |
|---|---|---|
messages | 消息条数 | { type: 'messages', value: 20 } |
tokens | token 数量 | { type: 'tokens', value: 50000 } |
fraction | 占上下文窗口比例 | { type: 'fraction', value: 0.7 } |
4.3 Prompt 缓存(S3.3)
createMemoryMiddleware 还支持 cacheControl 选项:
createMemoryMiddleware({
backend,
sources: ['/AGENTS.md'],
cacheControl: true, // 启用 prompt caching
});
开启后,Memory 内容块会被标记为 cache_control: { type: "ephemeral" }。对于支持 Prompt Caching 的模型(如 Claude),重复的 Memory 内容不会重复计费——省钱又省时间。
生产建议:Prompt Caching 在 Anthropic 原生模型上效果最好。通过兼容接口调用的第三方模型(如本项目),缓存效果取决于代理服务器是否支持。
4.4 代码变化:加入 middleware
export const agent = createDeepAgent({
model,
tools: [calculatorTool, getCurrentTimeTool],
backend,
memory: ['/AGENTS.md'],
skills: ['/skills/'],
// 摘要压缩中间件:对话过长时自动压缩旧消息
middleware: [summarizationMiddleware],
// ...
});
middleware 是一个数组,可以传入多个中间件。deepagents 会按顺序应用它们。
5. Multimodality:多模态能力边界
5.1 什么是多模态?
多模态(Multimodality)指模型能处理文本之外的输入:图片、PDF、音频等。
在 LangChain 中,发送多模态消息的方式是把 content 设为数组:
await agent.stream({
messages: [{
role: 'user',
content: [
{ type: 'text', text: '这张图片里有什么?' },
{ type: 'image_url', image_url: { url: 'https://example.com/cat.jpg' } },
],
}],
}, { ...config, streamMode: 'messages' });
5.2 模型能力对照表(S3.4)
| 模型 | 图片理解 | PDF 解析 | 音频转写 | 视频分析 |
|---|---|---|---|---|
| Claude 3.5 Sonnet | ✅ | ✅(PDF 转图片) | ❌ | ❌ |
| GPT-4o | ✅ | ✅ | ✅ | ✅ |
| qwen3.7-plus | ✅ | ⚠️ | ❌ | ❌ |
本项目使用的 qwen3.7-plus 通过 Anthropic 兼容接口调用,多模态支持取决于代理服务器是否透传图片内容。发送图片不会报错,但如果模型不支持,会被忽略或返回"无法处理图片"。
实战建议:如果需要稳定的多模态能力,建议切换到 Claude 3.5 Sonnet 或 GPT-4o。本项目侧重 Agent 架构学习,多模态作为知识拓展点到为止。
6. 三个新测试:验证 Memory 和 Skills
在 index.ts 中,我们注释掉了第二篇的 shell 命令测试(测试 4~6),新增了 3 个测试来验证 Phase 3 的能力。
6.1 测试 7:验证 Memory(持久记忆)
console.log('--- 测试 7:Memory(持久记忆) ---');
console.log('用户:帮我写一个简单的工具函数,把两个数组合并');
const stream7 = await agent.stream(
{ messages: [{ role: 'user', content: '帮我写一个简单的工具函数,把两个数组合并' }] },
{ ...config, streamMode: 'messages' },
);
await printStream(stream7);
虽然这是第一条消息(没有对话历史),但 Agent 会参考 AGENTS.md 中的偏好:
- 用 TypeScript 写
- 用 camelCase 命名
- 中文注释
- 不使用 any 类型
验证方法:对比有/无 memory 配置时的输出差异。没有 Memory 时,Agent 可能用英文注释、用 JavaScript 写;有了 Memory,输出应该符合 AGENTS.md 中的约束。
6.2 测试 8:验证 Skills(产品 FAQ)
console.log('--- 测试 8:Skills(产品 FAQ) ---');
console.log('用户:这个项目支持哪些工具?怎么运行?');
const stream8 = await agent.stream(
{ messages: [{ role: 'user', content: '这个项目支持哪些工具?怎么运行?' }] },
{ ...config, streamMode: 'messages' },
);
await printStream(stream8);
Agent 应该能从 product-faq skill 中找到答案,而不是凭空编造。
6.3 测试 9:验证 Skills(退款政策)
console.log('--- 测试 9:Skills(退款政策) ---');
console.log('用户:我买了一个产品,用了 2 次,现在想退款,能退多少?');
const stream9 = await agent.stream(
{ messages: [{ role: 'user', content: '我买了一个产品,用了 2 次,现在想退款,能退多少?' }] },
{ ...config, streamMode: 'messages' },
);
await printStream(stream9);
这是一个需要精确回答的问题。Agent 应该读取 refund-policy skill,根据退款金额表回答:
7 天内使用 ≤ 3 次 → 退款 80%
如果 Agent 回答"80%"或"可以退 80%",说明 Skills 加载成功了。如果它说"抱歉我不清楚退款政策",说明 Skill 没有被正确读取。
7. 回顾与展望
我们做了什么
在前两篇文章的基础上,Agent 获得了三项新能力:
- Memory(持久记忆) —
AGENTS.md让 Agent 记住用户偏好,不再每次启动都失忆 - Skills(领域知识) —
skills/目录下的SKILL.md文件,Agent 按需加载领域知识回答问题 - Context Engineering(上下文工程) —
createSummarizationMiddleware自动压缩长对话,防止上下文窗口爆炸 - Multimodality(多模态) — 了解模型的能力边界,知道什么能做、什么不能做
完整运行
pnpm dev
输出三个新测试场景:Memory 验证(写工具函数时遵循偏好)、Skills FAQ(项目问题)、Skills 退款(精确政策回答)。
项目结构总览
lingshi/
├── src/
│ ├── tools.ts # 工具定义(calculator、get_current_time)
│ ├── agents.ts # Agent 创建(Memory + Skills + Summarization)
│ └── index.ts # 入口,流式输出 + 测试场景
├── workspace/
│ ├── AGENTS.md # 📝 持久记忆(用户偏好)
│ ├── skills/
│ │ ├── product-faq/SKILL.md # 📚 产品 FAQ
│ │ ├── refund-policy/SKILL.md # 📚 退款政策
│ │ └── code-review/SKILL.md # 📚 代码审查规范
│ ├── public/ # Agent 可读写文件
│ └── private/ # Agent 可读写文件
├── .env
├── package.json
└── tsconfig.json
后续可以做什么
- Subagents(子 Agent):让主 Agent 把复杂任务分发给专门的子 Agent 处理
- Structured Output(结构化输出):让 Agent 返回 JSON schema 校验的结构化数据
- Human-in-the-loop:关键操作前要求用户确认
- 持久化存储:把
MemorySaver换成PostgresSaver,让会话记忆跨重启保留
从"会算数"到"会操作电脑"再到"有记忆有知识能压缩",Agent 从一个玩具变成了一个真正有用的助手。