资讯动态

从空白工程逆推规则判定引擎:需求分析、技术选型与落地实践

发布时间:2026/10/20 3:51:24 来源:尧图企业网站定制
前几天我拿到一个几乎空白的代码工程代号只有三个字母rea。整个仓库里能跑的东西只有一行启动日志README 是空的需求文档更是连影子都没有。我要靠目录结构、命名残留和一段残缺的配置把它逆推成一套能稳定运行的规则判定工具。如果你也接过这种“只有名字没有说明”的半成品或者想训练自己从模糊输入里快速找出技术实现路径这篇内容应该能给你一些可复用的方法。下面我会把完整的推理过程、模块设计、选型对比、踩坑记录和部署细节都摊开来说不绕弯子。1. 代号“rea”背后一个只有轮廓的工程基座1.1 我第一眼看到的仓库结构拿到代码仓库后我先没有急着翻代码而是把整个目录树过了一遍。当时的结构非常简单rea/ ├── run.py ├── README.md ├── config/ │ └── rule.json └── output/ └── .gitkeeprun.py里只有寥寥几行核心逻辑是读取一个叫rea的配置文件然后打印一句“service started”。配置目录里有一个rule.json里面包含了一条示例规则当记录里的amount字段大于1000时标记为review。除此之外没有任何测试、没有接口定义、没有调用方说明。这种状态在内部项目里其实非常常见主程写了一半就转去忙别的留下一个“能启动但什么也不做”的骨架。但从另一个角度看这也是最友好的起点——代码量小没有历史包袱我可以放心大胆地重构只要最终形态能跑通真实场景。1.2 从命名残留里提取线索“rea”这三个字母看着很随意但通常内部项目代号不会完全没有来由。我根据rule.json里的字段和启动日志的风格做了一个候选推断可能的全称是否吻合现有代码Rule Engine Analyzer比较吻合配置里就是规则和判定Route Engine Assistant不太吻合代码里没有任何路由相关逻辑Resource Allocation不太吻合没有资源分配或调度相关接口Recommend Engine API有一定可能但缺少推荐相关的数据模型这里面最有价值的信息不是“rea”到底代表什么而是我们要承认命名只是一层外壳真正定义项目的是数据和流向。我每次接手这种代号型工程都会先写一份“假设清单”把从代码里观察到的事实和猜测分开列出来。比如事实存在config/rule.json字段包含condition、op、value、action。事实启动时会加载名为rea的 profile。猜测系统需要读取业务记录按规则给出处理建议。猜测处理结果可能要被下游消费但目前看不到下游。这份清单看起来简单却能帮你避免最糟糕的启动方式——凭项目名字猜功能然后按猜错的方向写一堆无用代码。1.3 写“契约”比写功能更重要骨架工程最缺的不是功能而是接口契约。项目里没有文档那我就从代码残片里反推它可能接收什么输入、产生什么输出、出错时该怎么办。我把已知线索整理成了表格作为第一版设计的基础模块已知线索未知项优先级配置加载读取 JSON 规则文件是否支持多个配置文件、环境变量覆盖高输入解析无明确定义输入格式、必填字段、字段类型高规则判定字段比较操作符支持哪些操作符、多规则优先级高结果输出无明确定义写入哪里、是否需要查询接口中可观测性只有启动日志错误码、日志级别、监控指标中这张表看起来“不够激动人心”但它决定了后续所有开发的顺序。我个人的经验是先定契约再填实现比边写边想顺手十倍。尤其当需求文档缺失时一份自己整理的契约就是临时的需求规格说明书。2. 需求逆推把“不知道做什么”翻译成“第一版做什么”2.1 从配置结构里锁定真实场景规则文件里只有一条示例规则但结构很清晰规则由id、priority、condition和action组成。condition里又是三段式字段、操作符、目标值。这种结构在业务系统里最常见的用途就是“打标”和“分流”。我当时的推断是某个业务模块会收到一批记录比如订单、请求或工单每条记录带着若干属性rea负责读取这批记录按照配置好的规则逐条判定最终给每条记录一个处理建议比如放行、复核或拒绝。虚构但合理的场景是这样的一个小团队每天要处理上千条业务记录其中有些明显超过常规阈值需要人工复核另一些带有特定标签的记录可能默认放行。手工处理太慢于是他们打算做一个轻量的规则判定工具。rea就是这个工具的内核。2.2 明确第一版的最小闭环接手这种工程最忌讳一上来就设计“完整能力”。我的习惯是先定义一条端到端的最小链路一条输入记录经过解析、判定、落库、查询能完整走通就算第一版达标。第一版范围我圈定为以下四条支持从命令行传入一批 JSON 格式的业务记录。支持从配置文件加载规则并按priority排序执行。对每条记录输出判定建议并写入本地数据库。提供一个简单的查询接口能按时间或记录 ID 拉取判定结果。这个范围刻意排除了很多东西不做可视化面板、不做用户权限、不做规则热加载、不做分布式部署。原因很直接——内部工具的第一价值是“今天就能用起来”而不是“未来所有场景都覆盖”。2.3 写一份可验收的清单需求逆推不是闷头猜而是要把结论写成别人能确认的东西。我当时整理了五条验收条件拿去和团队负责人对齐编号验收条件预期结果1输入一条大于阈值的记录输出review并附带命中的规则 ID2输入一条普通记录输出allow规则 ID 为空3同一批数据重复执行结果不重复写入记录唯一4缺少可选字段不崩溃按默认值参与判定5查询接口返回最近 100 条结果按时间倒序返回包含判定依据这份清单价值巨大因为它把所有模糊的“应该能跑”变成了具体到可以手工验证的条目。后面写测试脚本时我直接拿这五条当用例。2.4 边界划分只做判定不做执行需求逆推还有一个容易被漏掉的部分明确“不做什么”。rea在我的设计里只负责“判定”和“留痕”不负责实际执行动作。比如判定结果是review它不会去通知审核人员判定结果是block它也不会拦截任何请求。这些动作留给下游系统处理。这么划分的理由有两个一是职责清晰判定引擎只需要关心规则和输入执行逻辑的变化不会反向污染核心代码二是未来扩展更灵活同一个判定结果可以既用于通知也用于报表甚至用于训练模型只要输出格式保持稳定。3. 技术选型与数据流设计为什么我没急着写第一行代码3.1 三个候选方案的对比在动手之前我在三个方案之间反复权衡。这里直接放出当时的对比表维度方案A纯Python标准库本地轻量数据库方案B引入重量级规则引擎方案C异步运行时内存队列开发成本低1到2天可以出原型高装配和调优都费时中异步逻辑有一定心智负担并发能力中单机单进程足够内部工具使用高天生为复杂场景设计高事件驱动适合高吞吐维护成本低依赖少高版本升级、表达式语法都要学中部署和排障都更复杂部署体积很小较大中等适合场景规则少、变更不频繁的内部工具规则复杂、需要统一管理的大型平台高并发、低延迟的在线服务最终我选了方案A。理由很现实团队当时只有两类规则变更频率不高系统每天处理量也就是几千条级别。引入一个重量级规则引擎就像在小区门口修一座立交桥能力很足但维护成本远超收益。这种“为了架构而架构”的冲动在只有三行启动代码的项目里尤其危险。3.2 数据流怎么画出来代码还没写我先把数据流在纸上过了一遍输入 JSON 批记录 - 字段标准化补默认值、统一类型 - 规则判定按优先级逐条比较 - 结果聚合记录 action、命中规则 ID - 持久化写入本地数据库避免重复 - 查询接口按条件返回结果每一步的输入输出我都尽量定义成普通字典或结构化对象这样不同模块之间天然解耦。比如规则判定模块只接收标准化后的记录不关心记录来自命令行还是来自 HTTP 请求持久化模块只接收判定结果对象不关心判定逻辑内部怎么写的。这种“先画数据流再写代码”的做法帮我提前发现了一个问题如果直接在启动函数里把解析、判定、写入串在一起将来加一个 HTTP 入口就得复制三遍逻辑。于是我把每条链路拆成独立函数主流程只负责编排。3.3 两个一直挂在嘴边的约束选型过程中我一直拿两个约束做过滤器凡是违反的选项一律砍掉。第一个约束是“尽量不引第三方依赖”。内部工具的生命周期往往比想象中长依赖越多环境迁移成本越高。Python 标准库加上一个嵌入式轻量数据库已经能覆盖绝大多数需求没理由为了一点性能去背更多包袱。第二个约束是“判定过程必须是无状态的”。每一条记录的判定只依赖当前规则和当前记录不依赖上一条记录的结果。这意味着模块里不能出现全局可变状态所有状态要么写在函数参数里要么写在数据库里。这个约束在后面测试阶段帮了大忙也直接避免了多个经典并发问题。4. 逐模块落地把空接口填成完整闭环4.1 配置加载模块配置加载是第一块拼图。它的职责并不只是读一个 JSON 文件而是要把配置变成规则引擎能用的内部结构同时提供合理的默认值和错误提示。我当时实现了一个简单的加载函数import json import os def load_rules(pathNone): path path or os.environ.get(REA_CONFIG, config/rule.json) with open(path, r, encodingutf-8) as f: data json.load(f) rules data.get(rules, []) if not rules: raise ValueError(fno rules found in {path}) return rules这里的细节是支持用环境变量REA_CONFIG覆盖配置文件路径。这样部署到不同环境时不需要改代码只要改运行参数就行。规则文件本身保持纯数据里面不写任何可执行代码尽可能降低“配置”被恶意利用或误用的风险。4.2 规则判定核心规则判定是整个项目的心脏。我没有选择任何现成的规则表达式语法而是实现了很薄的一层比较逻辑支持、、、in四种操作符。之所以够用是因为业务场景里真正用到的就是这几种其余的都是预想中的复杂性。核心函数如下def match_rule(rule, record): cond rule[condition] field_value record.get(cond[field]) op cond.get(op, ) target cond[value] if op : return field_value is not None and field_value target if op : return field_value is not None and field_value target if op : return field_value target if op in: return field_value in target raise ValueError(funsupported op: {op}) def evaluate(record, rules): record normalize(record) for rule in sorted(rules, keylambda r: r.get(priority, 0)): if match_rule(rule, record): return { record_id: record[record_id], action: rule[action], rule_id: rule[id], } return { record_id: record[record_id], action: allow, rule_id: None, }优先级排序是这里容易被忽视的点。规则列表在配置文件里的顺序不代表执行顺序所以我显式依赖priority字段排序。这样后续运营同学调整规则时不需要关心 JSON 数组里的位置只要改数字就行。4.3 持久化与查询接口判定结果需要落地否则每次重启就丢工具就失去了“留痕”的价值。我选择用轻量嵌入式数据库存储建一张简单的表字段包括记录 ID、作用域、金额、判定动作、命中的规则 ID、判定时间。表结构大致如下CREATE TABLE IF NOT EXISTS evaluations ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_id TEXT UNIQUE, scope TEXT, amount REAL, action TEXT, rule_id TEXT, evaluated_at TEXT );写入时使用INSERT OR IGNORE保证同一记录不会被重复入库。这一步直接满足了验收条件里的“不重复写入”。查询接口我没有选择引入完整的 Web 框架而是用 Python 标准库自带的 HTTP 服务实现了一个很小的接口提供POST /evaluate用来批量判定提供GET /records用来查询最近结果。对内部工具来说功能完全够用也没有额外依赖。4.4 联调测试手工验证那五条验收项模块拼接完成后我写了一个批处理入口可以直接读取本地 JSON 文件并执行判定。接着造了一批测试数据覆盖“超阈值记录”“普通记录”“缺字段记录”“重复记录”四种情况。跑出来的结果和我预期一致record_idREC-001 actionreview ruler1 record_idREC-002 actionallow ruleNone record_idREC-003 actionallow ruleNone record_idREC-003 actionallow ruleNone # 已通过 INSERT OR IGNORE 去重拿到这个输出时第一版其实已经能用了。但真正的挑战是后续压力测试阶段那几个翻车问题才是最有价值的经验。5. 测试与调优中发现的三个“翻车位”5.1 状态穿透导致的“假成功”第一个问题藏得很深当时批量跑测试数据连续通过了三轮我以为大功告成。结果在第四轮时我把某条记录的amount从 500 改到 1500期望它触发review输出却仍然是allow。排查后发现问题出在模块级变量我把一个“最近一次结果”缓存放在了模块顶层后续判定时会先检查这个缓存如果再命中就直接返回缓存结果。前面三批测试数据恰好顺序稳定掩盖了这个问题一旦数据顺序变化缓存的旧结果就会污染新结果。解决方式也很简单移除所有模块级可变状态把每一次判定都包装成独立调用。这个教训后来变成了团队里的口头禅——内部工具可以没有复杂架构但不能没有状态隔离。5.2 并发写库时的锁等待第二阶段我把入口改成了 HTTP 服务开始模拟并发请求。结果压力稍微上来一点日志里就开始出现数据库锁错误。根因是多个请求线程同时写入同一个本地数据库文件默认的日志模式下容易出现写锁互斥。解决思路有两步一是把数据库的日志模式切换为 WAL 模式让读写并发能力强很多二是设置一个合理的忙等超时时间让并发写入在锁竞争时排队重试而不是立即失败。相关配置很简单PRAGMA journal_modeWAL; PRAGMA busy_timeout3000;这两行设置之后并发写入立刻稳定下来。这个坑在单线程模型下永远不会暴露但只要上到并发场景就会卡脖子建议大家一开始就把这两行配置写好。5.3 错误被日志噪音淹没最后一个问题不是功能性的而是可观测性。早期代码里 logging 风格比较随意错误信息、调试信息和普通运行信息混在一起级别也没有严格区分。有一次线上结果异常我翻日志翻了几屏才发现真正的报错被埋在一堆DEBUG输出里。后来我强制规定业务错误必须使用结构化的错误码而不是只靠文案描述。错误码表如下错误码触发场景处理建议REA-1001输入字段缺失补默认值后继续记录 warningREA-1002不支持的比较操作符拒绝该条记录不影响整批任务REA-1003数据库写入锁超时退避重试或转存待处理文件REA-1004规则文件为空或不可解析立即停止任务避免误判有了错误码之后监控脚本终于可以直接按码聚合统计我能一眼看出今天到底哪类问题最多而不是对着几百条日志靠肉眼猜。6. 部署与持续运维从“能跑”到“能一直跑”6.1 最小部署方案rea的基础设施需求非常低一台普通单机就够。我最终提供了两种运行模式批处理模式和常驻 HTTP 模式。批处理模式适合定时任务比如每五分钟处理一次积压记录常驻模式适合在线查询和少量实时判定。批处理我写了一个很薄的 shell 脚本加上一个简单的运行锁保证同一时间只跑一个实例#!/usr/bin/env bash cd /opt/rea flock -n /tmp/rea.lock -c python3 run.py --batch --config config/rule.json logs/rea.log 21配合系统定时任务每五分钟调用一次即可。这个方案看起来朴素但对内部工具来说稳定性和可解释性远胜于花哨的编排系统。6.2 巡检与监控建议上线之后我给项目加了一个最朴素的巡检脚本每天统计处理量、异常率、命中率最高的三条规则。SQL 很简单却非常有价值SELECT action, COUNT(*) FROM evaluations WHERE evaluated_at datetime(now, -1 day) GROUP BY action;如果review或block的比例突然大幅上升说明业务数据或规则配置可能出了问题需要人工介入。再配一个最简单的阈值告警比如异常率超过百分之五就发一条内部消息提醒整个巡检闭环就完成了。6.3 后续可扩展的方向rea第一版刻意做得很克制但它的扩展点我一直在心里留着。如果未来规则种类变多可以把判定逻辑从内置函数改成可插拔的处理脚本如果查询量变大可以把本地轻量数据库换成集中式数据库如果规则需要运营同学自助修改可以加一个配置文件在线编辑和版本回滚功能。这些扩展方向不需要现在实现但当时我在代码里预留了清晰的分层接口将来无论加哪种能力都不需要推翻重写。扩展的关键不是“预留很多抽象”而是“每层只做一件事”。6.4 我最后想说的经验接手这类只带代号、没有文档的工程技术难度往往不高真正考验人的是对模糊信息的处理方式。我后来把这份推理过程写进了项目的决策文档记录了“为什么选 JSON 不选脚本”“为什么判定函数要保持无状态”“为什么结果一定要落库”这几个关键判断。三个月后回看今天记录的这些“为什么”比任何注释都有价值。我自己处理“rea”时最大的一个体会是别急着否定一个模糊的起点也别急着给它堆满架构。先跑起来再问自己三个实际的问题——它到底在解决谁的什么痛点、第一版最少的闭环是什么、哪些部分将来最可能变。想清楚这三点剩下的事情都会顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑