📌 痛点:一条微博 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 同样计费,而且大模型默认会“啰嗦”。
我的做法:
max_tokens设到刚好够用
payload = {
"model": "deepseek-chat",
"max_tokens": 10 # 分类只需返回"基础层"三个字,10 token 足够
}
- Prompt 中禁止废话
【输出格式】
直接返回最符合的层次名称:
基础层/保障层/舒适层/尊重层/共鸣层/其他
不要写“请输出结果:”,因为模型会返回“结果是:基础层”,多消耗 token。
- 后处理截断
即使模型多输出了,代码里也可以强制截取:
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条) | 输入费用 | 输出费用 | 合计 |
|---|---|---|---|---|---|
| 全文输入 | 400 | 5,600,000 | ¥5.6 | ¥0.3 | ¥5.9 |
| 截断 500 字 | 250 | 3,500,000 | ¥3.5 | ¥0.3 | ¥3.8 |
| 截断+清洗 | 200 | 2,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,不妨检查一下:你扔给模型的文本里,有多少是真正有用的信息?