Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/majiayu000/sage --skill sage-context-management명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | sage-context-management |
| description | Sage 上下文管理开发指南,涵盖 Token 估算、消息裁剪、自动压缩、摘要生成 |
| when_to_use | 当涉及上下文窗口管理、Token 计数、消息裁剪、Auto-Compact 时使用 |
| allowed_tools | ["Read","Grep","Glob","Edit","Write","Bash"] |
| user_invocable | true |
| priority | 89 |
上下文模块管理 LLM 对话的上下文窗口,代码量 4029 行,包含:
crates/sage-core/src/context/
├── mod.rs # 公开接口 (68行)
├── config.rs # 配置 (263行)
├── estimator.rs # Token 估算器 (223行)
├── pruner.rs # 消息裁剪器 (409行)
├── summarizer.rs # 摘要生成器 (359行)
├── compact.rs # 压缩操作 (459行)
├── manager/ # 上下文管理器
│ ├── mod.rs # 入口
│ ├── core.rs # 核心实现 (148行)
│ ├── operations.rs # 操作方法 (180行)
│ ├── types.rs # 类型定义 (67行)
│ └── tests.rs # 测试
├── auto_compact/ # 自动压缩(学习自 Claude Code)
│ ├── mod.rs # 入口
│ ├── config.rs # 配置
│ ├── manager.rs # 管理器
│ ├── operations.rs # 操作
│ ├── partition.rs # 分区
│ ├── result.rs # 结果
│ ├── stats.rs # 统计
│ ├── summary.rs # 摘要 (142行)
│ └── tests.rs # 测试
└── streaming/ # 流式计数
├── mod.rs # 入口
├── counter.rs # 计数器 (157行)
├── metrics.rs # 指标 (163行)
├── types.rs # 类型 (79行)
└── tests.rs # 测试
// crates/sage-core/src/context/manager/core.rs
#[derive(Clone)]
pub struct ContextManager {
/// 配置
pub(super) config: ContextConfig,
/// Token 估算器
pub(super) estimator: TokenEstimator,
/// 消息裁剪器
pub(super) pruner: MessagePruner,
/// 摘要生成器
pub(super) summarizer: ConversationSummarizer,
}
impl ContextManager {
/// 创建管理器
pub fn new(config: ContextConfig) -> Self {
let estimator = TokenEstimator::new();
let pruner = MessagePruner::new(config.clone());
let summarizer = ConversationSummarizer::new();
Self { config, estimator, pruner, summarizer }
}
/// 针对特定 Provider 优化
pub fn for_provider(provider: &str, model: &str) -> Self {
let config = ContextConfig::for_provider(provider, model);
= TokenEstimator::for_provider(provider);
}
(&, messages: &[LlmMessage]) {
.estimator.(messages)
}
(&, messages: &[LlmMessage]) {
= .estimator.(messages);
current_tokens >= .config.()
}
(&, messages: &[LlmMessage]) ContextUsageStats {
= .estimator.(messages);
= .config.max_context_tokens;
ContextUsageStats {
current_tokens,
max_tokens,
usage_percentage: (current_tokens / max_tokens ) * ,
messages_count: messages.(),
}
}
}
// crates/sage-core/src/context/config.rs
#[derive(Debug, Clone)]
pub struct ContextConfig {
/// 最大上下文 Token 数
pub max_context_tokens: usize,
/// 触发裁剪的阈值比例 (0.0-1.0)
pub threshold_ratio: f32,
/// 溢出处理策略
pub overflow_strategy: OverflowStrategy,
/// 保留的系统消息数
pub preserve_system_messages: bool,
/// 保留的最近消息数
pub preserve_recent_count: usize,
/// 为响应预留的 Token 数
pub reserved_for_response: usize,
}
#[derive(Debug, Clone, Copy)]
pub enum OverflowStrategy {
/// 裁剪旧消息
Prune,
/// 生成摘要
Summarize,
/// 自动压缩(Claude Code 风格)
AutoCompact,
/// 返回错误
Error,
}
impl ContextConfig {
/// 获取特定 Provider/Model 的配置
pub fn for_provider(provider: &str, model: &str) -> Self {
match provider {
"anthropic" => match model {
m if m.contains("opus") => Self::claude_opus(),
m m.() => ::(),
_ => ::(),
},
=> model {
m m.() => ::(),
m m.() => ::(),
_ => ::(),
},
_ => ::(),
}
}
(&) {
(.max_context_tokens * .threshold_ratio)
}
}
// crates/sage-core/src/context/estimator.rs
pub struct TokenEstimator {
/// 每字符平均 Token 数
chars_per_token: f32,
/// Provider 特定调整
provider_overhead: usize,
}
impl TokenEstimator {
/// 估算消息 Token 数
pub fn estimate_message(&self, message: &LlmMessage) -> usize {
let content_tokens = self.estimate_text(&message.content);
let role_tokens = 4; // 角色标记开销
content_tokens + role_tokens + self.provider_overhead
}
/// 估算对话总 Token 数
pub fn estimate_conversation(&self, messages: &[LlmMessage]) -> usize {
messages.iter().map(|m| self.estimate_message(m)).sum()
}
/// 估算请求总 Token 数(含工具)
pub fn estimate_request(
&self,
messages: &[LlmMessage],
tools: Option<&[ToolSchema]>,
) -> usize {
let = .(messages);
= tools
.(|t| .(t))
.();
conversation_tokens + tool_tokens
}
(&, text: &) {
(text.() / .chars_per_token).()
}
}
impl TokenEstimator {
pub fn for_provider(provider: &str) -> Self {
match provider {
"anthropic" => Self {
chars_per_token: 4.0,
provider_overhead: 8,
},
"openai" => Self {
chars_per_token: 4.0,
provider_overhead: 4,
},
"google" => Self {
chars_per_token: 4.5,
provider_overhead: 6,
},
_ => Self::default(),
}
}
}
// crates/sage-core/src/context/pruner.rs
pub struct MessagePruner {
config: ContextConfig,
}
impl MessagePruner {
/// 裁剪消息到目标 Token 数
pub fn prune(&self, messages: Vec<LlmMessage>, target_tokens: usize) -> PruneResult {
let mut result = messages.clone();
let mut removed_count = 0;
// 1. 保留系统消息
let system_messages: Vec<_> = result
.iter()
.filter(|m| m.role == MessageRole::System)
.cloned()
.collect();
// 2. 保留最近消息
let recent_count = self.config.preserve_recent_count;
let recent_messages: Vec<_> = result
.iter()
.rev()
.take(recent_count)
.cloned()
.collect();
// 3. 从中间裁剪
while self.estimate_tokens(&result) > target_tokens {
result.() <= recent_count + system_messages.() {
;
}
= result.().(|m| m.role != MessageRole::System);
(i) = idx {
result.(i);
removed_count += ;
}
}
PruneResult {
messages: result,
removed_count,
tokens_saved: .(&messages) - .(&result),
}
}
}
{
messages: <LlmMessage>,
removed_count: ,
tokens_saved: ,
}
当上下文接近限制时,自动压缩历史对话为摘要:
原始对话:
[System] You are a helpful assistant
[User] Hello
[Assistant] Hi there!
[User] What is Rust?
[Assistant] Rust is a systems programming language...
[User] Show me an example
[Assistant] Here's a simple example...
... (更多消息) ...
压缩后:
[System] You are a helpful assistant
[Compact Boundary] <summary of previous conversation>
[User] (最近的消息)
[Assistant] (最近的响应)
// crates/sage-core/src/context/auto_compact/config.rs
pub struct AutoCompactConfig {
/// 是否启用
pub enabled: bool,
/// 最大上下文 Token 数
pub max_context_tokens: usize,
/// 触发压缩的比例
pub compact_threshold_ratio: f32,
/// 为响应预留的 Token 数
pub reserved_for_response: usize,
/// 目标压缩后大小比例
pub target_ratio: f32,
}
impl AutoCompactConfig {
/// 从环境变量覆盖
pub fn with_env_override(mut self) -> Self {
if let Ok(val) = std::env::var(AUTOCOMPACT_PCT_OVERRIDE_ENV) {
if let Ok(pct) = val.parse::<f32>() {
self.compact_threshold_ratio = pct / 100.0;
}
}
self
}
}
// crates/sage-core/src/context/auto_compact/manager.rs
pub struct AutoCompact {
config: AutoCompactConfig,
llm_client: Option<Arc<LlmClient>>,
stats: AutoCompactStats,
}
impl AutoCompact {
/// 检查是否需要压缩
pub fn needs_compaction(&self, messages: &[LlmMessage]) -> bool {
if !self.config.enabled {
return false;
}
// 只考虑上次压缩边界之后的消息
let active_messages = slice_from_last_compact_boundary(messages);
let current_tokens = self.estimate_tokens(&active_messages);
current_tokens >= self.config.threshold_tokens()
}
/// 检查并自动压缩
pub async fn check_and_compact(
&mut self,
messages: &mut Vec<LlmMessage>,
) -> SageResult<CompactResult> {
if !self.needs_compaction(messages) {
self.stats.skipped_count += 1;
return Ok(CompactResult::Skipped);
}
= .(messages).?;
.stats.compact_count += ;
.stats.tokens_saved += result.tokens_saved;
(result)
}
(&, messages: & <LlmMessage>) SageResult<CompactResult> {
= (messages);
= boundary_idx.(|i| i + ).();
(to_compact, to_keep) = partition::(
&messages[active_start..],
.config.preserve_recent_count,
);
= (client) = &.llm_client {
summary::(client, &to_compact).?
} {
summary::(&to_compact)
};
= (&summary);
= &messages[..active_start];
= [
preserved,
&[compact_boundary],
&to_keep,
].();
= .(messages);
= .(&new_messages);
*messages = new_messages;
(CompactResult::Compacted {
messages_compacted: to_compact.(),
tokens_saved: tokens_before - tokens_after,
})
}
}
// crates/sage-core/src/context/compact.rs
pub const COMPACT_BOUNDARY_KEY: &str = "compact_boundary";
pub const COMPACT_SUMMARY_KEY: &str = "compact_summary";
pub const COMPACT_TIMESTAMP_KEY: &str = "compact_timestamp";
pub const COMPACT_ID_KEY: &str = "compact_id";
/// 创建压缩边界消息
pub fn create_compact_boundary(summary: &str) -> LlmMessage {
let mut metadata = HashMap::new();
metadata.insert(COMPACT_BOUNDARY_KEY.to_string(), json!(true));
metadata.insert(COMPACT_SUMMARY_KEY.to_string(), json!(summary));
metadata.insert(COMPACT_TIMESTAMP_KEY.to_string(), json!(Utc::now().to_rfc3339()));
metadata.insert(COMPACT_ID_KEY.to_string(), json!(Uuid::new_v4().to_string()));
LlmMessage::assistant(format!(
"[Previous conversation summary]\n\n{}\n\n[End of summary]",
summary
)).with_metadata(metadata)
}
(message: &LlmMessage) {
message.metadata
.(COMPACT_BOUNDARY_KEY)
.(|v| v.())
.()
}
(messages: &[LlmMessage]) <> {
messages.().(is_compact_boundary)
}
(messages: &[LlmMessage]) &[LlmMessage] {
(messages) {
(idx) => &messages[idx + ..],
=> messages,
}
}
// crates/sage-core/src/context/summarizer.rs
pub struct ConversationSummarizer {
llm_client: Option<Arc<LlmClient>>,
}
impl ConversationSummarizer {
/// 使用 LLM 生成摘要
pub async fn summarize(&self, messages: &[LlmMessage]) -> SageResult<String> {
if let Some(client) = &self.llm_client {
self.summarize_with_llm(client, messages).await
} else {
Ok(self.simple_summarize(messages))
}
}
/// LLM 摘要
async fn summarize_with_llm(
&self,
client: &LlmClient,
messages: &[LlmMessage],
) -> SageResult<String> {
let prompt = build_summary_prompt(messages);
let response = client.chat(&[LlmMessage::user(prompt)], None).await?;
Ok(response.content)
}
/// 简单摘要(无 LLM)
fn simple_summarize(&, messages: &[LlmMessage]) {
= messages.().(|m| m.role == MessageRole::User).();
= messages.().(|m| m.role == MessageRole::Assistant).();
(
,
user_count, assistant_count
)
}
}
// crates/sage-core/src/context/streaming/counter.rs
pub struct StreamingTokenCounter {
estimator: TokenEstimator,
input_tokens: AtomicUsize,
output_tokens: AtomicUsize,
}
impl StreamingTokenCounter {
/// 记录输入 Token
pub fn record_input(&self, text: &str) {
let tokens = self.estimator.estimate_text(text);
self.input_tokens.fetch_add(tokens, Ordering::SeqCst);
}
/// 记录输出 Token
pub fn record_output(&self, chunk: &str) {
let tokens = self.estimator.estimate_text(chunk);
self.output_tokens.fetch_add(tokens, Ordering::SeqCst);
}
/// 获取统计
pub fn get_stats(&self) -> StreamingStats {
StreamingStats {
input_tokens: self.input_tokens.load(Ordering::SeqCst),
output_tokens: self.output_tokens.load(Ordering::SeqCst),
}
}
}
use sage_core::context::{ContextManager, ContextConfig};
// 创建管理器
let config = ContextConfig::for_provider("anthropic", "claude-3.5-sonnet");
let manager = ContextManager::new(config);
// 检查上下文使用
let stats = manager.get_usage_stats(&messages);
println!("Usage: {:.1}%", stats.usage_percentage);
// 检查是否需要处理
if manager.is_approaching_limit(&messages) {
let pruned = manager.prune(messages.clone(), 8000);
println!("Removed {} messages", pruned.removed_count);
}
use sage_core::context::{AutoCompact, AutoCompactConfig};
let config = AutoCompactConfig::default()
.with_env_override();
let mut auto_compact = AutoCompact::with_llm_client(config, llm_client);
// 每次 LLM 调用前检查
let result = auto_compact.check_and_compact(&mut messages).await?;
match result {
CompactResult::Compacted { messages_compacted, tokens_saved } => {
println!("Compacted {} messages, saved {} tokens", messages_compacted, tokens_saved);
}
CompactResult::Skipped => {
// 不需要压缩
}
}
sage-llm-integration - LLM 客户端(用于摘要生成)sage-session-management - 会话管理(消息存储)sage-agent-execution - Agent 执行(上下文集成)最后更新: 2026-01-10