context-management.md 11 KB

Context 管理

文档维护规范

  1. 先改文档,再动代码 - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
  2. 文档分层,链接代码 - 重要或复杂设计可以另有详细文档;关键实现需标注代码文件路径;格式:module/file.py:function_name
  3. 简洁快照,日志分离 - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议、决策历史、修改日志、大量代码;决策依据或修改日志若有必要,可在 docs/decisions.md 另行记录

概述

Context = 每次 LLM 调用时发送的完整消息列表。管理目标:

  1. 注入:确保模型在正确的时间看到正确的信息(system prompt、plan、skill 策略)
  2. 压缩:在 token 预算内去除冗余,保留核心上下文

两者互相影响:压缩可能清掉已注入的内容,需要重新注入。


Context 构成

[system]     System Prompt(角色 + 内置 skills)        ← 静态注入,trace 创建时
[user]       第一条用户消息
[assistant]  ...
[tool]       ...                                         ← 可能包含周期注入的 context
[assistant]  ...
[tool]       ...                                         ← 可能包含动态注入的 skill
[user]       第 N 条用户消息
[assistant]  待模型生成

注入机制

1. System Prompt(静态注入)

Trace 创建时构建一次,后续续跑不重复发送。

内容:角色 prompt + 按 preset/call-site 过滤的内置/项目 skills。

优先级:messages 中的 system message > config.system_prompt > preset.system_prompt > DEFAULT_SYSTEM_PREFIX

实现agent/core/runner.py:_build_system_prompt


2. Context 周期注入(当前实现)

CONTEXT_INJECTION_INTERVAL(当前=5)轮迭代,框架自动向模型的 tool_calls 中追加一条 get_current_context 调用(如果模型本轮未主动调用)。

注入内容(_build_context_injection 构建):

  • 当前时间
  • GoalTree(精简视图)+ focus 提醒
  • Active Collaborators 状态
  • IM 消息通知(如有)
  • Context Injection Hooks 注册的自定义内容

注入形态:合成的 tool_call + tool_result 消息对。模型看到的效果等同于"我之前主动调用了 get_current_context"。

[assistant]  tool_calls: [..., get_current_context()]   ← 框架自动追加
[tool]       { 当前时间, GoalTree, Collaborators... }   ← 工具正常执行

特点

  • 固定间隔,不做去重(每次内容都是最新状态快照)
  • 仅在主路径执行(侧分支中跳过)
  • 检查模型是否已主动调用,避免重复

实现agent/core/runner.pyCONTEXT_INJECTION_INTERVAL, 工具执行后的自动注入逻辑)

详细文档架构设计 § Context Injection Hooks


3. Skill 指定注入(设计中,未实现)

区别于模型通过 skill() 工具主动加载 skill,指定注入是调用方在 runner.run() 时声明需要哪些 skill,由框架自动注入。

动机

System Prompt 中的内置 skills 是启动时一次性注入的。但某些 skill(如 Librarian 的查询策略/上传策略):

  • 不是每次都需要(按任务类型按需加载)
  • 在长期续跑的 agent 中会被压缩掉,需要重新注入
  • 应该和现有的 skill 工具统一管理

启用方式

调用方通过 runner.run()inject_skills 参数声明(不放在 RunConfig 中,因为同一个 agent 的不同调用可能需要不同的 skill):

# Librarian ask 场景
async for item in runner.run(
    messages=[{"role": "user", "content": "[ASK] 查询内容"}],
    config=config,
    inject_skills=["ask_strategy"],         # 本次调用需要的 skill
    skill_recency_threshold=10,             # 最近 N 条消息内有就不重复注入
):
    ...

# Librarian upload 场景(同一个 agent,不同 skill)
async for item in runner.run(
    messages=[{"role": "user", "content": "[UPLOAD:BATCH] 数据"}],
    config=config,
    inject_skills=["upload_strategy"],
    skill_recency_threshold=10,
):
    ...

注入机制

框架在工具执行阶段自动注入(与 context 周期注入同级处理)。

注入形态:合成的 skill("xxx") tool_call + tool_result 消息对,复用现有 skill 工具。

[user]       [ASK] 有没有工具能做人物姿态控制?
[assistant]  tool_calls: [skill("ask_strategy")]        ← 框架自动生成
[tool]       { ask_strategy skill 完整内容 }             ← skill 工具正常执行
[assistant]  让我来检索...                               ← 模型真实输出

防重复:message ID 追踪

trace.context 中维护已注入 skill 的记录:

trace.context["injected_skills"] = {
    "ask_strategy": {
        "message_id": "msg_abc123",   # tool_result 消息的 ID
        "sequence": 42                # 最新注入的消息序列号
    }
}

注入前检测流程:

  1. injected_skills[skill_id].message_id
  2. 检查该 message_id 是否仍在当前提交给模型的消息列表中
  3. 在且 sequence 在最近 recency_threshold 条消息内(从当前提交给模型的列表中查,不按编号计算) → 跳过
  4. 不在(被压缩掉)或超过阈值 → 重新注入,更新记录

Skill 的两种加载方式

方式 触发者 场景
指定注入(本节) 调用方,通过 run(inject_skills=...) 已知任务类型,确保策略在场
主动加载 模型自己调 skill() 工具 遇到未预见场景,自主判断需要什么

两者都走 skill 工具,消息格式一致。

Skill 文件位置

指定注入的 skill 文件遵循现有 skill 体系(Markdown + frontmatter),按项目组织:

knowhub/agents/skills/         # KnowHub Librarian 的 skills
├── ask_strategy.md            # 查询检索策略
└── upload_strategy.md         # 上传编排策略

压缩机制

压缩分两级,通过 RunConfig.goal_compression 控制 Level 1 行为:

模式 Level 1 行为 Level 2 行为
不压缩 "none" 跳过 Level 1 超限时直接进入 Level 2
完成后压缩 "on_complete" 每个 goal 完成时立刻压缩该 goal 的消息 超限时进入 Level 2
超长时压缩 "on_overflow" 超限时遍历所有 completed goal 逐个压缩 Level 1 后仍超限则进入 Level 2

默认值:"on_overflow"

Level 1:Goal 完成压缩(确定性,零 LLM 成本)

对单个 completed/abandoned goal 的压缩逻辑:

  1. 识别目标消息:找到该 goal 关联的所有消息(msg.goal_id == goal.id
  2. 区分 goal 工具消息和非 goal 消息:检查 assistant 消息的 tool_calls 中是否调用了 goal 工具
  3. 保留 goal 工具消息:保留所有调用 goal(...) 的 assistant 消息及其对应的 tool result(包括 add、focus、under、done 等操作)
  4. 删除非 goal 消息:从 history 中移除该 goal 的其他 assistant 消息及其 tool result(read_file、bash、search 等中间工具调用)
  5. 替换 done 的 tool result:将 goal(done=...) 的 tool result 内容替换为 "具体执行过程已清理"
  6. 纯内存操作:压缩仅操作内存中的 history 列表,不涉及新增消息或持久化变更,原始消息永远保留在存储层

压缩后的 history 片段示例:

... (前面的消息)
[assistant] tool_calls: [goal(focus="1.1")]
[tool] goal result: "## 更新\n- 焦点切换到: 1.1\n\n## Current Plan\n..."
[assistant] tool_calls: [goal(done="1.1", summary="前端使用 React...")]
[tool] goal result: "具体执行过程已清理"
... (后面的消息)

on_complete 模式

在 goal 工具执行 done 操作后,立刻对该 goal 执行压缩。优点是 history 始终保持精简,缺点是如果后续需要回溯到该 goal 的中间过程,信息已丢失(存储层仍保留原始消息)。

触发点agent/core/runner.py(工具执行后检测 goal(done=...) 调用)

on_overflow 模式

_manage_context_usage 检测到 token 超限时,遍历所有 completed goal,逐个执行压缩,直到 token 数降到阈值以下或所有 completed goal 都已压缩。如果仍超限,进入 Level 2。

触发点agent/core/runner.py:_manage_context_usage

实现agent/trace/compaction.py:compress_completed_goals

Level 2:LLM 摘要压缩

触发条件:Level 1 之后 token 数仍超过阈值(默认 context_window × 0.5)。

通过侧分支队列机制执行,force_side_branch 为列表类型:

  1. 反思(可选,由 knowledge.enable_extraction 控制):进入 reflection 侧分支,LLM 可多轮调用工具提取知识
  2. 知识评估(可选):进入 knowledge_eval 侧分支,评估待评估知识
  3. 压缩:进入 compression 侧分支,LLM 生成 summary

侧分支队列示例:

  • 启用知识提取:["reflection", "compression"]
  • 有待评估知识:["knowledge_eval", "compression"]
  • 仅压缩:["compression"]

压缩完成后重建 history 为:system prompt + 第一条 user message + summary(含详细 GoalTree)

实现agent/core/runner.py:_agent_loop(侧分支状态机), agent/core/runner.py:_rebuild_history_after_compression

任务完成后反思

主路径无工具调用(任务完成)时,如果 knowledge.enable_completion_extraction 为 True,通过侧分支进入反思,完成后退出循环。

GoalTree 双视图

to_prompt() 支持两种模式:

  • include_summary=False(默认):精简视图,用于日常周期性注入
  • include_summary=True:含所有 completed goals 的 summary,用于 Level 2 压缩时提供上下文

压缩存储

  • 原始消息永远保留在 messages/
  • 压缩 summary 作为普通 Message 存储
  • 侧分支消息通过 branch_typebranch_id 标记,查询主路径时自动过滤
  • 通过 parent_sequence 树结构实现跳过,无需 compression events 或 skip list
  • Rewind 到压缩区域内时,summary 脱离主路径,原始消息自动恢复

注入与压缩的交互

压缩会移除消息,包括之前注入的 skill 和 context 内容。不同注入类型的应对策略:

注入类型 压缩影响 恢复策略
System Prompt Level 2 重建时保留 无需额外处理
Context 周期注入 可能被 Level 1/2 移除 固定间隔自动重注入(下一个 interval 即恢复)
Skill 动态注入 可能被 Level 1/2 移除 message ID 检测 → 自动重注入

Skill 重注入的触发点:每次 runner 准备构建 LLM 调用前,检查 injected_skills 中记录的 message_id 是否仍在当前 history 中。不在则标记为需要重新注入,在下一轮工具执行阶段合成 tool_call。


相关文档

文档 内容
架构设计 Agent 框架完整架构
Skills 指南 Skill 文件格式、分类、加载机制
Prompt 规范 信息分层、条件注入原则
Trace API 压缩和反思的 REST API