上个月某个下午,我收到一条消息:“帮我看下 rea 这个项目,尽快理个方案。”没有附件,没有需求文档,连代码仓库都是空的。我盯着那三个字母看了很久,rea、REA、r.e.a,脑子里闪过无数种可能:是读取?是真实?还是某个产品代号?我给对方回了个“收到”,然后足足花了十分钟坐在那里发愣。
这篇文章就还原这个项目从“只有一个名字”到“跑通完整MVP”的全过程。我会讲清楚我怎么处理这种信息几乎为零的需求,如何把一个空项目名拆出合理的目标,怎么选型、怎么写代码、怎么测试,以及最后发现了哪些坑。如果你也遇到过“项目就一个代号,文档全没写”的情况,或者你想知道如何从模糊想法里快速做出可交付的东西,那这篇内容应该能给你一些可复用的方法。
1. 项目代号“rea”背后的真实状态:一个只有名字的空需求
1.1 收到“rea”时的第一反应
先还原一下当时的场景。团队里一位负责业务对接的同事发来一条简短消息,内容大概是:“后面这个项目统一叫 rea,你按这个来。”我这边的任务栏里多了一个条目,但是没有要求文档、没有原型图、没有接口约定,甚至没有一句话解释“rea”是干什么的。
我做的第一件事是搜索内部仓库,结果只看到一个刚初始化的空仓库,README 里没有任何内容。我又翻了和这位同事之前的聊天记录,发现上一周的沟通里只提过一次“想做一个实时分析的小模块”,但当时大家只是闲聊,没有落成正式文档。现在名字突然变成了 rea,这个缩写到底对应什么,谁也没有给过定义。
说实话,这种时候很容易陷入两种极端:一种是一直等更多信息,觉得“反正文档没给我,我也可以不着急”;另一种是直接按自己的想象开干,随后做出来的东西和真实需求完全对不上。我两种都吃过亏,所以这次决定换一个思路:先把“rea”当成一个开放命题,用最短的时间形成自己的假设,再拿着假设去和对方确认。
1.2 为什么先别急着追问“这是什么”
拿“rea”这个案例来说,如果我直接问“rea 是什么?”,对方的回答大概率只有三个字:“我也不太清楚。”这不是对方不想配合,而是需求方往往默认执行者能从上下文里自悟出来。尤其当项目名是缩写时,提问方自己可能也只是临时起了个代号,他真正想要的不是让你帮他解释名字,而是让你把事情理清楚。
我的做法是反向推动:先基于有限的线索给出一个可讨论的草案。相当于把选择题递给对方,而不是把填空题甩回去。比如我列了几个候选含义,做成一个表,让对方勾选或修改,这样沟通效率高很多,也容易暴露真实业务意图。
更重要的是,空项目名不等于空需求。只要有人发起这个项目,背后就一定存在某个具体痛点或目标。名字只是入口,真正要挖的是入口里面的场景。所以“先别急着追问”不是不沟通,而是要先建立假设模型,再用一次有准备的沟通来验证。
基于那次闲聊里的“实时分析小模块”,我初步假设 rea 可能是某种“实时事件分析系统”。带着这个假设去聊,对方只要回答“对”或“不对”,需求边界就能立刻收敛一半。如果当初只是干巴巴地问“rea是什么”,大概率绕半天又回到原点。
2. 从三个字母到明确需求:缩写推断与目标访谈
2.1 三步法拆解“rea”的含义
面对一个只有代码名的项目,我后来总结出了一个实用三步法:收集线索、枚举可能性、结合业务背景筛选。这套方法在 rea 这个案例上效果很好。
第一步是收集线索。我不只是盯住“rea”这三个字母,而是把过去一个月团队里相关的讨论、业务痛点、技术基建都翻了一遍。最后得到几个关键信息:团队正在做事件驱动架构改造,很多业务系统会推送状态变化事件;当前缺乏一个轻量的规则判断层,业务人员想实现“当某类事件发生时自动告警”;之前有人提过要做一个“事件接收和分发服务”。
第二步是枚举 rea 可能对应的全称。我当时列了一个表:
| 候选全称 | 业务含义 | 匹配度 |
|---|---|---|
| Real-time Event Analyzer | 实时事件分析器 | 高,与事件驱动架构强相关 |
| Resource Expense Aggregator | 资源费用聚合器 | 中,与成本核算相关,但近期没有这类需求 |
| Report and Analytics API | 报表与统计API | 低,未出现在团队讨论中 |
| Read Eval Act | 读取-评估-执行循环 | 低,更偏向技术方法论,不像业务项目 |
第三部是结合业务背景筛选。团队正在做事件流处理,但是消息中间件和实时计算平台一直没确定,很多运营同事抱怨“出了问题总是事后才知道”。一个轻量、能用 HTTP 接口接收事件,然后按规则做判断并触发告警的服务,恰好解决了当前的矛盾。所以我把“Real-time Event Analyzer”定为最可能解释,并以此为核心与需求方确认。
需要说明的是,这三步并不是要做一次严谨的学术考证。我的目标是找到一个“最值得继续验证的假设”,而不是证明某个缩写是唯一正确答案。哪怕最后全称变了,只要核心能力判断准确,后面动手做原型的成本就值得投入。
2.2 和需求方对齐后的MVP边界
带着“事件接收+规则匹配+告警通知”这个初步理解,我和需求方代表开了一次短会。会后我们明确了 rea 的 MVP 范围,我用一张表把它固定下来,防止后面越做越偏。
| 包含(MVP) | 不包含(后续再说) |
|---|---|
| 通过 HTTP/JSON 接收业务事件 | 用户权限和登录体系 |
| 规则管理:新增、编辑、删除、禁用 | 可视化拖拽编排规则 |
| 规则引擎:字段比较和逻辑组合 | 机器学习异常检测 |
| 告警输出:Webhook 通知和日志记录 | 复杂报表与图表展示 |
| 事件去重和通知重试 | 海量数据分布式存储 |
这个表看起来简单,但它是整个项目后续推进的“锚”。一方面,我明确知道了“rea”最核心的价值是“让业务规则自动响应事件”,而不是做一个大数据平台;另一方面,我也能挡掉一些明显超出 MVP 的需求,比如有人提出要前端可视化,我直接把这个划到第二阶段。
为什么要做这个边界?因为信息缺失的项目最容易失控。没有文档,每个人都会按自己的想法往里加东西。如果不提前把边界说死,你会陷入无止境的需求蔓延。定边界这件事,甚至比选技术栈还要重要。
2.3 技术选型:为什么是FastAPI+SQLite
范围确定之后,技术选型就变得非常顺利。我当时选了 Python 的 FastAPI 作为 Web 框架,配合标准库自带的 SQLite 作为存储,没有引入额外消息队列,也没有上重型数据库。这个选择看起来比较常规,但对 rea 这类 MVP 来说非常合适。
FastAPI 的理由有三个:一是异步支持好,接收事件接口本身就是 IO 密集场景,异步模型天然合适;二是 Pydantic 模型绑定请求体校验,可以直接把异常事件挡在外层,省掉手写一堆校验逻辑;三是它自带 OpenAPI 文档,接口定义能变成可交互的调试页面,这对没有文档的项目来说等于补了一份“活文档”。
SQLite 的理由更直接:MVP 阶段我们不需要多节点并发写,也不需要复杂权限管理,SQLite 零配置、单文件、备份容易,非常适合先跑通主流程。等用户量或事件量真的大到 SQLite 撑不住,再平滑迁移到 PostgreSQL 也不迟。另外我还考虑过直接用 Redis 做事件存储,但 Redis 的持久化策略在规则管理和历史事件查询上不够自然,最后还是选择了 SQLite。
对了,为什么不上消息队列?因为 rea 在第一版只需要同步接收事件并同步触发规则,没有削峰填谷的硬需求。用消息队列会增加运维成本,也会让整个调试链路变复杂。在实际项目中我一直信奉一句话:MVP 阶段能用一个进程跑完的,就别拆成三个服务。
3. 把“rea”变成能跑的系统:核心模块与实现细节
3.1 项目结构与依赖
技术选型确定后,我开始搭项目骨架。这个项目整体结构非常清晰,文件不多,但是每个模块都只干一件事。
rea/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口,路由注册 │ ├── models.py # Pydantic 数据模型 │ ├── rules.py # 规则引擎核心逻辑 │ ├── storage.py # SQLite 读写封装 │ └── notifier.py # 通知发送与重试 ├── tests/ │ └── test_flow.py # 模拟数据测试 ├── requirements.txt # 依赖清单 └── README.md # 项目说明requirements.txt 只有几个核心依赖:
fastapi==0.104.1 uvicorn[standard]==0.24.0 pydantic==2.5.2SQLite 用的是 Python 标准库 sqlite3,所以不需要额外安装。我故意不引入 ORM,原因是 MVP 阶段最需要的是轻量可控,SQLite 的几个表直接用原生 SQL 写反而更直观。
3.2 事件接入层:HTTP API设计
rea 对外暴露的第一个接口就是POST /api/v1/events。任何业务系统都能把事件以 JSON 形式推送过来,rea 负责把它们规范化并进入规则引擎。这个接口用 Pydantic 定义事件结构,代码大概长这样:
from datetime import datetime from pydantic import BaseModel, Field class EventIn(BaseModel): event_id: str = Field(..., description="事件唯一ID") event_type: str = Field(..., description="事件类型,如 order_failed") occurred_at: datetime = Field(..., description="事件发生时间") source: str = Field("unknown", description="来源系统") payload: dict = Field(default_factory=dict, description="业务字段")event_id 是去重的关键,event_type 是规则引擎最常用的条件,payload 里放业务数据的 KV。把事件统一成这个结构,后面的规则匹配就不用关心每个系统字段命名差异,只在接入层做一次适配。
FastAPI 路由处理函数也简单:
from fastapi import FastAPI, HTTPException from models import EventIn app = FastAPI(title="rea") @app.post("/api/v1/events") def receive_event(event: EventIn): # 先检查是否重复 if storage.event_exists(event.event_id): return {"status": "duplicate", "event_id": event.event_id} # 写入事件表 storage.save_event(event) # 进入规则引擎 results = rules.match_all(event) # 触发通知 for r in results: notifier.dispatch(event, r) return {"status": "ok", "event_id": event.event_id, "matched_rules": len(results)}这段代码很短,但已经覆盖了完整主链路:接入、去重、入库、匹配、通知。我在生产环境里一直强调,入口层要做两件事:统一结构和第一道校验。事件本身来自多个系统,如果不在这里把脏数据挡住,规则引擎再聪明也会被垃圾输入带偏。
3.3 规则引擎:让“rea”真正有分析能力
规则引擎是整个项目里最有意思的部分。我把它设计成“JSON 规则”而不是硬编码判断,这样业务人员以后可以通过 API 维护规则,不需要改代码。
规则模型大概是这样的:
class RuleCondition(BaseModel): field: str # 要匹配的字段,如 payload.amount operator: str # eq, ne, gt, ge, lt, le, contains value: object # 期望值 class Rule(BaseModel): name: str enabled: bool = True conditions: list[RuleCondition] action: dict # 通知配置,如 {"type": "webhook", "url": "..."}匹配逻辑先取出字段值,再做类型统一和操作符判断:
def evaluate_condition(cond: RuleCondition, event: EventIn) -> bool: # 支持 payload.a.b.c 这类路径取字段 value = get_field_value(event, cond.field) expected = cond.value if cond.operator == "gte": return to_number(value) >= to_number(expected) if cond.operator == "contains": return expected in value if cond.operator == "eq": return value == expected ... return False为什么每次比较前都要做一次 to_number?因为不同事件源传来的数字格式时不时不同,有的传字符串"1000",有的传整数1000。如果直接比较,规则会失效。这个细节在实际联调时坑了我一次,后面专门展开。
一条完整规则的 JSON 长下面这样:
{ "name": "大额订单失败告警", "enabled": true, "conditions": [ {"field": "event_type", "operator": "eq", "value": "order_failed"}, {"field": "payload.amount", "operator": "gte", "value": 1000} ], "action": { "type": "webhook", "url": "https://your-domain.com/ops/webhook" } }规则的全部 conditions 之间用 AND 逻辑连接,已经能满足 80% 的 MVP 场景。如果需要 OR 逻辑,我可以把 conditions 改为“分组嵌套”,但第一版没做,降低复杂度。
3.4 通知输出层:结果怎么用起来
规则匹配成功之后,rea 要产生对外动作。第一版实现了两类通知:写日志和 Webhook 推送。Webhook 的配置存在规则 action 里,通知层只解析执行,不关心业务语义。
这里有一个我认为特别重要的设计:通知不能直接抛给外部就完事,需要“可靠投递”。实现方式是先把待发送通知写到本地表,再由后台线程定期轮询发送并更新状态。代码有点像这样:
def dispatch(event: EventIn, rule: Rule): notification_id = uuid4().hex storage.save_notification({ "id": notification_id, "event_id": event.event_id, "rule_name": rule.name, "payload": event.json(), "status": "pending" }) def send_pending_notifications(): pending = storage.find_pending_notifications() for notif in pending: try: requests.post(rule.action["url"], json=notif["payload"], timeout=5) storage.update_status(notif["id"], "sent") except Exception: retry_count = storage.increase_retry(notif["id"]) if retry_count >= 3: storage.update_status(notif["id"], "failed")这样做的好处很明显:通知不会因为外部服务偶发抖动而永久丢失。如果调用方挂了,rea 会重试三次;如果三次还不行,至少落一条可查的失败记录,后面排查问题有据可依。很多类似项目忽略了这个点,结果规则明明匹配到了,告警却悄悄没了,运维同学完全无感知。
4. 在信息缺失状态下验证功能:测试、模拟与问题排查
4.1 用模拟事件跑通主链路
由于业务系统还没接入 rea,我选择先写一个模拟脚本,造几类事件直接打过来,验证整个链路是否闭环。这比直接在真实环境里试错要安全得多,也能快速暴露设计问题。
模拟脚本发三批事件:一批是 order_failed 且金额大于 1000,预期触发告警;一批是 order_failed 但金额很小,预期不触发;一批是 login_anomaly 事件,不匹配任何规则,预期没有通知。
用 curl 模拟也很方便:
curl -X POST http://localhost:8000/api/v1/events \ -H "Content-Type: application/json" \ -d '{ "event_id": "evt_001", "event_type": "order_failed", "occurred_at": "2024-11-01T10:30:00Z", "source": "order-service", "payload": {"amount": 5000} }'当时测试结果是这样的:
| 事件 | 金额 | 规则要求 | 预期 | 实际 |
|---|---|---|---|---|
| evt_001 | 5000 | >1000 | 触发 | 触发 |
| evt_002 | 599 | >1000 | 不触发 | 不触发 |
| evt_003 | 20000 | >1000 | 触发 | 不触发 |
第三个事件格外刺眼:明明金额是 20000,规则却显示不触发。于是我开始排查。
4.2 三个最容易踩的坑与排查过程
第一个坑是字段类型不一致。我打出日志发现 evt_003 的 payload 里,金额字段叫amount没错,但值是字符串"20000",而规则条件里写的是整数1000。程序用"20000" >= 1000做比较,Python 直接抛 TypeError,被异常处理吞掉后返回了 False。加上我代码里没有打印异常,所以表现成“规则未触发”。
解决方法是写统一的类型归一化函数,在比较前先把两边都转成同一类型,并在异常时打印详细上下文。这个坑最典型的教训是:接外部系统数据时,永远不要假设字段类型会按文档来。
第二个坑是 SQLite 并发写入报 “database is locked”。FastAPI 的异步模型下,多个请求同时写 SQLite,偶尔会触发数据库锁。尤其是我在send_pending_notifications里又开了后台线程写库,竞争变得更明显。一开始我把所有写操作都启用check_same_thread=False,单机测试没问题,但并发一上来就偶发报错。
这个坑的解法有两步:先给 SQLite 连接设置timeout=30,同时把数据库写操作统一提交到一个线程中串行执行,避免多线程同时写。如果后续并发量进一步变大,我会考虑把 SQLite 换成 PostgreSQL 或使用独立队列。
第三个坑是重复通知。测试时为了验证重试逻辑,我用同一个 event_id 发了两次事件。结果 rea 在入口层已经判断重复并返回duplicate,但通知层还是重复发送了两次 Webhook。原因是重复判断发生在主流程开头,但通知发送是在匹配之后单独处理,没有再次消费“事件是否已处理”的状态。我在事件表上加了 event_id 唯一索引,并让同一个 event_id 在同一时间段内只能触发一次通知,才彻底解决了这个问题。
这三个坑不一定每个项目都会遇到,但类型不一致、数据库锁、重复通知这三类问题,在事件型服务里非常常见。我把完整排查链路写下来,就是想告诉大家:这类问题不是玄学,而是设计边界没划清楚导致的。
5. 项目收尾后的复盘:空项目名变成产品的一些方法
5.1 我总结出的需求澄清清单
rea 这个项目从三字母到跑通,总共花了一个半星期。其中真正写代码只占了一半时间,另外一半都花在“确定做什么”上。做完之后我给自己沉淀了一份“空项目需求澄清清单”,以后不管再遇到多模糊的项目名,我都会按这个顺序过一遍。
第一件事是确定用户和痛点:谁会用 rea,他们现在哪里不舒服?我们得出的答案是运营和运维人员,他们希望事件发生后第一时间被通知,而不是等到客户投诉才发现。
第二件事是定义输入和输出:rea 接收什么数据,输出什么结果?输入是 HTTP 事件,输出是告警消息。这个边界一旦说清楚,所有接口设计都顺理成章。
第三件事是设定完成标准:能做到什么程度算绝对不上线?我们定义成“能接收事件、能按规则触发通知、通知能在日志中追踪”,简单直接。
第四件事是明确不做的事:权限、报表、可视化、机器学习,全部划到后续迭代。把这些写进文档,相当于给未来沟通建了防火墙。
这套清单用起来并不复杂,核心思路就是八个字:先定边界,再想技术。
5.2 “rea”这个名字的教训和后续演进空间
现在回头看,rea 这个项目名最大的教训不是名字取得不好,而是“只有名字、其他全空”。所以后来我再从别人手里接项目时,如果手里只有一个代号,我会第一时间要求需求方补一句话,哪怕只是一句“rea 就是用来做告警的”,也能让团队省掉一整天的猜测。
同时我也意识到,缩写本身是有意义的,它代表团队在起名那一刻最想强调的东西。rea 里藏着 real-time 和 event,这说明核心诉求是时效性。如果把 rea 重命名为“事件规则服务”,其实会少一点识别度。这些是起名时的小乐趣,但放在项目管理里,还是要提醒自己:名字再好,也替代不了需求说明。
关于 rea 的演进,我认为方向可以很清晰:先把规则引擎从 JSON 配置升级成可视化配置;再把事件存储从 SQLite 迁移到更适合时序数据的存储;最后补上统计报表,让运营同学能看到规则命中趋势。但这些都是后话,MVP 已经验证了主链路,后续每一轮迭代都基于真实使用反馈,而不是脑补需求。
最后分享一个个人体会:信息越少的时候,越要主动制造一次“对齐”。空项目名不是黑洞,而是一张白纸,你画一个方案上去,所有模糊的地方都会浮现出来。别等别人给你完整需求,任何需求都是从一点点线索里长出来的。