AI ChatGLM实践笔记

服务器是 24GB 显存的 RTX 3090,这个配置跑大的模型会比较吃紧,所以从一开始就要在模型选择和优化上下功夫。

Python 环境用的是 3.10,PyTorch 2.0.1,CUDA 11.8。

为什么折腾 ChatGLM

出发点其实很简单:想在自己的服务器上跑一个能用的对话模型。

选 ChatGLM 主要有三个原因:

  • 国产模型,中文对话体验相对较好
  • 开源版本对硬件要求相对合理
  • 智谱提供了比较完整的生态和工具链

但我当时忽略了一个现实问题:硬件资源有限。服务器是 24GB 显存的 RTX 3090,这个配置跑大的模型会比较吃紧,所以从一开始就要在模型选择和优化上下功夫。

环境准备和模型选择

先说环境,这步其实没什么花样,但有几个版本坑需要注意。

Python 环境用的是 3.10,PyTorch 2.0.1,CUDA 11.8。这个组合相对稳定,但不要盲目升级 PyTorch 版本,ChatGLM 的模型加载代码对某些版本兼容性不好。

模型选择上,我试了几个版本:

  • ChatGLM-6B:轻量,对话能力一般
  • ChatGLM2-6B:性能提升,但显存占用增加
  • GLM-4-9B:体验最好,但需要更精细的量化

最终选了 GLM-4-9B 的 4-bit 量化版本,在性能和资源之间取了个平衡。

模型下载可以直接从 Hugging Face 拉,或者用 ModelScope 镜像。国内环境建议后者:

pip install modelscope
python -c "from modelscope import snapshot_download; snapshot_download('ZhipuAI/glm-4-9b-chat')"

基础对话实现

最基础的对话实现其实不复杂,用 Hugging Face 的 Transformers 库就能搞定:

from transformers import AutoTokenizer, AutoModelForCausalLM

model_path = "ZhipuAI/glm-4-9b-chat"
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    trust_remote_code=True,
    device_map="auto",
    load_in_4bit=True
)

def chat(message, history=[]):
    input_text = tokenizer.apply_chat_template(
        history + [{"role": "user", "content": message}],
        tokenize=False,
        add_generation_prompt=True
    )
    inputs = tokenizer(input_text, return_tensors="pt").to(model.device)
    outputs = model.generate(**inputs, max_new_tokens=512, do_sample=True, temperature=0.7)
    return tokenizer.decode(outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)

这段代码能跑起来,但有几个问题:

  • 没有对话历史管理
  • 没有流式输出,用户体验差
  • 没有任何异常处理

工程实践里这些都不能少,但先让它跑起来再说。

踩坑记录

显存不够

第一个坑就是显存。3090 24GB 看起来不少,但加载 9B 模型还是紧张。

试过几个方案:

  • 降低 max_new_tokens:从 512 降到 256,效果还行
  • 调整 load_in_4bit 参数:用 NF4 量化,节省了一些
  • 批处理大小设为 1:避免中间状态累积

后来发现一个更实用的办法,用 device_map="auto" 让 Transformers 自动分配,比手动指定更可靠。

上下文管理

ChatGLM 的上下文格式和 OpenAI 不太一样。开始没注意这个问题,直接按 GPT 的格式传历史记录,结果回复质量明显下降。

正确做法是用 apply_chat_template 处理:

history = [
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么我可以帮你的吗?"}
]

# 错误方式:直接拼接
# correct_input = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history])

# 正确方式:用 template
input_text = tokenizer.apply_chat_template(history, tokenize=False, add_generation_prompt=True)

生成参数调优

默认的生成参数往往不是最优的。temperature、top_p、top_k 这些参数需要根据实际场景调。

我的经验:

  • temperature 0.7:平衡创造性和稳定性
  • top_p 0.9:控制输出范围
  • top_k 40:适当限制候选词
  • repetition_penalty 1.1:减少重复

这些参数不是固定的,不同任务需要不同调优。

完整的对话服务

把前面的坑补齐后,整个对话服务的核心流程大概是这样:

flowchart TD A[用户输入] --> B[消息预处理] B --> C{对话历史管理} C -->|新对话| D[初始化上下文] C -->|继续对话| E[加载历史记录] D --> F[应用 chat_template] E --> F F --> G[模型推理] G --> H[后处理] H --> I[更新历史] I --> J[返回结果]

实际代码里还要加异常处理、并发控制、超时机制这些工程化内容。比如:

from transformers import GenerationConfig

def chat_service(message, conversation_id, timeout=30):
    try:
        history = load_history(conversation_id)
        history = history[-8:]  # 限制历史长度

        input_text = tokenizer.apply_chat_template(
            history + [{"role": "user", "content": message}],
            tokenize=False,
            add_generation_prompt=True
        )

        inputs = tokenizer(input_text, return_tensors="pt").to(model.device)

        gen_config = GenerationConfig(
            max_new_tokens=512,
            temperature=0.7,
            top_p=0.9,
            repetition_penalty=1.1,
            do_sample=True
        )

        outputs = model.generate(**inputs, generation_config=gen_config)
        response = tokenizer.decode(outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)

        save_history(conversation_id, history + [{"role": "user", "content": message}, {"role": "assistant", "content": response}])

        return response

    except Exception as e:
        logger.error(f"Chat failed: {e}")
        return "抱歉,出了一点问题,请重试。"

性能优化

单纯跑起来不够,还要考虑性能。

几个有效的优化手段:

  • 模型并行:如果有多 GPU,用 device_map="balanced"
  • KV Cache:减少重复计算
  • 批处理:多个请求合并处理(如果延迟要求不高)
  • 静态图:固定输入长度,减少内存分配

实测下来,KV Cache 和批处理效果最明显。单次响应从 3-4 秒降到了 1.5-2 秒。

结果和边界

折腾了一个多月,总算有了一个能用的对话服务。效果不算惊艳,但在特定场景下表现不错。

日常问答、代码解释、文本摘要这些任务都能胜任,但复杂推理和长文本生成还是会暴露问题。

几个明确的边界:

  • 硬件资源限制了模型规模
  • 量化版本在某些任务上损失明显
  • 对话历史管理还是手工活
  • 多轮对话的一致性需要额外处理

一些闲话

这次折腾最大的感受是:模型本身只是起点,真正要把对话体验做好,工程化细节比想象中重要。

从模型选择到参数调优,从上下文管理到性能优化,每个环节都有坑要踩。这些在论文和文档里往往一笔带过,但实际落地时必须认真对待。

ChatGLM 作为一个开源模型,已经做得不错了。但要用好它,还需要根据自己的场景和资源做很多定制。这可能是所有开源模型的共同特点。

最后提醒一句:别一上来就追求"完美方案"。先跑起来,再迭代。对话系统尤其如此,用户体验需要在实际使用中慢慢调优。

版权声明: 本文首发于 指尖魔法屋-AI ChatGLM实践笔记https://blog.thinkmoon.cn/post/415-ai-chatglm-glm-chat-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!