基于Qt信号槽机制的AI对话工具开发
在前面学习了Qt的Http请求,尝试完成了基于Qt界面调用DeepSeek的API,实现了一些基本功能,如记忆对话,流式输出等
点击这里查看
但是我发现内容多了过后代码过于冗杂,层次不清晰,于是打算重新架构一下,并记录一下开发思路
完整源码可以在这里查看:点击这里查看GitHub-Qt-ChatTool
架构思路
首先,不能只在一个widget.cpp中实现,应当将整个程序拆分为3个模块
这里架构了三大模块:
- 界面模块(WidgetUI):仅负责与界面之间的交互,不管理与请求相关的处理
- 核心模块(ChatPro):负责处理请求与响应,界面模块直接调用即可完成对应的请求
- 函数模块(FuncTool):在这里实现自定义函数,把函数从代码中隔离出来单独实现
Function Call功能
注意:要选择支持Function Call功能的模型
这也是重构最想实现的关键功能,下面了解一下这个功能的基本原理
构建函数
在请求体中增加tools
字段,通过函数名称,函数描述,参数等构建一个函数,就能让AI根据函数描述在对应的情况下该函数
这里是我构建的一个模拟请求天气API的函数
"tools": [{"function": {"description": "获取城市的天气信息","name": "get_weather","parameters": {"properties": {"city": {"description": "城市名","enum": ["杭州","北京"],"type": "string"}},"required": ["city"],"type": "object"}},"type": "function"}]
具体的函数实现需要在代码中实现
下面是模拟获取天气API的执行函数
QString FuncTool::getWeather(const QJsonObject &arguments)
{QString city = arguments["city"].toString();QJsonObject result;if("苏州" == city){result["weather"] = "晴天";result["temperature"] = "11℃";}else if("杭州" == city){result["weather"] = "晴天";result["temperature"] = "15℃";}else if("北京" == city){result["weather"] = "阴天";result["temperature"] = "9℃";}else{result = QJsonObject();}return QString(QJsonDocument(result).toJson(QJsonDocument::Indented));;
}
当询问AI关于天气信息时,AI就会调用对应名为get_weather
的函数,当然调用的结果需要我们返回给AI
调用函数
- 当AI识别到用户需要调用函数时AI会根据函数的
required
字段来从用户的询问中解析参数,然后返回一个包含tool_calls
字段的回复 - 我们需要对AI的回复作判断,当存在
tool_calls
字段时,我们需要解析其中的内容,如函数参数,函数名等; - 同时将这个
tool_calls
字段包装进一条角色为assistant
的消息中加入消息队列(让AI知道自己调用了哪个函数)
{"content": "","role": "assistant","tool_calls": [{"function": {"arguments": "{\"city\": \"北京\"}","name": "get_weather"},"id": "call_08d86f66db154ff79b6e9c","index": 0,"type": "function"}]}
- 然后根据函数名来执行我们自己的函数,然后将结果返回,将返回结果包装为一条角色为
tool
的消息,加入消息队列中
{"content": "\n{\n \"temperature\": \"9℃\",\n \"weather\": \"阴天\"\n}\n","name": "get_weather","role": "tool","tool_call_id": "call_08d86f66db154ff79b6e9c"}
- 把这两条消息都加入消息队列中之后,再次向AI发起请求,内容就是消息队列,这样AI就能理解自己调用了函数,并且获得了结果
- AI会根据自己的设定(比如活泼、冷漠等)结合获取到的结果,给出自己的回答。
- 至此就完成了一次自定义函数调用
界面模块
界面搭建
通过.ui
文件实现界面搭建,暂定了三个部分
一个历史记录对话框,一个用户输入框,一个发送按钮
基本参数
在widgetUI.h
中定义一些基本参数
m_messages
:消息队列用于记录历史消息m_tools
:从函数模块获取到的所有函数m_wholeMsg
:流式传输获取到的片段用于叠加m_record
:记录界面的消息记录(用于刷新流式传输)
槽函数
- 定义一个绑定按钮按下的槽函数,发送一次请求
- 定义
ChatPro
收到消息会发送信号,消息结束页发送信号,在界面模块为这两个信号绑定槽函数 - 接收信息就对ui的记录对话框清除再重写,从而实现流式传输逐字输出
- 接收信息完毕要判定是否为函数调用
- 如果不是函数调用,则直接将助手消息加入消息队列,这样AI才能记住上下文
- 如果是就要将两条消息(上面已经介绍)加入消息队列中,并再次发起请求
详细代码请看:这里
核心模块(ChatPro)
这里将其设计为单例模式,在这里面涉及很多通用的工具函数,如通过类型指定构建一个message
、QString
转Json对象等。
// 单例模式static ChatPro *Get(){static ChatPro cp;return &cp;}
核心参数
- API相关信息定义在头文件中,
m_url
、m_api_key
、m_model
均为QString
类型 QNetworkAccessManager *manager;
网络管理器,在Qt中由它来统一管理所有请求与响应QList<QJsonArray> m_toolCallPieces;
当请求为函数调用时,流式传输无法将所有函数包含的信息一次性传完,需要将其先存储起来,在请求结束后再解析合并为完整的信息bool m_isFunction = false;
是否为函数调用,在请求结束时传给界面模块
函数
QNetworkRequest buildRequestHeader();
构造请求头QByteArray buildRequestBody(const QJsonArray &messages, const QJsonArray &tools);
构造请求体QJsonObject qByteArrayToQJsonObject(const QByteArray &data);
数据流转Json对象QJsonObject qStringToQJsonObject(const QString &qStr);
QString转Json对象QByteArray qJsonObjectToQByteArray(const QJsonObject &obj);
Json对象转数据流QNetworkReply* getReply(const QJsonArray &messages, const QJsonArray &tools);
获取响应QNetworkRequest buildRequestHeader();
构造请求头- 构建信息对象(系统、用户、助手、工具四种枚举类型)
enum MessageType{SYSTEM_MESSAGE,USER_MESSAGE,ASSISTANT_MESSAGE,TOOL_MESSAGE
};QJsonObject buildMessage(const QString &message, MessageType type,const QJsonArray &toolCalls = QJsonArray(),const QString &name = "",const QString &id = "");
-
QJsonArray parseChunkResponse(const QByteArray &resp);
流式输出会自带data:
前缀,需要进行处理才能转换成Json对象,同时一次可能接受到多条data
,需要将其提取出完整的QJsonArray对象(json字段为choices
) -
QJsonArray parsetoolCallPieces(const QList<QJsonArray> &toolCalls);
从多条不完整的ToolCalls字段中解析出完整的字段 -
void ConnectReply(const QJsonArray &messages, const QJsonArray &tools);
连接请求,封装了请求的发送与处理,转而发送信号给界面模块,实现模块化
详细代码:在这里
函数模块
这里也使用单例模式,直接提供函数给界面模块使用
static FuncTool* Get(){static FuncTool ft;return &ft;}
QJsonArray m_tools
包含的所有函数,通过函数QJsonArray Tools(){return m_tools;}
返回给界面模块使用- 定义了一个函数模板,通过函数
newTool(const QString &name, const QString &description, const QJsonObject ¶ms)
直接添加到m_tools
中 - 这里完成了两个函数
- 获取天气
- 获取当前时间
QString FuncTool::getTime(const QJsonObject &arguments)
{
// 获取当前的日期和时间
QDateTime currentDateTime = QDateTime::currentDateTime();
QString info = "Current Date and Time:" + currentDateTime.toString();
return info;
}
- 调用tool的函数
QString FuncTool::executeFunction(const QString &name, const QJsonObject &arguments)
{
if(name == "get_weather")
return getWeather(arguments);
else if(name == "get_time")
return getTime(arguments);
else
return "";
}
详细代码:[这里](https://github.com/ChengYull/Qt-ChatTool/blob/master/functool.cpp)