Agent 构建 Memory(提示词对话存储)5. ConversationKGMemory(以图形式管理实体之间关系的模块, 版本>1.0和<1.0的区别)

41 阅读7分钟

( 教学 )Agent 构建 Memory(提示词对话存储)5. ConversationKGMemory(以图形式管理实体之间关系的模块, 版本>1.0和<1.0的区别)

ConversationEntityMemory不同,后者是以键值形式为单个实体管理相关信息,而ConversationKGMemory(即“对话知识图谱内存”)则是一个以图形式管理实体之间关系的模块。

它提取并构建了知识三元组(主体-关系-客体),以识别和存储实体之间的复杂关系,并通过图结构实现对实体连接性的探索。

这有助于模型理解不同实体之间的关系,并能够基于复杂的网络和历史背景更好地回应查询。

该类,是之前的版本1.0以前的,版本1.0以后的。我会列出两个版本的使用方式和特点。

对话知识图谱记忆

ConversationKGMemory是一种存储和管理从对话中提取的信息的内存模块,其采用图结构进行存储和管理。 此示例展示了以下关键功能:

  • 存储对话上下文(save_context
  • (参考)获取图中按因果依赖排序的实体名称列表。(get_topological_sort
  • 从当前对话中提取实体(get_current_entities
  • 提取知识三元组(get_knowledge_triplets
  • 读取存储的记忆(load_memory_variables) 以下示例展示了从关于一个设计师的对话中提取实体和关系,并将其以图形格式存储的过程。
from langchain_classic import memory
from langchain_openai import ChatOpenAI
from langchain_community.memory.kg import ConversationKGMemory

from dotenv import load_dotenv
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
load_dotenv()
# 创建ChatOpenAI对象,配置为使用硅基流动API
Qwen2_5_7B_Instruct_llm = ChatOpenAI(
    temperature=0.1,  # 控制输出的随机性和创造性,值越低输出越稳定可预测,值越高输出越有创意但可能偏离预期 (范围: 0.0 ~ 2.0)
    model_name="Qwen/Qwen2.5-7B-Instruct",  # 硅基流动支持的模型名称
    openai_api_key=os.getenv("SILICONFLOW_API_KEY"),  # 从环境变量获取API密钥
    openai_api_base="https://api.siliconflow.cn/v1"  # 硅基流动API的基础URL
)
memory = ConversationKGMemory(llm=Qwen2_5_7B_Instruct_llm, return_messages=True)
memory.save_context(
    {"input": "这是住在板桥的Shelly Kim。"},
    {"output": "你好Shelly,很高兴认识你!你是做什么工作的?"},
)
memory.save_context(
    {"input": "Shelly Kim是我们公司的新设计师。"},
    {
        "output": "太好了!欢迎加入我们的团队。希望你能在这里工作得开心。"
    },
)
(参考) get_topological_sort() → List[str]

你可以使用 get_topological_sort 方法来查看知识图谱中按照拓扑顺序存储的所有实体:

该方法:

  • 使用 NetworkX 库分析知识图谱结构
  • 基于有向边执行拓扑排序
  • 返回按依赖顺序排列的实体列表

排序结果反映了会话中实体之间的关系,展示了它们在知识图谱中是如何连接的。

memory.kg.get_topological_sort()

打印结果

['N shelley Kim', 'Shelly Kim', 'None', '板桥', 'NONE']

get_current_entities(input_string: str) → List[str]

以下是 get_current_entities 方法的工作原理:

1. 实体抽取链的创建

  • 使用 entity_extraction_prompt 模板创建一个 LLMChain
  • 该提示模板用于从对话的最后一行中提取专有名词。

2. 上下文处理

  • 从缓冲区获取最后 k*2 条消息。(默认值:k=2)
  • 使用 human_prefixai_prefix 生成对话历史字符串。

3. 实体抽取

  • 从输入字符串 "Who is Shelly Kim?" 中提取专有名词
  • 主要识别以大写字母开头的词作为专有名词
  • 在本例中,"Shelly Kim" 被提取为一个实体

该方法仅从问题本身提取实体,而之前的对话上下文仅用作参考。

memory.get_current_entities({"input": "谁是Shelly Kim?"})

打印结果

['Shelly Kim']

获取知识三元组(输入字符串:字符串类型) → 列表[知识三元组]

“get_knowledge_triplets” 方法的运作方式如下:

1. 知识三元组提取链

  • 使用“知识三元组提取提示”模板创建一个“语言模型链”。
  • 旨在从给定文本中提取以(主语-关系-宾语)格式呈现的三元组。

2.记忆搜索

  • 从之前存储的对话中查找与“雪莉”相关的信息。
  • 存储的背景信息:
  • “这是住在平友的雪莉·金。”
  • “雪莉·金是我们公司的新设计师。”

3.三重提取

  • 根据检索到的信息生成以下三元组:
  • (希利·金,居住于,潘乔)
  • (希利·金,是,设计师)
  • (希利·金,就职于,我们公司) 这种方法会从所有与特定实体相关的已存储对话内容中提取以“三元组”形式呈现的关系信息。
memory.get_knowledge_triplets({"input": "Shelly"}), "\n", memory.get_knowledge_triplets(
    {"input": "Pangyo"}
), "\n", memory.get_knowledge_triplets(
    {"input": "designer"}
), "\n", memory.get_knowledge_triplets(
    {"input": "Langchain"}
)

加载内存变量(输入参数:字典类型,键为字符串,值为任意类型) → 字典类型(键为字符串,值为任意类型)

“load_memory_variables” 方法的执行步骤如下:

1. 实体提取

  • 从输入“谁是谢莉·金?”中提取出实体(例如“谢莉·金”)
  • 内部使用“get_current_entities”方法。

2.知识检索

  • 会搜索与所提取的实体相关的所有知识三元组。
  • 通过“保存上下文”方法存储的信息将从图中查询获取。

3.信息格式化

  • 将找到的三元组转换为系统消息。
  • 若设置了“return_messages=True”,则会返回一系列消息对象。

这种方法会从存储的知识图谱中检索相关信息,并以结构化格式将其返回,随后这些信息可作为与语言模型进行后续对话时的背景信息。

memory.load_memory_variables({"input": "谁是Shelly Kim?"})

打印结果

{'history': [SystemMessage(content='On Shelly Kim: Shelly Kim lives in 板桥.', additional_kwargs={}, response_metadata={})]}

使用 LCEL 应用知识图谱记忆

让我们通过 LCEL 使用自定义的 ConversationChainConversationKGMemory 来检查对话后的记忆内容

from operator import itemgetter
from langchain_community.memory.kg import ConversationKGMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os
load_dotenv()
Qwen2_5_7B_Instruct_llm = ChatOpenAI(
    temperature=0.1,  # 控制输出的随机性和创造性,值越低输出越稳定可预测,值越高输出越有创意但可能偏离预期 (范围: 0.0 ~ 2.0)
    model_name="Qwen/Qwen2.5-7B-Instruct",  # 硅基流动支持的模型名称
    openai_api_key=os.getenv("SILICONFLOW_API_KEY"),  # 从环境变量获取API密钥
    openai_api_base="https://api.siliconflow.cn/v1"  # 硅基流动API的基础URL
)
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """这是一个人类和AI之间的友好对话。
                AI会健谈并从上下文中提供大量具体细节。
                如果AI不知道问题的答案,它会诚实地说不知道。
                AI只使用"相关信息"部分中包含的信息,不会产生虚构的内容。

            Relevant Information:{history}""",
        ),
        MessagesPlaceholder(variable_name="history"),
        ("human", "{input}"),
    ]
)
memory = ConversationKGMemory(llm=Qwen2_5_7B_Instruct_llm, return_messages=True, memory_key="history")

class ConversationChain:
    def __init__(self, llm, prompt, memory):
        self.memory = memory
        self.chain =(
            RunnablePassthrough()|
            RunnablePassthrough.assign(
                history=RunnableLambda(self.memory.load_memory_variables) 
                | itemgetter("history"))
            | prompt
            | llm
        )
    
    def invoke(self,input_dict):
        response = self.chain.invoke(input_dict)
        self.memory.save_context(input_dict, {"output": response.content})
        return response
    

conversation_with_kg = ConversationChain(
    llm=Qwen2_5_7B_Instruct_llm,
    prompt=prompt, 
    memory=memory
    )

from langchain_core.load.load import loads

# 将类JSON字符串反序列化为LangChain对象
secrets_map = {"OPENAI_API_KEY": os.getenv("SILICONFLOW_API_KEY")}

deserialized_llm = loads(serialized_llm,secrets_map=secrets_map)
print(deserialized_llm)
print(type(deserialized_llm))

deserialized_prompt = loads(serialized_prompt)
print(type(deserialized_prompt))

deserialized_chain = loads(serialized_chain,secrets_map=secrets_map)
print(type(deserialized_chain))

# 用一些基本信息来初始化对话。

response = conversation_with_kg.invoke(
    {
        "input": "My name is Teddy. Shelly is a coworker of mine, and she's a new designer at our company."
    }
)
# 查询记忆中关于 Shelly 的信息。
conversation_with_kg.memory.load_memory_variables({"input": "who is Shelly?"})

# 可以通过 ```memory.clear()``` 来重置内存。

conversation_with_kg.memory.clear()
conversation_with_kg.memory.load_memory_variables({"input": "谁是 Shelly?"})

使用最新版本逻辑实现

状态定义

我们定义了一个自定义状态 KGState,继承自 MessagesState,并添加了 kg_tripleskg_summary 字段。

  • kg_triples:用于存储从对话中提取的知识三元组列表。
  • kg_summary:用于存储对知识图谱的总结。
from typing import Dict, TypedDict
from langgraph.graph import MessagesState, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from neo4j import GraphDatabase
from langchain_core.messages import HumanMessage

class KGState(MessagesState):
    kg_triples: list = []  # [(subject, relation, object)]
    kg_summary: str = ""   # 图谱总结

# Neo4j 连接(可选)
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))

def extract_triples(state: KGState):
    """提取三元组节点"""
    model = ChatOpenAI(model="gpt-4o-mini")
    prompt = f"""
    从对话中提取知识三元组 (主体, 关系, 客体),JSON 格式:
    [
      ["Bob", "有弟弟", "Tom"],
      ["Bob", "住在", "上海"]
    ]
    
    对话:{state["messages"][-1].content}
    """
    response = model.invoke([("user", prompt)])
    
    # 解析三元组(生产用 JSON)
    triples = eval(response.content)
    state["kg_triples"].extend(triples)
    return state

def chat_with_kg(state: KGState):
    """知识图谱增强聊天"""
    model = ChatOpenAI(model="gpt-4o-mini")
    
    # 构建 KG 上下文
    kg_context = "\n".join([f"{s} {r} {o}" for s, r, o in state["kg_triples"][-10:]])
    
    prompt = f"""
    已知知识图谱:
    {kg_context}
    
    对话历史:{state["messages"]}
    
    使用知识图谱回答:
    """
    
    response = model.invoke([("user", prompt)])
    return {"messages": [response]}

# 构建图
checkpointer = MemorySaver()
builder = StateGraph(state_schema=KGState)
builder.add_node("extract_triples", extract_triples)
builder.add_node("chat", chat_with_kg)
builder.add_edge("extract_triples", "chat")

graph = builder.compile(checkpointer=checkpointer)

# 测试
config = {"configurable": {"thread_id": "user-1"}}
result = graph.invoke(
    {"messages": [HumanMessage(content="我叫Bob,有弟弟Tom,住在上海")]}, 
    config
)
print(result["messages"][-1].content)

print("知识图谱:", result["kg_triples"])