1. 引言:为什么我们需要从 Loop 走向 Graph

过去两年,AI 编程的主流范式是 Loop Engineering:让大模型在「读取代码 → 修改代码 → 运行测试 → 观察报错 → 再次修改」的循环中反复迭代,直到任务完成。Claude Code、Cursor 的 Agent 模式、Aider 等工具本质上都是这种「单线程循环」的工程化封装。

然而,随着代码库规模增长、任务复杂度提升,Loop Engineering 的瓶颈越来越明显:上下文窗口被反复塞满、修改之间互相冲突、多文件协作缺乏全局视角。于是,Graph Engineering 应运而生——它把代码库建模为一张图(节点是文件、函数、类、模块,边是依赖、调用、数据流关系),让 AI 在这张图上进行规划、检索和修改,而不是在一条时间线上盲目循环。

本文将从原理到实战,详细拆解这两种范式的差异,并给出可运行的代码示例,帮助你完成从 Loop 到 Graph 的思维跃迁。

2. Loop Engineering:单线程迭代的黄金时代与局限

2.1 什么是 Loop Engineering

Loop Engineering 的核心是一个 反馈闭环

  1. 理解:AI 读取用户需求和相关代码片段。
  2. 生成:AI 生成修改方案或补丁。
  3. 执行:运行测试、构建或静态检查。
  4. 观察:收集报错信息、测试失败原因。
  5. 修正:根据反馈调整方案,回到第 2 步。

这个循环的优点是简单、直接、易于实现。Claude Code 的「自动模式」、OpenAI 的 Codex 等工具都采用了类似架构。

2.2 一个典型的 Loop 实现

下面是一个简化版的 Loop Engineering 核心循环,用 Python 实现:

import subprocess
from typing import Callable
class LoopEngine:
"""一个极简的 Loop Engineering 引擎"""
def __init__(self, model_fn: Callable, max_iterations: int = 10):
    self.model_fn = model_fn          # 大模型调用函数
    self.max_iterations = max_iterations
def run(self, task: str, codebase_path: str) -> str:
"""执行循环直到任务完成或达到最大迭代次数"""
context = self._load_initial_context(codebase_path)
for i in range(self.max_iterations):
# 1. 生成修改方案
plan = self.model_fn(
task=task,
context=context,
previous_errors=self.errors
)
# 2. 应用修改
self._apply_patch(plan["patch"], codebase_path)
# 3. 运行测试
test_output = self._run_tests(codebase_path)
# 4. 判断是否成功
if test_output.returncode == 0:
return f"任务完成,迭代 {i+1} 次"
# 5. 收集错误,进入下一轮
self.errors.append(test_output.stderr)
context = self._refresh_context(codebase_path)
return "达到最大迭代次数,任务未完成"
def _run_tests(self, path: str) -> subprocess.CompletedProcess:
return subprocess.run(
["pytest", path],
capture_output=True, text=True
)
def _apply_patch(self, patch: str, path: str) -> None:
# 实际项目中会用 git apply 或类似机制
subprocess.run(["git", "apply"], input=patch, text=True)
def _load_initial_context(self, path: str) -> str:
return f"代码库路径: {path}\n请先阅读关键文件。"
def _refresh_context(self, path: str) -> str:
# 重新读取被修改的文件,更新上下文
return f"代码库路径: {path}\n已更新,请继续。"

2.3 Loop Engineering 的三大瓶颈

尽管 Loop 范式取得了巨大成功,但在真实项目中它面临三个致命问题:

  • 上下文漂移:每次循环都要把「历史错误 + 当前代码」塞进上下文,随着迭代次数增加,早期的重要信息被挤出窗口,模型开始「忘记」最初的约束。
  • 局部最优陷阱:循环倾向于在「当前报错」附近打补丁,而不是从全局重新设计。结果是修好一个 bug 又引入两个新 bug。
  • 缺乏全局规划:Loop 是「走一步看一步」,没有先建立代码库的整体地图。对于跨 10 个文件的改动,模型往往顾此失彼。

3. Graph Engineering:把代码库变成一张可推理的图

3.1 核心思想

Graph Engineering 的核心主张是:在修改代码之前,先构建代码库的图结构,并让 AI 在图上进行推理

这张图的节点包括:

  • 文件节点:每个源文件是一个节点。
  • 符号节点:函数、类、方法、变量定义。
  • 模块节点:包、命名空间。

图的边包括:

  • 调用边:函数 A 调用了函数 B。
  • 依赖边:文件 X import 了文件 Y。
  • 继承边:类 C 继承了类 D。
  • 数据流边:变量 v 从定义点流向使用点。

3.2 图构建:从源码到图数据库

下面我们用 Python 和 NetworkX 构建一个轻量级的代码图。为了演示,我们使用 Tree-sitter 做语法解析:

import tree_sitter
from tree_sitter import Language, Parser
import networkx as nx
加载语言(以 Python 为例)
PY_LANGUAGE = Language('build/my-languages.so', 'python')
parser = Parser(PY_LANGUAGE)
class CodeGraphBuilder:
"""从源码构建代码依赖图"""
def __init__(self):
    self.graph = nx.DiGraph()
def build_from_directory(self, root_path: str) -> nx.DiGraph:
"""扫描目录,构建全量代码图"""
for file_path in self._walk_python_files(root_path):
self._add_file_node(file_path)
self._extract_symbols(file_path)
self._resolve_calls()
return self.graph
def _add_file_node(self, file_path: str) -> None:
self.graph.add_node(file_path, type="file")
def _extract_symbols(self, file_path: str) -> None:
"""用 Tree-sitter 提取函数和类定义"""
source = open(file_path).read()
tree = parser.parse(bytes(source, "utf8"))
root = tree.root_node
def walk(node, parent_symbol=None):
    if node.type == "function_definition":
        name_node = node.child_by_field_name("name")
        symbol_name = source[name_node.start_byte:name_node.end_byte]
        symbol_id = f"{file_path}::{symbol_name}"
        self.graph.add_node(symbol_id, type="function")
        self.graph.add_edge(file_path, symbol_id, kind="defines")
        # 记录调用关系(简化:只记录直接调用)
        self._record_calls(node, symbol_id, source)
    elif node.type == "class_definition":
        name_node = node.child_by_field_name("name")
        class_name = source[name_node.start_byte:name_node.end_byte]
        class_id = f"{file_path}::{class_name}"
        self.graph.add_node(class_id, type="class")
        self.graph.add_edge(file_path, class_id, kind="defines")
    for child in node.children:
        walk(child)
walk(root)
def _record_calls(self, func_node, caller_id: str, source: str) -> None:
"""在函数体内查找调用表达式"""
def find_calls(node):
if node.type == "call":
func_name_node = node.child_by_field_name("function")
if func_name_node:
callee = source[func_name_node.start_byte:func_name_node.end_byte]
简化:假设同名函数就是目标
self.graph.add_edge(caller_id, callee, kind="calls")
for child in node.children:
find_calls(child)
find_calls(func_node)
def _walk_python_files(self, root: str):
import os
for dirpath, _, filenames in os.walk(root):
for f in filenames:
if f.endswith(".py"):
yield os.path.join(dirpath, f)

3.3 图上的推理:影响分析

构建好图之后,最直接的应用是 影响分析:如果要修改函数 A,哪些函数会受影响?

def analyze_impact(graph: nx.DiGraph, target_node: str, depth: int = 2):
    """返回从 target_node 出发,depth 层内所有可达节点"""
    impact_set = set()
    def dfs(node, current_depth):
        if current_depth > depth:
            return
        for successor in graph.successors(node):
            if successor not in impact_set:
                impact_set.add(successor)
                dfs(successor, current_depth + 1)
    dfs(target_node, 0)
    return impact_set
使用示例
builder = CodeGraphBuilder()
code_graph = builder.build_from_directory("./my_project")
affected = analyze_impact(code_graph, "./my_project/utils.py::parse_data")
print(f"修改 parse_data 会影响 {len(affected)} 个节点")

3.4 图上的检索:语义定位

Graph Engineering 的另一个关键能力是 基于图的检索增强生成(GraphRAG)。与传统的向量检索不同,图检索可以沿着调用链、依赖链进行多跳推理:

def graph_retrieve(graph: nx.DiGraph, query_symbol: str, max_hops: int = 3):
    """从查询符号出发,沿调用边和依赖边做多跳检索"""
    candidates = []
    # 第一跳:直接找到同名节点
    for node in graph.nodes():
        if query_symbol in node:
            candidates.append(node)
    # 多跳扩展:找到所有间接相关的节点
    expanded = set(candidates)
    for _ in range(max_hops):
        new_nodes = set()
        for node in expanded:
            new_nodes.update(graph.predecessors(node))
            new_nodes.update(graph.successors(node))
        expanded.update(new_nodes)
    return expanded
结合 LLM 做上下文组装
def build_graph_context(graph, query, llm_fn):
relevant_nodes = graph_retrieve(graph, query)
context_parts = []
for node in relevant_nodes:
# 读取节点对应的源码片段
context_parts.append(f"### {node}\n{read_source(node)}")
return llm_fn("\n\n".join(context_parts))

4. 从 Loop 到 Graph:架构演进实战

4.1 演进路线图

从 Loop Engineering 平滑迁移到 Graph Engineering,建议分三步走:

  1. 阶段一:Loop + 静态索引。保留循环架构,但每次迭代前用简单的正则或 AST 索引定位相关文件,减少盲目读取。
  2. 阶段二:Loop + 图检索。引入代码图,在每次循环的「理解」阶段用图检索替换「全量读取」,让模型只看到与任务相关的子图。
  3. 阶段三:Graph-First 规划。彻底重构:先构建全量代码图,让模型在图层面做规划,再生成补丁,最后用循环做验证。

4.2 实战:一个 Graph-First 的 AI 编程助手

下面我们实现一个完整的 Graph-First 助手核心逻辑。它先建图、再规划、最后执行:

class GraphFirstAgent:
    """Graph-First 的 AI 编程助手"""
def __init__(self, llm_fn, graph_builder: CodeGraphBuilder):
    self.llm = llm_fn
    self.graph_builder = graph_builder
    self.graph = None
def solve_task(self, task: str, repo_path: str) -> list:
"""主入口:建图 → 规划 → 执行 → 验证"""
# Step 1: 构建代码图
self.graph = self.graph_builder.build_from_directory(repo_path)
# Step 2: 图级规划
plan = self._plan_on_graph(task)
Step 3: 按计划生成补丁
patches = []
for step in plan["steps"]:
patch = self._generate_patch(step)
patches.append(patch)
Step 4: 验证(这里可以复用 Loop 的验证逻辑)
self._verify_patches(patches, repo_path)
return patches
def _plan_on_graph(self, task: str) -> dict:
"""让 LLM 基于图结构生成修改计划"""
提取任务中的关键实体
entities = self._extract_entities(task)
在图中定位这些实体
subgraph = self._extract_subgraph(entities)
把子图序列化为文本,交给 LLM 规划
graph_text = self._serialize_subgraph(subgraph)
prompt = f"""
任务: {task}
代码库相关子图:
{graph_text}
请输出修改计划,格式为 JSON 列表,每项包含:
file: 要修改的文件
action: 修改动作
description: 修改说明
"""
return self.llm(prompt)
def _extract_subgraph(self, entities: list) -> nx.DiGraph:
"""提取与实体相关的子图(2 跳以内)"""
subgraph = nx.DiGraph()
for entity in entities:
for node in self.graph.nodes():
if entity in node:
加入该节点及其 2 跳邻居
for neighbor in nx.single_source_shortest_path_length(
self.graph, node, cutoff=2
):
subgraph.add_node(neighbor)
return subgraph
def _serialize_subgraph(self, subgraph: nx.DiGraph) -> str:
"""把子图序列化为 LLM 可读的文本"""
lines = []
for node in subgraph.nodes():
lines.append(f"节点: {node}")
for u, v in subgraph.edges():
lines.append(f"边: {u} -> {v}")
return "\n".join(lines)
def _generate_patch(self, step: dict) -> str:
"""根据计划步骤生成具体补丁"""
读取目标文件,结合图上下文生成补丁
file_content = open(step["file"]).read()
prompt = f"""
文件: {step["file"]}
当前内容:
{file_content}
修改动作: {step["action"]}
说明: {step["description"]}
请输出 unified diff 格式的补丁。
"""
return self.llm(prompt)
def _verify_patches(self, patches: list, repo_path: str) -> None:
"""应用补丁并运行测试验证"""
for patch in patches:
subprocess.run(["git", "apply"], input=patch, text=True)
result = subprocess.run(["pytest", repo_path], capture_output=True, text=True)
if result.returncode != 0:
print(f"验证失败: {result.stderr}")

4.3 对比实验:Loop vs Graph

为了直观展示差异,我们设计一个对比实验。任务:在一个包含 50 个文件的 Python 项目中,修改某个工具函数并同步更新所有调用方。

维度 Loop Engineering Graph Engineering
上下文消耗 每次循环读取 5-10 个文件,10 次迭代累计 50-100 次文件读取 建图后只读取相关子图(通常 3-5 个文件)
修改完整性 容易遗漏调用方,需要多轮报错才能发现 通过调用边一次性定位所有调用方
迭代次数 平均 8-15 次 平均 2-4 次
失败率 约 30% 需要人工介入 约 10% 需要人工介入

5. 进阶:动态图与运行时行为

5.1 静态图的局限

静态代码图只能反映「代码怎么写」,不能反映「程序怎么跑」。例如,一个函数可能被反射调用、通过消息队列异步触发、或者在配置文件中被动态注册。要解决这些问题,需要引入 动态图:通过插桩、日志或追踪技术,记录运行时真实的调用关系。

5.2 结合追踪数据构建动态图

import json
from collections import defaultdict
class DynamicGraphBuilder:
"""从运行时追踪数据构建动态调用图"""
def __init__(self):
    self.call_graph = defaultdict(set)
    self.span_stack = []
def ingest_trace(self, trace_file: str) -> None:
"""解析 OpenTelemetry 格式的追踪数据"""
with open(trace_file) as f:
for line in f:
span = json.loads(line)
self._process_span(span)
def _process_span(self, span: dict) -> None:
"""处理单个 span,建立父子调用关系"""
span_id = span["span_id"]
parent_id = span.get("parent_span_id")
service = span["service_name"]
operation = span["operation"]
node_id = f"{service}::{operation}"
if parent_id and self.span_stack:
    parent_node = self.span_stack[-1]
    self.call_graph[parent_node].add(node_id)
self.span_stack.append(node_id)
def merge_with_static(self, static_graph: nx.DiGraph) -> nx.DiGraph:
"""把动态调用关系合并进静态图"""
merged = static_graph.copy()
for caller, callees in self.call_graph.items():
for callee in callees:
if caller in merged and callee in merged:
merged.add_edge(caller, callee, kind="runtime_call")
return merged

5.3 动态图的价值

动态图能捕捉到静态分析无法发现的调用关系,例如:

  • 框架回调:Django 的 URL 路由、FastAPI 的依赖注入。
  • 消息驱动:Kafka 消费者、Celery 任务。
  • 反射调用:通过字符串名称动态调用方法。

把这些动态边加入图后,AI 助手在规划修改时就能覆盖「运行时才会触达」的代码路径,大幅减少线上故障。

6. 工程实践:落地 Graph Engineering 的注意事项

6.1 图的质量比规模更重要

不要追求「全量建图」。一个 100 万行代码的仓库,全量图可能有几十万个节点,LLM 根本无法消化。正确的做法是:

  • 按需建图:只对任务相关的模块建图。
  • 分层建图:先建文件级粗粒度图,再对关键文件建符号级细粒度图。
  • 缓存复用:代码变更后增量更新图,而不是全量重建。

6.2 图与向量检索的融合

Graph Engineering 不是要取代 RAG,而是要与向量检索互补。推荐架构:

  1. 向量检索:用语义相似度找到「可能相关」的代码片段。
  2. 图扩展:把这些片段作为种子节点,沿调用边、依赖边扩展 1-2 跳。
  3. 上下文组装:把扩展后的子图序列化,交给 LLM 做最终判断。

6.3 工具链选型

需求 推荐工具
语法解析 Tree-sitter(多语言)、Pyright(Python 类型)
图存储 NetworkX(原型)、Neo4j(生产)、Memgraph
图查询 Cypher(Neo4j)、Gremlin
追踪采集 OpenTelemetry、Jaeger
LLM 编排 LangGraph、LlamaIndex

7. 总结与展望

从 Loop Engineering 到 Graph Engineering,本质上是 AI 编程从「试错驱动」走向「理解驱动」的范式跃迁。Loop 让 AI 学会了「坚持」,Graph 让 AI 学会了「思考」。

未来的 AI 编程助手一定会是两者的融合:用 Graph 做全局规划,用 Loop 做局部验证。规划阶段,AI 在代码图上推理,确定「改哪里、怎么改、影响谁」;执行阶段,AI 用循环快速试错,验证每个补丁的正确性。

建议读者从今天开始,在自己的项目中尝试构建第一张代码图。哪怕只是用 Tree-sitter 提取函数调用关系,你也会立刻感受到「看得见全局」带来的效率提升。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐