1. 从业务规则到可追溯文档:这个项目到底在解决什么问题
做过需求评审的人大概都有过这种体验:产品经理丢过来一份几十页的 PRD,开发看完说“这跟上次说的不一样”,测试看完说“验收标准在哪”,等真正开始写代码,发现文档里描述的业务规则和代码里实际跑的逻辑早就分家了。更麻烦的是,过几个月回头查某个字段为什么这么算,翻遍文档也找不到依据,只能去问当初写这块代码的人——而那个人可能已经换了项目。
这个项目的出发点很直接:让代码本身成为 PRD 的源头,而不是让 PRD 和代码各说各话。核心思路是用两个 Skill 把散落在代码里的业务规则抽取出来,自动生成一份可追溯的 PRD 文档。所谓“可追溯”,指的是文档里每一条业务规则都能对应到具体的代码位置,反过来,代码里每一处业务逻辑的变更也能反映到文档上。
适合谁来参考这个方案?如果你正在做 AI Agent 相关的开发,尤其是需要处理大量业务规则沉淀的场景,这套思路可以直接借鉴。如果你只是对“代码图谱”和“Skill 编排”这两个概念感兴趣,想看看它们怎么落地到一个具体问题里,这篇文章也会把每个环节拆开讲清楚。前置知识方面,需要你对 Python 有基本了解,知道什么是函数、类、模块,剩下的部分我会尽量用生活化的方式解释。
为什么选“两个 Skill”而不是一个大而全的 Agent?这是我在实际搭建过程中踩过坑之后做的取舍。单个 Agent 既要理解代码结构、又要抽取业务规则、还要生成格式化文档,任务耦合度太高,调试的时候根本不知道是哪一步出了问题。拆成两个 Skill 之后,一个专门负责“读懂代码”,另一个专门负责“写出文档”,职责边界清晰,出了问题也容易定位。这个设计决策后面会详细展开。
2. 整体架构设计:为什么是两个 Skill 而不是一个 Agent
2.1 核心思路拆解
整个系统的输入是一份代码仓库,输出是一份 Markdown 格式的 PRD 文档。中间经过两个阶段:
第一个阶段叫代码图谱构建。这个阶段的任务是把代码里的结构信息提取出来,包括函数定义、类继承关系、模块之间的调用链路、关键的业务判断分支。你可以把它想象成给一栋房子画一张结构图——哪里是承重墙、哪里是门窗、水电管线怎么走。代码图谱就是代码的“结构图”,它不关心具体的业务含义,只关心“什么东西调用了什么东西”。
第二个阶段叫业务规则抽取与文档生成。有了代码图谱之后,第二个 Skill 拿着这张图去“读”代码,识别出哪些逻辑属于业务规则,然后用自然语言把它们描述出来,最后按照 PRD 的格式组织成文档。这个阶段的关键在于:每一条规则都要标注它在代码图谱中的来源节点,这样才能做到可追溯。
两个 Skill 之间通过一个中间数据结构传递信息,我把它叫做“规则中间表示”。这个结构里既包含代码的静态信息(文件路径、行号、函数签名),也包含第一个 Skill 对代码结构的理解(调用关系、条件分支),第二个 Skill 在此基础上做业务语义的填充。
2.2 为什么不用单个 Agent 一把梭
我最初尝试过用一个 Agent 从头做到尾,结果遇到了三个绕不过去的问题。
第一个问题是上下文窗口不够用。一个中等规模的业务系统,代码量动辄几万行,把全部代码塞进一个 Prompt 里根本不现实。拆成两个 Skill 之后,第一个 Skill 的输出是结构化的图谱数据,体量比原始代码小一个数量级,第二个 Skill 只需要处理这个精简后的数据。
第二个问题是错误定位困难。单个 Agent 出错的时候,你很难判断是“代码没读对”还是“文档没写对”。两个 Skill 分开之后,第一个 Skill 的输出可以单独验证——拿它生成的调用关系和人工梳理的结果对比,准确率一目了然。如果图谱是对的但文档有问题,那问题一定出在第二个 Skill 上。
第三个问题是复用性差。代码图谱这个能力本身是通用的,今天用来生成 PRD,明天可以用来做代码影响分析、后天可以用来做新人 onboarding 的辅助材料。把它封装成一个独立的 Skill,后续换一个下游任务只需要替换第二个 Skill 就行。
2.3 技术选型背后的考量
语言选择上没什么悬念,Python 在这个场景下生态最成熟。代码解析有ast标准库和tree-sitter这样的第三方库,数据处理有pydantic做结构化校验,文档生成有jinja2做模板渲染。如果你用的是其他语言写的业务代码,Python 也能通过tree-sitter的多语言支持来解析。
Skill 的编排方式上,我没有用现成的 Agent 框架,而是自己写了一个轻量的调度层。原因很简单:这个场景的流程是线性的——先跑 Skill A,再把 A 的输出喂给 Skill B,中间不需要复杂的决策循环。引入一个完整的 Agent 框架反而增加了调试成本。调度层大概两百行代码,核心就是一个Pipeline类,负责按顺序执行 Skill、传递中间数据、记录执行日志。
中间数据的格式我用的是 JSON,配合pydantic做 schema 校验。这样做的好处是每一步的输入输出都可以序列化到磁盘上,调试的时候可以直接看中间结果,不用每次都从头跑一遍。
3. 第一个 Skill:代码图谱构建的实操细节
3.1 代码解析的三种粒度
构建代码图谱的第一步是决定解析到什么粒度。我试过三种方案,各有适用场景。
文件级粒度是最粗的,只记录每个文件里有哪些函数和类,不关心它们之间的调用关系。这种粒度实现最简单,用 Python 的ast模块遍历一遍就能拿到,但信息量太少,对生成 PRD 帮助有限。
函数级粒度是我最终采用的方案。除了记录函数和类的定义,还会分析函数之间的调用关系、函数内部的条件分支、函数读写的外部变量。这个粒度下,一个函数就是一个节点,函数之间的调用就是边,整个代码库变成一张有向图。
语句级粒度最细,会记录每一行代码的执行逻辑。理论上信息最全,但实际做下来发现噪音太大——大量的赋值语句、日志打印、异常处理跟业务规则无关,全部塞进图谱里反而干扰后续的规则抽取。
函数级粒度是一个平衡点:信息足够丰富,噪音又可控。具体实现上,我用ast.parse把每个 Python 文件解析成语法树,然后写了一个GraphBuilder类来遍历这棵树。遍历过程中维护两个栈:一个记录当前的函数调用栈,另一个记录当前的条件分支路径。
3.2 调用关系的提取与去重
提取调用关系的时候会遇到一个实际问题:同一个函数可能被多个地方调用,如果每次都记一条边,图谱会变得非常冗余。我的处理方式是按调用点去重——同一个调用者、同一个被调用者、同一个调用位置,只记一条边,但边上会标注调用次数。
还有一个坑是动态调用。Python 里可以通过getattr、eval这样的方式动态调用函数,静态分析拿不到这些信息。我的做法是:对于能静态解析的调用,正常记录;对于动态调用,在图谱里标记一个“不确定调用”节点,提醒后续的规则抽取环节注意这里可能有遗漏。
import ast class CallVisitor(ast.NodeVisitor): def __init__(self): self.calls = [] self.current_function = None def visit_FunctionDef(self, node): prev = self.current_function self.current_function = node.name self.generic_visit(node) self.current_function = prev def visit_Call(self, node): if isinstance(node.func, ast.Name): self.calls.append({ "caller": self.current_function, "callee": node.func.id, "lineno": node.lineno }) elif isinstance(node.func, ast.Attribute): self.calls.append({ "caller": self.current_function, "callee": node.func.attr, "lineno": node.lineno, "is_attribute": True }) self.generic_visit(node)上面这段代码是调用关系提取的核心逻辑。实际使用的时候还需要处理类方法、装饰器、异步函数等情况,但基本框架就是这样。
3.3 条件分支的业务含义标注
代码里的if/else是业务规则最集中的地方。比如“如果用户等级大于等于 3 且账户余额大于 100,则允许提现”,这条规则在代码里就是一个条件判断。第一个 Skill 的任务是把这些条件分支识别出来,并且标注它们所在的函数和行号。
我用的方法是在遍历语法树的时候,遇到If节点就记录下它的判断条件表达式、条件为真时执行的语句块、条件为假时执行的语句块。条件表达式会被转成字符串形式保存,后续第二个 Skill 会尝试把它翻译成自然语言。
这里有个细节需要注意:嵌套条件分支。实际代码里经常出现if里面套if的情况,如果只记录最外层的条件,内层的规则就丢了。我的处理方式是递归遍历,把嵌套的条件分支展开成一条条独立的规则路径,每条路径上标注完整的条件组合。
3.4 图谱的存储与查询
图谱构建完成之后需要存下来,方便第二个 Skill 读取。我选的是 JSON 文件,结构大概是这样的:
{ "nodes": [ { "id": "module.function_name", "type": "function", "file": "src/service/order.py", "lineno": 42, "docstring": "处理订单结算", "conditions": [ { "expr": "order.amount > 1000", "lineno": 45, "branch": "true" } ] } ], "edges": [ { "from": "module.function_a", "to": "module.function_b", "lineno": 50, "call_count": 3 } ] }没有用图数据库是因为这个场景下数据量不大,一个中等规模的业务系统也就几千个节点,JSON 完全够用。而且 JSON 的好处是肉眼可读,调试的时候直接打开看就行,不需要额外装数据库客户端。
查询方面我封装了几个常用方法:按文件路径查节点、按函数名查节点、查某个节点的所有下游调用、查某个节点的所有上游调用。这些方法在第二个 Skill 里会频繁用到。
实操心得:图谱构建阶段不要追求一步到位。我第一版只做了函数级调用关系,跑通之后发现条件分支信息缺失导致规则抽取不准,第二版才补上。建议先把最小可用的图谱跑通,再根据下游反馈逐步丰富。
4. 第二个 Skill:从代码图谱到 PRD 文档的转化逻辑
4.1 业务规则的识别策略
拿到代码图谱之后,第二个 Skill 要做的第一件事是判断“哪些节点包含业务规则”。不是所有代码都跟业务相关——工具函数、日志封装、异常处理这些通常不承载业务语义。
我的识别策略是三看:一看函数名和 docstring 里有没有业务关键词(比如“计算”“校验”“判断”“审批”),二看函数内部有没有条件分支(有分支的地方大概率有规则),三看这个函数有没有被业务入口调用(比如 API 路由处理函数、定时任务入口)。
三个条件满足任意两个,就认为这个函数包含业务规则,纳入后续处理。这个策略不是百分之百准确,但实测下来召回率能到八成以上,剩下的靠人工补漏。
4.2 条件分支到自然语言的翻译
这是整个项目里最考验 Prompt 工程的部分。代码里的条件表达式是给机器看的,PRD 里的业务规则是给人看的,中间需要一个翻译过程。
我的做法是把条件表达式、所在的函数名、函数的 docstring、以及这个函数上下游的调用关系一起打包成一个 Prompt,让模型输出自然语言描述。比如代码里是if user.level >= 3 and user.balance > 100,模型输出的是“当用户等级不低于 3 级且账户余额大于 100 时”。
这里有个技巧:给模型提供业务术语表。不同公司的业务术语不一样,有的叫“用户等级”,有的叫“会员层级”,有的叫“VIP 档位”。在 Prompt 里附上一份术语映射表,模型输出的文档会更贴合团队的实际用语习惯。
翻译过程中还要注意条件的完整性。一个if语句可能包含多个and和or连接的条件,翻译的时候不能漏掉任何一个。我的做法是在 Prompt 里明确要求模型“逐条列出所有条件,不要合并”,然后在后处理阶段再根据业务逻辑决定是否合并。
4.3 可追溯性的实现方式
可追溯是这个项目的核心卖点,具体实现上分两个方向。
正向追溯:PRD 文档里每一条规则后面都附一个引用标记,格式是[来源: 文件路径:行号]。读者看到这条规则,可以直接跳到代码里对应的位置去核实。
反向追溯:代码图谱里的每个节点都记录它被哪些规则引用了。当代码发生变更时,可以通过图谱快速定位到受影响的 PRD 章节,提醒更新文档。
实现上,第二个 Skill 在生成每条规则的时候,都会从图谱节点里取出file和lineno字段,拼成引用标记附加在规则文本后面。同时,在输出的 JSON 结构里维护一个rule_to_code的映射表,记录每条规则 ID 对应的代码位置列表。
4.4 文档模板的设计
PRD 文档的格式我参考了常见的产品需求文档结构,但做了一些调整。标准 PRD 通常包含背景、目标、功能列表、验收标准这些章节,但自动生成的文档很难写出“背景”和“目标”这种需要产品思考的内容。所以我把重点放在功能规则和验收条件这两个章节上。
功能规则章节按模块组织,每个模块下列出该模块涉及的所有业务规则,每条规则附带代码来源。验收条件章节从规则里自动推导——比如规则是“当订单金额大于 1000 时免运费”,验收条件就是“订单金额 1001 元,确认运费为 0”。
模板用jinja2渲染,好处是格式调整只需要改模板文件,不用动生成逻辑。模板里预留了几个变量:modules(模块列表)、rules(规则列表)、traceability_map(追溯映射表)。
from jinja2 import Template PRD_TEMPLATE = """ # 产品需求文档(自动生成) 生成时间:{{ generated_at }} 代码版本:{{ commit_hash }} {% for module in modules %} ## {{ module.name }} {% for rule in module.rules %} ### {{ rule.title }} {{ rule.description }} > 来源:{{ rule.source }} {% endfor %} {% endfor %} """上面是模板的简化版本,实际使用的模板会更复杂一些,包含目录、规则编号、变更记录等章节。
5. 实操全流程:从零跑通一个最小可用版本
5.1 环境准备与依赖安装
先把环境搭起来。Python 版本建议 3.10 以上,因为用到了match语句和新的类型注解语法。依赖不多,核心就三个:
pip install tree-sitter tree-sitter-python pydantic jinja2tree-sitter用来做代码解析,比标准库的ast更灵活,支持增量解析和多语言。pydantic用来做数据校验,确保中间数据结构不会因为字段缺失导致后续步骤崩溃。jinja2负责文档模板渲染。
如果你要解析的不是 Python 代码,还需要装对应语言的tree-sitter语法包,比如tree-sitter-javascript、tree-sitter-java。安装方式和 Python 的一样,pip install就行。
5.2 第一个 Skill 的完整实现
第一个 Skill 的入口是一个build_graph函数,接收代码仓库路径,返回图谱数据结构。
from pathlib import Path from pydantic import BaseModel class GraphNode(BaseModel): id: str type: str file: str lineno: int docstring: str = "" conditions: list = [] class GraphEdge(BaseModel): source: str target: str lineno: int call_count: int = 1 class CodeGraph(BaseModel): nodes: list[GraphNode] edges: list[GraphEdge] def build_graph(repo_path: str) -> CodeGraph: nodes = [] edges = [] for py_file in Path(repo_path).rglob("*.py"): tree = parse_file(py_file) visitor = GraphVisitor(str(py_file)) visitor.visit(tree) nodes.extend(visitor.nodes) edges.extend(visitor.edges) return CodeGraph(nodes=nodes, edges=edges)GraphVisitor是核心的遍历器,继承自tree-sitter的 Visitor 基类,在遍历过程中收集节点和边。具体实现比较长,关键点是在进入函数定义时创建节点,遇到调用表达式时创建边,遇到条件语句时往当前节点的conditions列表里追加条件信息。
跑完这一步,你会得到一个CodeGraph对象,可以序列化成 JSON 存到磁盘上。建议存的时候按代码仓库的 commit hash 分目录,这样不同版本的图谱不会互相覆盖。
5.3 第二个 Skill 的完整实现
第二个 Skill 的入口是generate_prd函数,接收图谱数据和输出路径。
def generate_prd(graph: CodeGraph, output_path: str): business_nodes = filter_business_nodes(graph) rules = [] for node in business_nodes: node_rules = extract_rules(node, graph) rules.extend(node_rules) modules = group_by_module(rules) doc = render_template(modules, graph) Path(output_path).write_text(doc, encoding="utf-8")filter_business_nodes就是前面说的“三看”策略的实现。extract_rules负责把节点的条件分支翻译成自然语言规则,这一步会调用大模型。group_by_module按文件路径把规则分组,同一个文件下的规则归到同一个模块。render_template用jinja2渲染最终文档。
调用大模型的部分需要你有一个可用的模型接口。我用的是本地部署的小模型做初筛,复杂规则再走云端大模型。这样做的好处是成本可控,简单规则不需要消耗大模型的 token。
5.4 跑通第一个完整流程
假设你的代码仓库在./my_project,输出文档想放到./output/prd.md,完整的调用代码是这样的:
from graph_builder import build_graph from prd_generator import generate_prd graph = build_graph("./my_project") graph.save("./output/graph.json") generate_prd(graph, "./output/prd.md")第一次跑的时候建议先拿一个小项目试水,比如只有十几个文件的 Flask 应用。跑通之后再换大项目,这样出问题容易定位。
跑完之后打开prd.md,你会看到一份按模块组织的文档,每个模块下列出了从代码里抽取出来的业务规则,每条规则后面跟着代码来源。如果发现某些规则翻译得不准,可以回头调整 Prompt 或者补充术语表。
注意事项:第一次跑的时候大概率会遇到模型输出格式不稳定的问题。我的做法是在 Prompt 里明确要求模型“只输出 JSON 格式,不要加任何解释文字”,然后在代码里做 JSON 解析的容错处理。如果解析失败,记录下原始输出,人工检查后再决定是重试还是调整 Prompt。
6. 常见问题与排查技巧实录
6.1 图谱构建阶段的典型问题
问题一:循环导入导致遍历死循环。Python 项目里模块之间互相导入是常见现象,如果遍历的时候不记录已访问节点,会陷入无限循环。解决方法是在 Visitor 里维护一个visited集合,每个节点只处理一次。
问题二:装饰器包裹的函数拿不到原始签名。用了@functools.wraps的装饰器还好,如果没用,tree-sitter解析出来的是装饰器函数的签名,不是被装饰函数的。我的处理方式是优先读__wrapped__属性,读不到就退回到装饰器签名,并在图谱里标记“签名可能不准确”。
问题三:动态生成的函数无法解析。有些代码通过exec或者元类动态创建函数,静态分析拿不到。这种情况只能在图谱里标记“动态生成”,提醒后续环节注意。
6.2 规则抽取阶段的典型问题
问题一:条件表达式太复杂,模型翻译不准。比如一个条件里嵌套了三层括号、五个and连接,模型很容易漏掉其中一两个。我的做法是先把复杂条件拆成多个简单条件,分别翻译之后再合并。拆分逻辑用ast的BoolOp节点来做,遇到and或or就拆一层。
问题二:业务术语不统一。同一个概念在不同模块里叫法不一样,生成的文档读起来很割裂。解决方法是维护一份术语映射表,在 Prompt 里附上,让模型统一输出用语。
问题三:规则重复。同一个业务规则可能在多个地方实现,比如前端校验一次、后端再校验一次。如果不做去重,文档里会出现重复条目。我的去重策略是:规则文本相似度超过 0.9 的合并为一条,来源标注多个代码位置。
6.3 文档生成阶段的典型问题
问题一:Markdown 格式错乱。模型输出的规则文本里可能包含 Markdown 特殊字符,比如*、_、#,直接渲染会导致格式错乱。解决方法是在渲染前对规则文本做转义处理,把特殊字符替换成 HTML 实体。
问题二:代码来源链接失效。如果代码仓库迁移了或者文件重命名了,文档里的来源链接就失效了。我的做法是在文档头部记录生成时的 commit hash,读者可以根据这个 hash 去代码仓库里定位对应版本。
问题三:文档太长,阅读体验差。大型项目的 PRD 可能几百条规则,全部平铺在一个文件里很难读。解决方法是按模块拆分成多个文件,主文档只保留目录和模块摘要,详细规则放到子文件里。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 图谱节点数量异常少 | 文件过滤规则太严格 | 检查rglob的匹配模式 | 放宽过滤条件,排除测试文件即可 |
| 规则翻译结果为空 | 模型接口超时或返回格式错误 | 查看调用日志和原始返回 | 增加重试机制,校验返回格式 |
| 文档里出现乱码 | 文件编码不是 UTF-8 | 用chardet检测文件编码 | 统一转成 UTF-8 再解析 |
| 追溯链接指向错误行号 | 代码格式化导致行号偏移 | 对比生成时和当前的代码版本 | 记录 commit hash,按版本追溯 |
| 规则重复出现 | 多处代码实现了同一规则 | 计算规则文本相似度 | 相似度超阈值则合并,标注多来源 |
实操心得:排查问题的时候,先把中间数据 dump 出来看。图谱 JSON 和规则抽取的原始输出都存到磁盘上,出问题的时候直接打开看,比在代码里打断点效率高得多。我习惯在每一步的输入输出都加一个
save_debug开关,调试的时候打开,生产环境关掉。
7. 进阶优化与扩展方向
7.1 增量更新:代码变了,文档怎么跟着变
全量生成适合第一次跑,但日常开发中代码是持续变更的,每次都全量跑一遍太浪费。增量更新的思路是:只处理变更的文件,更新对应的图谱节点和规则条目。
实现上需要记录上一次生成时的代码版本(commit hash),然后用git diff拿到变更的文件列表,只对这些文件重新构建图谱。规则抽取阶段,根据变更的节点 ID 找到受影响的规则,只重新生成这些规则。最后把新规则合并到旧文档里,更新追溯映射表。
这个方案有个前提:代码仓库用 Git 管理。如果你用的是其他版本控制工具,需要做适配。
7.2 多语言支持:不止 Python
tree-sitter本身支持多种语言,只要装上对应的语法包,解析逻辑基本可以复用。主要的差异在于不同语言的语法结构不一样,比如 Java 的类方法和 Python 的函数在语法树里的节点类型不同,需要针对每种语言写适配层。
我的做法是抽象出一个LanguageAdapter接口,定义parse_file、extract_functions、extract_calls、extract_conditions这几个方法,每种语言实现一个适配器。新增语言的时候只需要写一个适配器,核心的图谱构建和规则抽取逻辑不用动。
7.3 与 CI/CD 集成:让文档自动更新
最理想的形态是把这套流程集成到 CI/CD 里,每次代码合并到主分支就自动跑一遍,生成最新的 PRD 文档并推送到文档仓库。
具体做法是写一个 CI 脚本,在post-merge钩子里触发。脚本先检查本次合并是否涉及业务代码变更(排除测试文件、配置文件),如果有变更就调用图谱构建和文档生成,然后把生成的文档提交到文档仓库。
这里有个细节要注意:避免文档频繁变更导致噪音。如果每次代码提交都更新文档,文档的变更记录会非常冗长。我的做法是设置一个阈值,只有业务规则发生实质性变更(新增、删除、修改)才更新文档,纯粹的代码重构(比如函数改名但逻辑不变)不触发更新。
7.4 规则冲突检测
多个模块实现的规则之间可能存在冲突,比如模块 A 说“订单金额大于 100 免运费”,模块 B 说“订单金额大于 200 才免运费”。这种冲突在人工写文档的时候很容易被忽略,但自动生成的时候可以通过规则比对发现。
实现思路是把所有规则转成结构化的条件表达式,然后两两比对。如果两条规则的作用对象相同但条件阈值不同,就标记为潜在冲突,在文档里高亮提示。这个功能我还在完善中,目前的准确率大概七成左右,误报主要来自业务上合理的差异化规则(比如不同地区的免运费门槛不同)。
7.5 从 PRD 到测试用例的延伸
PRD 生成之后,下一个自然的需求是生成测试用例。验收条件章节其实已经包含了测试用例的雏形——每条验收条件对应一个测试场景。进一步的工作是把验收条件转成可执行的测试代码,比如 pytest 的测试函数。
这个延伸方向我做过一个原型,核心思路是把验收条件里的输入输出描述转成测试断言。比如“订单金额 1001 元,确认运费为 0”转成assert calculate_shipping(1001) == 0。难点在于函数名的推断和参数的构造,目前还需要人工介入调整,但已经能省掉不少写测试的时间。
8. 一些踩坑之后的个人体会
这套方案我从最初的想法到跑通完整流程,前后花了大概三周时间,其中大部分时间不是在写代码,而是在调 Prompt 和处理各种边界情况。有几个体会比较深。
第一,不要追求全自动。我一开始的目标是“一键生成完美 PRD”,实际做下来发现完全不现实。代码里的业务规则有太多隐含上下文,模型不可能全部理解。现在的定位是“生成初稿,人工润色”,把产品经理从“从零写文档”变成“审阅和补充文档”,效率提升已经很明显了。
第二,图谱的质量决定一切。第二个 Skill 的输出质量高度依赖第一个 Skill 的图谱质量。如果图谱里漏了关键的函数调用关系,规则抽取就会缺失上下文,翻译出来的规则就很生硬。我在图谱构建阶段花的调试时间比规则抽取阶段多得多,但这是值得的。
第三,术语表比 Prompt 技巧更重要。我试过各种 Prompt 模板、few-shot 示例、思维链引导,效果都不如直接给模型一份准确的业务术语表。模型不缺语言能力,缺的是对你们团队业务用语的理解。花半个小时整理一份术语映射表,比调一天 Prompt 管用。
第四,中间结果一定要落盘。调试的时候最痛苦的就是不知道哪一步出了问题。我的做法是每一步的输入输出都存成 JSON 文件,出问题的时候直接打开看。这个习惯帮我省了大量时间,强烈建议你也这么做。
最后分享一个小技巧:如果你觉得全量生成太慢,可以先只处理核心业务模块,把非核心模块排除在外。等核心模块跑通了,再逐步扩大范围。我第一版只处理了订单和支付两个模块,跑通之后才扩展到全量。这样出问题的时候排查范围小,迭代速度也快。