模型回答质量不只取决于模型本身。空输入、过长文本、残留密钥和混入控制字符的内容,都会让调用变得不稳定或产生不必要的风险。本文把前面学过的输入边界、上下文预算和安全原则组合起来,只实现一个核心能力:在请求发给模型之前,先用确定性的 Python 规则判断输入是否适合继续处理。示例完全离线可运行,不需要密钥。

为什么需要输入预检

把所有文本直接交给模型,常见后果有三类:空白请求浪费一次调用额度;超长内容挤占上下文窗口;日志或用户复制的内容中夹带 API Key 等敏感信息。模型可以帮助理解自然语言,却不应该承担这些确定性边界的判断。

预检器的职责是小而明确的:接收原始文本,返回通过或拒绝、规范化后的文本以及可读的原因。它不调用模型、不修改数据库,也不把“看起来像密钥”的内容直接打印到日志。这样即使模型服务不可用,也能独立测试这层保护。

先定义检查结果

用数据类表达结果,比返回一个难以解释的布尔值更适合接入命令行或队列。cleaned_text 只有通过时才有值;拒绝原因使用稳定的代码,展示给用户的文字可以在上层翻译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from dataclasses import dataclass
import re


@dataclass(frozen=True)
class PreflightResult:
ok: bool
code: str
message: str
cleaned_text: str | None = None


SECRET_PATTERNS = (
re.compile(r"sk-[A-Za-z0-9_-]{16,}"),
re.compile(r"(?i)(api[_ -]?key|token)\s*[:=]\s*\S+"),
)

这里的正则只是降低误传风险,不是完整的秘密扫描器。它可能误报,也可能漏报;生产环境还应结合专门的密钥检测工具,并在真正的密钥管理系统中轮换已经泄露的凭据。

实现可解释的预检函数

下面只设三条规则:不能是空白文本,长度不能超过应用设定的上限,不能命中明显的凭据模式。长度按 Python 字符数计算,适合教学示例;接入具体模型时仍应根据 tokenizer 估算 Token,并为系统提示词和输出预留空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def preflight(text: str, max_chars: int = 2_000) -> PreflightResult:
if not isinstance(text, str):
return PreflightResult(False, "not_text", "输入必须是字符串")

cleaned = text.strip()
if not cleaned:
return PreflightResult(False, "empty", "输入不能是空白内容")

if len(cleaned) > max_chars:
return PreflightResult(
False, "too_long", f"输入超过 {max_chars} 个字符的上限"
)

if any(pattern.search(cleaned) for pattern in SECRET_PATTERNS):
return PreflightResult(
False, "possible_secret", "输入疑似包含凭据,请移除后再提交"
)

return PreflightResult(True, "ok", "预检通过", cleaned)

注意,预检器不是万能的内容审核器,也不能证明文本安全。它只负责提前发现几种可确定、可自动处理的问题。每条规则最好有独立的错误代码,方便统计“拒绝主要来自过长还是疑似凭据”,而不是把所有失败都记成 invalid。

接到一个离线处理流程

为了验证分支,写一个不访问网络的 process 函数。它把通过的文本交给 demo_model;这个函数只是模拟模型适配层,返回固定文本,不代表真实模型的运行结果。接入服务时,只替换适配层,不要绕过预检。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def demo_model(text: str) -> str:
return f"待处理文本长度为 {len(text)} 个字符。"


def process(text: str) -> dict[str, str]:
result = preflight(text)
if not result.ok:
return {"status": "rejected", "code": result.code, "message": result.message}

answer = demo_model(result.cleaned_text or "")
return {"status": "completed", "answer": answer}


if __name__ == "__main__":
samples = [" ", "请把这段需求整理成三条任务。", "api_key=sk-abcdefghijklmnopqrstuvwxyz"]
for sample in samples:
print(process(sample))

把代码保存为 preflight.py 后运行 python preflight.py,可以看到空白文本和疑似凭据被拒绝,普通文本进入离线处理分支。这里没有伪造 API 响应;固定的演示函数只用于验证程序控制流。真实模型调用应从环境变量读取密钥,并设置超时、错误处理和调用预算。

与上下文和日志配合

预检通过不等于一定能放进上下文窗口。真正调用前还要把系统提示词、历史消息和预期输出一起估算;如果总量超限,应按既定策略裁剪历史或分块,而不是静默截断用户输入。预检器可以增加 estimated_tokens 字段,但估算方法必须和目标模型匹配,不能把字符数当成精确 Token。

日志也要遵守最小化原则。可以记录请求编号、预检状态、错误代码、字符数和耗时,不要记录原始文本,更不要把命中的疑似密钥写入日志。若需要排查问题,应对文本做脱敏或只保存不可逆的摘要。通过后的文本仍是不可信输入,模型输出也必须继续经过解析和业务校验。

常见问题

为什么不在提示词里要求模型忽略密钥? 因为提示词不是访问控制,也不能阻止敏感内容进入请求、代理日志或第三方服务。能在本地拒绝的内容,应尽量在网络请求前拒绝。

发现疑似密钥时能否自动替换? 低风险场景可以把值替换为 [REDACTED] 后让用户确认,但不要未经同意改变原文。高风险流程应拒绝、提示轮换凭据,并限制错误信息暴露的细节。

只检查字符数够吗? 不够。它只是成本低的第一道门。多语言、代码和表情符号的 Token 比例不同;生产系统应使用目标模型对应的 tokenizer,并把估算失败当作需要人工处理的异常。

预检器应该放在哪里? 放在所有模型调用的共同入口,而不是某个页面按钮里。命令行、后台任务和接口只要共享同一个调用适配层,就不会因入口不同而漏掉规则。

小结

输入预检器把几项分散的工程知识收束成一个清晰边界:先检查类型、空白、长度和疑似凭据,再决定是否允许模型调用。它不替代 Token 估算、内容安全或输出校验,却能以很低成本减少浪费和敏感信息外泄。更重要的是,返回稳定错误代码、保留离线测试路径,并把模型适配层与确定性规则分开,后续更换模型或增加规则时都不必重写业务流程。