大模型对话Python环境集成
1
前言
),这个需要大家自行去补课。本文也就是先来打个基础,尝试将Python环境和基本的LLM对话能力在afsim的框架下进行集成,后续理论上就可在此基础上做扩展,因此本文可能对afsim本身的能力提升作用不大,大家捡感兴趣的不是看看就行了。2
wizard插件
首先是afsim相关的插件集成。之所以首先选wizard来做,一是因为后面会考虑基于LLM来做想定智能生成,二是之前没有写过wizard相关的插件。所以本文就先把创建wizard插件的基本过程进行记录一下。
1)插件框架
之前有文章讲过wkf插件和warlock插件的创建方法《wkf界面开发和插件扩展》《天线方向图实时绘制插件》,而wizard插件的创建方法跟warlock大概是类似的,就是在插件注册时,将最后那个all参数改为wizard即可。

同样的,既然创建的是wiard的插件,那么我们就直接从wizard的插件基类继承,同样可以获得wizard框架封装好的一些类和方法。
下面是warlock插件类的头文件和实现文件代码:
// HSWizardLLMPlugin.hpp#ifndef HSWizardLLMPlugin_HPP#define HSWizardLLMPlugin_HPP#include<QObject>#include"Plugin.hpp"class HSWizardLLMPlugin : public wizard::Plugin{public:HSWizardLLMPlugin(const QString& aName, const size_t aUniqueId);~HSWizardLLMPlugin() override;};#endif// HSWizardLLMPlugin_HPP
// HSWizardLLMPlugin.cpp#include"HSWizardLLMPlugin.hpp"#include<WkfEnvironment.hpp>#include<WkfMainWindow.hpp>WKF_PLUGIN_DEFINE_SYMBOLS(HSWizardLLMPlugin,QObject::tr("HSWizard LLM").toStdString().c_str(),QObject::tr("LLM Dialog with python.").toStdString().c_str(),"wizard");HSWizardLLMPlugin::HSWizardLLMPlugin(const QString& aName, const size_t aUniqueId): wizard::Plugin(aName, aUniqueId){}HSWizardLLMPlugin::~HSWizardLLMPlugin(){}
编译运行后,能够从PluginManager界面上查看到插件已经成功加载。

2)LLM对话框
接下来做一个LLM对话框,这个界面可以根据之前的wkf开发教程来选择添加哪种类型的界面,过程就不重复了,查看之前的文章就行了,我这里做成DockWidget先,效果如下:

以上是基本的wizard插件创建,现在点击发送也没有任何反应,下面先来解决Python集成的问题。
3
Python集成
目前要做LLM开发,如果仅仅是做问答对话式那么其实有很多选择,各种语言通过封装一下LLM提供商的接口就能做一个对话框了。但如果要真正做一个Ai应用,光有对话是不够的,还要根据Ai的回答做对应的数据处理。而Ai应用开发的大部分工具都是用Python语言写的,要集成到afsim的C++/Qt环境的话就需要对Python进行集成。
不过这个技术已经很成熟了,网上一抓一大把,我就不献丑讲太多了。本文只把过程和遇到的问题进行记录,大家参考参考就行。
我用的是python3.12.7,我之前安装的是更高版本的Python3.13,但在安装LangChain时出过问题,所以又降低到这个版本了。
1)cmake配置
这里主要将Python安装路径的头文件和库文件路径配置到本插件的CMakeList文件,如下:
# 查找Pythonfind_package(Python 3.12 REQUIRED COMPONENTS Interpreter Development)# 输出找到的Python信息message(STATUS "Python executable: ${Python_EXECUTABLE}")message(STATUS "Python include path: ${Python_INCLUDE_DIRS}")message(STATUS "Python library path: ${Python_LIBRARIES}")# Python 在 Debug 模式下会链接带 _d 后缀的库set(PYTHON_LIB_DEBUG "${Python_LIBRARY_DIRS}/python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}_d.lib")set(PYTHON_LIB_RELEASE "${Python_LIBRARY_DIRS}/python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}.lib")# 包含头文件并链接库target_include_directories(HSWizardLLMPlugin PRIVATE ${Python_INCLUDE_DIRS})target_link_libraries(HSWizardLLMPlugin$<$<CONFIG:Debug>:${PYTHON_LIB_DEBUG}>$<$<CONFIG:Release>:${PYTHON_LIB_RELEASE}>)
再次对afsim源码进行cmake,会输出找到的python信息。此时在项目属性里也可以看到正确连接了系统安装的Python库了。

2)Python集成集成代码
要调用Python代码,需要首先对Python在Qt中的环境进行初始化,退出时需要清理资源。我这里专门封装了一个类PyBridge,因为要流式输出到界面,所以需要用线程的方式来调Python函数,不然在Qt界面输出时会出现假死的现象,原因就是与Qt的Ui更新线程都在主线程而阻塞了界面刷新。因此这个类是继承QThread的,在run方法里面进行初始化。
voidPyBridge::run(){static QString prompt;// 注册回调PyImport_AppendInittab("qt_callback", &PyInit_qt_callback);// 初始化python解释器.C/C++中调用Python之前必须先初始化解释器Py_Initialize();// 判断python解析器的是否已经初始化完成if (!Py_IsInitialized())qDebug() << "Py_Initialize fail";elseqDebug() << "Py_Initialize success";while (!m_isStop){QThread::msleep(1);m_mutex.lock();// 有数据才执行if (m_prompt.isEmpty()) {m_mutex.unlock();continue;}else {prompt.swap(m_prompt);m_mutex.unlock();// TODOprompt.clear();}}// 销毁自上次调用Py_Initialize()以来创建的所有子解释器。Py_Finalize();}
此时编译会报下图错误:

点进去后查看时python头文件目录下的object.h文件说PyType_Slot这个结构体问题。

起初一头雾水,明明上面就是这个PyType_Slot的定义,这里怎么会报错呢,后来查了下网络,知道是因为上图的slots与Qt的关键字冲突了,挺隐蔽的。因此按以下修改就好了:

另外再注释掉57行的Debug定义来解决后续会遇到的无法解析的外部符号 __imp__Py_NegativeRefcount

再次编译时如果使用的是debug模式编译,链接时还会出现无法找到debug版的python312_d.lib,如下图

这个问题可以直接将Python安装目录下的python312.lib复制一份为python312_d.lib即可链接编译成功。
3)简单测试
上面编译成功后,我这里直接通过代码的方式测试环境是否正常,即在单击界面的发送按钮时,将输入的字符串添加到PyBridge中,由线程run函数执行一个print语句打印这个字符串,即//TODO下面添加以下代码:
// 以上略// TODOPyRun_SimpleString(QString("print('%1')").arg(prompt).toStdString().c_str());// 以下略
编译后加载插件和执行这个语句,运行结果如下图:

注:如果控制台没有看到输出,需要从wizard项目属性->链接->系统,改为控制台类型,否则通过Python调用的print函数无法输出显示到控制台。
4)调用真正的Python脚本
把Python脚本语句直接写到C++代码里面执行,肯定不是我们想要的,我们想要的是在C++中直接执行py文件。所以首先创建一个chat.py文件里面定义一个方法接收传入的字符串,并将字符串倒序后返回。这里返回是通过回调的方式进行的,因为LLM作为流式输出时,C++端无需频繁调用,而只需要处理回调的数据就行。
首先,在PyBridge中实现回调:
// PyBridge.h#pragma once#include<QThread>#include<QMutex>#include<Python.h>class PyBridge : public QThread{Q_OBJECTpublic:PyBridge(QObject *parent);voidquery(const QString& prompt);voidstop();// Python 调用的静态包装函数static PyObject* py_emitTriggered(PyObject* self, PyObject* args);signals:// 回调的消息voidtriggeredCallback(const QString& message);protected:virtualvoidrun()override;private:bool m_isStop = false;QString m_prompt;QMutex m_mutex;};// 模块方法定义static PyMethodDef CallbackMethods[] ={{"trigger_callback", PyBridge::py_emitTriggered, METH_VARARGS, "Trigger callback"},{NULL, NULL, 0, NULL}};static struct PyModuleDef callback_module ={PyModuleDef_HEAD_INIT,"qt_callback",NULL,-1,CallbackMethods};PyMODINIT_FUNC PyInit_qt_callback(void);
// PyBridge.cppPyObject* PyBridge::py_emitTriggered(PyObject* self, PyObject* args){const char* message = nullptr;if (!PyArg_ParseTuple(args, "s", &message)) {return NULL;}// 发射信号emit g_this->triggeredCallback(QString::fromUtf8(message));return Py_BuildValue("");}PyMODINIT_FUNC PyInit_qt_callback(void){return PyModule_Create(&callback_module);}
根据Pyhon回调机制,需要在Python环境初始化时,将回调对象进行注册,即在Py_Initialize()之前调用:
PyImport_AppendInittab("qt_callback", &PyInit_qt_callback);Py_Initialize();
char.py里定义一个函数:接收传入的字符串,倒序后通过回调返回到C++
import qt_callback # 这个模块就是上面代码注册到Python环境的# 回调实现def chat_with_callback(prompt):s = prompt[::-1]qt_callback.trigger_callback(s)
将这个py文件放到与bin同级的py_scripts目录里面,然后获取输入的字符串设置到PyBridge,run函数自动检测到有数据则开始调用在py文件里面的方法,代码如下:
voidPyBridge::run(){static QString prompt;// 注册回调PyImport_AppendInittab("qt_callback", &PyInit_qt_callback);// 初始化python解释器.C/C++中调用Python之前必须先初始化解释器Py_Initialize();// 判断python解析器的是否已经初始化完成if (!Py_IsInitialized())qDebug() << "Py_Initialize fail";elseqDebug() << "Py_Initialize success";while (!m_isStop){QThread::msleep(1);m_mutex.lock();if (m_prompt.isEmpty()) {m_mutex.unlock();continue;}else {prompt.swap(m_prompt);m_mutex.unlock();// 添加 Python 模块路径(如果需要)PyRun_SimpleString("import sys");PyRun_SimpleString("sys.path.append('../py_scripts')"); // 添加当前目录到搜索路径// 加载 python 脚本PyObject* pModule = PyImport_ImportModule("chat"); // 脚本名称,不带.pyif (pModule) {// 流式回复调用callFlow(pModule, prompt);// 释放模块Py_DECREF(pModule);}else {qDebug() << QStringLiteral("模块导入失败");PyErr_Print();}prompt.clear();}}// 销毁自上次调用Py_Initialize()以来创建的所有子解释器。Py_Finalize();}
// 流式回复void PyBridge::callFlow(PyObject* pModule, const QString& prompt){// 获取函数对象PyObject* pFunc = PyObject_GetAttrString(pModule, "chat_with_callback");if (pFunc && PyCallable_Check(pFunc)) {// 准备字符串参数(将 QString 转为 Python 字符串)PyObject* pArgs = PyTuple_New(1);PyObject* pStr = PyUnicode_FromString(prompt.toUtf8().constData());PyTuple_SetItem(pArgs, 0, pStr);// 调用 Python 函数,这里会阻塞直到流式回复完成PyObject* pResult = PyObject_CallObject(pFunc, pArgs);// 处理返回值if (pResult) {// 释放返回值Py_DECREF(pResult);}else {qDebug() << QStringLiteral("函数调用失败");PyErr_Print();}// 释放资源Py_DECREF(pArgs);Py_DECREF(pFunc);}else {qDebug() << QStringLiteral("无法找到函数或函数不可调用");PyErr_Print();}}
编译后,在下方的输入框中输入字符串,点击发送后,在上方的框中显示返回的字符串(注:还没有涉及到LLM的流式回复):
4
LLM调用
import qt_callbackimport osfrom openai import OpenAIdef chat_with_callback(prompt):messages = [{"role": "user", "content": prompt}]# 创建client客户端client = OpenAI(api_key=os.getenv("DASHSCOPE_API_KEY"),base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",)response = client.chat.completions.create(model="qwen3.6-flash", # 大模型名称messages=messages,temperature=0,# temperature采样温度:取值范围[0, 2),表示模型输出的随机性,0表示随机性最小。stream=True, # 此处设置流式回复)for chunk in response:if chunk.choices[0].finish_reason:qt_callback.finish_callback()breakif chunk.choices[0].delta.content == None:continues = chunk.choices[0].delta.content;# 回调到C++/Qtqt_callback.trigger_callback(s)
注:
1、上面的代码是无法直接在Python环境运行的,因为qt_callback模块是C++注入的
2、如果出现PyImport_ImportModule时,模块导入失败则检查系统Python环境里面是否安装了OpenAi的库
最后的效果就是本文开篇的视频。
5
后记
但目前仅仅是问答式对话,跟afsim本身还没有产生关系,后续可以在这个基础上扩展AI应用能力(如智能想定生成、RAG检索增强、Agent决策等)。
往
期
推
荐
