保持业务功能完全不变,如何让自动出题系统的 Token 成本降低近一半
摘要
本文记录了一次真实生产项目的优化过程。项目是一个基于知识库(RAG)的 LLM 自动出题系统,目标是在不修改任何业务功能、接口、数据库结构和校验逻辑的前提下,仅通过 Prompt Engineering 与工程优化,提高 Prompt Cache 命中率、降低 Token 消耗。
最终实现:
- Prompt Cache:约 10%~30% → 80%~90%(依模型实现而异)
- Token 成本降低约 40%~60%
- Context 构建从 N 次降为 1 次
- 保持 API、JSON、数据库、校验逻辑完全不变
一、背景
自动出题流程:
flowchart LR
A[知识库] --> B[Chunk切分]
B --> C[构建Prompt]
C --> D[LLM生成问题]
D --> E[答案溯源校验]
E --> F[(数据库)]
F --> G[评分模型]
线上监控发现:
- 评分阶段缓存命中率很高;
- 出题阶段缓存命中率极低;
- Token 成本几乎全部集中在出题阶段。
于是开始定位问题。
二、Prompt Cache 为什么失效?
Prompt Cache 的核心不是"语义相同",而是Token 前缀连续一致。
错误理解:
Prompt A
Context 相同
Category 不同
很多人认为应该命中缓存。
实际上缓存比较的是:
token1
token2
...
token1024
前缀任意位置变化,都可能导致缓存无法复用。
三、第一版代码的问题
① 分类导致 Context 不一致
sample_chunks = _stable_sample(chunks, category)
不同分类得到不同 Chunk。
② 每个分类重复构建 Context
context = "\n".join(...)
CPU 与 Prompt 都重复。
③ Prompt 前部变化太多
Category
Count
DocNames
Context
④ Request Count 动态变化
request_count=max(count*3,10)
不同请求 Prompt 不一致。
⑤ Context 无上限
题量越大,Prompt 越长。
四、优化过程
Step1:共享 Context
shared_chunks = sorted_chunks[:need]
shared_context = build_context(shared_chunks)
for category in categories:
generate_questions_by_category(
context=shared_context
)
收益:
- Prompt 更稳定
- CPU 降低
- Cache 明显提升
Step2:固定 Chunk
取消按分类随机采样:
shared_chunks = sorted_chunks[:need]
所有分类共享同一 Context。
Step3:固定 Request Count
GLOBAL_REQUEST_COUNT = 60
Prompt 永远一致。
真正数量:
return valid[:count]
保证业务结果一致。
Step4:限制 Context
MAX_SHARED_CHUNKS = 50
content[:300]
保证 Prompt 长度稳定。
Step5:共享 DocNames
fixed_doc_names = sorted({
c["filename"]
for c in shared_chunks
})
避免分类之间文档列表不同。
Step6:调整 Prompt 顺序
Prefix
↓
DocNames
↓
Context
↓
Category
↓
Count
让固定内容尽量靠前。
Step7:降低随机性
temperature = 0.2
提高稳定性。
五、为什么这是"无损优化"?
整个优化过程中没有修改:
- generate_all_questions()
- generate_questions_by_category()
- 输出 JSON
- 数据库结构
- 答案溯源
- 去重
- 幻觉题处理
- 分类规则
变化的是实现,不是业务能力。
六、优化前后对比
| 项目 | 优化前 | 优化后 |
|---|---|---|
| Prompt Cache | 10~30% | 80~90%* |
| Token 成本 | 100% | 40~60% |
| Context 构建 | N 次 | 1 次 |
| Prompt 稳定性 | 低 | 高 |
| CPU 拼接 | 多次 | 一次 |
*实际效果与模型厂商 Prompt Cache 策略有关。
七、踩坑总结
❌ Prompt 相同语义 ≠ Cache 命中
❌ 随机采样会破坏 Prompt 稳定性
❌ Request Count 动态变化会影响前缀一致性
❌ Context 无限增长会增加成本
真正有效的是:
- 固定 Context
- 固定 Chunk
- 固定 Request Count
- 固定 DocNames
- 控制 Prompt 长度
- 保持 Prompt 前缀稳定
八、适用场景
这些优化适用于绝大多数 OpenAI Compatible API:
- OpenAI
- DeepSeek
- OpenRouter
- 阿里百炼
- 火山引擎
- SiliconFlow
尤其适用于:
- RAG
- 自动出题
- 知识库问答
- Agent
- 企业内部 AI
九、结语
这次优化最大的收获不是某一个 Prompt 技巧,而是理解了 Prompt Cache 的本质。
很多时候,降低成本并不需要重写业务逻辑。
通过稳定 Prompt、共享 Context、控制 Token,就可以在保持功能完全一致的前提下获得非常可观的收益。
对于生产环境来说,这类优化风险小、收益高、易于回滚,非常值得优先实施。