文章目录

前言

DSPy 提供了一套非常简洁的 API,您可以快速上手。然而,构建一个全新的 AI 系统是一个更为开放的迭代开发之旅,您需要组合运用 DSPy 的各种工具和设计模式,以不断优化,最终达成您的目标。在 DSPy 中构建 AI 系统主要分为三个阶段:

  1. DSPy 编程:这个阶段的核心是定义您的任务及其约束条件,通过少量示例进行探索,并基于这些信息来指导您完成初始的流水线设计。
  2. DSPy 评估:当您的系统初步运行起来后,就进入了此阶段。您需要收集一个初始的开发数据集,定义您的 DSPy 评估指标,并利用这些工具来更系统地对系统进行迭代改进。
  3. DSPy 优化:一旦您建立了有效的评估方法,就可以使用 DSPy 的优化器来微调程序中的提示或模型权重。

我们建议您按照这个顺序来学习和应用 DSPy。例如,如果程序设计本身存在缺陷或评估指标不合理,那么直接启动优化流程将是徒劳无功的。

快速开始

以一个生成复杂检索关键词的代码示例

  • 安装:
uv add dspy
  • 示例:以前需要一个复杂的提示词,使用这个框架不需要了
输入:分析香港大火事件
输出:(((香港|香港特别行政区)+(火灾|火警|大火|失火)+(伤亡|死亡|受伤|遇难|救援))|((香港|香港特别行政区)+(住宅|商业大厦|工业大厦|公共场所)+(火灾|火警|消防隐患|安全设施))|((香港|香港特别行政区)+(消防处|消防部门|消防员)+(救援行动|灭火|调查原因|责任认定))|((香港|香港特别行政区)+(火灾预防|消防安全|防火条例|安全检查)+(公众教育|应急演练))|((香港|香港特别行政区)+(近期火灾|重大火灾|火灾事故)+(时间|地点|原因|影响|后续处理)))
  • 代码
import re

import dspy

lm = dspy.LM('deepseek/deepseek-chat', api_key='xxxx')
dspy.configure(lm=lm)

trainset = [
    dspy.Example(question='分析苏丹发生大屠杀',
                 keyword_expression='(((苏丹|苏丹共和国)+(大屠杀|屠杀|种族清洗|暴力冲突)+(伤亡|死亡|遇难|受害者))|((苏丹|苏丹共和国)+(内战|武装冲突|军事冲突)+(人道主义|人道危机|难民|流离失所))|((苏丹|苏丹共和国)+(国际社会|联合国|国际组织)+(谴责|制裁|干预|调停))|((苏丹|苏丹共和国)+(地缘政治|地区冲突)+(达尔富尔|喀土穆|冲突地区)+(局势|现状))|((苏丹|苏丹共和国)+(人权|人道主义)+(侵犯|危机|援助|救援)))').with_inputs(
        'question'),
    dspy.Example(question='七星杯荒野求生',
                 keyword_expression='(((七星杯荒野求生)+(七星杯|荒野求生|野外生存)+(赛事|比赛|竞赛)+(规则|赛制|流程))|((七星杯荒野求生)+(七星杯|荒野求生|野外生存)+(选手|参赛者|运动员)+(表现|成绩|经历))|((七星杯荒野求生)+(七星杯|荒野求生|野外生存)+(举办地|地点|场地)+(环境|条件|特点))|((七星杯荒野求生)+(七星杯|荒野求生|野外生存)+(装备|工具|物资)+(使用|配置|技巧))|((七星杯荒野求生)+(七星杯|荒野求生|野外生存)+(挑战|困难|危险)+(应对|解决|经验)))').with_inputs(
        'question'),
    dspy.Example(question='奇瑞就撞坏天门山“天梯”护栏致歉',
                 keyword_expression='(((奇瑞|奇瑞汽车|Chery)+(天门山|((张家界|慈利县|桑植县|武陵源区|永定区))|天梯|护栏)+(事故|损坏|撞坏)+(致歉|道歉|声明))|((奇瑞|奇瑞汽车|Chery)+(天门山|((张家界|慈利县|桑植县|武陵源区|永定区))|天梯|护栏)+(赔偿|修复|维修)+(责任|承担))|((奇瑞|奇瑞汽车|Chery)+(天门山|((张家界|慈利县|桑植县|武陵源区|永定区))|天梯|护栏)+(争议|批评)+(品牌|形象|声誉))|((奇瑞|奇瑞汽车|Chery)+(天门山|((张家界|慈利县|桑植县|武陵源区|永定区))|天梯|护栏)+(旅游|景区|安全)+(管理|规范|制度)))').with_inputs(
        'question'),
    dspy.Example(question='分析女子误踩化骨水身亡',
                 keyword_expression='(((女子|女性|受害人|受害者)+(误踩|误触|接触|踩到)+(化骨水|氢氟酸|强酸|腐蚀性化学品|危险化学品)+(身亡|死亡|致死|丧命|遇难))|((化骨水|氢氟酸|强酸|腐蚀性化学品|危险化学品)+(安全规范|使用标准|管理规定|应急预案|事故预防))|((化骨水)+(氢氟酸)+(化学品|危险品|腐蚀性物质)+(安全事故|意外事故|责任认定|法律责任|赔偿标准|民事纠纷))|((化骨水)+(氢氟酸)+(公共场所|工作场所|生产场所)+(危险化学品|腐蚀性物质|有毒物质)+(存放管理|安全监管|隐患排查|防护设施))|((氢氟酸|化骨水|氟化氢)+(中毒|灼伤|腐蚀)+(医疗救治|治疗方案|预后情况)))').with_inputs(
        'question'),
]


def metric(gold, pred, trace=None):
    # --- 第1步:定义“裁判”的评分标准和提示词 ---
    JUDGE_INSTRUCTIONS_TEMPLATE = """
    你是一个专业的信息检索评估专家。你的任务是根据用户的问题和模型生成的关键词表达式,从以下几个维度进行评分,评分范围为0到5分。

    **评分标准:**

    1.  **格式正确性 (1分)**:表达式是否遵循了基本格式,如使用 `+` 连接核心词,使用 `()` 和 `|` 进行同义词拓展。
    2.  **主体识别与包含 (1分)**:生成的每组关键词是否都包含了用户问题中的核心主体(人名、地名、国名、公司等)或其同义词。这是必须满足的硬性要求。
    3.  **维度多样性 (2分)**:是否从多个角度生成了关键词(例如,军事、经济、民生、技术、社会影响等),而不是局限于单一维度。维度越多,得分越高。
    4.  **拓展质量 (1分)**:同义词拓展是否准确、丰富,能否有效扩大搜索范围,同时又不偏离主题。

    **输出格式:**
    请只输出一个0到5之间的整数分数,不要包含任何其他解释。

    ---
    **用户问题:** {question}
    **模型生成的关键词:** {keyword_expression}
    ---
    请给出评分:
    """

    # --- 第2步:准备输入数据 ---
    question = gold.question
    keyword_expression = pred.keyword_expression

    # --- 第3步:构建并发送给 LLM 进行评分 ---
    judge_prompt = JUDGE_INSTRUCTIONS_TEMPLATE.format(
        question=question,
        keyword_expression=keyword_expression
    )

    # 让“裁判”进行评分
    try:
        raw_score_text = lm(judge_prompt)
    except Exception as e:
        print(f"Error calling LM for judgment: {e}")
        return 0.0

    # --- 第4步:解析 LLM 的返回并计算最终分数 ---
    try:
        # 使用正则表达式从返回文本中提取第一个整数
        match = re.search(r'\b([0-5])\b', raw_score_text)
        if match:
            score = int(match.group(1))
        else:
            # 如果找不到数字,尝试直接转换
            score = int(raw_score_text.strip())

        # 确保分数在有效范围内
        score = max(0, min(5, score))

        # 将 0-5 分归一化到 0-1,以便 DSPy 的优化器使用
        return score / 5.0

    except (ValueError, TypeError, AttributeError):
        # 如果 LLM 返回的不是有效数字或格式错误,则给一个最低分
        print(f"Could not parse a valid score from LM output: '{raw_score_text}'")
        return 0.0


def main():
    unopt_agent = dspy.Predict("question->keyword_expression")
    print("--- 未编译模型的预测 ---")
    print(f"问题: '分析一下香港火灾'")
    print(f"预测查询: {unopt_agent(question='分析一下香港火灾').keyword_expression}\n")

    optimizer = dspy.BootstrapFewShot(
        metric=metric,
        max_bootstrapped_demos=3,
        max_labeled_demos=2,
    )

    print("--- 开始编译模型 ---")
    opt_agent = optimizer.compile(unopt_agent, trainset=trainset)
    opt_agent.
    print("--- 模型编译完成 ---\n")

    # --- 6.3 测试编译后的模型 ---
    print("--- 编译后模型的预测 ---")
    print(f"问题: '分析一下香港火灾'")
    print(f"预测查询: {opt_agent(question='分析一下香港火灾').keyword_expression}\n")


if __name__ == '__main__':
    main()

DSPy 编程

DSPy 的核心理念是用代码代替字符串。换言之,构建正确的控制流程至关重要。

首先,定义您的任务。您的系统输入是什么?应该输出什么?它是一个基于您数据的聊天机器人,还是一个代码助手?或许是一个翻译系统、一个用于高亮搜索结果片段的工具,或是一个能生成带引用报告的系统?

接下来,设计您的初始流水线。您的 DSPy 程序是只需要单个模块,还是需要拆解成多个步骤?您是否需要检索功能,或是其他工具,比如计算器或日历 API?解决您的问题是否存在一个典型的、包含多个边界清晰步骤的工作流?又或者,您希望为任务配备一个能够进行更开放式工具调用的智能体?请思考这些问题,但务必从简单开始——或许先用一个 dspy.ChainOfThought 模块,然后根据观察结果逐步增加复杂性。

在此过程中,请精心制作并尝试少量输入示例。现阶段,建议您使用一个强大的语言模型,或尝试几个不同的模型,目的只是为了探索可能性。请记录下您尝试过的那些有趣的案例(既包括简单的,也包括复杂的),这些数据在后续的评估和优化阶段将大有裨益。

签名

在 DSPy 中,当我们向语言模型分配任务时,我们会将所需的行为规范指定为一个签名

签名是一种对 DSPy 模块输入/输出行为的声明式规范。通过签名,您可以告诉语言模型它需要做什么,而不必具体规定应该如何去要求它。

您可能对函数签名很熟悉,它指定了函数的输入、输出参数及其类型。DSPy 的签名与此类似,但存在几点关键差异。首先,普通的函数签名仅用于描述,而 DSPy 签名则能声明并初始化模块的行为。更重要的是,在 DSPy 签名中,字段的命名至关重要。您需要用简洁的英文来表达其语义角色:例如,question(问题)和 answer(答案)的内涵截然不同,sql_query(SQL 查询)和 python_code(Python 代码)也代表完全不同的概念。

为什么要使用签名

为了实现模块化、简洁的代码,让语言模型的调用能够被优化成高质量的提示(或自动微调)。大多数人通过编写冗长且脆弱的提示来强迫语言模型完成任务,或是通过收集/生成数据进行微调。相比之下,编写签名这种方式远比修补提示或进行微调更加模块化、适应性强且可复现。DSPy 编译器会负责为您的签名,在您的数据上、在您的流水线中,构建出针对您的语言模型高度优化的提示(或对您的小型模型进行微调)。在许多案例中,我们发现编译生成的提示比人工编写的质量更高。这并非因为 DSPy 优化器比人类更有创造力,而仅仅是因为它们能尝试更多可能性,并直接对评估指标进行调优。

内联 DSPy 签名

签名可以被定义为一个简短的字符串,其中包含参数名和可选的类型,用于定义输入/输出的语义角色。

  • 问答"question -> answer",这等同于 "question: str -> answer: str",因为默认类型始终是 str
  • 情感分类"sentence -> sentiment: bool",例如,如果情感为正面则为 True
  • 摘要生成"document -> summary"

您的签名也可以包含多个带类型的输入/输出字段:

  • 检索增强问答"context: list[str], question: str -> answer: str"
  • 带推理的多项选择问答"question, choices: list[str] -> reasoning: str, selection: int"

提示:对于字段,任何有效的变量名都可以!字段名应具有语义意义,但请从简单开始,不要过早优化关键词!把这种“黑客”式的优化工作留给 DSPy 编译器。例如,对于摘要任务,使用 "document -> summary""text -> gist""long_context -> tldr" 可能都是可以的。

您还可以为内联签名添加指令,这些指令可以在运行时使用变量。使用 instructions 关键字参数来为您的签名添加指令。

toxicity = dspy.Predict(
    dspy.Signature(
        "comment -> toxic: bool",
        instructions="如果评论包含侮辱、骚扰或讽刺性贬损言论,则标记为 'toxic'。",
    )
)
comment = "you are beautiful."
toxicity(comment=comment).toxic

输出:

False

一个摘要的例子:

# 摘自 XSum 数据集的示例。
document = """这位 21 岁的球员为西汉姆联队出场 7 次,并在上赛季对阵安道尔球队 FC Lustrains 的欧联杯资格赛中打进了他唯一的一粒进球。李上赛季曾两次被租借到英甲联赛,先后效力于布莱克浦队和科尔切斯特联队。他为科尔切斯特联队攻入两球,但未能帮助球队免于降级。李与这支成功晋级的球队(约克郡队)的合同长度尚未公布。请访问我们的专属页面,了解所有最新的足球转会信息。"""

summarize = dspy.ChainOfThought('document -> summary')
response = summarize(document=document)

print(response.summary)

输出:

这位 21 岁的李上赛季为西汉姆联队出场 7 次并打入 1 球。他曾被租借至英甲联赛的布莱克浦和科尔切斯特联队,并为后者攻入两球。他现已与巴恩斯利签约,但合同长度尚未公布。

许多 DSPy 模块(dspy.Predict 除外)会通过在底层扩展您的签名来返回辅助信息。例如,dspy.ChainOfThought 还会添加一个 reasoning 字段,其中包含语言模型在生成输出摘要之前的推理过程。

print("Reasoning:", response.reasoning)

可能的输出:

Reasoning: 我们需要突出李在西汉姆联队的表现、他在英甲联赛的租借经历,以及他与巴恩斯利的新合同。我们还需要提及他的合同长度尚未披露。

基于类的DSPy签名

对于一些高级任务,您需要更详细的签名。这通常是为了:

  • 阐明任务本身的性质(如下面的文档字符串所示)。
  • 为输入字段的性质提供提示,通过 dspy.InputFielddesc 关键字参数来表达。
  • 对输出字段施加约束,通过 dspy.OutputField`` 的 desc` 关键字参数来表达。
from typing import Literal
class Emotion(dspy.Signature):
    """对情绪进行分类。"""
    sentence: str = dspy.InputField()
    sentiment: Literal['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] = dspy.OutputField()
sentence = "当巨大的聚光灯开始让我目眩时,我开始感到有点脆弱"  # 摘自 dair-ai/emotion
classify = dspy.Predict(Emotion)
classify(sentence=sentence)

签名中的类型解析

DSPy 签名支持多种注解类型:

  • 基本类型,如 strintbool
  • typing 模块中的类型,如 list[str]dict[str, int]Optional[float]Union[str, int]
  • 您代码中定义的自定义类型
  • 通过点表示法引用的嵌套类型(需正确配置)
  • 特殊数据类型,如 dspy.Imagedspy.History

使用自定义类型

# 简单的自定义类型
class QueryResult(pydantic.BaseModel):
    text: str
    score: float
signature = dspy.Signature("query: str -> result: QueryResult")
class MyContainer:
    class Query(pydantic.BaseModel):
        text: str
    class Score(pydantic.BaseModel):
        score: float
signature = dspy.Signature("query: MyContainer.Query -> score: MyContainer.Score")

模块

一个 DSPy 模块是构建使用语言模型(LM)的程序的基础单元。

每个内置模块都抽象了一种提示技术(如思维链或 ReAct)。关键在于,它们被泛化以处理任何签名。

一个 DSPy 模块拥有可学习的参数(即构成提示和语言模型权重的细小部分),并且可以被调用以处理输入并返回输出。

多个模块可以被组合成更大的模块(即程序)。DSPy 模块的灵感直接来源于 PyTorch 中的神经网络模块,但被应用于语言模型程序。

如何使用内置模块,如 dspy.Predict 或 dspy.ChainOfThought?

让我们从最基础的模块 dspy.Predict 开始。在内部,所有其他 DSPy 模块都是使用 dspy.Predict 构建的。我们假设您已经对 DSPy 签名至少有了一些了解,签名是一种声明式规范,用于定义我们在 DSPy 中使用的任何模块的行为。

要使用一个模块,我们首先通过为其提供一个签名来声明它。然后,我们用输入参数调用该模块,并提取输出字段!

sentence = "it's a charming and often affecting journey."  # 摘自 SST-2 数据集的示例。
# 1) 用一个签名来声明。
classify = dspy.Predict('sentence -> sentiment: bool')
# 2) 用输入参数调用。
response = classify(sentence=sentence)
# 3) 访问输出。
print(response.sentiment)

输出:

True

当我们声明一个模块时,可以向其传递配置键。

下面,我们将传递 n=5 来请求五个补全结果。我们也可以传递 temperaturemax_len 等。

让我们使用 dspy.ChainOfThought。在许多情况下,简单地用 dspy.ChainOfThought 替换 dspy.Predict 就能提升质量。

question = "ColBERT 检索模型有什么了不起的地方?"
# 1) 用一个签名来声明,并传递一些配置。
classify = dspy.ChainOfThought('question -> answer', n=5)
# 2) 用输入参数调用。
response = classify(question=question)
# 3) 访问输出。
response.completions.answer

可能的输出:

['ColBERT 检索模型的一个了不起之处在于其相比其他模型的卓越效率和有效性。',
 '其能够高效地从大型文档集合中检索相关信息。',
 'ColBERT 检索模型的一个了不起之处在于其相比其他模型的卓越性能,以及对预训练语言模型的高效利用。',
 'ColBERT 检索模型的一个了不起之处在于其相比其他模型的卓越效率和准确性。',
 'ColBERT 检索模型的一个了不起之处在于其能够整合用户反馈并支持复杂查询。']

让我们来讨论一下这里的输出对象。dspy.ChainOfThought 模块通常会在您签名的输出字段之前注入一个 reasoning(推理过程)。
让我们检查一下(第一个)推理过程和答案!

print(f"Reasoning: {response.reasoning}")
print(f"Answer: {response.answer}")

可能的输出:

Reasoning: 我们可以考虑这样一个事实:ColBERT 在效率和有效性方面已被证明优于其他最先进的检索模型。它使用上下文嵌入,并以既准确又可扩展的方式执行文档检索。
Answer: ColBERT 检索模型的一个了不起之处在于其相比其他模型的卓越效率和有效性。

无论我们请求一个还是多个补全结果,都可以访问到这些信息。
我们还可以将不同的补全结果作为一个 Prediction(预测)对象列表来访问,或者作为多个列表(每个字段一个列表)来访问。

response.completions[3].reasoning == response.completions.reasoning[3]

输出:

True

还有哪些其他的 DSPy 模块?我该如何使用它们?

其他模块非常相似。它们主要改变了实现您签名的内部行为!

  • dspy.Predict:基础预测器。不修改签名。处理关键的学习形式(即存储指令和示例,以及对语言模型的更新)。
  • dspy.ChainOfThought:教导语言模型在给出签名的响应之前,一步一步地思考。
  • dspy.ProgramOfThought:教导语言模型输出代码,代码的执行结果将决定最终的响应。
  • dspy.ReAct:一个可以使用工具来实现给定签名的智能体(agent)。
  • dspy.MultiChainComparison:可以比较来自 ChainOfThought 的多个输出来产生最终预测。

我们还有一些函数风格的模块:

  • dspy.majority:可以进行基本的投票,从一组预测中返回最受欢迎的响应。

如何将多个模块组合成一个更大的程序?

DSPy 就是 Python 代码,您可以按照自己喜欢的任何控制流来使用模块,并在编译时内部进行一些“魔法”操作来追踪您的语言模型调用。这意味着,您可以自由地调用这些模块。

请参阅像多跳搜索(multi-hop search)这样的教程,其模块代码如下所示,作为一个示例。

class Hop(dspy.Module):
    def __init__(self, num_docs=10, num_hops=4):
        self.num_docs, self.num_hops = num_docs, num_hops
        self.generate_query = dspy.ChainOfThought('claim, notes -> query')
        self.append_notes = dspy.ChainOfThought('claim, notes, context -> new_notes: list[str], titles: list[str]')
    def forward(self, claim: str) -> list[str]:
        notes = []
        titles = []
        for _ in range(self.num_hops):
            query = self.generate_query(claim=claim, notes=notes).query
            context = search(query, k=self.num_docs)
            prediction = self.append_notes(claim=claim, notes=notes, context=context)
            notes.extend(prediction.new_notes)
            titles.extend(prediction.titles)
        return dspy.Prediction(notes=notes, titles=list(set(titles)))

然后,您可以创建这个自定义模块类 Hop 的一个实例,然后通过其 __call__ 方法来调用它:

hop = Hop()
print(hop(claim="斯蒂芬·库里是人类历史上最出色的三分射手"))

适配器

什么是适配器?

适配器是 dspy.Predict 与实际语言模型(LM)之间的桥梁。当您调用一个 DSPy 模块时,适配器会接收您的签名、用户输入以及其他属性,并将它们转换为发送给语言模型的多轮消息。适配器系统负责:

  • 将 DSPy 签名转换为定义任务和请求/响应结构的系统消息。
  • 根据 DSPy 签名中概述的请求结构来格式化输入数据。
  • 将语言模型的响应解析回结构化的 DSPy 输出,例如 dspy.Prediction 实例。
  • 管理对话历史和函数调用。
  • 将预构建的 DSPy 类型(如 dspy.Tooldspy.Image 等)转换为语言模型的提示消息。

配置适配器

您可以使用 dspy.configure(adapter=...) 来为整个 Python 进程选择适配器,或者使用 dspy.context(adapter=...): 来仅影响某个特定的命名空间。

如果在 DSPy 工作流中没有指定适配器,那么每次 dspy.Predict.__call__ 调用默认都会使用 dspy.ChatAdapter。因此,下面这两个代码片段是等效的:

import dspy
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
predict = dspy.Predict("question -> answer")
result = predict(question="What is the capital of France?")
import dspy
dspy.configure(
    lm=dspy.LM("openai/gpt-4o-mini"),
    adapter=dspy.ChatAdapter(),  # 这是默认值
)
predict = dspy.Predict("question -> answer")
result = predict(question="What is the capital of France?")

适配器在系统中的位置

其工作流程如下:

  1. 用户调用他们的 DSPy 智能体,通常是一个带有输入的 dspy.Module
  2. 内部的 dspy.Predict 被调用以获取语言模型的响应。
  3. dspy.Predict 调用 Adapter.format(),该方法将其签名、输入和示例转换为发送给 dspy.LM 的多轮消息。dspy.LM 是对 litellm 的一个轻量级封装,用于与语言模型端点通信。
  4. 语言模型接收消息并生成响应。
  5. Adapter.parse() 将语言模型的响应转换为签名中指定的结构化 DSPy 输出。
  6. dspy.Predict 的调用者接收解析后的输出。
    您可以显式调用 Adapter.format() 来查看发送给语言模型的消息。
# 简化的流程示例
signature = dspy.Signature("question -> answer")
inputs = {"question": "What is 2+2?"}
demos = [{"question": "What is 1+1?", "answer": "2"}]
adapter = dspy.ChatAdapter()
print(adapter.format(signature, demos, inputs))

输出应类似于:

{'role': 'system', 'content': 'Your input fields are:\n1. `question` (str):\nYour output fields are:\n1. `answer` (str):\nAll interactions will be structured in the following way, with the appropriate values filled in.\n\n[[ ## question ## ]]\n{question}\n\n[[ ## answer ## ]]\n{answer}\n\n[[ ## completed ## ]]\nIn adhering to this structure, your objective is: \n        Given the fields `question`, produce the fields `answer`.'}
{'role': 'user', 'content': '[[ ## question ## ]]\nWhat is 1+1?'}
{'role': 'assistant', 'content': '[[ ## answer ## ]]\n2\n\n[[ ## completed ## ]]\n'}
{'role': 'user', 'content': '[[ ## question ## ]]\nWhat is 2+2?\n\nRespond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.'}

您也可以通过调用 adapter.format_system_message(signature) 来仅获取系统消息。

import dspy
signature = dspy.Signature("question -> answer")
system_message = dspy.ChatAdapter().format_system_message(signature)
print(system_message)

输出应类似于:

Your input fields are:
1. `question` (str):
Your output fields are:
1. `answer` (str):
All interactions will be structured in the following way, with the appropriate values filled in.
[[ ## question ## ]]
{question}
[[ ## answer ## ]]
{answer}
[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Given the fields `question`, produce the fields `answer`.

适配器的类型

DSPy 提供了多种适配器类型,每种都针对特定的用例量身定制:

ChatAdapter

ChatAdapter 是默认的适配器,适用于所有语言模型。它使用一种基于字段的格式,并带有特殊的标记。

格式结构

ChatAdapter 使用 [[ ## field_name ## ]] 标记来划分字段。对于非原始 Python 类型的字段,它会包含该类型的 JSON 模式。下面,我们使用 dspy.inspect_history() 来清晰地展示由 dspy.ChatAdapter 格式化的消息。

import dspy
import pydantic
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"), adapter=dspy.ChatAdapter())
class ScienceNews(pydantic.BaseModel):
    text: str
    scientists_involved: list[str]
class NewsQA(dspy.Signature):
    """获取关于给定科学领域的新闻"""
    science_field: str = dspy.InputField()
    year: int = dspy.InputField()
    num_of_outputs: int = dspy.InputField()
    news: list[ScienceNews] = dspy.OutputField(desc="科学新闻")
predict = dspy.Predict(NewsQA)
predict(science_field="计算机理论", year=2022, num_of_outputs=1)
dspy.inspect_history()

输出内容如下:

[2025-08-15T22:24:29.378666]
系统消息:
Your input fields are:
1. `science_field` (str):
2. `year` (int):
3. `num_of_outputs` (int):
Your output fields are:
1. `news` (list[ScienceNews]): 科学新闻
All interactions will be structured in the following way, with the appropriate values filled in.
[[ ## science_field ## ]]
{science_field}
[[ ## year ## ]]
{year}
[[ ## num_of_outputs ## ]]
{num_of_outputs}
[[ ## news ## ]]
{news}        # 注意:你生成的值必须遵守 JSON 模式: {"type": "array", "$defs": {"ScienceNews": {"type": "object", "properties": {"scientists_involved": {"type": "array", "items": {"type": "string"}, "title": "Scientists Involved"}, "text": {"type": "string", "title": "Text"}}, "required": ["text", "scientists_involved"], "title": "ScienceNews"}}, "items": {"$ref": "#/$defs/ScienceNews"}}
[[ ## completed ## ]]
In adhering to this structure, your objective is:
        获取关于给定科学领域的新闻
用户消息:
[[ ## science_field ## ]]
计算机理论
[[ ## year ## ]]
2022
[[ ## num_of_outputs ## ]]
1
Respond with the corresponding output fields, starting with the field `[[ ## news ## ]]` (must be formatted as a valid Python list[ScienceNews]), and then ending with the marker for `[[ ## completed ## ]]`.
响应:
[[ ## news ## ]]
[
    {
        "scientists_involved": ["张三", "李四"],
        "text": "2022年,研究人员在量子计算算法方面取得了重大进展,证明了它们解决复杂问题的潜力比经典计算机更快。这一突破可能会彻底改变密码学和优化等领域。"
    }
]
[[ ## completed ## ]]
何时使用 ChatAdapter

ChatAdapter 提供以下优势:

  • 通用兼容性:适用于所有语言模型,尽管较小的模型可能生成不符合所需格式的响应。
  • 回退保护:如果 ChatAdapter 失败,它会自动使用 JSONAdapter 重试。

一般来说,如果您没有特殊要求,ChatAdapter 是一个可靠的选择。

何时不使用 ChatAdapter

在以下情况下应避免使用 ChatAdapter

  • 对延迟敏感:与其他适配器相比,ChatAdapter 包含更多的样板输出标记,因此如果您正在构建对延迟敏感的系统,请考虑使用其他适配器。
JSONAdapter

JSONAdapter 提示语言模型返回包含签名中指定的所有输出字段的 JSON 数据。对于通过 response_format 参数支持结构化输出的模型,它非常有效,能够利用原生的 JSON 生成能力实现更可靠的解析。

格式结构

JSONAdapter 格式化的提示中,输入部分与 ChatAdapter 类似,但输出部分有所不同,如下所示:

import dspy
import pydantic
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"), adapter=dspy.JSONAdapter())
class ScienceNews(pydantic.BaseModel):
    text: str
    scientists_involved: list[str]
class NewsQA(dspy.Signature):
    """获取关于给定科学领域的新闻"""
    science_field: str = dspy.InputField()
    year: int = dspy.InputField()
    num_of_outputs: int = dspy.InputField()
    news: list[ScienceNews] = dspy.OutputField(desc="科学新闻")
predict = dspy.Predict(NewsQA)
predict(science_field="计算机理论", year=2022, num_of_outputs=1)
dspy.inspect_history()
系统消息:
Your input fields are:
1. `science_field` (str):
2. `year` (int):
3. `num_of_outputs` (int):
Your output fields are:
1. `news` (list[ScienceNews]): 科学新闻
All interactions will be structured in the following way, with the appropriate values filled in.
Inputs will have the following structure:
[[ ## science_field ## ]]
{science_field}
[[ ## year ## ]]
{year}
[[ ## num_of_outputs ## ]]
{num_of_outputs}
Outputs will be a JSON object with the following fields.
{
  "news": "{news}        # 注意:你生成的值必须遵守 JSON 模式: {\"type\": \"array\", \"$defs\": {\"ScienceNews\": {\"type\": \"object\", \"properties\": {\"scientists_involved\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"title\": \"Scientists Involved\"}, \"text\": {\"type\": \"string\", \"title\": \"Text\"}}, \"required\": [\"text\", \"scientists_involved\"], \"title\": \"ScienceNews\"}}, \"items\": {\"$ref\": \"#/$defs/ScienceNews\"}}"
}
In adhering to this structure, your objective is:
        获取关于给定科学领域的新闻
用户消息:
[[ ## science_field ## ]]
计算机理论
[[ ## year ## ]]
2022
[[ ## num_of_outputs ## ]]
1
Respond with a JSON object in the following order of fields: `news` (must be formatted as a valid Python list[ScienceNews]).
响应:
{
  "news": [
    {
      "text": "2022年,研究人员在量子计算算法方面取得了重大进展,证明了量子系统在特定任务上可以超越经典计算机。这一突破可能会彻底改变密码学和复杂系统模拟等领域。",
      "scientists_involved": [
        "张博士",
        "李博士",
        "王博士"
      ]
    }
  ]
}
何时使用 JSONAdapter

JSONAdapter 擅长于:

  • 结构化输出支持:当模型支持 response_format 参数时。
  • 低延迟:语言模型响应中的样板内容最少,从而带来更快的响应速度。
何时不使用 JSONAdapter

在以下情况下应避免使用 JSONAdapter

  • 使用不支持原生结构化输出的模型,例如在 Ollama 上托管的小型开源模型。

总结

适配器是 DSPy 的一个关键组件,它弥合了结构化的 DSPy 签名与语言模型 API 之间的鸿沟。理解何时以及如何使用不同的适配器,将帮助您构建更可靠、更高效的 DSPy 程序。

工具

DSPy 为能够与外部函数、API 和服务交互的工具使用代理提供了强大的支持。工具让语言模型能够通过执行操作、检索信息和动态处理数据,从而超越单纯的文本生成。在 DSPy 中使用工具有两种主要方法:

  • dspy.ReAct - 一个全自动的工具代理,能够自动处理推理和工具调用。
  • 手动工具处理 - 使用 dspy.Tooldspy.ToolCalls 和自定义签名,直接控制工具调用。

方法一:使用 dspy.ReAct(全自动)

dspy.ReAct 模块实现了推理与行动(ReAct)模式,语言模型在该模式下会迭代地对当前情况进行推理,并决定调用哪些工具。

基本示例
import dspy
# 将您的工具定义为函数
def get_weather(city: str) -> str:
    """获取一个城市的当前天气。"""
    # 在实际实现中,这里会调用天气 API
    return f"{city}的天气晴朗,气温为75华氏度"
def search_web(query: str) -> str:
    """在网络上搜索信息。"""
    # 在实际实现中,这里会调用搜索 API
    return f"'{query}'的搜索结果:[相关信息...]"
# 创建一个 ReAct 代理
react_agent = dspy.ReAct(
    signature="question -> answer",
    tools=[get_weather, search_web],
    max_iters=5
)
# 使用该代理
result = react_agent(question="东京的天气怎么样?")
print(result.answer)
print("已进行的工具调用:", result.trajectory)
ReAct 特性
  • 自动推理:模型会逐步思考问题
  • 工具选择:根据情况自动选择要使用的工具
  • 迭代执行:可以进行多次工具调用来收集信息
  • 错误处理:为失败的工具调用提供内置的错误恢复机制
  • 轨迹跟踪:记录推理和工具调用的完整历史
ReAct 参数
react_agent = dspy.ReAct(
    signature="question -> answer",  # 输入/输出规范
    tools=[tool1, tool2, tool3],     # 可用工具列表
    max_iters=10                     # 工具调用的最大迭代次数
)

方法二:手动工具处理

若要对工具调用过程进行更多控制,您可以使用 DSPy 的工具类型来手动处理工具。

基本设置
import dspy
class ToolSignature(dspy.Signature):
    """用于手动工具处理的签名。"""
    question: str = dspy.InputField()
    tools: list[dspy.Tool] = dspy.InputField()
    outputs: dspy.ToolCalls = dspy.OutputField()
def weather(city: str) -> str:
    """获取一个城市的天气信息。"""
    return f"{city}的天气晴朗"
def calculator(expression: str) -> str:
    """计算一个数学表达式。"""
    try:
        result = eval(expression)  # 注意:在生产环境中请安全使用
        return f"结果是 {result}"
    except:
        return "无效的表达式"
# 创建工具实例
tools = {
    "weather": dspy.Tool(weather),
    "calculator": dspy.Tool(calculator)
}
# 创建预测器
predictor = dspy.Predict(ToolSignature)
# 进行预测
response = predictor(
    question="纽约的天气怎么样?",
    tools=list(tools.values())
)
# 执行工具调用
for call in response.outputs.tool_calls:
    # 执行工具调用
    result = call.execute()
    # 对于 3.0.4b2 之前的版本,请使用:result = tools[call.name](**call.args)
    print(f"工具: {call.name}")
    print(f"参数: {call.args}")
    print(f"结果: {result}")
理解 dspy.Tool¶

dspy.Tool 类将常规 Python 函数封装,使其与 DSPy 的工具系统兼容:

def my_function(param1: str, param2: int = 5) -> str:
    """一个带参数的示例函数。"""
    return f"已处理 {param1},值为 {param2}"
# 创建一个工具
tool = dspy.Tool(my_function)
# 工具属性
print(tool.name)        # "my_function"
print(tool.desc)        # 函数的文档字符串
print(tool.args)        # 参数模式
print(str(tool))        # 完整的工具描述
理解 dspy.ToolCalls

dspy.ToolCalls 类型代表能够进行工具调用的模型的输出。每个单独的工具调用都可以使用 execute 方法来执行:

# 在获得包含工具调用的响应后
for call in response.outputs.tool_calls:
    print(f"工具名称: {call.name}")
    print(f"参数: {call.args}")
    # 使用不同选项执行单独的工具调用:
    # 选项 1:自动发现(在 locals/globals 中查找函数)
    result = call.execute()  # 按名称自动查找函数
    # 选项 2:将工具作为字典传递(最明确的方式)
    result = call.execute(functions={"weather": weather, "calculator": calculator})
    # 选项 3:将 Tool 对象作为列表传递
    result = call.execute(functions=[dspy.Tool(weather), dspy.Tool(calculator)])
    # 选项 4:对于 3.0.4b2 之前的版本(手动查找工具)
    # tools_dict = {"weather": weather, "calculator": calculator}
    # result = tools_dict[call.name](**call.args)
    print(f"结果: {result}")

使用原生工具调用

DSPy 适配器支持原生函数调用,该功能利用底层语言模型内置的工具调用能力,而非依赖基于文本的解析。对于支持原生函数调用的模型,此方法可以提供更可靠的工具执行和更好的集成效果。

适配器行为

不同的 DSPy 适配器对原生函数调用有不同的默认设置:

  • ChatAdapter - 默认使用 use_native_function_calling=False(依赖文本解析)
  • JSONAdapter - 默认使用 use_native_function_calling=True(使用原生函数调用)

您可以在创建适配器时,通过显式设置 use_native_function_calling 参数来覆盖这些默认值。

配置
import dspy
# 启用了原生函数调用的 ChatAdapter
chat_adapter_native = dspy.ChatAdapter(use_native_function_calling=True)
# 禁用了原生函数调用的 JSONAdapter
json_adapter_manual = dspy.JSONAdapter(use_native_function_calling=False)
# 配置 DSPy 使用该适配器
dspy.configure(lm=dspy.LM(model="openai/gpt-4o"), adapter=chat_adapter_native)

您可以启用 MLflow 跟踪来检查原生工具调用的使用情况。如果您在上方代码片段所示示例中,对 JSONAdapter 或启用了原生函数调用的 ChatAdapter 使用了该功能,您应该会看到原生函数调用参数 tools 的设置如下图所示:

模型兼容性

原生函数调用会使用 litellm.supports_function_calling() 自动检测模型支持情况。如果模型不支持原生函数调用,即使设置了 use_native_function_calling=True,DSPy 也会回退到手动基于文本的解析。

异步工具

DSPy 工具同时支持同步和异步函数。在使用异步工具时,您有两种选择:

为异步工具使用 acall

推荐的方法是在使用异步工具时使用 acall

import asyncio
import dspy
async def async_weather(city: str) -> str:
    """异步获取天气信息。"""
    await asyncio.sleep(0.1)  # 模拟异步 API 调用
    return f"{city}的天气晴朗"
tool = dspy.Tool(async_weather)
# 对异步工具使用 acall
result = await tool.acall(city="New York")
print(result)
在同步模式下运行异步工具

如果您需要从同步代码中调用异步工具,可以使用 allow_tool_async_sync_conversion 设置启用自动转换:

import asyncio
import dspy
async def async_weather(city: str) -> str:
    """异步获取天气信息。"""
    await asyncio.sleep(0.1)
    return f"{city}的天气晴朗"
tool = dspy.Tool(async_weather)
# 启用异步到同步的转换
with dspy.context(allow_tool_async_sync_conversion=True):
    # 现在您可以对异步工具使用 __call__
    result = tool(city="New York")
    print(result)

最佳实践

工具函数设计
  • 清晰的文档字符串:具有描述性文档的工具效果更好
  • 类型提示:提供清晰的参数和返回类型
  • 简单参数:使用基本类型(str, int, bool, dict, list)或 Pydantic 模型
def good_tool(city: str, units: str = "celsius") -> str:
    """
    获取特定城市的天气信息。
    Args:
        city: 要获取天气的城市名称
        units: 温度单位,'celsius' 或 'fahrenheit'
    Returns:
        描述当前天气状况的字符串
    """
    # 带有适当错误处理的实现
    if not city.strip():
        return "错误:城市名称不能为空"
    # 天气逻辑在此处...
    return f"{city}的天气:25°{units[0].upper()},晴朗"
在 ReAct 和手动处理之间选择

在以下情况使用 dspy.ReAct

  • 您需要自动推理和工具选择
  • 任务需要多次工具调用
  • 您需要内置的错误恢复
  • 您希望专注于工具实现而非编排

在以下情况使用手动工具处理:

  • 您需要对工具执行进行精确控制
  • 您需要自定义错误处理逻辑
  • 您希望最小化延迟
  • 您的工具没有返回值(void 函数)

DSPy 中的工具提供了一种强大的方式,可以将语言模型的能力扩展到文本生成之外。无论是使用全自动的 ReAct 方法还是手动工具处理,您都可以构建能够通过代码与世界交互的复杂代理。

DSPy 评估

一旦你有了初始系统,就该收集一个初始开发集,以便更系统地对其进行优化。即使是 20 个任务输入示例也可能很有用,而 200 个示例则效果更佳。根据你的指标,你可能只需要输入而完全不需要标签,或者你需要输入和系统的最终输出。(在 DSPy 中,你几乎永远不需要为程序中的中间步骤准备标签。)你或许可以在 Hugging Face 数据集或像 StackExchange 这样的自然来源上找到与你任务相邻的数据集。如果有许可证足够宽松的数据,我们建议你使用它们。否则,你可以手动标注几个示例,或者开始部署一个系统演示并以这种方式收集初始数据。

接下来,你应该定义你的 DSPy 指标。是什么让你系统的输出是好是坏?投入精力定义指标并随时间逐步改进它们;如果你无法定义,就很难持续改进。一个指标是一个函数,它接收来自你数据的示例和系统的输出,并返回一个分数。对于简单的任务,这可能只是“准确率”,例如简单的分类或简短问答任务。对于大多数应用,你的系统将生成长篇输出,因此你的指标将是一个较小的 DSPy 程序,用于检查输出的多个属性。第一次就把它做对是不太可能的:从简单的开始,然后迭代。

现在你已经有了一些数据和一个指标,就可以对你的管道设计运行开发评估,以了解它们的权衡利弊。查看输出和指标分数。这可能会让你发现任何主要问题,并且它将为你的下一步定义一个基线。

数据集

DSPy 是一个机器学习框架,因此在其中工作涉及训练集、开发集和测试集。对于你数据中的每个示例,我们通常区分三种类型的值:输入、中间标签和最终标签。你可以在没有任何中间或最终标签的情况下有效使用 DSPy,但你至少需要几个示例输入。

DSPy Example 对象

DSPy 中数据的核心数据类型是 Example。你将使用 Example 来表示训练集和测试集中的项目。

DSPy 的 Example 类似于 Python 的字典,但有一些实用的工具。你的 DSPy 模块将返回 Prediction 类型的值,它是 Example 的一个特殊子类。

当你使用 DSPy 时,会进行大量的评估和优化运行。你的每个数据点都将是 Example 类型:

qa_pair = dspy.Example(question="This is a question?", answer="This is an answer.")
print(qa_pair)
print(qa_pair.question)
print(qa_pair.answer)

输出:

Example({'question': 'This is a question?', 'answer': 'This is an answer.'}) (input_keys=None)
This is a question?
This is an anwser.

Example 可以有任何字段键和任何值类型,不过通常值是字符串。

object = Example(field1=value1, field2=value2, field3=value3, ...)

现在,你可以这样表示你的训练集,例如:

trainset = [dspy.Example(report="长报告1", summary="简短摘要1"), ...]
指定输入键

在传统的机器学习中,“输入”和“标签”是分开的。

在 DSPy 中,Example 对象有一个 with_inputs() 方法,可以将特定字段标记为输入。(其余的则只是元数据或标签。)

# 单个输入。
print(qa_pair.with_inputs("question"))
# 多个输入;除非你有意为之,否则请小心不要将你的标签标记为输入。
print(qa_pair.with_inputs("question", "answer"))

可以使用点(.)运算符来访问值。你可以通过 object.name 访问已定义对象 Example(name="John Doe", job="sleep") 中键 name 的值。

要访问或排除某些键,请使用 inputs()labels() 方法,它们分别返回仅包含输入键或非输入键的新 Example 对象。

article_summary = dspy.Example(article= "这是一篇文章。", summary= "这是一份摘要。").with_inputs("article")
input_key_only = article_summary.inputs()
non_input_key_only = article_summary.labels()
print("仅包含输入字段的 Example 对象:", input_key_only)
print("仅包含非输入字段的 Example 对象:", non_input_key_only)

输出:

仅包含输入字段的 Example 对象: Example({'article': '这是一篇文章。'}) (input_keys={'article'})
仅包含非输入字段的 Example 对象: Example({'summary': '这是一份摘要。'}) (input_keys=None)

指标

DSPy 是一个机器学习框架,因此你必须考虑用于评估(以跟踪你的进度)和优化(以便 DSPy 能让你的程序更有效)的自动指标。

什么是指标,以及如何为我的任务定义指标?

指标就是一个函数,它接收来自你数据的示例和系统的输出,并返回一个用于量化输出好坏的分数。是什么让你系统的输出是好是坏?

对于简单的任务,这可能只是“准确率”、“精确匹配”或“F1 分数”。对于简单的分类或简短问答任务,情况可能就是如此。

然而,对于大多数应用,你的系统将生成长篇输出。在这种情况下,你的指标很可能应该是一个较小的 DSPy 程序,用于检查输出的多个属性(很可能使用来自语言模型的 AI 反馈)。

第一次就把它做对是不太可能的,但你应该从简单的开始,然后迭代。

简单指标

DSPy 指标只是一个 Python 函数,它接收一个 example(例如,来自你的训练集或开发集)和来自你的 DSPy 程序的输出 pred,并输出一个浮点数(或整数或布尔值)分数。

你的指标还应该接受一个名为 trace 的可选第三个参数。你可以暂时忽略它,但如果你想将指标用于优化,它将启用一些强大的技巧。

下面是一个简单的指标示例,它比较 example.answerpred.answer。这个特定的指标将返回一个布尔值。

def validate_answer(example, pred, trace=None):
    return example.answer.lower() == pred.answer.lower()

有些人觉得这些内置的工具函数很方便:

  • dspy.evaluate.metrics.answer_exact_match
  • dspy.evaluate.metrics.answer_passage_match

你的指标可以更复杂,例如检查多个属性。下面的指标将在 traceNone 时(即用于评估或优化时)返回一个浮点数,否则(即用于为每个步骤自生成好的示例时)返回一个布尔值。

def validate_context_and_answer(example, pred, trace=None):
    # 检查真实标签和预测答案是否相同
    answer_match = example.answer.lower() == pred.answer.lower()
    # 检查预测答案是否来自检索到的上下文之一
    context_match = any((pred.answer.lower() in c) for c in pred.context)
    if trace is None: # 如果我们正在进行评估或优化
        return (answer_match + context_match) / 2.0
    else: # 如果我们正在进行自举,即为每个步骤自生成好的演示
        return answer_match and context_match

定义一个好的指标是一个迭代的过程,因此进行一些初步评估并查看你的数据和输出是关键。

评估

一旦你有了一个指标,就可以在一个简单的 Python 循环中运行评估。

scores = []
for x in devset:
    pred = program(**x.inputs())
    score = metric(x, pred)
    scores.append(score)

如果你需要一些工具函数,也可以使用内置的 Evaluate 工具。它可以帮助处理诸如并行评估(多线程)或向你展示输入/输出样本及指标分数等事情。

from dspy.evaluate import Evaluate
# 设置评估器,它可以在你的代码中重复使用。
evaluator = Evaluate(devset=YOUR_DEVSET, num_threads=1, display_progress=True, display_table=5)
# 启动评估。
evaluator(YOUR_PROGRAM, metric=YOUR_METRIC)

进阶:为你的指标使用 AI 反馈

对于大多数应用,你的系统将生成长篇输出,因此你的指标应该使用来自语言模型的 AI 反馈来检查输出的多个维度。
这个简单的签名可能会派上用场。

# 定义用于自动评估的签名。
class Assess(dspy.Signature):
    """根据指定的维度评估一条推文的质量。"""
    assessed_text = dspy.InputField()
    assessment_question = dspy.InputField()
    assessment_answer: bool = dspy.OutputField()

例如,下面是一个简单的指标,它检查生成的推文 (1) 是否正确回答了给定的问题,以及 (2) 是否具有吸引力。我们还检查 (3) len(tweet) <= 280 个字符。

def metric(gold, pred, trace=None):
    question, answer, tweet = gold.question, gold.answer, pred.output
    engaging = "被评估的文本是否构成一条独立、有吸引力的推文?"
    correct = f"文本应使用 `{answer}` 来回答 `{question}`。被评估的文本是否包含此答案?"
    correct =  dspy.Predict(Assess)(assessed_text=tweet, assessment_question=correct)
    engaging = dspy.Predict(Assess)(assessed_text=tweet, assessment_question=engaging)
    correct, engaging = [m.assessment_answer for m in [correct, engaging]]
    score = (correct + engaging) if correct and (len(tweet) <= 280) else 0
    if trace is not None: return score >= 2
    return score / 2.0

在编译时,trace 不为 None,我们希望对判断保持严格,因此只有当 score >= 2 时我们才会返回 True。否则,我们返回一个 1.0 分制的分数(即 score / 2.0)。

进阶:将 DSPy 程序用作你的指标

如果你的指标本身就是一个 DSPy 程序,那么最强大的迭代方法之一就是编译(优化)你的指标本身。这通常很容易,因为指标的输出通常是一个简单的值(例如,5分制的分数),所以指标的指标很容易定义,并且可以通过收集一些示例来进行优化。

进阶:访问追踪信息

当你的指标在评估运行中被使用时,DSPy 不会尝试追踪你程序的步骤。

但在编译(优化)期间,DSPy 会追踪你的语言模型调用。追踪信息将包含每个 DSPy 预测器的输入/输出,你可以利用它来验证优化过程中的中间步骤。

def validate_hops(example, pred, trace=None):
    hops = [example.question] + [outputs.query for *_, outputs in trace if 'query' in outputs]
    if max([len(h) for h in hops]) > 100: return False
    if any(dspy.evaluate.answer_exact_match_str(hops[idx], hops[:idx], frac=0.8) for idx in range(2, len(hops))): return False
    return True

DSPy 优化

一旦你拥有了一个系统以及评估它的方法,你就可以使用 DSPy 优化器来调整程序中的提示或权重。现在,将你的数据收集工作扩展到构建一个训练集和一个保留的测试集就很有用了,这要加上你之前一直用于探索的开发集。对于训练集(及其子集,验证集),你通常可以从 30 个示例中获得巨大价值,但目标应至少为 300 个示例。一些优化器只接受训练集。另一些则要求提供训练集和验证集。在为大多数基于提示的优化器分割数据时,我们建议采用与深度神经网络不同的划分方式:20% 用于训练,80% 用于验证。这种反向分配强调了稳定验证的重要性,因为基于提示的优化器经常会过拟合到小的训练集上。相比之下,dspy.GEPA 优化器遵循更标准的机器学习惯例:最大化训练集大小,同时让验证集的大小刚好足以反映下游任务(测试集)的分布。

在你完成了前几次优化运行后,你要么对一切都感到非常满意,要么已经取得了很大进展但对最终程序或指标有些不满意。此时,请回到第一步(在 DSPy 中编程),并重新审视那些主要问题。你是否很好地定义了你的任务?你是否需要为你的问题收集(或在线查找)更多数据?你是否想更新你的指标?你是否想使用一个更复杂的优化器?你是否需要考虑像 DSPy 断言这样的高级功能?或者,也许最重要的是,你是否想在你的 DSPy 程序本身中增加一些复杂性或步骤?你是否想按顺序使用多个优化器?

优化器

DSPy 优化器是一种算法,它可以调整 DSPy 程序的参数(即提示和/或语言模型的权重),以最大化你指定的指标(例如准确率)。一个典型的 DSPy 优化器需要三样东西:

  1. 你的 DSPy 程序。这可以是一个单独的模块(例如 dspy.Predict),也可以是一个复杂的多模块程序。
  2. 你的指标。这是一个评估你程序输出的函数,并为其分配一个分数(越高越好)。
  3. 一些训练输入。这些输入可能非常少(即只有 5 或 10 个示例)并且不完整(只有你程序的输入,没有任何标签)。

如果你碰巧有大量数据,DSPy 可以利用这些数据。但你可以从小处着手,并获得强大的结果。

DSPy 优化器会调整什么?它如何进行调整?

DSPy 中的不同优化器会通过以下方式来提升你程序的质量:

  • 为每个模块(如 dspy.Predict)合成好的少样本示例,例如 dspy.BootstrapRS [1]。
  • 提出并智能地探索每个提示更好的自然语言指令,例如 dspy.MIPROv2 [2] 和 dspy.GEPA [3]。
  • 为你的模块构建数据集,并用它们来微调你系统中的语言模型权重,例如 dspy.BootstrapFinetune [4]。

目前有哪些 DSPy 优化器?

可以通过 from dspy.teleprompt import * 来访问优化器。

自动少样本学习

这些优化器通过在发送给模型的提示中自动生成并包含优化的示例来扩展签名,从而实现少样本学习。

  • LabeledFewShot:简单地从提供的带标签的输入和输出数据点构建少样本示例。需要参数 k(提示中示例的数量)和 trainset,以便从中随机选择 k 个示例。
  • BootstrapFewShot:使用一个教师模块(默认为你的程序)为程序的每个阶段生成完整的演示,并结合 trainset 中的带标签示例。参数包括 max_labeled_demos(从 trainset 中随机选择的演示数量)和 max_bootstrapped_demos(由教师生成的额外示例数量)。自举过程使用指标来验证演示,只包含那些通过指标验证的演示。进阶用法:支持使用一个具有兼容结构的不同 DSPy 程序作为教师程序,以处理更困难的任务。
  • BootstrapFewShotWithRandomSearch:多次应用 BootstrapFewShot,并对生成的演示进行随机搜索,然后选择优化过程中表现最佳的程序。参数与 BootstrapFewShot 类似,但增加了 num_candidate_programs,它指定了在优化过程中评估的随机程序的数量,包括未编译程序的候选、LabeledFewShot 优化程序的候选、使用未打乱示例的 BootstrapFewShot 编译程序的候选以及 num_candidate_programs 个使用随机示例集的 BootstrapFewShot 编译程序的候选。
  • KNNFewShot:使用 k-近邻算法为给定的输入示例找到最近的训练示例演示。这些最近邻的演示随后被用作 BootstrapFewShot 优化过程的训练集。
自动指令优化

这些优化器为提示生成最优指令,在 MIPROv2 的情况下,还可以优化少样本演示集。

  • COPRO:为每个步骤生成并优化新指令,并使用坐标上升法(利用指标函数和 trainset 进行爬山)进行优化。参数包括 depth,即优化器运行的提示改进迭代次数。
  • MIPROv2:在每个步骤中生成指令和少样本示例。指令生成是数据感知和演示感知的。使用贝叶斯优化来有效地跨模块搜索生成指令/演示的空间。
  • SIMBA:使用随机小批量抽样来识别具有高输出变异性的挑战性示例,然后应用 LLM 进行内省分析失败原因,并生成自我反思的改进规则或添加成功的演示。
  • GEPA:使用 LM 来反思 DSPy 程序的轨迹,以识别哪些有效、哪些无效,并提出解决差距的提示。此外,GEPA 可以利用特定领域的文本反馈来快速改进 DSPy 程序。关于使用 GEPA 的详细教程可在 dspy.GEPA Tutorials 找到。
自动微调

此优化器用于微调底层 LLM。

  • BootstrapFinetune:将基于提示的 DSPy 程序提炼为权重更新。输出是一个具有相同步骤的 DSPy 程序,但每个步骤由微调后的模型执行,而不是由带提示的 LM 执行。有关完整示例,请参阅分类微调教程。
程序转换
  • Ensemble:将一组 DSPy 程序集成,并使用完整集或随机抽样子集到单个程序中。

我应该使用哪个优化器?

最终,找到要使用的“正确”优化器以及任务的最佳配置需要实验。在 DSPy 中取得成功仍然是一个迭代的过程——要在你的任务上获得最佳性能,需要你去探索和迭代。话虽如此,以下是关于入门的一般性指导:

  • 如果你只有很少的示例(大约 10 个),请从 BootstrapFewShot 开始。
  • 如果你有更多数据(50 个或更多示例),请尝试 BootstrapFewShotWithRandomSearch
  • 如果你只想进行指令优化(即你想保持提示为 0-shot),请使用配置为 0-shot 优化的 MIPROv2
  • 如果你愿意使用更多的推理调用来执行更长的优化运行(例如 40 次或更多次试验),并且有足够的数据(例如 200 个或更多示例以防止过拟合),那么可以尝试 MIPROv2
  • 如果你已经能够将其中一个优化器与大型 LM(例如 7B 参数或更大)一起使用,并且需要一个非常高效的程序,那么可以使用 BootstrapFinetune 为你的任务微调一个小型 LM。

如何使用优化器?

它们都共享这个通用接口,只是关键字参数(超参数)有所不同。完整的参数列表可以在 API 参考中找到。让我们以最常用的 BootstrapFewShotWithRandomSearch 为例来看一下。

from dspy.teleprompt import BootstrapFewShotWithRandomSearch
# 设置优化器:我们想要“自举”(即自我生成)程序步骤的 8-shot 示例。
# 优化器将重复此过程 10 次(加上一些初始尝试),然后在开发集上选择其最佳尝试。
config = dict(max_bootstrapped_demos=4, max_labeled_demos=4, num_candidate_programs=10, num_threads=4)
teleprompter = BootstrapFewShotWithRandomSearch(metric=YOUR_METRIC_HERE, **config)
optimized_program = teleprompter.compile(YOUR_PROGRAM_HERE, trainset=YOUR_TRAINSET_HERE)

保存和加载优化器输出

在通过优化器运行程序后,保存它也很有用。稍后,可以从文件加载程序并用于推理。为此,可以使用 loadsave 方法。

optimized_program.save(YOUR_SAVE_PATH)

生成的文件是纯文本 JSON 格式。它包含源程序中的所有参数和步骤。你随时可以读取它,查看优化器生成了什么。

要从文件加载程序,你可以从该类实例化一个对象,然后对其调用 load 方法。

loaded_program = YOUR_PROGRAM_CLASS()
loaded_program.load(path=YOUR_SAVE_PATH)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐