返回博客列表
性能优化2026-06-0416 分钟阅读

三级裁剪:零 LLM 成本的旧 Tool 结果瘦身术

不花一个 token 给旧工具结果瘦身:三级裁剪策略详解。

三级裁剪:零 LLM 成本的旧 Tool 结果瘦身术


专栏信息

《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏

本文是模块八第 11 篇,讲解压缩前的预处理裁剪流水线。


作者与项目

作者简介:翁勇刚 WENG YONGGANG 新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者 理念:"再复杂的技术,就能用代码讲清楚"


摘要

本文结构概览: 本文讲解在调用 LLM 生成摘要之前,如何通过三级无损预处理裁剪旧的工具结果,大幅减少 LLM 的输入 token,降低压缩成本。三级分别为 MD5 去重、信息性摘要替换、大参数截断。

背景:上下文压缩需要调用 LLM 生成摘要。但旧消息中可能包含大量冗余的工具输出(重复的文件内容、相似的搜索结果),直接交给 LLM 摘要既浪费 token 又增加成本。

核心问题:如何在不调用 LLM 的前提下,预先减少 40-60% 的旧消息 token?

解决方案:三级裁剪流水线——Pass 1 MD5 去重 + Pass 2 信息摘要替换 + Pass 3 参数截断

关键成果

  • LLM 摘要的输入 token 减少 40-60%
  • 零额外 LLM 调用成本
  • 近期消息(token-budget 保护区域内)不受影响

适合读者:LLM Agent 开发者,关注成本控制和大窗口模型优化

阅读时长:约 10 分钟

关键词预处理裁剪MD5去重信息摘要Token优化成本控制


一、为什么要在压缩前先裁剪?

1.1 数据说话

在一次典型的长对话中,旧消息的 token 分布可能是这样的:

消息类型Token 数占比信息价值
文件读取结果45,00045%低(已被模型分析过)
搜索结果15,00015%低(重复/过期信息)
代码执行结果10,00010%
对话内容20,00020%
工具调用参数10,00010%低(旧参数无需保留)

60% 的 token 给了工具结果,而这些结果的"原始细节"在压缩后只需要一个摘要就够了。

1.2 成本计算

不裁剪: 100K tokens 输入 LLM 生成摘要 → 费用 $0.50
裁剪后: 50K tokens 输入 LLM 生成摘要 → 费用 $0.25
节省: 50%

对于高频使用的应用,这个节省非常可观。


二、三级裁剪流水线

[图片: 三级裁剪流水线图 | 生成方式: 文生图 PROMPT: "A three-stage pipeline diagram for tool result pruning: Stage 1 Deduplication with filter icon showing duplicate messages being removed, Stage 2 Summary Replacement with compress icon showing long text being replaced by one-line summaries, Stage 3 Argument Truncation with scissors icon showing long parameters being shortened, with before/after token counts shown at each stage, clean technical pipeline style, light background"]

原始消息 (100K tokens)
    ↓
[Pass 1: MD5 去重] → 移除重复的工具结果
    ↓ (90K tokens)
[Pass 2: 信息摘要替换] → 用一行摘要替换旧的工具结果
    ↓ (55K tokens)
[Pass 3: 大参数截断] → 截断超过 500 字符的 tool_call 参数
    ↓ (50K tokens)
    ↓
LLM 摘要 (输入 50K tokens,比不裁剪节省 50%)

2.1 Token-budget 保护区域

重要:裁剪只针对旧消息,近期消息受 token-budget 保护,不参与裁剪。

PROTECTED_RATIO = 0.30  # 最近 30% token 预算内的消息不裁剪

def get_prunable_range(messages, token_limit):
    """获取可裁剪的消息范围"""
    protected_budget = int(token_limit * PROTECTED_RATIO)
    accumulated = 0
    protected_start = len(messages)

    # 从后向前计算保护区域
    for i in range(len(messages) - 1, -1, -1):
        accumulated += estimate_tokens(messages[i])
        if accumulated > protected_budget:
            protected_start = i + 1
            break

    return range(0, protected_start)  # 只有这个范围内的消息参与裁剪

三、Pass 1:MD5 去重

3.1 场景:同一个文件被读取多次

Step 1: read_file("/src/main.py") → 返回 8000 tokens 的文件内容
Step 3: read_file("/src/main.py") → 又返回 8000 tokens(内容相同)
Step 7: read_file("/src/main.py") → 再次返回 8000 tokens

同一个文件在长对话中被多次读取(比如每次修改后重新读取验证),产生了 24000 tokens 的冗余。

3.2 实现

import hashlib

def pass1_dedup(messages, prunable_range):
    """Pass 1: MD5 去重——相同内容只保留最后一条"""
    seen_hashes = {}  # hash → 最后出现的位置
    to_remove = set()
    MIN_DEDUP_LENGTH = 200  # 只去重超过 200 字符的消息

    # 从后向前遍历(保留最新的)
    for i in reversed(range(prunable_range.start, prunable_range.stop)):
        msg = messages[i]
        if msg.get("role") != "tool":
            continue
        content = msg.get("content", "")
        if len(content) < MIN_DEDUP_LENGTH:
            continue

        # 计算 MD5
        content_hash = hashlib.md5(content.encode()).hexdigest()

        if content_hash in seen_hashes:
            # 重复!标记为删除
            to_remove.add(i)
        else:
            seen_hashes[content_hash] = i

    # 移除重复消息
    return [m for i, m in enumerate(messages) if i not in to_remove]

3.3 去重效果

去重前: 3 次 read_file("/src/main.py") = 24000 tokens
去重后: 1 次 read_file("/src/main.py") = 8000 tokens
节省: 16000 tokens (67%)

四、Pass 2:信息性摘要替换

4.1 设计思路

对于不同类型的工具,用专门的一行摘要替换完整的工具结果:

[图片: 10 个高频工具摘要示例表 | 生成方式: Markdown 表格]

工具原始结果一行摘要
search_web5 条搜索结果(2000 字符)搜索 "python async": 返回 5 条结果
read_file文件内容(8000 字符)读取 /src/main.py: 245 行 Python 文件
shell_exec命令输出(3000 字符)执行 "pytest": 37 passed, 0 failed
write_file写入确认(200 字符)写入 /src/utils.py: 成功
stock_query行情数据(1500 字符)查询 AAPL: $185.23 (+1.2%)
web_fetch网页内容(5000 字符)获取 example.com: 标题 "Example Page"
analyze_pdfPDF 解析结果(10000 字符)解析 report.pdf: 42 页, 提取 15 段文本
list_dir目录列表(500 字符)列出 /src/: 12 个文件, 3 个子目录
create_chart图表数据(1000 字符)生成图表: 折线图, 5 个数据点
translate翻译结果(800 字符)翻译 EN→ZH: 200 字符文本

4.2 实现

# 工具摘要生成器映射
TOOL_SUMMARIZERS = {
    "search_web": _summarize_search,
    "read_file": _summarize_read_file,
    "shell_exec": _summarize_shell,
    "write_file": _summarize_write,
    "stock_query": _summarize_stock,
    # ... 其他工具
}

def pass2_summary_replace(messages, prunable_range):
    """Pass 2: 信息性摘要替换"""
    result = list(messages)

    for i in range(prunable_range.start, min(prunable_range.stop, len(result))):
        msg = result[i]
        if msg.get("role") != "tool":
            continue

        # 查找对应的 tool_call 获取工具名
        tool_name = _find_tool_name(messages, msg.get("tool_call_id", ""))
        if not tool_name:
            continue

        # 使用专门的摘要器
        summarizer = TOOL_SUMMARIZERS.get(tool_name)
        if summarizer:
            summary = summarizer(msg.get("content", ""), tool_name)
            result[i] = {**msg, "content": summary}

    return result

def _summarize_read_file(content, tool_name):
    """read_file 的专门摘要"""
    lines = content.count('\n') + 1
    # 提取文件路径(通常在结果开头)
    path_match = re.search(r'[/\\][\w/\\.-]+\.\w+', content[:200])
    path = path_match.group(0) if path_match else "unknown"
    # 推断文件类型
    ext = path.rsplit('.', 1)[-1] if '.' in path else "unknown"
    return f"[read_file] 读取 {path}: {lines} 行 {ext} 文件"

五、Pass 3:大参数截断

5.1 场景:tool_call 的 arguments 过长

{
    "role": "assistant",
    "tool_calls": [{
        "id": "call_001",
        "function": {
            "name": "write_file",
            "arguments": "{\"path\": \"/src/main.py\", \"content\": \"import os\\nimport sys\\n...[5000字符的代码]...\"}"
        }
    }]
}

tool_call 的 arguments 字段包含了完整的文件内容(5000+ 字符),但在压缩时,这个细节已不需要——write_file 的 tool_result 已经确认写入成功。

5.2 实现

MAX_ARGUMENT_LENGTH = 500  # 超过 500 字符的参数截断

def pass3_truncate_args(messages, prunable_range):
    """Pass 3: 大参数截断"""
    result = list(messages)

    for i in range(prunable_range.start, min(prunable_range.stop, len(result))):
        msg = result[i]
        if msg.get("role") != "assistant":
            continue
        if "tool_calls" not in msg:
            continue

        for tc in msg["tool_calls"]:
            fn = tc.get("function", {})
            args = fn.get("arguments", "")
            if len(args) > MAX_ARGUMENT_LENGTH:
                fn["arguments"] = args[:MAX_ARGUMENT_LENGTH] + "...[truncated]"

    return result

六、完整流水线

def prune_tool_results_advanced(messages, token_limit):
    """三级裁剪的完整流水线

    Args:
        messages: 消息列表
        token_limit: 总 token 阈值

    Returns:
        裁剪后的消息列表(不影响原始列表)
    """
    prunable_range = get_prunable_range(messages, token_limit)
    result = list(messages)

    # Pass 1: MD5 去重
    result = pass1_dedup(result, prunable_range)
    # 注意:去重可能改变索引,需要重新计算范围
    prunable_range = get_prunable_range(result, token_limit)

    # Pass 2: 信息摘要替换
    result = pass2_summary_replace(result, prunable_range)

    # Pass 3: 大参数截断
    result = pass3_truncate_args(result, prunable_range)

    return result

6.1 实测效果

指标裁剪前Pass 1 后Pass 2 后Pass 3 后总计节省
Token 数100K85K55K50K50%
消息数12010510510512.5%
LLM 摘要成本$0.50$0.43$0.28$0.2550%

七、总结与展望

7.1 核心要点回顾

  1. 预处理裁剪是"免费午餐":零 LLM 成本减少 50% 的摘要输入 token
  2. 三级流水线层层递进:去重 → 摘要替换 → 参数截断
  3. 保护区域不裁剪:最近 30% token 预算内的消息完整保留
  4. 工具专门分支提升摘要质量:不同工具用不同的摘要格式

下期预告:《实战踩坑录:上下文管理的 10 个反直觉 Bug》

  • 从阈值失效到正则陷阱
  • 从占位累积到 API 不兼容
  • 一份完整的工程踩坑清单

敬请期待!


版权声明:本文为 CSDN 博主「翁勇刚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。