1. 从"人写文档"到"代码吐文档"的转变逻辑
1.1 为什么 PRD 总是和代码对不上
做过几年业务系统的人都清楚一个尴尬的现实:需求文档和代码库是两条平行线。产品经理在 PRD 里写"订单满 200 元包邮",开发在代码里写if (orderAmount >= 200) shippingFee = 0,测试再根据 PRD 写用例。三份东西,三个地方,谁也不知道谁过期了。
我待过的团队里,PRD 的平均"保鲜期"大概只有两周。两周之后,代码改了、规则调了、边界条件补了,但 PRD 还停留在上个版本。等到新人接手或者做合规审计的时候,翻出来的文档基本等于废纸。更麻烦的是,很多业务规则压根就没写进 PRD——它们藏在代码的if-else里、藏在配置表的某个字段里、藏在某个老员工脑子里。
这个项目的出发点就是解决这个错位:让代码库本身成为 PRD 的唯一事实来源,用两个 Skill 把散落在代码里的业务规则自动抽取、结构化、生成可追溯的文档。核心关键词codebase-graph-prd-rules说的就是这个思路——先构建代码库的图谱,再从图谱里把规则"捞"出来,最后渲染成 PRD。
1.2 两个 Skill 的分工:一个建图,一个写文档
整个方案拆成两个 Skill,这个拆分不是拍脑袋定的,而是基于"关注点分离"的工程直觉。
第一个 Skill 负责代码库图谱构建。它扫描指定目录,解析 AST(抽象语法树),识别出条件分支、常量比较、配置读取、状态机转换这些"规则载体",把它们抽成结构化的节点和边,存进一个轻量图数据库。这个 Skill 的输出是一份机器可读的规则图谱,不涉及任何自然语言生成。
第二个 Skill 负责PRD 渲染与追溯。它读取图谱,按业务域聚类规则,用模板 + 轻量 LLM 润色的方式生成人类可读的 PRD 章节,同时给每条规则打上"代码位置"的锚点。这样 PRD 里每句话都能点回到具体的文件和行号,实现真正的可追溯。
为什么不用一个大 Skill 全干完?因为图谱构建是确定性的、可测试的、跑一次能复用;而 PRD 渲染是带主观判断的、需要反复调模板的。混在一起会导致每次改文档格式都要重跑全量解析,效率极低。分开之后,图谱可以增量更新,文档可以随时重生成。
1.3 适合谁来用这套东西
这套方案不是给所有人准备的。如果你做的是纯前端展示页面、或者业务规则简单到只有三五个if,那手写 PRD 更快。它真正适合的场景是:
- 中大型业务系统,规则数量在几百条以上,人工梳理成本高
- 合规敏感领域,需要文档和代码严格对应,能审计
- 人员流动频繁的团队,新人需要快速理解业务规则全貌
- 已有一定代码规范基础的项目,AST 解析才有意义
技术栈上,核心用 Python 实现,因为 Python 的ast模块和tree-sitter生态对多语言解析支持好,写起来也快。如果你团队是 Java 或 Go 为主,思路一样,只是解析层要换工具。
2. 核心细节解析:规则到底藏在代码的哪里
2.1 业务规则的四种典型载体
在动手写解析器之前,得先搞清楚"业务规则"在代码里长什么样。我梳理了实际项目中最常见的四类载体,这决定了图谱要抽哪些节点。
第一类是条件分支。if、switch、三元表达式里藏着大量的阈值判断和业务逻辑。比如if user.level == 'VIP' and order.amount > 500,这就是一条完整的会员折扣规则。解析时要提取条件表达式、涉及的变量、比较运算符和常量值。
第二类是常量与配置。很多规则不是硬编码在逻辑里,而是通过常量或配置注入的。比如MAX_RETRY = 3、FREE_SHIPPING_THRESHOLD = 200。这些常量单独看没意义,但和引用它的分支结合起来就是规则。图谱里要把常量和引用点连起来。
第三类是状态机转换。订单状态、审批流、工单流转这类业务,规则体现在状态之间的迁移条件上。比如"订单从待支付到已支付,条件是支付回调成功且金额匹配"。这类规则要从状态枚举和迁移函数里抽。
第四类是校验逻辑。表单校验、参数校验、权限校验里往往包含业务约束。比如"手机号必须是 11 位数字"、"转账金额不能超过日限额"。这些校验函数是规则的密集区。
提示:不要试图抽取所有代码逻辑。只抽那些"业务人员会关心"的规则。判断标准很简单——如果这条逻辑变了,产品经理需不需要知道?需要,就抽;不需要,就跳过。
2.2 图谱的数据结构设计
图谱的节点和边设计直接决定了后续 PRD 的质量。我用的结构是这样的:
| 节点类型 | 含义 | 关键属性 |
|---|---|---|
| Rule | 一条业务规则 | rule_id, condition, action, domain |
| Constant | 常量/配置项 | name, value, type |
| CodeLocation | 代码位置 | file_path, line_start, line_end |
| Domain | 业务域 | name, description |
| Entity | 业务实体 | name, fields |
边的关系主要有:Rule -[references]-> Constant、Rule -[located_at]-> CodeLocation、Rule -[belongs_to]-> Domain、Rule -[operates_on]-> Entity。
这个结构的好处是,生成 PRD 时可以按 Domain 聚类,按 Entity 组织章节,每条规则都能追溯到代码位置。用 NetworkX 或者直接存 JSON 都行,规则量在几千条以内,不需要上 Neo4j 这种重型图数据库。
2.3 解析器的技术选型考量
解析层我最终选了tree-sitter而不是 Python 自带的ast。原因很实际:ast只能解析 Python,而真实项目往往是多语言的——后端 Python、前端 TypeScript、配置 YAML。tree-sitter有各语言的语法定义,一套接口通吃。
代价是tree-sitter的 API 比ast底层,需要自己遍历语法树节点。但换来的是跨语言能力和更好的容错性——代码有语法错误时tree-sitter还能部分解析,ast直接抛异常。
安装上,tree-sitter和对应语言包通过 pip 装:
pip install tree-sitter tree-sitter-python tree-sitter-typescript tree-sitter-javascript如果你只想快速验证思路,先用ast跑通 Python 部分也完全够用,后面再换tree-sitter扩展。
3. 实操过程:从零搭起两个 Skill
3.1 环境准备与项目骨架
先把项目结构定下来。两个 Skill 分别放在独立目录,共享一个图谱存储层:
prd-from-code/ ├── skills/ │ ├── graph_builder/ │ │ ├── __init__.py │ │ ├── parser.py │ │ ├── extractor.py │ │ └── graph_store.py │ └── prd_renderer/ │ ├── __init__.py │ ├── cluster.py │ ├── template.py │ └── render.py ├── config/ │ └── domains.yaml └── main.pyPython 环境建议 3.10 以上,因为用到了match语法和新的类型标注。装依赖:
pip install tree-sitter tree-sitter-python pyyaml jinja2 networkxjinja2用来做 PRD 模板渲染,networkx做图谱的聚类分析,pyyaml读业务域配置。
3.2 第一个 Skill:代码库图谱构建
先写解析器。核心思路是遍历语法树,找到所有条件分支节点,提取条件表达式和分支体。
import tree_sitter_python as tspython from tree_sitter import Language, Parser PY_LANGUAGE = Language(tspython.language()) parser = Parser(PY_LANGUAGE) def parse_file(file_path): with open(file_path, 'rb') as f: source = f.read() tree = parser.parse(source) return tree, source def walk_tree(node, source, results): if node.type == 'if_statement': condition = node.child_by_field_name('condition') if condition: cond_text = source[condition.start_byte:condition.end_byte].decode() results.append({ 'type': 'condition', 'text': cond_text, 'line': condition.start_point[0] + 1, 'file': file_path }) for child in node.children: walk_tree(child, source, results)这段代码是骨架,实际用的时候要处理elif、else、嵌套分支,还要把条件里的常量识别出来。我踩过的坑是:tree-sitter的节点类型名各语言不一样,Python 是if_statement,JavaScript 是if_statement但字段名不同,TypeScript 又多了类型注解节点。所以解析器要按语言分派。
提取完原始条件后,做一轮归一化。把order.amount > 200这种表达式拆成{left: 'order.amount', op: '>', right: 200}的结构化形式。这一步用正则 + 简单解析就够,不用上完整的表达式解析器。
然后是常量提取。扫描模块级的赋值语句,把全大写的变量名识别为常量:
def extract_constants(tree, source): constants = {} root = tree.root_node for node in root.children: if node.type == 'expression_statement': assign = node.children[0] if assign.type == 'assignment': left = assign.child_by_field_name('left') right = assign.child_by_field_name('right') if left and right and left.type == 'identifier': name = source[left.start_byte:left.end_byte].decode() if name.isupper(): value = source[right.start_byte:right.end_byte].decode() constants[name] = value return constants常量提取出来后,要在条件表达式里做引用替换。比如条件里出现FREE_SHIPPING_THRESHOLD,就把它替换成实际值 200,这样 PRD 里写的是"满 200 元包邮"而不是"满 FREE_SHIPPING_THRESHOLD 包邮"。
图谱存储用 NetworkX 的 DiGraph,序列化成 JSON 落盘:
import networkx as nx import json def save_graph(G, path): data = nx.node_link_data(G) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)3.3 业务域聚类:让规则各归其位
原始规则是一盘散沙,直接生成 PRD 会乱成一锅粥。聚类这一步是把规则按业务域分组,这是 PRD 章节结构的基础。
聚类策略我用的是"配置 + 启发式"混合。先在domains.yaml里定义业务域和关键词:
domains: - name: 订单履约 keywords: [order, shipping, delivery, 订单, 发货] entities: [Order, Shipment] - name: 会员权益 keywords: [member, vip, level, 会员, 等级] entities: [Member, Level] - name: 支付结算 keywords: [payment, refund, 支付, 退款] entities: [Payment, Refund]然后对每条规则,看它涉及的变量名、函数名、文件路径里命中哪个域的关键词,归到对应域。命中多个的按权重打分,取最高分。都没命中的归到"其他规则",人工再处理。
这个启发式方法实测准确率在 80% 左右,剩下的 20% 靠人工微调配置就能收敛。比上聚类算法(KMeans 之类)靠谱得多,因为业务域是人为定义的,不是数据自然聚出来的。
3.4 第二个 Skill:PRD 渲染与追溯锚点
渲染层用 Jinja2 模板。模板结构按业务域组织,每个域下按实体分小节,每条规则一个条目:
{% for domain in domains %} ## {{ domain.name }} {% for entity in domain.entities %} ### {{ entity.name }} {% for rule in entity.rules %} - **规则**:{{ rule.description }} - 条件:{{ rule.condition }} - 动作:{{ rule.action }} - 代码位置:`{{ rule.location.file }}:{{ rule.location.line }}` {% endfor %} {% endfor %} {% endfor %}rule.description是自然语言化的规则描述。这一步用轻量 LLM 做,把结构化的{left, op, right, action}翻译成"当订单金额大于 200 元时,免收运费"这样的句子。Prompt 要约束得紧一点,只做翻译不做发挥,避免 LLM 编造不存在的规则。
追溯锚点是这套方案的核心价值。每条规则在 PRD 里都带文件:行号,读者可以直接跳到代码。更进一步,可以生成一个 HTML 版本,锚点做成超链接,点击直接打开编辑器对应位置。
3.5 增量更新与 CI 集成
图谱构建支持增量:记录每个文件的 hash,只重新解析变更的文件,图谱做局部更新。这样在 CI 里跑的时候,全量构建几分钟,增量构建几秒钟。
CI 集成的方式是在 PR 合并时触发图谱更新,然后重新生成 PRD 并提交到文档仓库。如果 PRD 有变化,自动在 PR 里评论提示"业务规则文档已更新,请产品同学确认"。这样文档永远不会落后于代码。
4. 常见问题与排查技巧实录
4.1 解析准确率上不去的三个原因
实际跑下来,解析准确率上不去通常不是解析器的问题,而是代码本身的问题。我总结了三个高频原因:
原因一:规则藏在动态调用里。比如getattr(config, rule_name)这种动态属性访问,静态解析根本看不到。这类只能靠运行时追踪或者人工标注补充。我的做法是在图谱里标记"疑似动态规则点",提示人工复核。
原因二:魔法数字满天飞。代码里直接写if amount > 200,没有常量名。解析器能抽出来,但不知道 200 是什么含义。解决办法是维护一个"数字语义映射表",把常见阈值和业务含义对应起来,解析时查表补充描述。
原因三:条件表达式过于复杂。一个if里套了五六个and/or,抽出来是一大坨。这时候要做表达式拆分,把复合条件拆成原子条件,PRD 里分条列出。拆分逻辑用递归下降解析器,或者简单点用sympy的布尔表达式处理。
4.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 图谱节点数远少于预期 | 解析器未覆盖某语言 | 检查文件扩展名与语言映射 | 补充对应 tree-sitter 语言包 |
| PRD 里规则描述重复 | 同一规则在多处引用 | 检查图谱去重逻辑 | 按 condition+action 哈希去重 |
| 常量值替换错误 | 常量作用域混淆 | 检查常量提取的模块边界 | 按文件+作用域限定常量 |
| 聚类结果混乱 | 关键词配置不准 | 统计未命中规则的关键词 | 迭代补充 domains.yaml |
| 增量更新后图谱不一致 | 依赖文件未重解析 | 检查文件依赖图 | 变更文件的下游全部重解析 |
| LLM 描述编造规则 | Prompt 约束不足 | 检查生成描述与原始条件 | 加 few-shot 示例,禁止发挥 |
4.3 几个踩过的坑和独家技巧
坑一:别一上来就全量解析。我最初拿一个几十万行的老项目跑全量,解析跑了二十分钟还内存溢出。正确做法是先圈定范围,只解析业务逻辑目录,跳过测试、迁移脚本、第三方库。范围缩小后,解析时间降到几十秒。
坑二:注释是宝藏。很多规则在代码注释里有自然语言描述,比如# 满200包邮,VIP用户满100即可。解析时把注释和相邻的条件分支关联起来,能大幅提升 PRD 的可读性。这个技巧让我的规则描述准确率提升了将近 30%。
技巧一:用 git blame 补充规则来源。每条规则关联到代码位置后,可以查 git blame 拿到最后修改人和时间。PRD 里标注"规则最后更新于 X 时间,由 Y 修改",追溯性直接拉满。
技巧二:给规则打置信度标签。解析器抽出来的规则,按抽取方式打置信度:直接条件分支是高置信,动态调用是低置信。PRD 里低置信规则标注"待人工确认",避免误导。
技巧三:PRD 生成后做一次 diff。每次重新生成 PRD,和上一版做 diff,只展示变化的规则。这样产品同学 review 的时候只看增量,不用通读全文。这个功能在 CI 里特别有用。
5. 规则图谱的扩展玩法
5.1 从 PRD 反向生成测试用例
图谱建起来之后,PRD 只是第一个应用。同样的规则图谱可以直接生成测试用例骨架。每条规则对应一组边界值:条件里的阈值取threshold-1、threshold、threshold+1三个点,自动生成参数化测试。
def generate_test_cases(rule): threshold = rule['right'] cases = [] for delta in [-1, 0, 1]: cases.append({ 'input': {rule['left']: threshold + delta}, 'expected': rule['action'] }) return cases这样测试用例和业务规则同源,规则变了测试自动更新,不会出现"规则改了测试没改"的漏测。
5.2 规则冲突检测
图谱里规则多了之后,冲突是难免的。比如两条规则对同一实体的同一字段给出不同动作,或者条件区间重叠但动作矛盾。用图谱做冲突检测很直接:找operates_on同一实体、condition区间重叠、action不同的规则对,标记为潜在冲突。
这个检测在规则数量上千之后特别有价值,人工根本看不过来。我实测在一个 800 条规则的项目里,检测出 17 处潜在冲突,其中 5 处是真实 bug。
5.3 规则变更影响分析
当某条规则要修改时,图谱能快速算出影响范围:哪些规则引用了同一个常量、哪些规则操作同一个实体、哪些下游规则依赖当前规则的状态。这个影响分析在改需求的时候特别有用,能避免"改一处崩三处"。
实现上就是图上的邻居查询,NetworkX 的neighbors和descendants方法直接搞定。把影响范围渲染成一份简报,附在需求评审材料里,评审效率提升明显。
6. 落地这套方案的真实体会
这套东西我从想法到跑通大概花了两周,其中一半时间在调解析器的边界情况。真正让我觉得值的是上线三个月后的一次合规审计——审计方要求提供所有涉及资金操作的业务规则清单,我从图谱里一条命令导出了 200 多条规则,每条都带代码位置和最后修改记录,审计当天就过了。要是搁以前,光整理这份清单就得一周。
不过也得说清楚,这套方案不是银弹。代码写得越规范,效果越好;如果项目里全是动态调用、反射、字符串拼接的 SQL,解析准确率会掉到 50% 以下,这时候人工梳理反而更快。我的建议是先在规则密集、代码相对规范的模块试点,跑通了再逐步扩大范围。
另外,PRD 生成出来之后一定要有人 review。LLM 润色那一步偶尔会把"大于等于"写成"大于",这种细节错误在业务上可能是致命的。我的做法是让 PRD 里的每条规则都保留原始条件表达式作为"技术附录",人工核对时以原始表达式为准。
最后分享一个配置上的小技巧:domains.yaml里的关键词不要写太宽泛的词,比如"order"会命中所有订单相关代码,导致聚类过粗。用更具体的组合词,比如"order_status"、"order_amount",聚类精度会好很多。这个配置值得花时间打磨,它直接决定了 PRD 的可读性。