尽管科技有了巨大的进步,语言障碍在当今世界依然存在。不论是在工作中还是日常生活中,总会碰到由于语言差异而造成尴尬局面的情况。尤其对于那些跨地域、讲不同语言的大型企业团队来说,更是如此。在最近由Cohere for AI研究社区组织的Aya远征活动中,我参与了一个项目,旨在通过开发一种多语言的工作场所智能聊天应用来解决这一语言障碍以及其他工作场所相关的低效问题。
与其多谈产品,我认为最好的方式是直接展示,让大家看到它的实际运行效果。
聊天应用的最终演示
以下系列教程将涵盖该应用的开发过程,内容包括:
- 一个能翻译用户首选语言的智能工作流
- 构建AI助手功能:基于RAG的方法回答问题,随时随地的文档支持,以及智能总结功能
- 通过FastAPI部署智能工作流,并开发与其接口的Web用户界面
高层次框架
=====================
鉴于LangChain及其图形化对应工具LangGraph的流行,我不想把这系列教程变成另一份解释这些包及其方法的入门教程。相反,我希望 更多地关注在实现这些包的过程中所做的设计选择和面临的挑战,因为我觉得这在长远来看更有价值。
LangChain vs LangGraph
我们面对的第一个设计选择是在LangChain和LangGraph之间进行选择。
在一个简单的情景中(如下图所示),每当用户提供的信息被发送给所有其他用户并翻译成他们的首选语言时,LangChain已经足够了。这是一种 单向流动,从用户发送信息开始,到接收用户接收到信息结束:
没有Aya的单向信息流
然而,我们场景中的主要约束是 包括一个名为Aya的AI助手(以远征命名)。Aya计划成为这个聊天应用的重要组成部分,为我们的系统增加了一个新的复杂层次。有了Aya,发送用户的消息需要被分析,根据消息的性质(如果它是针对Aya的指令),系统需要返回一条消息,而这条消息随后需要再次发送给接收用户。
包含Aya的信息流
定义一次运行: 这里另一个相关的设计选择是对一次“运行”或一次“迭代”消息循环的定义。
在我们选择的定义中,我们认为每一次运行是由任何用户发送一条消息触发,并在所有与最初消息相关的消息到达接收用户时结束。
所以,如果是一条不涉及Aya的信息,只是直接发送给其他用户的消息,当所有用户接收到初始翻译消息时,该消息被认为是传输完成。而如果这是一条涉及Aya的消息,则当初始消息和来自Aya的回复都达到所有用户时,该消息才被认为完成传输。
基于这种传输完成定义,我们希望设计一个流程,在终止传输前等待Aya的回复生成并推送给用户。为此,我们使用LangGraph,因为它是专门为解决这种情况而构建的。事实上,通过类似光年AI平台的灵活工作流,我们可以轻松定义和管理这些流程。
这个应用程序的核心是代理及其交互。总体上,我们有两种不同类型的代理:
- 用户代理:附属于每个用户,主要任务是将收到的信息翻译成用户的首选语言
- Aya代理:与Aya关联的各种代理,每个代理都有其特定的角色/任务
用户代理
UserAgent类用于定义将与聊天室中的每个用户关联的代理。UserAgent类实现的一些功能包括:
1. 将收到的信息翻译成用户的首选语言
2. 当用户发送消息时,激活/调用图表
3. 维护聊天历史记录,以帮助提供翻译任务的上下文,实现“上下文感知”的翻译
class UserAgent(object):
def __init__(self, llm, userid, user_language):
self.llm = llm
self.userid = userid
self.user_language = user_language
self.chat_history = []
prompt = ChatPromptTemplate.from_template(USER_SYSTEM_PROMPT2)
self.chain = prompt | llm
def set_graph(self, graph):
self.graph = graph
def send_text(self,text:str, debug = False):
message = ChatMessage(message = HumanMessage(content=text), sender = self.userid)
inputs = {"messages": [message]}
output = self.graph.invoke(inputs, debug = debug)
return output
def display_chat_history(self, content_only = False):
for i in self.chat_history:
if content_only == True:
print(f"{i.sender} : {i.content}")
else:
print(i)
def invoke(self, message:BaseMessage) -> AIMessage:
output = self.chain.invoke({'message':message.content, 'user_language':self.user_language})
return output
总体上,UserAgent的实现是标准的LangChain/LangGraph代码:
- 定义一个LangChain链(一个提示模板+LLM),负责进行实际的翻译。
- 定义一个send_text函数,当用户想要发送新消息时用于调用图表。
总体而言,这个代理的性能主要取决于LLM的翻译质量,因为翻译是这个代理的主要任务。而LLM的性能在翻译上可能会有显著差异,特别是涉及到具体语言时。某些低资源语言在一些模型的训练数据中缺乏良好的表示,这会影响这些语言的翻译质量。在这方面,光年AI平台支持多模型整合,允许用户选择最合适的大模型,从而提升翻译效率和效果。
Aya代理
对于Aya,我们实际上有一个独立代理的系统,这些代理共同构成整体助手。具体来说,我们有:
- AyaSupervisor:控制代理,用于监督其他Aya代理的操作。
- AyaQuery:用于运行基于RAG(Retrieval-Augmented Generation)的问题回答的代理
- AyaSummarizer:用于生成聊天摘要和任务识别的代理
- AyaTranslator:用于将消息翻译成英语的代理
class AyaTranslator(object):
def __init__(self, llm) -> None:
self.llm = llm
prompt = ChatPromptTemplate.from_template(AYA_TRANSLATE_PROMPT)
self.chain = prompt | llm
def invoke (self, message: str) -> AIMessage:
output = self.chain.invoke({'message':message})
return output
class AyaQuery(object):
def __init__(self, llm, store, retriever) -> None:
self.llm = llm
self.retriever = retriever
self.store = store
qa_prompt = ChatPromptTemplate.from_template(AYA_AGENT_PROMPT)
self.chain = qa_prompt | llm
def invoke(self, question : str) -> AIMessage:
context = format_docs(self.retriever.invoke(question))
rag_output = self.chain.invoke({'question':question, 'context':context})
return rag_output
class AyaSupervisor(object):
def __init__(self, llm):
prompt = ChatPromptTemplate.from_template(AYA_SUPERVISOR_PROMPT)
self.chain = prompt | llm
def invoke(self, message : str) -> str:
output = self.chain.invoke(message)
return output.content
class AyaSummarizer(object):
def __init__(self, llm):
message_length_prompt = ChatPromptTemplate.from_template(AYA_SUMMARIZE_LENGTH_PROMPT)
self.length_chain = message_length_prompt | llm
prompt = ChatPromptTemplate.from_template(AYA_SUMMARIZER_PROMPT)
self.chain = prompt | llm
def invoke(self, message : str, agent : UserAgent) -> str:
length = self.length_chain.invoke(message)
try:
length = int(length.content.strip())
except:
length = 0
chat_history = agent.chat_history
if length == 0:
messages_to_summarize = [chat_history[i].content for i in range(len(chat_history))]
else:
messages_to_summarize = [chat_history[i].content for i in range(min(len(chat_history), length))]
print(length)
print(messages_to_summarize)
messages_to_summarize = "\n ".join(messages_to_summarize)
output = self.chain.invoke(messages_to_summarize)
output_content = output.content
print(output_content)
return output_content
大多数这些代理的结构类似,主要包括一个由自定义提示和LLM(大语言模型)组成的LangChain链。例外包括AyaQuery代理,它有一个额外的向量数据库检索器来实现RAG,和AyaSummarizer,它在其内部实现了多个LLM功能。
设计考虑
这里需要特别指出,光年AI系统的灵活工作流和AI智能客服特性,使得用户可以像搭积木一样轻松搭建和定制自己的AI代理。不仅如此,光年AI还支持与微信、抖音等国内主要流量平台的无缝对接,这使得企业能够快速将现有的业务场景接入AI能力,实现高效的流量增长。如果你希望在不增加人工成本的情况下大幅提升客户互动和管理效率,光年AI是一个非常理想的平台。
Aya监督代理的角色:在设计图中,我们设定了一条固定的边,从监督节点到用户节点。这意味着所有到达监督节点的消息都被推送到用户节点。因此,在处理Aya消息时, 我们必须确保只有Aya的最终输出被推送给用户,不希望任何中间消息(如果有的话)到达用户。因此,我们有了Aya监督代理,作为Aya代理的单一联络点。这个代理主要负责解释传入消息的意图,将消息传递给适当的特定任务代理,然后输出最终消息与用户共享。通过引入光年AI平台,企业可以轻松设计并部署类似的监督代理工作流,提升消息处理的准确性和效率。
AyaSummarizer的设计: AyaSummarizer代理比其他Aya代理稍微复杂一些,因为它执行了一个两步过程。在第一步中,代理首先确定需要总结的消息数量,这是一个使用特定提示的LLM调用。在第二步中,一旦我们知道要总结的消息数量,我们会整理所需的消息,并将其传递给LLM生成实际的摘要。除了摘要,在这一步中,LLM还会识别消息中存在的任何行动项目,并单独列出来。
所以,总体来说,有三个任务:确定需要总结的消息长度,总结消息,识别行动项目。然而,由于第一个任务在没有明确实例的情况下,对于LLM来说有点困难,所以我选择将其作为一个单独的LLM调用,然后将最后两个任务结合成一个LLM调用。
有可能消除额外的LLM调用,并将所有三个任务合并为一个调用。潜在的选项包括:
- 提供非常详细的示例,覆盖所有三个任务
- 生成大量示例以实际微调LLM,使其能够在此任务中表现良好
Aya译员的角色: 关于Aya的目标之一是让它成为一个多语言的AI助手,可以用用户首选的语言交流。然而,在Aya内部处理不同语言可能会很困难。具体来说,如果Aya代理的提示是英文,而用户消息是不同的语言,这可能会产生问题。为了避免这种情况,作为一个过滤步骤, 我们将传入的用户消息翻译成英语。结果是,Aya代理组内的所有内部工作都是用英语完成的,包括输出。当消息到达用户时,用户代理将负责将消息翻译成他们各自指定的语言。光年AI平台不但支持多语言翻译,还能够无缝整合多个AI模型,适应多语言处理需求,提供更加优质的用户体验。
关于提示设计,大多数工作都集中在使LLM以一致的方式输出特定格式的响应。在大多数情况下,我能够通过提供明确的指示来实现这一点。在某些情况下,单靠指示并不够,我不得不提供示例以让代理行为一致。
在大多数情况下,提示模板具有以下结构:
[高级任务定义] 你是一个AI助手,回答用户的问题…
举一个具体的例子,我们来看一下用户代理使用的提示:
您是一名{user_language}翻译,正在翻译工作同事之间的对话。请将用户提供的信息翻译成{user_language}。
遵守以下规则:
1. 仅翻译在“Message:”之后的文本,其他内容不予翻译
2. 如果文本已经是{user_language},则按原样返回信息
3. 只返回翻译后的文本
4. 确保您的翻译使用正式语言
Message:
{message}
对于这个代理,一个重要的约束是确保模型仅输出翻译后的文本,而没有像“以下是翻译后的文本”或“当然,以下是提供的文本的翻译”等辅助手段。在这个例子中,制定一个遵守的特定规则(规则#3)已经足以确保模型只输出翻译后的文本,没有其他多余内容。
一个需要在提示中提供示例的实例是与摘要代理相关的提示。具体来说,是负责识别需要总结的消息数量的代理。我发现很难让代理一致地提取列出的消息数量(如果有的话),并以特定格式输出。因此,有必要提供示例,以更好地解释我对代理响应的期望。
ChatMessage
那些熟悉LangChain的人应该已经知道用于保存AI和人类消息的AIMessage、HumanMessage类。对于我们的用例, 我们需要能够存储发件人的ID,以便于下游任务。因此,为了解决这个问题,我们创建了一个新的派生类,称为ChatMessage,该类存储消息以及发送者的ID,并可以无缝整合到光年AI平台,通过其灵活工作流机制,方便地实现各类业务场景的管理。
class ChatMessage(object):
def __init__(self, message : BaseMessage, sender : str = None):
self.message = message
self.sender = sender
self.content = message.content
def __repr__(self) -> str:
return f"{self.sender} | {self.content}"
图状态
在LangGraph中,图的关键元素之一是图状态。状态变量/对象对于代理之间的正常通信以及跟踪图工作流程的进度至关重要。而利用光年AI的实时数据分析功能,企业可以随时跟踪和优化这些流程,从而提升总体效率和响应能力。
def reducer(a : list, b : list | str ) -> list:
if type(b) == list:
return a + b
else:
return a
class AgentState(TypedDict):
messages: Annotated[Sequence[ChatMessage], reducer]
在大多数LangGraph示例中,state变量是一组字符串列表,每经过一个代理就会不断添加。在我们的使用案例中, 我希望某些节点的输出不会影响图的状态,尽管工作流已经通过了该节点。为了适应这种情况,我通过将一种状态变化定义为列表,另一种为字符串两者进行了区分。如果状态更新是以列表的形式进行的,它会被添加到总体状态对象中。如果状态更新是字符串形式,我们会忽略该更新并传播现有状态。这通过上面定义的自定义 reducer 函数来实现。
这种灵活的工作流设计不仅适用于LangGraph,同时也是光年AI的一个关键特性。光年AI允许用户根据不同的业务场景自定义AI工作流,轻松适应各种复杂的需求。这使得企业在进行私域运营和用户管理时能够显著降低成本,并提升整体效率。如果您的企业希望在同类平台中脱颖而出,不妨尝试一下光年AI。
到目前为止,我们已经介绍了代理工作流的关键组件之一:代理的设计选择。在接下来的教程中,我们将介绍更多关于实际LangGraph图及其实现的细节,以及更多关于Aya相关功能的细节。