做本地智能体最烦的不是模型能力不够,而是输入框根本装不下你的文档。有一回我处理一份两百多页的投标文件,PDF解析完光文本就六万多字,直接丢给模型调用,接口那边立刻抛了一个上下文超限错误,前半部分还白读了,因为截断点正好落在关键报价表附近,后面所有结论全是错的。那次之后我彻底放弃“一把梭”的思路,把重心放在文档预处理、任务调度这两件看似不起眼的事上,最后串成一条完整的本地智能体链路,长文本超限问题才真正被解决。这篇就把我的完整做法、参数设计、踩过的坑都整理出来,给正在做本地文档问答、批量文档分析、办公自动化的人一个可以直接抄作业的参考。
1. 整体设计思路:本地化、预处理、调度怎么串成一条链
1.1 本地智能体不是聊天机器人,而是一条数据流水线
很多文章把智能体说成“能对话的AI”,实操过你会发现,办公场景里真正值钱的部分全在对话之前。文档要读取、清洗、切块、入库,大模型调用要排队、限流、重试,这些问题不解决,对话框再聪明也白搭。我把它定位成一条流水线:文件进、结果出,中间每个环节都可观测、可替换。本地化的核心理由有两个,一个是数据安全,合同报价、人事制度这类东西不适合往外部API传;另一个是可控性,本地部署可以让每个环节的日志、缓存、失败原因都捏在自己手里。
这个定位很重要,因为它决定技术选型。如果只是简单调用云端接口,你根本不需要队列和状态机;但要走全链路,调度就成了刚需。我的建议是,哪怕你手头只有一两个文档处理需求,也先把流水线搭出来,后面扩展场景时不用推翻重来。部署方式也不用复杂,一台16G内存的机器跑全部服务,或者用Docker Compose按服务拆分都行,关键是每层职责清晰。
1.2 长文本超限的本质不是“不够长”,而是“不会拆”
模型上下文窗口有限,很多人第一反应是“买更大的窗口”,但实际办公场景里,200页文档你真正需要模型看的可能只有其中两页。硬塞全文,一来浪费token,二来无关信息会稀释注意力,导致回答质量反而下降。所以长文本超限的解法不是换更大的模型,而是让系统在问答前就完成一次信息筛选。
具体是三种能力:拆,文档切成若干语义完整的块;筛,根据问题召回最相关的块;缩,在必要时对长文档先生成摘要再回答。这三件事对应着后文要讲的预处理、检索和摘要策略。你先记住这个判断:超限报错是表象,信息管理才是内核。只要把文档提前处理好,模型窗口大小反而不是瓶颈,8K上下文足以应对绝大多数办公场景。
1.3 全链路四个环节:采集、预处理、调度、问答
以我最后落地的系统为例,链路分四层。采集层负责监听文件夹、读取邮件附件、扫描共享盘,把文件路径和元信息写入任务表。预处理层负责解析格式、清洗乱码、切片、生成向量,并把处理状态回写。调度层是所有任务的中枢,负责触发、排队、并发限制、失败重试。问答层接收用户问题,先从向量库检索,再把命中的切片和问题一起交给大模型生成回答。
这四层可以全部跑在同一台机器上,也可以用Docker拆成四个服务。关键不是架构多复杂,而是每一层的输入输出都是结构化数据。只要任务表里状态清晰,哪怕某一层挂了,重跑也能恢复,不会出现“不知道处理到哪”的情况。我最初犯的错就是跳过采集层,手动把文件扔进处理脚本,结果文件一多,整个人成了调度器。
2. 文档预处理:从原始文件到“模型能吃的干净文本”
2.1 别直接用open()读文件,格式解析要先归一化
办公文档最常遇到的就是PDF、Word、Excel、PPT,每种格式都有解析库,但细节坑非常多。我的经验是分格式处理:PDF用pdfplumber,能处理文字版PDF,带表格时效果比pypdf好;Word用python-docx,读取段落和表格分开处理,因为表格里的文字顺序和段落完全不同;Excel用openpyxl,直接读取所有sheet,不要只读第一个;PPT用python-pptx,只提取文本和表格,图片里的文字交给OCR。
这些库读取之后统一定义一个结构:文档ID、来源文件、块序号、块内容、页码、表格位置等元信息。后面的切片和检索都依赖这个统一结构,所以第一步必须做格式归一化。我见过有人在预处理阶段只保留纯文本,结果页码信息全丢了,后面定位“这份内容在原文第几页”时完全做不到。归一化还有一个容易被忽略的点:统一字符集和换行符,Windows和Mac的换行不一样,混合来源的文档经常在这上面出问题。
2.2 编码、乱码与特殊符号:最容易翻车的细节
中文办公文档最大的坑是编码。我自己遇到过从客户系统导出的TXT其实是GB18030编码,直接用UTF-8读出来全是乱码。后来统一用chardet探测编码,读取时指定编码。还有一个常见坑是带BOM的CSV,第一列列名会多出一个看不见的字符,处理数据时很容易踩雷。
预处理里还要做三个清洗动作:全角转半角(中文标点除外)、把连续空行压缩成一个、去掉不可见控制字符。另外,PDF解析出来的文字经常会有单词间多余空格或者错误的断行,尤其是双栏排版,解析顺序会混乱。这类问题没有通用解法,只能针对具体的模板加正则规则。我的做法是先把常见脏数据样本收集起来,写一套清洗规则,每次新文档类型进来再补充规则,慢慢积累成一张“脏数据特征库”。
2.3 长文本超限的第一道防线:切片策略与重叠参数设计
切片的目的是把长文档拆成独立、语义完整、又不超出模型上下文的小块。固定大小硬切是最省事的,但效果最差,因为可能一句话被拦腰截断,检索和生成都会出问题。我用的是滑动窗口法:设置chunk_size和overlap两个参数,让相邻切片之间保留重叠内容。
具体参数,中文文本我一般用chunk_size=1200字符,overlap=200字符。为什么是1200?中文一个字符在大多数模型里约等于0.7到1个token,1200字大约在800到1200 token之间,常见的嵌入模型和生成模型的上下文都放得下;再大就贴近上限,批处理时容易超时。200字的重叠是为了保证切在段落中间时,下一段还能“想起来”上一段的结尾。英文文本可以按词数切,chunk_size=300词、overlap=50词的配比也够用。
切片顺序也很关键。我建议优先选自然边界:段落边界、句子边界,其次才是字符数强制切。先按段落整理文本,段落太长就递归往下切,直到段落小于chunk_size;段落太短就合并相邻段落,直到接近chunk_size。这样切出来的块,语义完整性远高于纯固定长度。如果是表格类内容,不要把表格硬拆成两片,宁可把整张表作为一块,稍微超出阈值也没关系,检索时再单独处理。
2.4 预处理结果自检:切片入库前必须过一遍
切片不是切完就完事了。我每次批量预处理结束都会跑一个自检脚本,统计三样东西:切片总数、空切片数量、异常字符比例。空切片一般说明源文件有很多页是纯图片或解析失败;异常字符比例高说明编码或OCR出了问题。
自检通过后,把切片写入本地向量库(我用的sqlite-vec或者chroma都行,数据量不大不需要ES),同时把原始文本备份成JSON,方便之后回溯。这一步多花五分钟,后面问答环节出问题时能省一小时。还有一个小习惯:给每批处理生成一份质量报告,记录文件数量、切片数量、失败数量、耗时。下次调参前先翻报告,用数据说话,而不是靠感觉改参数。
3. 任务调度:把“人肉排队”变成自动化流水线
3.1 为什么要引入调度:文件一多,脚本就乱了
如果你的需求只是“今天处理这一份合同”,手动跑脚本没问题。但我做这个系统是因为每周要处理几十份标书和合同,这时候就会遇到三个问题:同一时间多个文件要处理,靠人力排队;某一步失败需要重跑,靠记忆容易漏;新文件到了要立刻处理,靠肉眼盯着文件夹太累。
调度器要解决的就是这三件事:怎么排队、怎么触发、怎么重试。我一开始用最简单的方案——一个Python脚本循环扫描目标文件夹,有文件就处理。后来文件多了,并发错乱,失败重试也说不清楚,才改成正式的队列加状态机。现在回想,调度器不是给机器用的,是给人用的——它把“下一步该干什么”这个脑力活,从人脑转移到了系统里。
3.2 任务队列与优先级:核心还是状态管理
任务队列的核心不是“队列”本身,而是每个任务的当前状态。我定义了一张任务表,字段包括:任务ID、文件路径、任务类型(解析、OCR、向量化等)、状态、优先级、重试次数、创建时间、完成时间。本地环境数据量不大,直接用SQLite存这张表,比Redis还稳当,重启服务也不会丢任务。
优先级怎么定?我一般只分两档:普通和紧急。紧急任务插队到队列头部,普通任务按创建时间排序。不需要搞复杂的权重计算,办公场景里规则越简单越好。调度进程启动时从库中取出所有PENDING状态的任务,按优先级排序后逐个处理。这里要特别提醒:任务表一定要加唯一约束,防止同一个文件被重复调度。我踩过这个坑,同一个PDF被两个调度进程同时取走,处理结果互相覆盖。
3.3 定时触发、并发控制与失败重试
定时触发我用APScheduler,三类场景:每天定时扫描共享目录、每N分钟检查一次任务表、新文件落地立即入队。代码大概这个样子:
from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() # 每天9点自动扫描 scheduler.add_job(scan_shared_folder, 'cron', hour=9, minute=0, id='scan') # 每5分钟检查待处理任务 scheduler.add_job(process_queue, 'interval', minutes=5, id='queue')并发控制是很多人会忽略的点。PDF解析、OCR这类任务非常吃CPU,盲目开多线程反而拖垮机器。我用线程池限制并发数为2,任务完成一个再接下一个。这里有个经验:解析类任务用多进程而不是多线程,因为CPU密集型的任务在多线程下反而因为GIL变慢;只有模型调用这类IO密集型任务才适合协程或线程。
失败重试也不建议无限重试。我设置最大重试次数为3次,间隔按1分钟、5分钟、20分钟递增。超过3次后任务状态变成FAILED,并写一条带堆栈的错误日志,宁可人去看一眼,也不要让它在队列里无限循环。设计调度系统时,永远把“系统自己会挂”当成默认前提,而不是意外情况。
3.4 状态机与日志:让每一步都看得见
任务在管道里会经历这几个状态:PENDING(等待)、PROCESSING(处理中)、SUCCESS(成功)、FAILED(失败)、RETRY(待重试)。我在每次状态切换时都会记录一条日志,包含任务ID、操作用时、输入输出摘要。这样事后排查问题时,不用靠回忆,直接翻日志就能知道是哪一步挂的。
日志我建议写到文件而不是只打控制台,因为定时任务大概率在后台无人值守跑,控制台的输出过几天就找不到了。按天拆分日志文件,配合任务表里的状态字段,基本能做到“任何一条数据从进到出全链路可追溯”。这听起来有点像运维方法论,但在本地智能体场景里同样适用,只是规模小一些。
4. 长文本超限的四套实战解法
4.1 检索增强问答:让模型只读最相关的几段
最常用也最推荐的做法是把文档切块向量化,存进向量库。用户提问时,先用相同嵌入模型把问题也向量化,然后在库里做相似度检索,取出最相关的3到5块,再连同问题一起交给生成模型。这样无论原文档是10页还是200页,最终进入模型上下文的只有几千字,根本不会触发超限。
这套方案的关键在嵌入模型选型。我是用本地的bge-m3,效果比我之前用的那个小模型好很多,中文语义召回准确率提升明显。参数上,我一般设置top_k=5,相似度阈值0.45,低于阈值的切片直接丢弃,宁缺毋滥,避免无关内容混进去污染答案。实测场景里,一本两百页的技术手册,用户问“这个接口的错误码有哪些”,系统只检索出三块内容,模型回答既准确又详细,响应速度还快。
4.2 摘要压缩:对超长文档先做分层概括
有些文档虽然长,但你问的问题非常宏观,比如“这份年度报告的核心结论是什么”。这时候检索切片反而麻烦,不如直接对文档做摘要。方案是分层摘要:先按章节切分,每章调一次模型生成200字以内的摘要,再把所有章节摘要合并成完整文档,最后再生成总摘要。两层结构的好处是单次调用的输入都控制在窗口内,不会超限。
我实测下来,一份三万字的年度报告,章节摘要加总摘要总共调用十几次模型,耗时大概两分钟,输出一份800字以内的核心内容,完全在一个对话里展示。这个方法适合政策文件、财报、合同条款这类“结论集中”的文档。注意,摘要任务也会消耗token,但比全文喂入便宜得多,而且摘要结果可以缓存,同一文档被问多次时直接复用。
4.3 增量式拆分:把大任务拆成子任务流水线
还有一类情况是单步逻辑太重,比如“把这份文档的核心信息提取成结构化表格”,需要对全文做多次判断。这时候可以拆成子任务链:先识别文档类型,再提取关键字段,然后校验字段完整性,最后汇总输出。每一步只依赖上一步的输出,上下文窗口自然不会爆。
举个例子,合同信息提取我拆成4步:解析合同文本、识别甲乙双方名称与金额、检查金额格式与单位、汇总成台账。每一步单独调用模型,输入输出都很小,随便跑都稳。如果非要一步到位让模型“从合同里提取所有关键信息”,长合同必超限,而且输出质量也会因为信息太多而下滑。增量式拆分的额外好处是每一步都可以单独调试,出问题时只需要重跑那一步,不用整个任务推倒重来。
4.4 动态降级:超限估算与应急截断策略
即使前面做了切片和调度,偶尔还是会有单条文本超限的情况,比如某个Excel单元格里放了三万字。我在调用模型前会先估算token数:中文按字符数乘以0.8估算;英文按词数乘以1.3估算。如果估算值超过模型上下文窗口的70%,就自动降级。降级的优先级是:先走摘要,再走分块,实在不行才做截断。
举个例子,上下文窗口是8k的话,我设在5.6k触发降级。一个3000字的中文段落估算约2400 token,安全;一段两万字文本估算16000 token,直接进入摘要流程,不做全文喂入。这个保障逻辑保证了接口永远不会因为超限而报错,因为它根本不会被送到超限的地步。动态降级策略让我在切换不同模型时很从容,不管窗口是4K还是128K,系统都能自适应,不用改核心代码。
5. 落地过程中的坑与排查实录
5.1 常见问题速查表
我用一张表把预处理和调度环节的高频问题整理出来,放在运维手册里。遇到问题先查表,能省下大量时间。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| PDF解析全是乱码 | 扫描版PDF没走OCR | 引入OCR模块,用PaddleOCR识别图片文字 |
| Excel只读到第一个sheet | openpyxl默认只读活动表 | 遍历workbook.sheetnames逐个读取 |
| 中文文件名入库后找不到 | 文件名编码不一致 | 统一用ID命名文件,原文件名存入元信息 |
| 模型调用经常超时 | 没有做并发控制 | 限制模型并发,设超时时间并自动重试 |
| 检索结果答非所问 | 切片太碎或重叠不足 | 调整chunk_size和overlap,走语义边界切 |
| 任务一直处于PROCESSING | 进程崩了没更新状态 | 加心跳检查,超时任务重置为RETRY |
| 向量库检索速度慢 | 数据量大且没建索引 | 用HNSW索引并限制检索范围 |
5.2 我踩过的几个典型坑
第一个坑是PDF双重解析。很多PDF工具包默认会提取页面的文本和图片,图片里的文字和正文文本区域出现重复,导致切片里同一段话出现两遍。我的解决办法是在解析阶段对同一页面做去重,只保留文本层的内容,图片层交给单独的OCR任务。
第二个坑是切片的“重复幻觉”。重叠窗口导致同一句话可能在两个切片里都出现,检索时如果两个切片都命中,模型可能把重复内容当成文档真的重复写了,回答时会出现啰嗦甚至错觉。我在召回阶段加了去重逻辑,按原文位置去重,保留首次出现的那块。这个问题不跑真实问答很难发现,很多人会误以为是模型生成能力弱。
第三个坑是调度器的死循环。刚开始失败重试没设上限,有个坏文件名让任务反复失败反复跑,日志一天刷了几千行。后来我把重试次数、错误类型都记录在任务表里,连续三次失败直接标记FAILED不再重试,问题立即缓解。现在我看任何任务队列,第一件事就是确认重试上限和失败转移逻辑。
5.3 性能调优的几条实操建议
预处理阶段,PDF解析和OCR是耗能大户,建议把这两个任务做成独立服务,用消息队列连起来,避免互相拖累。向量化阶段是GPU密集,如果机器没有GPU,可以用CPU跑,只是慢一点,效果不变。我在一台旧笔记本上跑过两千页文档的向量化,耗时大约半小时,完全可接受。
模型调用层面,强烈建议加缓存。同一段文本的向量化结果、同一组切片的摘要结果都可以做成KV缓存,命中缓存时直接返回。我实测过,高频文档去重后模型调用量能下降三成以上。缓存过期策略也很简单,按文档版本号失效:文档重新处理了,旧缓存就作废。
还有一个容易被忽视的点:中间产物一定要落盘。我开发阶段就因为没保留中间产物,调了几次参数后想回溯对比效果,只能重新跑一遍全量,白白浪费了两个小时。从那以后,每一批处理结束我都会生成一份JSON报告,包含切片数、耗时、错误数。调试效率和之前相比完全不一样。
本地智能体这个方向,我做得越多越觉得,难点从来不是模型,而是模型外面的工程链路。文档预处理决定了输入质量,任务调度决定了运行稳定性,检索与摘要策略决定了超限问题能不能根治。如果你正被长文本超限折磨,别急着换大窗口模型,先把这三层打好。我现在的环境里,8K上下文的模型处理两百页文档绰绰有余,靠的全是链路设计。至于下一步,我打算把定时报表生成和异常检测也接进这套流水线里,让智能体从“回答问题”进化为“主动发现问题”。你手头的场景哪怕再小,按这条链路走一遍,积累的经验后面都会加倍还给你。