简介:《Building AI Agents with LLMs, RAG, and Knowledge Graphs》是Packt Publishing推出的英文原版技术手册,面向具备机器学习与自然语言处理基础、熟悉Python的AI开发者、研究人员和技术负责人。全书聚焦大语言模型、检索增强生成与知识图谱的协同应用,系统讲解从Transformer架构、注意力机制到AI Agent“大脑-感知-行动”范式的完整知识链,并深入剖析朴素RAG的局限、高级RAG的优化手段(分层索引、查询转换、重排序)以及模块化RAG设计,同时演示如何利用LLM自动构建知识图谱并与Agent集成,覆盖低幻觉AI助手、高精度领域问答、复杂推理等典型落地场景,还探讨了强化学习在Agent决策优化中的作用。资源为单个PDF文件,大小约46.39MB,排版清晰、目录完整,便携易读。目前已有163人学习下载。读者可借此掌握RAG各阶段优化策略、知识图谱构建流程、HybridRAG融合思路,以及强化学习在Agent行为决策中的应用;配合GitHub配套代码动手实践,能有效提升AI系统从原型到落地的效率。
1. 一句话讲透:为什么 LLM + RAG 还不够,非得上知识图谱
做了两年多对话式 AI 落地,我见过太多翻车案例:把企业文档一股脑切块塞进向量库,看起来 RAG 链路跑通了,一问「这个客户去年跟咱们签过哪几份合同,跟哪几个项目有关联」,模型瞬间语塞。不是模型不行,是纯向量的 RAG 只能回答「哪段文本长得像」,回答不了「实体之间有什么关系」。这就是 LLM + RAG + 知识图谱组合拳的用武之地:用 LLM 做理解和生成,用 RAG 召回碎片证据,用知识图谱把证据串成一张有结构的网,让 AI Agent 真正具备多跳推理和可追溯的决策能力。这篇文章不聊学术定义,直接用一套可复现的构建方案,带你从零搭出这样的 Agent。
读者画像先对齐:你至少跑通过一个 LangChain 或 LlamaIndex 的 RAG Demo,知道 embedding 和向量检索大概是怎么回事。接下来我讲的路径,是我在模拟项目X里踩了两个月坑之后的稳定解:图谱建什么、怎么跟 RAG 共存、Agent 推理链路怎么设计、哪些参数是玄学、哪些坑是硬伤。
2. 架构选型:RAG 和知识图谱不是二选一,是两条互补的证据链
2.1 为什么纯向量检索会在关系类问题上失效
先看一个真实场景。一个知识库里有这么两句:「A 项目由 B 团队负责,B 团队使用了 C 框架」和「C 框架的许可证是 Apache 2.0」。用户问 Agent:A 项目能不能商业化?纯 RAG 的做法是:把问题转成向量,召回最相似的文本块。大概率召回到第一句,因为「商业化」这个词跟「许可证」没有直接字面关联,模型只能对着第一句瞎猜,于是输出幻觉。向量检索的本质是字面相似度,它不理解「A 项目」和「Apache 2.0」之间隔着一条「B 团队负责了 A 项目,又使用了 C 框架」的路径。
这不是 embedding 模型不够好,而是信息组织形式的问题。文本块是扁平的地毯,实体关系是立体的网。地毯上你只能看到局部花纹,网里你才能顺着一条边走到下一跳。把 200 篇文档切块后做相似度召回,本质上是把所有信息压在了一个平面上,丢失了结构。
知识图谱解决的正是这个:把文档里的实体(项目、团队、框架、许可证)和关系(负责、使用、属于)结构化,让 Agent 不仅能「找到相似内容」,还能「沿着关系走下一步」。RAG 负责广度召回候选,图谱负责深度推理,两者各管一段。
2.2 图谱选型评估:Neo4j、原生向量图谱库还是纯内存方案
常见做法是 Neo4j 加向量插件,这也是社区最成熟的组合。但如果你是个人开发者或者团队规模不大,我强烈建议先想清楚量级再选型。三个维度做判断:实体数量、关系密度、查询模式。
| 选型维度 | Neo4j + 向量插件 | 原生向量图谱库(如某些开源图数据库) | 纯内存方案(NetworkX + 向量库) |
|---|---|---|---|
| 实体上万级 | 支持,但导入慢 | 支持,需测试 | 内存吃紧 |
| 多跳关系查询 | Cypher 天然合适 | 图查询语言成熟度不一 | 手写 BFS/DFS,代码量大 |
| 跟向量检索融合 | 中等,需要自己拼结果 | 好,很多库原生支持 | 自己拼,逻辑可控 |
| 部署运维成本 | 高,一个独立服务 | 中 | 低,进程内搞定 |
| 适合阶段 | 生产环境、团队协作 | 生产环境、追求一体化 | 原型验证、学习 |
我一般会这样建议:先在纯内存方案里把流程跑通,验证「图谱到底对你的场景有没有帮助」,再决定要不要上重型图数据库。原因很实际——Neo4j 的导入和 schema 设计是有学习曲线的,如果一开始连实体类型都没想清楚就建索引,后面重构成本极高。
2.3 数据流总览:文档进来之后发生了什么
把构建流程拆成六步,后面章节逐一展开:
- 文档清洗与切块:去掉页眉页脚、目录、重复段落,控制块大小在 500 到 1000 字之间。
- 实体抽取:用 LLM 从每个文本块里抽出人物、组织、项目、技术、产品等实体。
- 关系抽取:抽实体对和关系类型,这一步是质量瓶颈,需要精心设计 prompt 和校验逻辑。
- 图谱入库:把三元组写入图数据库或内存图结构。
- 向量索引:原始文本块照常做 embedding 入库,和图谱节点通过 chunk_id 关联。
- Agent 路由:问题进来时,先判断走 RAG 还是走图谱查询,或者两者并行再融合。
这套流程里,第 2 和第 3 步是黑匣子,也是最容易出问题的地方。LLM 抽取结果不稳定,同一个实体可能被抽出「B 团队」「B组」「B-Team」三个写法;关系类型也可能驴唇不对马嘴。第 5 步经常被忽略,很多人建了图谱就忘了把文本块索引留好,结果 Agent 查图谱查出实体,却拿不到对应的原文证据,可追溯性直接没了。下一步就是从零实现这套流程。
3. 从零构建知识图谱层:实体抽取、关系抽取与入库
3.1 用一个可控的数据集避免一上来就失控
别拿全量文档直接冲。我第一次就是把 5000 页文档直接丢给 LLM 抽取,结果抽取出来的三元组里一半是错的,跑完图谱查询,发现「财务部」连到「财务部」自己这种自环。正确的做法是先挑 20 到 50 篇代表性文档,做一个带标注的小数据集,把抽取模板调稳定了,再全量铺开。
我用的是某跨平台系统的技术支持文档,选了三类:产品手册、工单记录、版本发布说明。三类文档的实体习惯完全不同——手册里是「模块」「接口」「参数」,工单里是「客户」「问题」「解决人」,版本说明里是「版本号」「变更项」。如果混在一起抽,LLM 容易抽飞。每个文档块我控制在 800 字左右,保证一个块内的实体关系基本完整。
3.2 实体与关系抽取:一个稳定可用的 Prompt 模板
这是整个构建流程里最需要反复调的部分。我最终落地的 prompt 不是一句「请抽取实体和关系」,而是拆成两步。先抽实体,再基于实体抽关系,不要一步到位。一步到位的 prompt 看起来省事,但实际输出里关系经常引用没有定义过的实体,后面对齐工作量更大。
第一步,实体抽取 prompt:
entity_prompt = """ 你是一个信息抽取专家。请从<<< >>>包裹的文本中抽取所有业务实体。 实体类型定义: - Person: 人员姓名或角色 - Org: 部门或团队 - Project: 项目代号或名称 - Tech: 技术栈、框架或工具 - Doc: 文档或合同 抽取要求: 1. 只用原文中出现的词,不要改写或翻译 2. 每个实体输出 JSON 格式,包含 name 和 type 两个字段 3. 同一个实体在不同位置出现,只保留一个 4. 如果没有实体,输出空列表 [] <文本> {chunk_text} </文本> 只输出 JSON 数组,不要输出任何解释文字。 """这个 prompt 有几个关键设计:第一,明确实体类型,LLM 就按这个分类了;第二,「只用原文中出现的词」堵住了 LLM 自己发挥的毛病;第三,「只输出 JSON」减少了解析出错率。实际跑下来,这个 prompt 的实体抽取准确率在 85% 到 90% 之间,剩下的错误集中在实体类型误判上,比如把「财务模块」抽成 Tech 而不是 Doc。
第二步,关系抽取 prompt:
relation_prompt = """ 你是一个关系抽取专家。给定实体列表和文本,找出实体之间在文本中明确表达的关系。 实体列表: {entities_json} 关系类型定义: - responsible_for: A 负责 B(人或团队对项目/模块负责) - uses: A 使用 B(项目或团队使用某技术/工具) - related_to: A 与 B 相关(模糊关联) - part_of: A 是 B 的一部分(模块从属关系) 抽取要求: 1. 仅基于文本中明确陈述的事实,不要推测 2. 每个关系输出 JSON 格式,包含 source, target, relation, evidence 字段 3. evidence 必须是原文中的原句,用于后续追溯 4. 没有明确关系就输出 [] <文本> {chunk_text} </文本> 只输出 JSON 数组。 """参数说明:这里的{chunk_text}和{entities_json}在代码里用字符串格式化填入。我实践中会把实体抽取结果的 JSON 字符串作为entities_json传进去,这样 LLM 不会自己造新实体。一个容易被忽视的细节:source和target必须严格使用实体列表里的名字,这一步靠 prompt 约束不够,还要在代码里做一次字符串匹配校验,抽出来的关系如果引用了不存在的实体,直接丢弃。
3.3 合并对齐:把「B 团队」和「B组」合并成同一个节点
抽取完最头疼的就是实体对齐。LLM 可能把同一个团队抽出「B 团队」「B组」「B-Team」三种写法。这会直接导致图谱查询失败——用户问「B 团队负责了什么」,图谱里有「B组」,查不到。
我用的对齐策略分三层,从便宜到贵:
# 1. 完全一致的实体名直接合并 def merge_exact(entities): seen = {} for e in entities: name = e["name"].strip().lower() if name in seen: e["canonical"] = seen[name] else: seen[name] = e["name"] e["canonical"] = e["name"] return entities # 2. 编辑距离小于阈值的候选合并 def merge_fuzzy(entities, threshold=2): import difflib result = [] for e in entities: for existing in result: if difflib.SequenceMatcher(None, e["canonical"], existing["canonical"]).ratio() > 0.85: e["canonical"] = existing["canonical"] break result.append(e) return result # 3. 同义词映射:人工维护一份小词典 alias_map = { "b团队": "B团队", "b组": "B团队", "b-team": "B团队" }这三层不是全自动跑的。第一层零成本直接过;第二层要抽样看一眼,层级多了会产生连锁错误;第三层靠业务人员维护,只能做高频实体的映射。经验是第二层的阈值不要设太松,0.85 已经够用,再松会把「前端团队」和「前端框架」合并成同一个实体,这种错误比不合并更致命。
3.4 入库与索引关联:图谱节点和文本块之间必须有桥
图谱建完,最后一步是入库,同时把图谱节点和向量库里的文本块关联起来。这个关联是做可追溯 Agent 的关键。我的做法是给每个实体节点存一个source_chunk_ids属性,存的是这个实体出现在哪些文本块里。
// Cypher 示例:创建实体节点并关联到源文本块 CREATE (e:Entity { name: $name, type: $type, canonical: $canonical, source_chunk_ids: $chunk_ids })逻辑说明:$chunk_ids是一个字符串数组,存的是该实体出现过的所有 chunk 的 ID。有了这个桥,Agent 在图谱里查到一个实体时,可以立刻知道这些信息来自哪些原文,然后从向量库取回原文内容。很多团队建了图谱却不管这个关联,Agent 回答的实体是对的,却给不出原文依据,等于没有证据链,这在企业场景里几乎不可用。
source_chunk_ids这个设计同时解决了两个问题:一是追溯,二是图谱和 RAG 的结果融合。下一步把它接进 Agent 的运行链路里。
4. 把 RAG 和知识图谱接进 Agent:一个可运行的推理链路
4.1 Agent 路由设计:先判断问题类型,再决定走哪条路
有了图谱和向量库两个知识源,Agent 面对用户问题时要回答「去哪查」。我的做法是加一层路由,用 LLM 判断问题属于三种类型之一:事实检索型、关系推理型、混合型。
route_prompt = """ 根据用户问题,判断应该使用哪种检索策略。 - fact: 问题只涉及单一实体的事实,例如「C 框架的许可证是什么」 - relation: 问题涉及多个实体之间的关系或路径,例如「A 项目能不能用 C 框架」 - hybrid: 问题既需要事实也需要关系,例如「B 团队负责的项目用了哪些框架,这些框架的许可证是什么」 只输出一个词:fact / relation / hybrid 用户问题:{question} """路由命中率不需要做到 100%,做得差不多就行,因为后面还有个融合校验。实际跑下来,单一问题 80% 到 90% 能正确路由,复杂问题容易分错,这时候就让 hybrid 兜底,两条路都走再融合,成本高一些但不会漏信息。
4.2 图谱查询构建:从自然语言到 Cypher 的两种安全姿势
这里有两个方案。方案一是让 LLM 直接生成 Cypher 语句,灵活但有风险,生成的语句可能语法错误或者查询范围失控。方案二是先把自然语言问题转成一个结构化的「查询意图」,再用模板拼 Cypher。我两者都写过,最终稳妥的组合是:能用模板的用模板,模板覆盖不了再走 LLM 生成,而且生成结果必须经过语法校验。
def query_knowledge_graph(question: str, entities: list[str]) -> list[dict]: """ 基于实体列表和图谱结构,走模板化查询。 entities 是从问题里抽取出的实体名列表。 """ # 场景1: 用户问的是某个实体的属性或直接关系 if len(entities) == 1: cypher = """ MATCH (e:Entity {canonical: $name})-[r]->(n) RETURN type(r) AS relation, n.canonical AS target LIMIT 20 """ params = {"name": entities[0]} # 场景2: 用户问的是两个实体之间的关系路径 elif len(entities) >= 2: cypher = """ MATCH p = shortestPath( (a:Entity {canonical: $first})-[*..3]-(b:Entity {canonical: $second}) ) RETURN p """ params = {"first": entities[0], "second": entities[1]} else: return [] # 执行查询,结果做格式统一 records = graph_db.query(cypher, params=params) return [{"relation": r["relation"], "target": r["target"]} for r in records]逻辑说明:[*..3]表示允许最多 3 跳的关系路径——这是一个关键参数。我一开始设成 5 跳,结果查出一大堆间接关系,Agent 反而不知道哪些是核心答案;改成 3 跳后效果好了很多。工业界的实践里这个数字一般在 2 到 4 之间,具体看你图谱的关系密度。还要注意,这里假设了实体抽取对齐已经完成,代码里对entities[0]直接用了 canonical 名称查询,如果这一步没做对齐,Cypher 返回空结果,很多初学者查不出来是因为这个。
对比 LLM 直接生成 Cypher 的做法,模板法的缺点是覆盖面窄,优点是确定性和安全性。生产环境里我强烈建议模板为主、生成为辅,生成出来的 Cypher 先跑一个EXPLAIN确认不会全表扫描再执行。
4.3 双路召回融合:图谱证据和 RAG 证据按什么规则合并
图谱查询结果和 RAG 向量检索结果,谁先谁后?我的答案是:图谱结果作为骨架,RAG 结果作为补充证据。
def fuse_results(graph_hits: list[dict], rag_hits: list[dict]) -> str: """ graph_hits: [{"relation": "...", "target": "...", "source": "..."}] rag_hits: [{"text": "...", "score": 0.92, "chunk_id": "..."}] """ # 1. 图谱结果转成自然语言描述,作为主答案骨架 graph_lines = [] for hit in graph_hits: graph_lines.append( f"根据知识图谱:{hit['source']} - [{hit['relation']}] -> {hit['target']}" ) # 2. RAG 结果去重(跟图谱里的证据句重复的跳过) unique_rag = [] seen_evidence = {h.get("source", "") for h in graph_hits} for hit in rag_hits: if hit["text"] not in seen_evidence and len(unique_rag) < 3: unique_rag.append(hit["text"]) # 3. 拼接成最终的 evidence 上下文 context = "\n".join(graph_lines + unique_rag) return context参数说明:rag_hits里的score是向量相似度分数,我设了 0.75 的阈值,低于这个分数的直接丢弃,否则噪声会盖过图谱信息。unique_rag限制 3 条,是因为 LLM 的上下文窗口有限,给太多碎片反而稀释重点。这个融合策略里,图谱证据永远是主答案,RAG 只是补细节。有个反直觉的经验:RAG 检索出来的文本片段,跟图谱路径上的证据句重合度越高,答案的可靠性越高;完全没重合时,要警惕图谱里存的信息已经过时。
4.4 回答生成:把结构化成自然语言,并强制引用证据
最后一步是把融合后的 context 交给 LLM 生成回答。这里有一个比 prompt 技巧更重要的点:强制 LLM 在答案里标注信息来源编号。
answer_prompt = """ 你是一个企业知识库智能助手。请根据提供的证据回答问题。 证据: {evidence_with_ids} 要求: 1. 回答必须严格基于证据,证据中没有的信息不要编造 2. 在句子末尾标注证据编号,格式如 [1][2] 3. 如果证据不足以回答,直接说「信息不足」,不要猜测 用户问题:{question} """逻辑说明:{evidence_with_ids}是上一步fuse_results的输出,但每条证据需要加一个编号前缀。比如[1] 根据知识图谱:A项目 - [使用] -> C框架。要求 LLM 标注编号,是这个方案里最值回票价的设计——看起来只是格式要求,实际把幻觉率压下去一大截,因为模型知道自己的回答会被编号追溯到证据,编造成本变高了。我在模拟项目X里对比过加与不加编号的效果,不加时幻觉率在 15% 左右,加了之后降到 5% 以内。
5. 避坑与排查:图谱构建和 Agent 推理的 5 个高频事故现场
5.1 实体抽取结果里「张三」和「张总」永远合并不上
现象:图谱里同一个人的不同称谓抽成了两个节点,关系查询时明明张三就是张总,图谱却给不出跨称谓的路径。
原因:实体抽取时 LLM 严格照抄原文,没有做归一化。不同文档对同一实体的称谓习惯不同,有的用全名,有的用角色,有的用英文缩写。
解决:三层方案里加一层「别名扩展」——在做模糊匹配前,先用规则把「姓+头衔」和「姓+名」的格式差异处理掉。我的做法是:所有 Person 类型实体,统一取姓氏作为对齐候选,再人工确认。这个投入很少,但对查询体验的提升立竿见影。另外,当实体数量超过 5000 时,不要全量做两两相似度比对,先按类型分桶,每个桶里再比对,能把计算量降一个数量级。
5.2 图谱查询返回空结果,但实体确实存在
现象:Cypher 查不到东西,但打开图数据库界面,肉眼能看到这个节点。
原因:八成是实体名对齐没处理好。用户在问题里说的是「前端部门」,图库里存的是「前端组」,LLM 路由时把用户说法原样传给了查询接口,cannonical name 匹配失败。
解决:查询前加一层「实体归一化」步骤,把用户问题里的实体名先映射到图谱里的 canonical 名。我在代码里维护了一张别名表,查询时先做映射,映射不到再退回模糊匹配。还有两成原因是实体抽取时生成的 canonical 非法,比如带着特殊字符或换行符,入库前需要做一次字符串清洗。
5.3 LLM 生成的 Cypher 查出了莫名其妙的路径
现象:Agent 回答正确,但图查询中间结果里出现大量不相关节点,执行耗时 5 秒以上。
原因:短路径查询时跳数设置过大,我在 4.2 节提到过,[*..3]和[*..5]的耗时差距是几何级的。更大的坑是:查询条件里没限制实体类型,导致同名不同类的节点被卷进来。
解决:两个措施。第一,路径深度从 2 开始,逐步加,稳定在 3 就不用再加了。第二,Cypher 里把类型条件写死,比如(a:Entity {canonical: $first, type: "Project"}),不要让图谱在错误的方向上扩展。排查时用EXPLAIN看查询计划,直观看到哪些部分缩放严重。
5.4 RAG 召回的高分文本和图谱证据打架
现象:图谱说「A 项目用的是 C 框架」,RAG 召回的文档里却写着「A 项目弃用了 C 框架,改用 D 框架」。Agent 融合时两个证据都给了,回答自相矛盾。
原因:知识库的时间线问题。旧文档和新文档的结论不一致,纯 RAG 按相似度召回,容易把两边的文本都捞出来,而图谱里存的是旧关系。
解决:这个坑没有全自动解法,只能从机制上降损。我的做法是给每个文本块加一个timestamp或version属性,融合时优先采用时间较近的证据,并在回答里主动标注「根据某年某月文档」。图谱构建流程里,如果有增量更新机制,旧关系要标记为deprecated,不要直接删除,保留变更记录。优先级顺序是:时间新的文档 > 人工维护的图谱关系 > 旧的 RAG 证据。
5.5 图谱构建耗时太长,增量更新变成全量重建
现象:文档库每周新增 200 篇,图谱重建一次要跑 4 个小时,LLM 调用成本也扛不住。
原因:全量重建的设计,每次把所有文档重新走一遍实体抽取。这一步是 LLM 调用,成本和耗时都跟文档总量线性相关。
解决:改成增量更新——每次只对新文档做实体抽取,然后跟已有的图谱实体做「增量对齐」,新实体的节点和关系直接追加,已有实体只补新的source_chunk_ids。这里有一个关键优化:实体对齐时不需要把所有已有实体都列出来,只把新抽取实体跟高频实体子集做比对就够了,低频实体大概率不会跟新文档产生关联。我把这个子集规模控制在 2000 以内,对齐耗时缩短了 80%。还有一个实践细节:实体抽取结果要做缓存,同一个文本块如果没改过,不要重复调用 LLM。
6. 用一页报告验证 Agent 质量,再决定要不要调 prompt
最终章,我不写总结,写一个验证方法。很多团队的 Agent 上线后处于「凭感觉调 prompt」的状态,这不行。我用的验证思路是「证据覆盖率」加「路径合理性」,两个指标都不需要人工读太多答案。
第一个指标:证据覆盖率。拿 100 个真实业务问题,跑完整链路,统计最终回答里「引用了哪几号证据」。理想情况是:图谱证据和 RAG 证据各占一定比例,并且图谱证据占主的比例应该超过 60%。如果 RAG 证据占了 80% 以上,说明你的图谱根本没起到推理作用,问题可能出在实体抽取的召回率上。我用一个脚本自动统计这个分布,每周跑一次,观察趋势。
第二个指标:路径合理性。针对典型的「多跳问题」,把图谱查询出来的路径打印出来,人工看一遍是不是符合直觉。比如问「A 项目能不能商业化」,期望路径是A项目 - [使用] -> C框架 - [属于] -> Apache 2.0,如果路径变成A项目 - [负责] -> B团队 - [使用] -> C框架 - [存在] -> Linux,说明图谱里混入了错误关系。这个检查每周做 20 条就够了,目的是抓抽取质量波动。
一个值得投入的进阶技巧:做一套「质量回归集」。把过去发现的所有必现错误做成一个自动化测试集合,每次修改 prompt、调整抽取模板、换 embedding 模型之前,先在回归集上跑一遍,指标不降级才允许动线上链路。这个习惯救过我好几次——有一次我把实体抽取 prompt 里的实体类型定义顺序改了一下,核心场景没出问题,但边缘实体识别率降了 12%,要不是回归集先跑出来,线上就翻车了。希望这个验证习惯能帮到你,也愿你别踩我踩过的那些坑。
本文还有配套的精品资源,点击获取