GraphRAG 的价值不只是“多查一张图”。它通常会先理解用户问题,再从向量索引召回候选文本,沿知识图谱扩展实体或关系,最后把整理后的证据交给大模型生成答案。相比单一路径的向量检索,这种方案能表达实体之间的关联,但也引入了更多网络往返、数据转换和计算步骤。
很多系统上线后遇到的第一个问题不是答案完全错误,而是响应时间不稳定:简单问题很快,涉及多个实体的问题突然变慢;图谱扩展一多,候选证据膨胀;模型调用本身还可能占据整个请求的大部分时间。若只增加机器或盲目减少检索结果,往往无法定位根因。
本文采用一个通用目标:让每次请求都能在明确的时间预算内完成,并且在部分组件超时或不可用时返回可解释的降级结果。文中的接口名称和参数仅代表示例,实际部署时应以所使用的向量数据库、图数据库和模型服务文档为准。
延迟从哪里产生
可以把一次 GraphRAG 请求拆成五段:
- 问题分析:抽取实体、识别意图、生成查询条件。
- 向量召回:把问题转换为向量,在文本或段落索引中寻找候选内容。
- 图谱扩展:根据实体节点查询一跳或多跳关系。
- 证据合并:去重、排序、截断,并生成最终上下文。
- 答案生成:调用模型完成回答、引用或结构化输出。
总耗时并不一定等于每一段耗时的简单相加。如果分析、向量召回和图查询可以并行,那么它们的最长耗时才是关键路径。相反,如果必须先完成实体抽取才能查询图谱,后续步骤就会被串行依赖拖慢。
因此,优化前应记录每个阶段的耗时、候选数量、输入输出字节数、超时次数和降级原因。没有这些指标时,“GraphRAG 变慢”只是现象,不足以支持技术决策。
先建立延迟预算
假设接口的整体目标是 3 秒,不能直接把 3 秒平均分给所有步骤,因为还要为排队、序列化和网络抖动预留空间。一种可操作的初始预算如下:
| 阶段 | 示例预算 | 约束 |
|---|---|---|
| 问题分析 | 300 毫秒 | 超时后使用规则抽取或跳过图扩展 |
| 向量召回 | 500 毫秒 | 固定候选上限 |
| 图谱扩展 | 600 毫秒 | 限制跳数、节点数和边数 |
| 证据整理 | 200 毫秒 | 在服务端完成截断 |
| 模型生成 | 1200 毫秒 | 设置连接与读取超时 |
| 预留 | 200 毫秒 | 应对网络和调度波动 |
这些数值不是通用性能结论,只是便于开始治理的配置样例。真实预算应根据业务的响应目标、数据规模、部署位置和模型服务情况重新测量。
检索策略:并行、限深、早停
向量与图查询并行
如果用户问题中已经能通过本地规则或缓存得到实体,就不必等待模型完成实体识别。向量召回和一跳图查询可以并行执行;当任一路径先获得足够证据时,也可以提前结束,但必须保留取消其他任务的机制,避免后台请求继续消耗资源。
控制图遍历边界
图查询至少要限制三个维度:最大跳数、每个节点的邻居数、最终返回的边数。多跳并不天然意味着更准确,关系噪声会随着扩展范围扩大。对问答场景,建议先使用一跳关系作为默认值,仅对明确要求路径或因果关系的问题开放更深遍历。
让召回结果尽早收敛
向量检索可以先取较小的候选集,再根据实体重合度、关系权重和文本相似度重新排序。不要把向量库和图数据库的全部结果直接拼接进提示词。候选越多,后续排序、序列化和模型输入成本越高,且相关证据更容易被噪声稀释。
下面是一个不依赖具体数据库 SDK 的 Python 服务层示例。它展示了超时、并行和结果上限,vector_search 与 graph_expand 需要替换为项目中的实际实现。
import asyncio
import time
from dataclasses import dataclass
@dataclass
class Evidence:
text: str
score: float
source: str
async def retrieve(question: str, entity_ids: list[str]) -> list[Evidence]:
started = time.perf_counter()
vector_task = asyncio.create_task(vector_search(question, limit=8))
graph_task = asyncio.create_task(
graph_expand(entity_ids, max_hops=1, max_neighbors=12)
)
try:
vectors, graph_items = await asyncio.wait_for(
asyncio.gather(vector_task, graph_task), timeout=0.9
)
except asyncio.TimeoutError:
vector_task.cancel()
graph_task.cancel()
# 超时后保留可以独立完成的向量召回结果
vectors = await vector_search(question, limit=5)
graph_items = []
merged = deduplicate_and_rank(vectors, graph_items)
result = merged[:10]
elapsed_ms = (time.perf_counter() - started) * 1000
record_metric("retrieval_latency_ms", elapsed_ms)
record_metric("evidence_count", len(result))
return result
生产代码还应在 finally 中处理任务取消,并为数据库客户端设置连接池上限。示例为了突出流程,省略了具体客户端和日志实现。
证据合并与缓存
GraphRAG 中比较容易被忽视的是“证据整理”。来自向量索引的内容可能与图谱节点描述重复,也可能只是同一文档的不同切片。可以采用以下顺序:
- 以文档编号、段落编号或实体编号生成稳定键,先去重。
- 对文本相似度、实体匹配、关系类型分别计算可解释分数。
- 为每个实体或文档设置配额,防止单一来源占满上下文。
- 按最终分数排序后截断,并保留来源标识。
- 将“没有找到关系”和“图查询超时”记录为不同状态。
缓存也要区分层级。问题向量适合短期缓存,但必须把模型、嵌入版本、规范化规则纳入键,否则索引更新后可能返回旧结果。图谱查询缓存则需要考虑节点或关系变更后的失效策略。对于答案生成,不建议默认缓存所有自然语言结果;涉及权限、实时数据或个性化内容时,缓存可能造成数据泄露或过期回答。
模型接入层如何避免拖慢主链路
模型服务应当被封装在独立适配层中,业务流程只依赖统一的请求和响应结构。例如:
import os
import httpx
MODEL_ENDPOINT = os.environ["MODEL_ENDPOINT"]
MODEL_TOKEN = os.environ["MODEL_TOKEN"]
async def generate_answer(question: str, evidence: list[Evidence]) -> str:
context = "\n\n".join(
f"[{item.source}] {item.text}" for item in evidence
)
payload = {
"input": {
"question": question,
"context": context,
},
"options": {
"temperature": 0.1,
"max_output_tokens": 800,
},
}
headers = {
"Authorization": f"Bearer {MODEL_TOKEN}"}
async with httpx.AsyncClient(timeout=httpx.Timeout(1.2, connect=0.3)) as client:
response = await client.post(
MODEL_ENDPOINT, json=payload, headers=headers
)
response.raise_for_status()
data = response.json()
return data["output"]["text"]
MODEL_ENDPOINT 可以指向自建推理服务、云端模型接口或经过统一鉴权的中转接口。若使用 HaerAPI 等第三方模型接入服务,应先核对其当前文档、请求格式、数据处理条款、可用模型和限流规则,再决定是否纳入生产链路。
适配层至少要统一四类行为:连接超时、读取超时、上游错误和输出解析失败。对模型生成失败,可以返回基于证据的摘要或明确提示“暂时无法生成答案”;不要把异常堆栈直接暴露给终端用户。日志中记录请求追踪号、模型标识、输入输出长度和耗时即可,原始敏感内容应按数据分级策略处理。
可观测性配置示例
无论使用 Prometheus、OpenTelemetry 还是其他系统,建议围绕阶段建立指标:
metrics:
- name: graphrag_stage_latency_ms
type: histogram
labels: [stage, status]
- name: graphrag_retrieval_candidates
type: histogram
labels: [source]
- name: graphrag_fallback_total
type: counter
labels: [reason]
- name: model_request_total
type: counter
labels: [provider, status]
告警条件不要只看平均延迟。平均值可能掩盖少量严重超时,实际应同时观察高分位延迟、超时率、候选数量异常和模型错误率。阈值需要结合业务基线设定,不能把示例配置直接当作生产阈值。
常见问题
图谱扩展越多,答案是否越准确?
不一定。扩展会增加候选覆盖面,但也会引入弱相关关系和过期节点。应通过带标注的问题集比较不同跳数、候选上限和排序策略,而不是仅凭个别案例判断。
为什么并行后总体时间没有明显下降?
常见原因是两条路径共享同一个连接池、线程池或 CPU 资源,或者模型实体抽取仍处在关键路径上。查看阶段级指标和资源使用情况,确认等待发生在网络、排队还是计算环节。
超时后直接使用向量结果会降低可靠性吗?
可能会。降级策略应明确标注证据来源,并限制回答范围。对于必须依赖关系链才能回答的问题,可以返回“证据不足”,而不是生成看似完整的结论。
如何验证优化确实有效?
准备覆盖简单问题、多实体问题、无结果问题和高并发场景的固定测试集,分别记录端到端延迟、各阶段耗时、召回数量、答案引用完整性和降级比例。测试结果只对当前数据、索引、部署资源和模型配置成立,不能外推到所有环境。
总结
GraphRAG 的性能治理核心不是简单删掉图谱,而是把复杂链路拆开管理:先建立阶段级指标和延迟预算,再通过并行检索、限制遍历范围、证据去重、分层缓存和明确降级控制关键路径。模型服务则应通过适配层隔离,使用环境变量管理密钥,并对超时、错误和数据处理边界做出可审计记录。
当系统能够回答“慢在哪里、为什么降级、用了哪些证据、调用了哪个模型”时,GraphRAG 才具备持续优化和稳定运营的基础。