27个扫描脚本不进上下文:RAG入库前文档扫描的Agent技能实践
看到这个标题可能不少正在做RAG检索增强生成的同学都会愣一下27个脚本还“不进上下文”一句话就能触发文档扫描我刚开始接触这个玩法的时候也觉得玄乎但把我手头那个知识库项目里堆积的入库前扫描需求一个个列出来之后我反而觉得“27个”这个数字太真实了。真到了RAG入库这个环节你会发现自己面对的不是“需不需要扫描”的问题而是“怎么让扫描在Agent的上下文里塞得下、跑得动、还能说得清”。这篇文章不聊理论就是把我自己实际搭建“文档扫描Agent技能”的经验拆开讲清楚。内容包括为什么入库前扫描非做不可Agent Skills到底是怎么省上下文的27个脚本怎么拆怎么编排以及我从零搭起这个扫描技能时踩过的一堆坑。如果你正在做RAG数据治理、文档清洗、或者想把一批脚本变成Agent可调用的能力这篇文章应该能给你省掉不少弯路。1. 先搞明白入库前扫描到底在防什么1.1 RAG的“垃圾进垃圾出”魔咒RAG的检索质量很大程度上取决于你喂进向量库的“原料”干不干净。我见过很多项目线上问答答得离谱查到最后根本不是模型的问题而是源文档本身就是一堆“地雷”PDF解析出来是一整页乱码、扫描件OCR识别出了错别字、正文里混着身份证号、同一篇文章被上传了十几个重复版本、Markdown标题层级乱七八糟导致切片逻辑直接崩溃。这些脏数据一旦进了向量库就会被切块、向量化、编入索引。等用户提问时检索结果把这些坏块返回给模型模型再聪明也只能基于坏内容一本正经地胡说八道。而且入库之后你再想去修得把索引里的对应块找出来删除、重新切分、重新向量化成本和风险比入库前做一次扫描高得多。所以我一直坚持入库前加一道“文档扫描”做质量闸门能拦截的错误绝不拖到检索阶段。这道闸门要看的不是单一指标而是格式、编码、内容质量、敏感信息、结构完整性等多个维度。它不像是跑一个“能跑就行”的脚本更像是给文档做一次全面体检。1.2 27个脚本背后的分类逻辑刚开始我以为把几个检查规则写进一个大脚本就完事了。后来发现不同来源的文档出问题的模式完全不同从某系统导出的PDF经常是编码错乱从网页抓下来的Markdown经常是空段落和重复标题而扫描件则要面对OCR置信度和敏感信息泄漏问题。于是我把扫描需求拆成了四类一共积累了27个独立脚本类别扫描重点代表性脚本脚本数量格式与编码乱码检测、编码识别、空白页、截断文件detect_garbled_text.py、check_truncated_pdf.py8内容质量空段落、低信息密度、相似重复、标题层级异常simhash_duplicate.py、empty_section_scanner.py9安全合规手机号、身份证、银行卡、API密钥、内部标记pii_regex_scanner.py、secret_key_detector.py6结构解析图片引用断裂、语言检测、元数据完整性markdown_link_checker.py、lang_detector.py4为什么拆这么细因为每个脚本都是独立单元可以单独调试、单独更新某类规则误报了不会影响其他检查更重要的是Agent 在调用时可以根据文档类型“按需组合”而不是每次把所有规则都跑一遍。比如内部技术文档可以跳过敏感信息扫描或只跑API密钥检测而对外资料则必须全量过合规检查。1.3 为什么必须赶在“入库前”很多人会问能不能等检索结果有问题再修复我的回答是等出了问题再去定位根本不知道是哪个源文档哪一段造成的。向量检索是相关性匹配不是你改了源文件索引就会自动更新的。如果脏数据早就进了库你会面临“检索命中了一堆坏块但用户的问题本身没有问题”的尴尬局面。把扫描前置到入库前核心好处有三点第一污染面可控坏文档压根没机会进入索引第二修复成本低重新处理原始文件比清理向量库便宜太多第三审计上有据可查每次入库都有一条“扫描报告”哪些文件通过、哪些被拦截、拦截原因是什么清清楚楚。这也是我后来坚持做扫描报告归档的原因。2. Agent Skills让27个脚本待在工具箱里而不是口袋里2.1 先理解Agent Skills和普通脚本调用的区别常见的Agent工具调用方式是把一堆函数直接注册成可调用接口。你当然可以把27个脚本封装成27个函数然后告诉Agent“你有这么多工具可以用”。但问题来了这些函数的描述、参数说明、使用示例都要写进系统提示词里也就是上下文里。一个精确定义的函数描述动辄一两百个token27个下来就是几千token而且Agent在理解一大批工具时很容易在选择上“犯迷糊”。Agent Skills更像是一种“可被按需加载的技能包”。它的核心不是把脚本内容塞给Agent而是只给Agent一份精简的技能清单。清单里包含技能名称、一句话说明、参数Schema。Agent看到用户需求后根据技能描述判断该调用哪个技能然后框架从外部加载真正的脚本并执行。脚本代码、依赖逻辑都在Agent上下文之外跑完之后只把结果摘要回传。我用一个比喻来理解这件事你是一个主厨掌握27道菜的做法但你不能把这27道菜谱全背在身上你只需要知道每道菜叫什么、适合什么场景下单时喊一句“做那道红烧肉”身后的助手就会去仓库取对应菜谱并按步骤执行。Agent Skills就是这套“菜谱仓库助手取菜”机制。2.2 为什么“不进上下文”反而是性能关键这是我最想强调的一点省上下文不只是为了省钱更是为了稳定性。我实测过如果把27个脚本的完整说明都塞进系统提示词Agent在做文档扫描任务时会出现三个明显问题一是响应速度变慢每次请求都会把这些工具定义重新编码一遍二是工具选择准确率下降脚本多了之后模型容易选错比如把“内容质量扫描”当成“敏感信息扫描”三是上下文可用空间被压缩真正需要的用户指令、文件摘要、扫描结果反而放不下。把脚本本体移出上下文后每个技能在上下文里只占一行描述加上参数定义大概几十个token。27个技能总共不到一千token对当前模型来说几乎可以忽略。扫描执行时真正跑的是外部进程不占用模型推理资源。整个过程中的思维链更清晰Agent也不会因为工具列表太杂而“选择困难”。2.3 一句话触发的核心技能名加自然语言参数映射“一句话触发”并不是搞什么神奇魔法本质上是Agent意图识别加参数提取。用户说“扫描一下这批PDF”Agent识别出意图是执行document_scanner技能然后从自然语言里提取出路径参数/data/uploads文件类型参数pdf。这里需要明确一点参数的准确度取决于技能描述写得好不好。我一开始把描述写得很宽泛“对文档执行扫描并返回问题列表”。结果Agent老是漏传strict等关键参数。后来我把描述改成“对指定路径下的文档执行入库前质量扫描支持PDF/Markdown/纯文本返回问题列表、严重级别和建议操作。当用户提到‘严格模式’时将strict设置为true。”改完之后参数正确率明显上升。技能名本身是触发点描述和参数定义才是触发质量的核心。3. 27个扫描脚本的完整规划与拆解3.1 第一梯队格式与编码扫描格式与编码问题是最容易被忽略却又最致命的。特别是从老旧系统导出的PDF或TXT经常会出现整个文档是乱码、一段文字里夹杂着大量控制字符、或者明明是中文内容却用错误编码打开后变成“锟斤拷”一类的内容。我常用的第一个脚本是乱码检测脚本。原理不复杂先尝试用UTF-8、GB18030等编码解码再统计解码后文本中替换字符的占比超过阈值就判定为乱码。再配合“可打印字符比例”和“常见中文字符覆盖率”两个辅助指标基本能覆盖大多数编码场景。import sys def detect_garbled(text: str) - dict: replacement_count text.count(\ufffd) printable_ratio sum(c.isprintable() for c in text) / max(len(text), 1) if replacement_count / max(len(text), 1) 0.01: return {error: garbled, level: blocker, detail: 含有大量替换字符} if printable_ratio 0.5: return {error: low_printable, level: warning, detail: 可打印字符比例过低} return {error: None, level: pass, detail: ok}check_truncated_pdf.py则负责检查PDF页数异常和文件尾部是否残缺。实践中最常见的情况是上传工具断了流导致PDF在文件中断位置出现不完整的对象结构。这类文件直接解析往往会崩溃所以要提前拦截。3.2 第二梯队内容质量与去重内容质量扫描解决的是“文档虽然能打开但里面根本没有料”的问题。空段落、只有标题没有正文的区块、信息密度过低的页面都会污染RAG的切片效果。我用一个empty_section_scanner.py输入Markdown或TXT输出所有标题下正文字符数小于阈值比如50字的区块清单。去重是我重点想聊的地方。直接用MD5只能去掉字节级完全相同的文件但真实场景里更多是“相近重复”同一份报告改了几个数字、同一篇文章在不同平台转换导致空白差异。我用SimHash做相似度检测通过分词、哈希加权、降维得到64位指纹再计算两篇文档之间的汉明距离。汉明距离小于3时就标记为疑似重复。def simhash(text: str) - int: tokens text.split() v [0] * 64 for token in tokens: h hash(token) for i in range(64): if h (1 i): v[i] 1 else: v[i] - 1 fingerprint 0 for i in range(64): if v[i] 0: fingerprint | (1 i) return fingerprint这里有个实际心得不要只看全文档相似度最好把文档按章节切块后分别计算因为长文档里可能只有一段是重复内容全局SimHash值反而不敏感。3.3 第三梯队敏感信息与合规红线对于对外知识库来说敏感信息扫描是刚需。我的pii_regex_scanner.py维护了一套正则库覆盖中国大陆手机号、身份证号、银行卡号、常见邮箱、IPv4地址以及几类内部工件编号。正则匹配只是第一步难点在误报处理。我踩过一次坑把“身份证号正则”写得比较宽松结果把文档里的条形码数字串全匹配出来了。后来我加了校验位校验身份证号必须通过GB 11643-1999的校验规则才标记手机号必须匹配前三位号段为常见运营商号段。全量正则有几十条但每个脚本只负责一个类型方便单独调整。密钥检测我用的是secret_key_detector.py重点不是找形如“sk-”开头的字符串而是找高熵字符串长度在20位以上、字符集包含大小写字母和数字、并且连续出现多个疑似密钥上下文关键词。没有上下文提示的随机字符串就算匹配到了也不敢标记为密钥否则误报会多到没法看。3.4 第四梯队结构解析与元数据补全这一类脚本不直接“拦截”文档而是为后续切片做准备。markdown_link_checker.py检查所有图片引用链接是否指向存在的相对路径避免入库后前端渲染满屏裂图lang_detector.py判断文档主语言方便向量化时选择合适的分词和Embedding模型metadata_completer.py检查文档是否包含标题、创建日期、作者、来源URL等元数据缺失的话打上missing_metadata标签。结构扫描最容易被省略但我觉得恰恰不能省。RAG的切块逻辑依赖标题层级如果一个文档的标题全是#一级大标题没有二级三级切片出来的块会非常粗。markdown_heading_tree.py会把标题树打印出来超过N层断层就提示“结构扁平建议重新组织”。3.5 组合编排如何保证27个脚本不乱跑27个脚本如果让Agent在每份文档上把全部跑一遍不仅慢而且没必要。我的做法是给每个脚本定义一个“适用文档类型”和“触发条件”在编排层用一个Pipeline来管理依赖关系格式类脚本永远最先跑因为如果格式都错了后面内容质量类脚本拿到的文本本身就是废的安全合规类脚本在文本清洗后、但在写入向量库前跑结构解析类脚本和内容质量类可以并行。配置片段我放在一个YAML文件里看起来大概是这样的pipeline: - stage: format scripts: - detect_garbled_text - check_truncated_pdf - stage: content scripts: - empty_section_scanner - simhash_duplicate - stage: security scripts: - pii_regex_scanner - secret_key_detectorAgent拿到一批文件后先读配置按阶段调度。不需要我手动写“先跑这个再跑那个”的指令自然语言一句话就能让Agent遵循这个编排。4. 实操从零搭建一个可用的文档扫描Agent Skill4.1 第一步定义技能清单与Manifest我现在使用的框架支持自定义Skill一个技能对应一个Manifest文件。Manifest本质上是一个JSON文档包含技能名、描述、参数定义和调用入口。下面是我给扫描技能写的精简版{ name: document_scanner, description: 对指定路径下的文档执行入库前质量扫描支持PDF/Markdown/纯文本返回问题列表、严重级别和建议操作。当用户提到严格模式时将strict设置为true。, parameters: { path: {type: string, description: 待扫描文件或目录的绝对路径}, file_type: {type: string, enum: [pdf, markdown, text]}, strict: {type: boolean, description: 是否启用严格模式会拦截更多低严重级别问题} } }Manifest里的描述千万不能写得太“文学”。我最初写过“对文档进行健康检查并报告潜在问题”Agent根本猜不到要传什么参数。后来改成包含文件类型、返回内容、参数含义的描述之后调用成功率提升了一大截。4.2 第二步把扫描逻辑封装成可独立运行的脚本每个脚本要保持统一入口入参可以是路径和参数出参必须是标准JSON。这样Agent拿到结果后直接解析不用关心每个脚本的不同返回值。我推荐脚本内部不直接操作Agent运行时而是通过标准输入输出通信。下面是一个典型脚本骨架import argparse, json def scan_file(file_path: str, strict: bool) - dict: # 实际扫描逻辑 return {file: file_path, issues: []} if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--path, requiredTrue) parser.add_argument(--strict, actionstore_true) args parser.parse_args() result scan_file(args.path, args.strict) print(json.dumps(result, ensure_asciiFalse))统一输出格式时我约定每个issues元素至少包含error类型、levelblocker/warning/pass、detail描述、suggestion建议。后续所有脚本都遵守这个约定Agent才能把多个脚本的结果汇总成一份连贯报告。4.3 第三步注册技能并设置参数约束在框架的Agent配置里把document_scanner注册为可用技能指向Manifest文件。同时还可以注册辅助技能report_generator用于生成扫描汇总报告file_fetcher用于从上传队列拉取文件。参数约束用JSON Schema实现。我特意在file_type里加了enum约束避免Agent传入“docx”这种没有定义的类型。加约束之后即使模型幻觉把参数传错框架也会在校验阶段直接拦截而不是把错误参数传给脚本导致“文件不存在”。4.4 第四步一句话触发全流程一切就绪后我测试时输入这样一句话“请扫描 /data/incoming 下的新文件先跑格式和敏感信息扫描质量扫描用严格模式最后输出一份汇总报告。”Agent的实际处理链路大概是这样的理解意图用户要执行文档扫描且指定了多个阶段调用document_scanner参数path/data/incomingstricttrue框架按Pipeline配置先运行格式类脚本再运行安全类脚本最后运行内容质量类脚本每个脚本实例都是独立进程逐份文件处理并返回JSON调用report_generator把所有问题按严重程度汇总成Markdown表格。最终我看到的模拟输出长这样扫描报告共扫描 34 个文件 - blocker: 3 个文件存在乱码已阻止入库 - warning: 5 个文件疑似重复建议合并 - warning: 1 个文件包含疑似API密钥需人工确认 通过 25 个文件已进入向量化队列从输入一句话到拿到报告整个过程用户可以完全不感知中间跑了多少个脚本。4.5 第五步把扫描结果接入RAG入库管道扫描结果不能只是“看一眼”它必须成为入库流水线里的一个硬门禁。我写了一个guardian模块读取扫描结果后做三个动作blocker级别问题直接拒绝入库文件被移动到/quarantine目录warning级别问题允许入库但把警告信息写入文档元数据检索时可以做降权或提示全部通过文件进入下一阶段切块和向量化。有了这个硬门禁之后我再也不用担心脏文件“漏”进库里。而且每条入库记录都有对应的扫描报告链接出问题时可以直接追溯。5. 实战中踩过的坑与排查清单5.1 技能串行执行扫描一批文件要半小时我第一次跑批量扫描时27个脚本是Agent挨个调用的每个脚本都要等待前一个的结果整个目录跑下来花了半个多小时。后来我在编排层做了三个优化把不依赖彼此的脚本分组放到同一阶段并行执行单个脚本内部按文件数做多进程并发对于超时超过120秒的脚本直接标记为失败并继续执行后续脚本避免单点卡死。优化后同样的一批文件扫描时间从半小时降到了三四分钟。“并行编排”不是把27个脚本一起扔出去而是根据依赖关系控制并发度。5.2 Agent把“严格模式”参数理解错了有一次我在工人那边明确说“严格模式”结果Agent返回的strict字段是false导致一批低级别重复根本没被拦截。排查后发现是Manifest描述里缺少“当用户提到严格模式时设置为true”这句话。模型不会猜你到底想要什么它只会依据描述来推断。解决方式有两个一是在描述里显式写出触发词二是在参数Schema里加默认值提示。我把strict参数的描述改成“默认false当用户要求严格/从严/加大拦截时设置为true”之后这类错误基本消失。5.3 脚本拿到的文件路径始终报“文件不存在”Agent在执行环境里的当前工作目录可能和脚本预期不一致尤其是通过调度器运行时相对路径会直接失效。我后来在全局配置里统一约定所有传入路径必须是/data/raw前缀的绝对路径脚本内部再做一层os.path.abspath规范化。还要注意一个问题如果Agent运行在容器或沙箱里宿主机路径和容器路径往往不一样。我踩过之后就在调度层加了一个路径映射函数把用户可见路径转换成容器内实际路径然后传给脚本。5.4 正则误杀严重身份证校验被短信验证码误匹配我用单纯正则匹配手机号和身份证号时出现过大量误报。尤其是文档里如果包含“验证码为123456789012345678”这类演示数据就会被当成身份证号。后来我给敏感信息脚本加了三层判定第一层正则粗筛第二层校验位算法验证第三层上下文关键词判断。只有连续通过至少两层的候选字段才标记为“疑似敏感信息”。并且给每个命中结果附上匹配位置和前后文本片段方便人工复核。5.5 怎么确认扫描结果真的可靠直接上线风险不小。我准备了一套“黄金文档集”包含20份故意注入各种问题的测试文档以及10份干净文档。每次调整脚本或修改正则后都跑一遍黄金集看各类脚本的精确率和召回率有没有下降。如果某个脚本的精确率低于95%我就调高判定阈值或增加上下文判断。这个方法帮我发现了不少“脚本自己写high了”的情况比如空段落扫描器把只包含换行符的文件全部标记为严重后来我加入“正文有效字符”逻辑才把误报压下来。6. 一点个人体会在做这套文档扫描Agent技能的过程中我最直观的感受是Agent开发的重心正在从“写提示词”转向“设计能力边界”。27个脚本各司其职Agent的角色更像是调度员和翻译官而不是执行者。你要做的不是把所有规则塞给模型而是把规则封装成模型可以随时调用的外部能力同时把调用方式设计得足够明确。如果让我给后来者一个建议我会说不要一上来就追求27个脚本先搭好5个核心扫描技能跑通流程再根据实际文档暴露的问题持续补脚本。“27”不是目标是积累的结果。技能描述一定要写到自己愿意读三遍的程度因为模型能清晰理解技能的前提是你自己先把这个技能想清楚。这套东西后续还能扩展把扫描结果反馈给生成模型让它自动修复一些简单问题或者把不同团队的扫描规则做成可共享的技能市场避免重复开发。项目迭代一段时间后你会发现真正有价值的不只是扫描能力本身而是围绕文档质量建立起来的这套可插拔、可演进的管理机制。