ChatGLM-6B多轮对话:实现上下文记忆功能
1. 为什么多轮对话不是“默认就通”?
你可能已经试过,在网页界面上输入“你好”,它回“你好!有什么可以帮您?”,再问“今天天气怎么样”,它却一脸茫然:“我不清楚今天的天气情况”。这不是模型笨,而是——对话状态没被记住。
ChatGLM-6B本身是一个强大的中文对话模型,但它在底层设计上并不自动维护“你刚才说了什么、我上一句怎么答的”这类信息。就像一个人听你说话时,每句话都当作全新开场,不翻前一页笔记。真正的多轮对话能力,需要我们在使用层显式构造上下文、合理组织历史记录、控制输入长度与格式。
本镜像提供的Gradio WebUI之所以能支持连续聊天,正是因为背后已封装了完整的上下文管理逻辑。但如果你打算调用API、写脚本集成,或想真正理解它“为什么能记住”,就必须掌握这套机制。本文不讲抽象原理,只带你一步步看清:上下文是怎么拼起来的、为什么不能无限制堆历史、哪些操作会让记忆“断片”,以及如何在实际工程中稳稳用好这个能力。
2. ChatGLM-6B的上下文记忆原理
2.1 模型视角:它只“看”你给它的那一段文本
ChatGLM-6B本质是一个自回归语言模型。它不会主动保存任何状态,每次推理都只接收一个输入字符串(prompt),然后逐字生成回复。所谓“记忆”,其实是把之前的对话轮次,按特定格式拼进当前输入里,让模型“看到”整个对话流。
官方推荐的格式是:
[Round 1]\n问:xxx\n答:yyy\n[Round 2]\n问:aaa\n答:bbb\n[Round 3]\n问:ccc\n``` 注意最后没有“答:”,因为这是当前要预测的部分。模型的任务就是接上最合理的下一句。 这种格式不是随便定的——它是训练时数据的真实分布。ChatGLM-6B在预训练和SFT阶段,大量使用了带`[Round N]`标记的多轮对话样本。所以当输入符合该模式时,模型才能准确识别“这是第几轮”、“前面发生了什么”、“现在该回答什么”。 ### 2.2 镜像实现:WebUI如何自动帮你拼上下文 打开镜像的Gradio界面,点击“清空对话”后,所有历史消失;而正常聊天时,每轮问答都会被实时存入前端内存,并在每次提交新问题前,自动组装成标准格式传给后端。 后端`app.py`中的关键逻辑如下(简化示意): ```python def build_prompt(history, query): prompt = "" for i, (q, a) in enumerate(history): prompt += f"[Round {i+1}]\n问:{q}\n答:{a}\n" prompt += f"[Round {len(history)+1}]\n问:{query}\n答:" return prompt这个函数确保:
- 历史轮次严格编号,从1开始递增;
- 每轮以
\n分隔,避免语义粘连; - 最后固定加上
答:作为生成起点,引导模型只输出回答部分。
你不需要自己写这段代码——镜像已内置。但理解它,是你调试、定制、或对接其他系统的前提。
2.3 上下文长度限制:62亿参数也扛不住无限回忆
ChatGLM-6B最大上下文长度为2048个token(注意:是token,不是字数)。中文里1个汉字≈1.5~2个token,标点、空格、特殊符号也都算。这意味着:
- 一段500字的对话历史,可能已占去1000+ token;
- 若用户连续发10条长消息,再加模型回复,很容易突破上限;
- 超出部分会被截断——通常是从最前面的历史开始丢弃(即“遗忘最早的话”)。
镜像默认采用滑动窗口策略:始终保留最近N轮,确保最新对话完整参与推理。你在WebUI里看到的“历史记录”可能有20条,但真正送进模型的,往往只有最后5~7轮。
这不是缺陷,而是权衡。强行保留全部历史,会导致:
- 推理变慢(计算量随长度平方增长);
- 回复变僵硬(模型被冗余信息干扰,抓不住重点);
- 显存溢出(尤其在单卡部署时)。
所以,“记忆”本质上是一种有选择的注意力分配——记住最近的,放下久远的,这反而更接近人类对话的真实逻辑。
3. 实战:三种方式调用上下文记忆能力
3.1 方式一:直接使用WebUI(零代码,适合验证与演示)
这是最快上手的方式,也是镜像最推荐的日常使用路径。
操作步骤:
- 启动服务:
supervisorctl start chatglm-service - 建立SSH隧道(按文档执行)
- 浏览器访问
http://127.0.0.1:7860 - 在输入框中连续提问,例如:
- 第一轮:“请用三句话介绍李白。”
- 第二轮:“他和杜甫是什么关系?”
- 第三轮:“他们一起喝过酒吗?”
你会明显感受到模型在“顺着聊”:第二轮知道“他”指李白,第三轮能关联到“李白和杜甫”的交集。这就是上下文生效的直观体现。
小技巧:
- 温度(Temperature)调至0.7左右,平衡稳定性与自然度;
- 若某轮回答跑偏,不必重开页面,点击「清空对话」即可重置上下文;
- 输入过长时,界面右下角会提示“输入已截断”,此时建议拆分成短句。
3.2 方式二:调用本地API(Python脚本,适合轻量集成)
镜像未开放公网API,但你可在服务器本地通过HTTP请求调用。后端app.py已内置FastAPI服务(监听0.0.0.0:8000),提供/chat接口。
示例代码(需安装requests):
import requests import json # 本地服务地址 url = "http://127.0.0.1:8000/chat" # 初始化对话历史(空列表表示新对话) history = [] # 第一轮 payload = {"query": "你好", "history": history} response = requests.post(url, json=payload) data = response.json() answer = data["response"] history.append(("你好", answer)) print(f"Bot: {answer}") # 第二轮:历史已携带 payload = {"query": "你是谁?", "history": history} response = requests.post(url, json=payload) data = response.json() answer = data["response"] history.append(("你是谁?", answer)) print(f"Bot: {answer}")关键点说明:
history参数必须是字符串元组列表,格式为[("问1", "答1"), ("问2", "答2")];- 每次请求都需传入完整历史,服务端不保存状态(无session);
- 返回的
"response"字段即模型生成的回答,不含前缀“答:”; - 错误处理建议添加:检查
response.status_code及data.get("error")。
这种方式让你完全掌控上下文生命周期,适合嵌入到内部工具、自动化流程中。
3.3 方式三:直连模型(高级定制,适合二次开发)
若你需要深度控制tokenization、修改prompt模板、或接入其他框架(如LangChain),可绕过WebUI和API,直接加载模型。
环境准备(已在镜像内预装):
- 模型权重位于
/ChatGLM-Service/model_weights/ - 使用Transformers库加载,无需额外下载
最小可运行示例:
from transformers import AutoTokenizer, AutoModel import torch # 加载分词器与模型(量化版,节省显存) tokenizer = AutoTokenizer.from_pretrained("/ChatGLM-Service/model_weights/", trust_remote_code=True) model = AutoModel.from_pretrained("/ChatGLM-Service/model_weights/", trust_remote_code=True).half().cuda() # 构建多轮prompt(手动拼接) history = [ ("北京的天气怎么样?", "北京今天晴,气温18到25摄氏度。"), ("那上海呢?", "上海今天多云,气温20到28摄氏度。") ] query = "广州呢?" # 拼接标准格式 prompt = "" for i, (q, a) in enumerate(history): prompt += f"[Round {i+1}]\n问:{q}\n答:{a}\n" prompt += f"[Round {len(history)+1}]\n问:{query}\n答:" # 编码并生成 inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=256, do_sample=True, temperature=0.8, top_p=0.9 ) response = tokenizer.decode(outputs[0], skip_special_tokens=True) # 提取“答:”之后的内容 final_answer = response.split("答:")[-1].strip() print("Bot:", final_answer)优势与适用场景:
- 完全自由控制输入格式(比如改用
<|user|>/<|assistant|>标签); - 可插入自定义system prompt(如“你是一名严谨的气象助手”);
- 支持LoRA微调、知识注入等高级操作;
- 适合构建企业级对话引擎,而非简单问答。
注意:此方式需一定PyTorch基础,且需关注显存占用(6B模型FP16约13GB)。
4. 常见问题与避坑指南
4.1 为什么有时“记着记着就忘了”?
最常见原因有三个:
- 历史过长被截断:如前所述,2048 token硬限制。解决方案:在拼接前主动精简历史,例如只保留最近3轮,或对长回答做摘要压缩。
- 格式不规范:少了一个
\n、多了一个空格、[Round 1]写成[round 1],都可能导致模型无法识别轮次结构。务必严格遵循[Round N]\n问:xxx\n答:yyy\n格式。 - 前后端不一致:WebUI用的是
chatglm-6b-int4量化版,而你直连模型用的是fp16全精度版,二者对同一prompt的输出可能略有差异。生产环境建议统一使用同一版本。
4.2 如何让模型“记得更牢”?三个实用技巧
显式锚定角色与任务
在第一轮加入清晰指令,例如:
“你是一名资深中医师,请根据我的症状描述给出专业建议。我会连续描述多个症状,请综合判断。”
这比单纯聊天更能稳定模型的角色认知。用关键词唤醒记忆
当对话跨度较大时,在新问题中复述关键信息:
“刚才你说张仲景的《伤寒论》是经典,那其中‘六经辨证’具体指哪六经?”
比直接问“六经辨证是什么?”更容易触发关联。人工干预历史列表
不必机械追加每轮。例如用户问“总结一下我们聊的”,你可以将此前所有问答整理成一段摘要,作为新query发送,而不是塞进history里。
4.3 多用户并发时,上下文会串吗?
不会。因为本镜像的WebUI和API均采用无状态设计:
- 每次请求都携带完整
history,服务端不维护任何全局变量; - Supervisor守护的是进程,不是会话;
- Gradio前端每个浏览器标签页独立维护自己的
history数组。
这意味着:
你和同事同时用不同浏览器访问,互不影响;
同一浏览器开多个标签页,各自历史隔离;
但刷新页面后,当前页历史丢失(因存储在前端内存)。
如需持久化历史,需自行扩展后端,接入Redis或数据库——这已超出本镜像定位,属于业务层增强。
5. 总结:让上下文记忆真正为你所用
ChatGLM-6B的多轮对话能力,不是魔法,而是一套可理解、可控制、可优化的工程实践。它不依赖神秘的状态管理,而是通过标准化的文本格式 + 精准的长度控制 + 合理的交互设计,把“记忆”转化成了确定性的输入处理。
回顾本文要点:
- 上下文记忆的本质,是把历史对话按
[Round N]格式拼进当前输入; - 镜像WebUI已封装完整逻辑,开箱即用,适合快速验证;
- 本地API调用给予你轻量集成能力,history参数即上下文载体;
- 直连模型提供最大自由度,适合深度定制与二次开发;
- 避免“遗忘”的关键是:控长度、守格式、巧引导。
多轮对话的价值,从来不在“能聊几轮”,而在“能否让每次对话都更接近一次真实的人类交流”。当你开始思考“用户此刻最需要记住什么”,而不是“模型最多能塞多少字”,你就真正掌握了这项能力。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。