本文主要讨论了在使用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: