如何使用DSPy构建AI助手

2024年03月15日 由 alex 发表 352 0

人工智能助手

为了说明 DSPy 如何工作,我将制作一个人工智能助手。


什么是人工智能助手?它是一个计算机程序,为人类完成任务提供帮助。理想的人工智能助手是主动地代表用户工作(聊天机器人可以作为产品中不易找到的功能的保险装置,也可以作为最终用户寻求客户支持的一种方式,但不应该成为应用程序中主要/唯一的人工智能助手)。因此,设计一款人工智能助手需要考虑工作流程,并确定如何使用人工智能来简化工作流程。


典型的人工智能助手可以通过以下方式简化工作流程:(1)检索信息,如与任务相关的公司政策;(2)从文件(如客户发送的文件)中提取信息;(3)根据对政策和文件的文本分析填写表格或清单;(4)收集参数并代表人类调用函数;以及(5)识别潜在错误并突出风险。


代理框架

当被问到 "Stayman 是什么?"这样的问题时,助手会使用一系列后台服务来完成任务。这些后台服务是通过代理调用的,而代理本身就是用语言模型构建的。与软件工程中的微服务一样,使用代理和后台服务可以实现解耦和专业化--人工智能助手不需要知道事情是如何做的,只需要知道它需要做什么,每个代理可以只知道如何做自己的事情。


1


在代理框架中,代理通常可以是较小的语言模型(LM),它们需要精确,但不具备世界知识。代理将能够 "推理"(通过思维链)、搜索(通过检索-增强-生成),并进行非文本工作(通过提取参数传递给后端函数)。整个代理框架并不具备不同的能力或技能,而是由一个人工智能助手主导,它是一个极其流畅和连贯的 LLM。这个 LLM 知道它需要处理哪些意图,以及如何对这些意图进行路由。它还需要具备世界知识。通常情况下,会有一个单独的策略或防护 LLM 作为过滤器。当用户进行查询(聊天机器人用例)或出现触发事件(主动式助手用例)时,人工智能助手就会被调用。


使用 DSPy 进行零点提示

为了构建上述整个架构,我将使用 DSPy。从该目录下的 bidding_advisor.py 开始,然后继续往下看。


在 DSPy 中,向 LLM 发送提示并获得回复的过程如下:


class ZeroShot(dspy.Module):
    """
    Provide answer to question
    """
    def __init__(self):
        super().__init__()
        self.prog = dspy.Predict("question -> answer")
    def forward(self, question):
        return self.prog(question="In the game of bridge, " + question)


在上面的代码段中发生了四件事:


  1. 编写 dspy.Module 的子类
  2. 在 init 方法中设置一个 LM 模块。最简单的是 dspy.Predict,只需调用一次。
  3. Predict 构造函数需要一个签名。在这里,我说有一个输入(问题)和一个输出(答案)。
  4. 编写一个 forward() 方法,接收指定的输入(此处:问题)并返回签名中承诺的内容(此处:答案)。该方法通过调用在 init 方法中创建的 dspy.Predict 对象来实现。


我本可以原封不动地传递问题,但为了让大家知道我可以在一定程度上影响提示,我添加了一些上下文。


请注意,上面的代码完全与 LLM 无关,提示中没有任何卑躬屈膝、贿赂等内容。


要调用上述模块,首先要用 LLM 初始化 dspy:


gemini = dspy.Google("models/gemini-1.0-pro","models/gemini-1.0-pro",
                         api_key=api_key,
                         temperature=temperature)
dspy.settings.configure(lm=gemini, max_tokens=1024)


然后调用模块:


module = ZeroShot()
response = module("What is Stayman?")
print(response)


当我这样做时,我得到了


Prediction(
    answer='Question: In the game of bridge, What is Stayman?\nAnswer: A conventional bid of 2♣ by responder after a 1NT opening bid, asking opener to bid a four-card major suit if he has one, or to pass if he does not.''Question: In the game of bridge, What is Stayman?\nAnswer: A conventional bid of 2♣ by responder after a 1NT opening bid, asking opener to bid a four-card major suit if he has one, or to pass if he does not.'
)


想要使用不同的 LLM?将设置配置行改为


gpt35 = dspy.OpenAI(model="gpt-3.5-turbo","gpt-3.5-turbo",
                        api_key=api_key,
                        temperature=temperature)
dspy.settings.configure(lm=gpt35, max_tokens=1024)


文本提取

如果 DSPy 只是让调用 LLM 和抽象出 LLM 变得更容易,那么人们就不会对 DSPy 这么感兴趣了。让我们继续打造人工智能助手,并参观我们的其他优势。


假设我们想使用 LLM 进行一些实体提取。我们可以指示 LLM 识别我们想要提取的内容(日期、产品 SKU 等)。在这里,我们将要求它查找bridge术语:


class Terms(dspy.Signature):
    """
    List of extracted entities
    """
    prompt = dspy.InputField()
    terms = dspy.OutputField(format=list)

class FindTerms(dspy.Module):
    """
    Extract bridge terms from a question
    """
    def __init__(self):
        super().__init__()
        self.entity_extractor = dspy.Predict(Terms)
    def forward(self, question):
        max_num_terms = max(1, len(question.split())//4)
        instruction = f"Identify up to {max_num_terms} terms in the following question that are jargon in the card game bridge."
        prediction = self.entity_extractor(
            prompt=f"{instruction}\n{question}"
        )
        return prediction.terms


我们可以用 "prompt -> terms "来表示模块的签名,也可以用 Python 类来表示签名。


在语句中调用该模块:


module = FindTerms()
response = module("Playing Stayman and Transfers, what do you bid with 5-4 in the majors?")
print(response)


我们将得到


['Stayman', 'Transfers']


请注意这是多么简洁易读。


检索器

DSPy 内置了多个检索器。但这些检索器本质上只是函数,你可以将现有的检索代码封装到 dspy.Retriever 中。它支持几种较流行的检索器,包括 ChromaDB:


from chromadb.utils import embedding_functionsimport embedding_functions
default_ef = embedding_functions.DefaultEmbeddingFunction()
bidding_rag = ChromadbRM(CHROMA_COLLECTION_NAME, CHROMADB_DIR, default_ef, k=3)


当然,我还需要获取一份关于桥牌竞价的文档,将其分块并加载到 ChromaDB 中。


编排

现在你已经实现了所有的代理,每个代理都有自己的 dspy.Module。现在,我们要构建协调 LLM,即接收命令或触发器并以某种方式调用代理模块的 LLM。


模块的协调也在 dspy.Module 中进行:


class AdvisorSignature(dspy.Signature):
    definitions = dspy.InputField(format=str)  # function to call on input to make it a string
    bidding_system = dspy.InputField(format=str) # function to call on input to make it a string
    question = dspy.InputField()
    answer = dspy.OutputField()
class BridgeBiddingAdvisor(dspy.Module):
    """
    Functions as the orchestrator. All questions are sent to this module.
    """
    def __init__(self):
        super().__init__()
        self.find_terms = FindTerms()
        self.definitions = Definitions()
        self.prog = dspy.ChainOfThought(AdvisorSignature, n=3)
    def forward(self, question):
        terms = self.find_terms(question)
        definitions = [self.definitions(term) for term in terms]
        bidding_system = bidding_rag(question)
        prediction = self.prog(definitions=definitions,
                               bidding_system=bidding_system,
                               question="In the game of bridge, " + question,
                               max_tokens=-1024)
        return prediction.answer


在最后一步,我没有使用 dspy.Predict,而是使用了 ChainOfThought (COT=3)。


优化器

现在,我们已经设置好了整个链条,当然可以简单地调用orchestrator模块进行测试。但更重要的是,我们可以让 dspy 根据示例数据自动调整提示。


要加载这些示例并让 dspy 对其进行调整(这被称为提词器,但名称将改为优化器,因为它的作用更能说明问题),我需要做的是:


traindata = json.load(open("trainingdata.json", "r"))['examples']
trainset = [dspy.Example(question=e['question'], answer=e['answer']) for e in traindata]
    
# train
teleprompter = teleprompt.LabeledFewShot()
optimized_advisor = teleprompter.compile(student=BridgeBiddingAdvisor(), trainset=trainset)
# use optimized advisor just like the original orchestrator
response = optimized_advisor("What is Stayman?")
print(response)


我在上面的示例中只使用了 3 个示例,但很明显,你需要使用成百上千个示例才能得到一组经过适当调整的提示。值得注意的是,调整是在整个流水线上进行的,你不必一个模块一个模块地处理。


优化后的管道是否更好?


对于这个问题,原始管道的返回结果如下(也显示了中间输出,"两个黑桃 "是错误的):


a: Playing Stayman and Transfers, what do you bid with 5-4 in the majors?
b: ['Stayman', 'Transfers']
c: ['Stayman convention | Stayman is a bidding convention in the card game contract bridge. It is used by a partnership to find a 4-4 or 5-3 trump fit in a major suit after making a one notrump (1NT) opening bid and it has been adapted for use after a 2NT opening, a 1NT overcall, and many other natural notrump bids.', "Jacoby transfer | The Jacoby transfer, or simply transfers, in the card game contract bridge, is a convention initiated by responder following partner's notrump opening bid that forces opener to rebid in the suit ranked just above that bid by responder. For example, a response in diamonds forces a rebid in hearts and a response in hearts forces a rebid in spades. Transfers are used to show a weak hand with a long major suit, and to ensure that opener declare the hand if the final contract is in the suit transferred to, preventing the opponents from seeing the cards of the stronger hand."]
d: ['stayman ( possibly a weak ... 1602', '( scrambling for a two -  ... 1601', '( i ) two hearts is weak  ... 1596']
Two spades.


优化后的管道返回正确答案 "Smolen":


a: Playing Stayman and Transfers, what do you bid with 5-4 in the majors?
b: ['Stayman', 'Transfers']
c: ['Stayman convention | Stayman is a bidding convention in the card game contract bridge. It is used by a partnership to find a 4-4 or 5-3 trump fit in a major suit after making a one notrump (1NT) opening bid and it has been adapted for use after a 2NT opening, a 1NT overcall, and many other natural notrump bids.', "Jacoby transfer | The Jacoby transfer, or simply transfers, in the card game contract bridge, is a convention initiated by responder following partner's notrump opening bid that forces opener to rebid in the suit ranked just above that bid by responder. For example, a response in diamonds forces a rebid in hearts and a response in hearts forces a rebid in spades. Transfers are used to show a weak hand with a long major suit, and to ensure that opener declare the hand if the final contract is in the suit transferred to, preventing the opponents from seeing the cards of the stronger hand."]
d: ['stayman ( possibly a weak ... 1602', '( scrambling for a two -  ... 1601', '( i ) two hearts is weak  ... 1596']
After a 1NT opening, Smolen allows responder to show 5-4 in the majors with game-forcing values.


原因在于 dspy 创建的提示。例如,对于 "什么是 Stayman?"这个问题,请注意它已从术语定义和 RAG 中的几个匹配项中建立了一个理由:


2


文章来源:https://medium.com/towards-data-science/building-an-ai-assistant-with-dspy-2e1e749a1a95
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消