
简介面向人工智能课程学习者、毕业设计开发者以及推荐系统设计入门读者复旦大学2022年人工智能课程的房源推荐专家系统项目是一份完整可参考的课程实践。项目围绕房源数据清洗、POI空间匹配、模糊规则推理和评分输出展示了从数据处理到推荐决策的完整链路。压缩包共39个文件整体约28.71MB其中包含17个Python脚本涉及Django管理、地图可视化、模糊推理与评分逻辑另有CSV数据集、JSON配置、TEX/Markdown文档、HTML展示页及上海市POI数据压缩包目录结构清晰。资源内附README说明、设计报告LaTeX源码和实验展示图片便于对照理解项目思路、复现运行效果或在此基础上二次开发。当前已有66人浏览学习适合用于课程项目复盘、毕业设计参考或智能推荐系统的方案借鉴。1. 一个人工智能大作业为什么偏偏选专家系统来推荐房源很多人在入门人工智能时第一时间想到的都是神经网络、大模型但 2022 年前后不少高校课程项目反而要求避开深度学习强制用“可解释、可审计”的经典人工智能方法来完成一个场景任务。房源推荐专家系统正是这类课程的典型落点用户输入预算、区域、户型、通勤容忍时间系统根据一套明确的规则库推断出合适房源并在结果末尾附上“为什么推荐这一套”的推理路径。相比黑盒推荐专家系统的诱人之处在于——知识库和推理机分离规则可以随时增删每一次推荐都能回溯到某条具体规则。这也是检索“专家系统”时最容易踩进去的误区以为只是堆if-else其实真正的工作量在知识工程把领域经验抽取成结构化规则再设计一套匹配、冲突消解、解释的推理机制。这篇博文就按我实际会用的思路从规则建模、推理机设计到代码实现把这个方向完整拆开。2. 专家系统建模房源知识库的规则从哪来、怎么表达才不塌2.1 规则库的三个来源专家访谈、公开数据统计、用户反馈常见做法是先定义一个最小可行规则集再逐步扩充。房源推荐领域没有标准开源知识库所以最可靠的做法是把规则来源分成三类分别抽样说明。第一类是专家访谈。房产中介的典型经验是“首套刚需关注总价和教育资源改善型关注户型通透性和梯户比投资型关注地铁距离和租金回报率”。这类经验抽取出来就是一条带条件属性和结论属性的产生式规则。第二类是公开数据统计。利用链家、贝壳的历史成交记录可以算出一些阈值区间例如“朝阳区两居室成交价下四分位是 420 万”量化后作为规则的边界条件。第三类是用户反馈日志。课程项目如果没有真实日志可以用问卷模拟找 20 个同学填偏好卡片统计出“超过 80% 的人不接受单程通勤超过 75 分钟”这个阈值就可以入规则。为了让规则库可维护我一般会把每个规则的设计依据记在元数据里形成一张规则登记表。表结构如下字段含义示例rule_id规则编号R0012优先级冲突消解时用数值越大越先被执行80条件属性匹配的用户输入字段budgetcommute_time条件逻辑比较操作符和阈值lte(3500000)结论属性推理出的房源属性或行为tag潜力学区置信度0~1表示该规则在领域中的可信程度0.92来源专家访谈/数据统计/用户反馈数据统计备注设计依据2021 年成交数据 p30 分位2.2 硬规则和软规则的拆分什么是必须满足什么只是加权房源推荐专家系统里最容易翻车的设计就是把所有规则都当成硬约束。比如用户输入“总价不超过 400 万”系统如果只做过滤会把“总价 405 万但步行到地铁 5 分钟”的优质房源直接丢弃。实际工程中的做法是双层规则设计。硬规则就是反向过滤所有不满足直接淘汰。常见值包括预算上限通常按用户输入的 1.05 倍做弹性放行完全无弹性的规则会因数据噪声导致零结果通勤时间上限由用户显式给出则严格执行用户未给出时用统计默认值户型要求比如用户明确要三居室就不应返回两居室。软规则则用于打分排序常见值包括教育配套因子、商圈繁华度、楼龄、朝向、同小区均价偏离率。规则表达上用 JSON 管理结构固定为condition / action / meta。condition 描述用户输入侧的匹配条件action 描述推理结果侧的修正meta 存放优先级和来源。一条融合规则可以写成下面这样{ rule_id: R0037, priority: 70, condition: { user_type: first_time, has_child: true }, action: { boost: { education_score: 2.0, commute_weight_scale: 1.2 } }, meta: { confidence: 0.9, source: expert_interview, dependency: [R0001] } }这段结构里condition里的has_child是布尔值action里的boost表示在后续打分时对education_score做加权放大dependency字段声明该规则必须依赖 R0001 先执行这是处理规则链的抓手。参数上常用的调整方式是优先级相差不超过 10 的规则不保证严格先后而是交给推理机的冲突消解策略处理。2.3 先分类再打分规则分组让知识库不变成一锅粥规则多起来以后如不分组会出现“规则互相覆盖又没人发现”的隐患。我一般把规则库分成四组过滤规则、修正规则、打分规则、解释规则。过滤规则负责硬淘汰修正规则负责做输入侧纠偏比如用户选了“南向”却实际指向“东南朝向”打分规则产出各维度的标准分解释规则把推理路径翻译成用户能看懂的话。每一条规则都只能归属一组不搞双重身份。分组名称直接作为group字段写入规则元数据这样单独导出一组规则做单元测试时只用一条grep命令即可。规则入库时还需要配一个简单的校验脚本检查四件事变量名是否都能在房源字典里对上、阈值是否都落在合法区间、规则之间的dependency是否有环、是否存在完全相同的 condition。跑一个 30 条左右的小规则库通常用不到复杂规则引擎用字典加json schema校验就完全够超过 200 条再考虑转入CLIPS或Drools。3. 推理机实现前向链接、冲突消解与置信度传播3.1 为什么房源推荐选择前向链接而不是反向链接规则推理的主流方法分为前向链接数据驱动从已知事实推结论和反向链接目标驱动从假设反推证据。房源推荐场景里用户输入的是事实集合目标集合是开放式的——系统并不知道该先验证哪套房子推荐过程要把全部候选房源逐套过一遍规则集所以前向链接是匹配度更高的选择。反向链接更适合故障诊断类系统比如根据“空调不制冷”倒推“压缩机是否损坏”。课程答辩时能把为什么选前向链接解释清楚比堆砌宽泛的“AI 概念”更能说明设计意图。推理机的基本循环是匹配规则条件、选择可执行规则、执行动作、更新事实库、重复上述步骤直到没有新事实产生或者达到最大迭代次数。3.2 冲突消解四个策略并非互斥通常混合使用同一轮迭代中经常出现多条规则同时命中的情况比如用户输入了“预算 400 万以下”同时命中“近地铁”和“楼龄 10 年以内”两条规则但系统当前只想产出最合适的三个候选这时就需要冲突消解。房源推荐系统里最常用的四类策略按执行先后排序如下。1. 按规则优先级排序priority 数值最高者先执行 2. 按 specificity 排序condition 中条件个数更多者先执行 3. 按 recency 排序最近更新过的规则优先 4. 按规则成本排序计算代价小的规则先跑快速剪枝实际项目里我采用组合策略先按优先级粗排再按条件个数精排最后用规则成本做剪枝。这样做的理由是优先级表达的是领域重要性条件个数表达的是匹配精确度两者同时考虑才不会让低优先级但高精确度的规则永远得不到执行机会。3.3 推理结果要有解释路径这一层是专家系统和普通过滤器拉开差距的地方规则命中后系统不只是返回房源列表还要保存一条推理路径。每条路径记录为三元组触发规则 ID、匹配到的用户事实、产出的房源属性。比如用户事实里“单程通勤 60 分钟”触发规则 R0012推理路径被序列化成 JSON 写入推荐结果的explain_trace字段。解释规则组负责把这条 JSON 翻译成自然语言。常见做法是维护一个短语模板表用 python 的str.format拼接而非训练一个文本生成模型——专家系统要保持可解释性没必要引入不可控的生成环节。例如“因为出现了 {user_fact}根据 {rule_id} 的规则本套房源被标记为 {conclusion}”。这块做得好在课程项目答辩时会被当作一个亮点看待。4. 房源推荐专家系统的代码骨架从规则加载到推荐输出4.1 一段能跑通的 Python 实现四大模块怎么协作这里给出一个适合课程项目的最小实现拆成四个模块rule_loader 负责加载 JSON 规则matcher 负责条件匹配engine 负责推理循环与冲突消解explainer 负责输出解释路径。为了让代码一眼看懂省略了异常处理和持久化只保留核心逻辑。import json from dataclasses import dataclass, field from typing import List, Dict dataclass class InferenceEngine: rules: List[Dict] max_iterations: int 10 def _match_conditions(self, fact: Dict, condition: Dict) - bool: # 条件中的 key 形如 budget_lte转换为比较操作 for op_key, target_val in condition.items(): attr_name, op op_key.rsplit(_, 1) if attr_name not in fact: return False actual_val fact[attr_name] if op lte and actual_val target_val: return False if op gte and actual_val target_val: return False if op eq and actual_val ! target_val: return False return True def infer(self, user_fact: Dict, house: Dict) - Dict: # 拷贝一份规则避免执行列表被修改 pending_rules sorted( self.rules, keylambda x: x.get(priority, 0), reverseTrue ) fact {**user_fact, **house} trace [] for _ in range(self.max_iterations): executed False for rule in pending_rules: cond rule.get(condition, {}) action rule.get(action, {}) if self._match_conditions(fact, cond): fact.update(action.get(set, {})) for k, v in action.get(boost, {}).items(): if k in fact: fact[k] fact.get(k, 0) v trace.append({ rule_id: rule[rule_id], matched_cond: cond, produced: action }) pending_rules.remove(rule) executed True break # 一轮只执行一条高优先级规则 if not executed: break return {fact: fact, trace: trace}代码逻辑说明_match_conditions把 JSON 条件解析为可比较的字典键值仅支持lte / gte / eq三种操作符减少不必要复杂度。infer每轮从最高优先级规则开始扫描命中即执行并更新事实库然后重新开始下一轮扫描这是典型的前向链接工作方式能保证规则之间的先后依赖关系可预期。参数含义如下max_iterations控制最坏情况下的迭代次数防止规则链死循环每条规则执行后从pending_rules中移除保证每条规则在同一轮推理中只触发一次。4.2 排序阶段与 Top-K 截断推理完成后事实库中会累积total_score或各维度增益值。排序过滤一般用独立函数实现。分数由硬规则的过滤结果加上软规则的加权求和得到。常见加权公式是final_score commute_score * 0.35 price_norm_score * 0.3 education_score * 0.2 house_quality_score * 0.15这里的系数构成了一组需要手工调整的参数。初学者最容易犯的错误是让所有权重加起来大于 1 或小于 1 时不做归一化。我一般先把每个维度打到 0~100 的标准分再乘权重最后只取前 K 个结果。K 通常取 3 或 5理由是一个主推加两个备选是购房者的心理极限超过 5 个推荐只会增加选择负担。Top-K 截断在推荐系统里是一个显式的输出参数也会被放进评测指标里一起说明。4.3 数据不完整时的兜底策略缺省值不该靠猜房源字段齐全时推理毫无问题但真实数据里“物业费”“梯户比”常缺失。如果把缺失值当作 0会产生灾难性的排序偏差。我给每条规则都增加一个on_missing字段取值是skip或default。skip 表示该条件缺失时整条规则跳过不参与匹配default 表示用房价均值、楼龄分位数等统计值代替。这个行为应该在课程报告中专门写一节因为从工程角度审视处理缺失值比调推理算法参数更影响最终体验。一个可以复用的兜底函数如下。def fill_missing(house: Dict, default_map: Dict) - Dict: for key, default_val in default_map.items(): value house.get(key) if value is None: house[key] default_val return house使用场景是对缺失的education_score填充该行政区平均值对缺失的decoration填充字符串unknown而非凭空猜测。注意填充时机应该在规则匹配之前否则_match_conditions会因为条件中某个 key 缺失直接返回 False导致不少房源被静默淘汰。5. 专家系统效果怎么验证回放测试和规则冲突检查5.1 用历史订单做离线回放算出假阳性和假阴性专家系统的验证方式与机器学习模型不同重点不是准确率曲线而是正反例覆盖度。方法是准备一批历史成交记录每一条标记“用户最终选中的房子是 A”然后把这套房源放入一个包含 50 套干扰房源的候选集。系统推荐出 Top-3 后检查真房源是否进入 Top-3在 30 条历史记录上统计命中率。这个命中率不应低于 70%低于 40% 说明规则库的偏好方向和真实市场明显不一致需要重新检查规则来源。正向误报是推荐里出现了明显不符合硬规则的房源这大多是硬规则条件写错或字段名拼错了。负向漏报是真房源根本没有进入候选池常见原因是硬规则弹性放行系数设得过小。检查手段是把漏报单拎出来打印其所有被拒绝时命中的硬规则逐个核对阈值。5.2 规则库的自检脚本死链、冲突与冗余当规则数量超过 30 条人工校对开始变得不现实。我建议在课程项目里附一个check_rules.py自动检测三类问题检测逻辑如下面的表格所示。检查项判定方法修复建议死链规则规则 condition 中的属性名不出现在 input schema 和输出 schema 里修正字段名或补齐 schema规则冲突两条规则条件相同但结论属性取值区间不相交降低其中一条的优先级并注明原因冗余规则规则 A 的条件是规则 B 条件的真子集且动作完全一致合并或在较窄的规则上加额外结论具体的检测代码本质上就是集合运算把所有规则的条件字段拆成集合判断子集关系。这个脚本不需要依赖任何第三方库纯 Python 就可以写。更重要的是在实验报告中总结“规则自动检查可以发现哪种错误哪种错误检查不出来”因为冗余问题不完全等价于语义冲突规则逻辑相同但设计意图不同时仍需人工审定。5.3 一个提升演示效果的技巧支持“为什么不要”的否定解释正向解释用户看得懂也更能在答辩时吸引注意力的是“为什么这套房子被排除”。实现方式是在硬规则过滤阶段记录下被淘汰房源命中的规则 ID。输出时增加一行提示例如“该房源因预算条件超过设定上限 5.2% 被优先排除”。做这一步需要把硬规则过滤从普通判空函数改为可调用的规则记录函数代码改动量只增加十行左右。否定解释的价值在于它证明了系统在做知识推理而不只是搜索过滤。推荐结果被用户质疑时能直接拿出被拒原因正是专家系统相比向量召回方案的核心差异所在。如果要在课程项目上再增加一点说服力可以用同样的规则库和同样的候选集跑一次不加否定解释的对照组统计用户在演示后提出的追问次数。追问次数大幅下降就是“可解释性”最直观的效果度量。本文还有配套的精品资源点击获取