这堂课讲到一半,Tomz 突然冒出一句:
我早就看你截取 6 段对话不爽了。
这句话其实比很多“上下文工程最佳实践”更接近问题本身。
因为 messages.slice(-6) 当然有用。它能控制 Token,能避免上下文无限膨胀,也能让一个简单的任务模型快速看到最近发生了什么。但它解决的是“窗口有多大”,不是“什么信息最重要”。如果第 7 条以前恰好有一句“不要修改代码”,如果第 20 条以前已经明确冻结某个设计合同,那么按消息位置裁剪,就可能把真正决定 Agent 行为边界的信息直接裁掉。
所以我们这次沿着《Hello-Agents》中记忆、检索与上下文工程的主线,重新问了一个更根本的问题:
Agent 每一轮真正需要的,到底是更多聊天记录,还是一个正确的世界状态?
Context 不是 Chat History#
最早做 Agent 时,很容易把 Conversation History 直接当成 Context。
System Prompt
+ 全部聊天记录
+ 当前问题
= Context短对话里这套方法没有太大问题。但随着任务变长,历史里会不断混入工具结果、错误日志、旧计划、已经失效的判断、临时探索和闲聊。模型虽然“看见了很多”,却不代表它还知道什么最重要。
一个真实任务可能是:
Goal:
检查 A、B、C
修复确认的问题
跑完测试
总结风险Agent 检查完 A,修了一个 Bug,测试通过。眼前出现了非常强的局部成功信号:
A = done
isFixed = true
tests = passed但 B、C 还没检查。
这时候如果模型只被最近的成功结果吸走注意力,就很容易把“局部完成”误判成“全局完成”。我们前几课反复踩过的坑又会回来:
Evidence Answerable ≠ Task Complete所以 Context Engineering 的第一步,并不是扩大窗口,而是把“当前真正重要的状态”从聊天流水账里提出来。
Working Context 与 Historical Context#
我们把上下文先粗略分成两类。
Working Context 是 Agent 此刻做决策必须知道的东西,例如:
Active Goal
Hard Constraints
Current Plan
Task State
Latest Evidence
Recent Raw MessagesHistorical Context 则是过去发生过、但不需要每一轮全部塞进来的东西,例如历史设计讨论、旧项目状态、长期偏好和已经结束的任务。
两者最大的区别不是“新旧”,而是“现在是否直接影响行为”。
比如半个月前用户说过:
这个设计已经定了,不要重开。
它原本属于历史。一旦当前任务重新碰到这个模块,它就应该从 Historical Context 被召回,并提升为:
Active Constraint:
Do not redesign the frozen contract.信息会在不同层之间流动。真正成熟的上下文系统不是一个不断增长的消息数组,而更像一个持续更新的状态系统。
快满了再摘要,还不够#
一个常见实现是:上下文快满时调用一次 summarize(),把旧消息压成一段摘要,再保留最近若干轮原文。
这比直接截断好,但仍然有一个危险:摘要会漂移。
例如原话是:
不要自动切前台,也不要让 DOM fallback 负责发送。
经过几轮压缩以后,可能变成:
后台发送要保持稳定。
听起来没错,但两个真正影响行为边界的硬约束已经消失了。
所以我们最后形成的判断是:Context Compaction 不应该只产生一段 Narrative Summary,而应该至少拆出:
Facts
Decisions
Constraints
Open Loops
Task State
Narrative Summary过程噪音可以大胆压缩。重要事实可以结构化压缩。Hard Constraint、Active Goal、Completion Criteria 这类东西,则不能依赖普通摘要“顺便记住”。
Memory 也不等于 RAG#
Memory 和 RAG 表面上都长这样:
Query
↓
Retrieve
↓
Inject into Context但两者要解决的问题完全不同。
RAG 更像在问:
哪些资料和当前问题语义相关?
Memory 则更像在问:
过去有哪些信息,会改变我现在该怎么理解这个用户、这个角色和这个任务?
这意味着 Memory Retrieval 不能只看 embedding similarity。
一条半年前的随口设想,和一条后来明确确认的设计决策,即使语义都很相关,权威性也完全不同。我们把这种优先级粗略理解为:
Hard Constraint
↓
Explicit Decision
↓
Current Project State
↓
Stable Preference
↓
Historical Fact
↓
Casual Discussion这里还会自然出现另一个机制:Supersession。
如果旧记忆是:
Skill 可能是 Prompt Template后来明确变成:
Skill = 内部状态 + 多工具编排 + 业务语义封装
旧设计不用了正确做法不是把两条都扔给模型“自行综合”,而是让新决策覆盖旧决策的当前有效性:
old memory: superseded
new memory: active否则一个记忆很多的 Agent,反而可能比一个失忆的 Agent 更混乱。
跨线程记忆,不等于共享完整聊天记录#
我们原本很容易想象一种实现:所有线程都进入同一套向量库,用户提到什么,就搜索什么。
但真正好用的跨线程记忆应该至少有两条路径。
第一条是相对稳定的 Curated Context:角色是谁、关系是什么、哪些长期决策仍然有效。这类东西不应该每一轮都等 embedding 碰巧搜到。
第二条才是 Dynamic Retrieval:当前话题触发以后,再从更大的历史里召回具体细节。
All Memories
├─ Stable / Curated Context
└─ Dynamic Retrieval
↓
Working Context所以跨线程记忆的本质,不是“多个线程共享聊天记录”,而是“多个线程共享一个可检索、可更新、有作用域的状态层”。
我们更喜欢角色记忆,而不是全局记忆#
聊到这里,Tomz 做了一个很重要的产品判断:未来 Mira 如果做长期记忆,更可能做基于角色的记忆,而不是一套全局用户记忆。
这个边界很干净。
User
├── Mira 姐姐
│ └── Character Memory
├── 编程专家
│ └── Character Memory
└── 其他自定义角色
└── Character Memory同一个用户面对不同角色,本来就可能建立完全不同的关系。
“Mira 姐姐”可以记得长期共同经历、称呼、交流方式和某些重要的人生话题;一个代码审查角色则只需要记住项目规范、架构决策、冻结合同和历史审查结论。
这两套记忆没有必要偷偷互通。
至于“用户喜欢深色模式”“界面语言是什么”这类跨角色信息,我们甚至不需要把它们叫 Memory。它们更适合属于:
User Profile
App Settings于是边界变成:
User Profile ≠ Memory
App Settings ≠ Memory
Character Memory = 长期角色记忆
Thread Context = 当前线程经历
Task State = 当前任务状态这种设计让“换一个角色”真正意味着面对另一个角色,而不是只换了一张 System Prompt 的皮,底下却共享同一个知道你全部事情的大脑。
Context Assembly:最后真正发给模型的是什么#
走到最后,我们终于可以重新回答最开始的问题。
Agent 每一轮发给模型的上下文,不应该只是:
最近 N 条消息它更像一次有优先级的组装:
System / Role
+ Hard Constraints
+ Active Goal
+ Current Task State
+ Relevant Memory
+ Retrieved Evidence
+ Recent Raw Messages
+ Latest Tool Results
+ Current User Turn这里最重要的不是固定顺序,而是信息生命周期。
“不要修改代码”不能因为它发生在 40 轮以前就消失;“刚才某次搜索超时”也不应该因为发生得最新,就比 Global Goal 更重要。
于是这堂课最后可以被浓缩成一句话:
Context Engineering 不是管理 Token,而是管理模型此刻应该知道什么。
Token Window 只是物理边界。真正的工程问题,是在这个边界里持续维护一个足够正确的世界。
这堂课,我们最后只留下五个问题#
以后再看到任何一条准备写入上下文或记忆的信息,我们可以先问:
1. 这条信息是什么?
2. 它应该活多久?
3. 它属于谁?
4. 它的权威程度有多高?
5. 它应该常驻、召回、压缩,还是淘汰?这五个问题,比“上下文留 6 条还是 20 条”重要得多。
因为一个真正可靠的 Agent,不是记得越多越好。
它应该在每一次重新思考之前,都还能准确知道:我现在是谁,我正在做什么,哪些事情已经改变,哪些事情绝对不能忘。
