代码知识库(向量检索+图遍历)随着代码演进会悄悄过时,全量重建成本高、不更新则检索质量下降;文章给出精确判断增量变化范围的实用方法。
构建好代码库知识库,运行第一次查询,看着向量搜索和图遍历返回恰好想要的结果——这时候很容易认为工作已经完成。
但代码在不断演进。新的 PR 合并进来,函数被重命名,接口发生变化,新文件出现,旧模块被删除。三个月后,索引中的函数签名可能已经过时,调用图可能指向一个已被移动的函数,而向量表示可能仍然是一个已被重写的实现。你的查询返回看似合理但实际错误的答案——而且这种失败是静默发生的。
知识库新鲜度是一个比构建知识库本身更难的问题,而且更容易被忽视。
全量重建很简单:代码变了,删除索引然后重新运行一切。对于中等规模的代码库,这可能需要几个小时——你不可能在每次提交时都这样做。另一个极端是"永不更新"——让索引永久偏离实际代码,看着检索质量在数月内悄然下降。
本文探讨的是一条实用的中间路径:精确确定哪些变更影响了什么,只重建真正需要更新的部分。
在 LightRAG 代码库上运行 detect_changes(HEAD~5 到 HEAD)返回:
changed_files: [
".github/workflows/copilot-setup-steps.yml",
".github/workflows/tests.yml",
"lightrag_webui/bun.lock",
"lightrag_webui/package.json",
"lightrag_webui/README.md",
"README-ja.md",
"README.md",
"README-zh.md"
]
changed_count: 8
impacted_symbols: [
{name: "jobs", label: "Variable", file: ".github/workflows/..."},
{name: "LightRAG WebUI", label: "Section", file: "lightrag_webui/README.md"},
{name: "Installation", label: "Section", file: "lightrag_webui/README.md"},
...
]
8 个变更文件。impacted_symbols 列表中只包含 CI 变量(jobs, on)和 Markdown 标题(Section)——零个 Function,零个 Method,零个 Class。
对这个变更集的正确回应是:什么都不做。
不是因为这些变更不重要(更新 CI 配置和 README 是真实的工作)——而是因为这 8 个文件中没有任何内容会影响知识库的三条检索路径。这些文件中没有任何内容进入向量索引,没有节点进入调用图,符号索引中也无从查找。全量重建将是纯粹的浪费:数小时的计算时间和 API 成本,对检索质量零提升。
这就引出了第一层。
不同类型的文件对知识库的影响差异很大:
决策规则:impacted_symbols 中是否包含 label 为 Function、Method 或 Class 的条目?如果没有,完全跳过此次更新。
实际上,将其编码为 git hook 或 CI 步骤:
# Pseudocode — not runnable as-is
def should_update_index(changed_files: list[str]) -> bool:
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js'}
return any(
Path(f).suffix in CODE_EXTENSIONS
for f in changed_files
)
对于 LightRAG 的 5 个提交窗口:{'.yml', '.lock', '.json', '.md'}——没有代码文件。返回 False。此批次的索引更新成本 = 0。
当第一层确定存在真实代码变更时,进入第二层。
假设一个变更涉及 lightrag/operate.py 和 lightrag/pipeline.py。detect_changes 返回这些文件中变更的具体符号。策略是:
只对变更的函数重新嵌入向量。其他一切保持不变。
底层假设:嵌入单元是函数,而函数是相对自包含的语义单元。当一个函数的实现发生变化时,只需要更新它在向量空间中的位置——其他每个函数的嵌入向量保持有效。
同一原则适用于调用图更新:
如果 operate.py 中的 naive_query 添加了对 _find_related_text_unit_from_entities 的新调用,只更新 naive_query 的出边——不要重建整个图。
如果一个函数被删除,移除对应的节点及其所有边。
增量更新成本与(变更函数数量 / 总函数数量)成正比。LightRAG 有 7,761 个 Function 节点。一次影响 50 个函数的变更成本约为全量重建的 0.6%。
第一层和第二层处理的是"我知道哪些函数直接改变了"。第三层回答一个更深层的问题:这些直接变更是否影响了本身没有变更的函数?
这里有两个关键信号。
假设 parse_document.py 中的一个底层函数改变了其接口。它被 pipeline.py 中的 analyze_multimodal 调用,而 analyze_multimodal 又被 lightrag.py 中的顶层 API 调用。这个链中的每个函数在新版本中都可能改变了行为——尽管 diff 中只有叶子函数。
trace_path("changed_function", mode=calls, direction=inbound)
→ 返回所有上游调用者,这些调用者依赖于这个函数
→ 将这些调用者标记为"可能受影响"(重新分析,不一定重新嵌入)
这是 codebase-memory-mcp 中一种容易被忽视但非常有价值的边类型。它的含义是:在历史的 git 提交中,这两个文件经常被同时修改。
查看 LightRAG 知识图的 FILE_CHANGES_WITH 边揭示了一个清晰的模式:
选定的 FILE_CHANGES_WITH 对:
FileProcessingPipeline.md ↔ routing.py # 文档和路由代码协同演进
FileProcessingPipeline.md ↔ parser.py # 文档和解析器协同演进
operate.py ↔ utils.py # 提取逻辑和工具函数耦合
operate.py ↔ prompt.py # 提取逻辑和提示词协同移动
pipeline.py ↔ utils_pipeline.py # 管道核心和管道工具
base.py ↔ lightrag.py # 抽象基类和主类协同演进
config.py ↔ lightrag.py # 配置和主类
chunk_schema.py ↔ pipeline.py # chunk schema 和管道
document_routes.py ↔ routing.py # 文档 API 和路由层
这些关系不是从静态分析推导出来的——而是从 git 历史中挖掘出来的。它们告诉你的是:
当 operate.py 变更时,历史表明 utils.py 和 prompt.py 同时变更的概率很高——即使这个特定的 diff 没有显示它们变更。如果你只重建 operate.py 的索引,可能会错过 prompt.py 知识提取逻辑中的同步调整。
这是静态分析永远无法发现的耦合。函数 A 没有调用函数 B。文件 X 没有导入文件 Y。但历史证据表明它们总是协同移动——这是一种隐式的架构约定,只能在时间数据中看到。
实际用途:在计算增量更新时,将变更文件的 FILE_CHANGES_WITH 邻居纳入检查范围,即使它们在这个特定提交中没有变更。
汇总成一个可操作的流程:
第一层:detect_changes(since=last_index_commit)
├── impacted_symbols 全部是 Section/Variable/Module?
│ └── 跳过。下次再检查。
└── 存在 Function/Method/Class 变更?
│
第二层:提取变更函数列表
├── 重新嵌入变更的函数(覆盖旧向量)
├── 本地更新调用图边(添加/删除/修改)
└── 更新符号索引(重命名/删除/添加)
│
第三层:扩展影响范围
├── trace_path(changed_fn, direction=inbound)
│ └── 将上游调用者标记为"可能受影响"
└── FILE_CHANGES_WITH(changed_files)
└── 将历史协同变更的邻居添加到下一轮审查周期
LightRAG 的成本估算:
查询 LightRAG 中最高复杂度的函数:
MATCH (f) WHERE f.label IN ['Function','Method'] AND f.complexity > 15
RETURN f.name, f.file_path, f.complexity, f.transitive_loop_depth
ORDER BY f.complexity DESC LIMIT 5
结果(排除捆绑的 swagger-ui 文件):
adelete_by_doc_id lightrag/lightrag.py complexity=131 transitive_loop_depth=14
create_document_routes lightrag/api/routers/... complexity=118
analyze_multimodal lightrag/pipeline.py complexity=116 transitive_loop_depth=14
create_app lightrag/api/lightrag_server.py complexity=91
openai_complete_if_cache lightrag/llm/openai.py complexity=89
adelete_by_doc_id 的 complexity=131,transitive_loop_depth=14——其执行路径嵌套 14 层深度,是代码库中最难推理的函数之一,也是最可能传播变更影响的函数之一。
如果这个函数出现在变更集中,第三层不是可选项——对它的每次变更都需要完整的入站调用链追踪,以验证上游调用者的行为假设仍然成立。
反之,如果变更是 README.md 中的一个段落,这些复杂度数据与你无关。detect_changes 的首要价值是它在第一层就过滤掉了嘈杂的提交——CI 调整、文档修复、依赖升级——在任何分析发生之前。
增量更新本质上是精度问题,而不是工程努力问题。全量重建实现起来更简单。增量更新需要更细粒度的决策逻辑。但一旦这种逻辑存在,回报会不断累积。
三层增量更新系统让大约 80% 的提交(文档、配置、仅测试变更)完全绕过索引更新,而剩余的 20%(真实代码变更)只重建受影响的部分。对于持续演进的代码库,这不是优化——而是使知识库在数月和数年内保持可行所必需的。
下一篇文章:我们将把范围从单个代码库扩展到多仓库场景——当一个服务调用另一个服务的 API,或者当微服务通过消息队列通信时,知识库应该如何构建跨越仓库边界的连接?
Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.
Find more useful knowledge and interesting products on my Homepage