审批系统接入 AI 润色后,如何拦截用户乱输入的文本?

1. 引言

在审批系统中接入「一键 AI 润色」能力,本意是帮用户把申请内容写得更规范、更正式。但上线后很快会遇到一个现实问题:用户随手乱输的文本,根本不该进大模型

比如用户粘贴了一串乱码、只敲了几个符号、或者输入了「asdfghjkl」这种无意义内容。如果这些文本直接发给大模型,不仅浪费 token、拖慢响应,还可能返回一堆莫名其妙的润色结果,体验反而更差。

所以,在调用大模型之前,我们必须先想清楚两件事:

  1. 怎么判断用户输入的是不是有效文本?
  2. 怎么在判断无效时,直接不请求大模型?

本文结合一个真实场景------在现有审批系统中融合「一键 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 润色」能力后,面对用户乱输入,核心原则是:能不调大模型就不调大模型

具体落地时记住三点:

  1. 前端先拦:用正则和字符占比快速判断,无效输入直接提示,不发请求。
  2. 后端再拦:防止绕过前端,校验不通过直接返回错误,不调大模型。
  3. 能省则省:如果内容已经是正式格式,直接返回原文本,连大模型都不用调。

这样既能保证用户体验,又能有效控制大模型的调用成本,让「AI 润色」真正成为审批流程的加分项,而不是负担。

相关推荐
阿黎梨梨18 分钟前
AI也有记忆?LangChain Memory 管理指南
langchain·node.js·llm
大连好光景30 分钟前
【AI Agent案例开发项目01解读】
chrome·python·langchain
未若君雅裁1 小时前
自定义中间件:Node-style 与 Wrap-style 钩子全解析
python·中间件·langchain
烬羽1 小时前
为 Agent 管好一张“上下文预算”:从数条数到数 token
架构·langchain·agent
烬羽1 小时前
把 Agent 的记忆写进文件:内存 vs 文件,两把钥匙搞定多会话
架构·langchain·agent
多多鼠3 小时前
Tool Calling的信任边界:从协议校验到执行沙箱的完整链路设计
运维·开发语言·网络·人工智能·python·langchain
breeze jiang3 小时前
LangChain Memory 实战:用 InMemoryChatMessageHistory 管理多轮对话
前端·算法·langchain
minji...3 小时前
LangChain AI应用开发框架的使用(3) - 聊天模型的绑定,聊天模型的结构化输出及使用场景
langchain
65岁退休Coder1 天前
LangGraph v1.2.9 节点容错策略 & 流式输出 & 持久化记忆管理
后端·python·langchain