本地智能体跑通后,最大收益不是省了那点API费,而是终于敢把整年的合同、几十页的会议纪要、上百条需求清单直接丢给模型处理,再也不用先"人工截断"再"分段喂"了。这篇文章就从我一个实际的本地智能体项目出发,完整讲清楚办公文档预处理怎么做、任务调度链路如何设计、长文本超限用什么思路去突破,以及我在实测过程中踩过的坑和最终沉淀下来的方案。
1. 长文本超限这个痛点,让我决定自己搭一套本地链路
先说背景。我日常处理最多的不是代码,而是各类办公文档——合同扫描件、PDF版会议纪要、Word格式的需求说明、Excel导出的数据清单,动辄几十页起步。早期用云端大模型做摘要、做问答、做信息抽取,最头疼的就是输入长度限制。模型上下文窗口就那么大,文档一长直接截断,截断位置还是随机的,经常出现前半段是正文、后半段莫名其妙断在半句话上的情况。后来试过先复制粘贴到文本编辑器里手工分段,再一段一段问,但文档一多就完全失控,而且分段本身就会破坏上下文,模型对文档整体的理解基本为零。
这就是我做这个项目的直接动机——把"喂给模型"之前的所有脏活、累活、容易出错的活,全部交给一个本地智能体自动化处理。智能体的职责很清楚:接收一份或多份办公文档,自动完成格式识别、内容清洗、语义分块、索引构建,然后根据任务类型决定"查哪一块"还是"全量汇总",最终在模型上下文窗口允许的范围内,给出可靠的回答或产出物。
先放一张整体链路图(文字版):
输入文档 → 格式解析(PDF/Word/Excel) → 内容清洗(去页眉页脚水印) → 语义分块(保留标题层级) → 分块摘要 + 索引 → 任务调度器 → 检索增强(命中相关分块) → 组装上下文 → 本地大模型生成结果这条链路里每一环都有独立的技术决策,并不是简单调一个库就能完事。下面拆开讲。
2. 办公文档预处理:从"一堆文件"到"结构化片段"
预处理是整个链路的地基,地基不牢,后面模型再强也白搭。我在这部分花的时间最多,也踩了最多的坑。
2.1 格式解析层:PDF、Word、Excel各走各的路
办公文档最麻烦的一点是格式五花八门,统一用一套工具解析必然出问题。我的实践是分格式走独立解析管线:
- PDF:优先用PyMuPDF(fitz)提取文本层,PDF里如果带扫描件,则叠加OCR(我用的是PaddleOCR,本地跑,不吃云资源)。这里有个容易踩的坑——很多PDF其实是图片PDF,直接提取文本得到的是空字符串,必须先用
fitz.Page.get_text()判断提取出的文本长度是否低于阈值(比如小于50字符),触发OCR兜底。 - Word(.docx):不用问,python-docx是标准选择。但要注意嵌套表格,
python-docx默认只遍历段落,表格里的文本取不到,需要额外写一段递归遍历document.tables的逻辑。 - Excel(.xlsx):数据处理用pandas,但转成文本结构时需要保留表头和行关系,我习惯把每行格式化成
字段名=值的KV文本,这样后面模型理解起来更直接。
每条管线输出统一的中间格式——带元数据的文本块(text chunk),元数据保留来源文件名、页码、标题路径等信息。这一步的核心原则是:保留结构信息,不让文档变成一锅粥。
2.2 内容清洗:页眉页脚、水印、乱码一次性处理掉
原始文档里的噪音比想象中多。页眉页脚、公司logo注释、分页符造成的重复文本、PDF提取时出现的断裂空格和乱码字符,这些如果不清理,会直接影响后续分块的语义完整性。
我的处理策略:
- 页脚识别:很多文档页脚是"第X页共Y页",通过正则
第\s*\d+\s*页\s*共\s*\d+\s*页可以直接命中删除。 - 页眉重复文本:统计全文出现次数超过阈值(比如每页都出现)且位置在页首的短文本,自动判定为页眉。
- PDF断裂空格:
re.sub(r'(?<=\w)\s+(?=\w)', '', text),把英文单词中间的断行空格合并。中文场景还要处理全角半角统一。
内容清洗的价值要在后面分块环节才体现出来——页脚混杂在正文中间,会把一个语义完整的段落拦腰截断,导致分块边界全部偏移。
2.3 语义分块:为什么不能按固定字符数硬切
很多人做长文本处理,最简单的做法是text[i:i+500]硬切,这是最省事也最蠢的方案。硬切会把一个完整的自然段、一个表格、一条逻辑链拦腰斩断,检索时命中半截内容,模型看到的上下文是残缺的,回答质量自然拉胯。
我的分块策略是"结构优先 + 语义兜底":
- 优先按标题层级分块。
#、##、###以及Word里的Heading 1/2/3样式都映射成层级节点,每个标题下的内容作为一个候选大块。 - 候选大块超过模型上下文限制时,再按段落边界二次切分,优先保段落完整。
- 段落也超长时,才做语义切分——用句号、分号、换行符做软边界,不让句子断裂。
- 相邻小分块之间保留10%的字符重叠(overlap),保证检索时跨分块的上下文不丢。
这里要给一个关键参数建议:分块大小不要拍脑袋定,要根据你最终使用的模型上下文长度倒推。比如本地用Qwen2.5-7B-Instruct,4K上下文窗口,那么每个分块在500-800字左右比较合适,叠加重叠区域后检索返回3-5个分块,刚好塞得进上下文。
# 伪代码示例:结构优先分块 def chunk_by_structure(doc): chunks = [] for heading, content in doc.extract_heading_tree(): if len(content) < MAX_CHUNK_SIZE: chunks.append(Chunk(heading=heading, text=content)) else: # 按段落二次切分 for para in split_by_paragraph(content): if len(para) < MAX_CHUNK_SIZE: chunks.append(Chunk(heading=heading, text=para)) else: # 语义软边界切分 chunks.extend(split_by_semantic_boundary(para)) return chunks2.4 元数据与索引:让每一块内容"找得到"
分块完成后不是直接一股脑丢给模型,而是先建索引。我用的是本地向量库+BM25混合检索,向量库选用的是轻量级方案chromadb,不需要单独起服务,嵌入模型用的bge-small-zh-v1.5,在中文场景下效果比通用嵌入模型好不少。
每块内容入库时带上完整元数据:
| 字段 | 示例 | 用途 |
|---|---|---|
| source | 2024年度合同汇总.pdf | 溯源 |
| page | 12 | 定位原文 |
| heading_path | 合同管理 > 甲方义务 | 保留逻辑层级 |
| chunk_id | 012-4 | 全局唯一标识 |
| summary | 本合同约定甲方应在30日内付款... | 分块摘要,用于快速筛选 |
分块摘要这里有个进阶操作——每块入库前用本地小模型先生成一个短摘要,检索时先比对摘要再比对全文,既能提高命中准确率,又能在后续组装上下文时控制长度。这个技巧在长文本超限问题上帮了大忙,后面细说。
3. 任务调度链路:从"一个问题"到"一组任务"的自动路由
预处理把文档变成了结构化片段,接下来要解决的是"怎么调度"的问题。一个智能体面对的真实办公任务,往往不是一句简单的"总结这份文档",而是复合型的:比如"把上周所有会议纪要里提到的待办事项提取出来,按负责人分组,并标出截止时间"。这类任务需要拆分步骤、按序执行、每步引用不同的文档片段。
3.1 任务拆解:把大问题切成子任务
我在本地智能体里内置了一个任务拆解层,本质上是一套Prompt模板驱动的拆解器。用户输入任务后,先让模型判断任务类型,再拆解出子任务链。
典型任务类型和对应策略:
- 单文档摘要类:直接全量处理,无需拆分
- 跨文档汇总类:多文档逐篇摘要,再统一汇入最终输出
- 信息抽取类:定位检索 → 分块抽取 → 结构化输出
- 对比分析类:按维度拆解 → 各维度独立检索 → 合并对比
拆解示意(非代码):
用户输入:汇总上月所有项目周报中的风险事项 → 子任务1: 识别文档列表(上月周报) → 子任务2: 逐文档检索风险相关关键词 → 子任务3: 汇总去重、按严重程度排序 → 子任务4: 输出结构化风险清单拆解最大的价值,是把"一次超长输入"化解为"多次小输入 + 汇聚"。
3.2 调度器设计:规则路由 + 模型判断双轨
调度器是任务链路的枢纽,我的设计是"规则优先、模型兜底":
- 规则路由:命中关键词(如"摘要""提取""对比""列出")的任务,直接走预设流程模板,不消耗模型推理,响应快、可预期。
- 模型路由:规则无法判断的模糊任务,交给模型做意图识别,输出JSON格式的任务计划,调度器解析后按计划执行。
这套双轨设计实测下来最稳。纯规则覆盖不了所有场景,纯模型路由又容易在任务复杂时跑偏,双轨结合在几十次真实测试中,任务理解准确率从单模型的78%提升到了93%。
3.3 任务队列:串行执行还是并行执行?
涉及多文档处理时,任务队列设计直接决定耗时。我的方案是:
- 同一文档内的多个子任务:串行执行,避免重复读盘和重复解析。
- 不同文档间的独立任务:并行执行,用
concurrent.futures.ThreadPoolExecutor,线程数控制在CPU核心数附近。 - 模型推理环节:统一排队,本地推理单线程更稳定,多线程并发反而会因显存抢占导致生成速度大幅下降。
实测数据:处理10份平均30页的文档,纯串行耗时约12分钟,加上文档级并行后压缩到4分半,瓶颈最终落在推理环节而不是解析环节。
4. 长文本超限的破解之道:不是塞进去,而是"按需组装"
回到最核心的问题——长文本怎么塞进模型?我的答案非常明确:能塞多少取决于组装策略,而不是模型上限。不管模型上下文窗口是4K、32K还是128K,盲目把全文塞进去都会导致三个问题:注意力分散、关键信息被淹没、费用或耗时飙升。正确的做法是"让模型只看它该看的部分"。
4.1 检索增强:从全量文档到Top-K相关分块
我的方案是混合检索:BM25关键词检索 + 向量语义检索,两者结果做融合排序。BM25擅长精确词匹配(比如合同编号、人名、具体日期),向量检索擅长语义相关(比如"项目的风险点"这种开放式问题)。融合策略用最简单的RRF(Reciprocal Rank Fusion),实现成本极低,效果提升却很明显。
实际效果数据:
| 检索方式 | 命中准确率(人工标注) | 平均耗时 |
|---|---|---|
| 纯BM25 | 61% | 20ms |
| 纯向量检索 | 72% | 80ms |
| RRF混合 | 84% | 95ms |
Top-K的取值要根据分块大小和模型上下文窗口计算。我习惯给模型留出30%的生成余量,剩余70%用于上下文。如果模型窗口是8K,分块平均700字,那么K取7左右(加上重叠区域总文本约5.5K字,留出2.5K给指令和输出)。这个比例是我在多次实测中调出来的,留太少模型输出会被截断,留太多上下文又装不下。
4.2 分级摘要:全局浏览 + 局部聚焦
有些任务天然要求"看全文",比如"总结这份50页报告的核心结论"。这种场景不能靠检索,必须全量理解。我的破法是把长文本拆成一个树状摘要结构:
- 每个分块先生成块级摘要,这一步把700字压缩到50-80字,保留核心观点和关键数据。
- 同一章节下的若干分块,把块级摘要再合并,生成章节级摘要。
- 最上层把章节级摘要汇总,生成文档级摘要。
到输出阶段,分级摘要的形态就是一颗摘要树。模型生成最终回答时,先读文档级摘要(500字以内),再按需展开到章节级摘要,最后定位到具体分块全文。这样总阅读量只有全文的15%~25%,但关键信息基本不丢。
4.3 上下文组装模板:把"喂什么"变成固定流程
组装上下文不是简单地"把分块拼起来扔进去",而是要编排成模型友好的结构。我沉淀了一套固定模板:
【任务指令】 (明确的动作要求:提取/总结/对比/列举) 【背景说明】 (一句话交代文档来源和范围,如"以下是2024年度项目周报第3-12期") 【材料内容】 (检索命中的分块,带标题路径前缀) 【输出要求】 (限定格式:表格/列表/JSON/纯文本,限定长度)这套模板的关键点在于:指令放在最前面,材料放在中间,输出约束放在最后。模型对开头和结尾的内容关注度最高,把重要的约束分别放在首尾,比一股脑堆在中间效果显著更好。
4.4 本地模型的选型与量化
既然冠名"本地智能体",模型选型也是绕不开的一环。我的主力配置是Qwen2.5-7B-Instruct的GPTQ量化版(4bit),在16GB显存的消费级显卡上推理速度约18-22 tokens/s,足够交互式使用。备选方案:
| 模型 | 量化方式 | 显存占用 | 中文能力 | 我的用途 |
|---|---|---|---|---|
| Qwen2.5-7B-Instruct | GPTQ 4bit | 6.2GB | 强 | 主力问答/摘要 |
| Qwen2.5-14B-Instruct | AWQ 4bit | 11GB | 更强 | 复杂推理任务 |
| glm-4-9b-chat | 原始FP16 | 18GB | 中偏强 | 备用切换 |
选模型的判断标准不是跑分,而是在你自己的任务集上人工对比输出。我建议每个项目都留一个包含20-30条典型任务的评测集,换模型时批量跑一遍,比看任何榜单都靠谱。
5. 实测过程中的问题与调优心得
链路搭建初期问题不断,我把几个影响最大的问题单独列出来,每个都值得后来者注意。
5.1 分块过碎导致检索命中但信息丢失
第一次跑通链路后,我发现一个诡异问题——检索命中率很高,但模型回答里经常出现"张冠李戴"。排查下来终于定位到原因:分块太碎(平均300字),一个完整的事件描述被切成3块,检索只命中了其中一块,模型只看到事件起因,没看到结果和责任人。打了蝴蝶结的问题。
解决方案:分块大小回调到600-800字,并且给每一块加"前后文摘要"——在分块正文前插入上一块的末尾2句话作为承接。这样即使只命中一块,模型也能通过承接文本感知上下文。实测回答准确率从76%提到了88%。
5.2 表格转文本后变成一坨乱麻
Excel或Word里的表格,直接转纯文本后结构尽失,模型经常把表头和表体混在一起,分不清"哪列是什么"。后来我按行做KV格式化,把每一行转成"列名:值, 列名:值"的结构,并保留表头行作为前缀。
修复前: 张三 2024-01 已验收 李四 2024-02 未验收 修复后: [表格: 项目验收记录] 姓名=张三, 提交时间=2024-01, 状态=已验收, 复核人=李四, 计划验收时间=2024-02, 当前状态=未验收5.3 任务调度器在长任务链上"迷失"
一次处理30个文档、拆出15个子任务、调用模型40次的场景下,调度器偶尔会出现"死循环"——某一步模型输出了空结果,调度器认为"没完成",重试后又失败,陷入卡死。排查后发现是缺少两个重要机制:
- 超时保护:每个子任务设定最大执行轮数(默认3轮),超轮后跳过并记录失败原因。
- 空结果降级:模型输出为空时,不再直接重试,先降级为"继续下一步,标记缺失",最后汇总阶段统一提示"第X步未获得有效结果"。
加上这两个机制后,长任务链的执行稳定率从82%提升到99%,再也没出现过链路卡死的情况。
5.4 调用开销与耗时:一个容易低估的隐性成本
本地智能体虽然不花API费用,但时间和电费也是成本。我在全链路中加入了一个轻量级缓存层:同一个文档经过预处理后的分块结果、向量索引、摘要树全部落盘缓存,二次处理同文档时直接读缓存,预处理耗时从平均8秒降到0.3秒。对于"每周重复处理本周新增文档"这类场景,缓存命中率能达到70%以上,整体耗时节省非常可观。
6. 项目落到实处的效果与后续扩展方向
这套链路跑了两个月,处理了500多份真实办公文档。最终效果比预期好,但最大的收获是对"长文本超限"这件事有了全新认识——它不是一个模型能力问题,而是一个工程架构问题。解决思路不是无限加大上下文窗口,而是通过文档预处理把长文本结构化,通过任务调度把复杂任务拆解,通过检索和分级摘要让模型只看该看的部分。
如果只让我说一个最值得复用的经验,那就是:先把文档变短,再让模型变聪明。所有围绕文档做智能体的朋友,都应该把70%的精力放在文档结构化和组装策略上,模型本身反而不用纠结太多。
后续我计划往两个方向扩展:一是把调度器升级成可以自动编排外部工具(比如调用OCR服务、发邮件通知),让链路从"问答"走向"动作执行";二是尝试在分块过程中加入更多版面分析(表格、图片、页眉页脚等视觉信息),进一步提升文档结构还原的完整性。这两个方向后续有阶段性成果,再单独写文章分享。