1. 引言
在审批系统中接入「一键 AI 润色」能力,本意是帮用户把申请内容写得更规范、更正式。但上线后很快会遇到一个现实问题:用户随手乱输的文本,根本不该进大模型。
比如用户粘贴了一串乱码、只敲了几个符号、或者输入了「asdfghjkl」这种无意义内容。如果这些文本直接发给大模型,不仅浪费 token、拖慢响应,还可能返回一堆莫名其妙的润色结果,体验反而更差。
所以,在调用大模型之前,我们必须先想清楚两件事:
- 怎么判断用户输入的是不是有效文本?
- 怎么在判断无效时,直接不请求大模型?
本文结合一个真实场景------在现有审批系统中融合「一键 AI 润色」能力------给出前端 + 后端双层校验的完整方案。
2. 核心思路:两道防线,缺一不可
面对乱输入,单靠前端或单靠后端都不够。正确做法是分层拦截:

- 前端校验:第一道防线,拦截明显无效的输入,避免无意义的网络请求。
- 后端校验:第二道防线,防止绕过前端直接调接口的恶意或异常请求。
两道防线各司其职,才能既保证体验,又保证安全。
3. 方案一:前端校验(第一道防线)
前端校验的目标是在用户点击「AI 润色」按钮的瞬间,快速判断文本是否值得发请求。校验不通过就直接提示,根本不发起网络请求。
3.1 校验规则设计
一个实用的前端校验函数,通常包含以下几层检查:
svelte
<script lang="ts">
// ... 其他代码
let isPolishing = $state(false);
// ✅ 检查文本是否有效
function isValidContent(text: string): { valid: boolean; reason?: string } {
const trimmed = text.trim();
// 1. 空内容检查
if (!trimmed) {
return { valid: false, reason: '内容不能为空' };
}
// 2. 长度检查
if (trimmed.length < 5) {
return { valid: false, reason: '内容太短,至少 5 个字符' };
}
if (trimmed.length > 500) {
return { valid: false, reason: '内容太长,最多 500 个字符' };
}
// 3. 检查是否包含有效的中文字符或英文单词
const hasChinese = /[\u4e00-\u9fa5]/.test(trimmed);
const hasEnglish = /[a-zA-Z]/.test(trimmed);
if (!hasChinese && !hasEnglish) {
return { valid: false, reason: '内容似乎不是有效的文本' };
}
// 4. 检查是否全是特殊字符
const hasValidChar = /[\u4e00-\u9fa5a-zA-Z0-9]/.test(trimmed);
if (!hasValidChar) {
return { valid: false, reason: '内容包含无效字符' };
}
// 5. 检查是否是乱码(连续特殊字符太多)
const specialChars = trimmed.match(/[^a-zA-Z\u4e00-\u9fa5\s]/g)?.length || 0;
const totalChars = trimmed.length;
if (specialChars / totalChars > 0.5) {
return { valid: false, reason: '内容包含过多特殊字符,请重新输入' };
}
return { valid: true };
}
async function aiPolish() {
const curValue = scopeValue[field.key] as string;
// ✅ 先进行前端校验
const validation = isValidContent(curValue);
if (!validation.valid) {
alert(validation.reason || '请输入有效的申请内容');
return;
}
// ... 继续润色逻辑
}
</script>
3.2 前端校验的要点
- 空内容与长度:最基础的检查,拦截空输入和明显过短/过长的内容。
- 有效字符检测:通过正则判断是否包含中文或英文字母,拦截纯符号、纯数字的乱输入。
- 乱码比例检测:统计特殊字符占比,超过阈值(如 50%)就判定为乱码。这是拦截「asdfghjkl!!!@@@###」这类输入的关键。
前端校验通过后,才发起请求。这样大部分无效输入在浏览器端就被拦下了。
4. 方案二:后端校验(第二道防线)
前端校验可以拦截大多数情况,但不能只依赖前端。因为:
- 用户可以绕过前端直接调用接口;
- 前端代码可能被篡改;
- 不同客户端(如脚本、爬虫)不会走前端逻辑。
所以后端必须再做一次校验,并且校验不通过时直接返回错误,不调用大模型。
4.1 后端校验与「是否调用大模型」的判断
以 NestJS + LangChain 为例,后端服务在调用大模型之前,先做两层判断:
typescript
// ai-polish.service.ts
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PolishRequestDto } from '../dto/ai-polish.dto';
@Injectable()
export class AiPolishService {
private readonly logger = new Logger(AiPolishService.name);
// ... 其他代码
/**
* ✅ 验证输入内容是否有效
*/
private validateContent(content: string): { valid: boolean; reason?: string } {
const trimmed = content?.trim() || '';
// 1. 空内容
if (!trimmed) {
return { valid: false, reason: '内容不能为空' };
}
// 2. 长度限制
if (trimmed.length < 5) {
return { valid: false, reason: '内容太短,至少需要 5 个字符' };
}
if (trimmed.length > 1000) {
return { valid: false, reason: '内容太长,最多 1000 个字符' };
}
// 3. 检查是否有有效字符
const hasValidChar = /[\u4e00-\u9fa5a-zA-Z]/.test(trimmed);
if (!hasValidChar) {
return { valid: false, reason: '内容包含无效字符,请输入中文或英文' };
}
// 4. 检查是否全是数字或符号
const charTypes = {
chinese: (trimmed.match(/[\u4e00-\u9fa5]/g) || []).length,
english: (trimmed.match(/[a-zA-Z]/g) || []).length,
number: (trimmed.match(/[0-9]/g) || []).length,
other: (trimmed.match(/[^a-zA-Z\u4e00-\u9fa5\s]/g) || []).length,
};
const total = trimmed.length;
const validChars = charTypes.chinese + charTypes.english;
// 如果有效字符占比太低,可能是乱码
if (validChars / total < 0.3) {
return { valid: false, reason: '内容包含过多特殊字符,请重新输入' };
}
return { valid: true };
}
/**
* ✅ 检查是否应该调用大模型
*/
private shouldCallAI(input: PolishInput): { shouldCall: boolean; reason?: string } {
// 1. 检查是否启用
if (!this.enabled) {
return { shouldCall: false, reason: 'AI 润色功能未启用' };
}
// 2. 验证内容
const validation = this.validateContent(input.content);
if (!validation.valid) {
return { shouldCall: false, reason: validation.reason };
}
// 3. 检查是否已经是正式格式(简单判断)
const isAlreadyFormal = this.isAlreadyFormal(input.content);
if (isAlreadyFormal) {
return { shouldCall: false, reason: '内容已经是正式格式,无需润色' };
}
return { shouldCall: true };
}
/**
* ✅ 判断是否已经是正式格式
*/
private isAlreadyFormal(text: string): boolean {
// 检查是否包含正式用语
const formalKeywords = [
'尊敬的', '领导', '您好', '申请', '审批', '批准', '恳请', '特此',
'根据', '规定', '制度', '流程', '报请', '呈请', '请示'
];
const hasFormalKeyword = formalKeywords.some(keyword => text.includes(keyword));
// 检查是否包含称呼和结尾
const hasGreeting = /尊敬的|您好|领导/.test(text);
const hasClosing = /恳请|批准|谢谢|感谢/.test(text);
return hasFormalKeyword && (hasGreeting || hasClosing);
}
}
4.2 后端校验的要点
validateContent:与前端规则类似但更严格,重点拦截乱码和无效字符。shouldCallAI:把「是否调用大模型」收敛成一个独立方法,包含三层判断------功能是否启用、内容是否有效、是否已经是正式格式。isAlreadyFormal:一个很实用的优化。如果用户输入的内容已经包含「尊敬的」「恳请」「批准」等正式用语,说明文本已经足够正式,没必要再花钱调大模型,直接返回原文本即可。
5. 完整调用流程
把前后端串起来,完整的调用流程如下:

6. 总结
在审批系统中融合「一键 AI 润色」能力后,面对用户乱输入,核心原则是:能不调大模型就不调大模型。
具体落地时记住三点:
- 前端先拦:用正则和字符占比快速判断,无效输入直接提示,不发请求。
- 后端再拦:防止绕过前端,校验不通过直接返回错误,不调大模型。
- 能省则省:如果内容已经是正式格式,直接返回原文本,连大模型都不用调。
这样既能保证用户体验,又能有效控制大模型的调用成本,让「AI 润色」真正成为审批流程的加分项,而不是负担。