Token 就是钱!LLM 长文本处理的截断策略与成本控制实战

0 阅读6分钟

📌 痛点:一条微博 2000 字,全扔给大模型?

在调用大模型 API 时,很多人习惯直接把整段文本扔进去,觉得“信息越多,结果越准”。

但现实是:

  • 成本飙升:输入 token 按量计费,一条 2000 字的微博可能消耗上千 token,14,000 条数据就是千万级 token。
  • 效果下降:大模型对长文本的“中间部分”注意力较弱,冗余信息反而干扰判断。
  • 速度变慢:输入越长,推理时间越长,批量处理时差异巨大。

我在处理 14,088 条南京地铁微博时,每条数据只用了前 300-500 字,不仅省下了大量费用,分类和抽取的准确率反而更高了。这篇文章就分享我的长文本截断策略和成本控制经验。

🤔 为什么截断反而更准?

对于情感分类、需求分类、关键词抽取这类任务,核心信息通常集中在文本开头

看几条典型微博:

“南京地铁 3 号线又延误了!我已经等了 15 分钟了,上班要迟到了啊啊啊!#南京地铁#”

“今天坐 S6 号线,车厢空调冷得像冰窖,穿了两件还是冻得发抖。@南京地铁”

“工作人员超级暖心!我钱包掉在安检口,小姐姐帮我收好还打电话通知我,太感动了。”

发现了吗?  核心诉求(延误、空调冷、工作人员帮助)都在前 1-2 句话。后面的情绪宣泄、@ 账号、话题标签对分类几乎没有价值。

截断的本质是“信噪比优化”——去掉冗余,保留信号。

📏 策略一:固定长度截断(最简单有效)

我的需求分类任务中,直接截取前 500 个字符:

def call_deepseek_api(text):
    prompt = """... 待分析内容:"{}" """.format(text[:500])

为什么是 500?

  • 一条中文微博通常 100-300 字(1 个汉字 ≈ 1 字符)。
  • 500 字符约等于 3-5 条微博的长度,足够覆盖核心内容。
  • 实测截取 300 字和 500 字准确率几乎相同,但 500 字给长文本留出了余量。

成本对比

  • 原始平均每条 280 字 → 截断后平均 200 字(很多不到 500)。
  • 14,000 条数据,节省约 30% 输入 token

🎯 策略二:按语义边界截断(更精细)

如果任务对上下文依赖更强(比如总结、问答),固定截断可能把句子切碎。更好的做法是按句号、换行符等自然边界截断。

def smart_truncate(text, max_chars=500):
    if len(text) <= max_chars:
        return text
    
    # 在 max_chars 范围内找最后一个句号或换行
    truncated = text[:max_chars]
    last_period = truncated.rfind('。')
    last_newline = truncated.rfind('\n')
    
    cut_pos = max(last_period, last_newline)
    if cut_pos > max_chars * 0.7:  # 至少保留70%,避免切太短
        return text[:cut_pos + 1]
    else:
        return text[:max_chars] + "…"  # 强制截断,加省略号

这种策略适合对文本完整性要求高的场景。我的分类任务用固定截断就够了。

📊 策略三:结构化摘要式截断(高级玩法)

如果你的文本有固定结构(比如客服工单:标题+描述+附件),可以只取最关键的部分。

例如我的微博数据,可以只保留正文,去掉 @ 提及和话题标签:

import re

def clean_weibo(text):
    # 去掉 @用户名
    text = re.sub(r'@\S+', '', text)
    # 去掉 #话题#
    text = re.sub(r'#\S+#', '', text)
    # 去掉多余空白
    text = re.sub(r'\s+', ' ', text).strip()
    return text[:500]

效果:同样 500 字,有效信息密度更高。

💰 策略四:输出端也要省 Token

很多人只关注输入成本,其实输出 token 同样计费,而且大模型默认会“啰嗦”。

我的做法:

  1. max_tokens 设到刚好够用
payload = {
    "model": "deepseek-chat",
    "max_tokens": 10  # 分类只需返回"基础层"三个字,10 token 足够
}
  1. Prompt 中禁止废话
【输出格式】
直接返回最符合的层次名称:
基础层/保障层/舒适层/尊重层/共鸣层/其他

不要写“请输出结果:”,因为模型会返回“结果是:基础层”,多消耗 token。

  1. 后处理截断

即使模型多输出了,代码里也可以强制截取:

result = response.json()['choices'][0]['message']['content'].strip()
result = result.split('\n')[0]  # 只取第一行
result = result[:10]  # 强制截断

📈 实际成本测算

以 DeepSeek 价格为例(输入 ¥1/百万 token,输出 ¥2/百万 token):

方案平均输入 token/条总输入 token (14,000条)输入费用输出费用合计
全文输入4005,600,000¥5.6¥0.3¥5.9
截断 500 字2503,500,000¥3.5¥0.3¥3.8
截断+清洗2002,800,000¥2.8¥0.3¥3.1

节省近 50% 成本,而且分类准确率从 89% 提升到 91.2%(去掉冗余信息后模型更聚焦)。

💡 三条核心经验

1. “少即是多”在 LLM 中尤其适用

大模型不是输入越多越好。冗余信息会分散注意力,增加推理噪声。截断本质上是帮模型做了一次“注意力聚焦”。

2. 根据任务类型选择截断策略

  • 分类/抽取:固定长度截断(300-500 字)足够。
  • 摘要/问答:按语义边界截断,保证句子完整。
  • 结构化数据:只取关键字段,去掉元数据。

3. 输出端同样需要“节流”

一个 max_tokens=10 的设置,能让每条数据的输出成本降到几乎可忽略。加上 Prompt 中的“不要解释”,一年下来能省出一顿饭钱。

🔧 完整代码示例

import re

def preprocess_text(text, max_chars=500, clean_weibo=True):
    """文本预处理:清洗 + 截断"""
    if not text:
        return ""
    
    # 清洗
    if clean_weibo:
        text = re.sub(r'@\S+', '', text)      # 去 @
        text = re.sub(r'#\S+#', '', text)     # 去话题
        text = re.sub(r'\s+', ' ', text)      # 合并空白
    
    text = text.strip()
    
    # 截断
    if len(text) > max_chars:
        # 尽量在句号处截断
        truncated = text[:max_chars]
        last_period = truncated.rfind('。')
        if last_period > max_chars * 0.7:
            text = text[:last_period + 1]
        else:
            text = truncated + "…"
    
    return text

# 使用示例
raw_text = "@南京地铁 今天3号线又延误了!#南京地铁# 等了20分钟还没来,上班要迟到了"
clean_text = preprocess_text(raw_text, max_chars=300)
print(clean_text)
# 输出: "今天3号线又延误了!等了20分钟还没来,上班要迟到了"

🔗 完整代码与项目

完整实现已开源在 GitHub:
👉 nanjing-metro-analysis/scripts/01_demand_classification/classify_demand.py

📮 写在最后

Token 就是钱,截断就是省钱。更妙的是,合理的截断不仅省钱,还能提升效果。

如果你也在批量调用 LLM API,不妨检查一下:你扔给模型的文本里,有多少是真正有用的信息?