AI 客户体验:零售示例

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

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

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

  • 推荐:提供有针对性的产品建议

  • 支持:为客户服务提供合规的 AI 脚本

本简短指南介绍了如何设置一个完整的 GraphRAG 应用程序,该应用程序使用 Neo4j、LangChain(与 LangServe 结合)和 OpenAI 演示了上述所有内容。该应用程序专注于使用 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 个结果的示例。

*In[1]:*

from langchain.vectorstores.neo4j_vector 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

*Out[1]:*

productCode document score

0

252298

##Product\nName: Didi denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Dresses La…​

0.938463

1

598423

##Product\nName: Night Denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Dresses L…​

0.936840

2

727804

##Product\nName: Didi HW Skinny denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: …​

0.934703

…​

…​

…​

…​

97

663133

##Product\nName: RELAXED SKINNY\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Trouse…​

0.922477

98

820827

##Product\nName: Jade HW Skinny Button dnm\nType: Trousers\nGroup: Garment Lower body\nGarment T…​

0.922452

99

309864

##Product\nName: Skinny Cheapo 89\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Trou…​

0.922402

100 rows × 3 columns

但是,如果我们将图模式合并以帮助根据共享购买历史记录过滤结果(如以下代码所示),我们可以获得高度区分的得分,这些得分基于具有类似购买模式的客户的购买次数。

*In[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

*Out[2]:*

productCode document vectorScore graphScore

0

670698

##Product\nName: Rachel HW Denim TRS\nType: Trousers\nGroup: Garment Lower body\nGarment Type: T…​

0.922642

22

1

706016

##Product\nName: Jade HW Skinny Denim TRS\nType: Trousers\nGroup: Garment Lower body\nGarment Ty…​

0.926760

11

2

777038

##Product\nName: Bono NW slim denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Tr…​

0.926300

8

…​

…​

…​

…​

…​

12

598423

##Product\nName: Night Denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Dresses L…​

0.936840

0

13

727804

##Product\nName: Didi HW Skinny denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: …​

0.934703

0

14

652924

##Product\nName: &DENIM Jeggings HW\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Tr…​

0.934462

0

15 rows × 4 columns

将向量和 kg 个性化结果合并在一起,我们可以看到重新排序的重要性以及我们可以为每个客户做出搜索结果多么专注和个性化。相同的模式可以在其他知识库上重复,以进行重新排序和过滤以提高搜索相关性。我们非正式地称这些模式为“图过滤”。

*In[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')
)

*Out[3]:*

productCode vectorRank graphRank document vectorScore graphScore

0

670698

95

0

##Product\nName: Rachel HW Denim TRS\nType: Trousers\nGroup: Garment Lower body\nGarment Type: T…​

0.922642

22

1

706016

41

1

##Product\nName: Jade HW Skinny Denim TRS\nType: Trousers\nGroup: Garment Lower body\nGarment Ty…​

0.926760

11

2

777038

47

2

##Product\nName: Bono NW slim denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Tr…​

0.926300

8

…​

…​

…​

…​

…​

…​

…​

12

598423

1

12

##Product\nName: Night Denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Dresses L…​

0.936840

0

13

727804

2

13

##Product\nName: Didi HW Skinny denim\nType: Trousers\nGroup: Garment Lower body\nGarment Type: …​

0.934703

0

14

652924

3

14

##Product\nName: &DENIM Jeggings HW\nType: Trousers\nGroup: Garment Lower body\nGarment Type: Tr…​

0.934462

0

15 rows × 6 columns

发现页面

发现页面使用与搜索页面相同的检索查询,但在完整的 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

图嵌入

图嵌入的一般概念类似于文本嵌入,只是它不是在向量空间中表示文本以用于 ML 和搜索任务,而是将图组件表示在向量空间中。当您想要根据图结构中的相似性搜索事物时,这尤其有用。即哪些节点在图中相对连接良好或在图中扮演类似的角色?或者哪些子图看起来相似?

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

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