这个标题通俗易懂,同时也符合中文的口语表达习惯。它准确地反映了文章的核心内容,即通过构建语义层来提高大型语言模型与图数据库之间的交互效果。
为LLM代理提供一套强大的工具,让它能与图数据库互动知识图谱提供了一种很好的数据表示方式,支持灵活的数据模式,可以存储结构化和非结构化信息。您可以从Neo4j这样的图数据库中检索信息。虽然这种方法提供了很好的灵活性,但是基础大语言模型在生成精确的Cypher语句时仍然存在不稳定和不一致的问题。因此,我们需要寻找替代方案以确保一致性和稳健性。如果大语言模型不是生成Cypher语句,而是从用户输入中提取参数,并根据用户意图使用预定义的功能或Cypher模板会如何?简而言之,您可以为大语言模型提供一组预定义的工具和基于用户输入何时以及如何使用它们的指南,这也就是所谓的语义层。
语义层是一种中间技术,它提供更准确和更稳健的方式,使大规模语言模型与知识图谱互动。作者提供的图片。灵感来源于这张图片,该图片展示了语义层如何作为AI驱动数据体验的支撑结构。
语义层包含各种工具,这些工具可以暴露给大型语言模型(LLM),以便它们能与知识图谱互动。这些工具的复杂程度各异。可以将语义层中的每个工具视为一个函数。比如,下面就是一个例子。
def get_information(entity: str, type: str) -> str:
candidates = get_candidates(entity, type)
if not candidates:
return "关于这部电影或人物,数据库中没有找到相关信息"
elif len(candidates) > 1:
newline = "\n"
return (
"需要更多信息来确定您指的是哪个:"
f"{'\n'.join(str(d) for d in candidates)}"
)
data = graph.query(
description_query, params={"candidate": candidates[0]["candidate"]}
)
return data[0]['context']
这些工具可以接受多个输入参数,正如前面的例子所示,这使你可以实现复杂的工具。此外,工作流程不仅可以包含一个数据库查询,还可以包括其他操作,使你能根据需要处理任何边界情况或异常。优点是你可以将可能仅在大部分时间起作用的提示工程问题,转变为每次都能按预定脚本精确运行的代码工程问题。
电影经纪人在这篇博客文章中,我们将展示如何实现一个语义层,以便让LLM代理能够与包含演员、电影及其评分信息的知识图谱进行交互。
如图所示,电影代理系统架构。作者提供图片。
这是从文档中摘取的,也是我自己写的,文档也是我写的。
代理使用多个工具与Neo4j图数据库进行有效互动。
*信息检索工具:检索有关电影或个人的数据,确保代理能够访问最新和最相关的信息。
*推荐工具:根据用户偏好和输入信息推荐电影。
*记忆工具:在知识图中存储用户偏好,以便在多次互动中提供个性化体验。
代理可以使用信息或推荐工具从数据库中检索信息,或者使用记忆工具将用户偏好存储到数据库中。
预定义的功能和工具使代理能够协调复杂的用户体验,引导用户达到特定目标或提供与其当前用户旅程阶段相匹配的定制信息。
这种方法增强了系统的稳健性,通过限制LLM的艺术创作自由度,确保响应更加结构化,符合预设的用户流程,从而提升整体用户体验。
电影代理程序的语义层后端实现已公开提供,并作为一个LangChain 模板提供使用。我用这个模板搭建了一个简单的Streamlit聊天程序。
Streamlit聊天界面插件。作者制作的图片。
代码可在 GitHub 获取。你可以通过设置环境变量并运行以下命令来启动项目。
docker-compose up
# 启动所有容器 (Starts all containers)
图形模型
这张图是根据MovieLens(https://grouplens.org/datasets/movielens/)数据集。它包含有关演员、电影和10万用户对电影的评价的信息。
图示。图片由作者制作。
这个可视化展示了一个包含电影相关人物的知识图谱,这些人物出演或导演过电影,并按电影类型进行了分类。每个电影节点都包含上映日期、电影名称和 IMDb 评分等信息。图谱还包含了用户对电影的评分,我们可以根据这些评分来提供电影推荐。
你可以运行位于根目录下的 ingest.py
脚本来填充图。
现在,我们将定义代理可以用来与知识图谱互动的工具。我们将从信息工具开始,这个工具旨在获取关于演员、导演和电影的相关信息。Python代码如下:
def 获取信息(entity: str, 类型: str) -> str:
# 使用全文索引查找相关电影或人物
候选者 = 获取候选者(entity, 类型)
如果 not 候选者:
返回 "未在数据库中找到与此电影或人物相关的任何信息"
elif len(候选人) > 1:
newline = "\n"
返回 (
"需要更多详细信息,您是指哪个:\n\n"
f"{newline.join(str(d) for d in 候选者)}"
)
数据 = 图查询(
描述查询, params={"candidate": 候选者[0]["candidate"]}
)
返回 数据[0]["context"]
该函数首先通过全文索引查找相关的人或电影。Neo4j中的全文索引底层使用Lucene引擎。它使得基于文本距离的查找得以无缝实现,即使用户拼写错误也能返回结果。如果没有找到相关实体,我们可以直接返回结果。另一方面,如果识别出多个候选对象,我们可以引导代理人向用户提问,以获取更多具体信息。比如说,用户问“谁是John?”。
print(get_information("John", "person"))
# 还需要更多的信息来澄清歧义,请问您是指下面的哪一位吗?
# 以下是几位符合条件的人选:
# {'candidate': 'John Lodge', 'label': 'Person'}
# {'candidate': 'John Warren', 'label': 'Person'}
# {'candidate': 'John Gray', 'label': 'Person'}
在这种情况下,工具告诉代理需要更多信息。通过简单的提示调整,我们可以引导代理向用户提出跟进问题。如果用户提供的信息足够具体,工具就能识别出特定的电影或人物。我们将使用参数化的Cypher语句来获取相关信息。
print(get_information("Keanu Reeves", "person"))
# 类型: 电影人
# 名称: Keanu Reeves
# 年份:
# 参演: 十一月的恋曲, 替补, 强棒, 黑客帝国, 恒常, 比得与泰德的荒唐历险, 街头之王, 湖屋, 反应链, 链锁反应, 小佛, 比得与泰德的荒唐历险, 魔鬼代言人, 意念, 行动代码, 尼古玛尼, 快跑, 明尼苏达情感, 人造妖精, 47浪人, 亨利的罪行, 寂静的鼓手, 疾速追杀, 河之边缘, 太极人, 德古拉, 冲浪者的天堂, 我自己的私人艾达, 扫描阴暗, 霓虹恶魔, 该死的东西, 监视者, 观察者, 礼物
# 导演: 太极人
凭借这些信息,代理人可以回答大多数关于基努·里维斯的问题。
现在,教你如何有效地使用这个工具。幸运的是,借助LangChain,这一切变得简单而高效。首先,我们用Pydantic对象来定义函数的输入参数。
class InformationInput(BaseModel):
entity: str = Field(description="问题中提到的电影或人")
entity_type: str = Field(
description="实体的类型。可选项为 'movie' 或 'person'"
)
在这里,我们描述了实体和实体类型这两个参数都是字符串形式的。实体参数指的是在问题中提到的电影或人物。另一方面,对于实体类型,我们也提供了一些选项。当不同值的数量较少(即低基数)时,我们可以直接向大模型提供可用选项,使其能够使用有效的输入。如前所述,我们使用全文索引来区分不同的电影或人物,因为这些值的数量太多,无法直接在提示中列出。
我们现在把它都结合起来,定义一个信息工具吧。
class InformationTool(BaseTool):
name = "资讯"
description = (
"当你需要了解关于各种演员或电影的信息时非常有用"
)
args_schema: Type[BaseModel] = InformationInput
def _run(
self,
entity: str,
entity_type: str,
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""本方法用于调用该工具."""
return get_information(entity, entity_type)
准确且简洁的工具描述是语义层(semantic layer)的重要组成部分,这样智能代理在需要时才能准确选择相关的工具。
这个推荐工具稍微有点儿复杂。
def recommend_movie(movie: Optional[str] = None, genre: Optional[str] = None) -> str:
"""
基于用户的观看记录和偏好推荐电影。
参数可以指定具体的电影或类型。
返回:
str: 推荐电影的列表,或错误信息。
"""
user_id = get_user_id()
params = {"user_id": user_id, "genre": genre}
if not movie and not genre:
# 尝试根据数据库内的信息推荐电影
response = graph.query(recommendation_query_db_history, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "你能告诉我们你最喜欢的一些电影吗?"
if not movie and genre:
# 推荐用户未曾观看过的该类型中的评分高的电影
response = graph.query(recommendation_query_genre, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "出现了一些问题"
candidates = get_candidates(movie, "movie")
if not candidates:
return "你提到的电影未在数据库中找到"
params["movieTitles"] = [el["candidate"] for el in candidates]
query = recommendation_query_movie(bool(genre))
response = graph.query(query, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "出了一点状况"
首先要注意的是,两个输入参数都是可选的,这意味着我们需要引入能够处理所有可能的输入参数组合的工作流程。为了提供个性化推荐,我们首先获取一个 user_id
,然后将它传递给下游的Cypher推荐语句处理。
像之前一样,我们需要把函数输入交给代理。
class RecommenderInput(BaseModel):
movie: Optional[str] = Field(description="推荐所用的电影")
genre: Optional[str] = Field(
description=(
"推荐所用的类型。可选的类型有:" f"{all_genres}"
)
)
由于只有20种可用的类型存在,我们将这些类型的值作为提示的一部分提供。为了区分电影,我们再次使用全文索引功能。和之前一样,我们用工具定义来告知LLM何时使用这个功能。
class 推荐工具类(BaseTool):
name = "推荐器"
description = "当你需要推荐电影时非常有用"
参数模式 = RecommenderInput
def _run(
self,
movie: Optional[str] = None,
genre: Optional[str] = None,
运行管理器: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""使用工具。推荐电影时使用此方法。"""
return recommend_movie(movie, genre)
到目前为止,我们已经定义了两种工具来从数据库中获取数据。信息流也可以是双向的。例如,如果用户告知代理他们看过这部电影并且可能喜欢它,我们可以将这些信息存储在数据库中,并在推荐时使用这些信息。这时,记忆工具就显得很有用了。
def store_movie_rating(movie: str, rating: int):
user_id = get_user_id()
candidates = get_candidates(movie, "movie")
if not candidates:
return "我们这边好像没有这部电影的信息哦"
response = graph.query(
store_rating_query,
params={"user_id": user_id, "candidates": candidates, "rating": rating},
)
try:
return response[0]["response"]
except Exception as e:
print(e)
return "出了点问题"
class MemoryInput(BaseModel):
movie: str = Field(description="用户喜欢的电影")
rating: int = Field(
description=(
"从1到5的评分,其中1表示非常不喜欢,5表示用户非常喜欢这部电影"
)
)
该记忆辅助工具有两个必填参数项,用于定义电影和其评分。这个工具非常简单。值得注意的是,在测试过程中我发现,最好提供一些特定评分的例子,因为在这一点上,LLM出厂设置可能需要改进。
代理人我们现在用LangChain表达式语言 (LCEL,即LangChain表达式语言)来定义一个代理角色,来创建一个代理。
llm = ChatOpenAI(temperature=0, model="gpt-4", streaming=True)
tools = [InformationTool(), RecommenderTool(), MemoryTool()]
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是一个乐于助人的助手,可以查找关于电影的信息并推荐它们。如有需要,向用户提问以澄清工具的相关问题。确保后续问题中包含所有需要澄清的选择。仅执行用户明确指定的任务。 ",
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
agent = (
{
"input": lambda x: x["input"],
"chat_history": lambda x: _format_chat_history(x["chat_history"])
if x.get("chat_history")
else [],
"agent_scratchpad": lambda x: format_to_openai_function_messages(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIFunctionsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True).with_types(
input_type=AgentInput, output_type=Output
)
LangChain表达语言使得定义一个代理并公开其所有功能非常方便。我们不会在此深入探讨LCEL语法,因为这超出了本文的讨论范围。
电影代理的后端系统通过LangServe作为API端点公开。
Streamlit聊天应用我们现在只需要,实现一个连接到LangServe API端点的Streamlit应用,就可以开始了。我们来看看用于获取代理的回复的异步函数。
async def 获取代理回复(
输入内容: str, 流处理器: StreamHandler, 聊天记录: Optional[List[Tuple]] = []
):
url = "http://api:8080/movie-agent/"
st.session_state["generated"].append("")
远程运行对象 = RemoteRunnable(url)
async for 数据块 in 远程运行对象.日志流(
{"input": 输入内容, "chat_history": 聊天记录}
):
日志条目 = 数据块.ops[0]
值 = 日志条目.get("value")
if isinstance(值, dict) and isinstance(值.get("steps"), list):
for 步骤记录 in 值.get("steps"):
流处理器.更新状态(步骤记录["action"].log.strip("\n"))
elif isinstance(值, str):
st.session_state["generated"][-1] += 值
流处理器.新分词(值)
get_agent_response
这个函数设计用于与电影代理的 API 进行交互。它将用户的输入和聊天历史发送给 API,并异步处理不同类型响应。该函数通过更新流处理器的状态并向会话状态追加新生成的文本,允许我们实时向用户推送结果。
我们来试试看
电影经纪人上阵。图片来自作者。
生成的电影助手与用户进行了意想不到的良好且有指导性的互动。
总结总之,通过我们开发的电影代理在语言模型与图数据库交互中集成语义层,代表了在提升用户体验和数据交互效率上的重大进步。通过将重点从生成任意的Cypher查询语句转移到使用结构化、预定义的工具和功能,语义层为语言模型的交互带来了更高的精准度和一致性。这种方法不仅简化了从知识图中提取相关信息的过程,而且确保了一个更加以目标为导向,以用户为中心的体验。
语义层充当桥梁,将用户的意图转化为具体且可操作的查询,使语言模型能够准确且可靠地执行这些查询。因此,用户受益于一个不仅能更好地理解用户的查询,还能更轻松且更清晰地引导他们更容易地达成期望的结果的系统。此外,通过将语言模型的响应限制在这些预定义工具的参数范围内,我们减少了不正确的或不相关的输出的风险,从而增强了系统的可信度和可靠性,让用户更加放心。
代码可在这里 GitHub 上找到。
数据集F. Maxwell Harper 和 Joseph A. Konstan. 2015. MovieLens数据集:历史和背景. ACM 交互智能系统 (TiiS) 5, 第4期: 19:1–19:19. https://doi.org/10.1145/2827872
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質文章