ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC专家系统实战:C++正向/逆向推理引擎开发

MFC专家系统实战:C++正向/逆向推理引擎开发 简介本资源是一套基于MFC开发的动物专家系统完整源码工程面向C与人工智能初学者、高校课程设计学生及AI推理算法实践者旨在通过可视化界面直观理解正向与逆向推理机制在知识系统中的落地实现。压缩包共44个文件涵盖6个头文件.h定义类结构与接口、3个源文件.cpp实现核心推理逻辑、2个可执行文件.exe支持免环境直接运行、1个解决方案文件.sln及配套资源文件.rc、.ico、.res另有编译中间产物.obj、.pdb、.tlog等体现VS2019完整构建流程整体大小为71.31MB。已有739人学习下载读者可获得可运行的MFC图形界面程序、带注释的正向/逆向双模推理源码、清晰分层的项目目录结构含UI层、逻辑层、资源层以及从知识库建模到规则引擎调用的完整技术路径参考。1. 动物专家系统不是“AI玩具”而是MFC环境下正向/逆向推理逻辑的完整工程实践你打开一个.zip文件里面是AnimalExpertSystem.sln和一堆.cpp/.h文件双击运行后弹出带“哺乳类”“鸟类”“爬行类”下拉框的对话框——这不是教学Demo而是一个可调试、可扩展、能真实走通规则引擎闭环的MFC桌面应用。它用C在Windows原生框架里实现了知识库加载、事实断言、规则匹配、路径回溯与解释链生成核心价值不在“识别动物”而在把人工智能课程里抽象的推理机制落地成VC工程中可单步调试的指针操作和消息响应。适合正在做软件综合实践、智能系统课程设计或想补足MFC规则引擎实战能力的开发者——尤其当你发现教材里的“正向推理伪代码”在VS2019里编译不过、CListCtrl插入规则时崩溃、或者逆向推理的回溯栈总越界时这个源码包就是你该逐行对照的参照系。它不依赖第三方AI库所有推理逻辑由CInferenceEngine类封装知识库以文本规则文件驱动真正体现“用MFC写专家系统”的技术纵深。2. 正向推理引擎从规则文件解析到冲突消解的MFC消息驱动实现正向推理不是简单遍历规则而是在MFC消息循环中构建事实集、触发规则匹配、处理冲突并更新UI。本项目采用基于规则优先级最近使用时间的混合冲突策略避免死循环和规则饥饿。2.1 规则文件格式与CRuleSet类解析逻辑规则存储于rules.txt每行一条格式为IF (毛发是) AND (哺乳是) THEN (类别哺乳类) PRIORITY80其中PRIORITY字段决定冲突时的激活顺序数值越大越优先。CRuleSet类负责加载与索引// CRuleSet.h class CRuleSet { public: struct SCondition { CString field; CString value; }; struct SAction { CString field; CString value; }; struct SRule { int priority; std::vectorSCondition conditions; std::vectorSAction actions; bool bActivated; // 是否已触发过 }; std::vectorSRule m_rules; void LoadFromFile(LPCTSTR lpszPath); // 实际解析函数 };提示LoadFromFile中关键点在于用AfxExtractSubString拆分AND条件并对每个fieldvalue对调用Trim()去除空格。若字段名含中文如“毛发”必须确保rules.txt保存为UTF-8 with BOM否则CString解析会乱码。2.2 推理主循环OnStartForwardInference() 的三阶段执行流点击“正向推理”按钮触发CMainFrame::OnStartForwardInference()其执行分三阶段2.2.1 事实初始化与工作内存构建// CMainFrame.cpp void CMainFrame::OnStartForwardInference() { // 1. 清空当前事实集m_facts 是 CMapStringToString m_facts.RemoveAll(); // 2. 从UI控件读取初始事实例ComboBox选中值 CComboBox* pCB (CComboBox*)GetDlgItem(IDC_COMBO_HAIR); int nSel pCB-GetCurSel(); if (nSel ! CB_ERR) { CString strVal; pCB-GetLBText(nSel, strVal); m_facts[毛发] strVal; // 键为中文字段名值为选项文本 } // 3. 启动推理线程避免UI冻结 AfxBeginThread(ForwardInferenceThreadProc, this); }注意MFC中严禁在UI线程直接执行长耗时推理。此处用AfxBeginThread启动独立线程但需通过PostMessage向主线程发送WM_UPDATE_FACTS消息刷新列表控件避免跨线程访问CListCtrl。2.2.2 规则匹配与冲突消解核心算法ForwardInferenceThreadProc调用CInferenceEngine::RunForward()其核心逻辑// CInferenceEngine.cpp int CInferenceEngine::RunForward(CMapStringToString facts, CRuleSet rules) { bool bChanged true; int nCycle 0; while (bChanged nCycle 100) { // 防死循环 bChanged false; std::vectorCRuleSet::SRule* candidates; // 遍历所有未激活规则收集满足条件的候选 for (auto rule : rules.m_rules) { if (rule.bActivated) continue; bool bMatch true; for (const auto cond : rule.conditions) { CString strFact; if (!facts.Lookup(cond.field, strFact) || strFact ! cond.value) { bMatch false; break; } } if (bMatch) candidates.push_back(rule); } // 冲突消解按PRIORITY降序取第一个 if (!candidates.empty()) { std::sort(candidates.begin(), candidates.end(), [](const CRuleSet::SRule* a, const CRuleSet::SRule* b) { return a-priority b-priority; }); auto pRule candidates[0]; // 执行动作更新事实集 for (const auto act : pRule-actions) { facts[act.field] act.value; bChanged true; } pRule-bActivated true; // 标记已触发 } } return nCycle; }参数说明nCycle限制最大迭代次数防止规则间形成循环依赖如规则A推导B规则B又推导A。bActivated标志位确保每条规则只触发一次符合经典正向链特性。2.2.3 UI同步CListCtrl动态刷新事实链推理完成后主线程收到WM_UPDATE_FACTS消息在OnUpdateFacts()中刷新列表// CMainFrame.cpp LRESULT CMainFrame::OnUpdateFacts(WPARAM wParam, LPARAM lParam) { CListCtrl* pList (CListCtrl*)GetDlgItem(IDC_LIST_FACTS); pList-DeleteAllItems(); POSITION pos m_facts.GetStartPosition(); int i 0; while (pos) { CString key, value; m_facts.GetNextAssoc(pos, key, value); pList-InsertItem(i, key); pList-SetItemText(i, 1, value); i; } return 0; }关键细节CListCtrl列宽需在OnInitDialog()中手动设置否则中文列名显示不全pList-SetColumnWidth(0, 120); pList-SetColumnWidth(1, 150);3. 逆向推理引擎目标驱动的深度优先回溯与解释路径生成逆向推理不是“正向的反向”而是以用户输入的目标结论如“类别鸟类”为起点递归寻找支持该结论的所有前提条件并生成可追溯的推理路径。本项目通过CBackwardEngine类实现其难点在于避免重复访问、管理回溯栈、以及将路径转化为用户可读的自然语言解释。3.1 目标设定与路径初始化OnStartBackwardInference() 的预处理点击“逆向推理”按钮后首先弹出输入框让用户指定目标// CMainFrame.cpp void CMainFrame::OnStartBackwardInference() { CString strTarget; if (CInputDlg::GetInput(_T(请输入目标结论格式如类别鸟类), strTarget) IDOK) { // 解析目标拆分类别鸟类为 field类别, value鸟类 int nPos strTarget.Find(); if (nPos -1) return; CString strField strTarget.Left(nPos).Trim(); CString strValue strTarget.Mid(nPos1).Trim(); // 初始化回溯栈存储待验证的子目标 std::stackstd::pairCString, CString stack; stack.push({strField, strValue}); // 启动逆向线程 AfxBeginThread(BackwardInferenceThreadProc, new SBackwardParam{this, stack, strTarget}); } }注意CInputDlg是自定义对话框类确保其DoModal()在UI线程调用参数通过SBackwardParam结构体传递给工作线程避免数据竞争。3.2 回溯核心DFS搜索与规则反向匹配BackwardInferenceThreadProc调用CBackwardEngine::RunBackward()采用深度优先搜索DFS遍历规则库// CBackwardEngine.cpp bool CBackwardEngine::RunBackward( CMapStringToString facts, CRuleSet rules, std::stackstd::pairCString, CString stack, std::vectorstd::vectorstd::pairCString, CString paths) { while (!stack.empty()) { auto target stack.top(); stack.pop(); CString field target.first; CString value target.second; // 检查是否已是已知事实 CString strKnown; if (facts.Lookup(field, strKnown) strKnown value) { // 目标已满足记录路径节点 paths.back().push_back({field, value}); continue; } // 查找能推出该目标的规则即action中包含fieldvalue bool bFound false; for (auto rule : rules.m_rules) { for (const auto act : rule.actions) { if (act.field field act.value value) { // 找到支持规则将其所有conditions压入栈 for (const auto cond : rule.conditions) { stack.push({cond.field, cond.value}); } // 记录当前推理步骤用于生成解释 paths.back().push_back({field, value}); paths.back().push_back({ _T(由规则), FormatRuleID(rule.priority) }); // 如由规则#80 bFound true; break; } } if (bFound) break; } if (!bFound) { // 无规则支持该目标需用户输入如询问“是否产卵” PostMessageToUI(WM_ASK_USER, (WPARAM)target); return false; // 等待用户响应 } } return true; }参数说明paths是二维向量每个内层vector存储一条完整推理路径FormatRuleID()将优先级转为可读ID如80→#80便于后续生成解释文本。3.3 解释链生成将回溯路径转为自然语言描述推理完成后CBackwardEngine调用GenerateExplanation()生成用户友好的解释// CBackwardEngine.cpp CString CBackwardEngine::GenerateExplanation( const std::vectorstd::vectorstd::pairCString, CString paths) { CString strExp; strExp _T(推理路径\r\n); for (size_t i 0; i paths.size(); i) { const auto path paths[i]; for (size_t j 0; j path.size(); j) { const auto item path[j]; if (item.first _T(由规则)) { strExp _T( → ) item.second _T(\r\n); } else { strExp _T( ) item.first _T() item.second _T(\r\n); } } if (i paths.size()-1) strExp _T(\r\n); } return strExp; }实际效果若目标为“类别鸟类”解释链可能为推理路径毛发否产卵是→ 由规则#75类别鸟类这种结构清晰展示“为什么得出该结论”是专家系统区别于黑盒模型的关键。4. MFC界面与知识库协同控件绑定、规则热加载与调试技巧MFC界面不是静态容器而是与推理引擎实时交互的控制中心。本项目通过消息映射、控件ID关联和文本文件监听实现UI与知识库的松耦合。4.1 控件与字段的双向绑定CFieldBinder类的设计为避免硬编码控件ID与字段名的映射引入CFieldBinder类统一管理// CFieldBinder.h class CFieldBinder { public: struct SBinding { int nCtrlID; // 控件ID如 IDC_COMBO_HAIR CString strField; // 对应知识库字段名如 毛发 int nCtrlType; // 1ComboBox, 2CheckBox, 3Edit }; std::vectorSBinding m_bindings; void BindToDialog(CDialog* pDlg); void UpdateFactsFromUI(CMapStringToString facts); void UpdateUIFromFacts(CMapStringToString facts); };在CMainFrame::OnInitDialog()中注册绑定// CMainFrame.cpp BOOL CMainFrame::OnInitDialog() { CDialog::OnInitDialog(); m_binder.m_bindings { {IDC_COMBO_HAIR, _T(毛发), 1}, {IDC_CHECK_FEATHER, _T(羽毛), 2}, {IDC_EDIT_EGGS, _T(产卵), 3} }; m_binder.BindToDialog(this); return TRUE; }优势新增字段如“水生”只需在m_bindings添加一行无需修改OnStartForwardInference()中的取值逻辑大幅提升可维护性。4.2 规则文件热加载监控rules.txt变更并重载为支持调试时修改规则立即生效添加文件监控// CMainFrame.cpp void CMainFrame::StartRuleFileWatch() { m_hWatch FindFirstChangeNotification( _T(.\\), // 监控当前目录 FALSE, // 不监控子目录 FILE_NOTIFY_CHANGE_LAST_WRITE); // 仅当文件被写入时触发 if (m_hWatch ! INVALID_HANDLE_VALUE) { // 在定时器中轮询MFC无异步IO Completion Port SetTimer(IDT_RULE_WATCH, 500, nullptr); } } void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent IDT_RULE_WATCH) { if (WaitForSingleObject(m_hWatch, 0) WAIT_OBJECT_0) { // 文件被修改重新加载规则 m_ruleSet.LoadFromFile(_T(rules.txt)); // 重置所有规则激活状态 for (auto rule : m_ruleSet.m_rules) rule.bActivated false; FindNextChangeNotification(m_hWatch); } } CFrameWnd::OnTimer(nIDEvent); }注意FindFirstChangeNotification需配合FindNextChangeNotification使用否则只触发一次。定时器间隔设为500ms平衡响应速度与CPU占用。4.3 调试关键点VS2013/2019中定位推理卡死与内存泄漏当推理过程无响应或CListCtrl显示异常时按以下顺序排查现象定位方法修复方案正向推理无限循环在RunForward()循环内设断点观察nCycle值及facts内容变化检查rules.txt是否存在循环规则A→B, B→A增加nCycle上限至50并打印日志逆向推理栈溢出查看调用堆栈深度确认stack.push()是否无终止条件在RunBackward()中添加stack.size() 20的保护判断防止深层嵌套UI控件更新失败在OnUpdateFacts()中检查pList是否为NULL或GetDlgItem()返回失败确保IDC_LIST_FACTS在对话框资源中正确声明且CListCtrl控件类型为CListCtrl而非CListBox中文字段解析失败在LoadFromFile()中AfxMessageBox(strLine)查看原始行内容将rules.txt用记事本另存为UTF-8 with BOM并在LoadFromFile()开头添加setlocale(LC_ALL, Chinese);提示启用MFC调试内存检测在stdafx.h末尾添加#define _CRTDBG_MAP_ALLOC#include crtdbg.h并在InitInstance()开头调用_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF \| _CRTDBG_LEAK_CHECK_DF);程序退出时自动报告内存泄漏。5. 工程化进阶VS2013兼容性适配、多线程安全与规则库版本管理源码包默认在VS2013环境开发但需适配更高版本VS如2019并保障多线程下的数据一致性。同时规则库的版本控制是生产部署的关键环节。5.1 VS2013到VS2019的平滑迁移三个必须修改的配置项VS2019默认使用v142工具集而VS2013为v120直接打开会导致afxwin.h编译错误。需手动调整平台工具集右键项目 → 属性 → 通用属性 → 平台工具集 → 改为Visual Studio 2013 (v120)若需新特性再逐步升级字符集属性 → 常规 → 字符集 → 改为使用Unicode字符集VS2013默认即UnicodeVS2019新建项目可能为多字节MFC引用路径属性 → 常规 → 使用MFC → 改为在共享DLL中使用MFC避免静态链接导致的CWinApp构造问题验证方法编译后运行检查CMainFrame构造函数中AfxMessageBox(_T(Hello))是否正常弹出。若报错LNK2001: unresolved external symbol __imp__AfxGetModuleState即为MFC引用方式错误。5.2 多线程安全CMapStringToString的临界区保护m_facts被正向/逆向线程及UI线程并发访问需加锁// CMainFrame.h class CMainFrame : public CFrameWnd { // ... CCriticalSection m_factLock; // 新增临界区对象 CMapStringToString m_facts; }; // CMainFrame.cpp void CMainFrame::UpdateFact(const CString key, const CString value) { m_factLock.Lock(); m_facts[key] value; m_factLock.Unlock(); } CString CMainFrame::GetFact(const CString key) { CString val; m_factLock.Lock(); m_facts.Lookup(key, val); m_factLock.Unlock(); return val; }注意CCriticalSection是MFC封装的Windows临界区比std::mutex更轻量且无需额外链接库。所有对m_facts的读写操作必须经过UpdateFact()/GetFact()封装。5.3 规则库版本管理rules_v1.txt 与 rules_v2.txt 的切换机制为支持A/B测试或回滚实现规则版本切换// CMainFrame.cpp void CMainFrame::OnSwitchRuleVersion() { static int nVersion 1; CString strFile nVersion 1 ? _T(rules_v1.txt) : _T(rules_v2.txt); if (m_ruleSet.LoadFromFile(strFile)) { AfxMessageBox(_T(规则库已切换至 ) strFile); nVersion 3 - nVersion; // 1→2, 2→1 } else { AfxMessageBox(_T(加载规则文件失败) strFile); } }生产建议在rules_v*.txt文件头添加版本注释# VERSION: 1.2.0# AUTHOR: AnimalExpertTeam# DATE: 2023-10-15并在LoadFromFile()中解析该行写入日志供运维追踪。在CMainFrame::OnStartForwardInference()中插入版本检查日志TRACE(_T(当前规则版本%s\r\n), GetRuleVersion());这样每次推理启动时Output窗口都会输出所用规则版本为问题复现提供关键线索。本文还有配套的精品资源点击获取
返回列表