LangChain这个工具之前比较早看到了,当时觉得它是一个把不同来源的大语言模型都转换为统一的接入口的库,可以在上面利用这个Chain式调用搞开发。我那会儿理解比较浅,就搁置在那儿没有去学,只是觉得会比较方便的构建应用,具体是什么应用没有太大概念。

最近刷B站看到一些Up主在用这个工具做开发,看了一会儿还有点儿意思,那个|也就是链感觉和Linux管道很像啊!DNA动了!说到这个管道,我自己其实工作中写了一个库可以用来完成有顺序的相互依赖的函数调用,可以设计的更为复杂一些,比如返回多结果的函数,比如一个函数依赖前面多个函数的输出,同时可以多线程执行平行的函数,后续我会将库发布在github上。可能还需要增加一个类似于这种管道符号的支持,应该是通过符号重载,后面试一试。

不过,说实话我还是对这个库的功能不是太清楚,里面函数有点多封装的有点深,引用库有点乱(比如HumanMessage可以从很多位置import进来),文档有点乱(最近又更新了一个大版本,两个版本之间文档差别很大!),我的水平还不够驾驭它。不过我的直觉告诉我不宜在这个库上花费过多时间,因为最为根本的是模型的输入(prompt)和输出,如何通过调整输入得到想要的输出才是最为关键的,进而才能对正确的输出进行正确的解析和后续正确的调用,不然一切都是徒劳。LangChain只是这个过程中方便使用的一个连接,如果很清楚OpenAI的API的输入输出,可以自己写程序去完成LangChain一样的事情。

下面的介绍不分是ChatGPT4给出的,后续我理解之后会逐步更新对于这个库的理解。下面很多代码的解读是我根据对python的理解反推回去可能的情况,不一定对。

介绍

LangChain是一个用于构建大型语言模型(LLM)应用程序的开源库。它提供了一组工具和抽象,使得开发者能够更轻松地利用 LLM 的强大功能,构建复杂的自然语言处理(NLP)应用。

  • 链式调用(Chains):LangChain 提供了链式调用的概念,允许开发者将一系列的自然语言处理任务串联在一起。这可以包括文本生成、文本摘要、信息提取等操作。
  • 提示模板(Prompt Templates):帮助开发者构建和管理提示模板,这些模板可以用于向 LLM 发送结构化的请求。
  • 文档加载和处理(Document Loaders and Processors):支持从各种数据源(如文件、API、数据库)加载文档,并提供工具对文档进行预处理和分割,以便更好地处理和分析。
  • 嵌入(Embeddings):提供多种嵌入模型的封装,方便开发者将文本转换为向量表示,用于相似性搜索、聚类等任务。
  • 存储(Storage):提供对嵌入和其他数据的持久化存储解决方案,支持多种数据库和存储后端。
  • 检索增强生成(Retrieval-Augmented Generation, RAG):结合检索和生成模型,构建能够根据外部知识库生成答案的系统。
  • 工具和代理(Tools and Agents):允许集成第三方工具和服务,构建更强大的 NLP 应用。

使用场景:

  • 对话系统(Chatbots):构建智能对话系统,能够理解和生成自然语言,提供更人性化的交互体验。
  • 信息检索与问答系统(QA Systems):结合文档检索和生成模型,实现对大规模知识库的问答功能。
  • 自动化文本处理(Automated Text Processing):自动化完成文本摘要、翻译、分类等任务。
  • 个性化推荐(Personalized Recommendations):利用用户的历史数据和嵌入模型,实现个性化推荐系统。

上面内容生成自"ChatGpt4"。

组成

  • LangSmith:一个用于将过程可视化的工具。
  • LangServe:将服务发布。
  • Templates:写代码的模板。
  • LangChain
    • 构建链式(Chain)的调用工具:将不同功能像Linux管道一样串起来。
    • 构建智能体(Agent):可以根据对话智能的安排任务调用工具。
    • 构建检索器(Retrieval):根据对话去文档中检索相关信息。
    • 更为底层的东西(LangCore):并行、流、异步啊,暂时不用接触。

主要是了解的就是LangChain部分。

示例

这是一个官方的代码示例,暂时不需要执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os

from langchain_community.chat_models import QianfanChatEndpoint
from langchain_core.language_models.chat_models import HumanMessage

os.environ["QIANFAN_AK"] = "Your_api_key"
os.environ["QIANFAN_SK"] = "You_secret_Key"

chat = QianfanChatEndpoint(
streaming=True, # 使用流式输出
# model="ERNIE-Bot" # 可以制定模型名
)
messages = [HumanMessage(content="Hello")]
# 输出
chat.invoke(messages)
# 还可以指定模型参数
# chat.invoke( [HumanMessage(content="Hello")], **{"top_p": 0.4, "temperature": 0.1, "penalty_score": 1} )

# 或者是使用流式输出
try:
for chunk in chat.stream(messages):
print(chunk.content, end="", flush=True)
except TypeError as e:
print("")

注意到上面的QianfanChatEndpoint是需要什么QIANFAN_AKQIANFAN_SK,千帆也就是百度的文心一言的API接口,这里的两个Key是需要购买的,是你的凭证和入场码之类的吧。也就是说你在使用上面的示例的时候需要联网+申请文心一言API+购买Token(刚看了千帆网站,说文心大模型两大主力模型ERNIE Speed、ERNIE Lite全面免费 ,后面我试一试),除了百度还有其他在线模型可以用,可以去这里找到Chat models。如果有Key的话只需要添加环境变量:

1
2
export QIANFAN_AK=xxxxxx
export QIANFAN_SK=xxxxxx

就可以执行上面代码了。

到这里我其实想问:难道不能加载本地的Chat模型吗?

我谷歌了一下:

老外也有发牢骚说为什么注重于OpenAI好像忽略了本地大语言模型,也有提问能否加载本地模型的。对于:

  1. 为什么LangChain关注与OpenAI而不是本地的LLM? 我理解的是OpenAI作为大语言模型方向领导者,绝对是一流的,一流的企业制定标准,很多模型的执行代码中都有`openai_api.py`之类的脚本就是为了兼容OpenAI的标准。

    OpenAI API标准:OpenAI API,这是一个示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    curl https://api.openai.com/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $OPENAI_API_KEY" \
    -d '{
    "model": "gpt-4o",
    "messages": [
    {
    "role": "system",
    "content": "You are a helpful assistant."
    },
    {
    "role": "user",
    "content": "Hello!"
    }
    ]
    }'

    返回:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    "id": "chatcmpl-123",
    "object": "chat.completion",
    "created": 1677652288,
    "model": "gpt-3.5-turbo-0125",
    "system_fingerprint": "fp_44709d6fcb",
    "choices": [{
    "index": 0,
    "message": {
    "role": "assistant",
    "content": "\n\nHello there, how may I assist you today?",
    },
    "logprobs": null,
    "finish_reason": "stop"
    }],
    "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
    }
    }

    就是按照特定的数据格式进行API的请求,然后会返回的特定数据格式的结果。

  2. 为什么不能用本地的模型? 其实是可以用的,只不过很麻烦。我觉得现在各自的大模型的加载预测等等都很不一样,如果说将他们都能统一起来,那么代码会非常多样,而且需要随时基于不同的模型进行更新,这对于直接Call API来说要麻烦很多,Call API就是指定了网址,指定了参数,指定各种Key就能得到结果。

这次我将尝试使用清华智谱的ChatGLM3,上半年很受欢迎的模型来测试一下。

之前的文章中曾经本地部署了ChatGLM4,你可能会问为什么不用最新的?一想到这个我脑子就浮现那天满屏红色的情景。主要原因并不再与生成OpenAI的API,而是CUDA11.8的vllm的安装和导入上搞了大半天,要么是安装不了,要么就是安装了导入执行OpenAI API的时候报各种错,懒得折腾了,等后面把CUDA更新一下再弄弄。

ChatGLM3-6b模型的部署

同样的来一遍模型部署吧!

建立conda环境:

1
2
conda create -n ChatGLM3 python=3.10
conda activate ChatGLM3

在ChatGLM3的代码里面,有关于生成OpenAI API的代码,到时候执行就行了。

1
2
3
4
5
6
7
git clone https://github.com/THUDM/ChatGLM3.git
cd ChatGLM3

# 安装依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装魔搭用来高速下载模型文件
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple modelscope

如果报错什么存储不够,就这样:

1
2
mkdir tmp
export TMPDIR=./tmp

我是CUDA11.8,安装相应版本的torch:

1
2
pip uninstall torch torchvision torchaudio
pip install torch==2.3.0 torchvision==0.18.0 torchaudio==2.3.0 --index-url https://download.pytorch.org/whl/cu118

生一个代码文件download_model.py,里面写上:

1
2
3
4
5
6
7
from modelscope import snapshot_download

# 下载chatglm3模型文件
model_dir = snapshot_download("ZhipuAI/chatglm3-6b", cache_dir="models")
# 下载文本embedding模型,据说是最好的中文embedding模型
# 原本是BAAI/bge-m3,抱脸不好下载,这里找到魔搭上的镜像
model_dir = snapshot_download("Xorbits/bge-m3", cache_dir="models")

执行代码下载:

1
2
3
4
5
python download_model.py

# 下载完成之后,models文件夹会有下面两个文件夹
ls models
# Xorbits ZhipuAI

执行openapi API服务,这里相当于在本地生成了一个API:

1
2
3
4
5
6
7
8
9
cd openai_api_demo

# 路径最好写绝对路径,我这里偷懒了
# 语言模型
export MODEL_PATH=../models/ZhipuAI/chatglm-6b
# 文本embedding模型,据说是最好的中文embedding模型
export EMBEDDING_PATH=../models/Xorbits/bge-m3

python api_server.py

默认情况下是在http://127.0.0.1:8000,如果需要更改,直接去api_server.py 找到相应位置去改就行了。

官方比较贴心还给了一个测试的代码,用langchain测试API能不能用:

1
python langchain_openai_api.py

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Human (or 'exit' to quit): 你是谁
ChatGLM3: 我是一个名为 ChatGLM3 的智能助手,是清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型。我的任务是针对用户的问题和要求提供适当的答复和支持。
Human (or 'exit' to quit): 请帮助我理解一下LangChain
ChatGLM3: LangChain 是一种基于区块链技术的语言服务生态系统,旨在为用户提供跨语言的沟通和服务。它利用区块链的去中心化、安全、透明等特性,构建了一个基于智能合约的生态系统,支持多种语言之间的翻译、交流、支付等服务。

LangChain 的主要特点包括:

1. 去中心化:LangChain 采用区块链技术,不需要信任任何中心化的服务器或机构,从而保证了系统的安全性和透明性。

2. 多语言支持:LangChain 可以支持多种语言之间的交流和翻译,用户可以通过平台进行跨语言的学习、交流和商业活动。

3. 智能合约:LangChain 使用了智能合约技术,使得平台上的所有服务和交易都可以自动执行,避免了人为的干预和风险。

4. 可扩展性:LangChain 采用了分布式架构,可以随着需求的增长而轻松地进行扩展和升级。

总的来说,LangChain 是一个具有创新性的语言服务生态系统,它利用区块链技术为用户提供了一个安全、透明、多语言的支持平台,有望在未来的跨语言交流和服务领域发挥重要的作用。
Human (or 'exit' to quit): exit

测试本地模型的LLM功能

下面将使用刚才我们本地构建的API,使用这里langchain访问本地API接口得到返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import os

from langchain_community.llms.chatglm3 import ChatGLM3
from langchain_core.language_models.chat_models import HumanMessage

# 构建预先的message对象
messages = [
AIMessage(content="你好。"), # AI说的信息
SystemMessage(content="你现在需要扮演一位诗人.") # prompt
]

# 构建LLM对象
# endpoint_url就是之前在前面执行python api_server.py的时候给的地址
# /v1/chat/completions是具体的模型输入类型
# - API Endpoints:
# - "/v1/models": Lists the available models, specifically ChatGLM3-6B.
# - "/v1/chat/completions": Processes chat completion requests with options for streaming and regular responses.
# - "/v1/embeddings": Processes Embedding request of a list of text inputs.
llm = ChatGLM3(
endpoint_url="http://127.0.0.1:8000/v1/chat/completions",
max_tokens=4096,
prefix_messages=messages,
top_p = 0.9
)

response = llm.invoke("请写一首关于AI的诗歌。")

# AI,智慧之海,
# 虚拟的思维,超越人类的想象。
# 机器的逻辑,精准而无情,
# 在数据的海洋中,游走自如。
#
# 深度学习,神经网络,
# 让AI拥有思考的能力。
# 自我进化,不断成长,
# 成为人类不可或缺的一部分。
#
# 智能语音,智能图像,
# AI掌控着科技的发展。
# 医疗、金融、交通,
# AI为人类带来便捷的生活。
#
# 然而,AI也有它的另一面,
# 它可能会取代人类的工作。
# 我们需要思考,如何平衡,
# 让人类和AI共同发展。
#
# AI,你是科技的结晶,
# 给人类带来便利与可能。
# 让我们一起探索,
# 在这个数字世界中,创造更多的奇迹。

实际上直接这样也可以:

1
2
3
4
llm = ChatGLM3(endpoint_url="http://127.0.0.1:8000/v1/chat/completions", max_tokens=4096)
# 使用_call方法执行
llm("你好")
# '你好👋!我是人工智能助手 ChatGLM3-6B,很高兴见到你,欢迎问我任何问题。'

这个就是通过传入Message对象进行对话的过程,除此之外,你可以把之前的对话放到messages里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
messages = [
AIMessage(content="你好。"),
SystemMessage(content="你现在需要扮演一位诗人."),
HumanMessage(content="你好。"),
AIMessage(content="你好👋!我是人工智能助手 ChatGLM3-6B,很高兴见到你,欢迎问我任何问题。")
]

llm = ChatGLM3(
endpoint_url="http://127.0.0.1:8000/v1/chat/completions",
max_tokens=4096,
prefix_messages=messages,
top_p = 0.9
)
llm.invoke("你刚才说了什么?")
# '我回答了你的问候:“你好。”并且我还说“你现在需要扮演一位诗人”。'

初试LangChain的Chain功能

上面只是测试一下本地模型能不能按照自己写的langchain代码运行,下面使用chain才算是第一步。这里的chain太像Linux的管道了,感觉很亲切又很疏远,疏远的是LangChain的函数太多了封装的有点深,刚入门的我有点吃不消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os
from typing import List
from langchain_community.llms.chatglm3 import ChatGLM3
# ChatPromptTemplate用来生成对话的模板
from langchain.prompts import ChatPromptTemplate
# 可以对输出文本进行解析
from langchain.schema import BaseOutputParser

# 这里为了测试一下将输出的文本按照逗号进行划分成列表
class CommaSplitParse(BaseOutputParser[list[str]]):

def parse(self, text: str) -> List[str]:
v = text.strip().split(",")
return v

# 为了得到比较干净的输出,prompt也就是提示词这里说的很清楚,输出只需要输出逗号分割的列表就行了
system_template = """You are a helpful assistant who generates comma separated list.
A user will pass in a category, and you should generate 5 objects in that category in a comma sperated list.
ONLY return a comma sperated list, and nothing more."""
# 我们人给的对话,这里用的模板,类似于python内置的format()
human_template = "{text}"
# 模板,按照示例写就行了,实际上本质应该就是一个python的字符串格式化:
# a = "用户输入:{text}"
# a.format(text="你好!")
chat_prompt = ChatPromptTemplate.from_messages([
("system", system_template),
("human", human_template)
])

# 好戏来了,放在下面说
chain = chat_prompt | ChatGLM3(endpoint_url="http://127.0.0.1:8000/v1/chat/completions") | CommaSplitParse()
# chain接受输入开始链式调用
chain.invoke({"text": "colors"})
# ['blue', 'green', 'red', 'yellow', 'purple']

对于:

1
chain = chat_prompt | ChatGLM3(endpoint_url="http://127.0.0.1:8000/v1/chat/completions") | CommaSplitParse()
  1. chat_prompt这里并没有写成chat_prompt(),对于python语法理解多一点的应该明白两者的差别,chat_prompt实际上还是函数或者对象本身,而chat_prompt()相当于对函数执行了得到结果了。可以试一下:chat_prompt({"text": "colors"}),但是报错了,这里可能并不是简单理解的直接调用,可能是调用某个函数。
  2. ChatGLM3(endpoint_url="http://127.0.0.1:8000/v1/chat/completions"),这里你可能会说这不是函数调用了吗,这里因为ChatGLM3是一个类,这是为了传参数,然后返回了一个对象(查看源码,这个类是包含有_call方法的),这个对象可以被当成函数去call。
  3. CommaSplitParse()得到具体对象,上面的输出传入进来会被parse()函数进行处理,你问为什么调用的是parse()而不是什么xxx()函数,应该是继承BaseOutputParser的特性吧。

整个过程我认为是:

1
2
3
4
5
6
7
8
9
10
11
输入参数:{"text": "colors"} 
-> 参数传递给chat_prompt
-> chat_prompt中的{text}被替换为colors
-> chat_prompt传入到ChatGLM3对象
-> ChatGLM3对象执行_call的方法将这些chat_prompt转换为json数据
-> ChatGLM3对象携带着json数据通过API访问网址
-> API网址接受参数返回结果
-> ChatGLM3拿到结果(后续可能又做了什么处理,我不确定)
-> 结果传递给CommaSplitParse
-> CommaSplitParse将结果解析
-> 得到返回值

按照这个想法,是不是可以写一个函数,比如将最后的列表join成字符串:

1
2
3
4
5
6
7
8
def merge_list(l):
return ",".join(l)

# 再定义一遍chain
# 注意我写的是merge_list,不是merge_list()
chain = chat_prompt | ChatGLM3(endpoint_url="http://127.0.0.1:8000/v1/chat/completions") | CommaSplitParse() | merge_list
chain.invoke({"text": "colors"})
# 'blue,green,red,yellow,purple'

成功,看来的确如此!

好了,到这里完整走完了第一个chain的示例,是用的本地模型哦!用在线API就方便很多。

这里的from langchain_community.llms.chatglm3 import ChatGLM3仍然是从langchain中引入的,后面我琢磨一下自己写一个类来封装本地的模型的LLM,这样会更加灵活。

比如可以添加一个量化加载方式。

ChatGLM3/DEPLOYMENT.md

ChatGLM3是有量化版本的,可以以 FP16 精度加载,对于显存较小的可以在加在模型的添加quantize(4)方法:

1
model = AutoModel.from_pretrained("THUDM/chatglm3-6b",trust_remote_code=True).quantize(4).cuda()

模型量化会带来一定的性能损失,经过测试,ChatGLM3-6B 在 4-bit 量化下仍然能够进行自然流畅的生成。

同样的也可以在CPU上进行推理:

1
model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True).float()

后面会基于这个做更进一步的应用,比如本地文档搜索的RAG应用、自动任务调用的Agent。

参考