跳到内容

订阅我们的时事通讯以获取更新和技巧

使用 Instructor 进行 OpenAI API 模型蒸馏

OpenAI 最近推出了一项名为 API 模型蒸馏 的新功能,它允许开发者创建针对特定用例定制的自定义模型。当与 Instructor 的结构化输出能力结合使用时,此功能尤其强大。在本文中,我们将探讨如何利用 API 模型蒸馏和 Instructor 来创建更高效、更专业的模型。

糟糕的 Schema 可能会破坏你的 LLM 结构化输出

使用错误的响应模型,你可能会损失高达 60% 的性能提升。响应模型对 Claude 和 GPT-4o 的模型性能影响巨大,无论你使用的是 JSON 模式还是工具调用。

使用正确的响应模型有助于确保你的模型以正确的语言响应,或在提取视频时间戳时防止幻觉

我们决定通过在 GSM8k 数据集上对 Claude 和 GPT-4o 进行基准测试来调查此事,发现

  1. 字段命名对性能有显著影响 - 将单个字段名从 final_choice 更改为 answer 使模型准确率从 4.5% 提高到 95%。我们在响应模型中组织和命名字段的方式可以从根本上改变模型解释和响应查询的方式。
  2. 思维链显著提升性能 - 添加一个 reasoning 字段使模型在 GSM8k 数据集上的准确率提高了 60%。模型在逐步解释其逻辑时表现更好。
  3. 小心使用 JSON 模式 - 在重命名字段时,JSON 模式表现出比工具调用多 50% 的性能变化。不同的响应模型在 JSON 模式和工具调用之间表现出不同水平的性能,表明 JSON 模式需要更仔细的优化。

确保语言模型的时间戳格式一致

Gemini 可以理解语言模型输出中的时间戳,但它们可能不一致。视频内容时间戳在 HH:MM:SS 和 MM:SS 格式之间变化,导致解析错误和计算问题。本文提出了一种处理剪辑和电影时间戳的技术,避免格式问题。

我们结合了 Pydantic 的数据验证和自定义解析,以实现一致的时间戳处理。你将学习处理任何格式的时间戳,减少视频内容工作流程中的错误。这有点像我们通过添加一个简单字段来确保多语言摘要中的语言匹配

本文提供了一个使用 Pydantic 的解决方案,以改进语言模型项目中时间戳的处理。此方法解决了格式不一致问题,并支持时间戳处理。

Instructor 提案:集成 Jinja 模板

作为 Instructor 的创建者,我一直致力于保持产品开发流程化,避免不必要的复杂性。然而,我现在确信是时候在我们的数据结构中加入更好的模板功能了,特别是集成 Jinja。

这个决定有多个目的

  1. 它解决了我的提示格式化需求日益增长的复杂性
  2. 它使我们能够与标准库区别开来,同时增加经过验证的实用性。
  3. 它与我在生产代码和客户端代码中一直采用的实践相符。
  4. 它提供了一个机会,可以引入已在 Instructor 私有版本中测试过的 API 更改。

Jinja 是正确的选择的原因

  1. 格式化能力
  2. 提示格式化复杂性增加了。
  3. 列表迭代和条件实现对于格式化是必要的。
  4. 这改进了块生成、少样本学习和动态规则。

  5. 验证

  6. Jinja 模板变量用于渲染和验证目的。
  7. Pydantic 的验证上下文允许在验证函数中访问模板变量。

  8. 版本控制和日志记录

  9. 渲染变量的分离增强了提示的版本控制和日志记录。
  10. 模板变量差异比对简化了提示更改的比较。

通过将 Jinja 集成到 Instructor 中,我们不仅仅是添加一个功能;我们正在增强处理复杂格式化、改进验证流程以及简化版本控制和日志记录的能力。这一新增功能将显著提升 Instructor 的能力和灵活性,使其成为用户更强大的工具。

增强格式化能力

在 Instructor 中,我们建议在我们的创建方法中实现一个新的 context 关键字。此新增功能将允许用户使用提供的上下文渲染提示,利用 Jinja 的模板功能。以下是其工作原理

  1. 用户将 context 字典传递给创建方法。
  2. 提示模板以 Jinja 语法编写,在消息的 content 字段中定义。
  3. Instructor 使用提供的上下文渲染提示,填入模板变量。

这种方法提供以下好处

  • 提示结构与动态内容分离
  • 使用条件语句和循环管理复杂提示
  • 在不同上下文中重用提示模板

让我们看一个例子来说明此功能

client.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": """
                You are a {{ role }} tasks with the following question 

                <question>
                {{ question }}
                </question>

                Use the following context to answer the question, make sure to return [id] for every citation:

                <context>
                {% for chunk in context %}
                  <context_chunk>
                    <id>{{ chunk.id }}</id>
                    <text>{{ chunk.text }}</text>
                  </context_chunk>
                {% endfor %}
                </context>

                {% if rules %}
                Make sure to follow these rules:

                {% for rule in rules %}
                  * {{ rule }}
                {% endfor %}
                {% endif %}
            """,
        },
    ],
    context={
        "role": "professional educator",
        "question": "What is the capital of France?",
        "context": [
            {"id": 1, "text": "Paris is the capital of France."},
            {"id": 2, "text": "France is a country in Europe."},
        ],
        "rules": ["Use markdown."],
    },
)

验证

让我们考虑一种从文本中编辑词语的场景。通过使用 ValidationInfo 访问上下文并将其传递给验证器和模板,我们可以实现一个处理敏感信息的系统。这种方法使我们能够

  1. 验证输入以确保不包含违禁词。
  2. 使用正则表达式编辑模式。
  3. 向语言模型提供关于词语使用限制的指示。

以下是使用 Pydantic 验证器演示此概念的示例

from pydantic import BaseModel, ValidationInfo, field_validator

class Response(BaseModel):
    text: str

    @field_validator('text')
    @classmethod
    def no_banned_words(cls, v: str, info: ValidationInfo):
        context = info.context
        if context:
            banned_words = context.get('banned_words', set())
            banned_words_found = [word for word in banned_words if word.lower() in v.lower()]
            if banned_words_found:
                raise ValueError(f"Banned words found in text: {', '.join(banned_words_found)}, rewrite it but just without the banned words")
        return v

    @field_validator('text')
    @classmethod
    def redact_regex(cls, v: str, info: ValidationInfo):
        context = info.context
        if context:
            redact_patterns = context.get('redact_patterns', [])
            for pattern in redact_patterns:
                v = re.sub(pattern, '****', v)
        return v

response = client.create(
    model="gpt-4o",
    response_model=Response,
    messages=[
        {
            "role": "user", 
            "content": """
                Write about a {{ topic }}

                {% if banned_words %}
                You must not use the following banned words:

                <banned_words>
                {% for word in banned_words %}
                * {{ word }}
                {% endfor %}
                </banned_words>
                {% endif %}
              """
        },
    ],
    context={
        "topic": "jason and now his phone number is 123-456-7890"
        "banned_words": ["jason"],
        "redact_patterns": [
            r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",  # Phone number pattern
            r"\b\d{3}-\d{2}-\d{4}\b",          # SSN pattern
        ],
    },
    max_retries=3,
)

print(response.text)
# > While i can't say his name anymore, his phone number is ****

更好的版本控制和日志记录

通过将提示模板和变量分离,我们获得了几个优势

  1. 版本控制:我们现在可以对模板进行版本控制,并为给定的提示检索适当的模板。这有助于更好地管理模板历史记录、差异比对和比较。

  2. 增强的日志记录:这种分离有助于结构化日志记录,使得调试以及与各种日志接收器、数据库和 OpenTelemetry 等可观测性工具的集成更加容易。

  3. 安全性:变量中的敏感信息可以与模板分开处理,从而实现更好的访问控制和数据保护。

这种关注点分离符合软件设计的最佳实践,从而形成一个更易于维护、可扩展和稳健的系统,用于管理提示及其相关数据。

上下文也是 Pydantic 模型带来的副作用

由于它们只是 Python 对象,我们可以使用 Pydantic 模型来验证上下文,并控制它们的渲染方式,这样即使是秘密信息也可以动态渲染!考虑使用密钥字符串将敏感信息传递给 LLM。

from pydantic import BaseModel, SecretStr


class UserContext(BaseModel):
    name: str
    address: SecretStr


class Address(BaseModel):
    street: SecretStr
    city: str
    state: str
    zipcode: str


def normalize_address(address: Address):
    context = UserContext(username="scolvin", address=address)
    address = client.create(
        model="gpt-4o",
        messages=[
            {
                "role": "user",
                "content": "{{ user.name }} is `{{ user.address.get_secret_value() }}`, normalize it to an address object",
            },
        ],
        context={"user": context},
    )
    print(context)
    #> UserContext(username='jliu', address="******")
    print(address)
    #> Address(street='******', city="Toronto", state="Ontario", zipcode="M5A 0J3")
    logger.info(
        f"Normalized address: {address}",
        extra={"user_context": context, "address": address},
    )
    return address

这种方法提供几个优势

  1. 安全日志记录:你可以放心地记录你的模板变量,而不会冒敏感信息泄露的风险。
  2. 类型安全:Pydantic 模型提供类型检查和验证,降低出错风险。
  3. 灵活性:你可以轻松控制不同类型的数据在模板中如何显示或使用。

我为什么应该使用提示缓存?

开发者在使用大上下文时经常面临两个主要挑战——响应时间慢和成本高。当我们需要随着时间推移进行多次此类调用时,情况尤其如此,这严重影响了我们应用的成本和延迟。借助 Anthropic 新的提示缓存功能,我们可以轻松解决这两个问题。

由于新功能仍在测试阶段,我们将在其普遍可用后才将其集成到 instructor 中。在此期间,我们整理了一份关于如何在你的应用中使用此功能的快速入门指南。

现在支持 Gemini 的结构化输出

我们很高兴地宣布,instructor 现在支持使用工具调用对 Gemini SDK 和 VertexAI SDK 进行结构化输出。

特别鸣谢 Sonal 对 Gemini 工具调用支持的贡献。

让我们通过一个简单示例来了解如何使用这些新功能

安装

开始之前,安装最新版本的 instructor。根据你使用的是 Gemini 还是 VertexAI,你应该安装以下内容

pip install "instructor[google-generativeai]"
pip install "instructor[vertexai]"

这确保你拥有使用 Gemini 或 VertexAI SDK 与 instructor 所需的依赖项。

基于两个主要原因,我们推荐使用 Gemini SDK 而不是 VertexAI SDK。

  1. 与 VertexAI SDK 相比,Gemini SDK 为开发者提供了每日 15 亿个 token 的免费配额。
  2. Gemini SDK 设置起来要容易得多,你只需要一个可以在 GCP 控制台中生成的 GOOGLE_API_KEY。另一方面,VertexAI SDK 需要一个 credentials.json 文件或 OAuth 集成才能使用。

开始

借助我们提供商无关的 API,你可以使用相同的接口与两个 SDK 交互,唯一改变的是我们如何初始化客户端本身。

在运行以下代码之前,你需要确保已在你的 shell 中将 Gemini API 密钥设置为别名 GOOGLE_API_KEY

import instructor
import google.generativeai as genai
from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int


client = instructor.from_gemini(
    client=genai.GenerativeModel(
        model_name="models/gemini-1.5-flash-latest",  # (1)!
    )
)

resp = client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": "Extract Jason is 25 years old.",
        }
    ],
    response_model=User,
)

print(resp)
#> name='Jason' age=25
  1. 当前支持工具调用的 Gemini 模型是 gemini-1.5-flash-latestgemini-1.5-pro-latest

我们可以使用 VertexAI SDK 实现类似的功能。为此,你需要对 VertexAI 进行身份验证。

此处有一些说明,但我发现最简单的方法是直接下载 GCloud CLI 并运行 gcloud auth application-default login

import instructor
import vertexai  # type: ignore
from vertexai.generative_models import GenerativeModel  # type: ignore
from pydantic import BaseModel

vertexai.init()


class User(BaseModel):
    name: str
    age: int


client = instructor.from_vertexai(
    client=GenerativeModel("gemini-1.5-pro-preview-0409"),  # (1)!
)


resp = client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": "Extract Jason is 25 years old.",
        }
    ],
    response_model=User,
)

print(resp)
#> name='Jason' age=25
  1. 当前支持工具调用的 Gemini 模型是 gemini-1.5-flash-latestgemini-1.5-pro-latest

我应该使用结构化输出吗?

OpenAI 最近宣布了结构化输出功能,可确保生成的响应匹配任何任意提供的 JSON Schema。在他们的公告文章中,他们承认该功能受到了 instructor 等库的启发。

主要挑战

如果你正在构建复杂的 LLM 工作流程,你很可能已经考虑将 OpenAI 的结构化输出作为 instructor 的潜在替代方案。

但在此之前,还有三个关键挑战

  1. 有限的验证和重试逻辑:结构化输出确保符合 Schema,但不保证内容有用。你可能会得到格式完美但无用的响应。
  2. 流式处理挑战:使用 SDK 从流式响应中解析原始 JSON 对象容易出错且效率低下。
  3. 不可预测的延迟问题:结构化输出存在随机延迟峰值,可能导致响应时间增加近 20 倍。

此外,采用结构化输出会将你锁定在 OpenAI 的生态系统中,限制你试验更适合特定用例的各种模型或提供商的能力。

这种供应商锁定增加了对提供商中断的脆弱性,可能导致应用程序停机和 SLA 违规,从而损害用户信任并影响你的商业声誉。

在本文中,我们将展示 instructor 如何通过验证失败时的自动重新请求、对经验证流式数据的自动支持等功能来解决其中许多挑战。