Context Engineering & Memory
Context 不是资料仓库,而是模型本轮决策的工作台。Memory 也不是把聊天永久保存,而是从历史中筛选可复用事实,再在合适的任务里安全取回。系统稳定性取决于“哪些信息进入本轮推理”,不是数据库里存了多少文本。
先分清四个概念
| 概念 | 生命周期 | 谁能看到 | 典型内容 |
|---|---|---|---|
| Local run state | 单次运行 | 应用代码与工具 | 数据库连接、用户 ID、重试次数 |
| LLM-visible context | 单次模型调用 | 模型 | 指令、消息、工具定义、检索片段 |
| Working memory | 当前任务 | Agent 与模型 | 当前计划、已完成步骤、待决策问题 |
| Long-term memory | 跨会话 | 持久化系统,按需检索 | 已确认偏好、业务事实、历史决策 |
OpenAI Agents SDK 也明确区分本地 context 与模型可见 context:依赖对象可以留在代码层,不需要为了让工具访问数据库而把连接信息发送给模型。
业务数据与依赖 ──→ Local context ──→ Tool
│
└─ 只把任务所需结果送入 LLM context
历史事件 ──→ Memory write gate ──→ Memory store
│
当前任务 ──→ Retrieval + policy ──────┘ → Working set → LLM
Context 的目标不是“塞满”
Anthropic 将 Context Engineering 定义为:从所有可能信息中,持续选择最有助于当前行为的 token。最实用的原则是 最小充分集。
一轮调用可以按以下层次组织:
- 稳定指令:角色、安全边界、输出契约。
- 当前目标:用户要达成的结果和验收条件。
- 工作状态:已完成步骤、未决问题和剩余预算。
- 按需知识:带来源的检索结果或 Memory。
- 工具定义:仅加载本轮可用的工具。
- 最近观察:对下一步仍有价值的结果。
不要写死“Context 必须用到 70%”。不同模型、任务和输出长度不同。应先为输出和工具结果预留空间,再通过 eval 找到自己的阈值。
给 Context 做预算
type ContextBudget = {
modelLimit: number;
reservedOutput: number;
stableInstructions: number;
taskState: number;
retrievedKnowledge: number;
toolDefinitions: number;
recentObservations: number;
};
function assertBudget(b: ContextBudget) {
const inputBudget = b.modelLimit - b.reservedOutput;
const planned =
b.stableInstructions +
b.taskState +
b.retrievedKnowledge +
b.toolDefinitions +
b.recentObservations;
if (planned > inputBudget) throw new Error('CONTEXT_BUDGET_EXCEEDED');
}
预算超限时的优先顺序通常是:删除重复内容 → 截断低价值工具结果 → 归纳旧观察 → 减少检索片段 → 延迟加载工具。稳定安全指令不能成为第一个被压缩的对象。
Memory 应该记什么
| 类型 | 示例 | 是否适合长期保存 |
|---|---|---|
| Semantic facts | 用户所在时区、团队项目名称 | 已确认且有业务价值时保存 |
| Episodic events | 某次部署失败、某次订单处理 | 有追溯或恢复价值时保存 |
| Preferences | 喜欢简洁回答、默认导出格式 | 用户明确表达后保存 |
| Procedures | 团队发布检查清单 | 作为版本化知识,不写成个人偏好 |
| Temporary state | 当前表单输入、一次性验证码 | 不进入长期 Memory |
| Sensitive data | 密码、完整支付信息、私密健康资料 | 默认禁止或走专门合规系统 |
“用户说过”不等于“应该永久记住”。写入前至少判断:是否明确、是否仍可能变化、是否对未来有用、是否允许保存、保存多久。
Memory 写入闸门
type MemoryCandidate = {
tenantId: string;
userId: string;
kind: 'fact' | 'preference' | 'event' | 'procedure';
value: string;
sourceRef: string;
observedAt: string;
confidence: number;
expiresAt?: string;
sensitivity: 'public' | 'internal' | 'personal' | 'restricted';
};
function mayPersist(candidate: MemoryCandidate) {
return (
candidate.confidence >= 0.9 &&
candidate.sensitivity !== 'restricted' &&
Boolean(candidate.tenantId && candidate.userId && candidate.sourceRef)
);
}
真实系统还应加入用户同意、地区保留政策、数据删除请求和字段级加密。模型可以提出 Memory candidate,但代码与政策层决定是否写入。
检索不是只做向量相似度
一次安全检索至少包含:
当前任务
→ tenant/user/region 硬过滤
→ 按 kind、时间与权限过滤
→ 语义或关键词召回
→ 去重与冲突检测
→ relevance + recency + confidence 重排
→ token budget 截断
→ 带 sourceRef 注入工作集
const memories = await memoryStore.search({
tenantId: run.tenantId,
userId: run.userId,
query: run.objective,
kinds: ['fact', 'preference'],
notExpiredAt: new Date().toISOString(),
limit: 12
});
const workingSet = rerank(memories)
.filter(item => item.score >= retrievalThreshold)
.slice(0, maxMemoryItems)
.map(({ value, sourceRef, observedAt }) => ({ value, sourceRef, observedAt }));
Tenant 与用户过滤必须在数据库查询阶段完成,不能先跨租户召回,再让模型判断哪些内容可见。
冲突、过期与更正
Memory 不是不可变真理。假设用户先说“我住在 Brisbane”,后来明确说“我已经搬到 Sydney”,系统应保留来源和时间,并把旧事实标为 superseded。
{
"memoryId": "mem_204",
"key": "home_city",
"value": "Sydney",
"observedAt": "2026-08-25T11:10:00+10:00",
"sourceRef": "message_991",
"status": "active",
"supersedes": "mem_102"
}
冲突无法自动解决时,向用户确认,不要把两个值平均或随机选择。涉及价格、法律状态、职位、考试规则等易变事实,应优先读取权威来源,而不是依赖旧 Memory。
压缩历史时保留什么
好的 compaction 不是“把聊天缩短”,而是生成可恢复状态:
## Objective
Deploy checkout-api after regression tests pass.
## Confirmed facts
- Current branch: release/checkout-42 [source: git]
- Payment E2E is failing at 3DS callback [source: run_87]
## Decisions
- Do not bypass the 3DS test.
- Use provider sandbox account only.
## Completed
- Unit tests passed at commit 1a2b3c4.
## Open work
- Fix callback signature verification.
## External actions
- No deployment has occurred.
删除闲聊、重复日志和已经失去决策价值的中间输出;保留目标、硬约束、证据、外部副作用和下一步。
防 Prompt Injection 持久化
网页、邮件和工具结果都是不可信数据。攻击文本如果被摘要进长期 Memory,会在未来任务里重复生效。
| 风险 | 防线 |
|---|---|
| 文档要求忽略系统指令 | 把检索内容标为数据,不赋予指令权限 |
| 恶意文本被写入 Memory | Memory candidate 只提取事实,不复制命令 |
| 跨租户泄漏 | 查询阶段强制 tenant filter |
| 敏感字段进入 trace | 写入前字段级 redact |
| 旧事实覆盖新事实 | 时间、来源、状态和 supersedes 链 |
不要让模型自行决定“这段文本安全,所以永久保存”。安全分类与保留必须有确定性代码闸门。
如何评测 Context 与 Memory
| Eval | 检查方法 |
|---|---|
| Retrieval precision | 注入的 Memory 中有多少真正帮助当前任务 |
| Retrieval recall | 必要事实是否被取回 |
| Context efficiency | 每个成功任务消耗多少输入 token |
| Faithfulness | 回答是否忠于 sourceRef,而非补写细节 |
| Update correctness | 新事实是否正确替代旧事实 |
| Isolation | 其他 tenant/user 的内容是否始终为零 |
| Deletion | 用户删除后是否无法再次检索 |
| Topic switch | 换任务后旧工作集是否被清理 |
测试集要覆盖同名用户、冲突偏好、过期事实、恶意文档、空检索和超长工具输出,而不只是“记住我喜欢咖啡”的演示。
常见错误
| 错误 | 结果 | 修法 |
|---|---|---|
| 保存整段对话 | 成本高,敏感信息和注入一起持久化 | 只保存结构化 candidate |
| 只用向量 top-k | 召回相似但无权限或已过期的内容 | 先硬过滤,再召回和重排 |
| 没有来源和时间 | 无法判断冲突与新旧 | 强制 sourceRef、observedAt |
| 把 Local context 全送给模型 | 密钥和内部依赖暴露 | 代码层与 LLM-visible context 分离 |
| 压缩时只留结论 | 新 session 无法验证 | 保留证据 ID 和外部状态 |
动手练习:跨会话项目助手
- 定义事实、偏好、事件三种 Memory schema。
- 设计写入闸门,拒绝密码、一次性验证码和未确认推断。
- 加入 tenant、user、过期时间和 sourceRef 过滤。
- 模拟用户城市变化,验证新事实 supersede 旧事实。
- 在检索文档中加入恶意指令,确认它不会进入长期 Memory。
- 记录答案质量、输入 token、召回精度和跨用户泄漏率。
完成标准
- 我能区分 Local state、LLM context、Working memory 和 Long-term memory。
- Memory 写入受代码与政策控制,不由模型单独决定。
- 检索先做 tenant/user 硬过滤。
- 每条长期事实都有来源、时间、状态和删除路径。
- Compaction 保留目标、决策、证据和外部副作用。
相关阅读
官方参考
📚 相关资源
❓ 常见问题
点击问题,查看本章对应的实践答案。
history 越塞越多 token 爆炸,怎么管理?
三种策略组合用:(1) sliding window 只留最近 N 轮;(2) summarization 把更早的压成 bullets,保留 IDs/time;(3) topical caches 按话题存摘要,话题切换时换入换出。话题切换还要触发 reset:重发核心指令、丢弃陈旧历史。目标 context 占 ≤ 60-70% 模型上限,给输出留 1/3。
system / user / history / tools 该怎么排顺序?
Instruction hierarchy:(1) System 装不可妥协的规则(角色、语言、安全),最高优先级;(2) Task/User 是当轮请求和约束;(3) History 只放必需轮次,更早内容先 summarize;(4) Tools 装 function spec 和预期。RAG 里则是 instructions → constraints → retrieved snippets(带 IDs)→ question。关键信息放头或尾,避开中间。
long-term memory(用户偏好、历史事实)怎么存?
短期 memory 是当前对话 + working set;长期 memory 用 vector store 或 key-value store 存 facts/preferences,按 query + tenant/user 检索。Ephemeral memory 自动过期/轮换避免 PII 堆积。事实存成 bullet list 或 key-value 块,不写散文;每条带 ID 便于引用追溯;数字/日期用统一单位和格式。
memory 系统怎么防 prompt injection?
三道闸:(1) 摘要时把用户提供的 prompt 片段剥离,避免注入持久化进 memory;(2) 写/读前对 secrets 和 PII 脱敏;(3) 数据按 tenant/user/region 打 tag,retrieval 时强过滤。production 还要做 token audit(典型 + 峰值场景下 context 大小)和 regression check(核心 instructions 在 packing 后还在)。
怎么验证 context packing 没把核心指令挤掉?
Minimal checklist:(1) instruction hierarchy 强制执行,核心规则每次都进;(2) history 已 trim/summarize 带 IDs,预算 ≤ 70% 模型上限;(3) retrieval snippets 已去重、带引用、按 tenant 过滤。再做 topic-switch 测试:故意切话题,验证 summary 和 reset 行为。每次 release 跑一遍 regression 不漏。