🏠

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 类只支持文本(注释中保留了 imagevideoaudio 字段但被注释掉了),说明系统设计时已经预见到多模态需求,但采用了渐进式实现策略——先支持文本,后续再逐步扩展。

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

这是一个简单的数据容器,用于封装向量化所需的文本。设计成单独类而非直接使用字符串,有两个原因:

  1. 扩展性:为未来多模态向量留出扩展空间(注释掉的 image/video/audio 字段)
  2. 语义清晰context.vectorize.textcontext.vectorization_source 更清晰地表达意图

Context 类

这是模块的核心类——一个统一的上下文表示。它的设计有几个关键特点:

URI 作为主键

每个 Context 都有一个 URI(如 viking://user/memories/preferencesviking://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.pyDirectoryInitializer 中,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 → 设置向量化文本 → 转换为嵌入消息 → 入队等待向量化

这个流程展示了类型系统的作用:

  1. context_type 决定了这个上下文属于哪一类
  2. abstract 字段用于 L0 向量化
  3. 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 类,为整个系统提供了上下文表示的基础类型。

这个模块的设计体现了几个关键原则:

  1. 统一抽象:用同一个类表示所有类型的上下文
  2. 约定优于配置:通过 URI 路径自动推导类型
  3. 渐进式实现:为多模态等未来功能预留扩展点
  4. 多租户隔离:通过 owner_space 确保数据安全

理解这个模块是理解整个 OpenViking 架构的前提——无论是目录系统、会话管理还是检索系统,都建立在这些类型定义之上。

On this page