作为 Instructor 的创建者,我一直致力于保持产品开发的精简并避免不必要的复杂性。然而,我现在确信是时候在我们的数据结构中融入更好的模板化,特别是通过集成 Jinja。
这一决定有多种目的:
- 它解决了我的 prompt 格式化需求日益增长的复杂性
- 它使我们能够在提供成熟实用功能的同时,与标准库区分开来。
- 它与我在生产代码和客户端代码中一直采用的实践相符。
- 它提供了一个机会,引入在 Instructor 私有版本中经过测试的 API 更改。
- 格式化能力
- Prompt 格式化复杂性增加。
- 列表迭代和条件实现对于格式化是必需的。
-
这改进了块生成、少样本和动态规则。
-
验证
- Jinja 模板变量用于渲染和验证目的。
-
Pydantic 的验证上下文允许在验证函数中访问模板变量。
-
版本控制和日志记录
- 渲染变量分离增强了 prompt 的版本控制和日志记录。
- 模板变量对比简化了 prompt 更改的比较。
通过将 Jinja 集成到 Instructor 中,我们不仅增加了功能;我们正在增强处理复杂格式化、改进验证过程以及简化版本控制和日志记录的能力。这一新增功能将显著提升 Instructor 的能力和灵活性,使其成为对用户而言更加强大的工具。
在 Instructor 中,我们建议在我们的创建方法中实现一个新的 context
关键字。这一新增功能将允许用户使用提供的上下文渲染 prompt,利用 Jinja 的模板能力。其工作原理如下:
- 用户将
context
字典传递给创建方法。 - 以 Jinja 语法编写的 prompt 模板在消息的
content
字段中定义。 - 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
访问上下文并将其传递给验证器和模板,我们可以实现一个处理敏感信息的系统。这种方法使我们能够:
- 验证输入以确保不包含禁用词。
- 使用正则表达式编辑模式。
- 向语言模型提供关于词语使用限制的指示。
这里有一个使用 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 模板和变量,我们获得以下几个优势:
-
版本控制:我们现在可以对模板进行版本控制,并为给定的 prompt 检索相应的模板。这有助于更好地管理模板历史、差异比较。
-
增强的日志记录:分离有助于结构化日志记录,使调试更容易,并便于与各种日志接收器、数据库以及 OpenTelemetry 等可观测性工具集成。
-
安全性:变量中的敏感信息可以与模板分开处理,从而实现更好的访问控制和数据保护。
这种关注点分离遵循软件设计的最佳实践,从而形成一个更易维护、可扩展且健壮的系统,用于管理 prompt 及相关数据。
由于它们只是 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
这种方法提供以下几个优势:
- 安全日志记录:您可以放心地记录您的模板变量,而无需担心敏感信息暴露的风险。
- 类型安全:Pydantic 模型提供类型检查和验证,降低出错风险。
- 灵活性:您可以轻松控制不同类型的数据在模板中的显示或使用方式。