跳到内容

Instructor 提案:集成 Jinja 模板

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

这个决定有多种目的

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

为什么 Jinja 是正确的选择

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

  5. 验证

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

  8. 版本控制和日志记录

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

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

增强格式化能力

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

  1. 用户将一个 context 字典传递给 create 方法。
  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 模型来验证上下文并控制它们的渲染方式,这样即使是秘密信息也可以动态渲染!考虑使用秘密字符串将敏感信息传递给 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. 灵活性:你可以轻松控制不同类型的数据如何在模板中显示或使用。