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

news/2025/3/23 3:36:42/文章来源:https://www.cnblogs.com/1873cy/p/18785015

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/902535.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中企出海财务合规难?交给「云+AI」来破局|SAP ERP海外实施商工博科技

2月20日,SAP全球化运营高峰论坛汇聚超1000位出海企业高管和行业专家,共同探讨中国企业在行业多样化、投资模式多元、全球布局因地制宜的全球化新趋势下面临的出海机遇与挑战。这次我们将从合规风险、供应链韧性、人才管理、IT架构、AI赋能等角度进行复盘回顾,探讨如何依托先…

乒乓球比赛

题目:乒乓球比赛(武汉大学机试真题):甲={a,b,c}、乙={x,y,z}两队进行比赛,一直a不和x比,c不和x和z比,请问所有可能的比赛安排 //应该使用全排列 列出所有的比赛结果,再输出满足要求的对局 //但还有一种取巧的方法实现 使用库函数next_Permutation(nums.begin(),nums.end(…

华为开辟的赛道,终究也只有华为敢跑起来

3月20日,华为Pura先锋盛典召开。一部被称为阔折叠的华为Pura X正式亮相。之前外界对这款新品有诸多猜想,今天终于揭开了神秘面纱:这是一部看上去不太像手机的手机,很宽很“阔”。全新形态的产品打破了人们对手机的刻板印象,也给了手机未来更多的想象。同时作为首款全面搭载…

RANSAC---从直线拟合到特征匹配去噪

Ransac全称为Random Sample Consensus,随机一致性采样。该方法是一种十分高效的数据拟合方法。我们通过最简单的拟合直线任务来了解这种方法思路,继而扩展到特征点匹配中的误点剔除问题。 (注意,RANSAC不是直接用于特征点匹配,而是一种在初步特征匹配后消除误匹配的方法)…

3.19 CW 模拟赛 赛时记录

前言 还是那几句话 冷静, 耐心, 放下, 不浮躁 不贪跟策略, 数据检验, 关键步记录 看题 \(\rm{T1}\) 逆向思维一下就是加点, 然而事实上应该比较好做 \(\rm{T2}\) 还挺有礼貌, 但是这种题放到后面一点吧 \(\rm{T3}\) 这个题可以尝试一下, 多半是最优解性质的题 \(\rm{T4}\) 更是…

buuctf re pyre 写题日记

进入文件,下载文件,文件是pyc文件,放入基本的反汇编出其源代码看样子,还是python2编写的代码。 根据代码的逻辑逆向出其解密代码,第二段算法的逆向好些,就是逆着再异或一遍,第一段代码更具取模运算的性质,因为128%128=0,所以后面相当于直接加了个0,所以逆操作就是,i…

harmony OS NEXT-TypeScript

TS基础内容 1.1 TypeScript简介TypeScript是由微软开发,是基于JavaScript的一个扩展语言 TypeScript包含了JavaScript的所有内容,即:TypeScript是JavaScript的超能 TypeScript增加了:静态类型检查、接口、泛型等很多现代开发特性,因此适合更大型的项目开发 TypeScript需要…

Unreal最佳实践——TSharedRef和Ptr的使用和坑

A:管理比较麻烦的时候用,像slate这种用得就多 A:这种东西能不用还是别用,性能肯定是低的(低不了多少),还有一个是要注意循环引用 A:写一个ptr记得在destruction里面加一个,或者endplay加一下,就能避免泄漏问题 养成习惯就行了

用户说 | 零基础用通义灵码 AI 程序员开发个人笔记网站

通义灵码是一款基于通义大模型的智能编码辅助工具,支持自然语言生成代码、单元测试生成、代码注释生成等功能,兼容多种主流IDE和编程语言。对于零基础用户,只需通过自然语言描述需求,通义灵码即可自动生成代码,帮助快速开发个人笔记网站,极大简化开发流程,提升效率。作者…

harmony OS NEXT-通过用户首选项实现数据持久化

鸿蒙通过用户首选项实现数据持久化 1.1 场景介绍 用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。当用户希望有一个全局唯一存储的地方,可以采用用户首选项来进行存储。Preferences会将该数据缓存在内存中,当用户读取的时…

harmony OS NEXT-启动页开发

鸿蒙启动页开发 1.1 更改应用名称和图标 1.更改应用图标找到moudle.json5文件,找到应用启动的EntryAbility下面的icon,将原来的图标改成自己设置的即可2.更改应用名称3.效果展示2.1 广告页面开发3.1 详细介绍 3.1.1 启动页面 import { PrivacyDialog } from ../views/componen…

harmony OS NEXT–状态管理器–@State详解

鸿蒙Harmony--状态管理器--@State详解 1.1 定义 @State装饰的变量,或者称为状态变量,一旦变量拥有了状态属性,就可以触发其直接绑定UI组件的刷新。当状态改变时,UI会发生对应的渲染变化 ,@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问。在…