跳到内容

LLM 技术

不良 Schema 可能破坏您的 LLM 结构化输出

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

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

我们决定通过在 GSM8k 数据集上对 Claude 和 GPT-4o 进行基准测试来调查这一点,结果发现:

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

Instructor 提案:集成 Jinja 模板

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

这一决定有多种目的:

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

为什么 Jinja 是正确的选择

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

  5. 验证

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

  8. 版本控制和日志记录

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

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

增强格式化能力

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

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

这种方法提供以下优势:

  • 将 prompt 结构与动态内容分离
  • 管理包含条件和循环的复杂 prompt
  • 在不同上下文中复用 prompt 模板

我们来看一个示例来说明此功能:

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 ****

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

通过分离 prompt 模板和变量,我们获得以下几个优势:

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

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

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

这种关注点分离遵循软件设计的最佳实践,从而形成一个更易维护、可扩展且健壮的系统,用于管理 prompt 及相关数据。

Context 也是 Pydantic 模型的附带好处

由于它们只是 Python 对象,我们可以使用 Pydantic 模型来验证上下文并控制其渲染方式,因此即使是秘密信息也可以动态渲染!考虑使用 secret string 将敏感信息传递给 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. 灵活性:您可以轻松控制不同类型的数据在模板中的显示或使用方式。

现已支持 Gemini 的结构化输出

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

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

我们来看一个如何使用这些新功能的简单示例:

安装

首先,安装最新版本的 instructor。根据您使用的是 Gemini 还是 VertexAI,您应该安装以下依赖:

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

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

我们推荐使用 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 Key 设置为别名 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

为什么 Instructor 是从 LLM 获取 JSON 的最佳方式

像 GPT 这样的大型语言模型 (LLM) 功能强大,但让它们返回格式良好的 JSON 可能具有挑战性。这正是 Instructor 库的闪光点。Instructor 允许您使用 Python 类型注解和 Pydantic 模型轻松地将 LLM 输出映射到 JSON 数据。

Instructor 使从 GPT-3.5、GPT-4、GPT-4-Vision 等 LLM 以及 Mistral/MixtralOllamallama-cpp-python 等开源模型中轻松获取 JSON 等结构化数据成为可能。

它以其简洁性、透明度和以用户为中心的设计脱颖而出,构建在 Pydantic 之上。Instructor 帮助您管理验证上下文、使用 Tenacity 进行重试以及流式处理列表部分响应。

使用 Instructor 通过时间过滤器增强 RAG

检索增强生成 (RAG) 系统通常需要处理包含时间限制的查询,例如“上个季度发布了哪些新功能?”或“显示过去一周的支持工单”。有效的时间过滤对于提供准确、相关的响应至关重要。

Instructor 是一个 Python 库,可简化将大型语言模型 (LLM) 与数据源和 API 集成。它允许使用 Pydantic 定义结构化输出模型,这些模型可用作 prompts 或解析 LLM 输出。

与 Langsmith 的无缝支持

人们普遍误认为 LangChain 的 LangSmith 仅与 LangChain 的模型兼容。实际上,LangSmith 是一个统一的 DevOps 平台,用于开发、协作、测试、部署和监控 LLM 应用程序。在本博客中,我们将探讨如何将 LangSmith 与 instructor 一起用于增强 OpenAI 客户端。

生成器和 LLM 流式传输

延迟至关重要,尤其是在电子商务和像 ChatGPT 这样的新型聊天应用程序中。流式传输是一种解决方案,它使我们能够在无需更快的响应时间的情况下增强用户体验。

是什么让流式传输成为可能?生成器!

使用 Python 中的 asyncioInstructor 异步处理 OpenAI

今天,我将向您介绍在 Python 中使用 asyncio 的各种方法。我们将使用 instructor 将其应用于批量数据处理,并学习如何使用 asyncio.gatherasyncio.as_completed 进行并发数据处理。此外,我们将探讨如何使用 asyncio.Semaphore 限制对服务器的并发请求数量。