使用 Python Guardrails 提高 LLM 输出的可靠性

使用 Python Guardrails 提高 LLM 输出的可靠性

Barry Lv6

利用验证函数防止您的 LLM 输出崩溃

合理使用IF可以使您的LLM输出更可靠

虽然在创造力和解决复杂任务方面表现出色,但LLM往往难以遵循严格的规则,并且经常提供略微超出设定边界的答案。在构建应用程序时,这一缺陷可能导致失败和荒谬的答案,从而使用户放弃。

好消息是,编写严格规则是任何程序员的基本技能,而仅仅因为我们现在使用LLM并不意味着我们忘记了如何使用if语句。

如果某些逻辑可以通过几行代码强制执行,您可能不需要将其输入到一个万亿参数模型中。

LLM 最后一公里交付问题

控制输出在将 LLM 与应用的其他组件(如 API)集成时尤其重要。在开发一个 text_to_params 工具,将用户的平面需求描述转换为搜索参数时,我意识到尽管生成的参数通常是正确的,但它们与 API 函数的不兼容性却过于频繁。

尽管可以访问完整的 API 文档,并且超过 90% 的参数是正确的,但其中一个参数会完全破坏答案。这些例子有些微不足道,例如将 price_per_metertotal_price 混淆,为数值过滤器添加单位如 m2,或者在价格标签表明为月租时寻找出售的优惠。

经过一番长时间的斗争,试图在提示中处理所有这些情况并将其扩展超过 1000 个标记后,我意识到可以通过几个 Python 验证函数更高效地处理它们。

正如道路护栏可以防止汽车在蜿蜒的山路上掉下悬崖,Python 护栏可以拯救您的 LLM 免于发生严重故障。

这个故事是关于构建生产就绪的 LLM 驱动应用程序系列中的第二篇;我鼓励您阅读介绍文章,以获取优化提示

使用 Python guardrails 验证 LLM 输出的优势

  • 处理边缘情况而不增加提示大小
  • 确保与应用程序其余部分的最后一公里兼容性
  • 使 LLM 应用程序对意外用户行为更具抵抗力
  • 提高性能并支持使用更小的模型

继续使用 text-to-params 工具示例,我进行了一个实验,比较了在一组特别棘手的问题上添加和不添加 guardrails 的工具准确性。

按模型的搜索参数准确性

增加约 300 行 Python 防护功能使得 GPT 3.5 的表现超过了 GPT 4.0,后者的成本高出 20 倍且速度慢 2-3 倍。

如何在LangChain中混合使用Python和LLM调用

LangChain允许您方便地将LLM调用、输出解析和普通Python函数结合在一起,通过将所有这些组件组合成一个RunnableSequence,这几乎是每个链的基础。

在下面的示例代码中,我们专注于一个简化的text-to-params工具,将用户需求转换为带有过滤器的字典。当处理结构化的JSON输出时,使用Python转换LLM输出是最有效的。然而,相同的方法也可以用于文本答案,例如,使用较小的NLP模型,如分类器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from langchain_core.output_parsers import JsonOutputParser
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, temperature=0, model="gpt-4o")

prompt = """
SYSTEM
Convert description into filter parameters with key:value pairs of $FEATURE and $VALUE.
Allowed filter features: [`area_min`, `area_max`, `price_min`, `price_max`]

Answers must be a valid $JSON_BLOB, as shown:


Begin! Reminder to ALWAYS respond with a valid json blob .

New message
{input}

(reminder to respond in a JSON blob no matter what)

"""


prompt = ChatPromptTemplate.from_template(prompt)


def search_params_guardrails(tool_output: dict) -> str:
min_sale_price = 100000
filters = tool_output.get("filters", {})

if 'price_max' in filters.keys():
price_threshold = filters["price_max"]
filters["offer_type"] = 'sale' if price_threshold >= min_sale_price else 'rent'

return {"filters":filters}


在示例代码的第一部分中,我们准备我们的链组件——OpenAI模型、提示模板、JsonOutputParser(与提示指令一起确保我们获得有效的JSON)以及一个基本的api_params_guardrail函数,该函数根据LLM设置的其他过滤器添加offer\_type字段。

1
chain_text_to_params = prompt | llm | JsonOutputParser() | search_params_guardrails

然后我们配置我们的链,定义准备好的组件的执行顺序。每个组件都获取上一步的输出并继续处理,最终在链的末尾提供结果。

一旦我们的链准备好,我们可以使用invoke调用它,描述我们需要的属性类型作为输入。这个简单的提示不仅正确地从文本中提取了信息,还将其解析为有效的字典,并基于函数而不是提示指令添加了offer_type参数。

三个通过 Python 函数提高 LLM 结果的实用示例

继续使用我准备的 text-to-params 工具,我准备了一些技巧,这些技巧有助于生成更可靠和兼容的 LLM 输出,并配有一些验证函数。

1. LLMs倾向于过于字面化,因此为它们的回答添加一些智能

一旦您开始测试初始的LLM应用程序,您会迅速意识到初始提示没有解决所有不寻常的用户查询和边缘情况,或者LLM缺少一些领域知识。

在处理Mieszko时,我面临的一个问题是将用户需求转换为搜索过滤器JSON的工具过于字面化。

例如,如果用户将他的需求描述为**一个面积约为50m2,预算在3500–3600之间的公寓**,LLM生成的过滤器如下:

1
2
3
4
search_filters = {
"price":{"min":3500, "max":3600},
"area":{"min":50, "max":50}
}

尽管它们在逻辑上并不错误,但用户也会很高兴看到一个每月3200 zł的55m2公寓,过滤器应该更像这样:

1
2
3
4
search_filters = {
"price":{"max":3600},
"area":{"min":45, "max":55}
}

我尝试在提示中添加更多指令,但由于有十几个搜索参数以及大多数参数的一些自定义验证规则,这将增加几百个标记。添加一个简单的保护函数来修改工具输出则以更可靠的方式产生了相同的结果。

以下是一个仅限于上述两个问题的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
def search_params_guardrails(params):
#Remove price min threshold if it is less than 20% from max
if 'price' in params and all(key in params["price"] for key in ['min', 'max'])
if (params['price']['max'] - params['price']['min']) / params['price']['max'] <= 0.20:
del params['price']['min']

# Spread area thresholds by +/-10% if both 'min' and 'max' are specified and equal
if 'area' in params and all(key in params["area"] for key in ['min', 'max'])
if params['area']['min'] == params['area']['max']:
params['area']['min'] *= 0.9
params['area']['max'] *= 1.1

return params

2. 验证模糊匹配是否可以替代对 LLM 进行数十个类别选择的指导

在构建搜索参数工具时,处理分类字段是另一个看似微不足道的问题。如果一个字段只有几个可能的值,例如 ‘offer_type‘,其中唯一的可能标签是 ‘sale‘ 或 ‘rent‘,你可以很容易地将其纳入提示说明中。

随着字段的细分,这变得更加复杂。在我的案例中,它是华沙的区(18 个实例)和社区(143 个实例)。将所有这些标签添加到提示中将增加 1,058 个标记,在使用 GPT 4-turbo 时,每次调用的费用将为 1 美分。

即使是 GPT 3.5 也对华沙的地形有基本的理解,因此我们不需要在提示中提供所有这些信息。关键挑战在于,它有时会变更拼写或直接复制用户输入中的错误或缩写。

为了确保区和社区与搜索 API 中可用的值兼容,我使用了模糊匹配来限制 LLM 设置的参数到一个经过筛选的列表,而不需要通过提示实际传递所有允许的标签。

这是一个简化的模糊匹配代码,用于验证分类标签是否与已知的有效标签集匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from fuzzywuzzy import process
import json

def fuzzy_match(label: str, allowed_labels:list, score_cutoff=90, allowed_len_diff = 3):
best_match = process.extractOne(label, allowed_labels, score_cutoff=score_cutoff)
if best_match and abs(len(best_match[0]) - len(label)) > allowed_len_diff:
# Remove matches with larger len difference to avoid spurious matches
best_match=None

return best_match[0] if best_match else None

def validate_category_feature(labels: list, allowed_labels:list)->list:
labels_val = [fuzzy_match(label, allowed_labels) for label in labels]
labels_val = [item for item in labels_val if item is not None]

return labels_val

def validate_category_filters(params:dict, cat_features: list)->dict:
#更改为包含每个cat_feature的允许标签列表的json路径
path = "allowed_labels_per_feature_map.json"
with open(path, "r") as f:
allowed_labels_per_feature = json.load(f)

for feature in cat_features:
allowed_labels = allowed_labels_per_feature[feature]
labels = params.get(feature, [])
if labels:
labels_val = validate_category_feature(labels, allowed_labels)
if labels_val:
params[feature] = labels_val


return params

同样的方法不限于地理区域,也可以在其他领域实施,例如用于电子商务中的品牌或型号的策划。它可以通过类似的方式实施更广泛的保护措施。

1
2
3
4
5
6
7
8
def search_params_guardrails(params):
... some previous logic
# make sure categorical filters are compatible with API allowed values
params = validate_category_filters(params, ["district", "subdistrict"])

... some following logic

return params

3. 通过放宽过于严格的过滤条件重试失败的API调用

最后一公里交付问题的另一个方面是,LLM在尝试尽可能多地满足用户请求的过滤条件时可能会过于严格。例如,当用户要求一个**低预算下的舒适公寓**时,LLM决定添加所有可能的设施,如淋浴、浴缸、洗碗机和阳台。结合低价要求,最终传递给API调用的过滤条件可能导致结果为零。

我们可以给模型一个重试设置参数的机会,但这意味着又一次LLM调用,这会增加成本和延迟。因此,我决定重试API调用,同时依次放宽一组最限制的过滤条件,希望最终能获得有效的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def get_offers_count_with_loosening_filters(params):

# List of optional filters to drop in case of empty response
optional_filters = ["interior_standard", "amenities", "building_type", "build_year", "market"]
valid_response = False
dropped_filters = {}

while True:
# Call API returning info about active offers count within selected filters
offer_json, valid_response = call_api(params)

if valid_response:
available_offers = offer_json.get("unique_offers_pool", 0)

output = f"Found {available_offers} offers fitting the following filters: {params}"

if dropped_filters:
output += f", but needed to ommit {dropped_filters} filters fitting these requirements."

return output

else:

if not optional_filters:
# If we've exhausted all optional filters, raise an exception
return f"Found 0 offers fitting the following filters: {params}, make filtering less strict to find available offers"
else:
# Remove one optional filter from params and try again
dropped_filter = optional_filters.pop(0)
if dropped_filter in params:
dropped_filters[dropped_filter] = params.pop(dropped_filter, None)

你可以意识到这个函数返回的所有输出都是完整的句子。由于整个text-to-params工具是由LLM代理使用的,因此以对话式解释的形式返回工具输出使其更容易集成到代理推理中。清晰描述放宽的过滤条件以使搜索成为可能,使代理能够向最终用户解释。

摘要

总结一下——请记住,在使用 LLM 时,您仍然可以编写简单、高效的代码,以更可预测的方式处理不那么复杂的任务。

尽管 LLM 发展迅速,但在可预见的未来,它们仍将是庞然大物,处理每个答案需要大量的能量成本和时间。在需要的地方利用这种力量,但也不要害怕在可能的情况下回归更简单、经过验证的解决方案。这样做将会让地球和您的钱包都感激不已。

  • 标题: 使用 Python Guardrails 提高 LLM 输出的可靠性
  • 作者: Barry
  • 创建于 : 2024-05-31 13:46:42
  • 更新于 : 2024-08-31 06:59:45
  • 链接: https://wx.role.fun/2024/05/31/e3b74272a9614d1990df6a43e9016409/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。