如何使用C++来实现OPENAI协议通过OLLAMA来与AI大模型通信
目前大模型在使用方面最大的长处就是聊天功能。在聊天中可以谈天说地。可以聊聊人生,可以聊聊历史,可以让它写一首七言绝句,也可作一篇洋洋洒洒的长篇报告。
如何才能与大模型进行沟通呢,一个通用的方法是openai协议,其实质就是用http来向大模型提问所获得回答。想用http,C++程序员第一时间想到的无疑就是curl库。现在github上有个项目叫openai-cpp,就是把curl用为作通信库,封装了主要的openai协议。ollama也支持openai协议,当然也可以用这个openai-cpp来对ollama进行通信,进而可与ollama支持的大模型进行通信。
openai-cpp的项目提供了各个功能的示例,其中,主要功能当然就是chat功能了。下面来谈谈openai-cpp的使用。openai-cpp的这个示例都是用cmake来管理和编译的,因我比较熟悉qt5的qmake,就用它来管理和编译示例项目。
拿openai-cpp的10-chat.cpp来说,它的代码非常简单如下所示
#include "openai.hpp"
#include <iostream>
int main() {
openai::start();
auto chat = openai::chat().create(R"(
{
"model": "gpt-3.5-turbo",
"messages":[{"role":"user", "content":"blah"}],
"max_tokens": 7,
"temperature": 0
}
)"_json);
std::cout << "Response is:\n" << chat.dump(2) << '\n';
}
如何在Qt5下运行这个代码呢。
首先,要把openai::start();替换为openai::start("ollama", "optional_organization",true,"http://127.0.0.1:11434/v1/");在这里,“ollama""optional_organization"两个参数可随便写。"http://127.0.0.1:11434/v1/",固定为本地IP。可根据需要把IP改为其它的IP进行远程操作。
其次,以大家熟悉的windows平台为例,需要在Qt5项目的pro文件中,加入
LIBS += -L$$PWD/curl/lib -lcurl
并将从curl官网下的win64版的curl中的include,lib,拷贝到项目所在的目录下。
然后,在pro文件中加入
SOURCES += \
main.cpp
INCLUDEPATH += \
curl/include \
openai/nlohmann \
openai
最后,我们可用系统需要最小的qwen:7b大模型来测试。
将原代码中的
auto chat = openai::chat().create(R"(
{
"model": "gpt-3.5-turbo",
"messages":[{"role":"user", "content":"blah"}],
"max_tokens": 7,
"temperature": 0
}
)"_json);
改为
auto chat = openai::chat().create(R"(
{
"model": "qwen:7b",
"messages":[{"role":"user", "content":"hello"}],
"max_tokens": 7,
"temperature": 0
}
)"_json);
这样,就可以编译成功了。
在运行前,确认ollama已正确安装并已执行过ollama run qwen:7b命令,第一次执行这个命令,ollama会自动下载qwen:7b的大模型,可能时间较长,差不多4G大小,我的网络用了将近一小时。
试运行一下,有回答了。但回答显然是不全的。这时把"max_tokens": 7,一行改为"max_tokens": 1024,就可以回答全了。
来分析一下代码:
第一行代码,是包含openai-cpp的头文件,事实上,整个openai-cpp的项目,就一个头文件。
主要的代码就是auto chat = openai::chat().create(),括号里的内容是nlohmann的json库,这个库是基于std:string的库。同样,也就只是一个名为json.hpp头文件。代码中的写法是用宏定义的方法生成了一个json字串内容为
{
"model": "qwen:7b",
"messages":[{"role":"user", "content":"hello"}],
"max_tokens": 1024,
"temperature": 0
}
的nlohmann的json对象。这种写法作为示例程序很简洁明了,但如果要写成实用的代码则不太方便了。在上述10-chat.cpp中的加入两行
#include <nlohmann/json.hpp>
using json = nlohmann::json;
然后,就可使用nlohmann的json库了。
我是这样写的
json prompt;
prompt["model"] = "qwen:7b";
json msg_json;
msg_json["role"] = "user";
msg_json["content"] = "邓丽君是日本人吗?";
prompt["messages"].push_back(msg_json);
prompt["max_tokens"] = 1024;
prompt["temperature"] = 1.5;
这时,那个chat代码就可改为openai::chat.create(prompt);
这时,prompt就可填入任何你想问的问题。你会发现,用英文提问,回答的是英文,是中文提问,则回答中文。
这个json对象的参数中,有两个重要的参数。“max_tokens”,可以认为就是指定回答的内容长度,长度不够,则会出现回答不全的情况。"temperature"参数,是指定回答的随机性。10-chat的代码中是0,你会发现每次回答的内容都一样。事实上,查到的资料表明,这个参数的取值是在0~2之间。如我取的是1.5,随机性就已经很强了,同一个问题,回答的实质内容基本一样,但表达的文字不一样。给人的感觉就是大模型像个人在回答了,而不是机械的给出一个始终不变的标准答案。
接下来,我们来试试让大模型更像个人。向人提问,“什么最好”,正常人在回答这个问题时肯定不知道你问的哪个方面的问题,不知如何回答,但如果之前向这个人问过,“米饭好吃吗”,那这个人就可据此认为你问的“什么最好”应该是指什么最好吃。这就是上下文。openai同样也支持这个上下文。“messages"这个参数就是可以把你的提问保存下来,如果你能把大模型对这个问题的问答也保存下来,大模型就更能理解你的上下文关系了。
下面我把最终的完整代码贴出来供大家参考。
#include "openai.hpp"
using namespace openai;
#include <iostream>
#include <vector>
using namespace std;
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <QDebug>
int main() {
auto& openai = openai::start("ollama", "optional_organization",true,"http://127.0.0.1:11434/v1/");
json prompt;
prompt["model"] = "qwen:7b";
json msg_json;
msg_json["role"] = "user";
msg_json["content"] = "邓丽君是日本人吗?";
prompt["messages"].push_back(msg_json);
prompt["max_tokens"] = 1024;
prompt["temperature"] = 1.5; //经试验,temperature取值在0~2之间有效
qDebug()<<"Q:"<<QString::fromStdString(msg_json["content"]);
auto chat = openai.chat.create(prompt);
//qDebug()<< "Response is:\n"<<QString::fromStdString(chat.dump(2));
string res = chat.dump(2);
//取出返回的json字串中的content内容关输出
json jr = json::parse(res);
// 先获取choices数组中的第一个元素(示例中只有一个元素)
json choice = jr["choices"][0];
// 再从对应的message对象中获取content字段
string role = choice["message"]["role"];
//qDebug()<<"role:"<<QString::fromStdString(role);
string content = choice["message"]["content"];
qDebug()<<"A:"<<QString::fromStdString(content);
//prompt["messages"].clear(); //实践证明,只是没有清除messages的内容,默认是上下文有效的。
//加入回答到上文中,这个添加似乎对AI联系上下文回答效果就是让AI接下来的问答知道是指邓丽君。与上文的效果相同。在有上文的情况下,看不出明显区别
msg_json["role"] = "assistant";
msg_json["content"] = content;
prompt["messages"].push_back(msg_json);
msg_json["role"] = "user";
msg_json["content"] = "她为什么总唱日本歌?";
prompt["messages"].push_back(msg_json);
qDebug()<<"Q:"<<QString::fromStdString(msg_json["content"]);
auto chat_1 = openai.chat.create(prompt);
res = chat_1.dump(2);
//qDebug()<< "Response is:\n"<<QString::fromStdString(res);
jr = json::parse(res);
// 先获取choices数组中的第一个元素(示例中只有一个元素)
choice = jr["choices"][0];
content = choice["message"]["content"];
qDebug()<<"A:"<< QString::fromStdString(content);
}
在代码中,使用了qDebug来在windows控制台上输出结果,这是因为这样做可以在windows的控制台上正确的输出utf-8编码的中文了。
为方便大家,我把完整的qt5下的项目上传以供大家正载
https://download.csdn.net/download/hugerat/90291612
该项目在curl/lib文件夹下,有一个名为libcurl-x64.dll的文件,要正常运示例,需将此文件拷贝到编译生成的exe文件的相同文件夹下,或者你的windows安装了libcurl并配置好了环境变量。由名字可知它是一个64位的文件,应此Qt5的项目在编译时,应选择64位的编译器,比如我用的是mingw 64-bit