去年年底我接手一个内部文件分发系统最烦的不是性能也不是并发而是那个已经叠了三百多行的文件分类方法。需求方隔三差五提一句“带‘作废’字样的PDF单独分出去”“合同附件里如果出现‘补充协议’要单独建目录”每次我都要顺着代码去查else if分支改完还要发版。后来我把这堆规则全从代码里拆了出来用「知识库」思路重构了文件分类逻辑效果比预期好得多。简单说就是把“规则是什么”和“规则怎么用”彻底分开代码里只剩一个通用匹配引擎所有分类规则变成可独立维护的数据甚至可以让业务同事直接改。再往后我又把向量检索和RAG的思路融进来解决纯规则匹配永远搞不定的模糊分类问题。这篇文章把整个过程拆开讲清楚适合被硬编码分类逻辑困扰的后端开发、写自动化脚本的运维以及正在折腾知识库工具但不知道怎么落地的朋友。1. 硬编码分类规则当代码里的if-else开始吞掉需求1.1 一个本来看起来很简单的分类器大多数文件分类逻辑的起点都很朴素。那时候代码大概长这样def classify_file(file_path): ext file_path.suffix.lower() if ext in (.jpg, .png, .gif, .webp): return image if ext in (.doc, .docx, .pdf): return document if invoice in file_path.name.lower(): return finance if 合同 in file_path.name: return contract return uncategorized写第一版时我觉得很清晰按扩展名分按文件名关键词分最后兜底。可一旦文件类型多起来需求方开始提“按内容特征区分”这个函数就进入失控轨道。我见过最夸张的版本classify_file 里嵌套了十来层 if else还夹杂着正则、文件名黑名单、路径白名单每次调用都是从第一个条件一路试到最后一个。这种写法的本质问题是规则长在代码里代码马上变成规则的“日志”。你翻代码时看到的不是“当前规则是什么”而是“这些年规则被叠加成了什么样”。到后期谁也不敢轻易删分支因为不知道哪条路径被哪个历史业务依赖着。1.2 硬编码的三种真实成本很多人觉得硬编码只是“改起来麻烦”我实际体会下来至少有三层成本是每天在发生的部署周期成本每改一个分类规则都要经历提交代码、走评审、打镜像、发布新版本。哪怕只是把「作废」两个字加进关键词列表也得等一个发布窗口。文件分类这种需求恰恰是业务方想随时改、马上生效的硬编码与业务期待天然冲突。上下文丢失成本规则写在代码里通常只有半个注释。半年后你看到一行if temp in file_path:根本不记得这是为了防止临时文件进库还是因为某个外部系统固定会上传带 temp 前缀的文件。没有规则描述、没有负责人、没有生效时间这类代码最后只能靠猜。需求被压低的成本产品经理和技术同事都知道改分类规则很不容易潜意识里会减少提需求。于是业务长期用一个不够准确的分类结果错误文件静默堆积这种沉淀到最后变成数据质量灾难。1.3 硬编码 vs 外部规则 vs 知识库方案我先拿一张表对比几种常见路线的差异再展开聊知识库思路对比维度硬编码 if-else外部规则文件YAML/JSON知识库方案规则语义检索改规则是否需要发版需要不需要加载配置即可不需要甚至可在后台编辑能否处理模糊分类很难只能靠正则碰运气基本不能可以通过向量召回兜底审计与回滚靠 git 历史且不直观靠配置版本清晰强规则版本化决策日志业务方能否参与维护不能部分能但仍要看技术文档能规则描述贴近自然语言实现复杂度最低中等偏高需要匹配引擎和向量检索设施核心结论是如果分类规则已经频繁变化或者存在大量“说不清但能凭经验判断”的文件知识库思路是一个值得认真的重构方向。2. 知识库方案的整体设计思想把规则当数据2.1 先定一个原则分类器不碰业务规则重构第一步不是写代码而是定规矩。我给自己定了一条硬性界限分类器只做三件事——把输入文件信息标准化向知识库查询匹配规则执行规则绑定的动作。至于“什么文件该分到什么类”完全由外部的规则数据决定。这意味着代码库里的classify_file不再是一个写满了业务判断的函数而是一个通用的规则执行器。你传入一份待分类文件的上下文它返回一个分类结果期间所有业务知识都来自外部规则源。这样设计之后业务规则全部沉淀在“知识库”里代码只是规则的执行环境。2.2 规则的三段式结构把规则当数据第一件事就是定一个稳定的规则结构。我比较推荐三段式条件condition、动作action、元信息meta。rules: - rule_id: rul_contract_001 name: 合同PDF识别 condition: all: - match: extension value: pdf - match: filename_regex value: (合同|contract|协议) action: category: contract priority: 90 meta: owner: 法务部 effective_from: 2026-01-01 effective_to: null拆开说condition规则触发的条件集合可以嵌套all、any进行组合。每个原子条件由“字段 算子 值”组成比如扩展名等于.pdf文件名匹配某个正则。这样设计的好处是以后加新条件只是新增一个匹配算子不用改整体结构。action规则命中之后要执行什么。最简单的就是返回一个分类标签复杂一点还能绑定 “移动到指定目录”“加标签”“通知审批”等动作。meta规则的“元信息”包括负责人、生效时间窗口、状态。这是容易被忽略但非常重要的部分。我曾遇到过规则文件里一堆没有生效期的规则节假日临时规则忘了下线一直跑了好几周直到一份统计数据严重异常才暴露。2.3 规则存储和加载的选型思路规则数据放哪儿决定了这套系统的维护边界。按项目规模我给三个建议最简单Git 仓库管理 YAML/JSON 文件。适合团队规模不大、规则总量不超过几百条的场景。规则文件跟着代码仓库走天然带上版本管理和 code review但规则变更多少还是要走一次变更流程。适中数据库表 缓存。规则放在数据库里后台界面直接增删改查业务方可以自己维护。适合规则数量大、需要频繁更新、还需要按团队权限控制的场景。但要注意缓存失效问题否则一定出现“改了规则却不生效”的投诉。复杂级别引入向量库/RAG 知识库。当规则条件本身包含大量自然语言描述或者你必须处理很多“没见过的新文件”时可以引入向量检索把规则变成“语义可召回的知识”。这个我放到第4节专门讲。2.4 为什么这种设计能带来后续收益规则变成数据后可以享受数据该有的待遇版本管理、权限分离、自动测试、灰度发布、审计回溯。这些不只是工程上的收益更重要的是改变了协作方式。以前业务方提一个“改分类规则”的需求要排队等开发排期开发也不一定有上下文。现在规则做成数据懂业务的人可以直接维护规则开发只需要保证匹配引擎稳定。这个转变在团队里带来的效率提升远比“少写几行代码”更有价值。3. 重构实战从if-else分类器到知识库驱动3.1 先把旧代码里的规则“挖”出来重构的第一步很枯燥但最关键把散落在代码里的规则完整挖出来整理成结构化数据。我当时的做法是逐个if分支看把条件、动作、为什么存在都记录下来。简单说就是四步把每个 if / match / regex 分支摘出来。用逻辑与或非组合成条件组统一语义。给每条规则补上负责人、生效时间、优先级。输出成 YAML 或 JSON 文件先人工 review 一遍。这一步很考验耐心。因为很多历史规则连当事人都不记得原因了只能从 git blame 和需求单里反推。我最后发现三条规则已经失效一年以上还有两条规则的条件相互覆盖顺序调换会直接影响结果。挖完之后顺手清理了一批垃圾规则。3.2 定义匹配引擎的接口规则建模之后需要一个匹配引擎来执行规则。关键设计是“引擎只认识结构化的规则对象不知道具体业务”。我的 Python 实现大致如下from dataclasses import dataclass from typing import Any, Dict, List, Optional dataclass class Rule: rule_id: str name: str condition: Dict[str, Any] action: Dict[str, Any] priority: int meta: Dict[str, Any] class RuleEngine: 通用规则匹配引擎不感知具体业务。 def __init__(self, rules: List[Rule]): self.rules sorted(rules, keylambda r: r.priority, reverseTrue) def match(self, context: Dict[str, Any]) - Optional[Rule]: for rule in self.rules: if self._eval_condition(rule.condition, context): return rule return None def _eval_condition(self, cond: Dict[str, Any], ctx: Dict[str, Any]) - bool: if all in cond: return all(self._eval_condition(c, ctx) for c in cond[all]) if any in cond: return any(self._eval_condition(c, ctx) for c in cond[any]) field cond.get(field, ) op cond[match] expected cond[value] actual ctx.get(field) return self._compare(op, actual, expected) def _compare(self, op: str, actual: Any, expected: Any) - bool: if op exact: return actual expected if op in: return actual in expected if op regex: return bool(re.search(expected, actual or )) if op keyword: return all(kw in (actual or ) for kw in expected) raise NotImplementedError(funsupported op: {op})这里我把所有匹配方式收敛成几个算子exact、in、regex、keyword。以后需要新增“按文件大小范围”“按上传时间”等条件只需要扩展_compare方法引擎主体不用动。这种注册表模式在实战中非常管用它能保证规则演进跟得上业务而核心代码保持稳定。3.3 影子模式先并跑再切换我踩过的最大一个坑重构后直接切换结果新引擎对旧规则的理解有偏差一堆文件被分错。后来学乖了先做“影子模式”。影子模式很简单新引擎和旧分类器同时运行但新引擎的结果只进日志不影响真实分类。跑一周对比新旧结果把所有差异项拎出来人工确认。python classifier.py --legacy-mode python classifier.py --shadow-mode --rule-file rules_v2.yaml --output diff_report.csv通过数据发现了两类问题一是旧规则在各个 if 分支之间本身就存在优先级冲突新引擎按 priority 排序后和旧逻辑顺序不一致二是某些正则表达式从代码搬到 YAML 时转义层级出错。这些问题靠代码 review 很难发现只有并跑对比才靠谱。3.4 原子切换和回滚开关影子模式跑稳之后真正切换时要设计好开关。核心原则是切换是配置驱动的不是改代码驱动的。我用环境变量控制规则来源import os from utils import load_rules RULESOURCE os.getenv(RULESOURCE, legacy) if RULESOURCE knowledge: engine RuleEngine(load_rules(rules_v2.yaml)) else: engine LegacyRuleEngine()这样线上想切到新引擎只需要改环境变量出问题立刻切回不需要重新发布。切换动作是原子的一个配置项搞定。回滚也是一秒完成不用经历漫长的部署流程。4. 规则检索的进阶引入向量召回和RAG解决模糊分类4.1 精确规则的局限性把规则从代码里拆出来只是解决了“规则可维护”的问题。真正让我头疼的是模糊分类场景有些文件不管用扩展名、正则还是关键词都难以准确判断。举个例子一个叫2024_年度结算_张三.pdf的文件文件名里没有“合同”两个字但内容是一份服务合同。靠文件名关键词匹配它大概率会被分到“财务”或者“其他”。这种文件靠精确规则永远搞不定必须靠内容语义。另一个更常见的场景用户上传的文件名是乱码只有扩展名docx但你看了正文第一段就能判断它属于技术方案。这类“人类凭经验秒懂机器毫无头绪”的情况就是向量召回和RAG发挥价值的地方。4.2 思路把规则条件描述成自然语言再用语义检索召回知识库方案走到这步就和一个真正的「知识库」接上了。具体思路是这样每条规则在保留精确条件字段的同时增加一段自然语言描述。比如- rule_id: rul_contract_002 name: 合同类 semantic_description: 包含合同、协议、结算条款、服务内容、甲方乙方等内容的文档归入合同类别当精确规则没有命中时我就用文件名扩展名文件内容前几百字拼一段查询文本去向量知识库里检索语义相近的规则用匹配到的规则动作做分类。4.3 代码示例一个本地语义召回分类器向量检索的实现有很多选型可以用 Dify 等开源知识库平台也可以直接用 embedding 模型自己拼。我这里用一段 Python 示例演示核心逻辑embedding 模型名称仅作示意import numpy as np from sentence_transformers import SentenceTransformer # 实际可替换成 bge-small-zh、m3e-small 等按部署环境定 model SentenceTransformer(BAAI/bge-small-zh-v1.5) rules_text [ 包含合同、协议、结算条款、甲方乙方等内容的文档归入合同类, 包含财务报表、账单、发票、税额等内容的文档归入财务类, 包含论文、开题报告、参考文献、研究方法的文档归入资料类, ] rule_vectors model.encode(rules_text, normalize_embeddingsTrue) def semantic_classify(query: str, threshold: float 0.55): q_vec model.encode([query], normalize_embeddingsTrue)[0] sims rule_vectors q_vec best_idx int(np.argmax(sims)) if sims[best_idx] threshold: return uncategorized, sims[best_idx] return rules_text[best_idx], sims[best_idx]注意几点向量模型选中文效果好的轻量模型即可不必一上来就用超大模型文件分类场景的语义空间没那么复杂。normalize 后算点积等价于余弦相似度速度会快很多。threshold阈值非常关键。设得低会出现乱分类设得高会大量落入“待人工分类”。我通常会通过一批历史文件跑出相似度分布再选一个落在低谷区的值。4.4 混合匹配架构精确规则优先语义召回兜底落到完整架构我的执行顺序是这样的先把文件信息标准化提取扩展名、文件名、路径、大小、内容片段。先走精确规则引擎按 priority 逐条匹配命中就直接返回。精确规则无命中进入向量召回用查询文本找到 Top3 相似规则。Top1 相似度超过阈值使用该规则绑定动作。低于阈值进入“待人工分类”队列同时把语义相似度、候选规则都记录到日志。这个流程保证了高频且明确的场景由精确规则处理延时低、可控性强模糊场景才动用向量检索避免把所有判断都押在 embedding 模型上。4.5 不要一上来就上 RAG索引遗忘规则在这里说一个我自己的教训第一次做语义召回时我把所有精确规则也直接转成文本丢了进去结果召回率反而下降。原因是精确规则文本化后丢掉算子约束向量检索把很多长得像但完全不该命中规则的样本捞出来。后来我强制区分规则索引精确规则存 YAML 文件语义规则单独维护文本描述只有后者才进向量库。分开索引、分开维护效果立刻正常。5. 规则治理版本、冲突和回滚这些绕不开的细节5.1 每条规则都应该有身份规则真正变成知识库数据后面临的问题和代码一样怎么保证一堆规则长期可维护我的答案是给每条规则一个不可变的身份rule_id配合状态字段。rule_id规则唯一标识创建后不变删除只做标记不能物理删除。statedraft、active、disabled、archived。effective_from/effective_to生效时间窗口到点自动下线避免过期规则继续跑。owner负责人至少能回答“这条规则当时是谁定的为什么要这么定”。这些字段不是装饰是长期运行的救命信息。我遇到过一条分类规则两周后发现分类结果明显不对后来查owner找到业务方对方才想起来那是上一个项目的临时方案忘了下线。5.2 冲突检测别让两条规则打架多条规则同时匹配一个文件时priority 会决定谁生效但这也引出一个新问题是否有些规则永远不会被触发我在重构后加了规则冲突扫描脚本主要检查三类问题条件完全一致但动作不同说明规则本身定义矛盾必须去重或调整优先级。高优先级规则完全覆盖低优先级规则低优先级规则躺枪永远不会命中要么删除要么调整条件。条件里引用了不存在的字段或非法正则这种规则在运行时才会报错扫脚本能提前发现。检查脚本可以做成 CI 的一环规则文件一提交就自动跑有冲突直接拦截。python scripts/check_rules.py --rules rules_v2.yaml5.3 灰度与回滚设计规则更新和代码发布一样也需要灰度机制。我的做法每次规则变更生成一个新版本比如rules_v2.3.yaml带着完整的变更记录。灰度时让 10% 流量使用新规则观察分类结果分布是否异常。关键指标是“未匹配比例”和“人工纠错比例”。如果未匹配比例突然飙升大概率新规则有改动失误。异常时一键切回上一个规则文件因为规则文件本身带版本切换就是改配置再刷新缓存不需要发版。5.4 我踩过的一些坑把这段经历分享出来希望能帮你避开规则文件一长就没人维护。几百条规则放在一个 YAML 里后来没人敢碰。后来我按分类维度拆成多个文件比如contract_rules.yaml、finance_rules.yaml每个文件单独维护才好转。规则放到数据库但忘了缓存失效。后台改了一条规则前端程序还在跑旧缓存业务方盯着后台界面说“改了没生效”。现在我对规则缓存设置了基于规则版本的主动刷新机制版本一变全量刷新。忽略规则的生效时间窗口。节假日临时规则到点没下线带着过期业务语义跑了很久。在规则里加effective_to字段后这个问题才根治。向量库模型升级导致相似度整体偏移。换 embedding 模型后同样一段文本的相似度分数普遍变化依赖旧阈值触发的规则全部失效。后来我把“规则语义文本”和“阈值”一起纳入版本管理模型升级必须重算阈值。6. 边界与取舍不是所有文件分类都适合“知识库化”6.1 适合知识库化的场景从我实际经验看知识库思路最适合以下三类场景规则变化频率高而且规则来源不完全由开发掌握。比如法务、财务、运营几个部门都会提出“什么文件应该归哪类”。文件类型多样存在大量规则无法精确描述的模糊场景。这种情况下纯精确规则永远会有漏网之鱼需要语义召回兜底。有审计和回溯需求希望知道“这个文件为什么被分成这一类”能明确指向某条规则的某次决策。6.2 不适合知识库化的场景反过来我也要泼点冷水。如果你属于下面这些情况别急着重构只是做一个临时整理脚本规则稳定两三年都不变。此时硬编码最简单重构得不偿失。对性能极其敏感一次分类必须毫秒级完成。知识库化不是不能优化但引入配置加载、规则解析、可能的向量检索后性能调优成本会明显上升。项目连基本日志都没有没有评估重构效果的基线数据。先把日志和监控建好再谈规则拆不拆否则你就是裸着重构。6.3 我建议的激进程度如果决定要做建议按激进程度分三级走别一步到位第一级只把规则从代码挪到 YAML。这个最简单收益最大危险性最低。第二级把匹配器做成通用引擎规则由业务方维护。这一步开始改造代码结构让业务规则真正脱离开发排期。第三级引入向量召回和语义分类接入 RAG 知识库。这是完整形态但意味着要承担 embedding 模型、阈值调优、语义规则维护的成本。我见过很多团队直接冲第三级最后被召回率、阈值漂移、语义规则不维护等问题拖垮。实际应该先把前两级走扎实确定精确规则确实应付不了模糊分类再动第三级。回到最开始的问题。把规则从代码里拆出来的真正意义不在于“少写 if else”而在于让规则成为可以被治理、被检索、被回滚的知识资产。文件分类只是一个切入点同样的思路完全可以延伸到工单自动分派、日志告警分类、文档自动打标这些场景。如果你发现项目里规则已经频繁变化、业务方天天找你对规则细节那确实到了该用知识库思路重构的时候。开始之前先把旧规则整理成结构化数据设计好你的规则 Schema从这第一步开始后面的一切都会顺理成章。
阅读完成 · 觉得有帮助?