原标题:在大语言模型结合知识图谱方面,有没有真实可用的案例?
关键字:知识图谱,LLM(大型语言模型)
最佳回答:恰同学
回答字数:12795字
在用大语言模型(LLM)开发对话agent应用时,特别是很多轮的对话场景下,我们往往需要一个外部的存储组件(memory),原因是现在的大模型往往有上下文长度的限制,当对话内容超过限制时,模型会忘记之前的对话信息。当然,现在已经有一些大模型支持100k的上下文长度,相信未来会有更多的大模型支持很大的上下文长度。但是即使支持很大的上下文长度的模型,在连续多轮对话时,还是会发生遗忘现象,或者模型的回答质量会下降。或者大模型会产生“幻觉”,用不可靠的知识作答。有了外部存储,我们可以在多轮对话时,从存储中检索出和当前问题相关的上下文信息,合并到提示词中发送给大模型,或者从相关知识库中,检索出可靠的知识给大模型,让它给出更可靠的答案。
在一个多轮对话的场景下,使用外部存储的整体的过程是:
1. 把每次和大模型交互的对话存储到外部存储。
2. 等下一轮对话时,根据提问,从外部存储中检索出和目标问题相关的信息作为上下文。
3. 把检索到的上下文信息合并到提问的提示词中,一并发给大模型。
上面过程中,最关键的部分是第二部分,从外部存储中检索和当前问题相关的上下文,这将直接影响大模型的回答质量。这里的难点是如何确定memory中的哪些信息是和当前问题相关的,确定这个相关性的逻辑是什么?
一种常见方式是把memory中的信息用词嵌入技术转换为向量存储起来,在对话提问时,提取和问题内容向量相似度最高的片段作为上下文,存储方案用向量数据库实现。但是这种方案有以下局限性:
1.无法捕捉到更深度的关联关系
一般来说,有关联关系的句子之间的向量相似度的确会高一些,这是因为有关联关系的句子中往往存在相同的实体,这些实体可能是句子的主语、谓语等,这些相同的实体在转换为向量之后也具有相同的向量表示或者有更近的“距离”,从而利用向量相似度可以把这些信息捕捉到。但是,当句子之间,存在语法或语义层面的关联关系,但是在句子的字面内容不太接近时,这种关联关系就不容易被捕捉到。
比如有以下历史对话:
用户:请介绍一下诸葛亮?
AI: 诸葛亮字孔明,号卧龙,三国时期蜀汉丞相,中国古代杰出的政治家、军事家、发明家、文学家。
用户:他有哪些文学作品?
AI: 《出师表》《诫子书》等
如果在这段对话之后,如果用户提问“《出师表》的作者是谁?”,如果基于向量相似度,只会检索出和这个问题字面内容接近的信息,如果我们限定,最大只能检索一轮对话的内容,那么,上面历史对话中的后两句话会被检索出来:
用户:他有哪些文学作品?
AI: 《出师表》《诫子书》等
但是仅凭这两句话并不能很准确的直接得出问题的答案,因为在用户的提问语句中,使用了代词“他”,在没有其他信息的情况下,无法得知《出师表》的真正作者是谁。
2. 存在获取信息完整性和准确性的问题
前面我们提到,大模型的上下文窗口是有限制的,那么,从外部存储中检索出来用于合并到提示词中的信息也必须有限制,在用向量存储的解决方案时,为了尽可能多的提供上下文信息,我们只能依靠向量相似度由高到低的排序,获取相关信息,直到达到限制。
但是在检索出的内容片段中,往往存在和当前目标问题不相关的额外的冗余信息,因为检索出的信息中要采纳的内容长度是有限的,这就意味着,冗余信息越多,有用的信息被采纳的概率就越低。
例如有以下对话内容:
用户:唐代有哪些作家,他们的著名作品有哪些?
AI: 唐代是中国文学发展的黄金时期,涌现出许多著名的作家和作品。以下是一些唐代著名作家及其代表作品的例子:
1. 白居易(772年-846年)
代表作品:《长恨歌》、《琵琶行》、《赋得古原草送别》等。
2. 杜甫(712年-770年)
代表作品:《月夜忆舍弟》、《茅屋为秋风所破歌》、《登高》等。
3. 李白(701年-762年)
代表作品:《静夜思》、《将进酒》、《行路难》等。
4. 王之涣(675年-761年)
代表作品:《登鹳雀楼》、《登飞来峰》等。
5. 孟郊(751年-814年)
代表作品:《登科后》、《游子吟》等。
6. 王维(701年-761年)
代表作品:《山居秋暝》、《使至塞上》等。
7. 骆宾王(798年-849年)
代表作品:《敕勒歌》、《乌衣巷》等。
8. 刘禹锡(772年-842年)
代表作品:《陋室铭》、《竹枝词》等。
上述对话是多轮对话中的一轮,假如这一段内容被转换为词向量并存储在向量数据库中,那么,当用户提问李白的代表作品有哪些时,这段内容可能会被检索到,进而这轮对话的全部内容会合并到提示词中,但是实际上,和问题相关的信息只有和李白相关的那两行。因为提示词本身是有限制的,所以,在其他轮对话中可能有与李白相关的内容被排除在外了,这样,外部存储中和目标问题相关的信息其实没有最大限度地被提取出。
另外,基于向量相似度去检索信息还可能有准确性的问题,例如,用户提问这个项目的产品经理是谁?, 可能在外部存储中有大量和产品经理相关的内容,比如产品经理和工程师之间有很多交流内容,这些交流内容中可能会出现很多工程师的名字,如果把这些信息合并到提示词,大模型可能会给出错误的答案。
3. 无法纠正和更新信息
在多轮对话中,有些对话中的概念可能会变化,例如有以下对话:
用户:王维的作品有哪些?
AI: 《山居秋暝》、《使至塞上》《登鹳雀楼》等
用户:你说错了,《登鹳雀楼》不是王维写的,是王之涣写的。
AI: 抱歉,我之前说错了,《登鹳雀楼》确实是王之涣写的。
在上述对话中,AI首先犯了一个错,把《登鹳雀楼》说成了王维写的,随后,用户和AI又产生了一轮对话对这个错误进行了纠正,但是,假如上述对话记录全部被存储到了向量数据库,如果用户提问王维有哪些作品?,向量数据库会把之前AI的错误回答检索到,然后合并到提示词给大模型,然后大模型会返回错误的结果。这个例子说明了,向量数据库作为memory的实现可能无法纠正和更新信息。
针对上述问题,用知识图谱作为外部存储方案可能会更好,下面是一个用知识图谱代替向量数据库作为外部存储的方案:
1. 在每轮对话之后,都提取近期对话中涉及的实体和实体间的关系,提取实体和关系的任务可以利用大语言模型完成。
2. 每轮对话之后,在提取了实体和关系之后,更新知识图谱。
3. 在每轮对话之前,先从用户问题中提取该问题涉及到的实体有哪些,然后从知识图谱中检索和实体相关的知识,这部分知识作为提示词中的上下文信息给大模型。
采用此方案,上面提到的三个问题都会得到一定程度的解决,因为知识图谱保存了不同实体以及实体间的关系,更能提取到深层次的关联关系,知识图谱可以只提取和实体相关的知识,没有太多的冗余信息,知识图谱还可以不断更新实体和关系的信息。
langchain框架中有用知识图谱作为外部存储的实现,下面我们结合源码分析具体的实现方式:
langchain中用于对话的类是ConversationChain,这个类在初始化的时候可以指定具体的大模型llm和存储方案memory, 下面是一个例子:
from langchain.memory import ConversationKGMemory
from langchain.llms import OpenAI
llm = OpenAI(temperature=0)
from langchain.prompts.prompt import PromptTemplate
from langchain.chains import ConversationChain
template = The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context.
If the AI does not know the answer to a question, it truthfully says it does not know. The AI ONLY uses information contained in the Relevant Information section and does not hallucinate.
Relevant Information:
{history}
Conversation:
Human: {input}
AI:
prompt = PromptTemplate(input_variables=[history, input], template=template)
conversation_with_kg = ConversationChain(
llm=llm, verbose=True, prompt=prompt, memory=ConversationKGMemory(llm=llm)
)
可以看到,上面代码中用ConversationKGMemory作为memory, 我们来看下ConversationKGMemory的源码:
#源码位置:libs/langchain/langchain/memory/kg.py
# 这里只展示源码关键信息
class ConversationKGMemory(BaseChatMemory):
Knowledge graph conversation memory.
Integrates with external knowledge graph to store and retrieve
information about knowledge triples in the conversation.
# 这个变量是知识图谱的底层实现
kg: NetworkxEntityGraph = Field(default_factory=NetworkxEntityGraph)
# 在ConversationChain给大模型发送提示词之前,在构造提示词时,会调用这个函数产生memory相关的变量
# memory相关的变量会替换提示词模板中的占位符,也就是把memory中检索的信息放到提示词中
# 从源码可以看到,这部分有以下步骤:
# 1. 从当前输入中提取相关的实体列表 entities = self._get_current_entities(inputs)
# 2. 遍历实体列表,分别提取每个实体的知识 knowledge = self.kg.get_entity_knowledge(entity)
# 3. 把所有实体和实体的知识信息放到context中
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
Return history buffer.
entities = self._get_current_entities(inputs)
summary_strings = []
for entity in entities:
# 和实体相关的知识是通过调用更底层的知识图谱的方法获取的,后面的分析中会说明
knowledge = self.kg.get_entity_knowledge(entity)
if knowledge:
summary = fOn {entity}: {. .join(knowledge)}.
summary_strings.append(summary)
context: Union[str, List]
if not summary_strings:
context = [] if self.return_messages else
elif self.return_messages:
context = [
self.summary_message_cls(content=text) for text in summary_strings
]
else:
context = \n.join(summary_strings)
return {self.memory_key: context}
# 这个方法用于提取输入信息中涉及的所有实体,可以看到这部分是调用大模型实现的
def get_current_entities(self, input_string: str) -> List[str]:
chain = LLMChain(llm=self.llm, prompt=self.entity_extraction_prompt)
buffer_string = get_buffer_string(
self.chat_memory.messages[-self.k * 2 :],
human_prefix=self.human_prefix,
ai_prefix=self.ai_prefix,
)
output = chain.predict(
history=buffer_string,
input=input_string,
)
return get_entities(output)
# 这个方法用于每轮对话时,提取输入中实体间的关系(知识),并且在底层的知识图谱中更新
def _get_and_update_kg(self, inputs: Dict[str, Any]) -> None:
Get and update knowledge graph from the conversation history.
prompt_input_key = self._get_prompt_input_key(inputs)
knowledge = self.get_knowledge_triplets(inputs[prompt_input_key])
for triple in knowledge:
self.kg.add_triple(triple)
ConversationKGMemory的load_memory_variables方法实现了如何从memory中检索出和当前输入相关的实体知识,这部分知识信息会放到最终发送到大模型的提示词中。另外_get_and_update_kg方法实现了在每轮对话时更新知识图谱。
ConversationKGMemory的kg变量为知识图谱的底层实现,包括知识图谱的建立和更新,以及实体知识的提取,下面我们再来看下这个底层实现:
# 源码位置:libs/langchain/langchain/graphs/networkx_graph.py
# 仅展示关键代码
class NetworkxEntityGraph:
Networkx wrapper for entity graph operations.
def __init__(self, graph: Optional[Any] = None) -> None:
Create a new graph.
try:
import networkx as nx
except ImportError:
raise ImportError(
Could not import networkx python package.
Please install it with `pip install networkx`.
)
if graph is not None:
if not isinstance(graph, nx.DiGraph):
raise ValueError(Passed in graph is not of correct shape)
self._graph = graph
else:
self._graph = nx.DiGraph()
# 增加知识(知识是个三元组,比如[ 实体A 关系, 实体B ]),如果图中已经存在实体间的关系,则会覆盖之前的关系
def add_triple(self, knowledge_triple: KnowledgeTriple) -> None:
Add a triple to the graph.
# Creates nodes if they dont exist
# Overwrites existing edges
if not self._graph.has_node(knowledge_triple.subject):
self._graph.add_node(knowledge_triple.subject)
if not self._graph.has_node(knowledge_triple.object_):
self._graph.add_node(knowledge_triple.object_)
self._graph.add_edge(
knowledge_triple.subject,
knowledge_triple.object_,
relation=knowledge_triple.predicate,
)
# 获取实体的相关知识,depth表示要提取的关系深度,默认为1, 更深的关系可以提供更多的信息
def get_entity_knowledge(self, entity: str, depth: int = 1) -> List[str]:
Get information about an entity.
import networkx as nx
# TODO: Have more information-specific retrieval methods
if not self._graph.has_node(entity):
return []
results = []
for src, sink in nx.dfs_edges(self._graph, entity, depth_limit=depth):
relation = self._graph[src][sink][relation]
results.append(f{src} {relation} {sink})
return results
可以看到,这里的底层使用networkx 这个库来实现的,实际上,你也可以用图数据库实现(例如neo4j).
思考:
1.知识图谱作为memory的实现仍然不是完美的,目前来看,解决长上下文最有效的方式依然是把希望寄托到大模型上下文长度的增扩上,如果大模型支持的上下文足够大,也就不需要外部的memory了。也就是说,从一个很长的上下文中提取关联信息这种能力本身应该就应该集成在大模型中。就像人的大脑一样,既可以做推理,又可以存储短时的上下文记忆,推理和存储,我认为应该是一体的。
2.知识图谱是基于符号主义的思想,我认为符号主义和连接主义不是矛盾的,可以相互促进,比如,是否可以利用知识图谱,从长文本中提取知识的关联信息,这些关联信息用于训练基于连接主义思想的神经网络,进而让神经网络在更长的上下文学习中有更高的能力。
联系作者
回答作者:恰同学
评论0