某头部AI实验室把自家 Agent Skills 仓库开源之后,我第一时间拉下来研究。本来以为只是例行公事的开源动作,顶多放几个示例技能撑撑场面,结果打开仓库发现,光“文档技能”这一个目录,就把底层实现从头到脚展示得干干净净:指令文件怎么组织、解析脚本怎么调度、上下文怎么构建、输出结构怎么约定,甚至连评估样本和失败兜底策略都写成了公开材料。做 Agent 应用这几年,我还是第一次这么直观地看到一个生产级技能的真实长相。
这篇文章不打算做泛泛的新闻解读,我把自己花了两周时间拆解、复现、改造这套文档技能的完整过程记录下来。内容包括:为什么 Agent Skills 这个概念能区别于传统工具调用、文档技能内部到底分哪几层、把技能迁移到自己框架里的最小可行路径,以及我在复现过程中踩过的三个比较典型的坑。如果你正在做文档问答、RAG 管线或者自研 Agent 工具链,这篇文章应该能帮你省掉不少试错时间。
1. 从“一堆工具”到“一个技能”:这次开源的价值到底在哪
先说一个我自己的体会。过去做 Agent,大家习惯的方式是给模型注册一堆 function,也就是工具调用。每个工具就是一个函数定义,模型根据用户问题决定调用哪一个。这种方式对付“查天气”“算个税”这种单步任务没问题,但一旦面对“把这份 PDF 里所有表格提取成结构化数据”这种复合任务,立刻就失控了。
1.1 Tools 的失控困境:为什么单个函数定义不够用
文档处理从来不是单函数逻辑。一个完整的文档技能,背后至少涉及文件格式探测、PDF 文本提取、表格结构识别、OCR 兜底、乱码处理、引用位置标记、JSON 输出校验这么多个环节。如果每个环节都暴露成一个独立 function,模型光理解这些工具的适用条件和调用顺序就够喝一壶,更别提十几个函数的定义会吃掉多少上下文预算。
我拆解过一套传统方案,假设提取一份 PDF 里的财务表格,需要顺序调用 detect_file_type、extract_text、extract_tables、parse_layout、validate_output 五个函数。每个函数的 description 平均要写一百多字模型才能理解“什么时候该用它”。组合起来之后,模型在第一步就很容易选错——比如对扫描件直接调 extract_text,拿回来一堆乱码,然后整条链路就崩了。更麻烦的是,这些函数之间没有任何状态共享,模型没有“我正在处理一个文档”的全局意识。
1.2 Skills 的封装逻辑:把“工具+说明书+验收标准”打包成一个文档任务
Agent Skills 的思路和 Tools 完全不一样。它不再是给模型一堆散装工具,而是直接把“完成某类任务的能力”整体打包。一个技能包通常包含四样东西:一份自然语言写的指令文档(通常叫 skill.md)、一组可执行的解析脚本、几个示例输入输出、以及一套验收测试。模型接到任务时,先读指令文档,按文档里写的流程来决定何时调用哪些脚本,而不是自己临场猜。
类比一下。传统 Tools 是给修理工一抽屉扳手和螺丝刀,让他自己摸索怎么修;Agent Skills 则是递过去一个“水管维修包”,里面有工具、有作业指导书、有验收标准。模型要做的不是发明流程,而是按指导书执行,遇到指导书没写的情况才知道需要报告异常。这个设计让复杂任务从“模型临场发挥”变成了“模型按剧本调度”,生产环境里的稳定性提升非常明显。
我拿到仓库后第一时间翻了文档技能目录,看到核心的 skill.md 写得相当细致——开头是任务目标和适用范围,正文按处理阶段拆成五步,每一步都写清楚用什么脚本、生成什么中间文件、出现什么错误时怎么办。这种“把行为边界写清楚”的做法,比起在代码里堆 try-except 要高明得多,因为模型能读到的不是异常栈,而是“下一步该怎么办”的决策依据。
2. 文档技能要处理的真实世界:我在样本集里看到的几种硬骨头
官方仓库能这么受关注,除了概念包装到位,更重要的原因是文档技能本身的通用性。真做 Agent 的人都知道,文档类任务是最高频刚需,也是最容易翻车的地方。我把仓库里的测试样例翻了一遍,发现设计者几乎把所有让人头疼的文档类型都拉出来做了覆盖。
2.1 三种最让人头疼的文档类型
第一类是扫描版 PDF。很多合同、票据、历史档案都是扫描件,PDF 里根本不存在文本层。直接提取文本拿到的是空结果或者乱码,必须 OCR。OCR 又牵扯到语言模型、清晰度、倾斜矫正、表格线识别,任何一个环节出问题都会导致整段内容不可用。
第二类是排版复杂的多栏文档。学术论文、报纸、产品手册经常是双栏甚至三栏布局。简单按页提取文本会把“栏一的后半句”和“栏二的前半句”拼在一起,读起来像梦话。我见过不少 RAG 项目在这一步就埋下了错误种子,后面模型再聪明也无力回天。
第三类是表格结构复杂的文档。合并单元格、跨页表格、表格里嵌图片、多级表头……这些在 PDF 里只表现为线条和坐标,没有语义信息,想还原成结构化的行列关系非常难。官方文档技能在这些样本上提供的提取结果基本保住了行列对应关系,让我比较意外。
2.2 结构还原与引用保留:文档技能的底层设计目标
看完这些测试样例,我总结出文档技能的两个底层设计目标:结构还原和引用保留。
结构还原指的是,不管输入是 PDF、DOCX 还是扫描件,最终都要转成“带层级的可读文本”,而不是一团糊在一起的纯字符流。标题层级要对应到 Markdown 的 H1/H2/H3,表格要转成真正的 Markdown 表格,列表要保留缩进关系。只有这样的文本喂给模型,它才能准确理解“这段内容属于哪个章节”“这个数字对应表格里的哪一列”。
引用保留则更关键。文档技能不仅在输出里给出答案,还会同时给出这个答案出自哪一页、哪一段、表格里哪一行。这个设计对 RAG 和知识库场景是生死线,因为用户拿到结论之后总要问一句“这个数是从哪里看出来的”。没有引用位置的输出,在生产环境里基本不具备可信度。
传统做法里这两点经常被忽略。很多人以为文档解析就是把 PDF 转成 txt,结果模型读是能读了,但丢掉了全部结构信息,输出结果完全没有回溯能力。这次开源的可贵之处在于,把“先还原文档结构,再做语义抽取”这套方法论用完整实现固定了下来。
3. 仓库里的完整调用链:从 PDF 字节到结构化输出的四层实现
拆解任何一套系统,我习惯先把调用链捋清楚。文档技能的架构可以分成四层:入口层、解析层、语义层、输出层。每一层都有明确的职责和边界,下面逐个说。
3.1 入口层:类型探测与分支调度
入口层做两件事:文件类型探测和任务调度。用户丢进来一个文件路径,系统先通过扩展名和文件头判断它到底是 PDF、DOCX、XLSX、PPT 还是纯图片。这个判断不能依赖扩展名,因为实际操作中经常出现扩展名和真实格式不匹配的情况。
判定完格式之后,入口层会把任务分支调度到对应的处理链路上。比如 PDF 走 PDF 链路,DOCX 走 DOCX 链路,扫描图片走 OCR 链路。每个链路内部再细分,例如 PDF 链路会先判断是文本型还是扫描型,再决定要不要调用 OCR。这种分支调度做的不是“万能解析”,而是“让专业模块处理专业格式”,避免拿文本提取器去硬啃扫描件。
我把大致的处理流整理成了一个简化目录,方便你理解仓库组织:
skills/document_skill/ ├── skill.md # 核心指令文档,模型读取的操作手册 ├── scripts/ │ ├── detect_type.py # 格式探测 │ ├── parse_pdf.py # 文本型 PDF 提取 │ ├── parse_docx.py # Word 文档解析 │ ├── parse_xlsx.py # 表格文件解析 │ ├── ocr_fallback.py # 扫描件 OCR 兜底 │ └── build_context.py # 中间表示(IR)构建 ├── schemas/ │ └── output_schema.json ├── examples/ │ ├── sample_01.pdf │ └── sample_01.output.json └── tests/ ├── eval_cases.json └── run_eval.py3.2 解析层:文本提取、表格识别与 OCR 的配合
解析层是本技能最“重”的部分。文本型 PDF 调用原生文本提取,拿到的内容再经过版面分析模块做分栏重组;DOCX 则走 XML 解析,直接取段落和表格的原生结构;对于扫描件,先做图像预处理,再送 OCR 引擎。
表格识别是解析层的核心难点。简单的 PDF 表格可以通过坐标推断行列,但合并单元格和多层表头会让坐标推断失效。仓库里给的方案是:把表格区域先从页面中裁剪出来,用专门的结构识别模型重建行列关系,再映射回原文坐标体系。这一步结束后,表格可以无损转成带行列结构的 Markdown 表格。
这里有个细节值得注意:解析层输出的不是最终结果,而是中间表示(IR)。中间表示里保留了页面编号、段落编号、表格行列索引这些结构信息,后续所有语义处理都基于 IR 展开,而不是重新读原始文件。这个设计的好处是,模型和解析逻辑彻底解耦,换解析引擎不会影响上层逻辑。
3.3 语义层:用中间表示(IR)喂养模型,而不是喂原始文本
IR 构建完成之后,需要把 IR 组织成适合模型阅读的上下文文本。这一步很多人会忽略,直接 “把 IR 文本拼起来塞进去” 就算完事。仓库的做法更高明,它把 IR 改写成带语义锚点的 Markdown 文本,并在关键位置插入元信息注释。
举个例子。一份 PDF 的 IR 经过构建之后,不是简单的“第一段文字、第二段文字”,而是这样:
## 第三章 营收数据 <!-- PAGE: 12 --> 2024 年总营收为 1.2 亿元,同比增长 15%。 | 季度 | 营收(万元) | 环比 | |------|--------------|--------| | Q1 | 2500 | - | | Q2 | 3200 | +28% | <!-- TABLE: 3-1 -->模型看到的不只是文字,而是一个“有章节、有页码、有表格索引”的结构化文档。问答时如果需要引用出处,模型可以直接读取这些元信息来生成页码和章节位置。这种“給模型一张上下文地图”的做法,比丢一堆纯文本进去让模型自己摸索要可靠得多。
我写了一个简化版本的 IR 构建逻辑,大概长这样:
def build_context(parsed_doc: dict) -> str: lines = [] for block in parsed_doc["blocks"]: if block["type"] == "heading": lines.append(f"## {block['text']}") elif block["type"] == "paragraph": lines.append(f"<!-- PAGE: {block['page']} -->") lines.append(block["text"]) elif block["type"] == "table": lines.append(f"<!-- TABLE: {block['index']} -->") lines.append(block["markdown"]) return "\n\n".join(lines)3.4 输出层:JSON Schema、行内引用与可追溯性
最后一步是输出。仓库里给了一份明确的 JSON Schema,规定了文档技能输出的结构,包括提取的正文内容、表格数据、关键字段列表,以及每一项对应的引用位置。输出层还会对模型生成结果做一次校验,不符合 Schema 的就触发重试或标记异常。
值得借鉴的是它的“行内引用”设计。比如模型回答“Q2 营收 3200 万元”时,输出里不仅要给数字,还要附上引用标记引用表格 3-1 第 2 行。这个标记不是装饰,而是后续 RAG 系统做溯源和审计的关键凭证。没有这套验证机制,模型输出再准确都只是一段无法自证的话。
我在实际使用中的感受是:真正把输出层做好,比把解析层做精细更难。解析层面对的是确定性规则,输出层面对的是模型自由生成。没有严苛的 Schema 约束和失败重试机制,输出的不稳定性会直接影响下游任务。
4. 公开的最有价值资产:评估集、失败模式与降级策略
如果说代码是技能的血肉,那评估集和失败处理就是技能的骨架。这次开源最被我珍视的部分,恰恰不是那几个解析脚本,而是测试目录里那套不动声色却极有分量的评估体系。
4.1 评估集长什么样:样例、指标和通过线
评估集里是几百份覆盖了常见复杂场景的样例文档。每份样例都标记了一个或多个预期输出,系统会把模型实际输出和预期输出做对比,计算出结构保留率、字段提取准确率、引用命中率、整体失败率四个指标。发布者给了一条通过线——只有全部指标都超过阈值的技能版本,才算可发布状态。
我整理了一份官方样例评估结果和传统单工具方案的对照(数值做了一定脱敏),差异非常直观:
| 指标 | 传统 Tools 方案 | Skill 方案 |
|---|---|---|
| 文本段落提取准确率 | 78% | 96% |
| 表格结构保留率 | 41% | 93% |
| 引用位置命中率 | 未设计 | 95% |
| 扫描件处理成功率 | 23% | 87% |
表格结构保留率从 41% 涨到 93%,是我认为这套设计最值钱的地方。传统 Tools 方案里表格一直是最大的丢分项,模型拿到了文本却拿不到行列关系,自然没法输出结构化的表格数据。而 Skill 通过专门的解析层和 IR 设计,把表格问题降维成了一个可解决的问题。
4.2 失败不是意外而是预期:降级链路的工程哲学
我见过太多 Agent 项目把异常处理当成事后补丁,但这份仓库把失败模式直接设计进了技能定义。加密 PDF、损坏文档、空扫描页、乱码编码,每一种失败都有明确的返回码和错误描述。更关键的是,skill.md 里写明了模型在遇到这些错误时应该如何决策。
以扫描件为例。如果 OCR 第一次识别置信度过低,技能不会直接给结果,二是会触发第二步——调整图像分辨率后重试。重试仍失败,再降级成“返回可读片段并标记遗漏区域”。这样做的好处是:模型永远知道当前状态是“成功、可部分交付、还是彻底失败”,不会憋出一个错误的“成功结果”。
这套降级哲学对我很有启发。传统管线追求“尽量成功”,而生产级 Agent 应该追求“稳定失败”——失败信息明确、可预期、可处理。一个偶尔成功但失败原因随机的系统,比一个稳定失败但失败原因清楚的系统更难调试。
5. 把 Skills 迁移到自己的 Agent 框架:一份可以直接抄的实战路线
拆解到这一步,自然要把它搬到自己项目里试试。我花了一个周末,把仓库里的文档技能移植到了自己常用的本地 Agent 框架上,过程比预想顺利。核心路线就三步:加载 skill.md 做系统提示词、把脚本包装成工具集、强制模型按 Schema 输出。
5.1 最小复刻:加载 skill.md 当系统提示词,挂脚本当工具集
我的框架要手动维护 system prompt,第一件事就是把 skill.md 的核心段落整理成一段更通用的系统提示词。技能里的每一步,对应一个可调用的解析函数。这一步的关键在于:模型必须先读流程,再决定调哪个函数,而不是反过来。
我把顺序颠倒了测试过,结果非常惨。不读 skill.md 的时候,模型拿到 PDF 路径直接调用 parse_pdf 脚本,完全不理会文件是不是扫描件,导致大量错误。换成先读 skill.md 之后,模型会主动检测文件类型,再决定走文本提取还是 OCR 兜底。顺序的好处在于,skill.md 实际上充当了模型的“认知脚手架”,让它知道当前任务的全貌。
5.2 一个可运行的 Python 示例:本地 PDF 抽取
下面是我改造后的最小实现,读取一份 PDF 并输出结构化 JSON:
import json from pathlib import Path SKILL_MD = Path("skill.md").read_text(encoding="utf-8") def process_document(file_path: str): # 第一步:让模型基于 skill.md 形成整体认知 system_prompt = SKILL_MD + "\n\n你负责处理用户提交的文档,按流程完成解析和抽取。" # 第二步:格式探测(对应 scripts/detect_type.py) file_type = detect_type(file_path) # 第三步:按类型选择解析链路 if file_type == "pdf": if is_scanned(file_path): ir = ocr_fallback(file_path) # 扫描版走 OCR else: ir = parse_pdf(file_path) # 文本版直接提取 elif file_type == "docx": ir = parse_docx(file_path) elif file_type == "xlsx": ir = parse_xlsx(file_path) else: ir = {"error": "unsupported_type"} # 第四步:构建上下文并调用模型 context = build_context(ir) user_prompt = f"请提取以下文档中的关键信息:\n\n{context}" result = call_model( system_prompt=system_prompt, user_prompt=user_prompt, output_schema=OUTPUT_SCHEMA, # 强制按 Schema 输出 ) return result output = process_document("contract_2024.pdf") print(json.dumps(output, ensure_ascii=False, indent=2))这段代码能跑通的核心前提是,每个脚本函数都必须返回结构统一的 IR。我在自己项目里用最朴素的方式保证这一点:解析函数统一返回一个 dict,里面必须有 blocks 和 meta 两个键。模块之间通过 dict 通信,谁也不依赖对方的内部实现,后续换掉任何一段底层库都不会传染到其他层。
5.3 做好这三件事,你就能自己写新技能
迁移完官方技能之后,我开始琢磨怎么定义自己的新技能。参考官方仓库的设计,我总结出三个要点。第一,指令文档必须写清楚“什么时候不该用这个技能”,这决定了模型在任务边界上的判断。第二,要把常见失败模式写进技能定义,而不是留在代码注释里。第三,每个技能都必须配一组验收样例,没有评估集就谈不上迭代。
我现在已经按这套模板,在自己的 Agent 框架里新增了“合同审批摘要”和“周报数据汇总”两个自定义技能。新技能的开发效率比我想象中高不少,因为模板把大部分架构问题解决了,我只需要关注特定领域的解析规则和输出 Schema。
6. 踩坑实录:我在复现文档技能时遇到的三类问题
这部分是实际的调试记录。我在复现过程中遇到的问题,主要集中在 OCR、表格解析和上下文预算三个方向。
6.1 OCR 语言模型未指定导致中文文档乱码
第一次跑扫描件测试,我对着一份中文合同执行 OCR,结果输出是一堆拉丁字符和“口口口”占位符。一开始以为是 OCR 引擎的问题,翻了半天日志才发现是语言模型没指定——默认引擎只加载了英文识别包。换用中文识别包之后,整体准确率立刻恢复正常。这个坑在官方文档里其实写了,只是我当时图快没注意,白白浪费了半天。
如果你用的是常见开源 OCR 方案,建议在处理前先显式指定语言参数,并且每次输出后检查置信度阈值。遇到低置信度结果,宁可返回“识别失败”也不要硬翻译。
6.2 表格提取时合并单元格被拆散
另一个问题是复杂表格。我拿一份多级表头的财务表测试,提取出来的 Markdown 表格行数和列数对不上,合并单元格被拆成了空行。后来发现是因为我没有正确回传表格坐标信息,导致表格结构重建模块拿到的区域范围不完整。
官方仓库的做法是先从版面分析模块拿表格所在区域的矩形坐标,然后在这个矩形内部做结构识别。我最初直接让识别引擎在整个页面范围找表,误判率自然高。加上坐标裁剪之后,合并单元格的关系基本都能正确还原。
6.3 长文档上下文预算爆炸的缓解办法
最后一类问题出在长文档上。一份 60 页的说明书转成 IR 之后,文本量远超模型上下文上限。我一开始抱着“片段总结再回填”的朴素思路做了两次,结果模型在长距离依赖上频繁漏信息。
后来参考仓库里的设计,改成两段式处理:先按章节拆分 IR,每一章做独立抽取;再把各章结果汇总成总结。这样虽然调用次数增加了,但每段上下文里都保留了章节和页码元信息,汇总时的引用依然能精确到具体位置。官方仓库里其实也是这么做的,只是我一开始没仔细读流程描述。
折腾完这一圈,我对这套技能设计的评价从“有点想法”变成了“确实合理”。它最值钱的地方,不是那几个解析脚本,而是把文档任务的边界、流程、失败处理全部标准化了。这套方法论其实可以平移到任何复杂任务场景——不只是文档,邮件处理、报表生成、代码仓库分析都可以用同样的技能包装思路来落地。
最后分享一个自己的小习惯。我拆解这类开源仓库的时候,会先把评估集和失败分支的测试样例打印出来逐个过一遍,而不是先跑 demo。demo 只能证明它做了什么,故障样本才能暴露它拒绝做什么。这正是这次开源里最值得抄的作业:把验收标准亮出来,比把代码亮出来更有说服力。