Instructor 提案:集成 Jinja 模板¶
作为 Instructor 的创建者,我一直致力于保持产品开发流程精简,避免不必要的复杂性。然而,我现在深信是时候将更好的模板功能整合到我们的数据结构中,特别是通过集成 Jinja。
这个决定有多种目的
- 它解决了我在 prompt 格式化方面日益增长的复杂需求
- 它使我们能够在提供已验证实用性的同时,与标准库区别开来。
- 它与我一直在生产环境和客户端代码中采用的实践一致。
- 它提供了引入已经在 Instructor 私有版本中测试过的 API 更改的机会。
为什么 Jinja 是正确的选择¶
- 格式化能力
- Prompt 格式化复杂性增加了。
- 列表迭代和条件实现对于格式化是必需的。
-
这改进了 chunk 生成、few-shot 示例和动态规则。
-
验证
- Jinja 模板变量用于渲染和验证目的。
-
Pydantic 的验证上下文允许在验证函数中访问模板变量。
-
版本控制和日志记录
- 渲染变量分离增强了 prompt 的版本控制和日志记录。
- 模板变量的差异比较简化了 prompt 更改的比较。
通过将 Jinja 集成到 Instructor 中,我们不仅仅是增加一个功能;我们正在增强处理复杂格式化、改进验证流程以及简化版本控制和日志记录的能力。这一新增功能将显著提升 Instructor 的强大性和灵活性,使其成为对用户而言更加健壮的工具。
增强格式化能力¶
在 Instructor 中,我们提议在我们的 create 方法中实现一个新的 context
关键字。这一新增功能将允许用户使用提供的上下文来渲染 prompt,利用 Jinja 的模板能力。工作原理如下
- 用户将一个
context
字典传递给 create 方法。 - 使用 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 及其相关数据。
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
这种方法提供了几个优势
- 安全日志记录:你可以自信地记录模板变量,而不用担心敏感信息暴露的风险。
- 类型安全:Pydantic 模型提供类型检查和验证,降低了出错的风险。
- 灵活性:你可以轻松控制不同类型的数据如何在模板中显示或使用。