资讯中心

长文本超限不再怕:本地智能体文档预处理与任务调度实战

📅 2026/10/1 4:51:27
长文本超限不再怕:本地智能体文档预处理与任务调度实战
办公文档处理这件事我在本地折腾了大半年踩过的坑比写过的代码还多。最初的想法很简单让本地大模型帮忙读几份PDF、写写摘要、做个知识问答。结果一试就卡住——不是模型答得不好而是文档一长请求直接报“context window exceeded”。这其实就是标题里说的“长文本超限”。后来我把整个流程拆开搭了一条“文档预处理任务调度”的本地智能体全链路才真正把问题解决。这篇文章就是来分享这条链路怎么设计、怎么落地以及有哪些坑可以提前绕开。适合正在做本地知识库、文档问答、批量文档处理的同学参考。1. 整体设计与思路拆解先说结论长文本超限不是一个模型问题而是一个工程问题。大模型的上下文窗口就那么大硬塞长文档进去必然失败。真正靠谱的做法是把文档先拆成小块按需取用再把大批量的小任务调度起来执行。整个过程涉及文档解析、清洗、分块、向量化、检索、任务队列、模型调用七个环节环环相扣少一环都会出问题。1.1 为什么需要一条“文档预处理任务调度”的全链路我最早犯的错误是把文档预处理和智能体拆成两套东西先用脚本把PDF转成TXT再手动把TXT丢给智能体问答。结果遇到两个问题。第一TXT里的页眉页脚、冗余空白、OCR乱码全被当成有效内容塞给模型既浪费token又污染回答。第二第一次问还好第二次问换个话题模型需要重新读全文上下文塞满之后就彻底卡死。所以必须把预处理和调度做成一条自动化的链路。预处理负责把原始文档变成干净、有结构、可检索的片段任务调度负责把这些片段分配给模型并控制并发、重试和汇总。只有这样智能体面对任意长度的文档都能做到“按需取用”而不是“全部读入”。1.2 方案选型本地优先轻量可控我当时在Dify和自研框架之间犹豫过。Dify确实是开箱即用的智能体平台有现成的知识库、工作流和模型接入适合快速验证。但我的场景比较特殊文档格式混杂、数据不能出内网、对调度细节要求高Dify这种统一平台反而有些施展不开。最后我选了“Python脚本 Chroma向量库 Ollama本地模型 asyncio任务队列”的组合。选这套组合的理由很直接。Python生态对文档解析的支持最全pypdf、python-docx、openpyxl都能直接调Chroma足够轻量一个进程内跑起来不需要额外的数据库服务Ollama把本地模型的部署简化成一条命令不用折腾CUDA和模型格式asyncio做任务调度足够用还不需要引入Celery和Redis这些重组件。整套方案跑在一台32G内存、8G显存的机器上完全够用。1.3 全链路流程这条链路最终跑起来是这样走的原始文档进入系统后先经过格式解析和内容清洗得到干净的纯文本然后执行分块每块控制在400到600字符之间并设置少量重叠接着对每块文本做向量化写入本地向量库当用户提出问题时先做向量检索召回最相关的几个块再把这些块拼进提示词交给本地模型生成回答。如果要做整篇文档总结思路稍有不同先按顺序把每块分别交给模型生成分段摘要再由调度器把分段摘要汇总成最终摘要。这条链路的核心思路是“把大问题拆成小问题把小问题并发解决再由上层合并结果”。它在工程上对应两个核心模块文档预处理管线和任务调度器。前者负责“拆”后者负责“调”。2. 核心细节解析与实操要点这个部分我挑最关键的四个环节讲每一步都直接影响最终效果。2.1 文档解析从PDF/Word/TXT到干净文本文档解析是整个预处理的第一步也是最容易出脏数据的地方。PDF文件尤其麻烦有些是文字型PDF可以直接抽文字有些是扫描件必须先做OCR否则抽出来全是空壳。我实际用下来pypdf能应对大部分文字型PDF但如果遇到表格或双栏排版pypdf抽出来的文本顺序会乱。这时候pdfplumber会好一些它能按位置关系还原阅读顺序。Word文档用python-docx能保留段落结构但需要自己过滤文本框和页眉页脚。TXT相对干净注意编码就行。解析完还要清洗。清洗不是简单去掉空白而是要把页眉页脚、页码、目录、重复分隔符、乱码字符、超链接这些都识别出来并清除。我的经验是写一个规则函数依次处理空白字符压缩、页眉页脚特征剔除、控制字符过滤、编码错误修复。这一步做不好分块之后就会有一堆“垃圾块”混进向量库检索时被反复召回回答质量断崖式下降。2.2 分块策略解决长文本超限的第一道关卡分块是解决长文本超限最关键的一环没有之一。分块逻辑不仅决定模型能不能把内容装进上下文还决定检索能不能命中关键信息。我把常见的分块方式分成三种固定字符分块、段落分块、语义分块。固定字符分块最简单按字符数硬切速度快但很容易把一句话从中间切断导致语义残缺。段落分块按换行符切能保留段落完整性但段落长短不一有的块几百字有的块几个字。语义分块用嵌入模型判断句子之间的关联度再切块效果最好但计算量大。我实际项目里通常用“段落优先字符兜底”的混合策略先按段落切超过上限就按句子再切低于下限就并入相邻段落。参数选择上我用的块大小是512字符重叠64字符。这个参数不是拍脑袋定的是拿测试集跑出来的512字符对中文文本来说大概能覆盖80到120个词既不会太小增加检索次数也不会太大浪费上下文空间。64字符的重叠是为了避免刚好切断一个完整论点的首尾信息。如果你的文档学术性很强可以降到384字符检索精度会更高。2.3 向量化与检索让智能体只关注真正需要的片段分块之后每块文本要转成向量存进向量库。这里有个容易忽视的点嵌入模型的选择直接影响检索质量。本地环境我用的是BGE-M3这一类的模型它的中文效果优于很多通用模型支持长文本而且在Ollama里能直接跑。如果机器性能有限也可以考虑更轻量的bge-small-zh速度更快但精度略微下降。向量库索引建立之后智能体问答时先根据问题做向量检索。检索不是简单取相似度最高的那一个块而是取top-k个块。我设的k是5也就是每次问答最多召回5个文本块约2500字符远小于模型上下文窗口。检索方法上除了普通向量相似度还可以用MMR最大边际相关来避免召回内容雷同的块。我在做技术文档问答时对比过MMR召回结果覆盖更广答案更全面。这里要特别提醒向量检索不是万能的。对于数字、编号、代码片段的精确匹配目标关键词检索混合BM25检索效果会好很多。很多系统里可以做“稠密稀疏”双路召回也就是同时用向量相似度和关键词权重取交集再合并。本地跑起来也不难Chroma已经支持内置的BM25算法虽然没有外部引擎那么强已经足够应付大部分办公文档场景。2.4 任务调度把“一次超限请求”拆成“一组可控子任务”文档问答场景其实还算简单真正考验调度能力的是“整篇文档总结”和“批量文档处理”。比如要给200页PDF生成摘要分块之后可能产生300多个文本块直接一次性全部塞给模型显然不可能。正确的做法是把每个块当成一个独立任务交给调度器排队执行由多个工作进程并发向本地模型发送摘要请求最后再把每个块的小摘要按顺序合并成一个大摘要。任务调度模块我选择了asyncio队列。理由有几点本地模型服务的IO等待主要在网络与显存推理asyncio可以做到单线程内异步管理大量请求不需要开一堆线程asyncio天然支持超时控制、任务取消和并发限制而且代码简单不容易出现线程安全问题。如果你要跨机器分布式调度再考虑CeleryRedis但目前我的场景还不需要。并发数设置很关键。本地模型跑在单卡上并发太高会导致显存溢出或请求排队毫无意义。我用8G显存跑7B量化模型实测并发数设为2比较合适再多就会导致单次推理响应时间剧增。给每个任务设置超时时间也很重要某个块如果生成出现异常不能一直卡死队列超时要自动重试重试两次仍失败就直接记录失败原因并跳过。调度器还要维护一个任务状态表记录每个块的“待处理/处理中/成功/失败”这样即使中途崩溃也能从断点续跑。3. 实操过程与核心环节实现这一部分直接给复现路径。我会按最终跑通的版本来讲你跟着做也能搭出一套能用的本地智能体。3.1 本地模型与向量库环境准备负责推理的模型我用的是Ollama部署的Qwen2.5优势是中文理解稳、部署省事一条命令就能拉起服务。代码是ollama pull qwen2.5:7b ollama pull bge-m3 ollama serveOllama服务默认监听11434端口之后代码里通过HTTP调用即可。向量库安装Chroma直接用Python包就能操作pip install chromadb langchain langchain-community pypdf pdfplumber python-docx如果还需要OCR可以安装PaddleOCR或Tesseract但要注意额外占用的资源。我这边扫描件不多暂时先用Tesseract兜底。3.2 写一个可复用的文档预处理管线文档预处理管线是这个项目的核心资产我把它封装成一个函数输入文件路径输出干净的分块列表。from pypdf import PdfReader import re def extract_text(path): reader PdfReader(path) text \n.join(page.extract_text() for page in reader.pages) return text def clean_text(text): text re.sub(r\s, , text) text re.sub(r\bPage \d\b, , text) text re.sub(r[^\u4e00-\u9fff\u3000-\u303fA-Za-z0-9。、《》\.\- ], , text) return text def split_text(text, chunk_size512, overlap64): chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) if end len(text): end end - overlap chunks.append(text[start:end]) start end return chunks这段代码是简化版实际使用时分块函数我还会做段落感知先按换行切成段再对段做字符限制。clean_text里的正则需要根据你的文档类型不断调整比如有些PDF会在页脚出现“第 X 页共 Y 页”这也要过滤掉。清洗规则的积累是个长期过程每喂一批新文档就补一条规则。3.3 任务调度模块设计与代码落地调度模块用asyncio写核心逻辑是import asyncio import httpx async def summarize_chunk(client, chunk): payload { model: qwen2.5:7b, prompt: f用不超过100字概括以下内容{chunk}, stream: False, } r await client.post(http://localhost:11434/api/generate, jsonpayload) return r.json()[response] async def run_batch(chunks, concurrency2): sem asyncio.Semaphore(concurrency) async with httpx.AsyncClient(timeout120) as client: async def worker(chunk): async with sem: for attempt in range(2): try: return await summarize_chunk(client, chunk) except Exception as e: if attempt 1: return f[失败] {e} await asyncio.sleep(2) results await asyncio.gather(*[worker(c) for c in chunks]) return results这里面有个细节容易踩坑Ollama的 /api/generate 接口在并发请求时会以队列方式处理如果并发设太高请求会无限排队看起来像是卡死了。所以信号量Semaphore必须设置成和服务端实际并发能力匹配的值。重试策略我基本上只重试一次因为本地模型很少出现偶发故障如果第一次失败重试往往也是因为提示词太长或文本编码出问题这时候不如记录日志跳过。3.4 问答智能体与分段摘要智能体的实现问答智能体和分段摘要智能体是两个不同的入口逻辑不同。问答智能体的关键在于“先检索再作答”。每次用户提问先根据问题做向量检索召回5个块把块和问题一起组成提示词再交给模型。def ask(query): docs collection.query(query_texts[query], n_results5) context \n.join(docs[documents][0]) prompt f根据以下文档内容回答问题\n{context}\n问题{query} return ollama_chat(prompt)这种方式每一次问答最多消耗约2000字符的上下文远低于模型上限。分段摘要智能体则是按顺序处理全部块生成多个小摘要再把小摘要合并起来生成最终摘要。合并摘要时需要注意如果小摘要超过200个二次合并还可能超限那就需要分组两次汇总。这就是“多级摘要”层级可以扩展到三层。3.5 用一份200页PDF实测全链路我拿一份200页的行业研究报告做了实测全文约12万字符。预处理耗时约6秒分块产生约240个块向量索引耗时约10秒分段摘要240个块并发数2总耗时约48秒汇总最终摘要约3秒。整条链路跑完约1分钟输出一份500字的报告摘要。这个过程中最直观的感受是没有预处理链路时这份报告根本喂不进模型有了链路之后时间成本主要体现在分块摘要的批量执行上任务调度把240个独立小任务排队执行虽然不能减少总计算量但把不可行的“一次超限请求”变成了可行的“一组并发子任务”。4. 常见问题与排查技巧实录最后这部分是实战中最容易踩的坑整理成速查表方便你直接对照排查。4.1 上下文超限报错的处理清单如果模型调用时报错context_length_exceeded或token limit exceeded按优先级检查是否忘了分块是否把整篇文档直接拼进提示词检索时是否设置了合理的top_k上限是否在多次对话中累积了历史消息很多问答系统把历史对话也拼进去第一次不超第三次就超了。解决办法是把历史消息压缩成摘要或只保留最近两轮。4.2 分块切碎语义的修正办法分块效果不好最常见的症状是检索召回了一个块内容涉及“但是……所以……”然而“但是”之前的论证在另一个块里。原因是分块时把语义关系从中间切断了。最简单的修正办法是增加重叠长度我之前用64字符遇到长句多的文档会调到128字符。另外分块时尽量等在句号、问号、感叹号处切不要等长度到了就硬切。4.3 本地推理资源紧张时的调优本地资源有限最关键的优化方向是减少不必要的推理调用。比如分段摘要时如果某些块本身没有实质内容只有图片说明、目录可以直接跳过不调用模型。再比如模型量化程度4bit量化会损失一点质量但在8G显存上运行速度比7bit快很多。如果要跑更大模型可以考虑把数据分块存在内存里推理时再加载避免模型常驻显存但这会牺牲响应速度。4.4 向量检索结果不理想怎么办检索结果不好的时候很多人第一反应是换嵌入模型但往往是分块粒度的问题。块太大导致一个块里包含多个主题检索时相似度被稀释块太小导致上下文不足召回内容不完整。我的建议是先用测试问题跑一批候选块看看是被哪一步坑了。如果召回的相关性排序不对再考虑混合检索和重排序模型。Rerank模型可以在召回后用一段小模型对候选块重新评分能明显提升精排效果。还有一个容易被忽略的点嵌入模型对中文专业词汇的理解受分词影响。如果你们的文档里有大量行业黑话可以考虑在分块之前做术语同义词替换把不常用的表达标准化。我在合同文本场景里试过把“甲方”“乙方”“委托方”“受托方”统一替换之后检索准确率提高了不少。写在最后折腾完这套本地智能体我最大的体会是长文本超限这个问题并不是模型能力不行而是我们还没给模型准备好它需要的“切好的菜”。文档预处理和任务调度这两个环节一个负责把内容整理成模型能消化的形态一个负责把大批量计算拆成可控的节奏。踩过几次坑之后我现在对几个细节特别敏感一是清洗规则必须持续迭代二是分块参数要按文档类型调整三是调度并发不要贪多。如果这三件事做好了本地智能体处理几百页的办公文档其实非常稳。后面我还在尝试加一层多级摘要和重排让整条链路在更长文档下也能保持回答质量。希望这篇实战记录能帮你少走几步弯路。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案