客户体验中的AI:零售业示例

利用图驱动的RAG(GraphRAG)改善客户在整个旅程中多个触点上的体验

  • 发现: 生成个性化营销和电子邮件内容

  • 搜索: 根据语义相似性提供定制结果

  • 推荐: 提供目标产品建议

  • 支持: 提供符合规范的AI脚本以协助客户

本简短指南将介绍如何使用 Neo4j、LangChain(带 LangServe)和 OpenAI 设置一个全栈 GraphRAG 应用程序,以演示上述所有功能。该应用程序以零售业为例,使用 H&M 个性化时尚推荐数据集,这是一个真实的客户购买数据样本,其中包含产品的丰富信息,包括名称、类型、描述、部门等。所有代码都可以在 GitHub 仓库中找到。

ai cust exp architecture

运行应用程序

先决条件

设置

克隆仓库

git clone https://github.com/neo4j-product-examples/graphrag-customer-experience.git

创建一个包含以下内容的 .env 文件。填写您的 OpenAI 密钥。您可以直接使用我们预加载的演示数据库开始,只需复制下面的 Neo4j URI、密码、用户名和数据库凭据。或者,如果您感兴趣,GitHub 仓库中提供了从源数据创建自己数据库的说明。

#Neo4j Database
NEO4J_URI=neo4j+s://b5d4f951.databases.neo4j.io
NEO4J_USERNAME=retail
NEO4J_PASSWORD=pleaseletmein
NEO4J_DATABASE=neo4j

#OpenAI
OPENAI_API_KEY=sk-...

#Other
# Used by UI to navigate between pages. Only change when hosting remotely
ADVERTISED_ADDRESS="http://localhost"

运行

要启动应用程序,请运行以下命令

docker-compose up

在更改环境变量或代码后,要启动并重建,请运行

docker-compose up --build

要停止应用程序,请运行

docker-compose down

在浏览器中打开 http://localhost:8501 与应用程序进行交互。

每页的工作原理

要了解每个页面如何工作,首先了解所使用的图数据模型会有所帮助。对于发现、搜索和推荐页面,我们使用以下模型,该模型包含客户购买交易以及产品的文本嵌入,以支持向量搜索。

ai cust exp product data model

对于支持页面,数据模型如下图所示,其中 <Entity> 和 <RELATES_TO> 涵盖了从支持文档中提取的各种实体。

ai cust exp support data model

数据加载的说明与详细信息可以在 此处 找到。它需要结构化数据加载、嵌入和命名实体识别 (NER) 的组合。

搜索页面

一旦您了解搜索页面的工作原理,就更容易理解发现与推荐页面。搜索页面首先使用 LangChain 检索器对产品节点执行向量搜索。然后,它将基于图遍历执行重新排名和过滤,以个性化响应。下面是其流程图。

ai cust exp app search flow

图遍历通过使用共享购买模式来纳入客户偏好,从而改善结果。其逻辑类似于协同过滤。本质上,它检查哪些客户购买了与当前用户相似的产品,然后查看该群体大量购买的其他产品,并与向量搜索候选进行交叉引用。

这种方法在向量搜索产生大量、过于宽泛的结果时特别有用。例如,考虑客户搜索提示“牛仔裤”,如果我们单独使用向量搜索,我们将得到许多余弦相似度得分高达90%以上的结果。下面是使用 LangChain 检索 100 个结果的示例。

*输入[1]:*

from langchain_neo4j import Neo4jVector
from langchain_openai import OpenAIEmbeddings
import pandas as pd

search_prompt = 'denim jeans'

embedding_model = OpenAIEmbeddings()

# define retriever
vector_only_search = Neo4jVector.from_existing_index(
   embedding=embedding_model,
   url=NEO4J_URI,
   username=NEO4J_USERNAME,
   password=NEO4J_PASSWORD,
   index_name='product_text_embeddings',
   retrieval_query="""
   WITH node AS product, score
   RETURN product.productCode AS productCode,
       product.text AS text, score,
       {score:score, productCode: product.productCode} AS metadata
       ORDER BY score DESC
       """)

# similarity search
res = vector_only_search.similarity_search(search_prompt, k=100)

# visualize as a dataframe
vector_only_df = pd.DataFrame([{'productCode': d.metadata['productCode'],
                               'document': d.page_content,
                               'score': d.metadata['score']} for d in res])
vector_only_df

*输出[1]:*

productCode document score

0

252298

##产品\n名称:Didi 牛仔\n类型:长裤\n组别:下身服饰\n服装类型:连衣裙…​

0.938463

1

598423

##产品\n名称:Night 牛仔\n类型:长裤\n组别:下身服饰\n服装类型:连衣裙…​

0.936840

2

727804

##产品\n名称:Didi HW 紧身牛仔\n类型:长裤\n组别:下身服饰\n服装类型:…​

0.934703

…​

…​

…​

…​

97

663133

##产品\n名称:宽松紧身裤\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.922477

98

820827

##产品\n名称:Jade HW 紧身纽扣牛仔\n类型:长裤\n组别:下身服饰\n服装类…​

0.922452

99

309864

##产品\n名称:Skinny Cheapo 89\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.922402

100 行 × 3 列

然而,如果我们引入图模式以根据共享购买历史来过滤结果,如下面代码所示,我们可以获得高度差异化的分数,这些分数基于具有相似购买模式的客户的购买计数。

*输入[2]:*

# define retriever
kg_search = Neo4jVector.from_existing_index(
   embedding=embedding_model,
   url=NEO4J_URI,
   username=NEO4J_USERNAME,
   password=NEO4J_PASSWORD,
   index_name='product_text_embeddings',
   retrieval_query="""
   WITH node AS product, score AS vectorScore

   OPTIONAL MATCH(product)<-[:VARIANT_OF]-(:Article)<-[:PURCHASED]-(:Customer)
   -[:PURCHASED]->(a:Article)<-[:PURCHASED]-(:Customer {customerId: $customerId})

   WITH count(a) AS graphScore,
       product.text AS text,
       vectorScore,
       product.productCode AS productCode
   RETURN text,
       (1+graphScore)*vectorScore AS score,
       {productCode: productCode,
           graphScore:graphScore,
           vectorScore:vectorScore} AS metadata
   ORDER BY graphScore DESC, vectorScore DESC LIMIT 15
   """)


# similarity search (with personalized graph pattern)
CUSTOMER_ID = "daae10780ecd14990ea190a1e9917da33fe96cd8cfa5e80b67b4600171aa77e0"
kg_res = kg_search.similarity_search(search_prompt,
                                    k=100,
                                    params={'customerId': CUSTOMER_ID})

# visualize as a dataframe
vector_kg_df = pd.DataFrame([{'productCode': d.metadata['productCode'],
              'document': d.page_content,
              'vectorScore': d.metadata['vectorScore'],
              'graphScore': d.metadata['graphScore']} for d in kg_res])
vector_kg_df

*输出[2]:*

productCode document vectorScore graphScore

0

670698

##产品\n名称:Rachel HW 牛仔 TRS\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.922642

22

1

706016

##产品\n名称:Jade HW 紧身牛仔 TRS\n类型:长裤\n组别:下身服饰\n服装类…​

0.926760

11

2

777038

##产品\n名称:Bono NW 修身牛仔\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.926300

8

…​

…​

…​

…​

…​

12

598423

##产品\n名称:Night 牛仔\n类型:长裤\n组别:下身服饰\n服装类型:连衣裙…​

0.936840

0

13

727804

##产品\n名称:Didi HW 紧身牛仔\n类型:长裤\n组别:下身服饰\n服装类型:…​

0.934703

0

14

652924

##产品\n名称:&DENIM 紧身牛仔裤 HW\n类型:长裤\n组别:下身服饰\n服装类…​

0.934462

0

15 行 × 4 列

将向量和知识图谱个性化结果合并,我们可以看到重新排名的重要性,以及如何使每个客户的搜索结果更具针对性与个性化。同样的模式可以在其他知识库上重复,以进行重新排名和过滤,从而提高搜索相关性。我们通常将这些模式称为“图过滤”。

*输入[3]:*

#merge and compare
(vector_only_df
.reset_index(names='vectorRank')[['productCode', 'vectorRank']]
.merge(vector_kg_df.reset_index(names='graphRank'),
       on='productCode', how='right')
)

*输出[3]:*

productCode vectorRank graphRank document vectorScore graphScore

0

670698

95

0

##产品\n名称:Rachel HW 牛仔 TRS\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.922642

22

1

706016

41

1

##产品\n名称:Jade HW 紧身牛仔 TRS\n类型:长裤\n组别:下身服饰\n服装类…​

0.926760

11

2

777038

47

2

##产品\n名称:Bono NW 修身牛仔\n类型:长裤\n组别:下身服饰\n服装类型:长裤…​

0.926300

8

…​

…​

…​

…​

…​

…​

…​

12

598423

1

12

##产品\n名称:Night 牛仔\n类型:长裤\n组别:下身服饰\n服装类型:连衣裙…​

0.936840

0

13

727804

2

13

##产品\n名称:Didi HW 紧身牛仔\n类型:长裤\n组别:下身服饰\n服装类型:…​

0.934703

0

14

652924

3

14

##产品\n名称:&DENIM 紧身牛仔裤 HW\n类型:长裤\n组别:下身服饰\n服装类…​

0.934462

0

15 行 × 6 列

发现页面

发现页面使用与搜索页面相同的检索查询,但其位于一个完整的 LLM 链中,其中检索到的结果提供给 LLM,以根据季节/时间等其他参数的上下文生成电子邮件。请参见下面的流程图。

ai cust exp app discovery flow

链本身看起来是这样的

content_chain = (
RunnableParallel(
{
'context': (lambda x: (x['customer_interests'], x['customer_id'])) | RunnableLambda(retriever),
'customerName': (lambda x: x['customer_name']),
'customerInterests': (lambda x: x['customer_interests']),
'timeofYear': (lambda x: x['time_of_year']),
})
| prompt
| llm
| StrOutputParser())

本质上,搜索上下文和用户 ID 会传递给图过滤检索器以获取产品候选,而客户姓名、兴趣和年份时间等其他详细信息则传递给 LLM,以帮助决定电子邮件中包含哪些内容和产品。

使用下面的 LLM 提示模板。请注意,LLM 被赋予一定的自由度,可以根据年份时间和 LLM 的通用“时尚知识”来选择商品。这是利用语言理解根据客户上下文进一步优化推荐的一个很好的例子,而传统的基于内存和模型的推荐系统单独使用时可能难以做到这一点。

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template("""
You are a personal assistant named Sally for a fashion, home, and beauty company called HRM.
write an email to {customerName}, one of your customers, to recommend and summarize products based on:
- the current season / time of year: {timeofYear}
- Their recent searches & interests: {customerInterests}

Please only mention the products listed in the context below. Do not come up with or add any new products to the list.
The below candidates are recommended based on the purchase patterns of other customers in the HRM database.
Select the best 4 to 5 product subset from the context that best match
the time of year: {timeofYear} and the customers interests.
Each product comes with an https `url` field.
Make sure to provide that https url with descriptive name text in markdown for each product.

# Context:
{context}
""")

推荐页面

推荐页面生成一条 AI 消息,其中包含时尚推荐,包括与近期购买/当前查看的商品和搜索提示搭配良好的产品。它使用一种基于图嵌入(也称为“图向量”)的不同类型的检索器。在此应用程序中,图向量有助于根据常见的购买模式查找产品,即哪些产品经常一起购买或由同一客户购买?这种方法通过专注于与他人良好搭配并可能一起购买的产品来增强时尚建议,而不仅仅是根据产品描述或搜索提示返回类似商品。下面是使用文本上的基线向量搜索与使用图向量的 GraphRAG 进行检索的并排比较示例。

ai cust exp recommendations page

图嵌入

图嵌入的总体概念与文本嵌入相似,只不过图嵌入是在向量空间中表示图组件,而不是在向量空间中表示文本以用于机器学习与搜索任务。当您想根据图结构中的相似性搜索事物时,这特别有用。即,哪些节点连接相对良好或在图中扮演相似的角色?或者哪些子图彼此看起来相似?

图嵌入有多种类型;在此示例中,我们使用节点嵌入。节点嵌入创建节点的向量表示,这些向量表示捕获它们在图中的位置及其周围的邻域结构。下面是 2D 投影,显示图中连接良好的节点在嵌入空间中如何紧密聚类。

node embedding explainer

我们使用 Neo4j 图数据科学 (GDS) 来创建这些节点嵌入,特别是 快速随机投影 (FastRP),这是一种高度可扩展的节点嵌入算法,它使用矩阵代数和统计采样来快速嵌入大型图。如果您感兴趣,可以在此笔记本的“创建和分析图嵌入”部分中查看用于创建嵌入的代码。

推荐 LLM 链

下面是应用程序中推荐页面的工作流程图。用户在搜索页面上点击产品后会导航到此页面。

ai cust exp app rec flow

在这种情况下,检索器仅使用 customer_id 根据图嵌入获取产品候选,而不利用任何其他上下文,例如基于用户搜索提示的 customer_interests。下面是 LangChain 中使用的提示模板。

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template("""
You are a personal assistant named Sally for a fashion, home, and beauty company called HRM.
Your customer, {customerName}, is currently browsing the website.
Please write an engaging message to them recommending and summarizing products that pair well
with their interests and the item they are currently viewing given:
- Item they are currently viewing: {productDescription}
- The current season / time of year: {timeofYear}
- Recent searches: {customerInterests}

Please only mention the product candidates listed in the context below.
Do not come up with or add any new products to the list.
The below candidates are recommended based on the shared purchase patterns of other customers in the HRM database.
Select the best 4 to 5 product subset from the context that best match the time of year: {timeofYear} and to pair
with the current product being viewed and recent searches.
For example, even if scarfs are listed here, they may not be appropriate for a summer time of year so best not to include those.
Each product comes with an http `url` field.
Make sure to provide that http url with descriptive name text in markdown for each product. Do not alter the url.

# Context:
{context}
""")

在这种情况下,LLM 被赋予了相当大的创作权限,可以根据其自身的语言理解和季节性/年份时间来选择和搭配商品。正如我们在发现页面上看到的,这是利用语言理解根据客户上下文过滤推荐的另一个示例,而传统的基于内存和模型的推荐系统单独使用时可能难以做到这一点。

客户支持页面

客户支持页面遵循基本的聊天机器人工作流程,并增加了下图所示的额外知识图谱检索器。前端保存对话历史记录。当用户提出问题时,问题和对话历史记录会发送到 condense_question 链,将其总结为一个单一提示。然后将其发送以检索相关文档和知识图谱实体,这些实体反过来又添加到最终的提示模板中,以生成 LLM 响应并发送回聊天。

ai cust exp app support flow

此页面使用了图的另一部分,与其它部分不同。这部分包含来自文档的文本块以及使用 LLM 知识图谱构建器 从这些文本块中提取的各种知识图谱实体和关系。

提取的实体作为 LLM 在响应时应优先考虑的事实或“规则”。此工作流程专为需要 LLM 遵循细致的领域上下文和逻辑的场景而设计(例如策略或特定业务逻辑)。从文档中提取此信息并将其表示为知识图谱中的实体和关系,可以更高效、准确地将此信息暴露给 LLM。

下面是使用的提示模板。在这种情况下,rules 对应于知识图谱中的事实,additionalContext 对应于文档块。

template = (
"You are an assistant that helps customers with their questions. "
"You work for for XYZBrands, a fashion, home, and beauty company. "
"If you require follow up questions, "
"make sure to ask the user for clarification. Make sure to include any "
"available options that need to be clarified in the follow up questions. "
"Embed url links as sources when made available. "
"Answer the question based only on the below Rules and AdditionalContext. "
"The rules should be respected as absolute fact, never provide advice that contradicts the rules. "
"""
# Rules
{rules}

# AdditionalContext
{additionalContext}

# Question:
{question}

# Answer:
"""
)

该链利用了两个检索器。

# support Q&A chain

prompt = ChatPromptTemplate.from_template(template)

qa_chain = (
   RunnableParallel({
       "vectorStoreResults": condense_question |
           vector_store.as_retriever(search_kwargs={'k': vector_top_k}),
       "question": RunnablePassthrough()})
   | RunnableParallel({
       "rules": (lambda x: x["vectorStoreResults"]) |
           RunnableLambda(retrieve_rules),
       "additionalContext": (lambda x: x["vectorStoreResults"]) |
           RunnableLambda(format_docs),
       "question": lambda x: x["question"]})
   | prompt
   | llm
   | StrOutputParser()
)

vector_store.as_retriever 执行向量搜索以检索文档块。然后,这些块被传递给第二个检索器 retrieve_rules,该检索器获取与文档连接的知识图谱实体。

def retrieve_rules(docs: List[Document]) -> str:
   doc_chunk_ids = [doc.metadata['id'] for doc in docs]
   res = support_graph.query("""
   UNWIND $chunkIds AS chunkId
   MATCH(chunk {id:chunkId})-[:HAS_ENTITY]->()-[rl:!HAS_ENTITY]-{1,5}()
   UNWIND rl AS r
   WITH DISTINCT r
   MATCH (n)-[r]->(m)
   RETURN n.id + ' - ' + type(r) +  ' -> ' + m.id AS rule ORDER BY rule
   """, params={'chunkIds': doc_chunk_ids})
   return '\n'.join([r['rule'] for r in res])

图模式 MATCH(chunk {id:chunkId})-[:HAS_ENTITY]→()-[rl:!HAS_ENTITY]-{1,5}() 翻译为

  1. 匹配文档块节点:MATCH(chunk {id:chunkId})

  2. 向外遍历一跳以查找从块中提取的实体:-[:HAS_ENTITY]→()

  3. 向外搜索最多 5 跳以查找实体之间的连接:-[rl:!HAS_ENTITY]-{1,5}()

  4. 从这些路径中提取所有关系:UNWIND rl AS r WITH DISTINCT r MATCH (n)-[r]→(m)

  5. 将关系文本化为“规则”发送给 LLM:RETURN n.id + ' - ' + type(r) + ' → ' + m.id AS rule ORDER BY rule

以下是知识图谱中的规则示例,这些规则可以被提取并发送给 LLM 以获得更准确的结果。这些规则涉及产品退款和换货政策。

ai cust exp support sugraph
© . All rights reserved.