mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-09 03:25:20 +03:00
- Refactor system message structure to combine system prompt and topology info using template variables - Update token calculation process with merged system message approach - Clarify priority order for message retention during context window management - Add detailed token counting implementation using tiktoken library - Include boundary case handling for system message exceeding budget - Add SPDX license headers to tool files for proper licensing documentation
855 lines
28 KiB
Markdown
855 lines
28 KiB
Markdown
# LLM 上下文窗口管理实现文档
|
||
|
||
## 概述
|
||
|
||
本文档说明了 GNS3 Copilot 如何处理不同 LLM 模型的上下文窗口限制,以及如何实现自动消息裁剪功能。
|
||
|
||
## ⚠️ 重要:context_limit 必须手动配置
|
||
|
||
**由于模型供应商频繁更新上下文窗口大小,系统不再提供内置默认值。**
|
||
|
||
用户在创建 LLM 模型配置时**必须提供 `context_limit`**。请从模型供应商的官方文档获取最新的上下文窗口大小。
|
||
|
||
### ⚡ 依赖要求
|
||
|
||
**tiktoken 是必需依赖**,系统无法在没有 tiktoken 的情况下运行。
|
||
|
||
```bash
|
||
pip install tiktoken>=0.8.0
|
||
```
|
||
|
||
如果未安装 tiktoken,系统将在首次尝试计数 tokens 时抛出 ImportError。
|
||
|
||
### ⚡ 单位说明
|
||
|
||
**`context_limit` 的单位是 K tokens(千 tokens)**
|
||
|
||
- `1` = 1K = 1,000 tokens
|
||
- `128` = 128K = 128,000 tokens
|
||
- `200` = 200K = 200,000 tokens
|
||
- `2800` = 2800K = 2,800,000 tokens
|
||
|
||
### 为什么使用 K tokens?
|
||
|
||
1. **更简洁** - `128` 比 `128000` 更易读易写
|
||
2. **减少错误** - 避免少写或多写 0
|
||
3. **符合习惯** - 与业界常用的 "128K", "200K" 表示法一致
|
||
|
||
### 为什么必须手动配置?
|
||
|
||
1. **模型更新频繁** - OpenAI、Anthropic、Google 等供应商经常发布模型更新
|
||
2. **上下文大小变化** - 新版本往往增加上下文窗口,旧版本可能被废弃
|
||
3. **维护成本高** - 内置默认值很快过时,可能导致配置错误
|
||
4. **责任明确** - 用户根据实际使用的模型配置,避免混淆
|
||
|
||
---
|
||
|
||
## 数据库配置方式
|
||
|
||
### 配置要求
|
||
|
||
`context_limit` 是**必填字段**,单位为 **K tokens**。创建或更新 LLM 模型配置时必须提供。
|
||
|
||
| 字段 | 类型 | 必填 | 单位 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `context_limit` | `int` | ✅ **是** | K tokens | 模型上下文窗口限制(128 = 128K tokens) |
|
||
| `context_strategy` | `"conservative" \| "balanced" \| "aggressive"` | 否 | - | 裁剪策略,默认 "balanced" |
|
||
|
||
### 获取上下文窗口大小
|
||
|
||
#### 官方文档链接
|
||
|
||
| 供应商 | 文档链接 | 示例值(tokens) | 配置值(K) |
|
||
|--------|---------|------------------|------------|
|
||
| OpenAI | https://platform.openai.com/docs/models | 128,000 | `128` |
|
||
| Anthropic | https://docs.anthropic.com/claude/docs/models-overview | 200,000 | `200` |
|
||
| Google | https://ai.google.dev/gemini-api/docs/models | 2,800,000 | `2800` |
|
||
| DeepSeek | https://platform.deepseek.com/api-docs/ | 128,000 | `128` |
|
||
| xAI | https://docs.x.ai/ | 128,000 | `128` |
|
||
|
||
**转换示例**:
|
||
- 官方文档显示 `128,000 tokens` → 配置为 `128`
|
||
- 官方文档显示 `200,000 tokens` → 配置为 `200`
|
||
- 官方文档显示 `2,800,000 tokens` → 配置为 `2800`
|
||
|
||
#### 参考工具
|
||
|
||
运行参考脚本查看常见模型的上下文限制(显示为 K tokens):
|
||
|
||
```bash
|
||
python scripts/show_model_context_limits.py
|
||
```
|
||
|
||
**注意**:此脚本仅提供参考值,请以官方文档为准。
|
||
|
||
---
|
||
|
||
## API 使用示例
|
||
|
||
### 创建配置(必须提供 context_limit)
|
||
|
||
```bash
|
||
POST /v3/users/{user_id}/llm-model-configs
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "GPT-4o Configuration",
|
||
"model_type": "text",
|
||
"is_default": true,
|
||
"provider": "openai",
|
||
"base_url": "https://api.openai.com/v1",
|
||
"model": "gpt-4o",
|
||
"temperature": 0.7,
|
||
"api_key": "sk-...",
|
||
"context_limit": 128, # ← 必填:单位是 K tokens (128 = 128K = 128,000 tokens)
|
||
"context_strategy": "balanced" # 可选,默认 "balanced"
|
||
}
|
||
```
|
||
|
||
### 配置示例对比
|
||
|
||
#### 官方文档 → API 配置
|
||
|
||
| 模型 | 官方文档 | API 配置 |
|
||
|------|---------|---------|
|
||
| GPT-4o | 128,000 tokens | `"context_limit": 128` |
|
||
| GPT-3.5 Turbo | 16,385 tokens | `"context_limit": 17` (向上取整) |
|
||
| Claude 3.5 Sonnet | 200,000 tokens | `"context_limit": 200` |
|
||
| Gemini 1.5 Pro | 2,800,000 tokens | `"context_limit": 2800` |
|
||
|
||
### 错误示例(缺少 context_limit)
|
||
|
||
```bash
|
||
POST /v3/users/{user_id}/llm-model-configs
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "Invalid Config",
|
||
"provider": "openai",
|
||
"model": "gpt-4o",
|
||
# ❌ 缺少 context_limit
|
||
}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"detail": "context_limit is required (unit: K tokens, e.g., 128 = 128K = 128,000 tokens). Please check your model provider's documentation for the current context window size and specify it in the configuration."
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 常见模型配置参考
|
||
|
||
以下是一些常见模型的 `context_limit` 配置值(**请以官方文档为准**):
|
||
|
||
### OpenAI
|
||
| 模型 | 官方文档 | 配置值 |
|
||
|------|---------|-------|
|
||
| GPT-4o | 128,000 tokens | `"context_limit": 128` |
|
||
| GPT-4o-mini | 128,000 tokens | `"context_limit": 128` |
|
||
| GPT-4 Turbo | 128,000 tokens | `"context_limit": 128` |
|
||
| GPT-3.5 Turbo | 16,385 tokens | `"context_limit": 17` |
|
||
|
||
### Anthropic
|
||
| 模型 | 官方文档 | 配置值 |
|
||
|------|---------|-------|
|
||
| Claude 3.5 Sonnet | 200,000 tokens | `"context_limit": 200` |
|
||
| Claude 3 Opus | 200,000 tokens | `"context_limit": 200` |
|
||
| Claude 3 Haiku | 200,000 tokens | `"context_limit": 200` |
|
||
|
||
### Google
|
||
| 模型 | 官方文档 | 配置值 |
|
||
|------|---------|-------|
|
||
| Gemini 2.0 Flash | 1,000,000 tokens | `"context_limit": 1000` |
|
||
| Gemini 1.5 Pro | 2,800,000 tokens | `"context_limit": 2800` |
|
||
| Gemini 1.5 Flash | 2,800,000 tokens | `"context_limit": 2800` |
|
||
|
||
### DeepSeek
|
||
| 模型 | 官方文档 | 配置值 |
|
||
|------|---------|-------|
|
||
| DeepSeek Chat | 128,000 tokens | `"context_limit": 128` |
|
||
| DeepSeek Coder | 128,000 tokens | `"context_limit": 128` |
|
||
|
||
### xAI
|
||
| 模型 | 官方文档 | 配置值 |
|
||
|------|---------|-------|
|
||
| Grok Beta | 128,000 tokens | `"context_limit": 128` |
|
||
|
||
---
|
||
|
||
## 问题背景
|
||
|
||
### 原始问题
|
||
- 消息历史无限制累积,使用 `operator.add` 追加所有消息
|
||
- 不同模型的上下文窗口限制不同
|
||
- 当消息历史超过模型限制时,LLM 调用会失败
|
||
- 没有优雅的降级或裁剪机制
|
||
|
||
### 解决方案
|
||
使用 LangChain/LangGraph 的内置功能实现智能上下文管理:
|
||
- **`trim_messages`**: LangChain 的消息裁剪工具
|
||
- **`count_tokens_approximately`**: Token 计数功能
|
||
- **用户配置**: 必须提供 context_limit(单位:K tokens),无默认值
|
||
|
||
## 实现架构
|
||
|
||
### 1. 核心模块
|
||
|
||
**文件位置**: `gns3server/agent/gns3_copilot/agent/context_manager.py`
|
||
|
||
#### Token 计数策略
|
||
|
||
系统使用 **tiktoken** 进行准确的 Token 计数:
|
||
|
||
1. **tiktoken(OpenAI 的 tokenizer)**
|
||
- 使用 `cl100k_base` 编码(GPT-4)
|
||
- 对大多数现代 LLM 准确(OpenAI、Anthropic、DeepSeek)
|
||
- 准确率约 95%+
|
||
- **必需依赖**:系统强制要求安装 tiktoken
|
||
|
||
2. **工具定义 Token 估算**
|
||
- 工具的 schema(name、description、parameters)会被转换为 JSON 发送给 LLM
|
||
- 系统会自动计算这些定义的 token 消耗
|
||
- 每个工具约 500-1500 tokens(取决于 schema 复杂度)
|
||
|
||
#### 安装 tiktoken(必需)
|
||
|
||
```bash
|
||
pip install tiktoken>=0.8.0
|
||
```
|
||
|
||
**为什么使用 tiktoken?**
|
||
- 比字符估算准确 2-3 倍(特别是中文内容)
|
||
- 支持 LangChain 使用的所有主流模型
|
||
- 性能优秀(缓存编码器)
|
||
- **这是必需依赖**,系统将无法运行如果未安装
|
||
|
||
#### 关键函数
|
||
|
||
**`get_model_context_limit(model_name: str, llm_config: dict) -> int`**
|
||
- 从数据库配置获取模型的上下文窗口大小
|
||
- **`llm_config` 必须包含 `context_limit` 字段(单位:K tokens)**
|
||
- 如果未提供或无效,抛出 `ValueError`
|
||
- 返回值单位为实际 tokens(K tokens × 1000)
|
||
|
||
**`calculate_max_tokens(model_limit, strategy) -> int`**
|
||
- 计算可用 token 数量(预留输出空间)
|
||
- 三种策略:
|
||
- `conservative`: 使用 60% 限制(更安全)
|
||
- `balanced`: 使用 75% 限制(默认)
|
||
- `aggressive`: 使用 85% 限制(最大化输入)
|
||
|
||
**`trim_messages_for_context(messages, model_name, strategy, tool_tokens) -> list`**
|
||
- 使用 tiktoken 裁剪消息
|
||
- 保留最近的消息
|
||
- 始终保留系统消息
|
||
- **关键**:考虑工具定义占用的 tokens
|
||
|
||
### 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: 16,000 tokens │
|
||
│ strategy: conservative (60%) │
|
||
│ │
|
||
│ 输入预算 = 16,000 × 0.6 = 9,600 tokens │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
第二步:减去工具定义
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 输入预算: 9,600 tokens │
|
||
│ 工具定义: 1,862 tokens │
|
||
│ │
|
||
│ 可用于消息 = 9,600 - 1,862 = 7,738 tokens │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
第三步:计算 System Message token(已合并)
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 可用于消息: 7,738 tokens │
|
||
│ │
|
||
│ System Message (已包含 system + topology): │
|
||
│ - System prompt (base): ~1,000 tokens │
|
||
│ - Topology info: ~3,000 tokens │
|
||
│ = 4,000 tokens (合并后) │
|
||
│ │
|
||
│ 可用于历史 = 7,738 - 4,000 = 3,738 tokens │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
第四步:裁剪对话历史
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 历史消息: 5,000 tokens → 需要裁剪到 3,738 tokens │
|
||
│ │
|
||
│ 丢弃旧消息,直到满足限制 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 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 > 预算 | 警告日志,建议增加 context_limit 或减少工具数量 |
|
||
| 历史全被裁剪 | 保留最后1条用户消息 |
|
||
|
||
**重要提示**:
|
||
- 当 system + topology 超出可用预算时,**两者都会被保留**
|
||
- 无法只丢弃 topology 而保留 system prompt(因为已合并)
|
||
- 建议在 system prompt 中精简 topology 信息或使用更短的 system prompt
|
||
|
||
### 2. 集成到 GNS3 Copilot
|
||
|
||
**修改文件**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
|
||
|
||
#### 改动内容
|
||
|
||
**导入新模块**:
|
||
```python
|
||
from gns3server.agent.gns3_copilot.agent.context_manager import (
|
||
prepare_context_messages,
|
||
)
|
||
```
|
||
|
||
**替换消息构建逻辑**:
|
||
```python
|
||
# 旧代码(手动构建)
|
||
full_messages = (
|
||
[SystemMessage(content=current_prompt)]
|
||
+ context_messages
|
||
+ state["messages"]
|
||
)
|
||
|
||
# 新代码(自动裁剪)
|
||
full_messages = prepare_context_messages(
|
||
state_messages=state["messages"],
|
||
system_prompt=current_prompt,
|
||
topology_context=topology_context,
|
||
model_name=llm_config.get("model", "default"),
|
||
llm_config=llm_config, # ← 包含 context_limit 和 context_strategy
|
||
)
|
||
```
|
||
|
||
### 3. Token 计数实现细节
|
||
|
||
系统使用 **tiktoken** 进行准确的 Token 计数:
|
||
|
||
#### 3.1 Token 计数器
|
||
|
||
**使用的编码**:`cl100k_base` (GPT-4)
|
||
|
||
**支持的模型**:
|
||
- ✅ OpenAI (GPT-4, GPT-3.5)
|
||
- ✅ Anthropic (Claude 系列)
|
||
- ✅ DeepSeek (deepseek-chat, deepseek-coder)
|
||
- ✅ 大多数基于 GPT-4 架构的模型
|
||
|
||
**准确率**:
|
||
- 英文:95%+
|
||
- 中文:95%+
|
||
- 代码:90-95%
|
||
|
||
#### 3.2 工具定义 Token 估算
|
||
|
||
```python
|
||
def estimate_tool_tokens(tools: list[Any]) -> int:
|
||
"""动态序列化工具 schema 并计算 token"""
|
||
for tool in tools:
|
||
tool_schema = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": tool.name,
|
||
"description": tool.description,
|
||
"parameters": tool.args_schema.schema()
|
||
}
|
||
}
|
||
# 使用 tiktoken 计数
|
||
schema_str = json.dumps(tool_schema, ensure_ascii=False)
|
||
tool_tokens = len(encoding.encode(schema_str))
|
||
```
|
||
|
||
**典型消耗**:
|
||
- 简单工具(1-2 个参数):500-800 tokens
|
||
- 中等工具(3-5 个参数):800-1200 tokens
|
||
- 复杂工具(嵌套参数):1200-1500 tokens
|
||
- 8 个工具:约 6000-10000 tokens
|
||
|
||
---
|
||
|
||
## 策略详细说明
|
||
|
||
### Conservative(保守策略)
|
||
|
||
**参数配置**:
|
||
```json
|
||
{
|
||
"context_limit": 128,
|
||
"context_strategy": "conservative"
|
||
}
|
||
```
|
||
|
||
**实际效果**:
|
||
- **使用比例**:60% (context_limit × 0.6)
|
||
- **计算公式**:`max_tokens = context_limit × 1000 × 0.6`
|
||
- **示例**:对于 128K 上下文限制
|
||
- 保留空间:`128 × 1000 × 0.6 = 76,800` tokens 用于输入
|
||
- 预留空间:`128,000 - 76,800 = 51,200` tokens 用于输出
|
||
|
||
**适用场景**:
|
||
1. **长输出任务**
|
||
- 代码生成(可能生成数百行代码)
|
||
- 文章写作(需要完整文章内容)
|
||
- 详细报告生成(包含多个章节)
|
||
|
||
2. **复杂任务**
|
||
- 多步骤推理任务
|
||
- 需要深度分析的请求
|
||
- 综合性问题的解决方案
|
||
|
||
3. **不确定输出大小时**
|
||
- 不确定 LLM 会返回多长的内容
|
||
- 首次尝试某种类型的任务
|
||
- 需要额外安全边界的场景
|
||
|
||
**优缺点**:
|
||
- ✅ **优点**:输出更不容易被截断,安全性高
|
||
- ❌ **缺点**:输入上下文较少,可能遗漏早期信息
|
||
|
||
**日志示例**:
|
||
```
|
||
INFO: Context prepared: 20 msgs, ~72800 tokens / 128K limit (56.9%), strategy=conservative
|
||
INFO: Available for output: ~51200 tokens
|
||
```
|
||
|
||
---
|
||
|
||
### Balanced(平衡策略,推荐)
|
||
|
||
**参数配置**:
|
||
```json
|
||
{
|
||
"context_limit": 128,
|
||
"context_strategy": "balanced"
|
||
}
|
||
```
|
||
|
||
**实际效果**:
|
||
- **使用比例**:75% (context_limit × 0.75)
|
||
- **计算公式**:`max_tokens = context_limit × 1000 × 0.75`
|
||
- **示例**:对于 128K 上下文限制
|
||
- 保留空间:`128 × 1000 × 0.75 = 96,000` tokens 用于输入
|
||
- 预留空间:`128,000 - 96,000 = 32,000` tokens 用于输出
|
||
|
||
**适用场景**:
|
||
1. **一般对话**
|
||
- 日常聊天交互
|
||
- 问答式对话
|
||
- 技术支持和咨询
|
||
|
||
2. **大多数场景**
|
||
- 网络配置命令生成
|
||
- 故障排查建议
|
||
- 知识问答
|
||
|
||
3. **平衡输入和输出**
|
||
- 需要较多上下文,但输出也较长的场景
|
||
- 中等复杂度的任务
|
||
|
||
**优缺点**:
|
||
- ✅ **优点**:在输入上下文和输出空间之间取得良好平衡
|
||
- ✅ **优点**:适用于大多数使用场景
|
||
- ⚠️ **注意**:对于特别长的输出可能被截断
|
||
|
||
**日志示例**:
|
||
```
|
||
INFO: Context prepared: 30 msgs, ~85600 tokens / 128K limit (66.9%), strategy=balanced
|
||
INFO: Available for output: ~32000 tokens
|
||
```
|
||
|
||
---
|
||
|
||
### Aggressive(激进策略)
|
||
|
||
**参数配置**:
|
||
```json
|
||
{
|
||
"context_limit": 128,
|
||
"context_strategy": "aggressive"
|
||
}
|
||
```
|
||
|
||
**实际效果**:
|
||
- **使用比例**:85% (context_limit × 0.85)
|
||
- **计算公式**:`max_tokens = context_limit × 1000 × 0.85`
|
||
- **示例**:对于 128K 上下文限制
|
||
- 保留空间:`128 × 1000 × 0.85 = 108,800` tokens 用于输入
|
||
- 预留空间:`128,000 - 108,800 = 19,200` tokens 用于输出
|
||
|
||
**适用场景**:
|
||
1. **简短输出任务**
|
||
- 是/否判断
|
||
- 简短回答确认
|
||
- 状态查询类请求
|
||
|
||
2. **分析类任务**
|
||
- 日志分析(输出分析结果,但不需要很长)
|
||
- 数据解读(输出简洁的结论)
|
||
- 配置检查(返回 OK 或简短说明)
|
||
|
||
3. **输出比较确定的场景**
|
||
- 明确知道输出会很短
|
||
- 只需要简单确认或状态
|
||
- 不需要长篇解释的任务
|
||
|
||
**优缺点**:
|
||
- ✅ **优点**:最大化输入上下文,保留更多历史信息
|
||
- ❌ **缺点**:输出容易被截断,不适用于长输出任务
|
||
|
||
**日志示例**:
|
||
```
|
||
INFO: Context prepared: 50 msgs, ~105200 tokens / 128K limit (82.2%), strategy=aggressive
|
||
INFO: Available for output: ~19200 tokens
|
||
```
|
||
|
||
---
|
||
|
||
## 策略对比总结
|
||
|
||
| 特性 | Conservative | Balanced | Aggressive |
|
||
|------|-------------|----------|------------|
|
||
| **输入比例** | 60% | 75% | 85% |
|
||
| **输出预留** | 40% | 25% | 15% |
|
||
| **上下文数量** | 最少 | 中等 | 最多 |
|
||
| **输出空间** | 最大 | 中等 | 最小 |
|
||
| **适用性** | 长输出 | 一般使用 | 短输出 |
|
||
| **风险** | 上下文不足 | 平衡 | 输出截断 |
|
||
|
||
**选择建议流程**:
|
||
```
|
||
1. 任务类型是什么?
|
||
├─ 代码生成/长文档 → Conservative
|
||
├─ 日常对话/一般任务 → Balanced (推荐)
|
||
└─ 简短确认/分析 → Aggressive
|
||
|
||
2. 输出长度预估?
|
||
├─ 不确定/可能很长 → Conservative
|
||
├─ 中等长度 → Balanced
|
||
└─ 很短/简洁 → Aggressive
|
||
|
||
3. 上下文重要性?
|
||
├─ 早期历史不太重要 → Aggressive (更多上下文)
|
||
├─ 需要平衡 → Balanced
|
||
└─ 重点是输出完整性 → Conservative (更多输出空间)
|
||
```
|
||
|
||
## 工作流程
|
||
|
||
```
|
||
用户发送消息
|
||
↓
|
||
获取消息历史 state["messages"]
|
||
↓
|
||
构建系统提示 + 拓扑信息
|
||
↓
|
||
调用 prepare_context_messages()
|
||
↓
|
||
├─ 估算 token 数量
|
||
├─ 获取模型上下文限制
|
||
├─ 判断是否需要裁剪
|
||
└─ 如果需要 → 调用 trim_messages()
|
||
↓
|
||
调用 LLM(带裁剪后的消息)
|
||
↓
|
||
返回响应
|
||
```
|
||
|
||
## 日志输出示例
|
||
|
||
### 正常情况(使用 tiktoken,包含工具定义)
|
||
```
|
||
INFO: Using tiktoken (cl100k_base) for accurate token counting
|
||
INFO: Tool definitions estimated at ~8500 total tokens (8 tools)
|
||
INFO: Using database config context limit: 128000 tokens for model 'gpt-4o'
|
||
INFO: Context prepared: 15 msgs, ~28432 tokens (messages) + 8500 tokens (tools) = 36932 total / 128K limit (28.9%), strategy=balanced
|
||
INFO: LLM call completed: tool_calls=2
|
||
```
|
||
|
||
### 发生裁剪时
|
||
```
|
||
INFO: Using tiktoken (cl100k_base) for accurate token counting
|
||
INFO: Tool definitions estimated at ~8500 total tokens (8 tools)
|
||
INFO: Using database config context limit: 128000 tokens for model 'gpt-4o'
|
||
INFO: Trimming messages: 95000 → 82000 tokens (model: gpt-4o)
|
||
INFO: Trimmed 50 → 25 messages
|
||
INFO: Context prepared: 27 msgs, ~82000 tokens (messages) + 8500 tokens (tools) = 90500 total / 128K limit (70.7%), strategy=balanced
|
||
```
|
||
|
||
### 配置错误时
|
||
```
|
||
ERROR: context_limit is required but not provided for model 'gpt-4o'.
|
||
Please configure context_limit in your LLM model configuration.
|
||
Refer to the model provider's documentation for the current context window size.
|
||
```
|
||
|
||
### 日志格式说明
|
||
|
||
新的日志格式(包含工具定义和 topology 分解):
|
||
```
|
||
Context prepared: system={总tokens} (base={base_tokens} + topology={topology_tokens}) + history={history_tokens} + tools={tool_tokens} = {总计} total / {限制}K limit ({使用百分比}%), strategy={策略}
|
||
```
|
||
|
||
字段说明:
|
||
- **system**:System Message 的总 token 数(包含 system prompt + topology)
|
||
- **base**:基础 system prompt 的 token 数(不含 topology)
|
||
- **topology**:Topology info 的 token 数
|
||
- **history**:对话历史(HumanMessage + AIMessage + ToolMessage)的 token 数
|
||
- **tools**:工具定义(schema)的 token 数
|
||
- **总计**:所有部分的 token 总和
|
||
- **限制**:模型的上下文窗口大小(K tokens)
|
||
- **使用百分比**:总计 tokens / 限制 × 100%
|
||
- **策略**:使用的裁剪策略(conservative/balanced/aggressive)
|
||
|
||
**示例输出**:
|
||
```
|
||
Context prepared: system=6124 (base=2804 + topology=3320) + history=11800 + tools=1862 = 19786 total / 128K limit (15.5%), strategy=balanced
|
||
```
|
||
|
||
这表示:
|
||
- System Message 总共 6124 tokens
|
||
- 其中基础 system prompt 2804 tokens
|
||
- 其中 topology info 3320 tokens
|
||
- 对话历史 11800 tokens
|
||
- 工具定义 1862 tokens
|
||
- 总计 19786 tokens,使用 128K 限制的 15.5%
|
||
|
||
---
|
||
|
||
## Token 计数准确性
|
||
|
||
### 为什么估算值和实际值可能不同?
|
||
|
||
#### 1. **工具定义的影响**
|
||
|
||
每次 LLM 调用时,工具定义会被转换为 JSON 并发送给 LLM:
|
||
|
||
```json
|
||
{
|
||
"type": "function",
|
||
"function": {
|
||
"name": "ExecuteMultipleDeviceCommands",
|
||
"description": "在多个网络设备上执行命令...",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"commands": {...}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**典型消耗**:
|
||
- 简单工具:500-800 tokens
|
||
- 复杂工具:1000-1500 tokens
|
||
- 8 个工具:约 6000-12000 tokens
|
||
|
||
**系统处理**:
|
||
- ✅ 现在会自动估算工具定义的 tokens
|
||
- ✅ 日志中会显示工具 tokens
|
||
- ✅ 裁剪决策会考虑工具定义
|
||
|
||
#### 2. **中文内容的 Tokenization**
|
||
|
||
不同语言对 token 的使用效率不同:
|
||
|
||
| 语言 | 平均字符/token | 示例 |
|
||
|------|---------------|------|
|
||
| 英文 | 3-4 字符 | "Hello world" ≈ 2-3 tokens |
|
||
| 中文 | 1-1.5 字符 | "你好世界" ≈ 3-4 tokens |
|
||
| 代码 | 2-3 字符 | `print("Hello")` ≈ 4-5 tokens |
|
||
|
||
**使用 tiktoken 后**:
|
||
- 对中文的准确率从 30-40% 提升到 95%+
|
||
- 对英文的准确率保持在 95%+
|
||
- 对代码的准确率约 90-95%
|
||
|
||
#### 3. **System Prompt 和 Topology Context**
|
||
|
||
- **System Prompt**: 固定内容,约 1000-2000 tokens
|
||
- **Topology Context**: 动态内容,取决于拓扑大小
|
||
- 小型拓扑(< 10 节点):约 500-1000 tokens
|
||
- 中型拓扑(10-50 节点):约 2000-5000 tokens
|
||
- 大型拓扑(> 50 节点):约 5000-10000+ tokens
|
||
|
||
这些都会被准确计数并计入总限制。
|
||
|
||
---
|
||
|
||
## Token 使用对比
|
||
|
||
### 改进前(LangChain 估算)
|
||
|
||
```
|
||
INFO: Context prepared: 29 msgs, ~11523 tokens / 128K limit (9.0%), strategy=conservative
|
||
实际发送: 39700 tokens
|
||
差距: 28177 tokens (2.4x 低估)
|
||
```
|
||
|
||
**问题**:
|
||
1. ❌ 工具定义没有被计入(约 8000-12000 tokens)
|
||
2. ❌ 中文内容被低估(约 2.4x 误差)
|
||
3. ❌ 总体低估约 2-3 倍
|
||
|
||
### 改进后(tiktoken + 工具定义)
|
||
|
||
```
|
||
INFO: Using tiktoken (cl100k_base) for accurate token counting
|
||
INFO: Tool definitions estimated at ~8500 total tokens (8 tools)
|
||
INFO: Context prepared: 29 msgs, ~27500 tokens (messages) + 8500 tokens (tools) = 36000 total / 128K limit (28.1%), strategy=conservative
|
||
实际发送: 39700 tokens
|
||
差距: 3700 tokens (1.1x 误差)
|
||
```
|
||
|
||
**改进**:
|
||
1. ✅ 工具定义被准确估算
|
||
2. ✅ 中文内容准确率提升到 95%+
|
||
3. ✅ 总体误差降低到 10% 以内
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
### tiktoken 未安装
|
||
|
||
如果 tiktoken 未安装,系统将在首次尝试计数 tokens 时抛出错误:
|
||
|
||
```python
|
||
ImportError: tiktoken is required for accurate token counting.
|
||
Please install it with: pip install tiktoken>=0.8.0
|
||
```
|
||
|
||
**解决方法**:
|
||
```bash
|
||
pip install tiktoken>=0.8.0
|
||
```
|
||
|
||
### Token 计数失败
|
||
```python
|
||
try:
|
||
tokens = count_messages_tokens(messages)
|
||
except ImportError as e:
|
||
logger.error("tiktoken not available: %s", e)
|
||
raise # Re-raise to fail fast
|
||
except Exception as e:
|
||
logger.error("Failed to count tokens: %s", e)
|
||
raise
|
||
```
|
||
|
||
### 裁剪失败
|
||
```python
|
||
try:
|
||
trimmed = trim_messages(...)
|
||
except Exception as e:
|
||
logger.error("Failed to trim: %s", e)
|
||
# 回退到简单的切片操作
|
||
return system_msgs + other_msgs[-N:]
|
||
```
|
||
|
||
## 参考资源
|
||
|
||
- [tiktoken (OpenAI Tokenizer)](https://github.com/openai/tiktoken)
|
||
- [LangChain Messages Utils](https://python.langchain.com/docs/messages/)
|
||
- [LangGraph Memory Management](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#memory)
|
||
- [OpenAI Models Context Limits](https://platform.openai.com/docs/models)
|
||
- [Anthropic Models Context Limits](https://docs.anthropic.com/claude/docs/models-overview)
|
||
|
||
## 相关源文件
|
||
|
||
- `gns3server/agent/gns3_copilot/agent/context_manager.py` - 上下文管理核心逻辑
|
||
- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` - LLM 调用节点
|
||
- `gns3server/agent/gns3_copilot/agent_service.py` - Agent 服务接口
|
||
|
||
## 总结
|
||
|
||
通过使用 LangChain/LangGraph 的内置功能和自定义优化,我们实现了:
|
||
1. ✅ 智能裁剪消息历史,避免超限
|
||
2. ✅ **必须手动配置 context_limit**,确保使用正确的上下文窗口大小
|
||
3. ✅ 可配置的裁剪策略(保守/平衡/激进)
|
||
4. ✅ 始终保留系统消息(包含 system prompt 和 topology)
|
||
5. ✅ 详细的日志输出,便于调试
|
||
6. ✅ 优雅的错误处理和明确的错误提示
|
||
7. ✅ 提供参考工具,帮助查找常见模型的上下文限制
|
||
8. ✅ **使用 tiktoken 进行准确的 token 计数**(准确率 95%+)
|
||
9. ✅ **自动估算工具定义的 token 消耗**
|
||
10. ✅ **支持中文、英文、代码等多种内容类型**
|
||
11. ✅ **使用模板变量动态注入 topology info** (新增)
|
||
12. ✅ **日志中详细显示 system/topology/history 分解** (新增)
|
||
|
||
### 关键优势
|
||
|
||
- **准确性**:
|
||
- 用户从官方文档获取最新的上下文限制,避免使用过时数据
|
||
- tiktoken 提供准确的 token 计数,误差 < 10%
|
||
- 工具定义 token 被正确计入估算
|
||
|
||
- **灵活性**:
|
||
- 每个配置独立设置,支持不同用户使用不同限制
|
||
- 自动适配不同语言和内容类型
|
||
- 模板变量注入方式易于维护和调整
|
||
|
||
- **明确性**:
|
||
- 缺少配置时立即报错,避免静默失败
|
||
- 详细日志显示所有 token 消耗(system base + topology + history + tools)
|
||
- 清楚显示各部分的 token 分解
|
||
|
||
- **可维护性**:
|
||
- 无需维护内置默认值,减少代码维护负担
|
||
- 模板变量注入方式,system prompt 和 topology 管理更清晰
|
||
- 模块化设计,易于扩展和调试
|
||
|
||
- **可观测性**:
|
||
- 详细日志显示上下文使用情况和裁剪决策
|
||
- 区分 system base tokens、topology tokens、history tokens 和工具 tokens
|
||
- 显示使用百分比和策略选择
|
||
|
||
这个实现确保了即使在进行长对话时,系统也不会因为上下文溢出而失败。
|