本文主要讨论了在使用LangChain连接LLM推理服务时,应调用OpenAI还是ChatOpenAI的问题。文章深入解释了两个接口的区别,涉及到completions和chat completions的区别,以及LLM推理时使用的chat template。文章还讨论了如何正确调用这些接口,特别是在使用vLLM等开源推理框架时需要注意的问题。
OpenAI用于调用/v1/completions接口,提供续写能力;ChatOpenAI用于调用/v1/chat/completions接口,提供对话能力。这两个接口分别对应LLM的Base Model和Instruct Model。
chat template是用于将对话历史内容转化为free text的模板,对于Instruct Model的推理服务非常重要。
可以从模型文件夹中加载,也可以通过启动参数指定。如果没有正确加载chat template,可能会影响模型的回答准确度。
对于Base Model的推理服务,只能使用LangChain的OpenAI类;对于Instruct Model的推理服务,推荐使用ChatOpenAI类,也可以在调用OpenAI类之前手动拼接prompt。
浮言易逝,唯有文字长存。
今天来聊一个非常具体的技术问题。
对于工程师来说,当我们使用LangChain来连接一个LLM推理服务时,多多少少会碰到一个疑问:到底应该调用OpenAI
还是ChatOpenAI
?我发现,每次解释这个问题时,都会费很多唇舌,所以干脆写下来供更多人参考。这背后其实涉及到两个关键问题:
completions 和 chat completions
通过LangChain来调用LLM的时候,通常会引用下面这两个类:
from langchain_openai import OpenAI
from langchain_openai import ChatOpenAI
从这两个类的名字就不难猜出,它们是用来调用OpenAI提供的LLM接口的。具体来说,是这两个接口:
不过呢,由于OpenAI的影响力实在太大,很多其他闭源和开源的LLM,在提供推理服务的API时,也都不约而同地遵循了OpenAI的接口形式。这两个接口成了事实性的标准。
这两个接口的区别是什么呢?
Base Model 和 Instruct Model
一般来说,上一节提到的这两个接口,分别对应两类LLM模型:
以Llama 3模型系列为例:
在以上这个表格中,以“-Instruct”结尾命名的模型,就属于Instruct Model;反之就是Base Model。
OpenAI在早期推出API服务的时候,/v1/completions 和 /v1/chat/completions 这两个接口是都提供的。但随着时间的推移和技术的迭代,大部分AI应用场景都能用指令对话的形式来支持,所以后来OpenAI也就不再为最新的模型提供 /v1/completions 接口了。现在如果访问OpenAI关于 /v1/completions 接口的API reference文档页面[1],你会发现这个接口已经被标记为“Legacy”了。
结合前一节所讨论的,我们现在容易得出结论,如果我们想调用OpenAI的GPT模型,那么应该选择使用LangChain的ChatOpenAI
这个类。
但是,如果我们使用开源推理框架(如vLLM[2])来为了开源模型在本地架设推理服务,那么通常来说,这个推理服务可能会同时支持/v1/completions 和 /v1/chat/completions 这两个接口。当我们使用LangChain进行调用的时候:
如果加载的是一个Base Model,那么只有 /v1/completions 接口可用,/v1/chat/completions 接口是不可用的(或者说,调用它没有意义)。我们只能使用LangChain的OpenAI
这个类进行调用。
如果加载的是一个Instruct Model,那么理论上来说,我们应该调用 /v1/chat/completions 接口进行推理。也就是使用LangChain的ChatOpenAI
这个类来进行调用。但是,由于对话消息在经过格式化之后,最终也是表达成一个文本串的,所以其实 /v1/completions 也是可用的,这时候就可以调用LangChain的OpenAI
这个类。这个过程我们后面的章节再仔细展开。
关于chat template
根据前面的分析,我们知道了,调用 /v1/chat/completions 接口,我们需要使用LangChain的ChatOpenAI
这个类,并且传入一个message list。下面是一段示例代码:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(
openai_api_key="EMPTY",
openai_api_base="http://127.0.0.1:8000/v1",
model_name="llama3.2-1B-instruct"
)
prompt = ChatPromptTemplate.from_messages([
("system", "Your are a helpful assistant."),
("user", "Hello, how are you?"),
("assistant", "I'm doing well, thank you for asking."),
("user", "Can you tell me a joke?")
]
)
chain = prompt | llm
reponse = chain.invoke({})
在这段代码中,我们看到,传入给LLM的是一个有结构的对话历史列表。但是,不管是Base Model还是Instruct Model,模型最终接受的输入,应该是一段free text(再转成token)。那么问题来了,这个有结构的对话历史列表,是如何转成free text的呢?显然,这里需要一个模板(template),这就是所谓的chat template[3]。
对于前面示例代码中的 llama3.2-1B-instruct 模型,它所对应的chat template是下面这个样子的:
这是一个遵循Jinja格式的模版[4]。模板表达了各种message(包括system message,user message,assistant message以及其它类型的message)的渲染方式。
基于这个chat template,前面示例代码中的对话历史内容,最终输入到LLM时会转化成如下的free text(也就是prompt):
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Cutting Knowledge Date: December 2023
Today Date: 28 Dec 2024
Your are a helpful assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>
Hello, how are you?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
I'm doing well, thank you for asking.<|eot_id|><|start_header_id|>user<|end_header_id|>
Can you tell me a joke?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
那么,这个chat template是从哪里来的呢?对于vLLM来说,它启动的时候,有两种方式可以获取到chat template:
我们顺便看一个tokenizer_config.json文件的具体例子。还是以前面示例代码中调用的 llama3.2-1B-instruct 模型为例,它的chat template模板内容就存在tokenizer_config.json文件中的chat_template字段中,如下:
这里多说一句:我们需要注意的是,tokenizer_config.json文件中并不一定包含chat_template字段。具体有没有这个字段,取决于模型文件的创建过程。对于Llama 3模型来说,我们从Meta官方[5]申请并下载到模型文件之后,一般情况下,需要使用Hugging Face的Transformers框架中提供的一个工具[6],将模型转换成hf的通用格式。这样,vLLM以及其它开源生态中的框架或工具才能方便地加载它。
以 llama3.2-1B-instruct 模型为例,这个模型格式的转换过程,需要执行以下命令来调用Transformers的这个工具:
python src/transformers/models/llama/convert_llama_weights_to_hf.py --input_dir --model_size 1B --llama_version 3.2 --output_dir --instruct
在以上命令中,如果带着--instruct参数,那么转换成的模型配置文件tokenizer_config.json中就包含chat_template字段;否则就不包含chat_template字段。
注意,以上命令执行之前,需要先执行huggingface-cli login命令登录Hugging Face,并确保在"meta-llama/Llama-3.2-1B-Instruct"的Hugging Face模型主页上提交了访问申请并获批,只有这样这个命令才能执行成功。
其他需要注意的问题
由上一节可知,假如tokenizer_config.json文件中没有包含chat_template字段,并且vLLM在启动时也没有指定--chat-template,那么,vLLM会以未指定chat template模板的方式启动起来。
这个时候,如果还是像本文前面的代码那样调用LangChain的ChatOpenAI
,就会出现意想不到的结果。一定要注意!具体会得到怎样的结果,取决于你使用的vLLM运行环境中Transformers的版本:
openai.BadRequestError: Error code: 400 - {'object': 'error', 'message': 'As of transformers v4.44, default chat template is no longer allowed, so you must provide a chat template if the tokenizer does not define one.', 'type': 'BadRequestError', 'param': None, 'code': 400}
显然,高版本的Transformers和vLLM对于这个情况的处理,更合理一些。通过明显的报错避免了不易察觉的错误。
如前所述,由于对话消息在经过格式化之后,最终也是表达成一个文本串的,所以也可以调用LangChain的OpenAI
这个类来完成。这时候其实背后是在调用 /v1/completions 这个接口。相当于client端在调用OpenAI
之前,先把prompt按照需要的对话格式拼好。下面的代码,可以实现跟前面调用ChatOpenAI
的代码同样的效果:
from langchain_openai import OpenAI
from langchain_core.prompts import PromptTemplate
from datetime import datetime
llm = OpenAI(
openai_api_key="EMPTY",
openai_api_base="http://127.0.0.1:8000/v1",
model_name="llama3.2-1B-instruct"
)
prompt_text = """<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Cutting Knowledge Date: December 2023
Today Date: {today_date}
Your are a helpful assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>
Hello, how are you?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
I'm doing well, thank you for asking.<|eot_id|><|start_header_id|>user<|end_header_id|>
Can you tell me a joke?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
prompt = PromptTemplate.from_template(prompt_text)
chain = prompt | llm
reponse = chain.invoke({"today_date":datetime.now().strftime('%d %b %Y')})
小结
现在我们总结一下本文开头提出的问题:
(正文完)
参考文献:
其它精选文章:
DSPy下篇:兼论o1、Inference-time Compute和Reasoning
科普一下:拆解LLM背后的概率学原理
程序员眼中的「技术-艺术」光谱
扫码或长按关注微信公众号:张铁蕾。
有时候写点技术干货,有时候写点有趣的文章。
这个公众号有点科幻。