基于Qt信号槽机制的AI对话工具开发——使用流式输出且支持Function Call

基于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)

这里将其设计为单例模式,在这里面涉及很多通用的工具函数,如通过类型指定构建一个messageQString转Json对象等。

    // 单例模式
    static ChatPro *Get(){
        static ChatPro cp;
        return &cp;
    }

核心参数

  • API相关信息定义在头文件中,m_urlm_api_keym_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 &params)直接添加到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)
posted @ 2025-03-21 16:54  风陵南  阅读(475)  评论(0)    收藏  举报