Vibe-Trading 通用文档阅读器 read_document 技能全解析:多格式文本提取、OCR 引擎切换与安全边界
Vibe-Trading 的 doc-reader 技能为 Agent 提供了一套统一的文档读取入口:无论面对 PDF、Word、Excel、PowerPoint、图片(OCR)、CSV/TSV、配置文件还是源代码文件,都只需调用同一个 read_document 工具,返回结构一致的 JSON 信封。本文基于 doc-reader 技能文档 展开,并结合 doc_reader_tool.py 的完整实现,深入讲解每种格式的底层提取逻辑、OCR 引擎的本地/云端切换机制、质量元数据字段以及路径安全校验,帮助你理解并安全地使用这一能力。
技能定位与核心设计
doc-reader 是一个 category: tool 类型的 Agent 技能,其核心设计理念是「按扩展名分派、统一返回信封」:调用方永远只关心一个工具和一个返回结构,具体格式差异由内部 handler 消化。
技能文档明确要求:始终直接调用 read_document 工具,不要从 bash 里手动运行 Python 脚本(见 SKILL.md 的 Usage 一节)。这是为了让 Agent 的每一次读取都经过路径校验、安全扫描、统一截断与进度上报,而不是绕过工具管线直读文件系统。
该工具的入口定义在 doc_reader_tool.py 的 read_document(file_path, pages="", min_text_per_page=50) 函数中,并通过 DocReaderTool(doc_reader_tool.py)注册为名为 read_document 的可重复调用工具(repeatable = True)。
支持的格式矩阵
技能文档给出了一张完整的格式支持表,实际源码中的分派逻辑完全一致(见 doc_reader_tool.py):
| 类别 | 扩展名 | 提取方式与说明 |
|---|---|---|
.pdf |
文本页毫秒级提取;扫描/图片页自动降级 OCR(默认阈值 50 字符/页) | |
| Word | .docx |
段落文本 + 表格单元格,单元格以 | 拼接 |
| Excel | .xlsx、.xls |
全部 sheet,每 sheet 默认只读前 100 行作为预览 |
| PowerPoint | .pptx |
逐页提取幻灯片文本框内容 |
| 图片 | .png/.jpg/.jpeg/.gif/.bmp/.webp/.tiff |
仅走 OCR 管线(无 OCR 引擎时返回空文本并附提示) |
| CSV / TSV | .csv、.tsv |
按原始文本读取,带编码回退 |
| 纯文本 | .txt/.md/.log/.rst |
编码回退读取 |
| 配置类 | .json/.yaml/.yml/.toml/.ini/.cfg/.env |
原始文本读取(不解析、不美化) |
| 标记语言 | .html/.htm/.xml |
原始文本,不做 HTML 剥离 |
| 源代码 | .py/.js/.ts/.tsx/.go/.rs/.java/.cpp/.c/.sql/.sh/... |
原样返回,禁止重新格式化或缩进 |
| 未知扩展名 | 其他任意后缀 | 尽力按 UTF-8/GBK 文本读取 |
从源码看,_TEXT_EXTS 集合(doc_reader_tool.py)还额外覆盖了 .pyi/.jsx/.kt/.swift/.h/.hpp/.cc/.rb/.php/.pl/.lua/.bash/.zsh/.ps1/.bat/.r/.m/.dockerfile/.makefile/.cmake/.conf 等扩展名;对于完全未知的扩展名,分派逻辑同样落入文本读取路径(doc_reader_tool.py),这是一种"尽力而为"的设计。
上传阶段的阻断清单
技能文档强调,以下类型在 /upload 阶段即被拒绝(Blocked):
- 可执行文件:
.exe/.dll/.so/... - 压缩归档:
.zip/.tar/...
遇到归档文件时应提示用户先在本地解压再上传,而不是尝试直接解析二进制内容。
统一的 JSON 返回信封
所有格式共用同一个返回结构。技能文档给出了如下示例:
{
"status": "ok",
"file": "paper.pdf",
"format": "pdf",
"char_count": 52000,
"truncated": true,
"text": "..."
}
该信封由 _envelope() 函数构建(doc_reader_tool.py),其中 char_count 记录的是截断前的完整字符数,truncated 标记是否被截断,text 是实际返回的内容。当文本超过 15000 字符时,会以 ... (truncated, total N chars) 的形式裁剪(doc_reader_tool.py,_MAX_CHARS = 15000)。测试 test_doc_reader.py 专门验证了信封键的完备性与 20000 字符文件的截断行为。
各格式的附加元数据字段
技能文档中的字段表,对应源码里的 **extra 参数(doc_reader_tool.py):
| 格式 | 附加键 |
|---|---|
pdf |
total_pages、pages_read、ocr_pages、ocr_engine、ocr_quality、skipped_pages |
docx |
paragraphs、tables |
excel |
sheets({name, rows, cols} 数组) |
pptx |
slides |
text |
encoding、size |
例如读取 Excel 时,sheets 会返回每个 sheet 的名称、总行数、总列数(doc_reader_tool.py),同时 text 中附带每 sheet 的预览块:--- Sheet: {name} ({rows} rows × {cols} cols) ---。
使用方法与参数详解
基本调用
技能文档给出的标准调用示例:
read_document(file_path="uploads/paper.pdf")
read_document(file_path="uploads/annual_report.pdf", pages="1-10")
read_document(file_path="uploads/contract.docx")
read_document(file_path="uploads/sales.xlsx")
read_document(file_path="uploads/deck.pptx")
read_document(file_path="uploads/chart.png") # image → OCR
read_document(file_path="uploads/config.yaml")
read_document(file_path="uploads/notes.md")
pages 参数(仅 PDF 生效)
pages 支持 "1-10"、"5"、"1,3,5-8" 三种写法,其他格式会忽略该参数。底层解析器 _parse_pages(doc_reader_tool.py)有几个值得注意的实现细节:
- 容错 Unicode 破折号:会把 en-dash(
–U+2013)、em-dash(—U+2014)、数学减号(−U+2212)统一替换为 ASCII 连字符,兼容 Word/LLM 粘贴产生的特殊字符。对应测试见 test_doc_reader_unicode_page_dashes.py。 - 拒绝反向范围:
"5-1"这类起始大于结束的范围会抛出inverted page range错误,与 alpha_bench 工具的_parse_period行为保持一致。对应测试见 test_doc_reader_inverted_pages.py。 - 自动越界裁剪:页码会被裁剪到
[1, total_pages]区间内。 - 去重排序:最终返回按升序去重后的零基索引列表。
min_text_per_page 参数(PDF OCR 触发阈值)
该参数控制「文本不足才触发 OCR」的临界值,默认 50 字符:
read_document("scanned_report.pdf", min_text_per_page=10) # 更激进:更早触发 OCR
read_document("mixed_pdf.pdf", min_text_per_page=100) # 更保守:尽量用原生文本
在源码中,每页提取文本后若 len(text) >= min_text_per_page 则直接采用原生文本,否则进入 OCR 分支([doc_reader_tool.py](https://gitcode.com/GitHub_Trending/vi/Vibe-Trading/blob/5f85c6d515aa5219e941e08d9df176387fbebd66/agent/src/tools/doc_reader_tool.py?utm_source=gitcode_repo_files#L149-L190)。OCR 分支下,页面会以 300/72 DPI(约 4 倍缩放的位图)渲染后交给 OCR 引擎。读取进度通过 emit_progress("reading_pdf", ...) 逐页上报,便于前端展示长文档的读取进度。
各格式底层处理逻辑
PDF:pypdfium2 + OCR 降级
PDF 提取依赖 pypdfium2(doc_reader_tool.py),未安装时返回明确的错误提示。整体流程为:
- 打开文档、解析页码范围;
- 逐页调用
page.get_textpage().get_text_range()提取原生文本; - 文本量达标 → 直接输出(标记
--- Page N ---); - 文本量不足 → 渲染位图交给 OCR;OCR 无可用引擎 → 该页计入
skipped_pages; - 全部页都是扫描页且无 OCR 引擎时,返回错误并附带安装提示(来自
get_ocr_install_hint)。
DOCX:段落 + 表格
_read_docx(doc_reader_tool.py)用 python-docx 依次输出非空段落,表格部分以 --- Table N --- 开头,每行单元格用 | 拼接、单元格内换行替换为空格。返回中带 paragraphs 与 tables 计数。
Excel:全部 sheet 预览
_read_excel(doc_reader_tool.py)通过 pandas 的 ExcelFile 遍历全部 sheet,统一以字符串类型(dtype=str)解析,每 sheet 取 head(100) 预览,避免解析精度丢失与预算超支。若用户需要完整数据(如交易日志),技能文档建议改用 analyze_trade_journal 工具(见 trade-journal 技能),而不是直接读取整个工作簿。
PPTX:逐页幻灯片文本
_read_pptx(doc_reader_tool.py)遍历每页幻灯片的 shape,仅提取含文本框(has_text_frame)的内容,逐行输出。
图片:纯 OCR 管线
_read_image(doc_reader_tool.py)将图片转为 RGB numpy 数组后交给 OCR 引擎。无可用引擎时返回错误并附安装提示;OCR 返回空文本时,text 为空、note 字段提示"OCR returned no text (empty or unreadable image)"。技能文档中的工作流强调:若 OCR 返回空,应如实告知用户,不要编造内容(SKILL.md 的 Chart / screenshot / scanned PDF 工作流)。
文本类文件:编码回退链
_read_text(doc_reader_tool.py)按如下顺序尝试解码:utf-8 → utf-8-sig → gbk → gb2312 → big5 → latin-1。两个实现细节值得注意:
- UTF-16 BOM 优先:检测到
\xff\xfe/\xfe\xff时先尝试 utf-16 解码(doc_reader_tool.py)。原因是 latin-1 永不抛异常,若把 UTF-16 留到最后会被静默解成带 NUL 字节的乱码(Notepad「Unicode」与 Excel「Unicode 文本」导出的正是 UTF-16)。测试 test_doc_reader.py 验证了hello 动量的 UTF-16 文件能正确解码且不含\x00。 - GBK 中文场景:A 股券商导出的文本常为 GBK 编码,回退链保证了这类文件可读。测试 test_doc_reader.py 验证了 GBK 编码的中文内容能正确解码并报告
encoding: gbk。
所有解码结果都带 encoding 与 size(原始字节数)元数据。
OCR 引擎架构:本地与云端的可插拔切换
OCR 是 doc-reader 最具可配置性的部分。技能文档指出内置两套引擎,不需要除引擎 SDK 之外的额外包:
| 引擎 | 类型 | 依赖 | 安装方式 |
|---|---|---|---|
rapid |
本地(离线) | rapidocr_onnxruntime |
pip install rapidocr_onnxruntime |
llm-vision |
云端 | 视觉能力 LLM 模型 + API key | 无需额外安装,复用现有 LLM provider 配置 |
引擎选择机制
引擎选择由环境变量 VIBE_TRADING_OCR_ENGINE 控制(env_schema.py,默认 auto):
auto(默认):只使用本地引擎,永不使用云端。这是隐私红线——文档页面绝不离开本机。源码中_select_first_local(engine.py)会按名称排序实例化所有已注册引擎,跳过is_cloud=True的引擎,返回第一个is_available()的本地引擎;rapid:强制 RapidOCR(本地 ONNX);llm-vision:强制 LLM 视觉 OCR(云端,页面会发送到所配置的 LLM provider);none:彻底禁用 OCR。
引擎实例化被设计为廉价操作(仅做属性赋值),耗时的初始化(如加载 ONNX 模型、建立 OpenAI 客户端)都推迟到 is_available() 或 recognize() 里惰性执行,以保证 auto 模式的探测开销可控(engine.py 注释说明了这一约定)。
可插拔插件机制
OCR 引擎不止内置两款。engine.py 支持通过 pip 的 entry-points(分组名 vibe_trading.ocr_engines)发现第三方引擎(engine.py),插件与内置引擎重名时插件优先。所有引擎只需实现 OcrEngine 协议(engine.py):提供 name、is_cloud、install_hint 属性以及 is_available()、recognize(numpy_rgb)、可选 confidence() 方法。
RapidOCR:本地离线引擎
rapid_ocr.py 中的 RapidOcrEngine 基于 ONNX Runtime,离线可用、无需 API key。recognize 直接调用 RapidOCR()(image),拼接检测框的文本行;无结果时返回空字符串。它被注册为 name="rapid"、is_cloud=False。
llm-vision:任意 OpenAI 兼容视觉模型
llm-vision 引擎(llm_vision_ocr.py)能与任意 OpenAI 兼容的视觉模型配合(GPT-4o、Qwen-VL、Gemini、Claude、GLM-4V 等),且无需单独维护 provider 映射——它直接复用现有的 LANGCHAIN_PROVIDER / LANGCHAIN_MODEL_NAME / API key 配置,通过 provider_env_names()(覆盖 18+ provider 的动态环境变量解析)确定 base_url 与 api_key。
几个重要的实现参数:
- 模型可单独覆盖,不改变 Agent 主模型:
VIBE_TRADING_OCR_LLM_MODEL=qwen3.7-plus(对应配置项见 env_schema.py); - 系统提示词对提取保真度做了细致约束(llm_vision_ocr.py):表格优先 Markdown 管道表格、合并单元格用 HTML
<table>;数学公式用\(...\)/\[...\]LaTeX(避免$与货币冲突);不可读内容标记为[illegible]、部分可读用[?];禁止幻觉、禁止翻译、禁止修正原文错别字、禁止输出注释; - 请求参数:超时 90 秒、
max_tokens=8192、temperature=0.0(提取任务要求最大确定性)、图片以 JPEG quality=85 压缩后 base64 传输(比 PNG 小 5~10 倍); - 重试策略:仅对瞬时错误(
APITimeoutError/APIConnectionError/5xx)重试一次,4xx 客户端错误(401/403/404/422)立即抛出——「真实的 API 报错比启发式猜测是更清晰的反馈」(llm_vision_ocr.py)。
技能文档特别说明:如果你显式设置 VIBE_TRADING_OCR_ENGINE=llm-vision,模型选择会被信任,视觉能力的校验不做拦截。另外,旧环境变量 VIBE_TRADING_OCR_QWEN_MODEL 已向后兼容地映射到 VIBE_TRADING_OCR_LLM_MODEL(env_schema.py),而旧的 qwen-vl 引擎名则被别名映射到 llm-vision(engine.py,会输出弃用警告)。
PDF 响应中的 OCR 元数据
技能文档列的字段,源码中由 ocr_quality 对象承载(doc_reader_tool.py):
ocr_engine:实际使用的引擎名(如"rapid"、"llm-vision")或null;ocr_pages:经过 OCR 处理的页数;skipped_pages:因无可用 OCR 引擎而跳过的页数;ocr_quality:包含quality_flag(取值good/degraded/no_ocr_engine/no_ocr_needed)、ocr_pages和text_density(每页字符密度)。
quality_flag 的判定逻辑(doc_reader_tool.py):无 OCR 页且无跳过 → no_ocr_needed;OCR 尝试过但返回空 → degraded;有跳过页 → no_ocr_engine 或 degraded;OCR 成功且无跳过 → good。图像文件同样返回 ocr_quality(单页场景下 text_density 即总字符数)。
安全边界:路径校验与文件类型限制
doc-reader 的路径安全由 path_utils.py 的 safe_document_path 保障:所有文件路径必须解析后落在允许的文档导入根目录内,支持 ~ 展开,拒绝 UNC 共享路径,否则抛出 outside allowed document roots 错误并提示通过 VIBE_TRADING_ALLOWED_FILE_ROOTS 环境变量添加导入目录。安全回归测试 test_doc_reader_security.py 验证了三件事:
- 读取
/etc/passwd这类系统路径会被拒绝(返回status: error); - 未配置根目录时,临时目录外的文档不可读;
- 配置
VIBE_TRADING_ALLOWED_FILE_ROOTS后,该目录下的文档正常读取。
此外,_err() 返回的错误信封统一为 {"status": "error", "error": "..."} 结构;_envelope 在组装完成后还会经过 with_security_warnings 对 text 字段做告警扫描(doc_reader_tool.py),与安全扫描器(scanner.py)联动。文件不存在与目录路径也分别返回明确的错误信息(doc_reader_tool.py)。
实战工作流
技能文档给出了四类典型工作流,均以 read_document 为唯一入口:
论文 / 报告摘要
1. read_document(file_path="paper.pdf") → full text
2. Extract abstract, methodology, conclusion → summarize
长 PDF 建议配合 pages 分片读取,避免 15000 字符截断丢失结论部分。
合同审查
1. read_document(file_path="contract.docx") → paragraphs + tables
2. Flag key clauses (termination, liability, payment, IP)
DOCX 返回中的 paragraphs / tables 元数据可帮助判断合同结构化程度,表格以管道符拼接便于 Agent 快速定位条款。
表格快速预览
1. read_document(file_path="sales.xlsx") → all sheet previews
2. If user wants trade journal analysis specifically, pivot to
`analyze_trade_journal` tool instead (see trade-journal skill).
Excel 的 100 行预览用于判断数据规模与结构;涉及交易日志的深度分析应切换到专用工具。
图表 / 截图 / 扫描 PDF
1. read_document(file_path="scan.png") → OCR text
2. If OCR returns empty, tell the user; don't fabricate.
扫描件依赖 OCR 引擎可用性:本地可 pip install rapidocr_onnxruntime,云端则设置 VIBE_TRADING_OCR_ENGINE=llm-vision 并配合视觉模型。ocr_quality.quality_flag == "no_ocr_engine" 是安装 OCR 引擎的明确信号(strategy-dev-manager 技能文档同样引用该字段作为引导依据)。
测试与验证依据
doc-reader 在 tests 目录 下有完整的测试矩阵,可作为行为契约:
- test_doc_reader.py:覆盖文本变体(
.txt/.md/.log/.rst)、配置类(.json/.yaml/.yml/.toml/.ini/.env)、源代码(.py/.js/.ts/.go/.rs/.sql/.sh)、GBK 回退、UTF-16 BOM、未知扩展名、CSV、DOCX 段落表格、Excel 多 sheet、PPTX、缺失文件/目录拒绝、信封形状与截断; - test_doc_reader_security.py:系统路径拒绝与允许根目录配置;
- test_doc_reader_inverted_pages.py:反向页码范围拒绝;
- test_doc_reader_unicode_page_dashes.py:Unicode 破折号容错;
- test_ocr_integration.py:OCR 管线集成行为。
小结
doc-reader 技能把 Vibe-Trading 中「文档 → 文本」这件事收敛成一个工具、一套信封、一个引擎开关:格式分派逻辑集中在 doc_reader_tool.py,OCR 的可插拔架构位于 ocr 包,隐私红线由 auto 模式默认守卫(本地优先、云端显式选择),路径安全由 VIBE_TRADING_ALLOWED_FILE_ROOTS 根目录约束兜底。对于 Agent 而言,理解 pages 分片、min_text_per_page 阈值、ocr_quality.quality_flag 语义以及编码回退链,就能在研报阅读、合同审查、行情数据表预览与扫描件转录等场景中精准、安全地完成文本提取。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00