diff --git a/src/agentsight/dashboard/src/test/TokenSavingsPage.test.tsx b/src/agentsight/dashboard/src/test/TokenSavingsPage.test.tsx
index 45654a438..4f6363b61 100644
--- a/src/agentsight/dashboard/src/test/TokenSavingsPage.test.tsx
+++ b/src/agentsight/dashboard/src/test/TokenSavingsPage.test.tsx
@@ -91,6 +91,7 @@ describe('TokenSavingsPage', () => {
sessions: [],
summary: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, total_compounded_saved: 0, total_compounded_tool_saved: 0, total_compounded_mcp_saved: 0, compounded_savings_rate: 0 },
stats_available: false,
+ optimization_tips: [{ level: 'warning', title: '未检测到 Tokenless 组件', description: '未发现 stats.db,请确认 tokenless 组件已安装并启用。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
@@ -116,6 +117,7 @@ describe('TokenSavingsPage', () => {
],
},
stats_available: true,
+ optimization_tips: [{ level: 'success', title: '节省效果良好', description: '当前复合节省率 25.0%,已达到良好水平。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
@@ -153,6 +155,7 @@ describe('TokenSavingsPage', () => {
strategy_breakdown: [],
},
stats_available: true,
+ optimization_tips: [],
});
await act(async () => { renderPage(); });
await act(async () => {
@@ -180,6 +183,9 @@ describe('TokenSavingsPage', () => {
before_tokens: 400,
after_tokens: 100,
compounded_saved: 300,
+ compression_ratio: 75.0,
+ explanation: '工具输出优化: 原始 400 tokens → 100 tokens,压缩率 75.0%。后续 1 轮LLM调用均受益,复合节省 300 tokens。',
+ compounding_turns: 1,
before_text: 'long original text',
after_text: 'short text',
diff_lines: [],
@@ -198,19 +204,30 @@ describe('TokenSavingsPage', () => {
],
},
stats_available: true,
+ optimization_tips: [],
});
await act(async () => { renderPage(); });
await act(async () => {
fireEvent.click(screen.getByText('查询'));
});
- // Click session row to expand
- const row = screen.getByText('Expander').closest('tr');
+ // Click session row to expand - find the one inside the table (has
ancestor)
+ const allExpander = screen.getAllByText('Expander');
+ const row = allExpander.map(el => el.closest('tr')).find(tr => tr !== null);
await act(async () => {
fireEvent.click(row!);
});
- expect(screen.getByText('工具输出')).toBeInTheDocument();
- expect(screen.getByText('Schema 压缩')).toBeInTheDocument();
+ expect(screen.getAllByText('工具输出').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText('Schema 压缩').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('详情').length).toBeGreaterThanOrEqual(1);
+ // Click "详情" to expand DiffView and verify compression_ratio renders
+ const detailBtns = screen.getAllByText('详情');
+ await act(async () => {
+ fireEvent.click(detailBtns[0]);
+ });
+ // The explanation banner should show compression ratio text
+ expect(screen.getByText(/75\.0%/)).toBeInTheDocument();
+ // The explanation text itself
+ expect(screen.getByText(/压缩率/)).toBeInTheDocument();
});
it('should show savings rate badge as 优秀 when >= 30%', async () => {
@@ -227,6 +244,7 @@ describe('TokenSavingsPage', () => {
strategy_breakdown: [],
},
stats_available: true,
+ optimization_tips: [{ level: 'success', title: '节省效果优秀', description: '当前复合节省率 50.0%,表现优秀!继续保持当前配置。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
@@ -234,4 +252,78 @@ describe('TokenSavingsPage', () => {
});
expect(screen.getByText('优秀')).toBeInTheDocument();
});
+
+ it('should render optimization tips panel', async () => {
+ mockFetchTokenSavings.mockResolvedValue({
+ sessions: [],
+ summary: {
+ total_input_tokens: 5000,
+ total_output_tokens: 3000,
+ total_tokens: 8000,
+ total_compounded_saved: 200,
+ total_compounded_tool_saved: 200,
+ total_compounded_mcp_saved: 0,
+ compounded_savings_rate: 2.5,
+ },
+ stats_available: true,
+ optimization_tips: [
+ { level: 'warning', title: '节省率较低', description: '当前复合节省率不足 5%,建议检查 tokenless 配置。' },
+ { level: 'info', title: '建议开启 MCP 响应压缩', description: '当前仅有工具输出优化,未检测到 MCP 响应压缩。开启后可进一步降低 Token 消耗。' },
+ ],
+ });
+ await act(async () => { renderPage(); });
+ await act(async () => {
+ fireEvent.click(screen.getByText('查询'));
+ });
+ expect(screen.getByText('优化建议')).toBeInTheDocument();
+ expect(screen.getByText('节省率较低')).toBeInTheDocument();
+ expect(screen.getByText('建议开启 MCP 响应压缩')).toBeInTheDocument();
+ });
+
+ it('should render savings breakdown panel with top items', async () => {
+ mockFetchTokenSavings.mockResolvedValue({
+ sessions: [{
+ session_id: 'sess-breakdown',
+ agent_name: 'BreakdownAgent',
+ total_input_tokens: 3000,
+ total_output_tokens: 1500,
+ saved_tokens: 800,
+ compounded_saved: 800,
+ compounded_savings_rate: 17.8,
+ optimization_items: [
+ {
+ id: 'item-1',
+ category: 'tool_output',
+ before_tokens: 500,
+ after_tokens: 100,
+ saved_tokens: 400,
+ compounded_saved: 800,
+ compounding_turns: 2,
+ compression_ratio: 80.0,
+ explanation: '工具输出优化: 原始 500 tokens → 100 tokens,压缩率 80.0%。',
+ before_text: 'original',
+ after_text: 'compressed',
+ diff_lines: [],
+ },
+ ],
+ }],
+ summary: {
+ total_input_tokens: 3000,
+ total_output_tokens: 1500,
+ total_tokens: 4500,
+ total_compounded_saved: 800,
+ total_compounded_tool_saved: 800,
+ total_compounded_mcp_saved: 0,
+ compounded_savings_rate: 17.8,
+ },
+ stats_available: true,
+ optimization_tips: [],
+ });
+ await act(async () => { renderPage(); });
+ await act(async () => {
+ fireEvent.click(screen.getByText('查询'));
+ });
+ expect(screen.getByText('节省排行 Top 5(按复合节省量)')).toBeInTheDocument();
+ expect(screen.getAllByText('BreakdownAgent').length).toBeGreaterThanOrEqual(1);
+ });
});
diff --git a/src/agentsight/dashboard/src/utils/apiClient.ts b/src/agentsight/dashboard/src/utils/apiClient.ts
index 0dc43cf76..e7cb96508 100644
--- a/src/agentsight/dashboard/src/utils/apiClient.ts
+++ b/src/agentsight/dashboard/src/utils/apiClient.ts
@@ -211,6 +211,8 @@ export interface OptimizationItem {
saved_tokens: number;
compounded_saved: number;
compounding_turns: number;
+ compression_ratio: number;
+ explanation: string;
before_summary: string;
after_summary: string;
optimization_reason: string;
@@ -259,10 +261,17 @@ export interface SavingsSummary {
strategy_breakdown: StrategyBreakdownItem[];
}
+export interface OptimizationTip {
+ level: 'success' | 'info' | 'warning';
+ title: string;
+ description: string;
+}
+
export interface TokenSavingsResponse {
stats_available: boolean;
summary: SavingsSummary;
sessions: SessionSavings[];
+ optimization_tips: OptimizationTip[];
}
/**
diff --git a/src/agentsight/src/bin/cli/audit.rs b/src/agentsight/src/bin/cli/audit.rs
index b65bad063..b067f232b 100644
--- a/src/agentsight/src/bin/cli/audit.rs
+++ b/src/agentsight/src/bin/cli/audit.rs
@@ -222,6 +222,7 @@ mod tests {
args: Some(args.into()),
exit_code: None,
},
+ session_id: None,
}
}
diff --git a/src/agentsight/src/server/token_savings.rs b/src/agentsight/src/server/token_savings.rs
index 2a2d3230d..ee5a1a6bc 100644
--- a/src/agentsight/src/server/token_savings.rs
+++ b/src/agentsight/src/server/token_savings.rs
@@ -62,6 +62,8 @@ pub struct OptimizationItemDto {
pub saved_tokens: i64,
pub compounded_saved: i64,
pub compounding_turns: i64,
+ pub compression_ratio: f64,
+ pub explanation: String,
pub before_summary: String,
pub after_summary: String,
pub optimization_reason: String,
@@ -79,7 +81,7 @@ pub struct DiffLineDto {
}
/// Per-session savings data
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Clone)]
pub struct SessionSavingsDto {
pub session_id: String,
pub agent_name: String,
@@ -97,12 +99,21 @@ pub struct SessionSavingsDto {
pub optimization_items: Vec,
}
+/// An actionable optimization tip
+#[derive(Debug, Serialize, Clone, PartialEq)]
+pub struct OptimizationTip {
+ pub level: String,
+ pub title: String,
+ pub description: String,
+}
+
/// Full response for /api/token-savings
#[derive(Debug, Serialize)]
pub struct TokenSavingsResponse {
pub stats_available: bool,
pub summary: SavingsSummary,
pub sessions: Vec,
+ pub optimization_tips: Vec,
}
/// Response for /api/token-savings/session/{session_id}
@@ -318,6 +329,119 @@ fn now_ns() -> u64 {
.as_nanos() as u64
}
+/// Compute compression ratio clamped to [0.0, 100.0].
+/// Returns 0.0 when before_tokens is 0 or after >= before (no real compression).
+pub(crate) fn compute_compression_ratio(before_tokens: i64, after_tokens: i64) -> f64 {
+ if before_tokens > 0 {
+ let raw = (1.0 - after_tokens as f64 / before_tokens as f64) * 100.0;
+ raw.clamp(0.0, 100.0)
+ } else {
+ 0.0
+ }
+}
+
+/// Build a human-readable explanation string for an optimization item.
+pub(crate) fn build_explanation(
+ category: &str,
+ before_tokens: i64,
+ after_tokens: i64,
+ compression_ratio: f64,
+ compounding_turns: i64,
+ compounded: i64,
+) -> String {
+ if category == "mcp_response" {
+ format!(
+ "MCP响应压缩: 原始 {} tokens → {} tokens,压缩率 {:.1}%。后续 {} 轮LLM调用均受益,复合节省 {} tokens。",
+ before_tokens, after_tokens, compression_ratio, compounding_turns, compounded
+ )
+ } else {
+ format!(
+ "工具输出优化: 原始 {} tokens → {} tokens,压缩率 {:.1}%。后续 {} 轮LLM调用均受益,复合节省 {} tokens。",
+ before_tokens, after_tokens, compression_ratio, compounding_turns, compounded
+ )
+ }
+}
+
+/// Generate optimization tips based on aggregated savings data.
+pub(crate) fn generate_optimization_tips(
+ stats_available: bool,
+ grand_total: i64,
+ grand_compounded_rate: f64,
+ grand_compounded_tool_saved: i64,
+ grand_compounded_mcp_saved: i64,
+ sessions: &[SessionSavingsDto],
+) -> Vec {
+ let mut tips: Vec = Vec::new();
+
+ if !stats_available {
+ tips.push(OptimizationTip {
+ level: "warning".to_string(),
+ title: "未检测到 Tokenless 组件".to_string(),
+ description: "未发现 stats.db,请确认 tokenless 组件已安装并启用。启用后可自动压缩工具输出和 MCP 响应,显著降低 Token 消耗。".to_string(),
+ });
+ } else if grand_compounded_rate < 15.0 && grand_total > 0 {
+ tips.push(OptimizationTip {
+ level: "warning".to_string(),
+ title: "节省率较低".to_string(),
+ description: "当前复合节省率不足 15%,建议检查 tokenless 配置是否已对所有 Agent 生效,确保工具输出和 MCP 响应压缩均已开启。".to_string(),
+ });
+ }
+
+ if grand_compounded_tool_saved > 0 && grand_compounded_mcp_saved == 0 && grand_total > 0 {
+ tips.push(OptimizationTip {
+ level: "info".to_string(),
+ title: "建议开启 MCP 响应压缩".to_string(),
+ description:
+ "当前仅有工具输出优化,未检测到 MCP 响应压缩。开启后可进一步降低 Token 消耗。"
+ .to_string(),
+ });
+ }
+
+ if grand_compounded_mcp_saved > 0 && grand_compounded_tool_saved == 0 && grand_total > 0 {
+ tips.push(OptimizationTip {
+ level: "info".to_string(),
+ title: "建议开启工具输出优化".to_string(),
+ description:
+ "当前仅有 MCP 响应压缩,未检测到工具输出优化。开启后可进一步降低 Token 消耗。"
+ .to_string(),
+ });
+ }
+
+ let zero_savings_sessions = sessions
+ .iter()
+ .filter(|s| s.compounded_saved == 0 && s.total_tokens > 1000)
+ .count();
+ if zero_savings_sessions > 0 {
+ tips.push(OptimizationTip {
+ level: "info".to_string(),
+ title: format!("发现 {} 个未优化会话", zero_savings_sessions),
+ description: "部分会话消耗较高但无优化记录,可能是对应 Agent 未启用 tokenless 或工具调用较少。建议检查这些会话的 Agent 配置。".to_string(),
+ });
+ }
+
+ if grand_compounded_rate >= 30.0 {
+ tips.push(OptimizationTip {
+ level: "success".to_string(),
+ title: "节省效果优秀".to_string(),
+ description: format!(
+ "当前复合节省率 {:.1}%,表现优秀!继续保持当前配置。",
+ grand_compounded_rate
+ ),
+ });
+ } else if grand_compounded_rate >= 15.0 {
+ tips.push(OptimizationTip {
+ level: "success".to_string(),
+ title: "节省效果良好".to_string(),
+ description: format!(
+ "当前复合节省率 {:.1}%,已达到良好水平。可尝试调整压缩策略以进一步提升。",
+ grand_compounded_rate
+ ),
+ });
+ }
+
+ tips
+}
+
// ─── GET /api/token-savings ──────────────────────────────────────────────────
/// GET /api/token-savings?start_ns=&end_ns=&agent_name=
@@ -449,8 +573,6 @@ pub async fn get_token_savings(
let strategy_label = map_operation_to_strategy_label(&row.operation).to_string();
// FIX(#2): aggregate by strategy key so unknown ops merge into one slice.
- // Use operation name as key for known ops (frontend STRATEGY_CONFIG matches on this),
- // and "other" for unknown ops so they collapse into a single slice.
let strategy_key = match row.operation.as_str() {
"compress-response" | "compress-toon" | "rewrite-command"
| "compress-schema" => row.operation.clone(),
@@ -464,6 +586,17 @@ pub async fn get_token_savings(
entry.1 += saved;
entry.2 += compounded;
+ let compression_ratio =
+ compute_compression_ratio(row.before_tokens, row.after_tokens);
+ let explanation = build_explanation(
+ category,
+ row.before_tokens,
+ row.after_tokens,
+ compression_ratio,
+ compounding_turns,
+ compounded,
+ );
+
items.push(OptimizationItemDto {
id: row.tool_use_id.clone(),
category: category.to_string(),
@@ -475,6 +608,8 @@ pub async fn get_token_savings(
saved_tokens: saved,
compounded_saved: compounded,
compounding_turns,
+ compression_ratio,
+ explanation,
before_summary: format!("原始内容 {} tokens", row.before_tokens),
after_summary: format!("优化后 {} tokens", row.after_tokens),
optimization_reason,
@@ -567,7 +702,15 @@ pub async fn get_token_savings(
total_compounded_mcp_saved: grand_compounded_mcp_saved,
strategy_breakdown,
},
- sessions: resp_sessions,
+ sessions: resp_sessions.clone(),
+ optimization_tips: generate_optimization_tips(
+ stats_available,
+ grand_total,
+ grand_compounded_rate,
+ grand_compounded_tool_saved,
+ grand_compounded_mcp_saved,
+ &resp_sessions,
+ ),
})
}
@@ -671,6 +814,15 @@ pub async fn get_session_savings(
saved_tokens: saved,
compounded_saved: compounded,
compounding_turns,
+ compression_ratio: compute_compression_ratio(row.before_tokens, row.after_tokens),
+ explanation: build_explanation(
+ category,
+ row.before_tokens,
+ row.after_tokens,
+ compute_compression_ratio(row.before_tokens, row.after_tokens),
+ compounding_turns,
+ compounded,
+ ),
before_summary: format!("原始内容 {} tokens", row.before_tokens),
after_summary: format!("优化后 {} tokens", row.after_tokens),
optimization_reason: generate_optimization_reason(
@@ -1119,6 +1271,27 @@ mod tests {
}
}
+ // ─── Unit tests for helper functions ─────────────────────────────────
+
+ fn make_session_for_tips(compounded_saved: i64, total_tokens: i64) -> SessionSavingsDto {
+ SessionSavingsDto {
+ session_id: "sess-1".to_string(),
+ agent_name: "TestAgent".to_string(),
+ total_input_tokens: total_tokens / 2,
+ total_output_tokens: total_tokens / 2,
+ total_tokens,
+ baseline_tokens: total_tokens + compounded_saved,
+ saved_tokens: compounded_saved,
+ compounded_saved,
+ savings_rate: 0.0,
+ compounded_savings_rate: 0.0,
+ request_count: 1,
+ tool_saved: 0,
+ mcp_saved: 0,
+ optimization_items: vec![],
+ }
+ }
+
#[test]
fn test_diff_replace_remove_before_add() {
let before = "old_line";
@@ -1132,6 +1305,65 @@ mod tests {
);
}
+ #[test]
+ fn test_compute_compression_ratio_normal() {
+ let ratio = compute_compression_ratio(1000, 250);
+ assert!((ratio - 75.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn test_compute_compression_ratio_zero_before() {
+ assert_eq!(compute_compression_ratio(0, 100), 0.0);
+ }
+
+ #[test]
+ fn test_compute_compression_ratio_no_compression() {
+ let ratio = compute_compression_ratio(500, 500);
+ assert!((ratio - 0.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn test_compute_compression_ratio_negative_clamped() {
+ let ratio = compute_compression_ratio(100, 200);
+ assert_eq!(ratio, 0.0);
+ }
+
+ #[test]
+ fn test_build_explanation_mcp_response() {
+ let explanation = build_explanation("mcp_response", 1000, 200, 80.0, 3, 2400);
+ assert!(explanation.contains("MCP"));
+ assert!(explanation.contains("1000"));
+ assert!(explanation.contains("200"));
+ assert!(explanation.contains("80.0%"));
+ assert!(explanation.contains("3"));
+ assert!(explanation.contains("2400"));
+ }
+
+ #[test]
+ fn test_build_explanation_tool_output() {
+ let explanation = build_explanation("tool_output", 500, 100, 80.0, 2, 800);
+ assert!(explanation.contains("工具输出"));
+ assert!(explanation.contains("500"));
+ assert!(explanation.contains("100"));
+ }
+
+ #[test]
+ fn test_tips_stats_unavailable() {
+ let tips = generate_optimization_tips(false, 0, 0.0, 0, 0, &[]);
+ assert_eq!(tips.len(), 1);
+ assert_eq!(tips[0].level, "warning");
+ assert!(tips[0].title.contains("Tokenless"));
+ }
+
+ #[test]
+ fn test_tips_low_savings_rate() {
+ let tips = generate_optimization_tips(true, 10000, 10.0, 100, 200, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "warning" && t.title.contains("节省率"))
+ );
+ }
+
#[test]
fn test_diff_large_input_fallback() {
let big_before = (0..1200)
@@ -1169,4 +1401,85 @@ mod tests {
let result = compute_diff_lines(Some(&before), Some(&after));
assert!(result.len() <= 200);
}
+
+ #[test]
+ fn test_tips_boundary_at_15_no_warning() {
+ let tips = generate_optimization_tips(true, 10000, 15.0, 1000, 500, &[]);
+ assert!(
+ !tips
+ .iter()
+ .any(|t| t.level == "warning" && t.title.contains("节省率"))
+ );
+ assert!(tips.iter().any(|t| t.level == "success"));
+ }
+
+ #[test]
+ fn test_tips_boundary_at_30_excellent() {
+ let tips = generate_optimization_tips(true, 10000, 30.0, 2000, 1000, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "success" && t.title.contains("优秀"))
+ );
+ }
+
+ #[test]
+ fn test_tips_boundary_just_below_15_warning() {
+ let tips = generate_optimization_tips(true, 10000, 14.9, 500, 500, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "warning" && t.title.contains("节省率"))
+ );
+ }
+
+ #[test]
+ fn test_tips_only_tool_saved_suggest_mcp() {
+ let tips = generate_optimization_tips(true, 10000, 10.0, 500, 0, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "info" && t.title.contains("MCP"))
+ );
+ }
+
+ #[test]
+ fn test_tips_only_mcp_saved_suggest_tool() {
+ let tips = generate_optimization_tips(true, 10000, 10.0, 0, 500, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "info" && t.title.contains("工具"))
+ );
+ }
+
+ #[test]
+ fn test_tips_zero_savings_sessions() {
+ let sessions = vec![
+ make_session_for_tips(0, 5000),
+ make_session_for_tips(200, 3000),
+ ];
+ let tips = generate_optimization_tips(true, 8000, 10.0, 100, 100, &sessions);
+ assert!(tips.iter().any(|t| t.title.contains("1")));
+ }
+
+ #[test]
+ fn test_tips_excellent_rate() {
+ let tips = generate_optimization_tips(true, 10000, 35.0, 2000, 1500, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "success" && t.title.contains("优秀"))
+ );
+ }
+
+ #[test]
+ fn test_tips_good_rate() {
+ let tips = generate_optimization_tips(true, 10000, 20.0, 1000, 1000, &[]);
+ assert!(
+ tips.iter()
+ .any(|t| t.level == "success" && t.title.contains("良好"))
+ );
+ }
+
+ #[test]
+ fn test_tips_empty_when_no_data() {
+ let tips = generate_optimization_tips(true, 0, 0.0, 0, 0, &[]);
+ assert!(tips.is_empty());
+ }
}