8.1 KiB
上下文工作空间
本文介绍 Partner 中 ContextWorkspace 机制的设计思路与工作流程。
Partner 是一个多模块、异步协作的智能体运行时,因此,“如何使得上下文能够在各个模块中有序参与、有序共享信息”,是维持智能体个体感、前后一致性的重要问题。
ContextWorkspace 将不同来源的上下文建模为按域划分、按域查询的 ContextBlock。一个 ContextBlock 持有三种展开级别的 BlockContent,支持时间衰减与替代衰减的活跃分数,并声明它本身对应的“聚焦域”。调用方在需要上下文时,不直接读取所有历史状态,而是向 ContextWorkspace 提交一组目标域,由工作空间负责筛选、激活、排序、聚合与渲染。
核心概念
什么是“域”
- 域是一次上下文读取请求的目标语境,表示“这次调用方是为了哪类工作来索取上下文”。当前实现中域由
ContextBlock.FocusedDomain枚举表示,包括ACTION、MEMORY、PERCEIVE、COGNITION与COMMUNICATION。 resolve(domains)接收的是一个有序域列表,而不是无序集合。列表中的第一个域会被视为主域;越靠前的域权重越高。工作空间会把域顺序转换为线性权重,并把匹配到多个目标域的 block 累积加权。- 域本身不是模块所有权,也不是存储分区。它更像一次读取上下文时的关注面:例如行动模块可以请求
ACTION相关上下文,也可以在同一次请求中附带MEMORY或COGNITION作为辅助关注面。
什么是“聚焦域”
- 聚焦域是
ContextBlock生产方对该上下文块用途的声明,由focusedOn: Set<FocusedDomain>表示。它回答的是:“这个 block 对哪些语境有用?” - 当调用方执行
resolve(domains)时,工作空间会计算block.focusedOn与目标域列表的交集。没有交集的 block 不会进入本次结果;有交集的 block 才会继续参与激活、投影与排序。 - 如果 block 命中了本次请求的第一个域,它会以
PRIMARYexposure 参与渲染;否则即使命中了后续辅助域,也只会以SECONDARYexposure 参与渲染。这个区分决定了 block 最终能展开到多详细。 - 同一个 block 可以聚焦多个域。例如某段 memory 既能支持
MEMORY,也能支持ACTION,它就可以在行动决策和记忆整理两类请求中被激活。
“展开层级”与“衰减分数”如何生效
- 每个
ContextBlock内部有一个activationScore,初始值为100.0。当分数降到0.0时,该 block 会从工作空间中移除。 - 分数会通过两类机制变化:其一是时间衰减,
timeFadeFactor按分钟折算到经过的秒数;其二是替代衰减,当新的 block 与旧 block 具有相同SourceKey(blockName, source)时,旧 block 会扣除replaceFadeFactor。 - 展开层级由当前活跃分数决定:低于
30.0时为ABSTRACT,低于70.0时为COMPACT,否则为FULL。 - block 持有三份内容投影:
blockContent是完整内容,compactBlock是压缩内容,abstractBlock是摘要内容。缺省情况下,compactBlock退化为完整内容,abstractBlock退化为压缩内容。 PRIMARYexposure 至少会渲染到 compact:当 block 处于 full 层级时输出blockContent,处于 compact 或 abstract 层级时输出compactBlock。SECONDARYexposure 不会渲染 full:当 block 处于 abstract 层级时输出abstractBlock,处于 compact 或 full 层级时输出compactBlock。- 每次命中目标域后,block 会根据 exposure 被激活。主域激活更强,辅助域激活更弱;同时实现通过 ceiling 限制层级跃迁,避免一个已经衰减到 abstract 的 block 在一次辅助命中中直接升回 compact 或 full。
SourceKey 与同源快照
ContextBlock 的同源判断只看 blockName 与 source,也就是 SourceKey(blockName, source)。这意味着两个 block 即使内容不同,只要来自同一个逻辑来源,就会被视为同源上下文。
当注册新的同源 block 时,旧 block 不会立即被无条件删除,而是先执行替代衰减:
- 如果旧 block 衰减后分数归零,它会被移除。
- 如果旧 block 仍有活跃分数,它会保留下来,后续在 resolve 时可能与新 block 聚合为同一个结果块。
- 新 block 总是会被加入工作空间,代表该来源的最新状态。
这种设计允许上下文同时表达“最新快照”和“仍有价值的历史快照”,但不会让同源历史无限堆积;旧内容会随着替代衰减和时间衰减自然退出。
解析与排序
resolve(domains) 是 ContextWorkspace 的核心读取流程。它不会简单返回所有 block,而是执行一组选择规则:
- 空域列表直接返回空结果。
- 根据目标域顺序计算域权重,第一个域权重最高。
- 对工作空间内所有 block 先执行时间衰减,分数归零的 block 立即移除。
- 只保留
focusedOn与目标域有交集的 block。 - 根据是否命中主域判定
PRIMARY或SECONDARYexposure。 - 对命中的 block 执行激活,并使用 exposure 选择最终渲染的投影内容。
- 结果先按累积域权重降序,再按活跃分数降序排序;分数相同或权重相同时,再用
blockName与source提供稳定顺序。 - 排序后按
SourceKey分组:单个 block 直接输出,多个同源 block 聚合输出。
聚合输出由 AggregatedBlockContent 完成。它会把同源 block 包装成一个 XML block,其中活跃分数最高的渲染结果标记为 <snapshot>,其他结果标记为 <history_snapshot>。聚合块的 urgency 取自已渲染投影中的最高 urgency,而不是无条件取完整内容的 urgency。
注册、过期与追踪
ContextWorkspace 提供两个状态变更入口:
register(contextBlock):注册新的上下文块,并对同源旧块执行替代衰减。expire(blockName, source):按SourceKey移除所有同源上下文块。
每次注册或过期导致状态变化时,工作空间会写入 TraceRecorder,事件名为 context-workspace。记录内容包括本次 action、受影响的 source key,以及当前工作空间中所有 block 的快照渲染内容。这让上下文系统的状态变化可以被外部观察和调试。
工作流程
flowchart TD
A["模块生成 BlockContent"] --> B["构造 ContextBlock\n声明 focusedOn 与衰减参数"]
B --> C["register(contextBlock)"]
C --> D{"是否存在同 SourceKey 旧块?"}
D -- "是" --> E["旧块 applyReplaceFade"]
E --> F{"旧块分数是否归零?"}
F -- "是" --> G["移除旧块"]
F -- "否" --> H["保留为历史快照候选"]
D -- "否" --> I["加入新块"]
G --> I
H --> I
I --> J["记录 context-workspace/register trace"]
K["调用方提交有序目标域"] --> L["resolve(domains)"]
L --> M["时间衰减并移除失效块"]
M --> N["按 focusedOn 与目标域交集筛选"]
N --> O["判定 PRIMARY 或 SECONDARY exposure"]
O --> P["激活 block 并选择投影内容"]
P --> Q["按域权重、活跃分数、SourceKey 排序"]
Q --> R["同 SourceKey 聚合为 snapshot/history_snapshot"]
R --> S["返回 ResolvedContext"]
设计取向
ContextWorkspace 的重点不是保存尽可能多的历史,而是在多模块智能体运行时中提供一个可衰减、可聚焦、可解释的上下文选择层。模块只需要发布带有语义域声明的 ContextBlock;消费方只需要声明本次要处理的目标域。两者之间的筛选、降噪、投影、历史聚合与追踪,由工作空间统一承担。
这样可以避免两个常见问题:
- 上下文过度展开:所有模块都读取完整历史,导致 prompt 噪音和状态漂移。
- 上下文过早丢失:新状态覆盖旧状态后,仍有短期价值的历史信息无法参与后续推理。
通过活跃分数、聚焦域和 exposure-specific projection,ContextWorkspace 在“保留历史”和“压缩上下文”之间提供了一个动态平衡。