7,104
社区成员
发帖
与我相关
我的任务
分享我们在做 GenieChat 的多轮对话能力,目标是“聊得下去、不卡、上下文别丢”。现在卡在一个设计问题:上下文越积越长,推理延迟和内存占用就开始不可控,体验很容易掉。
想请教下有经验的同学:
多轮对话在端侧/本地推理时,一般怎么做“推理侧 + 状态侧”的拆分比较合理?
比如这些点怎么设计会更顺:
上下文是每轮都把历史拼进 prompt 重算,还是用 KV cache 之类的方式续推?
历史消息怎么裁剪/摘要,才能既保语义连续又不拖慢延迟?(按 token 上限截断、按主题滚动、还是定期摘要)
状态存哪些更关键:原始对话、摘要、工具调用结果、用户偏好?分别放内存还是落盘?
工具调用/检索这种外部信息,怎么并进上下文才不让首 token 时间变长?
多线程/异步这块有没有推荐做法(流式输出、边生成边检索、UI 不阻塞)?
我们更关心的是工程落地:端到端要流畅,长对话也不要越来越慢。
在工程实现上建议关注以下方面:
1.对话历史的管理 (状态管理)
应用需要有一个“短期记忆”来存储当前的对话。我们可以在在App运行时,在内存中维护一个对话列表。如果希望即使用户关闭并重新打开App后,对话历史依然存在,就需要将对话记录持久化存储。可以考虑使用数据库(如SQLite)或文件的形式,将对话保存在手机本地。
对话历史不能无限增长,否则会消耗过多的内存和计算资源。因此,需要设定一个“记忆窗口”,比如只保留最近的10轮或20轮对话。当对话超出这个窗口时,最早的对话就会被“遗忘”。
2.利用对话历史进行推理
在向AI模型发送请求时,不再仅仅发送用户当前说的这句话。而是需要将之前存储的对话历史(短期记忆)一并打包,作为背景信息发送给AI。这样,AI才能“看到”之前的对话,理解当前的语境。
保证流畅性的优化建议:
为了避免用户在AI思考时长时间等待,可以让AI模型以“流”的方式,一个词一个词地返回答案,而不是等全部答案都生成好了再一股脑地返回。这能极大地提升用户的体验,让对话感觉更“实时”。(目前GenieChat已经实现了这一点)
上下文压缩(Context Pruning):当对话历史变得很长时,全部发送给AI会增加API的调用成本和延迟。可以采用一些策略来“精简”上下文,比如只发送最近的几轮对话,或者对早期的对话内容进行摘要总结。
另外,QAI AppBuilder中提供的GenieAPIService本身默认也是支持多轮对话(保持上下文)的。可查看GitHub上相关文档说明。
这是个典型的工程优化问题。要让本地大模型的对话体验"顺",需要做好 "渐进式降级" 的架构设计。我直接给工程落地方案:
一、上下文管理策略:分层压缩
不要把所有历史都拼进prompt。建议采用三级缓存策略:
[当前轮输入 + 重要记忆] → [滑动窗口历史] → [长期压缩摘要]
具体实现:
class ContextManager:
def init(self, max_tokens=4096):
self.max_tokens = max_tokens
self.recent_messages = [] # 最近3-5轮
self.compressed_summary = "" # 压缩后的历史摘要
self.important_facts = set() # 关键事实(用户偏好、关键决定等)
def add_message(self, role, content):
self.recent_messages.append((role, content))
# 滑动窗口控制
while self._estimate_tokens() > self.max_tokens * 0.7:
self._compress_oldest()
def _compress_oldest(self):
# 将最早的消息转为摘要
oldest = self.recent_messages.pop(0)
# 用LLM生成一句话摘要,或提取关键实体
summary = self._generate_summary(oldest[1])
self.compressed_summary += f"\n{summary}"
二、推理优化:KV Cache + 增量生成
必须用KV Cache,避免重复计算。但要注意内存管理:
class GenieChatInference:
def init(self):
self.kv_cache = None # 保存上一轮的KV cache
self.cache_tokens = 0
async def generate_stream(self, new_input: str):
# 1. 构建当前输入(含必要上下文)
prompt = self.context_manager.build_prompt(new_input)
# 2. 增量推理(复用KV cache)
if self.kv_cache:
# 只对新token做forward
new_tokens = self.tokenizer.encode(prompt[-100:]) # 只处理最新部分
output = self.model.generate(
new_tokens,
past_key_values=self.kv_cache,
use_cache=True
)
# 更新cache
self.kv_cache = output.past_key_values
else:
# 第一轮,全量计算
output = self.model.generate(prompt, use_cache=True)
self.kv_cache = output.past_key_values
# 3. 流式输出
async for token in output.stream():
yield token
三、状态管理:热数据vs冷数据
数据类型 存储位置 更新策略
最近3-5轮对话 内存 每轮更新,滑动窗口
对话摘要 内存 + 定期落盘 每10轮或关键节点更新
工具调用结果 内存(当前会话) 按需缓存,会话结束清理
用户偏好/关键事实 SQLite/本地文件 长期存储,异步更新
KV Cache GPU/NPU内存 会话内保留,超时释放
关键优化:对话摘要采用"滚动更新",避免一次性重算所有历史:
def update_summary(self):
"""每N轮或检测到话题切换时更新摘要"""
if len(self.recent_messages) % 5 == 0: # 每5轮更新一次
# 只基于新消息和旧摘要生成新摘要
new_summary = self.llm.summarize(
f"旧摘要:{self.compressed_summary}\n新消息:{self.recent_messages[-5:]}"
)
self.compressed_summary = new_summary
四、工具调用优化:并行化 + 懒加载
不要让外部调用阻塞生成:
async def handle_tool_call(self, user_query):
# 1. 并行:生成回复的同时检索
gen_task = asyncio.create_task(self.generate_stream(user_query))
retrieval_task = asyncio.create_task(self.retrieve_info(user_query))
# 2. 流式输出首token(不等待检索完成)
async for token in gen_task:
yield token
# 检查检索是否完成
if retrieval_task.done() and not self.has_injected_retrieval:
retrieved_info = retrieval_task.result()
yield f"\n[参考信息:{retrieved_info[:50]}...]"
self.has_injected_retrieval = True
五、异步架构设计:生产-消费模式
class AsyncInferencePipeline:
def init(self):
self.input_queue = asyncio.Queue(maxsize=10)
self.output_queue = asyncio.Queue(maxsize=10)
async def producer(self, user_input):
"""预处理 + 上下文构建"""
# 1. 异步预处理(分词、实体识别)
processed = await self.preprocess_async(user_input)
# 2. 上下文检索(不阻塞UI)
context = await self.retrieve_context_async(processed)
await self.input_queue.put((processed, context))
async def consumer(self):
"""推理生成"""
while True:
user_input, context = await self.input_queue.get()
# 流式生成(立即返回首token)
async for token in self.model.stream_generate(user_input, context):
await self.output_queue.put(token)
# UI可以立即显示首token
self.input_queue.task_done()
async def stream_to_ui(self):
"""推送结果到UI"""
while True:
token = await self.output_queue.get()
# 更新UI(通过回调或消息总线)
self.ui_callback(token)
self.output_queue.task_done()
六、延迟控制黄金法则
首token时间 < 300ms:用KV cache + 预加载模型权重到显存
长对话降级策略:
• 0-10轮:保留完整上下文
• 10-30轮:开启摘要压缩
• 30+轮:启动话题检测,自动清理无关历史
内存警戒线:
if get_gpu_memory() > 0.8: # 显存使用超过80%
# 1. 清空最旧的KV cache
# 2. 压缩更多历史为摘要
# 3. 提示用户"我们开始一个新话题吧"
体验兜底:当延迟>2秒时,先显示"思考中..."的占位符,让用户感知系统在响应
七、推荐工具栈
• 推理框架:vLLM(支持PagedAttention)、TensorRT-LLM
• 向量检索:FAISS(本地)、ChromaDB
• 异步框架:asyncio + aiohttp
• 状态存储:SQLite + msgpack
• 监控:prometheus-client + 自定义指标
最关键的是建立延迟SLO:首token<300ms,后续token>30tok/s,内存增长<10MB/轮。超过阈值就触发降级(更激进的摘要、丢弃更早历史)。
这样设计,即使聊到100轮,体验也不会明显下降。