背景痛点:PRD 像“橡皮筋”,一拉就乱
做智能客服的同学都懂,PRD 三天两头改一句话,开发就像被橡皮筋弹脸:
- 运营在飞书文档里加一句“支持广东话”,后端得重新撸一遍意图树;
- 人工读 40 页 PDF,把“用户说查账单”归类到 QueryBill 还是 QueryOrder,全凭眼神;
- 需求变更邮件发来,测试用例还没写完,代码已经合并了两版。
结果就是排期翻倍,30% 的人力都耗在“翻译”PRD 上。我们组去年 Q4 统计,平均每个需求从 PRD 到可开发 User Story 要 3.2 人日,老板直接拍桌子:必须砍到 1 人日以内。
技术选型:BERT 还是 Rasa NLU?
目标一句话:让机器读 PRD,吐出“机器能跑”的结构化数据。候选方案就俩:
自训 BERT+CRF 做实体识别,意图分类再挂一层全连接。
- 优点:领域词汇可控,F1 能刷到 92% 以上;
- 缺点:GPU 推理 150 ms,并发高时钱包疼。
Rasa NLU + DIETClassifier,轻量 CPU 跑。
- 优点:docker 镜像 400 MB,单机 200 QPS 无压力;
- 缺点:中文歧义(“转人工”到底转客服还是转账)需要喂大量语料。
钱包和 latency 双重压力下,我们选了“混合”:新领域先用 Rasa 冷启动,标注数据 >5 k 后自动切 BERT。部署链用 GitLab-CI + Helm 一键滚到 K8s,PRD 变更 → MR → 模型热更新 5 分钟内完成,实测开发周期直接砍 35%。
核心实现:PRD 结构化解析
下面这段 120 行 Python 是整条流水线的 MVP,跑通“飞书文档 → JSON → Swagger”。代码按 PEP8 缩进 4 空格,可直接贴进仓库。
# prd_parser.py import re from typing import List, Dict from rasa.nlu.model import Interpreter from pydantic import BaseModel # 1. 定义输出 schema,直接喂给 Swagger class IntentSlot(BaseModel): intent: str slots: Dict[str, str] class PRDOutput(BaseModel): intents: List[IntentSlot] api_list: List[str] class PRDParser: """ 将飞书导出的 html/Markdown 转成结构化意图+槽位 """ def __init__(self, model_dir: str): # 加载 Rasa 模型(CPU 版) self.interpreter = Interpreter.load(model_dir) @staticmethod def _clean_text(raw: str) -> str: """去掉飞书自带的时间戳、@人""" raw = re.sub(r"@\w+\s*", "", raw) raw = re.sub(r"\d{4}/\d{2}/\d{2}.*?\n", "", raw) return raw.strip() def parse(self, prd_raw: str) -> PRDOutput: prd_raw = self._clean_text(prd_raw) lines = [ln for ln in prd_raw.split("\n") if ln] intents, apis = [], [] for sent in lines: # 只处理带“用户说”的示例句 if "用户说" not in sent: continue res = self.interpreter.parse(sent) intent = res['intent']['name'] slots = {e['entity']: e['value'] for e in res['entities']} intents.append(IntentSlot(intent=intent, slots=slots)) # 根据意图名映射 RESTful 模板 api_template = self._intent_to_api(intent) apis.append(api_template) return PRDOutput(intents=intents, api_list=sorted(set(apis))) @staticmethod def _intent_to_api(intent: str) -> str: """简单硬编码,可换成更灵活的 Jinja2 模板""" mapping = { "query_bill": "GET /v1/bills?user={user_id}&month={month}", "transfer_human": "POST /v1/escalation {user_id, session_id}", } return mapping.get(intent, f"POST /v1/{intent}") # 本地调一段 PRD 看看 if __name__ == "__main__": parser = PRDParser(model_dir="models/nlu-20240605") with open("sample_prd.md", encoding="utf8") as f: out = parser.parse(f.read()) print(out.json(ensure_ascii=False, indent=2))跑完你会得到:
{ "intents": [ { "intent": "query_bill", "slots": {"month": "6 月", "user_id": "U1234"} } ], "api_list": ["GET /v1/bills?user={user_id}&month={month}"] }下一步把api_list喂给swagger-codegen,REST 文档和客户端 SDK 就自动生成了,前端小姐姐再也不用追着问字段含义。
架构设计:微服务化一张图
文字版“架构图”如下,方便复制粘贴到 PPT:
- Gateway:Kong,统一鉴权、限流;
- PRD-Parser 服务:上文代码容器化,单 pod 限制 0.5 核 1 G,横向扩容;
- Intent-Store:MongoDB,存解析后的意图模板,支持版本回滚;
- Model-Switcher:根据标注数据量自动切 Rasa ↔ BERT,灰度发布;
- Code-Gen 服务:swagger-codegen + 自定义模板,输出 Python/Java SDK;
- CI-Runner:GitLab-CI,推送新 PRD → 训练 → 镜像 → Helm 升级。
整个链路无状态,PRD 变更后 5 分钟之内新模型上线,开发同学只需 review 自动生成的 MR,真正“写完需求即上线”。
性能优化:并发场景的资源分配
618 大促前压测,单节点 200 QPS 时 CPU 飙到 85%,我们做了三件事:
- 把解析任务拆成异步:Gateway 收到请求先落 Kafka,返回 202,前端轮询结果,平均 RT 从 900 ms 降到 220 ms;
- 给 BERT 开 ONNXRuntime+量化,INT8 后 latency 150 ms→70 ms,GPU 显存占用减半;
- 用 K8s HPA 基于 CPU+队列长度双指标扩容,峰值 1.2 k QPS 时只用到 8 个 pod,成本持平。
避坑指南:生产环境三连击
- 中文歧义:同样是“转人工”,金融场景指 escalation,电商可能是“转售后”。一定在槽例里加领域前缀,如
transfer_human_banking,否则意图混淆率 15% 起步。 - 需求回溯:运营改一句话,开发说“没改多少”,结果测试用例全废。Intent-Store 里每份意图带 git-sha,自动回滚到任意版本,锅就能精准定位。
- 标注数据泄露:把用户真实语料直接喂模型,GDPR 等着罚。用正则+脱敏器先把手机号、卡号替换成
<MASK>,再进训练 pipeline。
互动思考
如何平衡 PRD 解析准确率与系统响应速度?
欢迎在评论区聊聊你的做法:是接受 5% 的误差换 50% 的 latency 下降,还是坚持 95%+ 准确率哪怕多堆十张 GPU?也许下个大促案例就来自你的经验。