实习中可量化成果
1. Token 消耗优化
我主要做了几件事:
- 重构 Prompt 模板
- 做上下文裁剪
- 减少重复 System Prompt
- 对历史消息做窗口压缩
- 对固定业务规则做结构化参数替代
- 增加缓存,避免重复生成
通过查看日志和token usage 监控得到token消耗降低了30%-40%
2.修复调用LLM失败之后无限入RabbitMQ导致token消耗问题,增加了失败重试次数的限制
"调用链路追踪 + 日志统计 + MQ 重试次数"
同一条requestid不断地入队,然后被消耗

这些具体是怎么实现地呢?
1. 重构 Prompt 模板
核心目标:
减少无效 token + 提高 Prompt 结构稳定性
传统低级写法
String prompt = """
你是一个企业知识库AI助手,
你需要遵守以下规则:
1. 不允许胡编乱造
2. 回答必须专业
3. 使用中文
4. 输出必须包含总结
5. 如果不知道要拒答
用户问题:
%s
参考知识:
%s
""";
我的优化方式
(1)拆分固定规则
做成固定模板
SYSTEM_TEMPLATE
USER_TEMPLATE
OUTPUT_TEMPLATE
最终组合
String finalPrompt =
SYSTEM_TEMPLATE
+ USER_TEMPLATE.formatted(question, context)
+ OUTPUT_TEMPLATE;
Prompt 是一大段字符串,很多身份说明、输出要求、业务规则都混在一起,而且多个场景里有重复描述。后面我把它拆成 System 模板、User 模板、Output 模板三部分。"
(2)用户Prompt 参数化
传统写法的问题
比如:
请帮我分析用户张三在北京地区2026年的订单情况
改成结构化
{
"rawQuestion": "请帮我分析用户张三在北京地区2026年的订单情况,并重点关注退款率异常的月份",
"task": "订单分析",
"userName": "张三",
"region": "北京",
"year": "2026",
"focus": "退款率异常的月份"
}
可以使用NLP进行这些参数提取
LLM会更好地理解用户问题,减少对问题拆分理解需要的token
2.上下文裁剪
我的优化方式
(1)Window Memory
只保留最近 N 轮。
java
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10) // 只保留最近 10 条 message,不是 10 轮
.build();
this.chatClient = builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
调用时通过 conversationId 区分会话:
java
chatClient.prompt()
.advisors(a -> a.param(
AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY,
conversationId
))
.user(question)
.call()
.content();
因为不会把整个会话历史都塞给模型,只保留最近几条和当前问题最相关的消息。
例如原来有 50 条历史,现在只带 10 条,历史 token 会明显减少。
(2)按 token 数裁剪:超过阈值就删旧内容
只按消息条数不够,因为一条消息可能很短,也可能很长。所以还需要设置最大 token 阈值
java
List<Message> history = chatMemory.get(conversationId);
// 1. Window Memory 已经保证最多 10 条
// 2. 再做 token 裁剪
while (countTokens(history) > maxHistoryTokens) {
history.remove(0); // 删除最旧消息
}
// 3. 组装最终上下文
String finalPrompt = buildPrompt(
systemPrompt,
summary,
history,
currentQuestion,
ragContext
);
(3)只保留有效消息:过滤低价值内容
java
private boolean isUsefulMessage(String content) {
if (content == null || content.isBlank()) {
return false;
}
String text = content.trim();
Set<String> useless = Set.of("好的", "嗯", "嗯嗯", "谢谢", "收到", "继续");
if (useless.contains(text)) {
return false;
}
return text.length() >= 4;
}
(4)历史上下文压缩:旧历史转成摘要或事实
实现方式
当历史超过阈值时,不直接丢弃旧历史,而是把旧历史压缩成更短的信息
Redis 可以分两部分存:
chat:memory:{conversationId} // 最近窗口消息
chat:summary:{conversationId} // 历史摘要/关键事实
触发条件:
历史消息超过 20 条
或者 history token 超过 3000
压缩流程:
读取旧历史
→ 提取关键事实/当前目标/用户约束
→ 生成 summary
→ 写入 Redis
→ 裁剪原始历史
3.增加缓存,避免重复生成
(1)问题简单规则归一化
(2)构建缓存 Key
(3)Redis 缓存答案
(4)命中直接返回
线上的OOM问题你是怎么定位的?具体是什么问题?
1.首先我看了服务日志和监控
java
java.lang.OutOfMemoryError: Java heap space
发现是堆空间满了
2.用 visualvm看JVM 内存变化
(1)Heap 内存曲线
发现:
内存不断上涨 Full GC 后下降不明显(2)GC 情况
发现:
- Full GC 频率越来越高
- GC 时间变长
说明:
老年代压力比较大。
3.分析 GC 日志
(1)Full GC 是否频繁
(2)GC 后堆内存是否下降
4. 导出 Heap Dump
5. 使用 MAT 分析 Heap Dump(JVM 某一时刻的"内存快照" )
重点去观察
(1)谁占内存最多
(2)删除这个对象后能释放多少内存
(3)GC Roots
为什么这些对象没有被 GC 回收
常见的内存泄露原因
1.集合类未清理(最常见)
List / Map / Set 从不 remove
2.ThreadLocal 未 remove
java
ThreadLocal<User> local = new ThreadLocal<>();
local.set(user);
3.连接资源未关闭
- IO
- Socket
- JDBC
4.静态变量持有对象
java
static Map<String,Object> cache
静态变量生命周期和 JVM 一样长
Threadlocal是什么,有什么用,底层原理是什么(面试热门)
ThreadLocal 是什么
"线程自己的局部变量"
作用:
给每个线程单独保存一份变量副本
1. 用户上下文传递
2. TraceId / 日志链路细节点
底层原理
Thread
内部有 ThreadLocalMap
关系图
Thread
↓
ThreadLocalMap
↓
Entry(ThreadLocal -> value)
电商订单如何保证幂等性?
最经典方案:唯一业务号
前端防重复
按钮点击后置灰
requestId 幂等
下单时:
userId + requestId
Redis setnx
java
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(
"order:req:" + requestId,
"1",
Duration.ofMinutes(10)
);
if (!success) {
return "重复下单";
}
支付如何保证幂等
典型方案:订单状态机
例如:
INIT
→ PAYING
→ PAID
回调时:
if(order.status == PAID){
return;
}
只有:
未支付
才能:
更新为已支付
库存怎么保证幂等?
MQ:
重复扣库存
会:
库存负数
扣减流水表
例如:
stock_deduct_log
记录:
orderId
skuId
扣之前:
先查:
是否已经扣过
