本文摘要:向量检索按相似度召回片段,代码问答常落到词面相近的错误文件。Graphify 用 AST 把代码与配置建成可查询图谱,每条边带行号与理由,可逐条核验。
一、问题与结论
在Claude Code里问「订单金额在哪算的」,检索层把report.py中带total变量的报表代码排在前面,真正的billing/calculate_total落在后面。模型基于错误上下文作答,读起来流畅,落点却是错的文件。
把这类失败拆开,通常落在三层:
- 词面层:提问是自然语言,代码是标识符,词面不重叠;
- 关系层:真正想问的是「谁调用谁、谁写哪张表、哪个配置影响哪些代码」,这是边;向量检索返回的是点(相似片段);
- 动态层:依赖注入、反射、配置驱动路由产生的运行期边,任何静态方案都看不见。
Graphify 以/graphifyskill 接入Claude Code、Cursor、Codex、Gemini CLI,本地确定性解析代码、SQL schema、配置与 PDF,每条边带解释,不使用向量库。它补的是第 2 层。
需要说清一点:「更准」在来源里没有测量依据。能确认的是归因准确——答案可指到文件、行号与理由;召回准确(找回正确文件的比例)必须自测,下文数字一律标注为作者自测且未验证。
二、排查与选择依据
结论:先按提问类型分类,再决定检索层——结构类问题靠边,意图类问题靠语义。
- 词面失配可用标识符拆分、同义词表缓解,但只改善排序,不产生关系;
- 关系失配需要显式边(调用、继承、读写表、配置引用)。这类边由语法结构决定,适合确定性解析,每条边可回溯到行号;
- 动态边缺失是静态方案的硬边界,只能靠运行期追踪补边,解析器解决不了。
替代方案与取舍
| 方案 | 选择条件 | 代价 | 边界 |
|---|---|---|---|
| 向量 RAG | 提问以意图类为主(如「哪里处理超时」) | 返回片段无关系,落点需人工核对 | 相似但无关的片段会挤掉真答案 |
grep+ Agent 多轮搜索 | 一次性小仓库、问题零散 | 成本转嫁到推理轮次 | 跨 SQL、配置、PDF 时检索碎片化 |
| AST 符号图谱 | 结构性问题占比高、答案需可核验 | 建图成本前置,需核对语言覆盖 | 动态派发边缺失,意图类提问偏弱 |
| 图谱 + 向量混合 | 两类提问都多 | 两套索引的一致性维护 | 成本最高,小项目不划算 |
不适用场景:一次性小项目、问题以意图为主、或仓库以反射与注入为主的框架代码占多数——此时图谱给出的边看起来完整且每条都有解释,却恰好漏掉框架织入的那条,可解释性反而放大误信。
采用前需核对:/graphifyskill 的安装入口、支持的语言与解析器、图的输出形态(JSON 还是图查询语言)、是否完全无向量依赖、大仓重建耗时、License与商用限制、PDF/SQL 的解析深度。来源均未提供,属未验证项。
三、关键原理
结论:图谱的价值在于每条边可解释,而不是「更准」这个未经测量的结论。
AST 建图把函数、表、配置项、文档段落建成节点,把调用、读写、引用建成边,每条边附from、to、line、why。回答「谁写orders表」时,返回的不是相似片段,而是写入点 -> orders的路径,可回溯到行号。这就是归因准确:答案自带依据。
召回准确取决于别名解析、跨文件符号表与语言覆盖,必须用标注提问集实测;仓库描述没有准确率数据,这里不给数字。
动态边要作为重点限制写进判断:@Autowired字段注入、@Transactional动态代理、ServiceLoader/SPI、反射调用都不在 AST 调用边上。问「谁会触发OrderService.confirm()」,图只能给显式调用点。
四、可运行示例
以下是作者自建的最小复现脚本,演示「边可解释」与「边悬空」,不是 Graphify 的命令输出。环境:Python 3.10+,仅用标准库ast,无第三方依赖。
步骤 1,建样例仓库:
sample_repo/ ├── report.py ├── billing.py └── app.pysample_repo/report.py:
defgenerate_summary():total=1returntotalsample_repo/billing.py:
defcalculate_total(items):returnsum(items)sample_repo/app.py:
frombillingimportcalculate_totaldefcheckout(cart):returncalculate_total(cart)步骤 2,写建图脚本mini_graph.py,为每条边写why:
importast,jsonfrompathlibimportPath ROOT=Path("sample_repo")defparse(root):funcs,calls=[],[]forpyinsorted(root.rglob("*.py")):tree=ast.parse(py.read_text(encoding="utf-8"),filename=str(py))forfnin[nforninast.walk(tree)ifisinstance(n,ast.FunctionDef)]:qual=f"{py}::{fn.name}"funcs.append({"name":fn.name,"id":qual,"line":fn.lineno})fornodeinast.walk(fn):ifisinstance(node,ast.Call):ifisinstance(node.func,ast.Name):calls.append((qual,node.func.id,node.lineno,"callee is plain Name, statically resolvable"))elifisinstance(node.func,ast.Attribute):calls.append((qual,node.func.attr,node.lineno,"callee is Attribute, receiver type unknown"))returnfuncs,callsdefmain():funcs,calls=parse(ROOT)table={}forfinfuncs:table.setdefault(f["name"],[]).append(f["id"])edges=[]forcaller,target,line,whyincalls:hits=table.get(target,[])edges.append({"from":caller,"to":target,"line":line,"why":why,"resolved":hitsorNone,"ambiguous":len(hits)>1})print(json.dumps({"nodes":funcs,"edges":edges},ensure_ascii=False,indent=2))print("\n[query] 谁调用了 calculate_total?")foreinedges:ife["to"]=="calculate_total":print(" ",e["from"],"line",e["line"],"|",e["why"],"->",e["resolved"])if__name__=="__main__":main()预期输出(运行结果节选):
[query] 谁调用了 calculate_total? sample_repo/app.py::checkout line 4 | callee is plain Name, statically resolvable -> ['sample_repo/billing.py::calculate_total']JSON 中还会出现sample_repo/billing.py::calculate_total -> sum的未解析边(库函数不在符号表内),why说明成因、resolved为null,这同样可解释。
实际输出(本机未执行,改写调用方式后的对照方法):把app.py中的调用改成cart.pricing.calculate_total(cart)再运行,同一查询输出callee is Attribute, receiver type unknown,resolved为null——边悬空,答案指不到目标文件。
常见失败与处理:同名方法多处定义时脚本标ambiguous: true,边指向多个候选。原因是只按函数名匹配、缺类型信息。可用模块限定名二次消歧或按导入别名解析;无法消歧时保留标记交给人工,不要静默丢边。
五、验证结果与边界
结论:边界必须写进采用决策,尤其第一条。
- 动态边缺失:注入、代理、SPI、反射产生的调用不在图上,答案看起来完整却漏掉关键路径;
- 接收者类型未知:
obj.helper()、鸭子类型会导致漏边或假边(示例中的悬空与ambiguous即此状态); - 意图类提问偏弱:「哪里处理了超时重试」是意图问题,图只能答结构关系,这是不用向量库的对价;
- 规模与非代码资产:确定性解析通常全量重建,monorepo 的耗时与内存未验证;PDF 若仅作纯文本节点入图,会污染路径查询。
自建评测流程(结果未验证):准备 20 个真实提问(如「事务在哪开启」「谁写orders表」),人工标注正确文件,分别跑向量检索与图查询,记录 Top-1 命中文件数与「答案可指认依据」的比例。跑出数字之前,「更准」只应理解为「更可追溯」。
思考
- 运行期注入边该由静态规则推导,还是用一次 trace 回填更可靠?
- 意图类提问占比到多少时,纯图谱方案不如图谱与向量混合?
参考资料
Graphify-Labs/graphify