gns3-server/docs/gns3-copilot/context-window-management.md
YueGuobin 8dc8facb13 docs: simplify context window management documentation
Removed extensive implementation details and configuration examples from the context window management documentation. The document now focuses on core permissions and prohibitions for tool usage, providing a clearer and more concise reference for allowed and forbidden actions. This streamlines the documentation to essential guidelines only.
2026-03-05 14:05:58 +08:00

12 KiB
Raw Blame History

LLM 上下文窗口管理实现文档

概述

本文档说明 GNS3 Copilot 的上下文窗口管理实现机制包括消息裁剪、Token 计数和配置验证。

实现架构

1. 核心模块

文件位置: gns3server/agent/gns3_copilot/agent/context_manager.py

Token 计数策略

系统使用 tiktoken 进行 Token 计数context_manager.py:60

_tiktoken_encoding = tiktoken.get_encoding("cl100k_base")

必需依赖

pip install tiktoken>=0.8.0

如果未安装 tiktoken系统将在启动时抛出 ModuleNotFoundError

关键函数

count_tokens(text: str) -> int (context_manager.py:84-100)

  • 使用 tiktoken 准确计数文本的 token 数
  • 使用 cl100k_base 编码
  • 返回精确的 token 数

estimate_tool_tokens(tools: list) -> int (context_manager.py:103-169)

  • 序列化工具 schema 为 JSON
  • 使用 tiktoken 计数工具定义的 token 消耗
  • 支持 Pydantic v1/v2 兼容性
  • 失败时使用 1000 tokens 的回退值

create_pre_model_hook(...) (context_manager.py:195-402)

  • 创建预处理函数pre_model_hook
  • 在每次 LLM 调用前自动执行:
    1. 注入 topology 信息到 system prompt
    2. 估算工具定义的 token 消耗
    3. 裁剪消息历史以适应上下文限制
  • 返回一个可调用的函数,用于准备消息

2. 裁剪逻辑详解

2.1 Token 预算分配

当调用 LLM 时,发送的内容包含两部分:

发送给 LLM 的完整请求:
┌─────────────────────────────────────────────────────────────┐
│ 1. Messages (我们管理的)                                     │
│    ├─ SystemMessage: system prompt + topology (模板注入)     │
│    └─ HumanMessage/AIMessage: 用户消息 / 历史消息             │
├─────────────────────────────────────────────────────────────┤
│ 2. Tool Definitions (LangChain 自动添加,不在消息中)          │
│    ├─ Tool 1 schema (name, description, parameters)        │
│    ├─ Tool 2 schema                                        │
│    └─ ... (约 500-1500 tokens per tool)                   │
└─────────────────────────────────────────────────────────────┘

System Message 结构

  • 使用模板变量 {{topology_info}} 动态注入 topology
  • System prompt 包含占位符:"### CURRENT TOPOLOGY\n{{topology_info}}"
  • 如果有 topology替换为实际内容
  • 如果没有 topology替换为 "(No topology information available)"

2.2 裁剪流程

第一步:计算输入预算
┌─────────────────────────────────────────────────────────────┐
│ context_limit: 128,000 tokens (128K)                        │
│ strategy: balanced (75%)                                    │
│                                                            │
│ 输入预算 = 128 × 1000 × 0.75 = 96,000 tokens              │
└─────────────────────────────────────────────────────────────┘
                            ↓
第二步:减去工具定义
┌─────────────────────────────────────────────────────────────┐
│ 输入预算: 96,000 tokens                                     │
│ 工具定义: 1,725 tokens                                     │
│                                                            │
│ 可用于消息 = 96,000 - 1,725 = 94,275 tokens                │
└─────────────────────────────────────────────────────────────┘
                            ↓
第三步trim_messages 处理
┌─────────────────────────────────────────────────────────────┐
│ 调用 LangChain 的 trim_messages:                            │
│ - max_tokens = 94,275 (包含 system message)                │
│ - strategy = "last" (保留最新消息)                          │
│ - token_counter = tiktoken 计数函数                         │
│ - include_system = True (始终保留 system)                   │
│                                                            │
│ trim_messages 会:                                           │
│ 1. 保留 SystemMessage (system + topology)                  │
│ 2. 从最新消息开始,保留尽可能多的历史                        │
│ 3. 超出限制时,丢弃最旧的消息                                │
└─────────────────────────────────────────────────────────────┘

2.3 裁剪优先级

系统按以下优先级保留内容:

优先级 内容 说明
1 System Message (system prompt + topology) 永远保留
2 最新用户消息 至少保留最后1条
3 旧对话历史 按时间顺序丢弃

注意System prompt 和 topology info 通过模板变量合并为一个 SystemMessage无法单独分离。

2.4 边界情况处理

情况 处理方式
System (包含 topology) > 预算 保留完整的 System Message无法分离 system 和 topology
Tools > 预算 ERROR 日志,建议增加 context_limit 或减少工具数量
历史全被裁剪 保留最后1条用户消息

重要提示

  • 当 system + topology 超出可用预算时,两者都会被保留
  • 无法只丢弃 topology 而保留 system prompt因为已合并

3. 集成到 GNS3 Copilot

文件位置: gns3server/agent/gns3_copilot/agent/gns3_copilot.py

实现方式

关键点:系统使用自定义 StateGraph,不是 LangGraph 的预构建 agent。

因此,pre_model_hook 不能通过 model.invoke(config={"configurable": {"pre_model_hook": ...}}) 传递。

正确的使用方式直接调用 pre_hook 函数准备消息。

def llm_call(state: dict, config: RunnableConfig | None = None):
    """LLM decides whether to call a tool or not."""

    # 1. 获取 topology 信息
    project_id = config["configurable"].get("project_id")
    topology_info = None
    if project_id:
        topology_tool = GNS3TopologyTool()
        topology = topology_tool._run(project_id=project_id)
        if topology and "error" not in topology:
            topology_info = topology

    # 2. 创建 pre_model_hook
    system_prompt = load_system_prompt()
    pre_hook = create_pre_model_hook(
        system_prompt=system_prompt,
        get_topology_func=lambda s: s.get("topology_info"),
        get_llm_config_func=get_current_llm_config,
        get_tools_func=lambda: tools,
    )

    # 3. 创建 model with tools
    model_with_tools = create_base_model_with_tools(tools, llm_config=llm_config)

    # 4. ⭐ 关键:直接调用 pre_hook 准备消息
    logger.info("Calling pre_hook to prepare %d messages", len(messages))
    prepared_state = pre_hook({"messages": messages, "topology_info": topology_info})
    prepared_messages = prepared_state["messages"]

    # 5. 使用准备好的消息调用 LLM
    response = model_with_tools.invoke(prepared_messages)

    return {"messages": [response], ...}

为什么不通过 config 传递?

LangGraph 的 pre_model_hook 参数仅适用于预构建的 agent,不适用于自定义 StateGraph。

Agent 类型 pre_model_hook 支持方式
create_react_agent 通过 pre_model_hook 参数
chat_agent_executor 通过 pre_model_hook 参数
自定义 StateGraph 不支持,需要直接调用

我们的实现使用的是自定义 StateGraphagent_builder = StateGraph(MessagesState)),所以必须直接调用 pre_hook

4. 执行流程

用户发送消息
     ↓
llm_call 节点被调用
     ↓
获取 project_id (从 config["configurable"])
     ↓
调用 GNS3TopologyTool._run(project_id) 获取 topology
     ↓
存储 topology_info 到 state
     ↓
创建 pre_model_hook (通过 create_pre_model_hook())
     ↓
【关键】直接调用 pre_hook({"messages": messages, "topology_info": topology_info})
     ├─ 1. 注入 topology 到 system prompt
     ├─ 2. 估算工具定义 tokens
     ├─ 3. 调用 trim_messages() 裁剪消息
     └─ 4. 返回准备好的消息列表
     ↓
使用准备好的消息调用 model.invoke()
     ↓
返回 LLM 响应

策略实现

Context Strategy Ratios

定义context_manager.py:68-72

CONTEXT_STRATEGY_RATIOS = {
    "conservative": 0.60,
    "balanced": 0.75,
    "aggressive": 0.85,
}

默认值context_manager.py:74

DEFAULT_CONTEXT_STRATEGY = "balanced"

策略对比

策略 输入比例 输出预留 计算公式
Conservative 60% 40% context_limit × 1000 × 0.60
Balanced 75% 25% context_limit × 1000 × 0.75
Aggressive 85% 15% context_limit × 1000 × 0.85

日志输出

正常情况topology 成功注入)

INFO: Calling pre_hook to prepare 1 messages
INFO: ✓ Topology injected: 7722 chars, nodes: ['netshoot-1', 'R1', 'R2', 'IOU-L3-1', 'IOU-L3-2']
INFO: Context ready: 2 msgs, ~3815 tokens + 1725 tools = 5540 / 128K (4.3%), strategy=conservative
INFO: Messages prepared: 1 → 2
INFO: LLM call completed: tool_calls=0

发生裁剪时

INFO: Calling pre_hook to prepare 50 messages
INFO: ✓ Topology injected: 8500 chars, nodes: ['R1', 'R2', ...]
INFO: Messages trimmed: 50 → 25 msgs. Total: ~82000 tokens + 1725 tools = 83725 / 128K (65.4%), strategy=balanced
INFO: Messages prepared: 50 → 25

topology 为 None 时

INFO: Calling pre_hook to prepare 1 messages
WARNING: ✗ Topology data is None, injecting placeholder
INFO: Context ready: 2 msgs, ~800 tokens + 1725 tools = 2525 / 128K (2.0%), strategy=balanced

错误处理

tiktoken 未安装

如果 tiktoken 未安装,系统将在启动时抛出错误:

ModuleNotFoundError: No module named 'tiktoken'

解决方法

pip install tiktoken>=0.8.0

context_limit 缺失或无效

如果 LLM 配置中没有 context_limit 或值无效context_manager.py:285-295

if "context_limit" not in llm_config:
    raise ValueError("context_limit is required in LLM config")

limit = llm_config["context_limit"]
if not isinstance(limit, int) or limit <= 0:
    raise ValueError(f"Invalid context_limit: {limit}")

裁剪失败

try:
    trimmed = trim_messages(...)
except Exception as e:
    logger.error("Failed to trim messages: %s", e)
    logger.warning("Returning original messages due to trimming error")
    return {"messages": messages_with_system}

相关源文件

  • gns3server/agent/gns3_copilot/agent/context_manager.py - 上下文管理核心逻辑
  • gns3server/agent/gns3_copilot/agent/gns3_copilot.py - LLM 调用节点StateGraph
  • gns3server/agent/gns3_copilot/agent/model_factory.py - 模型创建和工具绑定