ChatGLM+LangChain实战:从本地量化到ChatPDF与RAG全流程解析
2026/9/16 3:49:02 网站建设 项目流程

简介:面向大语言模型与生成式人工智能工程实践者的实例代码合集,按五大专题整理,涵盖对话大模型调用、语言模型应用编排、文档问答实现、向量数据库构建,以及扩散模型和主流绘画工具的使用;内容适合刚接触AIGC的读者按步骤实验,也方便有基础的开发者直接复用代码。压缩包共101个文件,以36个笔记本文档为核心,另有14个Python脚本、12个编译模块、11个示例文件及JSON、TXT、Markdown等配置;整体约138.21MB,按专题归置,目录清晰,便于定位。目前已有340人浏览学习,资源价值在于串联文本生成、对话问答、文档交互到向量检索与图像创作的完整链路,尤其对文档问答嵌入检索、流程编排等关键环节给出可对照实例;完成专题后可独立搭建本地知识库问答机器人,也可在现有项目接入对话生成、向量存储或绘画能力,节省大量搜集资料与从零调试的时间。

1. 2023 AIGC工程合集:ChatGLM、LangChain与生成式工作流的一次盘点

这组2023年的大模型LLM工程实例代码,适合两类人看:一类是刚开始在本地接大模型,对ChatGLM如何调用和量化还没有完整认知的开发者;另一类是已经知道Prompt Engineering只解决“送料”问题,但想看看RAG链路里一个PDF文档如何被切片、转成向量再检索、最后拼成答案的工程师。合集里五个专题表面上各占一个notebook,实际上是一条完整主线:先打通模型端本地推理,再通过LangChain把模型编排成能回答业务问题的工具,最后落到ChatPDF、向量数据库和文生图环节做工程验证。我在复跑时最关心的是三件事:代码能不能脱离原环境直接跑通,跑不通时应该去查模型调用还是查向量索引,以及中间哪些参数按默认值设一定会出问题。这个顺序也正好对应下面的章节安排。

2. ChatGLM调用与本地量化:显存边界、精度损失与脚本化批量推理

2.1 本地部署与闭源API的分界

很多做LLM应用的人第一反应是调闭源对话API,省事、效果也稳定。但我接触的企业场景里,数据进出边界才是最现实的约束:文档内容不能出内网、请求频率不能出现配额波动、每条Prompt都要留下来审计。这种情况下,开源模型自部署几乎是唯一路径。ChatGLM-6B是2023年中期个人和中小团队实践里最有代表性的模型之一,它的权重体积压在消费级显卡能覆盖的范围,中文指令跟随能力在同体量里也属于第一梯队。

不过把模型从HuggingFace拉下来只是第一步。同样的参数、同样的Prompt,在不同机器上的效果差异往往来自加载精度和推理调度方式。这组notebook里几乎每步都要依据当前显存做调整,按默认参数直接跑,大概率会在这里遇到第一个OOM。所以我建议先跑通最小调用,把加载、对话、批量生成拆开验证,再考虑加业务逻辑。

2.2 最小可用调用与history传递

import torch from transformers import AutoModel, AutoTokenizer checkpoint = "THUDM/chatglm-6b" tokenizer = AutoTokenizer.from_pretrained(checkpoint, trust_remote_code=True) model = AutoModel.from_pretrained(checkpoint, trust_remote_code=True).half().eval()

这几行代码有三个细节值得说明。trust_remote_code=True必须保留,因为ChatGLM在transformers目录里带了自定义模型代码,不开启会在加载阶段直接抛错;安全做法是先手动下载权重,确认内容后放在本地目录再从本地路径载入,避免每次启动都去远程拉取代码。.half()把模型切到fp16,显存占用减半,但如果显卡总显存只有8GB,这里就不要再走fp16,应该改用下一节的量化配置。.eval()用于切到推理模式,否则dropout层会继续随机遮蔽,导致同样输入每次输出差异偏大。

test_questions = [ "什么是检索增强生成?", "它和直接改写Prompt有什么区别?", ] with torch.no_grad(): for q in test_questions: response, history = model.chat(tokenizer, q, history=[]) print("Q:", q) print("A:", response)

这段批量对话逻辑里,隐蔽问题在historymodel.chat返回的第二个值是更新后的历史记录,如果只取response丢history,下一轮对话就没有上下文;并发请求时不同线程必须各自持有独立的history列表,不能共享同一个对象,否则多线程会出现内容互相串扰。实践里我习惯先固定几组相同问题重复跑两次,如果答案差异很大,说明模型未进入eval模式或temperature设置偏高,再检查上述两个点。

2.3 量化档位与显存换算

模型位宽每降一档,显存大约省一半,代价是输出稳定性和数字表达精度。下表是ChatGLM-6B在实际部署中的参考值,注意同类结论对7B以上模型不一定成立。

加载方式显存占用参考值适用硬件常见风险
fp16全精度12 ~ 14 GB3090、4090、V100 16G上下文超1024后占用继续上涨
int8量化9 ~ 11 GB2080Ti、A4000长对话末尾偶尔出现重复分句
int4量化6 ~ 8 GB3060 Laptop、4060 Laptop专有名词和数字容易生成错位
from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, ) model = AutoModel.from_pretrained( checkpoint, quantization_config=bnb_config, device_map="auto", trust_remote_code=True, )

这里用到了bitsandbytes的NF4量化,bnb_4bit_use_double_quant会把量化常数再做一次量化,进一步省显存,代价是加载时间变长。实际项目中如果纯做中文知识库问答,int8和int4的差别在短文本上不明显,但一旦检索回来的片段里含长数字编号、产品型号或金额统计,低位宽下错位概率显著上升。所以我的建议是:聊天机器人可以直接int4上8GB卡,正式文档问答至少要留出int8空间。

2.4 推理参数与批量生成稳定性

批量推理时,真正影响成功率的是三个生成参数:max_lengthtemperaturetop_p。把max_length直接设4096是最常见的错误,小显存机器第二轮就OOM。我一般先用256做冒烟测试,跑通后调到512,只有长文综述任务才开到1024以上。

response = model.chat( tokenizer, question, history=history, max_length=512, temperature=0.2, top_p=0.7, )[0]

temperature在0.1到0.3之间适合知识库问答,超过0.8适合创意文案,不要和top_p同时调高,否则两者叠加会加速幻觉产生。做评测对比时必须固定这两个参数,只改Prompt文本,结果才具有可比性。顺带提醒,批处理任务建议循环末尾加一次torch.cuda.empty_cache(),这能缓解多轮跑批时显存碎片导致的偶发OOM,但不要在每个请求里都调用,它会拖慢整体吞吐。

3. LangChain链式编排:Prompt模板、文档Loader与RetrievalQA的组装逻辑

3.1 为什么要引入编排框架

裸调用ChatGLM解决的是“模型能用”,但实际知识库项目需要把文档加载、文本切片、向量索引、检索召回、Prompt拼装、模型生成全部串在一个流程里。这套流程如果全用原生代码手写,每个项目都要重复实现一遍;LangChain在2023年沉淀了一下这些常见模式,核心价值是两条:第一,把模型调用抽象成统一的Chain接口;第二,把文档处理和检索逻辑封装成标准组件,让开发者把精力放在业务参数上,而不是纠结胶水代码。

3.2 把ChatGLM服务接进LangChain

这里有一个版本差异需要先说明。2023年大部分notebook里用的还是langchain.llms.ChatGLM;到了2024年后,组件被拆到了langchain_community包,接口也向OpenAI兼容协议收敛。所以如果直接抄旧代码遇到ImportError,优先检查LangChain版本,再做两行路径替换。

from langchain.llms import ChatGLM llm = ChatGLM( endpoint_url="http://127.0.0.1:8000", max_token=512, temperature=0.2, )

endpoint_url指向一个本地HTTP服务,常见的做法是用fastchatvllm把ChatGLM包成OpenAI兼容接口,LangChain只管发请求,不做任何模型加载。这种拆法在工程上更干净,原因是模型预热一次后常驻显存,后续多个Chain可以复用同一个服务进程,不用反复加载权重。

3.3 Prompt模板链:先做参数化再谈优化

提示词工程的第一步,是把Prompt从字符串变成可复用模板。比如要让模型按固定结构回答问题,先定义模板,再把业务变量传进去。

from langchain.prompts import PromptTemplate from langchain.chains import LLMChain template = """请基于以下文档片段回答问题。 片段:{context} 问题:{question} 要求:如果片段中没有答案,直接说“无法回答”,不要编造。""" prompt = PromptTemplate( input_variables=["context", "question"], template=template, ) chain = LLMChain(llm=llm, prompt=prompt) print(chain.run(context="LangChain是一个编排框架", question="LangChain是什么?"))

这里input_variables里的变量名必须和template中的花括号占位符完全一致,少一个都会在运行时抛ValueError。中文Prompt模板里有个细节:占位符前后最好加换行,因为ChatGLM系列对紧凑拼接文本的跟随能力较弱,换行能明显降低内容混在一起的概率。

3.4 组装RetrievalQA:从文档到答案的完整链路

from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.vectorstores import FAISS from langchain.embeddings import HuggingFaceEmbeddings from langchain.chains import RetrievalQA text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", ";", " "], ) docs = text_splitter.create_documents([source_text]) embeddings = HuggingFaceEmbeddings( model_name="shibing624/text2vec-base-chinese" ) vectorstore = FAISS.from_documents(docs, embeddings) qa = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever( search_kwargs={"k": 3} ), )

这段代码里,RecursiveCharacterTextSplitterseparators参数顺序就是切分优先级:先按段落分,再按句号分,最后再按空格兜底。这套顺序在中文场景下比单纯按字符数硬切要合理得多,能避免句子被拦腰截断。HuggingFaceEmbeddings内部用的是sentence-transformers,第一次运行时会把text2vec-base-chinese下载到本地缓存目录;离线环境需要提前手动放置模型文件。k是召回片段数,文档库规模在几百段以内设为3到4足够,调大会把噪音片段带进上下文。

3.5 三种chain_type怎么选

RetrievalQA的chain_type参数决定了文档片段如何喂给LLM,这也是调优时最容易忽略的点。

chain_type处理方式适用场景主要缺陷
stuff全部片段拼进一个Prompt文档短、片段少片段超长时直接超上下文窗口
map_reduce每个片段独立回答再合并文档长、问题需要覆盖全局合并阶段容易丢掉关键数值
refine逐个片段迭代修正答案长文逐步推理调用次数多、耗时翻倍

实际项目中我常用的策略是:先stuff跑通,如果出现截断或超时,再切map_reducerefine适合需要答案被逐步打磨的场景,但对成本敏感的生产环境不友好。判断依据很简单,看输入片段总量是否超过模型上下文窗口的一半,超过就升级处理方式。

4. ChatPDF工程实现:文件解析、切片重叠与向量召回路径中的埋点

4.1 ChatPDF的核心链路

代码包里的chatpdf-2-embedding.ipynb做的是“PDF解析→文本清洗→切片→Embedding→向量索引→相似度召回”这一段,这也是五个专题里最贴近知识库项目的部分。ChatPDF类应用后端本质就是一个RAG闭环,区别只在于文档解析的复杂度。真正决定问答质量的不是模型本身,而是解析后文本的完整度和切片边界是否合理。一个扫描版PDF就算用最强的大语言模型,也无法从一张图片里直接还原表格逻辑,所以第一步要把文本抽取做实。

4.2 PDF文本抽取与清洗

import fitz def extract_pdf_text(path: str) -> str: doc = fitz.open(path) pages = [] for page in doc: text = page.get_text() if text: pages.append(text) doc.close() return "\n\n".join(pages)

这里用PyMuPDF做抽取,它在纯文本PDF上的抽取速度和排版保留度比pypdf好,尤其适合技术文档里的段落结构。如果PDF是扫描版,get_text()返回空串,常见方案是接入OCR服务或本地OCR模型再做文本拼接。抽取后一定要检查文本里的特殊字符,全角半角、零宽空格在后续切片时会影响separators匹配,一定程度导致分句失效。

4.3 切片参数与召回质量的调优实验

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", "!"], ) chunks = splitter.split_text(full_text)

chunk_size是目标片段长度,chunk_overlap是相邻片段重叠的字符数。重叠的意义是避免一个完整语义被切走一半,比如一个问题的答案跨了两个切片,没有重叠就会在召回时丢后半句。但overlap也不是越大越好,过大会让多个片段内容高度相似,检索结果重复,浪费有限的上下文窗口。下面是我在几十份PDF上实测后的经验值,不同文档类型结论略有差异。

chunk_sizechunk_overlap问答表现
20040定位准,但答案被截断,需要模型自行补齐上下文
400 ~ 50080 ~ 100大多数技术文档的最佳区间,引用完整
1000150概括类问题好,但检索目标被放宽,无关内容混入概率上升

4.4 向量库召回与评分验证

embedding环节最常见的坑,是索引阶段和检索阶段用了不同模型。FAISS数据库保存的只有向量矩阵和一个整数ID映射,不记录当时用的是哪个embedding模型。如果你的索引是text2vec-base-chinese生成768维向量,后面换成512维的模型做检索,代码会报维度不匹配错误;如果两个模型维度碰巧相同,则不会报错但召回质量明显降低——这是最隐蔽的一类问题。

question = "模型的损失函数怎么定义" hits = vectorstore.similarity_search_with_score( question, k=3 ) for i, (doc, score) in enumerate(hits): print(i, round(score, 4), doc.page_content[:80])

注意similarity_search_with_score返回的距离类型取决于构建FAISS索引时的distance_strategy。默认走L2欧氏距离,分值越小表示越相似;如果建向量库时显式设置过COSINE策略,这个分值是0到2之间的余弦距离,依然越小越相似。很多人在阅读日志时习惯性认为score越大越相关,这在FAISS默认配置下是判断反了。比较稳妥的做法是每次调整切片参数后,用同一组问题重新跑一遍召回测试,记录分数分布和命中的片段序号,再判断参数是否合理。

5. 部署验证与踩坑清单:五个专题的检查顺序与高频报错定位

5.1 按依赖顺序跑通五个专题

建议按ChatGLM、LangChain、向量数据库、ChatPDF、文生图的顺序执行,前一个专题是后一个的依赖基础。具体到操作上,先把第二章的对话脚本抽成一个独立模块,确认模型加载和推理稳定后,再进第三章做LangChain链路测试。每切一个专题都要重新启动一个干净的Python进程,避免上一个notebook里的变量和CUDA显存残留影响判断。

5.2 三个高频报错的快速定位

第一个是BitAndBytes相关错误:通常是bitsandbytes版本与CUDA版本不匹配,表现为加载int4模型时抛CUDA error。常见做法是把bitsandbytestransformers同时固定到2023年的版本组合,跨大版本混用大多会触发此类问题。

第二个是Embedding dimension mismatch:按4.4节的思路,先确认两个环节的embedding模型路径一致,再确认模型维度。text2vec-base-chinese输出768维,m3e-small输出512维,混用必报错。

第三个是PDF解析后得到空文本:优先检查文件是否为扫描版,用fitz能否抽取文本;其次检查PDF是否有密码保护,有密码需要先调用doc.need_pass判断再解锁。

5.3 SD/MJ环节的工程接入注意点

第五个专题里的StableDiffusion和Midjourney,严格说不是语言模型工程,而是AIGC管线里的多模态部分。本地SD脚本重点检查采样器参数:步数设30到40比较常见,太高的步数不会显著提升画质,只拉长单张生成时间;分辨率不要盲上大图,显存不足时先锁短边。Midjourney如果走接口接入,则需要重点关注图片回传之后的URL过期逻辑,常见做法是生成结果落地到本地对象存储再转给下游,不直接依赖原始回传地址。

提示:这组专题验证到文生图阶段时,显存压力通常来自“LLM进程和SD进程同时驻留”。我一般把两个服务拆到不同GPU或不同机器,在同一张卡上共存会频繁触发OOM,且两个进程相互拖慢。检查方式很简单:起SD脚本后,再执行一次ChatGLM推理,观察显存峰值和单请求耗时是否出现异常上抬。如果在同一张卡上必须共存,就给两个服务分别设定CUDA_VISIBLE_DEVICES并限制推理并发数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询