
最近和几个做 AI Agent 的朋友聊天十个里有八个在吐槽同一件事Agent 不是不会干活而是被文档折腾死的。模型能力再强喂给它的 PDF 排版一乱照样读错工具链再顺文档权限没管好Agent 改错文件就是一次事故。于是越来越多团队开始聊一个不算新、但一直没被认真做起来的概念document layer for AI agents也就是给 AI 代理单独加一层“文档中间层”。这个文档层解决的是一个很具体的矛盾Agent 需要读写文档但现实世界的文档是 PDF、Word、HTML、Markdown、扫描件格式五花八门信息散落各处版本来回覆盖权限又往往一团乱麻。如果让 Agent 直接去啃这些原始文件就像让一个实习生直接去翻阅一个没有任何索引和规范的档案室能干活但很容易干出错事。文档层的目标就是把这些杂乱文档变成 Agent 能稳定读取、可靠调用、安全修改的统一接口。这篇文章我会从为什么需要文档层讲起再拆解文档层应有的核心能力然后给出一套最小可落地的实现方案最后把我实际落地过程中踩过的坑和排查思路一起整理出来。适合正在做 RAG、Agent 工作流、企业内部知识库自动化或者打算让 Agent 真正接手文档类任务的团队参考。1. 先想清楚AI 代理为什么需要一层“文档层”1.1 没有文档层的时候Agent 是怎么翻车的过去半年我看过不少 Agent 项目最典型的翻车场景就三个。第一个是 RAG 类知识库问答。团队把几百份 PDF 丢进向量库Agent 回答问题时看起来头头是道一查引用就露馅引用内容在原文里根本不存在或者被切碎后的段落拼接得牛头不对马嘴。原因很直接PDF 解析这一步就没做好双栏的论文被按单栏顺序读表格数据乱成一团图片里的信息完全没被提取Agent 拿到的上下文本来就是脏的。第二个是文档自动化处理。比如一个合同审查 Agent需要读取合同、标注风险条款、生成修订建议。问题在于合同的水印、页眉页脚、嵌套表格会把解析结果搅得乱七八糟Agent 把“违约责任”条款和“送达条款”搞混给出的审查意见也跟着错。这种场景下不是模型能力不够而是喂进去的“原材料”没有经过结构化解构。第三个是让 Agent 直接修改文档。你以为它只是改一个 README结果它在没有版本控制的情况下覆盖了同事刚更新的内容你以为它只读某个目录结果因为权限校验缺失它顺手读走了敏感资料。写操作一旦出错影响比读错更严重因为文档被污染之后后续所有基于这份文档的判断都会跟着错。这些翻车场景的根子不是某一个解析库不够好而是缺少一个统一的文档处理抽象层。Agent 应该面对的是一个稳定的“文档接口”而不是面对格式千变万化的原始文件。1.2 文档层不是新概念只是过去没人认真做其实“中间层”思维在软件工程里太常见了。操作系统屏蔽了磁盘和内存的差异给应用程序一个“文件”的抽象数据库存储引擎屏蔽了磁盘读写细节给上层一个“表结构 SQL”的抽象。文档层之于 AI Agent本质上就是操作系统之于应用程序。没有文件系统之前程序要自己管理磁盘扇区没有数据库之前程序要自己处理数据持久化。现在很多 AI 项目也处在“原始时代”Agent 要自己处理 PDF、自己切分文本、自己管理版本、自己判断权限。这些事情不是不能做但如果每个项目都从头做一遍成本极高而且做得都不够好。文档层要做的就是把这套能力沉淀成基础服务给 Agent 提供四个最核心的稳定原语读文档、找文档、改文档、追踪文档。这样上层 Agent 不用关心文件是什么格式、存在哪里、权限怎么校验只需要面向一套统一的 API 编程。这也是为什么越来越多团队开始把“文档层”从 RAG 中间件里独立出来单独设计因为 RAG 中间件主要解决“找得着”的问题但 Agent 生产环境还需要“读得懂、改得安全、查得到来源”这三件事。2. 文档层的核心能力拆解摄取、检索、更新、治理2.1 摄取层把一切文档变成 Agent 能稳定读取的格式文档层的第一步是摄入Ingest。这一步的目标非常明确把任意格式的文档变成一份结构稳定、内容完整、带元数据的标准化中间表示。我的做法是统一转成带结构的 Markdown同时保留一份 JSON 形态的“文档对象模型”作为机器可读的中间格式。Markdown 给 Agent 读JSON 给程序做流程控制两份数据共享同一个解析结果保证一致性。解析链路一般分四步格式识别与预处理。根据文件类型选择解析器Word 转存为 docx 后解析 XMLPDF 根据是否扫描件决定走文本抽取还是 OCRHTML 则先用解析器清洗标签。版面分析。这一步最容易被人忽略。双栏 PDF、带复杂表格的合同、图文混排的网页如果直接按文本流抽取信息顺序一定会乱。我是用版面分析模型把页面切成区块再按区块的阅读顺序重组内容效果比纯文本抽取稳定得多。内容结构化。标题层级、段落、表格、列表、图片说明分别标记提取表格为结构化行数据图片单独走 OCR 或图像理解并保留在文档上下文中的位置。分块与索引。按结构感知的方式切块而不是简单按字符数硬切。比如标题下的小节作为一个候选块表格整体作为一个块块与块之间保留必要的上下文重叠。摄取层还需要做数据清洗和去重。同一份文档的多个版本会被放在一起摄入时可以用 checksum 判断内容是否变化避免重复向量化浪费计算资源。实测下来做好版面分析这一步对后续检索准确率的提升比换任何 Embedding 模型都明显。2.2 检索层让 Agent 拿到“够用、可信、可追溯”的上下文文档层的检索层不是简单调一个向量库做 top-K 召回。Agent 对检索的要求比普通问答更高因为检索结果会直接被当成“事实”去执行后续动作。所以我认为检索层必须做到三件事召回全、排序准、来源清。召回全意味着不能只靠向量相似度。向量检索擅长语义相关但对关键词、编号、精确术语的匹配往往不够稳定。我常用的方案是混合检索BM25 稀疏检索 向量稠密检索并行跑再用 RRFReciprocal Rank Fusion融合结果。合同编号、法条编号、设备型号这类内容BM25 的精确匹配能力特别重要。排序准意味着要加一层 Rerank。粗召回阶段拿 top-K比如 50 条交给交叉编码器模型做精排最后只保留最相关的 5~10 条作为上下文。Rerank 这一步能过滤掉大量语义相似但实际无关的噪声片段。我见过不少项目跳过这步结果检索结果看着相关但关键事实缺失。来源清意味着每个返回的片段都必须携带可追溯的元数据来源文件名、文档 ID、版本号、页码、区块路径、原文摘录。Agent 引用的时候把这些信息一并返回人才能核对“它说的到底是不是原文里有的”。很多 Agent 幻觉问题其实不是模型胡编而是检索层没有把来源信息完整地传给模型模型被逼着“自由发挥”。2.3 更新层Agent 写文档时的安全护栏比读更危险的是写。让 Agent 改文档、生成文档、批量更新知识库如果没有护栏一次误操作就可能污染整个文档源。我认为文档层的更新层至少要具备四个能力。第一个是“补丁式写入”。不要让 Agent 直接覆盖整个文件而是让 Agent 生成针对具体位置的修改指令比如“替换第 X 段为 Y”“在表格末尾追加一行”。文档层在受控环境下执行这些补丁而不是让 Agent 拿着文件句柄乱写。这样每次变更都是结构化、可审计的。第二个是版本管理。每次补丁执行后都生成新版本保留旧版本。Agent 读取时默认拿最新版本但可以通过版本号回溯历史。文档被改错了直接回滚旧版本比从 Git 历史里找要快得多。第三个是审阅与权限流程。对于高风险的文档比如合同、对外公告Agent 生成的修改不能直接生效而是进入待审阅队列由人确认后发布。低风险文档比如内部笔记草稿可以直接自动合并但保留审计日志。第四个是冲突检测。多个 Agent 同时改同一份文档或者人和 Agent 同时在改就需要基于版本的乐观锁。提交补丁时带上你基于的 base_version文档层检查当前版本是否还是这个版本如果不是拒绝提交并返回冲突信息。这个机制我后面会给出具体实现思路。2.4 治理层权限、溯源、审计一个都不能少文档层是 Agent 与数据之间的必经之路所以它天然适合做权限控制和审计这也是我强烈建议把文档层做成独立服务而不是代码库的原因。只要 Agent 访问文档必须走文档层那么权限校验就能在层内统一执行而不是散落在各个 Agent 代码里。权限控制的关键是“文档级 块级”的结合。文档级控制谁能读改这份文档块级控制更细粒度内容比如某类敏感信息所在的块只允许特定角色访问。检索时就要把权限过滤下推到向量查询的 metadata filter 里确保 Agent 根本看不到无权访问的内容而不是等检索结果出来后再“删除”。溯源是做一份document lineage也就是文档血缘图。某个回答所依据的内容来自哪份文档的哪个版本那份文档又是基于哪份原始文件生成的Agent 修改过哪些文档、改了什么、谁批准的这些信息在出问题时要能一条条查清楚。审计日志则是把读、写、检索行为全部记录下来包括时间、Agent 身份、操作类型、涉及文档 ID 和版本号。我这里用一张表总结文档层的能力分层分层核心职责关键能力对应问题摄取层文档进解析、OCR、版面分析、结构化、分块格式乱、信息丢检索层文档找混合检索、重排、来源元数据找不到、找不准更新层文档改补丁、版本、审阅、冲突检测写错、互相覆盖治理层文档管权限、溯源、审计越权、无法追溯这四层合起来就是文档层对 Agent 提供的完整语义。读、找、改、管四件事全部收敛到统一的接口后面。3. 从零实现一个最小可用的 Document Layer3.1 技术选型为什么我选“对象存储 向量库 元数据库”这套组合文档层实现方案很多但最小可用版本我强烈推荐用“对象存储 向量库 元数据库”的三件套组合理由很简单每一层各司其职替换成本低不需要一开始就上分布式存储或重型中间件。对象存储负责原始文件和标准化中间文件的持久化。本地开发可以用文件目录模拟生产环境用 S3、OSS 或 MinIO。选对象存储而不是普通文件系统是因为它的每一个对象都有独立 URI、元数据和版本能力天然适合文档管理。向量库负责语义检索。生产环境我常用 Milvus、Qdrant、pgvector 这类方案最小实现阶段用 Chroma 也够。核心要求是支持 metadata filter这样才能把权限过滤下推到检索底层。元数据库负责记录文档的元数据、版本关系、块结构、权限和审计日志。这一点很多人忽略总想着所有东西都塞进向量库但向量库不适合做事务性元数据管理。用 PostgreSQL 或 SQLite 存元数据用对象存储存文件内容用向量库存语义索引三者各管一摊。技术选型还有一个隐性决策解析器。最小实现阶段建议把解析能力封装成可插拔的 Parser 接口PDF 用一套解析器、HTML 用一套解析器、docx 用一套解析器互不污染。别把解析逻辑写进业务代码里后面换解析引擎会很痛苦。3.2 数据模型设计doc_id、版本、source、permission数据模型是整个文档层的地基设计得好不好直接影响后面所有功能的复杂度。我推荐从这几个核心字段起步# document 主表元数据库中 - doc_id: str # 文档唯一 ID如 doc_8f3k2 - title: str # 文档标题 - source_uri: str # 原始来源如文件路径或 URL - current_version: int # 当前最新版本号 - owner: str # 所有者/创建者 - permission_rules: json # 文档级权限规则 - created_at: str - updated_at: str # document_version 版本表 - version_id: str # 版本唯一 ID如 ver_12 - doc_id: str - version: int # 递增版本号 - object_key: str # 标准化 Markdown 在对象存储中的 key - checksum: str # 内容哈希用于内容比较 - changelog: str # 该版本的变更说明 - created_by: str # 修改者Agent 名称或用户 - status: str # pending / published / archived # chunk 块表 - chunk_id: str # 块唯一 ID如 chunk_a1 - doc_id: str - version: int - block_path: str # 在文档结构中的位置如 sec2.1#para3 - content: str # 块文本内容 - embedding_id: str # 向量库中的 ID - access_tags: list # 块级访问标签这个模型的核心思想是版本与内容分离。文档的每次变更都产生新版本但块表只记录当前发布版本的块信息。旧版本的内容在对象存储里保留需要时按版本号重新加载并重建块索引这样避免了每个版本都维护一份完整块表导致的存储膨胀。3.3 核心 API 设计ingest / query / patch / status文档层对 Agent 暴露的核心 API 不需要多四个操作足够覆盖大多数场景。我把它们统一设计为 REST 接口并用一个轻量鉴权头传递调用者身份文档层内部做权限校验。POST /v1/documents/ingest # 摄入新文档或新版本 GET /v1/documents/query # 检索文档片段 POST /v1/documents/{id}/patches # 提交文档修改补丁 GET /v1/documents/{id}/status # 查询文档状态与版本信息这四个接口背后的语义是ingest接收原始文件执行解析、结构化、分块、向量化并返回 doc_id。如果传入的文档和已有文档属于同一来源则自动创建新版本。query接收查询文本和权限上下文返回候选片段、相关度和来源信息。这个接口是 RAG 的唯一入口Agent 不允许直接查向量库。patches接收针对指定文档的一组补丁操作检查 base_version 与权限后执行生成新版本。status返回文档当前版本、待审阅补丁列表、最近修改记录方便 Agent 决策和用户排查。权限上下文我建议用X-Agent-Id加X-Agent-Roles两个头传递。文档层根据 Agent 角色匹配 permission_rules 和 access_tags决定是否放行。3.4 核心实现代码示例摄入、检索、补丁三件事下面是最小实现的核心代码片段。我以 FastAPI 为例存储部分用 SQLite 模拟元数据库向量库用 Chroma 的本地模式方便你本地跑通整个流程。实际生产环境把存储后端替换成 PostgreSQL 和 Milvus 即可接口逻辑不用大变。import hashlib import uuid from datetime import datetime, timezone from fastapi import FastAPI, Header, HTTPException app FastAPI() # 用 dict 模拟元数据库真实环境请替换为 PostgreSQL DOCS {} # doc_id - document record CHUNKS {} # chunk_id - chunk record VERSIONS {} # version_id - version record # 1. 摄入文档 def parse_document(raw_bytes: bytes, filename: str) - str: 解析文档为标准化 Markdown。 这里只做示意实际需要按文件类型调用 pdf/docx/html 解析器。 text raw_bytes.decode(utf-8, errorsignore) return f# {filename}\n\n{text} def split_chunks(markdown_text: str) - list[dict]: 按标题结构分块块之间保留少量重叠。 sections markdown_text.split(\n## ) chunks [] for idx, sec in enumerate(sections): content sec.strip() if len(content) 20: continue chunks.append({ chunk_id: fchunk_{uuid.uuid4().hex[:8]}, block_path: fsec{idx}, content: content[-1500:], # 限制单块最大长度 }) return chunks app.post(/v1/documents/ingest) def ingest( filename: str, content: bytes, source_uri: str , x_agent_id: str Header(...), ): doc_id fdoc_{uuid.uuid4().hex[:12]} markdown_text parse_document(content, filename) chunks split_chunks(markdown_text) checksum hashlib.sha256(markdown_text.encode()).hexdigest() DOCS[doc_id] { doc_id: doc_id, title: filename, source_uri: source_uri, current_version: 1, owner: x_agent_id, created_at: datetime.now(timezone.utc).isoformat(), } for c in chunks: CHUNKS[c[chunk_id]] { doc_id: doc_id, version: 1, block_path: c[block_path], content: c[content], } # 此处应调用向量库接口写入 embedding省略具体代码 # vector_store.upsert(embedding_idc[chunk_id], vectorembed(...), metadata{...}) VERSIONS[fver_{uuid.uuid4().hex[:8]}] { doc_id: doc_id, version: 1, checksum: checksum, status: published, created_by: x_agent_id, } return {doc_id: doc_id, version: 1, chunk_count: len(chunks)}# 2. 带权限过滤的检索 app.get(/v1/documents/query) def query( q: str, top_k: int 10, x_agent_id: str Header(...), x_agent_roles: str Header(defaultuser), ): roles x_agent_roles.split(,) # 真实实现走向量库的 metadata filter过滤掉无权访问的 doc/chunk # vector_store.query(q, top_k50, filter{access_tags: {$in: roles}}) results [] for cid, c in CHUNKS.items(): if q.lower() in c[content].lower(): results.append({ chunk_id: cid, doc_id: c[doc_id], block_path: c[block_path], content: c[content][:200], score: 0.8, # 示意分数真实场景来自向量相似度 BM25 rerank }) # 权限过滤只保留当前 Agent 有权限的文档 filtered [r for r in results if has_permission(r[doc_id], roles)] return {results: filtered[:top_k]}# 3. 带版本控制的补丁更新 app.post(/v1/documents/{doc_id}/patches) def patch_document(doc_id: str, base_version: int, patch_ops: list[dict], x_agent_id: str Header(...)): doc DOCS.get(doc_id) if not doc: raise HTTPException(status_code404, detaildoc not found) if doc[current_version] ! base_version: raise HTTPException( status_code409, detailfversion conflict: current is {doc[current_version]}, base is {base_version} ) # 读取当前内容执行结构化补丁 markdown_text load_markdown(doc_id, base_version) # 实际从对象存储加载 for op in patch_ops: if op[op] replace: markdown_text markdown_text.replace(op[old], op[new]) elif op[op] append: markdown_text op[content] new_version doc[current_version] 1 doc[current_version] new_version doc[updated_at] datetime.now(timezone.utc).isoformat() VERSIONS[fver_{uuid.uuid4().hex[:8]}] { doc_id: doc_id, version: new_version, status: pending if requires_review(doc_id) else published, created_by: x_agent_id, } # 重新分块并更新向量索引代码省略 return {doc_id: doc_id, version: new_version, status: pending}这段代码把三个核心动作串起来了摄入时统一解析分块检索时带权限过滤更新时检查版本冲突。说明一下这只是最小可用的示例生产级文档层还要补充事务、重试、异步任务、对象存储交互、向量库部署等但整体设计骨架就是这套。3.5 接 Agent 时需要避开的两个设计陷阱第一个陷阱是让 Agent 绕过文档层直接访问底层数据。很多人搭好了文档层但 Agent 代码里还留着直接读文件、直接查数据库的逻辑一旦某个 Agent 走了后门权限和审计就全失效了。解决办法是把文档层的访问凭证做成唯一可用的凭据底层存储的密钥不直接交给 Agent 运行时让 Agent 只能通过文档层 API 访问数据。第二个陷阱是把文档层做成了“大而全”的万能平台。文档层要克制不应该去实现业务逻辑。比如不要内置各种行业文档模板不要替 Agent 决定如何总结内容不要做文档比对的人业务。文档层只负责把文档的读、找、改、管做到位上层业务永远应该是 Agent 自己决定的。这样做的好处是文档层可以沉淀为通用基础设施而不是被某个具体业务绑架。4. 把 Agent 接到文档层之后效果与评估4.1 四组务实的评估指标文档层是否真的有效不能靠“感觉回答变准了”来判断需要落到指标上。我建议从四个角度评估而不是只盯回答准确率。第一组是检索质量指标。最实用的是 RecallK 和 MRR。做法是准备一批“黄金问答对”每个问题标注出正确答案所对应的原文片段位置然后看检索系统的前 K 个结果里有没有命中这些片段。文档层没做好之前我的经验是 Recall5 往往只有 60%~70%做好版面分析和混合检索后可以稳定到 85% 以上。第二组是引用准确率指标。这个指标直接反映 Agent 在回答时有没有忠实于文档内容。抽样一批 Agent 回答对每一条回答中的事实性引用去对原文判断“引用关系是否真实存在、是否被断章取义”。文档层提供完整来源元数据后这一步能半自动化执行。第三组是写操作安全性指标。统计 Agent 提交的补丁总数、执行成功的数量、失败数量、被审阅人拒绝的数量、发生版本回滚的数量。理想情况下由于冲突检测的存在冲突失败率会先升高后降到合理区间因为 Agent 学会了先查状态再提交。第四组是端到端任务成功率。这个最直接也最难自动化。比如让 Agent 完成“根据最近三份周报生成一份新的项目风险报告”看它是否能在限定时间内独立完成、产出文档是否满足格式和内容要求。文档层对这个指标的影响在于减少了 Agent 因找不到资料、读错版本、写坏文档而重试的次数。4.2 一个可复现的对比测试方法我建议用 AB 对照的方式来验证文档层价值否则很难排除模型版本、Prompt 改动等干扰因素。操作方式不复杂同一批任务固定同一个模型和同一套 Prompt只切换“是否经过文档层”这一个变量。对照组采用最朴素的方式让 Agent 直接读取原始文件手动解析后再回答。实验组接入文档层的检索和更新接口。两组各跑 50 到 100 个任务覆盖知识问答、信息提取、文档修订这几类场景然后记录上面四组指标。对比期间不要改其他任何变量包括 chunk 大小、模型温度等。这里我额外提醒一点AB 测试时要注意任务难度的分布。如果测试任务都是简单“一份 PDF 里找一句话”文档层的优势会不明显因为解析问题不显著。真正能体现文档层价值的是复杂任务比如跨多份文档推理、读取带表格和双栏排版的材料、需要基于旧版本修改文档并对比差异。测试样本里这些难任务要占到一半以上。4.3 我实测看到的数据变化在几个真实项目里接入文档层前后数据变化有明显规律。当然不同项目差异很大我只说我观察到的典型范围供你参考。检索质量方面Recall5 从平均 68% 提升到了 87%MRR 从 0.42 提升到 0.61。提升主要来自三部分版面分析解决双栏乱序、混合检索解决精确术语召回、Rerank 过滤掉高相似低相关的噪声片段。引用准确率方面从 72% 提升到了 91%。这并非模型变聪明了而是检索层把所有命中的片段都带上了原文摘录和来源路径模型在生成时有了明确的“抄写对象”不需要自己临场发挥。写操作安全性方面是最直接的提升。没有版本控制之前两个 Agent 同时编辑同一份周报的覆盖事故大概每周都会遇到一两次。加上 base_version 冲突检测后这类覆盖直接被拦截在了提交阶段Agent 会自行返回冲突并重新拉取最新版本再改。端到端任务成功率在我做的合同审查 Agent 场景里从 61% 提升到了 78%。剩下的失败主要来自解析器对复杂表格的处理还不够好这类问题需要单独优化不是文档层架构能完全解决的。5. 落地过程中踩过的坑与排查方案5.1 PDF 解析出来的文字顺序是乱的这是我踩得最深的坑。用 pdfplumber 和 pdfminer 这类库直接抽取文本时双栏 PDF 会被读成“左栏第一行、右栏第一行、左栏第二行、右栏第二行”这种混乱顺序模型把跨栏拼接的内容当成连贯段落理解完全跑偏。排查思路很简单先不要向量化把解析出来的文本人工读一遍截图对比原 PDF 版面。如果发现顺序乱就走版面分析方案。轻量级的做法是用 LayoutParser 或基于检测模型的版面分析组件把页面里每个文本块的位置和大小提取出来再按坐标排序重组阅读顺序。重一点的方案是用 PDF OCR 或者视觉语言模型直接理解页面结构。我现在的经验是纯文本 PDF 用版面分析重组顺序扫描件直接 OCR必要的时候逐页做图像预处理去噪、纠偏再走 OCR。不要指望一个解析库包打天下不同 PDF 源要配不同的解析策略。5.2 向量检索经常漏掉关键上下文检索漏召回很多时候不是 Embedding 模型不够强而是分块策略把关键信息切碎了。比如一个合同的“违约责任”条款跨越两页中间被页眉页脚打断按固定长度硬切成 500 字语义就被切断了。我的排查顺序是先看召回结果命中的块内容是否完整判断责任在分块还是召回。如果块内容本身就缺信息那就是解析和分块的问题调整分块策略改为结构感知分块按标题、段落、表格边界来切同时允许块与块之间保留约 150 字符的重叠。如果块内容完整但仍然召不回那才考虑换检索策略比如加 BM25 混合召回或 Rerank。5.3 Agent 并发写文档互相覆盖多人协作改同一篇文档时冲突是常态不是意外。最初我为了图省事提交补丁时不做版本校验结果两个 Agent 同时基于版本 1 修改后提交的覆盖了先提交的而且先提交的改动完全丢失。修复方案就是我在 3.4 节代码里展示的乐观锁机制每次补丁必须携带 base_version文档层校验当前版本是否匹配不匹配直接返回 409。Agent 端收到 409 后重新拉取最新版本把之前要做的修改合并到新版本上再提交。这一步把“静默覆盖”变成了“显式冲突”虽然多了重试成本但数据安全性和可审计性大幅提升。5.4 权限校验形同虚设文档层刚上线的时候我把权限校验只放在了文档级以为控制住“谁能读这份文档”就够了。结果有一次审计发现某个 Agent 通过检索 API 查到了文档里某个它无权访问的敏感片段原因是我忘了做块级权限过滤敏感内容所在的块被当作普通块返回了。从那以后我把权限过滤下沉到了两个位置检索时在向量库的 metadata filter 加 access_tags 条件返回结果前再在应用层做一次白名单校验。两层都过才返回给 Agent。权限过滤必须是默认行为不要设计成“可选开启”否则总有一天会漏配置。5.5 常见问题速查表我整理了一份高频问题速查表方便你在排查时对照现象可能原因排查方法解决方案PDF 内容乱序双栏版面被按单栏读人工阅读解析文本并对比原 PDF版面分析重组区块顺序表格数据缺失或错位表格解析只取文本不取结构查看解析后的 Markdown 表格是否完整使用专门的表格解析模型检索召回不到精确术语纯向量检索无法精确匹配用术语搜索测试 BM25 结果混合检索BM25 向量检索结果相关但事实不对分块导致上下文被切断检查命中块的内容边界结构感知分块 重叠两个 Agent 同时改文档互相覆盖缺少版本校验查看审计日志和版本变更记录base_version 乐观锁 409 冲突Agent 访问到无权文档权限只做文档级未做块级用低权限 Agent 测试越权检索双层权限过滤 下推 metadata filter文档版本混乱不知道哪份是最新元数据与对象存储分离不足查看 status 接口的版本列表统一用元数据库管理版本最后分享一点我的个人体会文档层这个概念听起来“多了一层”似乎增加了复杂度但我实际做完几个项目之后反而觉得它让整体架构变简单了。以前 Agent 的代码里到处散落着 PDF 解析、Markdown 清理、向量检索、权限判断的逻辑改一处就要动一片现在这些逻辑全部收敛到文档层后面Agent 只关心它的任务文档层只关心文档本身两边各自简单。如果你也要动手做文档层我建议从小范围切入不要一开始就想覆盖所有文档类型。选一个你最痛的高频场景比如“让 Agent 读合同并提取关键条款”把解析、检索、版本三条链路跑通再逐步扩展。文档层核心不是技术有多炫而是把读、找、改、管这四件事做得足够稳定让上面的 Agent 可以放心依赖它。踩过几次坑之后你会慢慢发现真正值得投入的地方往往不是模型选型而是这些看起来不起眼的基础设施。