大模型应用实战

代码库知识库系列(11):跨库场景——当一个服务调用另一个服务

单库知识库已经可以回答'这个函数在哪、被谁调用、怎么修改'。但现实的工程系统往往是多个仓库通过 HTTP / 消息队列 / gRPC 相互协作。这篇文章从一次 cross-repo-intelligence 的真实运行结果出发——LightRAG × graphrag 返回 0 条跨库边——解释为什么这个结果是正确的,以及跨库分析真正在找什么:集成关系,不是功能相似性。

·约 9 分钟阅读·AI Engineering

一次结果为零的实验

对 LightRAG 和 graphrag 两个项目运行 cross-repo-intelligence

index_repository(
    repo_path="/path/to/LightRAG",
    mode="cross-repo-intelligence",
    target_projects=["mnt-hdd-...-graphrag"]
)

输出:

status: success
cross_http_calls: 0
cross_async_calls: 0
cross_channel: 0
cross_grpc_calls: 0
total_cross_edges: 0
elapsed_ms: 109

0 条跨库边。

第一反应可能是"工具没找到东西,分析失败了"。但这个结论是错的。这个结果是完全正确的,而且非常有信息量。

LightRAG 和 graphrag 是两个功能相近的开源框架——都在做基于图的 RAG——但它们是平行替代品,不是相互调用的集成系统。没有任何 LightRAG 的代码调用 graphrag 的 API,反过来也一样。cross-repo-intelligence 找的是集成关系,不是功能相似性。两者之间本来就没有集成关系,返回 0 才是正确答案。

这个结果之所以重要,是因为它划清了一条边界:跨库分析解决的问题,和单库分析完全不同。


单库分析 vs 跨库分析:两个不同的问题

回顾前十篇,单库知识库能回答的问题都在同一个仓库边界内:

  • "这个函数在哪里"(符号路径)
  • "它被谁调用"(图路径 inbound)
  • "它调用了什么"(图路径 outbound)
  • "哪些函数和它经常一起改动"(FILE_CHANGES_WITH)

这些问题的答案都在同一个代码库里,调用图是完整的,可以从任意节点出发做 BFS。

但当系统边界扩展到多个仓库时,出现了一类单库分析物理上无法回答的问题:

  • "服务 A 调用了服务 B 的哪些接口?"
  • "如果我修改了服务 B 的 /query 接口,哪些上游服务会受影响?"
  • "服务 C 通过消息队列发给服务 D 的消息格式变了,D 能感知到吗?"
  • "整个微服务网络里,谁是中心节点,谁是孤岛?"

这些问题的答案横跨仓库边界。单库的调用图在服务边界处戛然而止,成了一张"有很多悬空终节点"的残缺地图——看起来像调用了某个外部 URL,但不知道那个 URL 是谁处理的。

cross-repo-intelligence 的工作,就是把这些悬空的终节点连起来。


跨库边是怎么被发现的

cross-repo-intelligence 模式的核心逻辑是:把已索引项目里的 Route 节点和 HTTP_CALLS 节点做跨库匹配

具体来说:

  1. 发现"服务端路由":扫描所有项目,找 Route 类型的节点(即 HTTP endpoint 定义),建立 path → 项目 的路由表。

  2. 发现"客户端调用":扫描 HTTP_CALLS 边(即代码里的 HTTP 请求),提取被调用的 URL 或路径模式。

  3. 路径匹配:对每条 HTTP_CALLS,尝试在其他项目的路由表里找到匹配的 Route。找到了,就创建一条 CROSS_HTTP_CALLS 边,跨越仓库边界连接调用方和被调用方。

除了 HTTP,同样的逻辑也适用于:

  • CROSS_ASYNC_CALLS:消息队列(Kafka topic 名、RabbitMQ exchange 等)
  • CROSS_GRPC_CALLS:gRPC service/method 名
  • CROSS_CHANNEL:其他命名通道(WebSocket 频道、Redis pub/sub key)

这就解释了为什么 LightRAG × graphrag 返回 0:LightRAG 里没有任何代码向 graphrag 的路由发起 HTTP 请求,graphrag 也没有调用 LightRAG 的 API。工具没有失败——它正确地报告了"这两个系统之间没有集成关系"。


实际看两个系统的路由面貌

即使没有跨库边,看两个系统各自的路由定义,也能快速理解它们的角色。

LightRAG:REST API 服务端

Route 节点(lightrag/api/routers/):
  GET  /query
  POST /query
  GET  /query/stream
  POST /query/stream
  GET  /health
  POST /login
  GET  /auth-status
  GET  /graph/label/list
  POST /documents/paginated
  ...

LightRAG 暴露了完整的 HTTP 服务端接口——文档管理、查询、图浏览、健康检查一应俱全。从跨库分析的视角看,它是一个被调用方:如果你在另一个服务里向 http://lightrag-host/query 发请求,那条请求就会成为跨库边的一端。

对应的代码结构:create_query_routes 是 1566 行的路由注册函数,复杂度 118、认知复杂度 266——这是整个项目里最难的函数之一,因为它需要处理所有 HTTP 层面的边界情况。

graphrag:LLM 客户端

Route 节点(graphrag/):
  PATCH  /          (API root)
  外部调用: https://litellm.ai
  外部调用: https://raw.githubusercontent.com/...cspell.schema.json

graphrag 不是一个服务,而是一个命令行工具 + Python SDK。它的"路由"是它向外部 LLM 服务(litellm)和 GitHub 发起的 HTTP 调用,不是它自己暴露的 API 端点。从跨库分析的视角看,它是一个纯粹的调用方——如果你在同一个系统里同时部署了 graphrag 和 LightRAG,会发现 graphrag 可能调用 LiteLLM(CROSS_HTTP_CALLS),但 graphrag 不调用 LightRAG,LightRAG 也不调用 graphrag。

这个角色差异,从代码层面一眼可辨——一个有真正的 REST API 路由,一个只有对外的 HTTP 客户端调用。


跨库分析在实际工程里的价值

既然 LightRAG × graphrag 没有跨库边,什么样的系统才有?

典型的微服务架构

前端 App
  └─ CROSS_HTTP_CALLS → API Gateway (gateway-service)
                              └─ CROSS_HTTP_CALLS → User Service (user-service)
                              └─ CROSS_HTTP_CALLS → Auth Service (auth-service)
                              └─ CROSS_HTTP_CALLS → Order Service (order-service)
                                    └─ CROSS_ASYNC_CALLS → Kafka topic: order.created
                                                                └─ CROSS_ASYNC_CALLS → Notification Service

在这个架构里,如果 User ServiceGET /users/{id} 接口要修改(比如返回字段变了),跨库分析会告诉你:有哪些服务通过 CROSS_HTTP_CALLS 调用了这个接口,它们可能需要同步更新。这是纯单库分析永远做不到的。

cross_service 模式的 trace_path

trace_path(
    function_name="get_user_by_id",
    project="user-service",
    mode="cross_service",
    depth=3
)

这个调用会沿着 HTTP_CALLS → CROSS_HTTP_CALLS → Route 边界穿越,找出所有跨服务的调用链——从某个前端 handler 一路追到 User Service 的数据库查询。


跨库分析的三个实用场景

场景 1:服务依赖图

把所有微服务项目索引后,用 Cypher 查出跨库调用关系:

MATCH (a)-[r:CROSS_HTTP_CALLS]->(b)
RETURN a.file_path, r.path, b.file_path

结果就是整个系统的服务依赖图。哪些服务是"枢纽"(被多个服务调用)、哪些是"孤岛"(没有跨库调用)、哪些存在循环依赖——一条查询全都看到。

场景 2:接口变更影响评估

# 1. 找接口定义
search_code("GET /api/v2/payments", project="payment-service")
 
# 2. 找跨库调用方
trace_path("get_payment", mode="cross_service", direction="inbound")
→ 返回所有通过 CROSS_HTTP_CALLS 调用这个接口的上游服务

修改 /api/v2/payments 之前,先知道有哪些服务依赖它。这个操作在没有跨库分析的情况下,需要在全组所有仓库里手动 grep 这个 URL——往往有遗漏。

场景 3:消息契约追踪

如果服务间通过 Kafka 通信:

MATCH (producer)-[r:CROSS_ASYNC_CALLS]->(consumer)
WHERE r.channel = "order.completed"
RETURN producer.file_path, consumer.file_path

找到 order.completed 这条消息的所有生产者和消费者。消息 schema 要变更时,影响面一览无余。


没有跨库边的也有价值:相似性对比

回到 LightRAG 和 graphrag 这对没有跨库边的例子。虽然跨库分析没有发现集成关系,但同时持有两个项目的索引,仍然可以做一件有价值的事:接口设计对比

查 LightRAG 的查询接口:

# LightRAG
async def aquery(self, query: str, param: QueryParam) -> str

查 graphrag 的查询接口:

# graphrag
async def local_search(
    config: GraphRagConfig,
    entities: pd.DataFrame,
    communities: pd.DataFrame,
    community_reports: pd.DataFrame,
    text_units: pd.DataFrame,
    relationships: pd.DataFrame,
    covariates: pd.DataFrame | None,
    community_level: int,
    response_type: str,
    query: str,
) -> tuple[str | dict, str | list[pd.DataFrame] | dict[str, pd.DataFrame]]

两个做同类事情的框架,接口设计差异极大:LightRAG 把所有配置封装进 QueryParam(面向服务运行时),graphrag 要求调用方自己管理所有 DataFrame(面向批处理分析管道)。这两个接口设计哲学,反映了两个项目完全不同的使用场景定位。

这种跨项目接口对比,是技术选型调研时最有价值的视角之一,而它恰好是跨库知识库的副产品——不需要跨库边,只需要两个项目的索引同时存在。


总结

跨库分析不是"让单库分析变得更大",而是解决了一类全新的问题:服务边界处的知识连接

三条核心结论:

  1. 0 条跨库边是有意义的答案:它说明两个系统之间没有集成关系,而不是分析失败。不要把"工具找到东西"和"工具工作正常"混同。

  2. 跨库分析的核心是路由匹配:把一个服务暴露的 Route 节点,和另一个服务发出的 HTTP_CALLS / ASYNC_CALLS 节点做路径匹配——这是它能发现的,也只有这个它能发现的。功能相似性、代码风格对比、重复逻辑检测,都是别的工具的事。

  3. 没有跨库边的项目对,同样可以做接口对比:跨库知识库让你同时查询多个项目,即使它们之间没有调用关系,并列的索引本身就有分析价值。

下一篇,我们把视角转向知识库的评测——如何知道你的知识库"够不够好"?用什么指标衡量,怎么设计评测数据集,以及当 Recall@5 不再是唯一关注指标时,生产系统应该追踪什么。


如果这个系列对你有帮助,欢迎关注我的个人主页 dongqi.dev,持续更新 LLM 工程实践内容。

在 PrimeSkills,我们帮助工程团队建立多服务代码库知识库,包括跨库依赖图的建立和维护,以及接口变更的跨服务影响评估。如果你的团队正在管理多个微服务,欢迎探讨如何落地跨库知识库。

primeskills.dev