构建上下文引擎插件

上下文引擎插件(Context Engine Plugin)用替代策略替换内置的 ContextCompressor 来管理对话上下文。例如,一种无损上下文管理引擎(Lossless Context Management, LCM)构建知识 DAG(有向无环图),而不是有损摘要。

工作原理

代理的上下文管理基于 ContextEngine 抽象基类(ABC)(agent/context_engine.py)。内置的 ContextCompressor 是默认实现。插件引擎必须实现相同的接口。

同一时刻只能有一个上下文引擎处于活动状态。选择通过配置驱动:

# config.yaml
context:
  engine: "compressor"    # 默认内置引擎
  engine: "lcm"           # 激活名为 "lcm" 的插件引擎

插件引擎永远不会自动激活——用户必须显式地将 context.engine 设置为插件的名称。

目录结构

每个上下文引擎位于 plugins/context_engine/<name>/ 目录下:

plugins/context_engine/lcm/
├── __init__.py      # 导出 ContextEngine 子类
├── plugin.yaml      # 元数据(名称、描述、版本)
└── ...              # 引擎所需的其他模块

ContextEngine 抽象基类

你的引擎必须实现以下必需方法:

from agent.context_engine import ContextEngine
 
class LCMEngine(ContextEngine):
 
    @property
    def name(self) -> str:
        """简短标识符,例如 'lcm'。必须与 config.yaml 中的值匹配。"""
        return "lcm"
 
    def update_from_response(self, usage: dict) -> None:
        """每次 LLM 调用后,使用 usage 字典进行更新。
 
        更新 self.last_prompt_tokens、self.last_completion_tokens、
        self.last_total_tokens。
        """
 
    def should_compress(self, prompt_tokens: int = None) -> bool:
        """如果本轮应触发压缩,返回 True。"""
 
    def compress(self, messages: list, current_tokens: int = None,
                 focus_topic: str = None) -> list:
        """压缩消息列表,返回新的(可能更短的)列表。
 
        返回的列表必须是有效的 OpenAI 格式消息序列。
 
        ``focus_topic`` 是可选的焦点主题字符串,来自手动执行的
        ``/compress <focus>``;支持引导压缩的引擎应优先保留与之相关的信息,
        其他引擎可忽略该参数。
        """

引擎需维护的类属性

代理直接读取这些属性用于显示和日志记录:

last_prompt_tokens: int = 0
last_completion_tokens: int = 0
last_total_tokens: int = 0
threshold_tokens: int = 0        # 触发压缩的阈值
context_length: int = 0          # 模型的完整上下文窗口
compression_count: int = 0       # compress() 已执行的次数

可选方法

这些方法在抽象基类中有合理的默认实现。根据需要覆盖:

方法默认行为何时覆盖
on_session_start(session_id, **kwargs)空操作需要加载持久化状态(DAG、数据库)时
on_session_end(session_id, messages)空操作需要刷新状态、关闭连接时
on_session_reset()重置令牌计数器需要清除每个会话的状态时
update_model(model, context_length, ...)更新 context_length 和 threshold需要在模型切换时重新计算预算时
get_tool_schemas()返回 []引擎提供代理可调用的工具时(例如 lcm_grep
handle_tool_call(name, args, **kwargs)返回错误 JSON需要实现工具处理器时
should_compress_preflight(messages)返回 False可进行廉价的 API 调用前预估时
get_status()标准令牌/阈值字典需要暴露自定义指标时

引擎工具

上下文引擎可以暴露代理直接调用的工具。从 get_tool_schemas() 返回模式,在 handle_tool_call() 中处理调用:

def get_tool_schemas(self):
    return [{
        "name": "lcm_grep",
        "description": "搜索上下文知识图谱",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "搜索查询"}
            },
            "required": ["query"],
        },
    }]
 
def handle_tool_call(self, name, args, **kwargs):
    if name == "lcm_grep":
        results = self._search_dag(args["query"])
        return json.dumps({"results": results})
    return json.dumps({"error": f"未知工具: {name}"})

引擎工具在启动时被注入到代理的工具列表中,并自动分发——无需注册。

注册

通过目录方式(推荐)

将引擎放置在 plugins/context_engine/<name>/ 目录下。__init__.py 必须导出一个 ContextEngine 子类。发现系统会自动找到并实例化该引擎。

通过通用插件系统

通用插件也可以注册上下文引擎:

def register(ctx):
    engine = LCMEngine(context_length=200000)
    ctx.register_context_engine(engine)

只能注册一个引擎。第二个插件尝试注册时会被拒绝并发出警告。

生命周期

1. 引擎实例化(插件加载或目录发现)
2. on_session_start() — 对话开始
3. update_from_response() — 每次 API 调用后
4. should_compress() — 每轮检查
5. compress() — 当 should_compress() 返回 True 时调用
6. on_session_end() — 会话边界(CLI 退出、/reset、网关过期)

/new/reset 时调用 on_session_reset() 以清除每会话状态,而无需完全关闭。

配置

用户通过 hermes plugins → Provider Plugins → Context Engine,或编辑 config.yaml 来选择引擎:

context:
  engine: "lcm"   # 必须与引擎的 name 属性匹配

compression 配置块(compression.thresholdcompression.protect_last_n 等)是内置 ContextCompressor 专用的。你的引擎应定义自己的配置格式(如有需要),在初始化时从 config.yaml 读取。

测试

from agent.context_engine import ContextEngine
 
def test_engine_satisfies_abc():
    engine = YourEngine(context_length=200000)
    assert isinstance(engine, ContextEngine)
    assert engine.name == "your-name"
 
def test_compress_returns_valid_messages():
    engine = YourEngine(context_length=200000)
    msgs = [{"role": "user", "content": "hello"}]
    result = engine.compress(msgs)
    assert isinstance(result, list)
    assert all("role" in m for m in result)

完整的抽象基类契约测试套件请参见 tests/agent/test_context_engine.py

参见