ChatGPT 生成的图代理
基于检索的生成(RAG)将会长期存在——而且理由充分。它是一个强大的框架,融合了先进的语言模型和有针对性的信息检索技术,实现更快地访问相关数据,并生成更具上下文感知的响应。虽然RAG应用通常关注未结构化的数据,但我非常赞同将结构化数据整合进来,这是一个重要但常被忽视的方法。我最喜欢的其中一种做法是利用像Neo4j这样的图数据库来实现这一点。
通常,从图中检索数据的方法是Text2Cypher,即将自然语言查询自动转换为Cypher语句来查询图数据库。这种方法依赖于语言模型或基于规则的系统来解释用户查询,推断其意图,并将其翻译成有效的Cypher查询,从而使RAG应用程序能够从知识图中检索相关信息并给出准确的答案。
使用LLM生成Cypher语句 — 图片来源:https://neo4j.com/developer-blog/fine-tuned-text2cypher-2024-model/
Text2Cypher 提供了显著的灵活性,因为它允许用户用自然语言提出问题,无需了解底层的图模式或 Cypher 语法。但由于语言解释的细微差别和对精确模式特定细节的需求,其准确性可能仍存在不足,如以下 Text2Cypher 文章中所述。
使用Neo4j Text2Cypher (2024年) 数据集进行性能测试作者:Makbule Gulcin Ozsoy, Leila Messallem, Jon Bes(来自Medium.com)以下是最重要的基准测试结果,通过下面的可视化展示来呈现。
Text2Cypher 基准测试结果 — 图片来自 https://medium.com/neo4j/benchmarking-using-the-neo4j-text2cypher-2024-dataset-d77be96ab65a 点击链接查看原文
从宏观层面来看,基准比较了三种模型:
- 针对文本到Cypher任务的微调模型
- 开源基础模型
- 闭源基础模型
评估基准使用这两个指标来衡量他们生成正确Cypher查询的表现:Google BLEU(图表上的)和ExactMatch(图表下的)。
Google 的 BLEU 指标衡量生成查询与参考查询之间 n-gram 的重合程度。更高的分数通常表示与参考查询更接近,但高分并不意味着查询在数据库环境中一定会正确运行。
ExactMatch是一种基于执行的指标。它表示与正确查询文本完全匹配的生成查询的百分比,这意味着当执行时,它们会产生相同的结果。这使得ExactMatch成为更严格的正确性衡量,并且更直接地反映了查询在实际应用中的实用性。
尽管经过微调取得了一些令人鼓舞的结果,Text2Cypher的整体准确率仍然显示它仍是一项正在发展的技术。一些模型在某些情况下仍然难以生成完全正确的查询,这凸显了进一步改进的必要性。
在这篇文章中,我们将尝试使用LlamaIndex Workflows来实现更主动的Text2Cypher策略。我们不会依赖于单一的查询生成方式,这通常是大多数基准测试中使用的方法,而是会采用多步骤的方法,允许重试或采用替代查询方式。通过增加额外步骤和备用选项,我们旨在提高整体准确性和减少Cypher生成错误。
代码可在_ GitHub 上找到。我们还提供了一个 托管版本 ,可以直接使用。感谢 Anej Gorkic _的帮助,她不仅贡献了代码,还帮忙调试。谢谢!:)
托管的 web 应用程序,包含所有代理,可以通过https://text2cypher-llama-agent.up.railway.app/访问。
LlamaIndex 流程LlamaIndex 工作流 是一种实用的方法,通过事件驱动系统连接不同的操作步骤来帮助组织多步骤的AI流程。它们有助于将复杂任务分解为更小、更易于管理的部分,这些部分可以以结构化的方式相互交流,形成一系列操作链条,可以完成诸如文档处理、回答问题或生成内容等任务。系统自动处理步骤之间的协调,使构建和维护复杂的AI应用变得更加容易。
Naive Text2Cypher 流程这种简单的Text2Cypher架构是一种将自然语言问题转换为Cypher查询以供Neo4j图数据库使用的简洁方法。它通过一个三阶段的工作流程运行:
- 使用类似示例进行少量样本学习,从输入问题生成Cypher查询,这些示例存储在向量数据库中。
- 系统执行生成的Cypher查询来访问图数据库中的信息。
- 通过语言模型处理数据库返回的结果,生成直接回答原始问题的自然语言回答。
此架构保持了一个简单而有效的管道,例如,利用向量相似性搜索(例如,少量样本检索),使用大型语言模型(LLM)生成Cypher查询并格式化响应。
如下的基本 Text2Cypher 可视化工作流如下。
基础文本转换成Cypher的过程
值得注意的是,大多数Neo4j模式生成方法难以处理带有多个标签的节点。这个问题不仅因为增加了复杂性,还因为标签的组合爆炸性增长导致提示过载。为解决这个问题,我们在生成模式的过程中排除了演员和导演这两个标签。
schema = graph_store.get_schema_str(exclude_types=["演员", "导演"])
# 获取图谱存储中排除了"演员"和"导演"类型的模式
该流程从generate_cypher
步骤开始:
@步
async def generate_cypher(self, ctx: Context, ev: StartEvent) -> ExecuteCypherEvent:
question = ev.input
# 使用大语言模型生成Cypher查询
cypher_query = await generate_cypher_step(
self.llm, question, self.few_shot_retriever
)
# 将事件信息流式传输到Web界面。
ctx.write_event_to_stream(
SseEvent(
label="生成Cypher",
message=f"生成Cypher:{cypher_query}",
)
)
# 返回下个步骤
return ExecuteCypherEvent(question=question, cypher=cypher_query)
生成 generate_cypher
步骤会将自然语言问题转换为 Cypher 查询,通过使用语言模型,并从向量存储中检索类似示例来实现转换。该步骤还会实时将生成的 Cypher 查询显示在用户界面上,提供即时的查询生成过程反馈。你可以通过点击此处来查看整个代码和提示。
简易的Text2Cypher重试流程
这是带有重试功能的Text2Cypher增强版,在此基础上增加了自我修复机制。当生成的Cypher查询执行失败时,系统不会直接失败,而是在CorrectCypherEvent
步骤中尝试修复查询,通过反馈错误信息给语言模型。这使得系统更加健壮,更具有弹性,类似于人类在收到错误反馈后会调整方法。
以下展示了带有重试功能的基础Text2Cypher可视化工作流。
简单Text2Cypher的重试逻辑
我们来看看 ExecuteCypherEvent
。
@step
async def execute_query(
self, ctx: Context, ev: ExecuteCypherEvent
) -> SummarizeEvent | CorrectCypherEvent:
# 获取全局变量
retries = await ctx.get("retries")
try:
database_output = str(graph_store.structured_query(ev.cypher))
except Exception as e:
database_output = str(e)
# 要重试了
if retries < self.max_retries:
await ctx.set("retries", retries + 1)
return CorrectCypherEvent(
question=ev.question, cypher=ev.cypher, error=database_output
)
return SummarizeEvent(
question=ev.question, cypher=ev.cypher, context=database_output
)
执行查询函数首先尝试执行查询,如果成功,它会将结果传递给汇总步骤。然而,如果遇到问题,它并不会立即放弃——而是检查是否还有重试次数,如果有,它会重新尝试运行查询,并附上错误信息。这构建了一个更加宽容的系统,可以从错误中学习,就像我们根据反馈调整方法一样。你可以查看整个代码和提示。
简易的Text2Cypher重试机制与评估流程在基于简单的Text2Cypher重试流程的基础上,这个增强版本不仅增加了评估阶段,确保查询结果可以充分回答用户的问题,而不是简单地检查结果是否足够。如果结果被认为不够充分,系统将查询回退,并给出改进建议。如果结果令人满意,流程将进入最终的总结步骤。这一额外的验证层进一步增强了流程的健壮性,确保用户最终获得尽可能准确和完整的答案。
简单的 Text2Cypher 方法 重试和评估流程
这个额外的评估步骤是这样做的:
@step
async def evaluate_context(
self, ctx: Context, ev: EvaluateEvent
) -> SummarizeEvent | CorrectCypherEvent:
# 获取全局变量
retries = await ctx.get("retries")
evaluation = await evaluate_database_output_step(
self.llm, ev.question, ev.cypher, ev.context
)
if retries < self.max_retries and evaluation != "Ok":
await ctx.set("retries", retries + 1)
return CorrectCypherEvent(
question=ev.question, cypher=ev.cypher, error=evaluation
)
return SummarizeEvent(
question=ev.question, cypher=ev.cypher, context=ev.context
)
evaluate_check
函数是用来简单检查查询结果是否充分满足用户需求。如果评估结果表明结果不足且还有重试机会的话,则返回一个CorrectCypherEvent
以便优化查询。否则,则继续进行SummarizeEvent
,表明结果适合最终汇总。
后来我意识到,捕捉那些由于修正无效的Cypher语句而成功自我修复的实例是个好主意。这些实例可以被用作动态的少量样本示例,以帮助未来的Cypher生成。这种方法不仅使代理能够自我修复,还能不断自我学习并随着时间推移而自我提升。用于存储这些示例的示例代码仅在此流程中实现,因为它提供了最佳的自我修复准确度。
@步骤定义
async def summarize_answer(self, ctx: Context, ev: SummarizeEvent) -> StopEvent:
重试次数 = await ctx.get("retries")
# 如果重试成功了:
if 重试次数 > 0 and check_ok(ev.evaluation):
# 打印(f"学到新示例: {ev.question}, {ev.cypher}")
# 将成功的重试存储为fewshots示例.
store_fewshot_example(ev.question, ev.cypher, self.llm的模型)
迭代规划过程
最后一步是最复杂的,巧的是,这是我最初野心勃勃设计的。我一直保留这个代码,你可以从我这学到东西。
迭代规划流程通过引入一个迭代规划系统,采用更复杂的方法。它不是直接生成Cypher查询,而是规划一系列子查询,在执行前验证每个子查询的Cypher语句,并包含一个信息核查机制,如果初始结果不充分,该机制可以修改计划。该系统最多进行三次信息收集迭代,每次根据之前的成果来优化其方法。这创建了一个更为全面的问题回答系统,能够通过将复杂查询拆解为可管理的步骤,并在每个阶段验证信息的准确性,来处理复杂的查询。
以下展示了可视化的迭代规划流程。
迭代规划过程
让我们来检查查询规划器的提示。一开始,我相当雄心勃勃。我预计这些模型会给出如下回复:
class SubqueriesOutput(BaseModel):
"""定义将问题转换为并行优化检索步骤的输出格式。"""
plan: List[List[str]] = Field(
description=(
"""包含多个查询组,每个组:
- 包含可以并行执行的查询请求
- 按依赖关系排序(前面的组必须先于后面的组执行)
- 每个查询必须是特定的信息检索请求
- 如果中间结果的返回值不超过25,则拆分成多个步骤
- 不涉及推理或比较任务,仅包含数据获取查询"""
)
)
输出是一个将复杂问题拆分成一系列可并行执行的查询步骤的结构化计划。每个步骤包含一组可以在并行执行的查询任务,后续步骤依赖于之前步骤的结果。查询仅用于信息检索,不涉及推理任务,并根据需要将查询拆分为更小的步骤来管理结果大小。例如,这个计划首先并行列出两位演员的电影,然后基于第一步的结果找出票房最高的电影。
plan = [
# 2 步平行执行
[
"列出汤姆·汉克斯在 2000年代拍摄的所有电影。",
"列出汤姆·克鲁斯在 2000年代拍摄的所有电影。",
],
# 第二步
["在第一步的结果中找出最赚钱的电影"],
]
这个想法无疑很酷。它是一种聪明的方法,可以把复杂的问题分解成更小、更可操作的步骤,甚至可以利用并行处理来优化检索。听起来这种策略真的可以大大加快速度。但在实际操作中,期望大语言模型可靠地执行这一点有点过于乐观。虽然并行处理理论上高效,但在实践中却引入了很多复杂性。依赖关系、中间结果和保持并行步骤间的逻辑一致性很容易让即使是高级模型也难以应对。顺序执行或许不那么吸引人,但现在它更可靠,显著减少了模型的认知负担。
此外,LLM在处理嵌套列表这样的结构化输出时经常会遇到困难,特别是在理解步骤间的依赖性时。这里,我仅仅依靠提示(不使用工具输出)看看能否改善模型在这些任务上的表现。
看看下面的内容中的迭代规划流程的代码:```(https://github.com/tomasonjo-labs/text2cypher_llama_agent/blob/main/app/workflows/iterative_planner.py)
基准创建一个用于评估文本到ypher代理的基准数据集,感觉像是在LlamaIndex工作流架构中向前迈出的一大步,令人兴奋。
我们想要找一个替代方案,来解决传统一次性的Cypher执行指标(如ExactMatch)经常无法充分反映潜力的问题。在这些流程中,使用多个步骤来优化查询并检索相关信息,这使得单步骤执行指标显得不够充分。
那就是为什么我们选择使用_ragas_中的_answerrelevancy——它更符合我们想要衡量的标准。在这里,我们使用大语言模型生成答案,然后用它作为评判标准来与真实结果进行比较。我们准备了一个包含约50个样本的定制数据集,精心设计以避免生成过于庞大的输出(指过于庞大或详细的数据库结果)。这样的输出会使大语言模型难以有效评估相关性,因此保持结果简短有助于公平而集中地比较单步和多步工作流。
这是结果。
基准测试结果
Claude 3.5 Sonnet、Deepseek-V3 和 GPT-4o 在答案的相关性得分上都超过了0.80,表现最佳。NaiveText2CypherRetryCheckFlow 总体上产生最高的相关性,而 IterativePlanningFlow 的排名则相对较低,最低时仅为 0.163。
尽管OpenAI o1模型相当准确,但由于多次超时(设置为90秒),它可能不是顶尖的。Deepseek-V3尤其令人期待,因为它不仅得分高,而且延迟低。总的来说,这些结果表明,不仅原始准确性,实际部署场景中的稳定性和速度也同样重要。
看一下一张表,可以轻松查看各流量间的提升情况。
基准测试结果
文本3.5从NaiveText2CypherFlow的0.596分逐步上升到NaiveText2CypherRetryFlow的0.616分,然后大幅增长至NaiveText2CypherRetryCheckFlow的0.843分。GPT-4o的整体表现相似,从NaiveText2CypherFlow的0.622分略有下降至NaiveText2CypherRetryFlow的0.603分,但随后显著上升至NaiveText2CypherRetryCheckFlow的0.837分。可以看出,增加重试机制和最终验证步骤显著提高了答案的相关度。
可以看看这个基准代码(benchmark代码)。
请注意,基准测试结果可能相差多达5%左右,这意味着您可能会观察到在不同运行中略有不同的结果和最佳表现者的变化
学习成果与应用到生产中这是一个为期两个月的项目,在此期间我学到了很多东西。一个亮点是我们在测试基准中达到了84%的匹配度,这是一个显著的成果。然而,这是否意味着在实际应用中你也能达到84%的准确率呢?恐怕未必。
生产环境带来了自己的一套挑战——真实世界的数据通常更为杂乱无章、多样化且结构化程度较低。还有个我们在实际应用和用户中会遇到的需求尚未讨论,那就是生产就绪步骤的必要性。这意味着不仅仅要专注于控制测试中的高准确率,还要确保系统在实际环境中可靠、灵活且能稳定输出。
在这种情况下,你需要设置一些保护措施来阻止无关问题进入Text2Cypher管道。
不相关的问题
我们有一个示例 guardrails 实现。除了简单地将无关的问题重定向外,初始的 guardrails 步骤还可以通过引导用户提问的方向、展示可用的工具,并演示如何有效地使用它们来帮助用户更好地了解这些问题。
在以下示例中,我们强调了添加一个将用户输入映射到数据库值的过程的重要性。这一步骤对于确保用户提供的信息与数据库模式一致至关重要,从而实现准确的查询执行,并减少因数据不匹配或模糊不清导致的错误。
将值关联到数据库上
这是一个例子,其中用户寻找“科幻”电影。问题在于数据库中该类型存储为“Sci-Fi”,这使得查询结果为空。
经常被忽略的是NULL值的存在。在现实世界的数据中,NULL值很常见,必须予以处理,特别是在执行排序等操作时。如果不妥善处理这些NULL值,可能引发意想不到的问题或错误。
如何应对NULL值
在这个例子中,我们得到一部随机的电影,其评分为Null
。为解决这个问题,查询需要有一个额外的条件,如WHERE
语句所示:WHERE m.imdbRating IS NOT NULL
。
也有一些情况下,缺失的信息不仅是一个数据问题,而是模式的局限性。例如,如果我们查询获得奥斯卡奖项的电影,但模式中没有任何关于奖项的信息,查询将无法提供所需的答案。
缺失数据
由于 LLM 是训练来取悦用户的,LLM 仍然会提供符合预定义模式但无效的内容。目前还不清楚如何更好地处理这样的情况。
最后要提到的是查询计划。我用这个计划回答了问题。
谁在21世纪初拍的电影更多,汤姆·汉克斯和汤姆·克鲁斯?那么找到胜出的一方的最赚钱的电影是哪部?
计划如下:
plan = [
# 并行的两步
[
"列出汤姆·汉克斯在 2000 年代的所有电影。",
"列出汤姆·克鲁斯在 2000 年代的所有电影。",
],
# 第二步
["找到第一步中利润最高的电影。"],
]
它看起来很厉害,但实际上Cypher非常灵活多变,GPT-4o可以用一个查询来搞定。
我觉得这种情况用并行处理有点小题大做。如果你处理的是确实需要查询规划器的复杂查询类型,可以包含一个查询规划器。但是记住,许多多步查询可以用单个Cypher语句高效处理。
这个例子突出了一个不同的问题:最终答案模棱两可,因为LLM仅被提供了有限的信息。具体来说,是关于《世界大战》的获胜者,汤姆·克鲁斯(《世界大战》的获胜者)。在这种情况下,数据库中已经完成了有关的推理工作,因此LLM无需自行处理这些逻辑。然而,LLMs通常会以这种方式操作,强调了提供完整上下文的重要性,以确保LLM的响应既准确又不模棱两可。
最后,你还要考虑如何应对那些产生大量答案的问题。
返回了很多结果
在我们的实现中,我们对结果的记录数量设置了硬性限制,最多为100条记录。虽然这有助于数据管理,但在某些情况下仍可能显得过多,并且可能在大型语言模型的推理过程中造成误导。
此外,并非此博客中提到的所有智能代理都具有对话功能。你可能需要在一开始加入一个重写问题的环节,以使它们变得对话化,或者这也可以是限制步骤的一部分。如果你有一个庞大的图谱,无法在提示中完全包含,你必须制定一个动态加载相关图谱的系统。
正式上线前要注意的地方很多。
摘要代理其实非常有用,但最好从简单开始,避免一开始就陷入过于复杂的实现中。专注于建立一个坚实的基础作为评价标准,以便有效地评估和比较不同的架构。在处理工具输出方面,考虑尽量减少使用或仅使用最简单的工具,因为许多代理难以有效处理工具输出,通常需要手动解析这些输出。
我很想看看你实现的一些方案!你可以把项目连接到你的Neo4j数据库并开始试验。
GitHub - tomasonjo-labs/text2cypher_llama_agent: 一组由 LlamaIndex 工作流驱动的代理,旨在将自然语言转换为 Cypher 查询……github.com网页界面:
Text2Cypher Llama 代理 由 LlamaIndex 工作流驱动的一组代理,能够将自然语言转换成 Cypher - text2cypher-llama-agent.up.railway.app 点击这里访问共同学习,写下你的评论
评论加载中...
作者其他优质文章