context_typing_and_levels 模块技术深度解析
概述
context_typing_and_levels 模块是 OpenViking 系统的类型定义层,它定义了系统如何对"上下文"进行分类、分级和表示。简单来说,这个模块回答了三个核心问题:这个上下文是什么类型的?它包含什么内容?它在向量检索体系中处于哪个层级?
在 OpenViking 这样的 AI Agent 系统中,上下文(Context)是核心抽象——它可以是一段记忆、一个技能定义、一个文档资源,甚至是一个会话。系统需要一种统一的方式来描述这些不同来源、不同用途的上下文,以便进行有效的存储、检索和利用。这个模块正是这个统一类型系统的基石。
问题空间与设计意图
为什么要专门的类型系统?
在一个复杂的 AI Agent 系统中,你会发现需要表示的"上下文"种类繁多:
- Skill(技能):Agent 可以调用的能力,如代码审查、文档生成
- Memory(记忆):用户的偏好、历史交互、项目信息
- Resource(资源):外部知识库、文档、代码文件
这些不同类型的上下文在语义上完全不同,但在数据结构上却高度相似——都有 URI、都有摘要、都需要向量化。如果为每种类型单独设计数据结构,会导致大量重复代码,且类型之间的转换会变得复杂。
这个模块的设计意图就是:用同一个 Context 类统一表示所有类型的上下文,通过类型标签(context_type)和层级标签(context_level)来区分它们的语义。这种设计类似于面向对象中的"同一类可以表示不同子类型"的思想。
为什么要分级?(L0/L1/L2)
向量检索的一个核心挑战是:检索的粒度问题。当你搜索"用户偏好的代码风格"时,你可能希望找到最具体的偏好记录;但当你只是需要一个会话的整体概述时,你可能只需要顶层目录。
ContextLevel 枚举定义了三个层级:
- L0 (ABSTRACT):抽象层,代表目录或主题的概览,用于快速筛选
- L1 (OVERVIEW):概要层,提供中等粒度的描述
- L2 (DETAIL):详细层,包含实际内容,用于精确匹配
这种分级思想借鉴了信息检索中的分层索引理念——类似于搜索引擎先返回摘要再加载详细内容。设计者选择让 Context 同时携带三个层级的信息(L0 来自 abstract 字段,L1 来自 overview,L2 来自完整内容),而不是创建三个独立的 Context 对象,这简化了存储管理,但也意味着检索时需要在层级之间做权衡。
核心抽象与类型设计
类型枚举
ResourceContentType
class ResourceContentType(str, Enum):
TEXT = "text"
IMAGE = "image"
VIDEO = "video"
AUDIO = "audio"
BINARY = "binary"
这个枚举定义了资源的内容媒体类型。它采用了"字符串枚举"的模式——继承 str 使得这个枚举值可以直接用于字符串比较和序列化,无需手动转换。这是 Python 3.11+ 推荐的枚举写法。
设计考量:这个类型目前主要服务于未来的多模态检索。当前版本中 Vectorize 类只支持文本(注释中保留了 image、video、audio 字段但被注释掉了),说明系统设计时已经预见到多模态需求,但采用了渐进式实现策略——先支持文本,后续再逐步扩展。
ContextType
class ContextType(str, Enum):
SKILL = "skill"
MEMORY = "memory"
RESOURCE = "resource"
这是上下文的核心类型标签。它只有三个取值,简洁地覆盖了系统的三大类上下文。设计者选择不做过度细分——例如不区分 "preference memory" 和 "entity memory"——而是通过 category 字段在需要时提供更细粒度的分类。
ContextLevel
class ContextLevel(int, Enum):
ABSTRACT = 0 # L0: abstract
OVERVIEW = 1 # L1: overview
DETAIL = 2 # L2: detail/content
这个枚举继承自 int 而非 str,有一个微妙但重要的设计考量:当需要比较层级时,可以直接用整数比较(L2 > L1 > L0)。这在检索结果排序、层级过滤等场景中非常方便。
Vectorize 类
class Vectorize:
text: str = ""
def __init__(self, text: str = ""):
self.text = text
这是一个简单的数据容器,用于封装向量化所需的文本。设计成单独类而非直接使用字符串,有两个原因:
- 扩展性:为未来多模态向量留出扩展空间(注释掉的 image/video/audio 字段)
- 语义清晰:
context.vectorize.text比context.vectorization_source更清晰地表达意图
Context 类
这是模块的核心类——一个统一的上下文表示。它的设计有几个关键特点:
URI 作为主键
每个 Context 都有一个 URI(如 viking://user/memories/preferences、viking://agent/skills/code-review)。URI 是系统中的全局唯一标识符,类似于文件系统中的路径。这种设计让上下文可以被寻址,就像文件可以被路径引用一样。
自动类型推导
Context 类的构造函数有一个巧妙的设计:如果未显式指定 context_type,它会根据 URI 自动推导:
def _derive_context_type(self) -> str:
if "/skills" in self.uri:
return "skill"
elif "/memories" in self.uri:
return "memory"
else:
return "resource"
这是一个约定优于配置的设计——系统约定 URI 包含特定路径片段时就代表特定类型。这样即使不显式指定类型,只要 URI 符合规范,Context 就能正确识别自己的类型。
同样的逻辑也适用于 category 字段——通过 URI 中的路径模式推断更细粒度的分类(patterns、cases、profile、preferences 等)。
owner_space 的推导
def _derive_owner_space(self, user: Optional[UserIdentifier]) -> str:
if not user:
return ""
if self.uri.startswith("viking://agent/"):
return user.agent_space_name()
if self.uri.startswith("viking://user/") or self.uri.startswith("viking://session/"):
return user.user_space_name()
return ""
这个方法体现了系统的多租户设计——Agent 上下文属于 agent_space,用户上下文属于 user_space。这种空间隔离确保了不同用户/Agent 的数据不会串门。
序列化支持
Context 类提供了 to_dict() 和 from_dict() 方法用于字典互转。这种设计选择了字典作为序列化格式而非专门的 DTO 类,是因为字典在 OpenViking 系统中广泛用于:
- HTTP API 的请求/响应体
- 存储层的数据传输
- 与其他模块(如 storage、viking_fs)的数据交换
字典的灵活性在这种异构系统中比严格的 Pydantic 模型更实用——它避免了频繁的类型兼容问题。
数据流分析
数据流动全景
┌─────────────────────────────────────────────────────────────────────────────┐
│ context_typing_and_levels │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ContextType │ │ContextLevel │ │ResourceContent│ │
│ │ Enum │ │ Enum │ │ Type Enum │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Context Class │ │
│ │ - uri, parent_uri (树形结构) │ │
│ │ - context_type, category (类型标签) │ │
│ │ - abstract, vectorize (向量化内容) │ │
│ │ - user, account_id, owner_space (所有权) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
←───────────────┼──────────────→
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────┐ ┌──────────────┐
│ directories.py │ │ session/ │ │ 存储层 │
│ (目录初始化) │ │ session.py│ │ (VikingDB) │
└───────────────────┘ └───────────┘ └──────────────┘
关键数据流路径
路径1:目录初始化
在 directories.py 的 DirectoryInitializer 中,Context 被创建并发送到向量存储:
context = Context(
uri=uri,
parent_uri=parent_uri,
is_leaf=False,
context_type=get_context_type_for_uri(uri),
abstract=defn.abstract,
user=ctx.user,
account_id=ctx.account_id,
owner_space=owner_space,
)
context.set_vectorize(Vectorize(text=defn.overview))
dir_emb_msg = EmbeddingMsgConverter.from_context(context)
await self.vikingdb.enqueue_embedding_msg(dir_emb_msg)
这里的流程是:创建 Context → 设置向量化文本 → 转换为嵌入消息 → 入队等待向量化。
这个流程展示了类型系统的作用:
context_type决定了这个上下文属于哪一类abstract字段用于 L0 向量化owner_space确保了数据隔离
路径2:会话上下文
在 session.py 中,会话被表示为带有特殊 URI 的 Context:
self._session_uri = f"viking://session/{self.user.user_space_name()}/{self.session_id}"
这意味着会话也被纳入统一的上下文体系,可以像检索其他上下文一样检索会话。
路径3:检索使用
虽然 ContextLevel 定义在枚举中,但在实际检索代码(如 hierarchical_retriever.py)中,你会看到使用 ContextType(来自 openviking_cli.retrieve.types)进行过滤——这说明类型系统在检索层被广泛使用。
设计决策与权衡
决策1:单一 Context 类 vs 多态类层次
选择:使用单一 Context 类,通过字段区分类型
权衡:
- ✅ 简化:存储层只需要处理一种对象类型
- ✅ 灵活:可以在运行时改变 context_type(虽然实际很少这样做)
- ❌ 语义:某些字段(如 skill 的 name/description)只在特定类型下有用
这是一个典型的对象-关系阻抗不匹配问题。设计者选择了面向对象的灵活性而非范式化的严格性。
决策2:URI 包含类型信息
选择:通过 URI 中的路径模式(如 /skills、/memories)推断 context_type
权衡:
- ✅ 自动一致:只要 URI 规范,类型就一致
- ✅ 可发现:看到 URI 就能知道类型
- ❌ 隐式:类型推导逻辑分散在多个地方(Context._derive_context_type 和 directories.get_context_type_for_uri)
这种设计反映了约定优于配置的思想,但也带来了"魔法字符串"的问题——如果 URI 不符合约定,推导就会失败。
决策3:整数级别的 ContextLevel
选择:ContextLevel 继承 int 权衡:
- ✅ 方便比较:可以用
<,>,max()等操作 - ❌ 类型安全:失去枚举的类型检查
在向量检索场景中,层级经常需要排序("我要 L1 及以上的上下文"),整数比较确实更实用。
决策4:缺失的多模态支持
选择:Vectorize 类目前只支持文本 权衡:
- ✅ 简单:实现成本低
- ❌ 不完整:无法直接对图片、音频进行向量检索
设计者在注释中保留了扩展空间,这是留白而不是疏忽。
与其他模块的关系
依赖该模块的组件
| 模块 | 用途 |
|---|---|
directories.py |
创建目录 Context,使用 context_type 分类 |
session/session.py |
管理会话上下文,生成会话 URI |
storage 层 |
接收 Context 字典进行存储和检索 |
viking_fs |
将 Context 的 abstract/overview 写入文件系统 |
该模块依赖的组件
openviking_cli.session.user_id.UserIdentifier:用于获取用户身份信息openviking.utils.time_utils:用于时间格式化
这种依赖方向是健康的——核心类型定义不应该依赖业务逻辑。
使用指南与最佳实践
创建 Context 的标准模式
from openviking.core.context import Context, Vectorize, ContextType
# 方式1:显式指定类型
context = Context(
uri="viking://user/projects/myapp",
abstract="用户正在开发的 Python Web 项目",
context_type=ContextType.RESOURCE.value,
category="projects",
user=user,
account_id="acc_123"
)
context.set_vectorize(Vectorize(text="这是一个 Flask 项目..."))
# 方式2:依赖 URI 自动推导
# 如果 URI 包含 "/memories",context_type 会被自动推导为 "memory"
context = Context(
uri="viking://user/memories/preferences",
abstract="用户的通信偏好",
user=user
)
检查上下文类型
# 推荐:使用值比较
if context.context_type == "skill":
# 处理技能
# 或使用枚举(需要导入)
from openviking.core.context import ContextType
if context.context_type == ContextType.SKILL.value:
# 处理技能
序列化与反序列化
# 存储时
data = context.to_dict()
# data 是一个普通字典,可以 JSON 序列化或存入键值存储
# 读取时
context = Context.from_dict(data)
边缘情况与注意事项
1. URI 推导的局限性
如果你创建的 Context URI 不包含 /skills、/memories、/resources 等路径片段,它会被默认归类为 resource 类型。这在大多数情况下是正确的,但如果你创建的是目录节点而非具体资源,可能需要显式指定。
2. owner_space 的空值风险
如果创建 Context 时没有提供 user 参数,owner_space 会是空字符串。这在某些查询场景中可能导致问题,因为空 owner_space 可能匹配到不属于任何用户的数据。建议始终传入 user 参数。
3. vectorize 与 abstract 的关系
abstract 字段用于 L0 向量化和显示摘要,而 vectorize.text 可能在某些场景下设置为不同的文本(如 L1 的 overview)。如果调用 context.set_vectorize(Vectorize(text=...)),之前的 abstract 不会被自动同步。使用时需要确保两者的一致性。
4. ContextLevel 枚举的实际使用
虽然定义了 ContextLevel 枚举,但在当前代码中并没有被直接使用。实际的层级信息是通过 abstract(L0)、overview(L1,由 Vectorize 承载)和完整内容(L2)隐式表示的。这个枚举更像是文档化的设计意图,实际检索时的层级处理逻辑在其他模块中。
5. 序列化时的 skill 特殊处理
if self.context_type == "skill":
data["name"] = self.meta.get("name", "")
data["description"] = self.meta.get("description", "")
这是一个值得注意的非对称设计:skill 类型的 Context 序列化时会额外提取 name 和 description 字段,但反序列化时并未特殊处理。这意味着从存储读取后,这些字段可能不在 meta 中而是直接在顶层字典里。
总结
context_typing_and_levels 模块是 OpenViking 系统的类型契约层。它通过三个枚举(ContextType、ContextLevel、ResourceContentType)和一个统一的 Context 类,为整个系统提供了上下文表示的基础类型。
这个模块的设计体现了几个关键原则:
- 统一抽象:用同一个类表示所有类型的上下文
- 约定优于配置:通过 URI 路径自动推导类型
- 渐进式实现:为多模态等未来功能预留扩展点
- 多租户隔离:通过 owner_space 确保数据安全
理解这个模块是理解整个 OpenViking 架构的前提——无论是目录系统、会话管理还是检索系统,都建立在这些类型定义之上。