我最早做AI Agent时,用的是最朴素的RAG方案:文档切块、向量化、Top-K召回、拼进提示词、让大模型作答。单点问答还行,直到业务方抛出一个跨文档的关系型问题——"A项目和B项目用的同一个中间件是什么版本?"——模型立刻原形毕露,答得支离破碎。向量检索擅长找"长得像"的片段,却完全不理解"谁依赖谁、谁负责谁"这类关系语义。后来我把知识底座换成了Neo4j知识图谱,用GraphRAG重新搭了一遍,同样的问题很快就顺了。这篇文章就是那次重构的完整记录:从GraphRAG和传统RAG的差异讲起,到Neo4j环境准备、图谱管线搭建、Agent编排,再到并发优化和踩坑经验,适合所有准备从0到1搭建AI Agent、以及正在为"Agent记不住关系"发愁的人参考。
1. 为什么AI Agent需要GraphRAG而不是纯向量检索
1.1 传统RAG的"记性差"问题
传统RAG把文档当作一块块孤立的文本切片,靠向量相似度召回"看起来相关"的内容。这种方式对事实型、碎片化的问题效果不错,比如"XX接口的超时时间默认是多少"。但在真实业务场景里,知识从来不是碎片,而是一张互相连接的网。
我复盘之前失败的那些问题,发现它们的共同特征是:答案的证据散布在多份文档里,需要跨文档做关系推理。向量检索只能把每份文档的相关片段分别捞出来,模型拿到这些片段后,根本拼不出"A项目→使用→中间件X"和"B项目→使用→中间件X"这样的共同关系结构。更麻烦的是,有些关键关系藏在语义完全不重叠的文档里——比如"某接口已被废弃"只出现在三个月前的会议纪要中,而"哪些模块还在调用它"只存在于当前代码变更文档里。这两段文本没有任何字面重合,向量召回直接漏掉。
这就是纯向量RAG的固有天花板:它在"语义空间"里做相似度匹配,但业务问题的答案往往藏在"关系空间"里。相似度解决不了逻辑链的问题。
1.2 GraphRAG让Agent从"翻字典"变成"看关系图谱"
GraphRAG的思路是:先让大模型把文档中的实体(人、项目、模块、接口、中间件)和关系(负责、依赖、使用、废弃)抽取出来,显式建一张知识图谱;查询时,先在图谱上定位到种子实体,再沿着关系边做多跳遍历,把相关的子图结构捞回来作为上下文给模型。
举个例子。问"哪些模块依赖了已经废弃的接口",传统RAG的做法是祈祷向量召回恰好覆盖到"废弃"和"依赖"两个词;GraphRAG的做法则是确定性地在图谱上执行一条路径:从接口节点出发,找到状态为废弃的那些,再反向沿依赖边找到所有模块节点。这不是"猜",而是"查到了"。
所以GraphRAG之于Agent,本质上是把回答问题的依据从"语义相似的文本碎片"升级为"结构化的关系证据链"。微软GraphRAG那篇论文里特别提到,面向整个语料库的全局性问题(比如"这个组织里最常见的协作模式是什么")和需要多跳推理的局部问题,纯向量方案很难稳定支撑,而图方案天然擅长。这也是微软为什么在论文题目里直接点明"From Local to Global"。Agent要做真正的业务决策,底层没有一张关系图谱,基本就是无根之木。
1.3 Neo4j在GraphRAG生态里的位置
做知识图谱的存储选型,我当时调研了Neo4j、Amazon Neptune、JanusGraph,甚至考虑过用关系型数据库硬模拟。最终选了Neo4j,核心原因有三点。
第一,Cypher查询语言在这个场景里几乎是降维打击。多跳遍历就是MATCH (a)-[:依赖]->(b)->(c)几行的事,换成SQL自连接,你写一半就想放弃。第二,生态成熟度在线:APOC扩展库、全文索引、向量索引全部内置,GraphRAG需要的"向量召回+图谱遍历"混合检索,在一个数据库里就能完成,不用接两套存储。第三,社区版免费且单机可用,练手和中小规模生产都够。
Neo4j在这套架构里扮演的是"知识底座"角色。它不负责文档存储,也不负责生成回答,而是管理所有抽取出的实体、关系和属性,对外提供子图检索能力。Agent本身不直接翻文档,而是把Neo4j当作"记忆中枢",每次推理前先从图里取出一块相关知识。
2. Neo4j环境准备:安装方式、数据模型设计与索引三件套
2.1 安装方式选择:练手用Desktop,项目用Docker
Neo4j的安装方式主要有三种:Desktop桌面版、社区版Server手动安装、Docker容器。搜索热度里总是有人在问"neo4j desktop下载"和"neo4j安装教程",说明这个选择困扰了不少人。我的结论很直接:
- 如果只是想可视化探索图数据、快速看效果,Neo4j Desktop最省事,自带Browser界面,拖拽就能看图谱。
- 如果要写代码对接、做项目、控制部署环境,我强烈建议用Docker。一条命令起服务,换版本、清数据都方便,生产环境也能平滑迁移。
启动社区版容器的命令我放在这里:
docker run -d \ --name neo4j-graphrag \ -p 7474:7474 -p 7687:7687 \ -e NEO4J_AUTH=neo4j/YourStrongPassword \ -e NEO4J_PLUGINS='["apoc"]' \ -v ./neo4j-data:/data \ neo4j:5.26-community7474是HTTP端口,浏览器访问管理界面用;7687是Bolt端口,所有程序驱动都走这个。NEO4J_PLUGINS环境变量会在首次启动时自动下载安装APOC插件,这是后续做批量导入、全文检索的基础。
这里有两个常见的坑值得提前说。第一,NEO4J_AUTH只对首次初始化生效,如果你挂载的./neo4j-data卷里已有旧认证数据,后面再怎么改环境变量都没用,密码以卷里的旧数据为准。我因为这个问题排查了整整一下午。第二,4G以下内存的机器跑Docker容器,一定记得加-e NEO4J_server_memory_heap_max__size=2G这类参数限制堆内存,否则Neo4j默认会试图吃掉大半个机器内存。
顺带回应一个老话题:经常有人搜"neo4j 3.5哪里可以下载"。3.x实在太老了,Cypher语法、插件机制和5.x差异巨大,官方早就停止维护。除非是没法动的遗留系统,所有新项目都请直接上5.x(当前建议5.26+)。
2.2 图数据模型设计:先跑小样本,再定Schema
很多初学者上来就让LLM猛抽实体,抽完一股脑塞进Neo4j,结果图谱乱得像毛线团。GraphRAG的图模型必须先想清楚,否则后面每个查询都是灾难。
我的做法是:先用一小批样本文档跑抽取,把LLM实际输出的实体类型统计出来,再收敛成正式Schema。拿"产品手册+项目周报"这类内部文档举例,最后设计的标签和关系类型如下:
| 标签(Node) | 关键属性 | 说明 |
|---|---|---|
| Project | code, name, status | 项目节点 |
| Person | name, email, title | 人员节点 |
| Module | name, repo, language | 系统模块 |
| Interface | name, method, path | 接口 |
| Middleware | name, vendor, version | 中间件 |
| Meeting | date, summary | 会议纪要 |
关系类型则是动作化的动词::负责人、:依赖、:使用、:实现了、:废弃、:讨论过。
设计原则我总结成三条,都是吃过亏换来的。
第一条,关系必须带明确方向语义。(Module)-[:依赖]->(Interface)和(Interface)<-[:依赖]-(Module)不能混写。Cypher查询是严格区分方向的,方向写反等于路上路牌立反,Agent查出来的结果就是错的。
第二条,实体必须有唯一标识。项目用code,人用邮箱,模块用name加repo。这个唯一键是后续MERGE去重的锚点。没有它,同一实体被抽十次就会产生十个节点,图谱直接废掉。
第三条,属性宁少勿多。把需要过滤、排序、展示的属性放节点上就够了,其余细节统一存个source_doc_id字段指向原文。图数据库不是文档仓库,别把大段正文往节点属性里塞。
2.3 索引三件套:标签索引、全文索引、向量索引
Neo4j查询性能很大程度上靠索引设计。GraphRAG项目至少需要建三类索引:
// 标签+属性上的常规索引,支撑实体精确定位 CREATE INDEX idx_project_code FOR (p:Project) ON (p.code); CREATE INDEX idx_person_name FOR (p:Person) ON (p.name); // 全文索引,处理中文模糊匹配和同义词场景 CREATE FULLTEXT INDEX entitySearch IF NOT EXISTS FOR (n:Project|Person|Module|Interface|Middleware) ON EACH [n.name]; // 向量索引,支撑实体向量的相似度召回 CREATE VECTOR INDEX entityEmbedding IF NOT EXISTS FOR (n:Module|Interface) ON (n.embedding) OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' }};全文索引特别值得多说一句。LLM抽取实体时经常有微小文本差异,比如"用户中心"和"用户中心服务",精确匹配会漏,全文索引可以模糊命中,为后面的实体消歧争取机会。向量索引则是GraphRAG双通道检索的关键支撑,后面Agent调用那节会展开。
3. 从零搭建GraphRAG管线:文档切分、实体抽取与批量落库
3.1 文档切分策略和普通RAG完全不同
普通RAG的切分目标是让每个块语义完整、相互独立;GraphRAG的切分目标则是让LLM在可控窗口内准确抽取出实体和关系。这两者差别很大。
我实际用的方案是按文档结构切分,并保留标题上下文:
from langchain_text_splitters import MarkdownHeaderTextSplitter splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[("#", "H1"), ("##", "H2"), ("###", "H3")] ) chunks = splitter.split_text(document_md) # 每个chunk都带着标题路径,例如 "项目A > 用户中心 > 接口说明"这个"带标题路径的chunk"对抽取质量影响极大。LLM看到当前片段属于"项目A的用户中心模块",抽取用户中心服务实现了GetUserInfo这类关系时就不会张冠李戴到项目B身上去。我见过不少团队省掉这一步直接按字数硬切,结果图谱里关系错乱、实体串项目,后面清理的代价远超省下的那点功夫。
3.2 用JSON Schema约束LLM做关系抽取
实体关系抽取是整个GraphRAG管线里成本最高、最影响质量的环节。LLM如果不加约束,会抽出一堆"涉及""包含""提到"这类没有检索价值的垃圾关系,图谱瞬间变成语义泥潭。我的做法是:用JSON Schema和类型白名单把LLM的输出严格框住。
抽取Prompt的核心结构如下:
你是一名知识抽取引擎。请从给定的文档片段中抽取实体与关系,输出JSON数组。 实体类型:Project, Person, Module, Interface, Middleware, Meeting 实体字段:name, type, props 关系类型:负责人(PP), 依赖(DEP), 使用(USE), 实现了(IMPL), 废弃(OBE), 讨论过(DISC) 关系字段:source, target, type, props 约束: 1. 只能使用上述实体类型和关系类型,禁止发明新类型 2. source和target必须在该片段中真实出现 3. 不确定的关系不要抽取,宁可少抽不要错抽 4. 如果实体可能与已知实体相同,在props.aliases中给出别名列表 示例输出: {"entities":[{"name":"用户中心服务","type":"Module","props":{}}], "relations":[{"source":"用户中心服务","target":"GetUserInfo","type":"IMPL","props":{}}]}为什么要限制关系类型白名单?因为LLM自由抽取时会产生几十种花样繁多的关系词,这些词对图谱查询毫无意义,还让Schema失控。把关系类型锁定在10种以内,图谱才能保持可用和可查。
中文资料还有一个特有的麻烦:同一系统在不同文档里可能有"用户中心""用户中心服务""UC"三种叫法。我的方案是让LLM为每个实体额外生成aliases字段,落库时用别名去匹配已有节点,这会极大地降低实体碎片化。至于这个消歧逻辑的坑,后面第六节专门讲。
3.3 Cypher批量导入:MERGE是生命线
抽取结果要写进Neo4j。这里最大的教训是:一定用批量参数方式,不要一条条发查询。我第一次实现时逐条执行,抽500个片段跑了半小时;改成批量参数后,速度提升了几个数量级。
from neo4j import GraphDatabase SAFE_LABELS = {"Project", "Person", "Module", "Interface", "Middleware", "Meeting"} class GraphKnowledgeBase: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) def upsert_batch(self, entities, relations): with self.driver.session() as session: session.execute_write(self._write_batch, entities, relations) @staticmethod def _write_batch(tx, entities, relations): # 先写实体,利用唯一键和MERGE保证幂等 for ent in entities: label = ent["type"] if label not in SAFE_LABELS: continue # 标签无法参数化,必须白名单校验后再拼接 tx.run( f"MERGE (n:`{label}` {{name: $name}}) " "SET n += $props, n.updated_at = timestamp()", name=ent["name"], props=ent.get("props", {}) ) # 再写关系,先定位两端实体,再MERGE关系 for rel in relations: rel_type = rel["type"] if rel_type not in SAFE_RELATIONS: continue tx.run( f"MATCH (a {{name: $src}}) MATCH (b {{name: $dst}}) " f"MERGE (a)-[r:`{rel_type}`]->(b) " "SET r += $props", src=rel["source"], dst=rel["target"], props=rel.get("props", {}) )这段代码里有三个工程要点值得展开。
第一,节点标签和关系类型在Cypher中无法参数化,只能字符串拼接。所以必须做白名单校验,否则LLM抽取结果里混入恶意或错误的标签名,就是Cypher注入风险。
第二,写入顺序必须是"先实体、后关系"。关系的MERGE需要先MATCH到两端的实体节点,顺序反了关系会挂在自动创建的空白点上。
第三,属性合并用SET n += $props而不用SET n = $props。前者是增量合并,只覆盖已有的同名属性、保留其余;后者是整个覆盖。增量合并能保留不同来源的补充信息——一个实体在A文档里抽到了版本号,在B文档里抽到了负责人,两次合并后两个属性都在。
提示:MERGE对关系的方向是敏感的。如果图谱里已经存在
(a)-[:依赖]->(b),你再MERGE一个(b)-[:依赖]->(a),会生成一条新关系而不是复用旧的。所以关系类型的方向规范必须在Schema设计阶段定死,不要今天写A依赖B,明天写B被依赖A。
我还会在每个实体上保存source_doc_id和chunk_id,一旦发现抽取错误,能快速定位到原文做人工修正。这一步在早期尤其重要——LLM抽取的准确率大约九成,剩下那一成错了关系,必须能追踪、能修。
4. Agent如何调用图谱:双通道检索、LangGraph编排与Cypher生成的取舍
4.1 两种检索模式:邻居遍历和混合召回
图谱建好后,Agent查询时怎么取上下文?我实践下来最常用两种模式。
模式一是实体定位加邻居遍历,适合"和X相关的信息都有什么"这类问题。先从用户问题里抽出生子实体,定位节点,再做一到两跳遍历把邻居和关系带回来:
MATCH (n:Module {name: '用户中心服务'})-[r]-(m) RETURN n, r, m LIMIT 50模式二是向量召回加图谱扩展,适合问题里没有明确实体、只有模糊描述的情况。先用embedding在向量索引里召回Top-N候选实体,再把这些实体当种子节点做子图扩展,合并去重后作为上下文:
CALL db.index.vector.queryNodes('entityEmbedding', 10, $queryVector) YIELD node, score MATCH (node)-[r]-(m) RETURN node, r, m LIMIT 80两种模式可以组合成"混合检索":向量负责找出可能相关的起点,图遍历负责把关系链条补全,最后同时把"子图三元组"和"命中的原文片段"一起喂给LLM。实际效果比单一模式好很多——向量兜底召回、图谱负责推理,各司其职。
这里有一个细节:喂给LLM的子图要序列化成文本三元组,比如(用户中心服务)-[依赖]->(GetUserInfo接口),而不是直接丢JSON。三元组的token消耗更少,大模型理解关系结构也比读嵌套JSON容易得多。
4.2 用LangGraph编排Agent:FastAPI + LangChain落地
既然是Agent项目,编排层我推荐LangGraph。配合FastAPI做API暴露和LangChain做工具集成,是目前快速落地又不容易跑偏的组合,也是网上讨论最多的路线。
Agent状态机设计成四步:意图识别、图谱检索、上下文组装、生成回答。用LangGraph表达如下:
from langgraph.graph import StateGraph, END from typing import TypedDict class AgentState(TypedDict): question: str entities: list[str] subgraph_context: str answer: str def retrieve(state: AgentState): # LLM抽取实体 -> Cypher子图查询 ... def build_context(state: AgentState): # 把三元组与原文片段拼成上下文 ... def generate(state: AgentState): # LLM基于上下文生成最终答案 ... graph = StateGraph(AgentState) graph.add_node("retrieve", retrieve) graph.add_node("build_context", build_context) graph.add_node("generate", generate) graph.set_entry_point("retrieve") graph.add_edge("retrieve", "build_context") graph.add_edge("build_context", "generate") graph.add_edge("generate", END)LangGraph的真正价值在于,每一步都是可插拔函数,中间状态可以持久化到checkpoint。这意味着Agent跑到第三步超时挂掉,恢复后可以从第二步重放,而不是整条链路重来。在真实生产环境,图查询超时、LLM调用限流太常见了,这个能力不是锦上添花,是刚需。
FastAPI那边只需暴露两个接口:POST /ingest用于接收文档并触发建图管线,POST /chat用于对话问答。在并发上,FastAPI的异步接口配合LangGraph的线程池运行,单机扛几十路并发问题不大——真正的瓶颈在Neo4j和LLM供应商那边,下一节细说。
4.3 两条Cypher生成路线的取舍
让Agent操作Neo4j,业界主流有两条路线,我各自踩过坑,直接说结论。
路线A是预定义Cypher模板加参数填充。把高频查询写成模板,Agent只负责抽出实体名和关系类型填进去。比如:
模板:查找依赖了 {interfaceName} 的所有模块 Cypher: MATCH (m:Module)-[:依赖]->(i:Interface {name: $interfaceName}) RETURN m.name优点是安全、可控、性能可预期,缺点是只能回答你提前想到的问题。
路线B是让LLM直接写Cypher,用自然语言到查询语句的翻译能力覆盖长尾问题。这个方案灵活,但必须设护栏:第一,只允许以MATCH、CALL、RETURN开头的只读语句,出现CREATE、DELETE、MERGE、SET、REMOVE直接拒绝;第二,执行前做语句类型检查;第三,把当前图谱的Schema(节点标签、关系类型、属性名)注入提示词,LLM才能写出有效查询。
我的实际策略是"模板保底,生成增强":高频问题走模板,模板覆盖不到的走LLM生成。这样既控制了成本与延迟,又保住了长尾问题能力。纯靠LLM生成Cypher的路线,第六节有惨痛教训。
5. 扛并发:Agent + GraphRAG的性能优化实录
5.1 先把瓶颈列出来,再谈优化
"ai agent 怎么扛并发"是搜索热度很高的词,我的经验是:别一上来就调数据库参数,先搞清楚请求耗时花在哪了。一个典型的GraphRAG问答请求,耗时构成大致如下:
| 环节 | 耗时占比 | 瓶颈点 |
|---|---|---|
| LLM实体抽取/意图路由 | 约30% | 大模型推理延迟 |
| GraphRAG检索(图遍历+向量召回) | 约20% | Neo4j查询、embedding服务 |
| LLM生成回答 | 约40% | 大模型推理延迟 |
| 网络与序列化开销 | 约10% | 服务间调用 |
从这张表能看出一个反直觉的结论:并发瓶颈往往不在Neo4j,而在LLM调用。每个请求如果同步调用两次大模型(一次抽实体、一次生成回答),QPS稍微上来,LLM服务的限流就会先把整个系统打挂。
所以第一条经验是:能缓存就不重算,能异步就不同步。
5.2 三大利器:连接池、语义缓存、异步化
Neo4j的Python/Java驱动都内置连接池机制,max_connection_pool_size默认就有100。多线程或异步环境里,每个进程保持一个独立Driver实例即可,不要在每次请求时新建连接,这是最基本的并发保障。
语义缓存才是解决并发问题的核心手段。对相同或相似的问题,直接复用之前的结果,连LLM都省了。我实现的是两层缓存:第一层是问题级别的精确缓存,完全相同的问题直接命中;第二层是语义缓存,把新问题的embedding和缓存历史做相似度比对,超过0.92就复用:
def get_cached_answer(question: str, embedding: list[float]) -> str | None: # Redis中维护最近一批问答对,逐个做余弦相似度比对 for cached_q, cached_emb, cached_ans in recent_pairs: if cosine_similarity(embedding, cached_emb) > 0.92: return cached_ans return None这个语义缓存实测能命中三到四成重复业务问题。运营类Agent效果尤其好——业务人员翻来覆去问的就是那几十件事。
异步化则是把耗时长的图数据管线(文档抽取、建图)放进后台任务队列,让问答接口只依赖已经建好的图谱,灌文档和问答互不阻塞。轻量场景用Python的ThreadPoolExecutor加上简单任务队列就够,没必要为此直接上重型MQ。但如果文档量大、抽取任务繁重,Celery或RQ这类工具会更顺手。
5.3 实测数据与具体调优参数
我在一台4核8G的部署机上做过一次压测。场景是内部知识Agent,图谱包含1900个实体、4300条关系。单路响应延迟约2.8秒,其中LLM生成就占1.8秒;加上语义缓存后,重复问题响应压到800毫秒以内。用Locust跑到100并发,配合连接池和缓存,P95延迟从6.5秒降到2.1秒。
具体调优还有几个点值得记录:
- Neo4j配置中把
dbms.memory.pagecache.size调大,机器内存的一半左右给pagecache,图查询会有肉眼可见的提升。 - Cypher查询务必加
LIMIT,尤其是邻居遍历。一个高连接度节点可能连了几千条边,不加限制能把内存撑爆。我在实体定位查询里默认限制50条边。 - embedding服务如果是自建的,加一层本地批量缓存,避免同一实体反复计算向量。
6. 踩坑实录:实体消歧、Cypher安全与数据更新的连环坑
6.1 实体消歧:同一个实体的"分身术"
图谱刚跑起来时,图里同时出现了"用户中心""用户中心服务""User Center Service"三个节点,它们其实是同一个系统。按name: '用户中心'精确匹配查询时,只能召回其中一部分关系,Agent的回答自然残缺。
我加了两道防线。第一道是抽取时让LLM生成aliases;第二道是落库前用全文索引做模糊匹配,把新实体映射到已有节点:
CALL db.index.fulltext.queryNodes('entitySearch', $alias) YIELD node, score WHERE score > 0.5 RETURN node.name AS existing_name, score ORDER BY score DESC LIMIT 3如果最高分节点的相似度足够高,就执行合并;否则才创建新节点。这套逻辑让图谱实体数量减少了约15%,查询准确率提升明显。
但合并要非常谨慎。同名但确实不同的实体——比如两个同名同姓的人、两个都叫"订单服务"的系统——绝不能强行合并,否则会出现关系张冠李戴的新问题。我的做法是给合并增加置信度阈值,拿不准的一律交给人工复核,宁可不合并也不乱合并。
6.2 LLM生成Cypher的安全事故预警
让LLM直接写Cypher这条路线,我差点出过真实事故。一次内部测试,有人提问"如果删除所有节点会发生什么",LLM真的生成了一条以MATCH (n) DETACH DELETE n结尾的查询。幸好执行层做了拦截,否则整个图谱灰飞烟灭。
这正是我在4.3节强调护栏的原因。现在的执行屏障有两层:第一层,白名单检查语句类型,只允许MATCH、CALL、RETURN开头,出现任何写操作关键字直接拒绝;第二层,为Agent创建独立的只读数据库账号,从数据库权限层面釜底抽薪,即使第一层被绕过了,也只读账号也执行不了写操作。这两层缺一不可。
6.3 数据更新的"新鲜度"难题
图谱建完不是一劳永逸的。文档更新了、旧接口废弃了、人员变动了,如果图谱不跟着变,Agent就会一本正经地用过期知识回答。我现在的方案是基于时间戳的增量管线:
- 每个实体和关系都带
updated_at时间戳。 - 定时任务扫描变更文档,只对增量片段重新抽取。
- 冲突处理采用"新时间戳覆盖旧时间戳"策略,但不直接物理删除旧数据,保留历史版本以便回滚。
这个方案应对周级更新的内部知识库足够。如果业务是分钟级更新——比如电商价格、实时监控状态——离线建图模式就不适用了,需要流式更新的架构,那是另一个故事。选型前先搞清楚自己的数据变更频率,能避免很多后期痛苦。
6.4 练手项目怎么选
如果你想从零开始练手,别一上来就喂几十万字语料。我推荐的练手路径是:找20到50篇技术博客或内部文档,跑通"文档→抽取→Neo4j→Agent问答"全链路,然后设计几个多跳问题做验证,比如"写过A技术的人还写过哪些B相关内容",或者"哪个模块依赖了最多外部中间件"。这个规模成本低、迭代快,一两天就能跑通,又能把GraphRAG的所有核心环节完整过一遍。
还有一个观察图谱的小技巧:多在Neo4j Browser里可视化地看看图。图数据库最大的好处就是图真的能"看"。哪个节点连了几百条边、哪块子图明显是孤岛、哪些关系方向反了,扫一眼就知道,很多设计问题根本不用等Agent答错才发现。每次建完一批数据,我总会先可视化逛一圈,再进入下一批处理,这个习惯帮我省掉了大量返工时间。