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, domainConstant常量/配置项name, value, typeCodeLocation代码位置file_path, line_start, line_endDomain业务域name, descriptionEntity业务实体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. 实操过程从零搭起两个 Skill3.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_statementJavaScript 是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, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)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 第二个 SkillPRD 渲染与追溯锚点渲染层用 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 里规则描述重复同一规则在多处引用检查图谱去重逻辑按 conditionaction 哈希去重常量值替换错误常量作用域混淆检查常量提取的模块边界按文件作用域限定常量聚类结果混乱关键词配置不准统计未命中规则的关键词迭代补充 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、threshold1三个点自动生成参数化测试。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 的可读性。